接入方式
选一个起点
把助手加到您的网站上
在您允许的任意页面加一个 script 标签——悬浮入口和访客聊天立刻可用。
把 AI 客服放进您的 iOS 应用
在 WKWebView 里加载聊天;一座三条消息的小桥完成用户登录。
把 AI 客服放进您的 Android 应用
在 WebView 里加载聊天;一个 JavaScript 接口完成用户登录。
把您的 MCP 服务器接成工具
您自己的 MCP 服务器成为助手的操作能力,并代表每一位顾客执行。
在终端里管理每一个工作区
通过 OAuth,从 Claude Code、Cursor 或任意 MCP 客户端驱动平台。
让 AI 智能体读懂您的站点
在 /llms.txt 提供一份纯文本站点地图,专为 AI 智能体而写。
01快速上手
各部分如何衔接
您的应用始终是事实来源。助手收到一个短时效的签名令牌,说明当前是谁在场,并以一个独立且受限的执行者身份调用您的工具。
01
您的应用
保管身份和账户数据;签发一个两分钟有效的令牌,说明是谁登录了。
02
助手
应用您工作区的设置:内容、模型、工具访问级别、确认步骤、转接。
03
您的 MCP 服务器
每次只为一位用户作答,用户身份取自已登录的 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": "00000000-0000-0000-0000-000000000000", // Your product's registered tenant claim
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("https://YOUR-PRODUCT-DOMAIN (set when you register the provider)")
.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 "bro" 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, logout, access-token/session
// rotation, or account switch. Do not send an identity postMessage directly.
window.refreshAssistantIdentity = () =>
window.BusymateAI?.refreshIdentity?.() ?? Promise.resolve();
</script>
<script
src="https://your-assistant.busymate.ai/embed/v1.js"
data-assistant="your-assistant"
data-label="Ask bro"
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
WKWebView 加一座窄口径的消息桥
只允许三条带版本号的消息:身份请求、身份响应和外部 URL。身份令牌通过应用已有的认证 API 客户端签发。
// SDK source: https://busymate.ai/sdk/v1/ios/BusymateAI.swift
// Load https://your-assistant.busymate.ai/?channel=ios in a WKWebView.
final class AssistantBridge: NSObject, WKScriptMessageHandler {
let webView: WKWebView
func userContentController(_ controller: WKUserContentController,
didReceive message: WKScriptMessage) {
guard message.name == "BusymateAI",
let body = message.body as? [String: Any],
body["type"] as? String == "busymate.ai.v1.identity_request"
else { return }
Task { // mint through YOUR authenticated API — never a key in the app
let identity = try await api.mintLaunchIdentity()
let payload: [String: Any] = [
"type": "busymate.ai.v1.identity",
"token": identity.token,
"nonce": identity.nonce,
]
let data = try JSONSerialization.data(withJSONObject: payload)
let json = String(decoding: data, as: UTF8.self)
await webView.evaluateJavaScript("window.postMessage(\(json), '*')")
}
}
}05Android
WebView 加一个白名单接口
将导航限制在租户 AI 源站,把外部链接交给系统浏览器,并且不要暴露通用原生方法。
// SDK source: https://busymate.ai/sdk/v1/android/BusymateAI.kt
// Load https://your-assistant.busymate.ai/?channel=android in a WebView.
class AssistantBridge(private val webView: WebView) {
@JavascriptInterface
fun postMessage(raw: String) {
val message = JSONObject(raw)
if (message.optString("type") != "busymate.ai.v1.identity_request") return
lifecycleScope.launch { // mint through YOUR authenticated API client
val identity = api.mintLaunchIdentity()
val response = JSONObject()
.put("type", "busymate.ai.v1.identity")
.put("token", identity.token)
.put("nonce", identity.nonce)
webView.evaluateJavascript(
"window.postMessage(${JSONObject.quote(response.toString())}, '*')", null
)
}
}
}
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(AssistantBridge(webView), "BusymateAINative")
// SupportChatNative + support.chat.v1.* remain accepted for shipped apps.06桌面端
同一套协议同样适用于 Electron、Tauri 和原生外壳
使用隔离的 WebView,禁止在视图内新开窗口,把安全的 URL 交给外部打开,并通过特权宿主层来响应身份请求。
import { mountBusymateAI } from "https://busymate.ai/sdk/v1/index.js";
// productAuth is a narrow preload/Tauri command bridge. It calls YOUR
// authenticated backend; no cookie, signing key, or refresh token is exposed
// to the renderer.
const assistant = await mountBusymateAI({
assistant: "your-assistant",
origin: "https://your-assistant.busymate.ai",
label: "Ask bro",
getIdentity: () => window.productAuth.mintAssistantIdentity(),
});
window.productAuth.onSessionChanged(() => assistant.refreshIdentity());
assistant.open();
// Electron main process (Tauri: use the equivalent shell/open allowlist):
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
const target = new URL(url);
if (target.protocol === "https:" || target.protocol === "http:") {
void shell.openExternal(target.href);
}
return { action: "deny" };
});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 bro 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 客户端的服务器,适用的都是同一套协议。
- OAuth 2.1 + PKCE(S256);公共客户端动态注册(RFC 7591)。
- 通过授权服务器元数据(RFC 8414)和受保护资源元数据(RFC 9728)进行发现。
- 授权类型:authorization_code 与 refresh_token;授权响应中带有签发方(RFC 9207)。
- 当平台作为 OAuth 客户端访问您的 MCP 服务器时,使用资源指示符(RFC 8707)。
- 在已配置的场景下使用令牌交换(RFC 8693)。
08转接与洞察
设定真人何时介入的规则
何时转接
顾客要求找真人、工具调用失败、退款超出您的限额、出现某个关键词,或任何由您定义的事件。
您的团队如何收到通知
一个收件箱;可手动指派、轮流接单,或分给未结对话最少的人;提醒方式包括应用内、Web Push、APNs 和个人 Telegram。
您的团队接手
观察对话、带着上下文加入、在同一段聊天里回复;真人在场期间,助手不会做任何变更。
洞察
重复出现的问题、缺失的知识和走不通的路径,从真实对话中归类整理,并附上证据链接。
09工作区登录
保留您自己的用户体系和认证方式
在 Console 中配置工作区的登录和账户 URL。访客点击登录后离开聊天框架,在您的产品上完成认证,再回到原来那个确切的 URL。随后您的后端提供一次性登录令牌;Busymate AI 永远拿不到用户的密码或您的签名密钥。
变更只会触及已登录用户本人的数据。 账户类工具从受委派的执行者那里取得顾客身份,在每次调用时重新校验授权,并对您配置的变更请求确认。
10上线检查清单
上线前检查清单
- 01设置品牌形象、SEO、您的网址和推荐问题。
- 02注册您的登录体系:签发方、JWKS、受众、claims 和令牌时效。
- 03接入 MCP,为每个工具设定访问级别,验证读取只限本人范围、写入必须确认。
- 04设定转接规则,为收件箱安排人力,设置工作时间和响应目标。
- 05测试访客、已登录用户、退出登录与切换账户、重放的令牌,以及被替换的工作区 id。
- 06检查整页聊天、嵌入、iOS、Android、桌面端、无障碍和外部链接。
- 07只发布您已检查过的版本;持续关注连接和域名的健康状况。
准备好配置您的工作区了吗?
Console 的集成页面会根据您已发布的设置,为每个入口生成对应的代码和配置。
管理用 MCP
从任意 MCP 客户端管理每一个工作区
218 个管理工具,基于 MCP 与 OAuth 2.1。客户端首次连接时会打开浏览器登录;无需粘贴任何东西。
发现文档
- MCP 端点
- https://busymate.ai/mcp
- 授权服务器元数据(RFC 8414)
- https://busymate.ai/.well-known/oauth-authorization-server/mcp
- 受保护资源元数据(RFC 9728)
- https://busymate.ai/.well-known/oauth-protected-resource/mcp
您登录所用的账户决定了客户端可以管理哪些工作区。每一次变更都会请求确认。
Claude Code
claude mcp add --transport http busymate-ai https://busymate.ai/mcp从任意 MCP 客户端管理平台
Cursor、Claude Desktop 以及任何会读取 mcp.json 的客户端都接受这段配置;支持远程 URL 的客户端直接填上面那个端点。
{
"mcpServers": {
"platform-management": {
"type": "http",
"url": "https://busymate.ai/mcp"
}
}
}