OnDuty

Kết nối dữ liệu sống (OnDuty Connector v1)

Cập nhật 2026-09-09

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ý.

Ai làm phần này

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ị.

Hình dạng

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.

Luật của v1

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][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.

Manifest

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ý.

Thử trước khi viết dòng nào

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 đó.

Mã mẫu PHP

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);

WordPress

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.

Next.js

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 });
}

Vì sao không phải MCP đúng nghĩa

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.

Bật lên

  1. Chủ tiệm vào trang quản trị, tab Trợ lý, mục Nối dữ liệu sống.
  2. Bấm Tạo mã bí mật, chép mã, đưa cho người làm web.
  3. Người làm web dựng endpoint, dán mã vào máy chủ.
  4. Chủ tiệm điền địa chỉ, tick Bật kết nối, bấm Lưu, rồi bấm Kiểm tra kết nối.

Kiểm tra hiện đúng những gì máy chủ trả về, kể cả lỗi, để không phải mò.

Khi có sự cố

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.