接入方式
在您允许的任意页面加一个 script 标签——悬浮入口和访客聊天立刻可用。
安装带版本号的 Swift 包,用于账户身份、聊天和点击控件后请求的麦克风权限。
安装带版本号的 Android 库,用于账户身份、聊天和点击控件后请求的麦克风权限。
您自己的 MCP 服务器成为助手的操作能力,并代表每一位顾客执行。
通过 OAuth,在 Claude、Claude Code、Cursor 或任何 MCP 客户端中操控平台。
WebMCP:您的页面把自身的操作以工具形式列出——预约、下单、查询状态——并内置确认环节。
01快速上手
您的应用始终是事实来源。助手收到一个短时效的签名令牌,说明当前是谁在场,并以一个独立且受限的执行者身份调用您的工具。
01
保管身份和账户数据;签发一个两分钟有效的令牌,说明是谁登录了。
02
应用您工作区的设置:内容、模型、工具访问级别、确认步骤、转接。
03
每次只为一位用户作答,用户身份取自已登录的 bearer 令牌——绝不取自工具参数。
7 种接入方式,一个助手: Web 嵌入 · 已登录顾客 · iOS · Android · 桌面端 · REST API · 您的 MCP 服务器
02已登录顾客
为了让助手确信提问者的身份,您后端上一个需要认证的端点会用您稳定的顾客 id 和少量可安全展示的字段,签发一个短时效令牌(一个 ES256 JWT)。
该令牌最多有效期为 120 秒,携带一次性 nonce 和 jti,并标识您的工作区。私钥和您的产品会话永远不会到达浏览器或聊天。
import { SignJWT, importJWK } from "jose";
// POST https://YOUR-PRODUCT-DOMAIN/api/bmai/identity — requires YOUR OWN logged-in product session.
app.post("/api/bmai/identity", requireSession, async (req, res) => {
// Fresh on EVERY mint. Never persist the launch token/nonce in localStorage,
// sessionStorage, cookies, React state, or module state: every assistant
// launch consumes this pair exactly once.
const nonce = typeof req.body?.nonce === "string" ? req.body.nonce : "";
if (!/^[A-Za-z0-9_-]{32,200}$/.test(nonce)) return res.status(400).json({ error: "invalid_nonce" });
const key = await importJWK(JSON.parse(process.env.AI_LAUNCH_PRIVATE_JWK), "ES256");
const token = await new SignJWT({
tenant_id: process.env.BMAI_TENANT_ID, // the tenant id shown in Console → Identity
nonce, // equals the sibling field below
name: req.user.displayName, // optional low-sensitivity display claim
})
.setProtectedHeader({ alg: "ES256", kid: process.env.AI_LAUNCH_KEY_ID })
.setIssuer(process.env.BMAI_ISSUER) // the Issuer you registered in Console → Identity
.setAudience("busymate-ai")
.setSubject(req.user.id) // IMMUTABLE internal account id; never email/phone/session id
.setJti(crypto.randomUUID()) // one-time (replay-protected)
.setIssuedAt()
.setExpirationTime("120s") // <= registered max age (120s)
.sign(key);
res.set("Cache-Control", "no-store");
res.status(201).json({ token, nonce, expiresIn: 120 });
});令牌说明的是谁在场,而不是他们可以做什么。 把余额、设备、设置和每一次变更,都放在只为单个用户作答的工具背后。
03Web 与整页
SDK 在需要身份时向您的后端索取,对访客同样可用,在登录和退出后自动刷新,把外部链接放到框架外打开,且完全不需要第三方 cookie。
<!-- Floating "mate" launcher for Your product.
Anonymous chat works immediately on the allowed origins. -->
<script>
function newLaunchNonce() {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
window.BusymateAI = {
getIdentity: async () => {
const accessToken = await getProductAccessToken(); // YOUR existing auth helper
if (!accessToken) return null;
const nonce = newLaunchNonce();
const returnTo = new URL(location.href); returnTo.search = ""; returnTo.hash = "";
const r = await fetch("https://YOUR-PRODUCT-DOMAIN/api/bmai/identity", {
method: "POST", credentials: "include", cache: "no-store",
headers: { "content-type": "application/json", authorization: "Bearer " + accessToken },
body: JSON.stringify({ nonce, returnTo: returnTo.href }),
});
if (r.status === 401) return null; // signed out -> anonymous chat
if (!r.ok) throw new Error("AI identity mint failed (" + r.status + ")");
// MUST be a newly minted { token, nonce } pair on every call. Never
// persist either value in localStorage, sessionStorage, cookies, React
// state, or module state.
const identity = await r.json();
if (identity.nonce !== nonce) throw new Error("AI identity nonce mismatch");
return { token: identity.token, nonce: identity.nonce };
},
};
// Call after YOUR product completes login, an access-token/session
// rotation, or an account switch — upgrades the live conversation in
// place, same thread. Do not send an identity postMessage directly.
window.assistantIdentityChanged = () =>
window.BusymateAI?.identityChanged?.() ?? Promise.resolve(false);
// Call after YOUR product completes logout. Without this the signed-out
// visitor keeps a verified chat and the previous transcript until the tab
// closes — a privacy defect, not a missing nicety.
window.assistantSignedOut = () =>
window.BusymateAI?.signedOut?.("signed_out");
</script>
<script
src="https://your-assistant.busymate.ai/embed/v1.js"
data-assistant="your-assistant"
data-label="Ask mate"
async></script>当已登录用户打开您助手的地址或您的自定义域名时,把登录令牌放在 URL 片段中传入。页面会在交换之前把它清除。
// Full-page open with identity in the URL FRAGMENT — never sent in the
// request line, referrer, or logs; the destination strips it before exchange.
function newLaunchNonce() {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
const nonce = newLaunchNonce();
const accessToken = await getProductAccessToken(); // YOUR existing auth helper
if (!accessToken) throw new Error("Sign in before opening an identified assistant");
const returnTo = new URL(location.href); returnTo.search = ""; returnTo.hash = "";
const response = await fetch("https://YOUR-PRODUCT-DOMAIN/api/bmai/identity", {
method: "POST", credentials: "include", cache: "no-store",
headers: { "content-type": "application/json", authorization: "Bearer " + accessToken },
body: JSON.stringify({ nonce, returnTo: returnTo.href }),
});
if (response.status === 401) throw new Error("Sign in before opening an identified assistant");
if (!response.ok) throw new Error("AI identity mint failed (" + response.status + ")");
const identity = await response.json();
if (typeof identity.token !== "string" || typeof identity.nonce !== "string") {
throw new Error("AI identity mint returned an invalid response");
}
if (identity.nonce !== nonce) throw new Error("AI identity nonce mismatch");
const url = new URL("https://your-assistant.busymate.ai/");
url.hash = new URLSearchParams({
bmai_token: identity.token,
bmai_nonce: identity.nonce,
}).toString();
location.assign(url);04iPhone
固定 Swift 包版本。连接需要身份验证的后端,并参照平台指南和示例应用添加系统麦克风权限。
// Swift Package: https://github.com/serebano/busymate-ai-sdk-ios.git (exact 1.0.2)
// Full lifecycle + demo: https://busymate.ai/docs/guides/ios-sdk
import WebKit
import BusymateAI
@MainActor
func installAssistant(
on webView: WKWebView,
account: @escaping () -> String?,
mint: @escaping @MainActor (BusymateBridge.MintRequest) async throws -> BusymateBridge.MintToken?
) {
BusymateBridge.install(on: webView, config: .init(
assistant: "your-assistant",
origins: ["https://your-assistant.busymate.ai"],
account: account,
mint: mint
))
// mint calls YOUR authenticated backend with request.nonce; never cache tokens.
// Backend endpoint: https://YOUR-PRODUCT-DOMAIN/api/bmai/identity
webView.load(URLRequest(url: URL(string: "https://your-assistant.busymate.ai/?channel=ios")!))
}
// Microphone: declare NSMicrophoneUsageDescription; install and retain
// BusymateMicrophone before load, and forward WKUIDelegate media permission.
// Call BusymateBridge.accountChanged() on login/logout/account changes.05Android
固定 Android 库版本。在加载聊天前安装,并按照 Activity 生命周期、麦克风和渲染器恢复指南进行操作。
// Gradle installation + lifecycle: https://busymate.ai/docs/guides/android-sdk
// Source: https://github.com/serebano/busymate-ai-sdk-android/tree/1.0.2
import android.webkit.WebView
import ai.busymate.bridge.BusymateBridge
fun installAssistant(
webView: WebView,
account: () -> String?,
mint: (Map<String, Any?>, (BusymateBridge.MintResult) -> Unit) -> Unit
) {
BusymateBridge.install(webView, BusymateBridge.Config(
assistant = "your-assistant",
origins = listOf("https://your-assistant.busymate.ai"),
account = account,
mint = mint
))
// mint calls YOUR authenticated backend with request["nonce"], then done(result).
// Backend endpoint: https://YOUR-PRODUCT-DOMAIN/api/bmai/identity
webView.loadUrl("https://your-assistant.busymate.ai/?channel=android")
}
// Microphone: declare INTERNET + RECORD_AUDIO; construct and retain
// BusymateMicrophone in Activity.onCreate BEFORE STARTED/loadUrl; forward
// WebChromeClient.onPermissionRequest. See the guide for renderer recovery.
// Call BusymateBridge.accountChanged() on login/logout/account changes.06桌面端
macOS、Windows、Electron、Linux 和 Tauri 的官方 SDK 在隔离的 WebView 中加载托管聊天,通过特权宿主层响应身份请求,并在外部打开安全链接。固定 SDK 版本,并像移动端一样连接你的已认证后端。
// Official desktop SDK for your platform — macOS, Windows, Electron, Linux or Tauri:
// https://github.com/serebano/busymate-ai-sdk-electron (and -macos, -windows, -linux, -tauri).
// Every SDK takes these settings and installs the identity bridge before the first load.
const chatOptions = {
url: "https://your-assistant.busymate.ai/?channel=desktop_sdk", // the window's top-level page
assistant: "your-assistant",
account: () => productAuth.currentUserId(), // null when signed out
// YOUR authenticated backend signs { token, nonce } for request.nonce.
mint: (request) => productAuth.mintAssistantIdentity(request),
};07MCP 工具
在发现阶段发布完整的 schema。为每个工具指定三种访问级别之一,标出需要确认卡片的变更,并在每次调用时重新校验授权。
任何聊天的人都可以调用。价格、状态、功能。
需要用户已登录。订单状态、账户类问题。
通过受限的执行者令牌或用户本人的 OAuth 授权,代表已登录用户行事。
每一次变更都在服务器端确认。 您列入 confirm_tools 的工具在执行前会停在确认卡片处;助手不认识的工具名同样如此。组件框架永远不会执行变更。
MCP endpoint ............ https://YOUR-DOMAIN/mcp
RFC 8707 resource ....... https://YOUR-DOMAIN/mcp (the token is audience-bound to this)
AS metadata (RFC 8414) .. https://YOUR-DOMAIN/.well-known/oauth-authorization-server
Resource meta (RFC 9728) https://YOUR-DOMAIN/.well-known/oauth-protected-resource
Client registration ..... Dynamic (RFC 7591) — public client, no secret
PKCE .................... S256 required (RFC 7636)
Authorization response .. iss parameter checked (RFC 9207)
Grants .................. authorization_code + refresh_token
Delegated tools/call .... no bearer -> 401; user derived from the bearer,
NEVER from an account id in tool arguments
Customer experience ..... one separate Authorize account tools action is expected;
use signed_actor_token instead for automatic SSO// POST https://YOUR-DOMAIN/mcp
// Authorization: Bearer <the per-user OAuth token mate obtained>
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": { "name": "get_my_account", "arguments": {} }
}
// Your server verifies the bearer, derives the user from its signed subject,
// and returns ONLY that user's data.无论平台是您服务器的客户端,还是您 MCP 客户端的服务器,适用的都是同一套协议。
REST API 和 webhooks
一个工作区作用域的 API 密钥可以同时认证 REST 端点和 MCP 服务器。列出并读取对话、发送操作员回复,或订阅 webhook 而不用轮询。
在 Console → API keys 中创建一个密钥,然后作为 Bearer token 发送。
curl https://busymate.ai/api/v1/conversations \
-H "Authorization: Bearer bmai_sk_..."完整参考: API 指南, Webhook 指南, OpenAPI 文档.
08转接与洞察
顾客要求找真人、工具调用失败、退款超出您的限额、出现某个关键词,或任何由您定义的事件。
一个收件箱;可手动指派、轮流接单,或分给未结对话最少的人;提醒方式包括应用内、Web Push、APNs 和个人 Telegram。
观察对话、带着上下文加入、在同一段聊天里回复;真人在场期间,助手不会做任何变更。
重复出现的问题、缺失的知识和走不通的路径,从真实对话中归类整理,并附上证据链接。
09工作区登录
在 Console 中配置工作区的登录和账户 URL。访客点击登录后离开聊天框架,在您的产品上完成认证,再回到原来那个确切的 URL。随后您的后端提供一次性登录令牌;Busymate AI 永远拿不到用户的密码或您的签名密钥。
变更只会触及已登录用户本人的数据。 账户类工具从受委派的执行者那里取得顾客身份,在每次调用时重新校验授权,并对您配置的变更请求确认。
10上线检查清单
准备好配置您的工作区了吗?
Console 的集成页面会根据您已发布的设置,为每个入口生成对应的代码和配置。
WebMCP
WebMCP 是 W3C Web 机器学习社区组提出的草案,用于让页面把自身操作列为工具。共有三种接入方式,从最小的改动到最完整的控制。
book_appointment、check_order_status、start_return——一个工具,一项职责。
“下周二十点”“加急”——绝不是需要助手去猜测的内部 ID。
提供本页面可运行的工具;页面跳转时取消注册。
预约、付款、退货:为其打上标记,客户确认后才会执行。
确认权始终掌握在客户手中。 工具在访客自己的浏览器中运行,位于您的源站上。您标记为有后果的工具会先经过客户可见的确认;exposedTo 则限制哪些源可以看到该工具。
最小的改动:在您已有的表单上加一个名称和一行描述。浏览器会把它变成一个工具;表单依然可见,对所有人照常可用。
<form action="/book" toolname="book_appointment"
tooldescription="Book an appointment for a date, a time and a service."
toolautosubmit>
<input name="date" type="date" toolparamdescription="The day, e.g. next Tuesday">
<input name="time" type="time" toolparamdescription="The start time">
<select name="service" toolparamdescription="Which service to book">
<option>Consultation</option>
<option>Follow-up</option>
</select>
<button type="submit">Book</button>
</form>完整控制:浏览器 API。可通过特性检测来判断——不支持 WebMCP 的浏览器只会看到现在的页面。Chrome 149 和 Edge 150 以源站试用形式提供;Firefox 和 Safari 正在评审该提案。
// The browser API (draft). Load the polyfill BEFORE this file so every browser has it
// (it stands down where the browser is native):
// <script src="https://busymate.ai/webmcp/polyfill-1.0.1.js" integrity="sha384-pLZ5lO3gwZIn8Nim+hDrUQY+bvpliRtZhrS1O1OGRFB3lvoW0sZ71DUFHIk3m+DG" crossorigin="anonymous"></script>
const ctx = document.modelContext ?? navigator.modelContext;
if (!ctx) throw new Error("no WebMCP registry: the polyfill did not load");
ctx?.registerTool({
name: "start_return",
description: "Start a return for a delivered order. Asks the customer to confirm first.",
inputSchema: { type: "object", properties: { orderNumber: { type: "string", description: "The order number, as printed on the receipt" } }, required: ["orderNumber"] },
annotations: { consequentialHint: true },
execute: async ({ orderNumber }) => startReturn(orderNumber),
}, { exposedTo: ["self"] });在已安装助手的地方,一次调用就能把您的工具列给它,也列给每一个访问的其他助手——在支持浏览器 API 的地方通过该 API,其余地方则通过助手自身的桥接。
// One call, both transports: the browser's own WebMCP where it exists,
// and the assistant's bridge everywhere else (Safari, Firefox, app WebViews).
BusymateAI.registerPageTools([
{
name: "check_order_status",
description: "Look up an order by its number and say where it is.",
inputSchema: {
type: "object",
properties: { orderNumber: { type: "string", description: "The order number, as printed on the receipt" } },
required: ["orderNumber"],
additionalProperties: false,
},
annotations: { readOnlyHint: true },
execute: async ({ orderNumber }) => (await fetch(`/api/orders/${orderNumber}`)).json(),
},
]);管理用 MCP
通过 MCP 和 OAuth 2.1 的管理工具。客户端在首次连接时打开浏览器登录;无需粘贴任何内容。
https://busymate.ai/mcp| 客户端 | 连接方式 |
|---|---|
| Claude | 从 Claude 的连接器目录添加连接器并登录,无需粘贴任何内容。 |
| ChatGPT and Codex | 该应用正在等待 OpenAI 审核,尚未上架。 |
| Claude Code (CLI) | 运行下面的命令——浏览器会处理 OAuth 登录。 |
| Cursor | 把下面的 mcp.json 条目粘贴到 Cursor 的 MCP 设置中。 |
| 其他 MCP 客户端 | 给客户端提供下面的端点;OAuth 2.1 discovery 和 PKCE 会处理其余的。 |
您登录所用的账户决定了客户端可以管理哪些工作区。每一次变更都会请求确认。
claude mcp add --transport http busymate-ai https://busymate.ai/mcpCursor、Claude Desktop 以及任何会读取 mcp.json 的客户端都接受这段配置;支持远程 URL 的客户端直接填上面那个端点。
{
"mcpServers": {
"busymate-ai": {
"type": "http",
"url": "https://busymate.ai/mcp"
}
}
}最新变更
构建 1636
Chat no longer shows an old "Authorize account tools" card after a workspace changes how a connector identifies customers.
构建 1634
One request lists conversations across several workspaces. AI assistants connected to Busymate AI can now list the conversations of several workspaces in one request, by passing a list of workspaces or "all". The answer is one table with a Workspace column, at most 10 workspaces and 50 conversations each per request, and a cursor to page each workspace further. In the ChatGPT iPhone app, a question about several workspaces now shows one table instead of several empty "Couldn't load" cards.
构建 1633
The per-connector "never show the Authorize card" switch is now the same thing as Customer identity "none". A connector listed in the older card-off switch behaves exactly like a connector whose customer identity is "none".