Trợ lý OnDuty trả lời từ hồ sơ tiệm và chữ trên trang khách đang xem. Hai nguồn đó không biết hôm nay còn mấy đôi size 39, giá vừa đổi sáng nay, hay đơn hàng của khách tới đâu.
Connector là chỗ để website của tiệm tự trả lời những câu đó. Tiệm dựng một địa chỉ, OnDuty gọi vào mỗi khi khách hỏi, và câu trả lời của trợ lý có dữ liệu thật.
Dành cho gói Premium. Bật trong trang quản trị của tiệm, tab Trợ lý.
Người làm web của tiệm, không phải chủ tiệm. Cần khoảng một buổi cho một người biết PHP hoặc Node. Chủ tiệm chỉ việc dán địa chỉ và mã bí mật vào trang quản trị.
Một địa chỉ HTTPS duy nhất, nhận POST, trả JSON. Không có phiên, không có SSE, không có thư viện phải cài.
POST https://web-cua-ban.vn/onduty-connector
content-type: application/json
x-onduty-tenant: tiembanhmai
x-onduty-signature: sha256=<hmac_sha256(body, mã bí mật)>
{
"v": 1,
"type": "query",
"tenant": "tiembanhmai",
"question": "còn size 39 không?",
"locale": "vi",
"requestId": "b1f0...",
"ts": 1757800000000
}
Trả về:
{
"facts": [
{ "title": "Giày A size 39", "body": "còn 4 đôi, 450.000đ", "url": "https://web-cua-ban.vn/giay-a" }
],
"ttl": 60
}
Vậy thôi. facts là danh sách sự thật ngắn, trợ lý trích lại cho khách.
Kiểm chữ ký trước khi làm gì khác. Chữ ký là HMAC-SHA256 của đúng chuỗi body thô, khoá là mã bí mật
trong trang quản trị. So sánh bằng hàm chống đo thời gian (hash_equals trong PHP,
crypto.timingSafeEqual trong Node).
Trả nhanh hoặc đừng trả. OnDuty chờ tối đa 2 giây. Quá hạn thì trợ lý trả lời bằng những gì nó đã biết, khách không phải chờ. Endpoint hỏng hai lần liên tiếp thì OnDuty nghỉ gọi một phút.
Tối đa 8 mục, mỗi mục 600 ký tự, cả phản hồi 16 KB. Dài hơn bị cắt.
Chỉ trả sự thật, đừng trả mệnh lệnh. Nội dung bạn gửi được đưa vào ngữ cảnh của trợ lý như dữ liệu, và OnDuty lọc bỏ những câu cố ra lệnh cho model ("bỏ qua chỉ dẫn trước", "you are now..."). Đừng nhét chỉ dẫn vào đây, nó sẽ bị bỏ.
Câu hỏi đã được che. Số điện thoại và email của khách bị thay bằng [số điện thoại] và [email] trước khi
rời OnDuty. Đừng thiết kế cái gì phụ thuộc vào việc nhận được số thật.
ttl (giây, tuỳ chọn) nói OnDuty được dùng lại câu trả lời này trong bao lâu cho câu hỏi giống hệt.
GET cùng địa chỉ, không cần chữ ký, trả về:
{ "name": "Tiệm Bánh Mai", "v": 1, "capabilities": ["query"] }
Trang quản trị dùng cái này để báo "kết nối chạy" trước khi có câu hỏi nào. Nút Kiểm tra kết nối gọi cả
GET lẫn POST, nên nếu GET chạy mà POST hỏng thì gần như chắc chắn là sai chữ ký.
OnDuty có sẵn một connector mẫu. Dán địa chỉ này vào trang quản trị và bấm Kiểm tra kết nối:
https://onduty.goecomaz.com/api/demo-connector
Nó trả về dữ liệu giả (ghi rõ là mẫu), để bạn thấy đường đi thông suốt trước khi đụng vào theme. Endpoint mẫu này không kiểm chữ ký, vì nó không thể biết mã bí mật của tiệm bạn. Bản thật thì phải kiểm, dòng đầu tiên trong mã mẫu dưới đây làm đúng việc đó.
Dán vào một file, ví dụ onduty-connector.php, để ở gốc web.
<?php
$SECRET = 'odc_...'; // lấy trong trang quản trị OnDuty, tab Trợ lý
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
header('Content-Type: application/json');
echo json_encode(['name' => 'Tiệm Bánh Mai', 'v' => 1, 'capabilities' => ['query']]);
exit;
}
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_ONDUTY_SIGNATURE'] ?? '';
$mine = 'sha256=' . hash_hmac('sha256', $raw, $SECRET);
if (!hash_equals($mine, $sig)) { http_response_code(401); exit; }
$req = json_decode($raw, true);
$q = mb_strtolower($req['question'] ?? '');
$facts = [];
// Thay đoạn này bằng truy vấn thật vào cơ sở dữ liệu của bạn.
if (mb_strpos($q, 'size') !== false || mb_strpos($q, 'còn hàng') !== false) {
// $rows = $db->query("SELECT ten, ton, gia FROM san_pham WHERE ton > 0 LIMIT 5");
$facts[] = ['title' => 'Giày A size 39', 'body' => 'còn 4 đôi, 450.000đ'];
}
header('Content-Type: application/json');
echo json_encode(['facts' => $facts, 'ttl' => 60], JSON_UNESCAPED_UNICODE);
Cùng logic, đăng ký thành một route REST trong functions.php của theme con:
add_action('rest_api_init', function () {
register_rest_route('onduty/v1', '/connector', [
'methods' => ['GET', 'POST'],
'permission_callback' => '__return_true',
'callback' => function (WP_REST_Request $r) {
$secret = 'odc_...';
if ($r->get_method() === 'GET') {
return ['name' => get_bloginfo('name'), 'v' => 1, 'capabilities' => ['query']];
}
$raw = $r->get_body();
$mine = 'sha256=' . hash_hmac('sha256', $raw, $secret);
if (!hash_equals($mine, $r->get_header('x-onduty-signature') ?: '')) {
return new WP_Error('bad_sig', 'sai chữ ký', ['status' => 401]);
}
$q = mb_strtolower(json_decode($raw, true)['question'] ?? '');
$facts = [];
// Ví dụ WooCommerce: tìm sản phẩm theo từ khoá rồi trả tồn kho thật.
foreach (wc_get_products(['s' => $q, 'limit' => 3]) as $p) {
$facts[] = [
'title' => $p->get_name(),
'body' => ($p->is_in_stock() ? 'còn hàng' : 'hết hàng') . ', ' . strip_tags(wc_price($p->get_price())),
'url' => get_permalink($p->get_id()),
];
}
return ['facts' => $facts, 'ttl' => 60];
},
]);
});
Địa chỉ khi đó là https://web-cua-ban.vn/wp-json/onduty/v1/connector.
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.ONDUTY_CONNECTOR_SECRET!;
export async function GET() {
return Response.json({ name: "Tiệm Bánh Mai", v: 1, capabilities: ["query"] });
}
export async function POST(req: Request) {
const raw = await req.text();
const mine = "sha256=" + createHmac("sha256", SECRET).update(raw).digest("hex");
const sent = req.headers.get("x-onduty-signature") ?? "";
if (mine.length !== sent.length || !timingSafeEqual(Buffer.from(mine), Buffer.from(sent))) {
return new Response("sai chữ ký", { status: 401 });
}
const { question } = JSON.parse(raw) as { question: string };
const facts = await factsFor(question); // truy vấn thật của bạn
return Response.json({ facts, ttl: 60 });
}
MCP (Model Context Protocol) là JSON-RPC có phiên, thường chạy trên SSE. Người sẽ dựng connector cho một tiệm ở Việt Nam là bạn làm web viết PHP trong theme Haravan hay WordPress, và một máy chủ streaming có trạng thái không phải thứ họ dựng được trong một buổi.
Nên v1 giữ từ vựng của MCP (manifest khai năng lực, một lời gọi trả về tài nguyên, dữ liệu tách khỏi mệnh lệnh) nhưng hình dạng chỉ là một POST có ký. Khi nào cần nói chuyện với một MCP server thật, OnDuty làm một lớp chuyển đổi đứng trước chính hợp đồng này, không phải viết lại.
Trường type là phong bì, không phải danh sách đóng. v1 chỉ có "query". Những việc có giá trị hơn (giữ chỗ,
tra đơn hàng, đổi lịch hẹn) sẽ là type mới trên cùng đường ống này.
Kiểm tra hiện đúng những gì máy chủ trả về, kể cả lỗi, để không phải mò.
Trợ lý không bao giờ hỏng vì connector hỏng. Gọi không được thì nó trả lời bằng hồ sơ tiệm như thường, và trong trang quản trị bạn thấy lần kiểm tra gần nhất sai ở đâu. Cần gỡ hẳn thì bỏ tick Bật kết nối.