28 KiB
Chat SDK 第三方系统集成指南
本文档说明第三方系统如何接入 AI 智能客服的 Chat SDK,完成从创建 API Key 到嵌入对话组件的全流程。
目录
- 集成架构总览
- 第一步:创建 API Key
- 第二步:绑定客服角色(可选)
- 第三步:后端换取 SDK Token
- 第四步:前端嵌入 Chat SDK
- API 接口参考
- SDK 配置参数参考
- 错误码与排查
- 安全建议
- 验证 Token 自动刷新
1. 集成架构总览
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 第三方前端 │ │ 第三方后端 │ │ AI 客服后端 │
│ (嵌入 SDK) │ │ (你的服务) │ │ (本系统) │
│ │ │ │ │ │
│ Chat SDK ──────┼─────┼──────────────────┼─────┼─► /ai/** 对话 │
│ ↑ token │ │ │ │ │
│ │ │ ① 携带 API Key │ │ ② 返回 JWT Token│
│ │ │ POST /open-api/ │─────┼─► /auth/token │
│ │ │ auth/token │ │ │
└─────────────────┘ └──────────────────┘ └──────────────────┘
两段式鉴权流程:
- 管理后台:管理员创建 API Key,可选绑定客服角色
- 第三方后端:用 API Key 换取短期 JWT Token + 可用角色列表
- 第三方前端:Chat SDK 携带 JWT Token 请求
/ai/**接口进行对话
2. 第一步:创建 API Key
在管理后台「系统设置 → API Key 管理」页面创建。
管理后台操作
- 登录管理后台
http://localhost:9090/index.html - 进入「系统设置 → API Key 管理」
- 点击「+ 新建 API Key」
- 填写名称、描述、频率限制等参数
- (可选) 在创建弹窗中勾选需要绑定的客服角色
- 创建后立即复制保存 Key,关闭后无法再次查看
API Key 格式
sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(sk_ 前缀 + 32 位 UUID)
3. 第二步:绑定客服角色(可选)
客服角色决定 AI 的人设、知识库范围和 MCP 工具权限。
角色绑定规则
| role_ids 状态 | Token 换取行为 |
|---|---|
空数组 [](默认) |
返回所有启用的客服角色(向后兼容) |
非空 [1, 2, 3] |
仅返回绑定的角色 |
通过管理后台绑定
在 API Key 列表中点击「绑定角色」按钮,勾选需要的角色后保存。
通过 API 绑定
PUT /api-key/{keyId}/roles
Content-Type: application/json
Authorization: Bearer {管理后台 JWT Token}
{
"roleIds": [1, 2, 3]
}
响应:
{
"success": true,
"message": "角色绑定更新成功"
}
4. 第三步:后端换取 SDK Token
⚠️ 重要:Token 换取必须在第三方后端完成,不可在前端暴露 API Key。
请求
POST /open-api/auth/token
Content-Type: application/json
X-API-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
{
"ttl": 7200
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
X-API-Key |
Header | 是 | 创建的 API Key |
ttl |
Body | 否 | Token 有效期(秒),默认 7200(2 小时),范围 [300, 86400] |
成功响应
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiJ9...",
"expiresIn": 7200,
"roles": [
{ "id": "1", "key": "general", "name": "通用客服" },
{ "id": "2", "key": "finance", "name": "财务顾问" }
]
}
| 字段 | 说明 |
|---|---|
token |
SDK JWT Token,传给前端使用 |
expiresIn |
过期时间(秒) |
roles |
可用客服角色列表,前端用于展示角色选择器 |
错误响应
| HTTP 状态码 | 说明 |
|---|---|
| 401 | API Key 无效或已禁用 |
| 403 | 无可用客服角色 |
5. 第四步:前端嵌入 Chat SDK
使用测试面板快速验证
访问 http://localhost:9090/sdk/test.html 打开测试面板:
- 在「🔐 SDK Token 鉴权」区域输入 API Key
- 点击「获取 Token」按钮自动换取(或手动粘贴已有的 Token)
- Token 获取成功后,integrateId 会自动填充为第一个可用角色
- 点击「🚀 初始化 SDK」即可测试对话功能
测试面板支持所有 P0/P1/P2 测试用例的自动化运行,包括 API 连通性验证。
5.1 引入 SDK
<script src="https://your-domain.com/sdk/chatbot-sdk.js"></script>
5.2 初始化
ChatbotSDK.init({
// ========== 必填参数 ==========
integrateId: '1', // 客服角色 ID(从 Token 换取接口的 roles 中选择)
requestDomain: 'https://your-domain.com', // AI 客服后端域名
// ========== 鉴权参数(推荐) ==========
token: 'eyJhbGciOiJIUzI1NiJ9...', // 后端换取的 SDK Token
expiresIn: 7200, // 该 Token 的有效期(秒,与换取接口返回的 expiresIn 一致)
tokenStrategy: 'expiry', // 'expiry'(默认)临期/401 才取;'always' 每个请求前都取
tokenUrl: '/my-backend/sdk-token', // 取 Token 的地址(与下面的 getToken 二选一,回调优先)
getToken: async function() { // 取 Token 回调:SDK 在临期 / 每请求前(always)/ 收到 401 时调用
// 注意:这里应调「你自己的后端」,由它持 API Key 去调 /open-api/auth/token
const res = await fetch('/my-backend/sdk-token');
const data = await res.json();
return { token: data.token, expiresIn: data.expiresIn };
},
roles: [ // 可用角色列表(传多个时 SDK header 显示角色切换下拉框)
{ id: '1', key: 'general', name: '通用客服' },
{ id: '2', key: 'finance', name: '财务顾问' }
],
// ========== 可选参数 ==========
userId: 'user_12345', // 用户标识(用于会话隔离)
title: 'AI 智能助手', // 对话窗口标题
theme: 'light', // 主题:light / dark
primaryColor: '#4F46E5', // 主题色(十六进制)
streaming: true, // 启用流式回复(默认 true)
enableRag: true, // 启用 RAG 知识库检索(默认 true)
quickReplies: ['如何退款?', '联系人工客服'], // 快捷问题
position: 'right-bottom', // 悬浮按钮位置:right-bottom / left-bottom
width: 500, // 窗口宽度(px)
height: 520, // 窗口高度(px)
debug: true, // 调试日志
// ========== 回调函数 ==========
onReady: function() { console.log('SDK 就绪'); },
onMessage: function(msg) { console.log('收到消息', msg); },
onError: function(err) {
// code: auth_expired / auth_refresh_failed / config_invalid / 通用错误码
console.error('SDK 错误', err.code, err.message);
}
});
5.3 SDK 方法
// 运行时更新 Token(宿主自行换好后推送)—— 无需 destroy + init 重建 DOM,下次请求即生效
ChatbotSDK.setToken(newToken, 7200); // 第二个参数为有效期(秒),可省略
// 让 SDK 调 getToken 回调立即换新 Token,返回是否换到了不同的 Token
const refreshed = await ChatbotSDK.refreshToken();
// 销毁实例(移除 DOM 和事件监听)
ChatbotSDK.destroy();
其余可用方法:open() / close() / toggle() / clearHistory()。
5.4 不使用 Token 的兼容模式
如果暂时不接入后端 Token 换取,可直接使用兼容模式(不推荐用于生产环境):
ChatbotSDK.init({
integrateId: '1', // 直接指定角色 ID
requestDomain: 'https://your-domain.com'
// 不传 token 和 roles
});
兼容模式下,integrateId 直接作为 roleId 传递给后端,无需 Token 换取步骤。由于不传 roles,SDK 不会显示角色选择器,用户只能使用初始指定的单一角色。
注意:此时 /ai/**、/feedback、/attachment/upload 等受 SdkAuthFilter 保护的接口仍会返回
401(对话不可用)。若只想免去自己管理过期时间、但仍要能对话,可以只配 getToken 不配 token:
SDK 会在首个受保护请求发出之前自动换取 Token("自举"),因此不会白跑一次 401。之后按正常刷新逻辑续期。
拿不到 expiresIn 时还可以配 tokenStrategy: 'always',让 SDK 每次请求前都取一次(见第 8 节常见问题)。
5.5 角色切换
当 roles 数组长度 > 1 时,SDK 会在聊天窗口头部自动显示角色选择下拉框,用户可随时切换当前使用的客服角色。
触发条件:
ChatbotSDK.init()时传入的roles数组包含 2 个及以上角色
切换行为:
- 自动保存当前对话到 localStorage(与旧角色关联)
- 清空当前消息列表
- 生成新的
chatId(与新角色关联) - 尝试从 localStorage 恢复新角色的缓存消息
- 异步从后端加载新角色的历史会话
示例:
// 1. 后端换取 Token,获取可用角色列表
const data = await fetch('/api/chatbot/token').then(r => r.json());
// 2. 初始化 SDK,传入角色列表
ChatbotSDK.init({
integrateId: data.roles[0].id, // 默认使用第一个角色
requestDomain: 'https://your-domain.com',
token: data.token,
roles: data.roles, // 传入多个角色 → header 显示角色选择器
userId: 'current_user_id'
});
安全说明:
- 角色切换由 SDK 前端发起,后端
SdkAuthFilter会校验 JWT Token 中的ridsclaim,确保用户只能切换到被授权的角色 - 切换角色不需要重新换取 Token,JWT 中已包含允许的角色 ID 列表
6. API 接口参考
6.1 Token 换取
POST /open-api/auth/token
6.2 对话接口(SDK 内部调用,无需手动对接)
| 接口 | 方法 | 说明 |
|---|---|---|
/ai/chat |
GET | 同步对话(已统一为 ChatPipeline,支持普通/RAG 自动判断) |
/ai/chat/stream |
GET | SSE 流式对话(OpenAI Chat Completions 兼容格式,已统一为 ChatPipeline) |
/ai/chat/sources |
GET | 获取 RAG 引用来源 |
/ai/sdk/conversation/list |
GET | 会话列表 |
/ai/sdk/conversation/{id}/messages |
GET | 会话消息 |
/ai/sdk/conversation/{id} |
DELETE | 删除会话 |
/ai/sdk/conversation/{id}/export |
GET | 导出会话 |
/category/tree |
GET | 知识库分类树 |
/feedback |
POST | 消息反馈 |
已废弃:
/ai/assistant_app/chat/server_sent_event和/ai/assistant_app/chat/sse_emitter已移除。旧路径/ai/assistant_app/chat/sync、/ai/assistant_app/chat/sse、/ai/assistant_app/chat/rag/sse、/ai/assistant_app/rag/sources仍保留(向后兼容)但已废弃,请统一使用/ai/chat、/ai/chat/stream、/ai/chat/sources。
6.3 Open API 对话接口(第三方系统直接调用)
| 接口 | 方法 | 说明 |
|---|---|---|
/open-api/chat |
POST | 同步对话(已补齐角色/RAG/FAQ/MCP/分类隔离) |
/open-api/chat/stream |
GET | SSE 流式对话(同上) |
/open-api/rag/search |
POST | 知识库检索 |
Open API 对话新增参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
categoryIds |
Query | 否 | 知识库分类 ID,逗号分隔(非角色场景用) |
rewriteStrategy |
Query | 否 | RAG 查询重写策略:REWRITE / TRANSLATION / COMPRESSION / MULTI_QUERY(默认) |
enableRag |
Query | 否 | 是否启用 RAG 检索(默认 true),false 时走普通对话 |
认证方式: 所有 /open-api/** 和 /ai/** 请求通过 X-API-Key Header 或 Bearer Token 鉴权。
6.3 管理接口(需要管理后台 JWT)
| 接口 | 方法 | 说明 |
|---|---|---|
/api-key/list |
GET | API Key 列表(admin) |
/api-key |
POST | 创建 API Key(admin) |
/api-key/{id}/revoke |
PUT | 吊销 API Key(admin) |
/api-key/{id}/enable |
PUT | 启用 API Key(admin) |
/api-key/{id} |
DELETE | 删除 API Key(admin) |
/api-key/{id}/roles |
PUT | 更新角色绑定(admin) |
/role/list |
GET | 客服角色列表(admin) |
/role/all |
GET | 全部角色含停用(admin) |
7. SDK 配置参数参考
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
integrateId |
String/Number | 必填 | 客服角色 ID |
requestDomain |
String | 必填 | 后端域名 |
token |
String | - | SDK JWT Token(推荐) |
expiresIn |
Number | - | token 有效期(秒,与换取接口返回的 expiresIn 同单位)。传入后 SDK 会在临期前主动刷新 |
getToken |
Function | - | 取 Token 回调,SDK 在临期 / 每请求前(always 模式)/ 收到 401 时调用。推荐返回 { token, expiresIn };只返回字符串则仅能靠 401 驱动 |
tokenStrategy |
String | 'expiry' | 取 Token 时机:'expiry' 临期 / 401 才取;'always' 每个受保护请求前都取一次(不依赖 expiresIn,代价是每请求多一次宿主后端往返) |
tokenUrl |
String | - | 取 Token 的地址(与 getToken 二选一,同时配置时 getToken 优先)。SDK 会 GET 它并解析 {token,expiresIn} 或 {success,token,expiresIn,roles};请求用 credentials: 'same-origin',跨域不带 Cookie |
roles |
Array | - | 角色列表 [{id, key, name}],长度 > 1 时 header 显示切换下拉框 |
userId |
String | - | 用户标识(会话隔离) |
categoryId |
Number | - | 默认知识库分类 ID |
showCategorySwitch |
Boolean | false | 显示分类切换器 |
title |
String | 'AI 智能助手' | 窗口标题 |
width |
Number | 500 | 窗口宽度(px) |
height |
Number | 520 | 窗口高度(px,最小 400) |
position |
String | 'right-bottom' | 悬浮按钮位置 |
primaryColor |
String | '#4F46E5' | 主题色 |
launcherIcon |
String | (内置) | 自定义悬浮按钮图标(URL 或 SVG 字符串) |
launcherSize |
Number/String | 80 | 悬浮按钮尺寸(px 或 CSS 长度字符串) |
theme |
String | 'light' | 界面主题:light / dark |
streaming |
Boolean | true | 流式回复 |
enableRag |
Boolean | true | RAG 知识库检索 |
rewriteStrategy |
String | 'REWRITE' | 查询重写策略 |
quickReplies |
String[] | [] | 快捷问题列表 |
showClear |
Boolean | true | 显示清空按钮 |
showTeaser |
Boolean | true | 显示提示气泡 |
teaserText |
String | '' | 提示气泡文本 |
sound |
Boolean | false | 消息提示音 |
notification |
Boolean | false | 浏览器通知 |
allowImageUpload |
Boolean | true | 允许上传图片参与对话 |
suggestions |
Boolean | true | AI 回复后展示推荐问题 |
resizable |
Boolean | true | 允许拖拽缩放窗口 |
watermark |
String | - | 聊天窗口水印文字 |
locale |
String | 'zh-CN' | 语言:zh-CN / en |
debug |
Boolean | true | 调试日志 |
onReady |
Function | - | SDK 就绪回调 |
onMessage |
Function | - | 消息回调 |
onError |
Function | - | 错误回调(code 见第 8 节) |
8. 错误码与排查
Token 换取阶段
| 错误 | 原因 | 解决方案 |
|---|---|---|
401 API Key 鉴权失败 |
X-API-Key 无效或缺失 | 检查 Header 是否正确携带 |
401 API Key 已被禁用 |
Key 被吊销 | 在管理后台重新启用或创建新 Key |
401 API Key 已过期 |
Key 超过有效期 | 创建新的 API Key |
403 无可用客服角色 |
未绑定角色且系统无启用角色 | 在管理后台创建并启用客服角色 |
SDK 对话阶段
| HTTP 状态码 | SDK 提示 | 原因 |
|---|---|---|
| 401 | 鉴权失败 | Token 过期或无效。SDK 会自动换一次 Token 并重试;若仍 401,则通过 onError 上报 auth_expired |
| 403 | 无访问权限 | roleId 不在 Token 允许范围内 |
| 429 | 请求过于频繁 | 超过 API Key 频率限制 |
| 500 | 服务器异常 | 后端错误,查看服务端日志 |
| 502/503 | 服务暂不可用 | 后端未启动或正在部署 |
onError 回调中的 code(用于程序化识别,比看中文文案可靠):
| code | 含义 | 解决方案 |
|---|---|---|
auth_expired |
自动刷新后仍鉴权失败,Token 彻底失效 | 检查 API Key 是否被吊销/过期、是否绑定了启用中的客服角色;修好后调 ChatbotSDK.setToken() |
auth_refresh_failed |
换 Token 的 getToken 回调失败 |
检查宿主换 Token 接口与网络(同一 code 30 秒内只上报一次,不会刷屏) |
config_invalid |
init() 传入的参数非法(如 token 非字符串、expiresIn 非正数),该参数被忽略 |
按提示修正配置 |
network / timeout / cors |
网络层错误 | 检查网络、CORS 白名单、后端可用性 |
常见问题
Q: Token 过期后怎么办?SDK 会自动刷新吗? A: 会。 SDK 内置了自动刷新,第三方无需自己轮询换 Token(Token 默认 2 小时过期)。两种触发方式:
- 临期主动刷新(推荐):
init()时传入expiresIn+getToken,SDK 会在 Token 剩余寿命不足 「总寿命 10% 与 5 分钟中的较小值」时主动换取新 Token,用户完全无感。 - 401 兜底重试:任何受保护请求收到 401 时,SDK 会换一次 Token 并自动重试原请求,
因此 UI 上不会闪出「鉴权失败」。(安全性说明:
SdkAuthFilter的 401 都发生在业务逻辑执行之前, 所以重试写请求不会产生重复副作用。)
如果不想给 getToken 回调,也可以由第三方后端自己盯过期时间,换好后调 ChatbotSDK.setToken(token, expiresIn)
推送给 SDK —— 同样是即时生效,不需要 destroy + init。
Q: 只配 getToken 不配 token 可以吗?
A: 可以。SDK 在首个受保护请求发出之前会自动换取 Token("自举"),不会白跑一次 401。
Q: 拿不到 expiresIn(或不想让 SDK 推算过期时间)怎么办?
A: 用 tokenStrategy: 'always'。此时 SDK 每个受保护请求发出前都会向你的后端取一次 Token,
完全不做过期时间推算,由你的后端决定何时真的重新换取(它自己的缓存命中就直接返回缓存值)。
代价是每个请求多一次宿主后端往返(含每次对话的首 token 之前)。若你的后端能给出可信的 expiresIn,
用默认的 'expiry' 更省往返。两者都建议配上取 Token 方式:
ChatbotSDK.init({
// ...
tokenStrategy: 'always',
tokenUrl: '/my-backend/sdk-token', // 或 getToken 回调
});
Q: 取 Token 接口需要带 Cookie 怎么办?
A: tokenUrl 用的是 credentials: 'same-origin':同源请求会自动带上你站点的 Cookie(宿主页面调
自己后端的最常见场景),跨域则不带。若你的取 Token 接口在另一个域上且需要 Cookie,
请改用 getToken 回调,用你自己的 fetch 控制 credentials。
Q: 页面刷新后 Token 还在吗?
A: 不在。Token 只存内存(不写 localStorage,避免被 XSS 读取)。页面刷新后请重新 init() 传入,
或只配 getToken 让 SDK 自动重取。
Q: 如何让用户只能使用特定角色?
A: 在 API Key 上绑定角色(PUT /api-key/{id}/roles),SDK 初始化时只传入绑定的角色 ID。
Q: 多个第三方系统可以用同一个 API Key 吗? A: 可以,但建议每个系统使用独立的 API Key,便于独立管控频率限制和角色权限。
9. 安全建议
- API Key 仅在后端使用:永远不要将 API Key 暴露到前端代码中(
getToken回调应指向你自己的后端,由它持 Key 去换 Token) - Token 短有效期:生产环境建议 TTL 设为 1-2 小时。SDK 已内置自动刷新(临期 + 401 双触发),短有效期不再影响长会话体验
- Token 不落盘:SDK 只在内存中持有 Token,不写 localStorage/sessionStorage;页面刷新后由宿主重新提供
- 角色最小权限:只绑定必要的客服角色,避免授予过多权限
- 频率限制:根据业务量设置合理的 rateLimit(默认 60 次/分钟)
- HTTPS:生产环境必须使用 HTTPS,防止 Token 被中间人截获
- 定期轮换 Key:定期吊销旧 Key 并创建新 Key
- 接好
onError:auth_expired/auth_refresh_failed是排查鉴权问题的第一手线索(控制台日志已关闭,不接回调等于静默失败)
10. 验证 Token 自动刷新
在浏览器控制台(或测试页 http://localhost:9090/sdk/test.html)按以下步骤可验证四种路径。
全程打开 DevTools 的 Network 面板,过滤 Authorization 请求头与 /open-api/auth/token。
// 公共:一个真实的换 Token 实现(把 sk_xxx 换成你的 API Key)
let providerCalls = 0;
const provider = async () => {
providerCalls++;
const r = await fetch('/open-api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': 'sk_xxx' },
body: JSON.stringify({ ttl: 7200 }),
});
const d = await r.json();
return { token: d.token, expiresIn: d.expiresIn };
};
| # | 验证项 | 操作 | 预期 |
|---|---|---|---|
| 1 | setToken 生效 |
init({ token: 'invalid.jwt', ... }) 发一条消息;再 ChatbotSDK.setToken('<真实 token>', 7200) 发一条 |
第一条弹「鉴权失败」气泡;第二条正常回复;Network 里 Authorization 头变为真实 token |
| 2 | 401 自动重试 | init({ token: 'invalid.jwt', getToken: provider, ... }) 发一条消息 |
Network 上「一条 401 → 一条 200」;providerCalls === 1;无错误气泡;onError 未触发 |
| 3 | 防死循环 | init({ token: 'invalid.jwt', getToken: async () => 'invalid.jwt', ... }) 反复发消息 |
只发 1 个请求、不重试;onError 收到 code === 'auth_expired';30 秒内不重复上报 |
| 4 | 临期主动刷新 | init({ token: '<真实 token>', expiresIn: 20, getToken: provider, ... }) 后不做任何操作 |
约 18 秒后 Network 出现一条新的 /open-api/auth/token(expiresIn: 20 时临期阈值为 2 秒) |
| 5 | destroy 清理 | 承上,等 providerCalls 增长后立刻 ChatbotSDK.destroy(),等 1 分钟 |
providerCalls 不再增长;再 init() 一次功能恢复正常 |
| 6 | 非受保护路径不受影响 | 观察 /category/tree、/ai/system-config/disclaimer 请求 |
不带 Authorization 头,也不会因临期检查而变慢 |
| 7 | always 模式每请求前取 |
init({ tokenStrategy: 'always', tokenUrl: '/api/sdk-token', ... }),连发两条消息 |
每条消息的 /ai/chat/stream 之前都各有一条取 Token 请求(Token 未过期时也照取) |
| 8 | 熔断不拖慢请求 | 把 tokenUrl 指向一个 404 地址后发消息 |
首次失败 onError 收到 auth_refresh_failed;随后 60 秒内不再出现取 Token 请求,消息仍照常发出(走 401 兜底) |
若
getToken返回裸字符串(不带expiresIn),第 4 项不会发生 —— 这是预期行为: 没有过期信息时只能靠 401 驱动(第 2 项)。生产环境建议始终返回{ token, expiresIn }。
附录:完整对接示例(Node.js)
// ==================== 第三方后端示例 ====================
const express = require('express');
const app = express();
const AI_DOMAIN = 'https://your-ai-domain.com';
const API_KEY = 'sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
// Token 缓存
let cachedToken = null;
let tokenExpireAt = 0;
let cachedRoles = [];
// 换取 Token(带缓存)
// 说明:SDK 侧已有自动刷新,这里的缓存是为了避免同一时刻多个用户各换一次 Token。
// 缓存窗口设为「有效期 - 5 分钟」,与 SDK 的临期阈值对齐,可保证 SDK 来取时拿到的是新 Token。
async function getSdkToken() {
if (cachedToken && Date.now() < tokenExpireAt) {
return { token: cachedToken, expiresIn: Math.round((tokenExpireAt - Date.now()) / 1000) };
}
const res = await fetch(`${AI_DOMAIN}/open-api/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
},
body: JSON.stringify({ ttl: 7200 })
});
const data = await res.json();
if (!data.success) throw new Error(data.message);
cachedToken = data.token;
tokenExpireAt = Date.now() + (data.expiresIn - 300) * 1000; // 提前 5 分钟刷新
cachedRoles = data.roles;
return { token: data.token, expiresIn: data.expiresIn };
}
// 给前端提供 Token(前端在 init 时取一次,之后由 SDK 自动调用续期)
app.get('/api/chatbot/token', async (req, res) => {
try {
const data = await getSdkToken();
res.json({ success: true, ...data, roles: cachedRoles });
} catch (e) {
res.status(500).json({ success: false, message: e.message });
}
});
app.listen(3000);
<!-- ==================== 第三方前端示例 ==================== -->
<script src="https://your-ai-domain.com/sdk/chatbot-sdk.js"></script>
<script>
// 从自己后端换取 Token(后端持 API Key,API Key 永远不进浏览器)
async function fetchToken() {
const res = await fetch('/api/chatbot/token');
const data = await res.json();
if (!data.success) throw new Error(data.message || 'Token 换取失败');
return data;
}
(async () => {
const first = await fetchToken();
ChatbotSDK.init({
integrateId: first.roles[0].id, // 默认使用第一个角色
requestDomain: 'https://your-ai-domain.com',
token: first.token, // 首次 Token
expiresIn: first.expiresIn, // 有效期(秒)→ SDK 据此在临期前主动刷新
getToken: async () => { // SDK 在临期 / 收到 401 时自动调用它续期
const next = await fetchToken();
return { token: next.token, expiresIn: next.expiresIn };
},
roles: first.roles, // 传入多个角色时自动显示角色选择器
userId: 'current_user_id',
title: '在线客服',
theme: 'light',
quickReplies: ['如何退款?', '查看订单', '联系人工'],
onError: (e) => {
// 鉴权类问题看这两个 code;其余错误可忽略或上报监控
if (String(e.code).startsWith('auth_')) console.error('鉴权异常', e.code, e.message);
}
});
})();
</script>