super-agent-sdk
v1.0.27
Published
Super Agent Web SDK — API + 嵌入式聊天组件
Readme
super-agent-sdk
Super Agent Web SDK —— 为 super-agent-service 提供的一站式浏览器端集成方案:既包含纯 JS 的 API 调用层(鉴权、SSE 流式解析、会话与消息接口),也包含开箱即用的嵌入式 React 聊天组件。
目录
1. 概述
super-agent-sdk 提供两个入口:
| 入口 | 说明 | 依赖 |
| ------------------------ | -------------------------------------------------------- | ---------- |
| super-agent-sdk | 纯 JS API 调用层,封装鉴权、SSE 流式解析、会话与消息接口 | 无 UI 依赖 |
| super-agent-sdk/widget | 可嵌入式 React 聊天组件 | React 18+ |
核心能力:
- 纯 API 模式:
createSuperAgent()返回轻量 SDK 实例,提供流式聊天、会话列表、历史消息、Token 刷新等能力。 - 嵌入式组件:
mount()挂载 React 聊天组件。 - 两种展示模式:
floating(右下角气泡 + 弹窗面板)与fullpage(整页 DeepSeek 风格布局)。 - Markdown 渲染:内置
react-markdown+ GFM(表格 / 任务列表 / 删除线),Claude 风格排版,默认转义防 XSS(见 6.3)。 - 会话持久化:自动加载会话列表、
localStorage记住最近会话、刷新后自动恢复。 - 附件上传(NOS 直传):内置 NOS 直传(分片 + 断点续传 + 进度),📎 选文件随消息发送,沙箱 Agent 可读取文件内容(详见 5.12 / 6.1)。
2. 安装
pnpm add super-agent-sdk纯 API 模式无需任何额外依赖。若使用聊天组件(widget),需同时安装 React:
pnpm add react react-dom
react/react-dom以peerDependencies(可选)声明,版本要求>=18.0.0。
3. 快速开始
最简 5 行接入:
import { createSuperAgent } from "super-agent-sdk";
import { mount } from "super-agent-sdk/widget";
const sdk = createSuperAgent({
botCode: "tech-researcher",
tokenGateway: "/api/gateway",
});
// Token 自动获取,无需手动设置
const widget = mount("#chat", { sdk });
widget.open();挂载完成后,页面右下角出现聊天气泡,点击展开面板即可开始对话。
4. SDK 初始化
function createSuperAgent(config: SDKConfig): SuperAgentSDK;export interface SDKConfig {
botCode: string; // Bot 业务编码(必填,bots.code,跨环境稳定)
tokenGateway: string; // Token 网关前缀(必填,不带 host),如 "/api/gateway";token URL = {origin}{tokenGateway}/superagent/v2/token/generate
baseUrl?: string; // 业务接口前缀(可选,默认 "/api/agent/runtime/v1"),不带 host
appId?: string; // 可选,不传则从 token 接口响应自动获取
token?: string; // 可选,不传则自动调 token 接口获取
tenantId?: number; // 可选:token 网关需租户上下文时传(如 Playground 走 admin 网关),fetchToken 带 X-Tenant-Id 头透传;自建公开网关不传
loginUrl?: string; // 可选:cookie 失效重登的兜底跳转页(agent 含 COOKIE 工具时 runtime 校验 cookie,失效 401 cookie_expired)
onAuthExpired?: (ctx: { loginUrl?: string; sessionId?: string; botCode: string }) => void; // 可选:完全自定义重登(如带 returnUrl 的 SSO),提供则 SDK 不自行跳转
}只需提供 botCode 与 tokenGateway 即可。baseUrl 默认为 /api/agent/runtime/v1,appId 从 token 接口响应中自动获取。SDK 不要求在创建时传入 token——组件挂载后会自动调用 sdk.getToken() 获取 token 并内部保存,无需手动设置。getToken() 自带并发去重:多 widget 共享同一 sdk 实例时,并发取 token 合并为一次签发请求;业务接口 401 时自动清缓存重签并重试一次。
const sdk = createSuperAgent({
botCode: "tech-researcher",
tokenGateway: "/api/gateway",
});凭证获取流程:
createSuperAgent({ botCode, tokenGateway })
↓
Widget 挂载时自动调用 sdk.getToken()
↓
POST {origin}{tokenGateway}/superagent/v2/token/generate(body: { botCode })
↓
后端返回 { token, appId }
↓
SDK 内部自动保存 token + appId
↓
readytokenGateway 用于拼接 token 接口路径,appId 自动从 token 响应获取(也可手动传入覆盖)。
cookie 失效重登(agent 含 COOKIE 工具时):runtime 入口校验浏览器 cookie,失效返回 401 {code:"cookie_expired", data:{login_url, session_id}}。SDK 暂存 pending 消息(含 botCode)后跳转重登(优先响应里的 login_url,其次配置的 loginUrl;提供 onAuthExpired 则完全交宿主处理);重登回来后 widget 恢复同一会话(botCode 定位)并自动重发暂存消息。
5. API 参考
字段命名:后端接口返回 camelCase 字段,SDK 直接使用,无需转换。
- 后端返回
sessionId、createTime、updateTime(bot 外部标识统一 botCode,SDK 不消费内部 id)- SDK 所有公开 API 均使用 camelCase
5.1 SDK 实例
export interface SuperAgentSDK {
readonly botCode: string; // 当前 Bot Code(bots.code,跨环境稳定)
createSession(): Promise<string>;
chat(options: ChatOptions): AbortController;
listConversations(
params?: ListConversationsParams,
): Promise<ListConversationsResult>;
renameConversation(sessionId: string, title: string): Promise<void>;
deleteConversation(sessionId: string): Promise<void>;
getMessages(sessionId: string, options?: { isToolCallVisible?: (toolCode?: string, toolName?: string) => boolean }): Promise<UIMessage[]>;
getToken(): Promise<string>;
setToken(token: string): void;
feedback(
messageId: string,
type: "like" | "dislike",
options?: { reason?: number; remark?: string },
): Promise<void>;
cancelFeedback(messageId: string): Promise<void>;
respondInterrupt(
interruptId: string,
sessionId: string,
response: InterruptResponse,
onMessage?: (event: ChatEvent) => void,
onError?: (error: Error) => void,
): Promise<void>;
listSkills(): Promise<SkillInfo[]>;
stopGeneration(sessionId: string): Promise<void>;
}5.2 createSession()
创建新会话,返回 sessionId。聊天前必须先创建会话。
- SDK 调用
POST /chat/sessions,请求体为{ botCode } - 后端返回
{ sessionId: "..." } - SDK 直接返回
sessionId字符串
const sessionId = await sdk.createSession();5.3 chat()
流式聊天。sessionId 为必填,必须先调用 createSession() 获取。返回 AbortController,可随时中断。
- SDK 发送
POST /chat,请求体为{ botCode, sessionId, message, stream } - 后端 SSE 在
done事件中返回sessionId
export interface ChatOptions {
sessionId: string; // 必填 — 先调用 createSession()
message: string;
/** 附件(≤10 个,仅沙箱模式 Agent 支持,其他模式后端返回 400)。需先经直传上传到 NOS 拿到 nosKey。 */
attachments?: ChatAttachment[];
stream?: boolean; // 默认 true
onMessage?: (event: ChatEvent) => void;
onError?: (error: Error) => void;
onDone?: (result: {
sessionId: string;
content: string;
messageId?: string;
roundId?: string; // 本轮对话 id(session.start/session.done 携带)
artifacts?: Artifact[]; // done 汇总的本轮文件产物(generate_file 等交付产物)
}) => void; // messageId:done/stop 事件带回的后端消息标识(= conversation_messages.message_id,feedback 按它定位;未带回时 SDK 回查历史兜底)
signal?: AbortSignal;
}
export interface ChatAttachment {
nosKey: string; // NOS 对象 key(必填,直传后获得)
filename: string; // 原始文件名(必填,模型靠扩展名选择解析方式)
size?: number; // 字节数(后端落地时校验)
mime?: string; // MIME 类型,如 "application/pdf"
url?: string; // 下载链接(仅前端回显;发送时 SDK 自动剥离,不进后端)
}带附件发送(附件文件会由后端写入沙箱,Agent 可通过 read_file / execute 读取):
const attachment = await createNosUploader({ baseURL: "/api/e5s" })(file); // 见 5.12
const controller = sdk.chat({
sessionId,
message: "总结这份报告",
attachments: [attachment],
onMessage, onDone, onError,
});const sessionId = await sdk.createSession();
const controller = sdk.chat({
sessionId,
message: "帮我写一段排序算法",
stream: true, // 默认 true
onMessage: (event) => {
switch (event.type) {
case "thinking":
console.log("[思考]", event.content);
break;
case "ai":
process.stdout.write(event.content); // 增量输出
break;
case "tool_call":
console.log("[调用工具]", event.toolName, event.args);
break;
case "tool_result":
console.log("[工具结果]", event.content);
break;
}
},
onDone: ({ sessionId, content }) => {
console.log("\n完成,sessionId =", sessionId);
},
onError: (error) => {
console.error("出错:", error.message);
},
});
// 取消:controller.abort();stream: false 时走非流式接口,onDone 收到完整回复:
sdk.chat({
sessionId,
message: "你好",
stream: false,
onDone: ({ sessionId, content }) => console.log("完整回复:", content),
onError: (error) => console.error(error),
});5.4 listConversations()
export interface ListConversationsParams {
page?: number; // 默认 1
size?: number; // 默认 20
}
export interface ListConversationsResult {
items: Conversation[];
total: number;
}
export interface Conversation {
sessionId: string;
title: string | null;
createTime: string;
updateTime: string;
}const { items, total } = await sdk.listConversations({ page: 1, size: 20 });- SDK 调用
GET /chat/conversations?botCode=tech-researcher&page=1&size=20 - 后端返回的每一项为
{ sessionId, title, createTime, updateTime }(bot 外部标识统一 botCode,SDK 不暴露内部 id)
5.5 getMessages()
拉取会话历史消息,已转换为 UI 结构:
const messages: UIMessage[] = await sdk.getMessages(sessionId);后端历史接口按轮次聚合返回 [{ roundId, artifacts, data: MessageItem[] }],SDK 自动摊平为
UIMessage[]:每组的轮级 artifacts(与 SSE done 同构)挂到该组最后一条 assistant 消息的
artifacts;roundId 为 null(存量旧数据)时 SDK 随机生成,业务方无需处理。
可选第二参注入工具过滤(与 widget 的 toolCallDisplay.hidden/visibleTools 同一谓词,纯 API 模式自行控制):
const messages = await sdk.getMessages(sessionId, {
// 返回 false 的工具调用/结果不出现在历史里;工具 id(toolCode)优先、展示名兜底
isToolCallVisible: (toolCode, toolName) => toolCode !== "query_order",
});5.6 renameConversation() / deleteConversation()
await sdk.renameConversation(sessionId, "关于部署的讨论");
await sdk.deleteConversation(sessionId);5.7 getToken()
sdk.getToken(): Promise<string>自动获取并保存访问 token:
- SDK 调用
POST {origin}{tokenGateway}/superagent/v2/token/generate,请求体{ botCode } - 后端返回
{ token, appId } - SDK 自动将 token + appId 保存在内部
- Widget 在挂载时自动调用此方法 —— 通常无需手动调用
const token = await sdk.getToken();5.8 setToken()
sdk.setToken(token: string): void手动设置 token(用于 token 由外部获取、或测试等场景)。正常情况下无需调用,Widget 挂载时会自动通过 getToken() 获取 token。
sdk.setToken("<TOKEN>");5.9 feedback() / cancelFeedback()
消息反馈(赞/踩)。调用 sdk.feedback() 发送 POST /chat/messages/{messageId}/feedback;调用 sdk.cancelFeedback() 发送 DELETE 取消反馈。
messageId 说明:即消息列表里的
messageId(后端conversation_messages.message_id,langchain 原始消息 id,UUID 字符串),不是UIMessage.id(历史加载为表主键、流式为客户端临时 id,仅作渲染 key)。纯 API 模式从getMessages()结果取UIMessage.messageId;流式消息由done/stop事件(onDone的messageId)带回,两者都有时以帧内为准。
const messages = await sdk.getMessages(sessionId);
const aiMsg = messages.findLast((m) => m.role === "assistant");
// 点赞
await sdk.feedback(aiMsg.messageId!, "like");
// 踩(可附原因 + 备注)
await sdk.feedback(aiMsg.messageId!, "dislike", {
reason: 1, // 1=事实错误 2=逻辑问题 3=不相关 4=信息过时 5=冗长啰嗦 6=难以理解
remark: "时间描述有误",
});
// 取消反馈
await sdk.cancelFeedback(aiMsg.messageId!);组件内置的点赞/踩按钮已自动调用 feedback()/cancelFeedback():
- 点击已选中的按钮 → 取消(DELETE)
- 点击另一个按钮 → 切换(POST)
- 点踩时弹出原因选择面板(可选填原因 + 备注后提交)
- 消息本身没带
messageId时(如后端 done/stop 帧未回传),SDK 自动拉取一次历史消息,按该消息的roundId定位到它并取messageId再发送,业务方无感知
5.10 respondInterrupt()
响应中断事件(Human-in-the-Loop),后端继续推送后续 SSE 事件:
sdk.respondInterrupt(
interruptId: string,
sessionId: string,
response: InterruptResponse,
onMessage?: (event: ChatEvent) => void,
onError?: (error: Error) => void
): Promise<void>Widget 内置中断卡片会自动调用此方法,纯 API 模式需手动处理(详见 12. Human-in-the-Loop)。
5.11 listSkills()
获取当前 Bot 绑定的技能列表(用于 / 斜杠命令选择技能):
const skills: SkillInfo[] = await sdk.listSkills();
// [{ code: "web-searcher", name: "web-searcher", displayName: "网页搜索", description: "..." }]5.12 createNosUploader()
内置 NOS 直传上传器(协议对齐 @netease-ehr/ui Upload 的 nosUpload 路径,零依赖该私有包):nosKey = MD5(name:size:ts) → GET {baseURL}/nos/token → nos-js-sdk 分片直传 NOS 边缘节点(默认 4MB 分片 + localStorage 断点续传)→ GET {baseURL}/nos/url 取下载地址。内置挂起防护(分片超时 120s + 无进度看门狗 130s)。
import { createNosUploader } from "super-agent-sdk";
export interface NosUploaderConfig {
baseURL: string; // 业务网关(与业务系统接口网关一致,如 "/api/e5s",同源相对路径)
headers?: Record<string, string>; // 网关鉴权头(token/url 两个 GET 携带)
trunkSize?: number; // 分片大小,默认 4MB
onProgress?: (percent: number) => void; // 上传进度 0-100
chunkTimeoutMs?: number; // 单分片超时,默认 120s
stallTimeoutMs?: number; // 无进度看门狗,默认 130s
fetchNosUrl?: boolean; // 上传后调 /nos/url 取下载地址,默认 true(失败降级不影响结果)
}
const upload = createNosUploader({
baseURL: "/api/e5s",
onProgress: (p) => console.log(`${p}%`),
});
const attachment = await upload(file); // → { nosKey, filename, size, mime, url }Widget 场景无需手动调用——
mount({ nosUpload: { baseURL } })一行开启(见 6.1)。
5.13 后端接口对照
SDK 方法 → 后端接口的完整映射:
除
getToken()外,其余接口请求均携带X-App-Id与X-Token请求头进行鉴权。
| SDK 方法 | HTTP 请求 | 请求体 | 后端返回 |
| --------------------- | -------------------------------------------- | --------------------------------------- | ----------------------------------------------- |
| getToken() | POST {gateway}/superagent/v2/token/generate | { botCode } | { token, appId } |
| createSession() | POST /chat/sessions | { botCode } | { sessionId } |
| chat() | POST /chat | { botCode, sessionId, message, stream, attachments? } | SSE 流,done/stop 事件含 sessionId |
| listConversations() | GET /chat/conversations | 查询参数 botCode、page、size | { items: [{ sessionId, ... }], total }(按 botCode 过滤) |
| feedback() | POST /chat/messages/{messageId}/feedback | { type, reason?, remark? } | null |
| cancelFeedback() | DELETE /chat/messages/{messageId}/feedback | 无 body | null |
| respondInterrupt() | POST /chat/interrupt/{interruptId}/respond | { sessionId, action, value? } | SSE 流(后续事件) |
| stopGeneration() | POST /chat/stop | { sessionId } | { stopped: "pending", sessionId } |
| listSkills() | GET /chat/bots/{botCode}/skills | — | [{ code, name, displayName, description }] |
6. 聊天组件
组件通过 mount() 从 super-agent-sdk/widget 导入。
6.1 mount()
export function mount(
target: string | HTMLElement,
options: MountOptions,
): WidgetInstance;
export interface MountOptions {
sdk: SuperAgentSDK;
mode?: WidgetMode; // 默认 'floating'
theme?: ThemeConfig;
slots?: Slots;
hooks?: EventHooks;
welcomeMessage?: string;
suggestedPrompts?: string[];
title?: string; // 标题,默认 "AI Assistant"
sidebarDefaultOpen?: boolean; // fullpage 模式:侧边栏初始展开(默认 true)
avatar?: AvatarConfig; // 自定义头像(AI 消息始终有内置默认头像;用户头像未配置则不显示)
artifactPreview?: boolean; // 产物预览总开关(html/markdown/pdf/docx 预览 + 下载 + 全屏),默认 false
capabilities?: CapabilityItem[]; // floating 首页:能力清单(icon + 标签 + 描述)
quickLinks?: QuickLinkItem[]; // floating 首页:快速入口
logo?: string; // floating 首页居中 Logo(URL),默认 SDK 内置 Logo
triggerLogo?: string; // floating 触发按钮 Logo(URL),默认 SDK 内置 Logo
showTimestamp?: boolean; // 显示消息时间戳,默认 false 不显示
/**
* 工具调用展示控制(单一配置面):显示开关 + 白名单。
* 未配置 = 全显示(现状)。工具条恒收起、点击展开(无流式自动展开)。
*/
toolCallDisplay?: {
/** 隐藏工具调用与返回(tool_call + tool_result 双向过滤);默认 false 全显示 */
hidden?: boolean;
/** 白名单:其中的工具照常显示,其余全藏。条目为工具 id(code,admin 工具管理-编码列),
* 兼容展示名匹配(忽略大小写)。仅在 hidden: true 时生效;缺省/空 = 全藏 */
visibleTools?: string[];
};
/** 附件上传器(完全自定义,优先于 nosUpload)。不配置则输入区无附件按钮。 */
attachmentUploader?: (file: File) => Promise<ChatAttachment>;
/** 内置 NOS 直传(推荐):配置即用,📎 按钮 + chip 进度 + 断点续传全内置 */
nosUpload?: { baseURL: string; headers?: Record<string, string>; trunkSize?: number };
/**
* 思维链 ThoughtChain 式节点:步骤默认收起、点击展开,
* 收起条显示「已深度思考 · 用时 Ns」(仅 live 轮有耗时,历史回显无数据)。
* 对象出现(含 {})即开启;子项仅覆盖(如 { showDuration: false })。
*/
collapse?: {
thinking?: { showDuration?: boolean };
};
}
export interface CapabilityItem {
icon?: string; // 图标 URL
label: string; // 能力名称
desc?: string; // 能力描述(超长自动省略)
}
export interface QuickLinkItem {
icon?: string; // 图标 URL
label: string; // 入口名称
prompt?: string; // 点击后发送的消息
action?: PanelAction; // 点击行为:整屏容器 / 外链(优先于 prompt)
onClick?: () => void; // 自定义点击行为(优先于 action / prompt)
}
export interface WidgetInstance {
open(): void;
close(): void;
destroy(): void;
}import { createSuperAgent } from "super-agent-sdk";
import { mount } from "super-agent-sdk/widget";
const sdk = createSuperAgent({
botCode: "tech-researcher",
tokenGateway: "/api/gateway",
});
const widget = mount("#chat-root", {
sdk,
mode: "floating",
title: "AI 助手",
welcomeMessage: "你好!有什么可以帮你的?",
suggestedPrompts: ["帮我写周报", "解释一下这段代码"],
theme: { primaryColor: "#6366f1" },
});
widget.open(); // 展开面板
widget.close(); // 收起面板
widget.destroy(); // 卸载并移除 DOM挂载后默认是收起状态,需调用
widget.open()展开。
附件上传(📎 按钮)
配置 nosUpload(内置 NOS 直传)或 attachmentUploader(完全自定义,优先级更高)任一即启用,二者都不配则附件功能完全隐藏(存量接入零影响):
// 内置直传(推荐,一行开启)
mount("#chat-root", {
sdk,
nosUpload: { baseURL: "/api/e5s" }, // 业务网关
});
// 完全自定义(其他对象存储 / 自建网关)
mount("#chat-root", {
sdk,
attachmentUploader: async (file) => ({
nosKey: await uploadSomewhere(file),
filename: file.name,
size: file.size,
mime: file.type,
}),
});内置行为:📎 按钮(输入框左下角,绝对定位)选文件(≤10 个)→ 待发送附件以 chip 托盘显示在输入框上方(不占输入框内部空间),实时显示上传百分比 → 上传完成显示文件大小(可点击下载)→ 随消息发送 → 沙箱 Agent 读取(仅沙箱模式 Agent 支持,其他模式后端 400)。上传中禁止发送,失败 chip 红标可移除重选;regenerate 会连同附件一起重发。
6.2 源码接入(monorepo alias)注意事项
Widget 内部使用 Tailwind CSS(utility 全部限定在 [data-super-agent-widget] 宿主选择器内、禁用 preflight,不影响宿主页样式)。分发包(dist)已内联全部样式;内置图片资产(Logo/图标/头像等)走 CDN 绝对路径(src/widget/assets/index.ts 的 BASE_URL),npm 引入无需任何配置(需能访问 CDN)。
若宿主工程通过 vite alias 直接引用 SDK 源码(如本仓库 frontend 的做法),则 SDK 的样式会由宿主自己的 PostCSS/Tailwind 管线处理,需要满足:
- 宿主
tailwind.config的content包含 SDK 源码路径,否则 widget 的 utility 类不会生成:
// frontend/tailwind.config.ts
export default {
content: [
'./index.html',
'./src/**/*.{js,jsx,ts,tsx}',
'../sdk/src/**/*.{ts,tsx}', // SDK 源码
],
// ...
};- 无需在宿主 config 中镜像 SDK 的
important: '[data-super-agent-widget]'(那会把宿主自己的 utility 也锁进 widget 容器内);宿主管线生成的 utility 为普通类,widget DOM 照常命中,动画/自定义类由 SDK 自带的tailwind.css(运行时注入)保证。
6.3 Markdown 渲染(AI 消息)
AI 文本消息(text part)按 Markdown 渲染,内置 react-markdown + remark-gfm(随 SDK 打包,宿主无需额外安装):
- 语法:CommonMark 全集 + GFM 扩展(表格、任务列表
- [ ]、删除线~~x~~、自动链接) - 排版:Claude 风格,色值对齐前端设计 tokens(
frontend/src/theme/tokens.ts)——链接品牌橙#d97757(hover 加深)、行内代码parchment #e8e6dc底、代码块card #efede6底 +border #e3e1d8hairline 边框(浅色暖调、横向滚动)、引用块clay左点缀、表头parchment底 - 安全:默认 HTML 转义 + URL 清洗(
urlTransform),Markdown 注入不会产生 XSS - 流式友好:未闭合语法(输出中的代码块 / 表格)渐进渲染不跳版
如需完全自定义渲染,通过 slots.TextPart 替换(见 8.3 组件插槽);renderMode === "html" 的工具结果不受影响,仍走 DOMPurify 内联渲染。
7. 展示模式
通过 mode 切换两种展示模式:
| 模式 | 说明 |
| ------------------ | ---------------------------------------------------------- |
| floating(默认) | 右下角 Logo 触发按钮 + 可拖拽/拉伸/最大化的浮窗聊天窗口 |
| fullpage | 整页布局,左侧会话栏(hr-for-help 风格)+ 右侧聊天区 |
mount("#chat-root", { sdk, mode: "fullpage" });fullpage 模式下,可通过 sidebarDefaultOpen 控制侧边栏初始展开状态:
mount("#chat-root", { sdk, mode: "fullpage", sidebarDefaultOpen: false });7.1 浮窗模式(floating)交互
浮窗窗口对齐 hr-for-help 设计,开箱具备以下交互能力:
- 标题栏拖拽:按住顶部导航栏移动窗口(视口内钳制,按钮区域不触发)
- 左侧拉伸调宽:窗口左侧 6px 手柄拖拽(右缘稳定不动),宽度范围 360 ~ 800 px,拖超过 800 自动进入大窗
- 大窗/小窗切换:header 右侧「大窗」按钮切换;大窗为底部弹出的近全屏 overlay(内容区 800px 居中),回小窗时宽度重置为默认值
- 内容自适应高度:聊天内容增长时窗口自动变高(默认 648px,上限 95% 视口,超出后消息区滚动),回到首页恢复默认高度
- 内嵌会话侧边栏:header「历史记录」按钮开合;小窗下侧边栏紧贴窗口左缘向外展开(无缝拼接为一个连续圆角窗口,主区位置不动);大窗模式自动展开(220px),大窗/小窗各自记住用户偏好
- 关闭即还原:关闭窗口后几何状态(位置/宽度/大窗态/高度)全部重置
浮窗消息展示对齐 hr-for-help:AI 头像独占一行(无背景色,生成中切换动效头像)、气泡白底描边、用户气泡浅蓝右对齐;fullpage 模式保持横排头像 + 经典气泡,两者互不影响。
首页(无消息时)包含:居中 Logo(浮动动画)+ 流光渐变标题 + 能力清单 + 建议词 chips + 快速入口,内容通过 capabilities / suggestedPrompts / quickLinks / logo 配置(见 6.1;icon 可复用 super-agent-sdk/widget 导出的内置 ASSETS)。
8. 定制化
8.1 主题定制
通过 ThemeConfig 定制外观,组件内部将其转换为 CSS 变量。
export interface ThemeConfig {
primaryColor?: string; // 默认 #6366f1
backgroundColor?: string; // 默认 #ffffff
fontFamily?: string; // 默认 system-ui, -apple-system, sans-serif
borderRadius?: number; // 默认 16 (px)
panelWidth?: number; // floating 初始宽度,默认 400 (px)
panelHeight?: number; // floating 初始高度,默认 648 (px)
zIndex?: number; // 默认 9999
}对应的 CSS 变量:
| 变量 | 默认值 | 对应字段 |
| ------------------- | -------------------------------------- | ----------------- |
| --sa-primary | #6366f1 | primaryColor |
| --sa-bg | #ffffff | backgroundColor |
| --sa-font | system-ui, -apple-system, sans-serif | fontFamily |
| --sa-radius | 16px | borderRadius |
| --sa-panel-width | 400px | panelWidth |
| --sa-panel-height | 648px | panelHeight |
| --sa-z-index | 9999 | zIndex |
mount("#chat-root", {
sdk,
theme: {
primaryColor: "#10b981",
backgroundColor: "#f9fafb",
fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif",
borderRadius: 12,
panelWidth: 400,
panelHeight: 600,
zIndex: 10000,
},
});8.2 头像定制
通过 avatar 配置助手与用户的头像,支持图片 URL、emoji 或文本。以 http(s)://、data:、blob:、/、./、../、// 开头的字符串渲染为图片,其余作为 emoji/文本展示(无背景色):
export interface AvatarConfig {
assistant?: string; // 图片 URL、emoji 或文本;默认 SDK 内置头像(生成中显示动效头像)
user?: string; // 图片 URL、emoji 或文本;未配置则不显示用户头像
}mount("#chat-root", {
sdk,
avatar: {
assistant: "🤖", // 配置后覆盖内置默认头像
user: "https://cdn.example.com/user-avatar.png",
},
});8.3 组件插槽(Slots)
通过 slots 替换内置子组件。所有插槽 Props 均从 super-agent-sdk/widget 导出。
export interface Slots {
Trigger?: ComponentType<TriggerProps>;
Header?: ComponentType<HeaderProps>;
Message?: ComponentType<MessageProps>;
Composer?: ComponentType<ComposerProps>;
ActionBar?: ComponentType<ActionBarProps>;
ThreadList?: ComponentType<ThreadListProps>;
ThinkingPart?: ComponentType<ThinkingPartProps>;
TextPart?: ComponentType<TextPartProps>;
ToolCallPart?: ComponentType<ToolCallPartProps>;
ToolResultPart?: ComponentType<ToolResultPartProps>;
ErrorPart?: ComponentType<ErrorPartProps>;
InterruptCard?: ComponentType<InterruptCardProps>;
WelcomeScreen?: ComponentType<WelcomeScreenProps>;
/** 首屏底部业务定制容器 */
AppendContainer?: ComponentType<AppendContainerProps>;
}| 插槽 | Props 接口 | 说明 |
| ---------------- | --------------------- | -------------------------------------------------------------------------------------------------- |
| Trigger | TriggerProps | 悬浮触发按钮 |
| Header | HeaderProps | 面板标题栏 |
| Message | MessageProps | 单条消息容器(完整自定义消息渲染) |
| Composer | ComposerProps | 输入框 / 发送栏 |
| ActionBar | ActionBarProps | 消息操作栏(复制 / 重试 / 反馈) |
| ThreadList | ThreadListProps | 会话列表(浮窗模式覆盖层 / 全屏模式侧边栏) |
| ThinkingPart | ThinkingPartProps | 思考过程块 |
| TextPart | TextPartProps | 文本块 |
| ToolCallPart | ToolCallPartProps | 工具调用卡片(可折叠 + 展开后复制参数) |
| ToolResultPart | ToolResultPartProps | 工具返回卡片(pre 滚动 + 复制,JSON 自动格式化;renderMode==="html" 时 DOMPurify 清洗后直接渲染,最高 60vh 滚动。文件产物卡片已上移至消息级渲染,本组件不再内联展示) |
| ErrorPart | ErrorPartProps | 错误提示块 |
| WelcomeScreen | WelcomeScreenProps | 空会话欢迎页 |
| AppendContainer| AppendContainerProps| 首屏底部业务定制区,openPanel(action) 开整屏容器或外链(见下) |
Trigger
export interface TriggerProps {
isOpen: boolean;
onClick: () => void;
unreadCount?: number;
triggerLogo?: string; // 触发按钮 Logo(URL)
}function MyTrigger({ isOpen, onClick, unreadCount }: TriggerProps) {
return (
<button
onClick={onClick}
style={{ position: "fixed", right: 24, bottom: 24 }}
>
{isOpen ? "关闭" : "聊天"}
{unreadCount ? <span>{unreadCount}</span> : null}
</button>
);
}Header
floating 与 fullpage 模式均支持替换;onToggleThreadList 在 floating 下切换内嵌会话侧边栏、在 fullpage 下切换左侧边栏;开启 artifactPreview 后额外提供产物入口字段。
注:fullpage 内置 Header 有标题与产物入口(无关闭按钮);整屏容器态标题换成面板
title、并出现 home 返回入口,需要关闭入口时通过自定义 Header 的onClose自行渲染。floating 内置 Header 含历史/回首页/大窗/关闭按钮。
export interface HeaderProps {
title: string;
onClose: () => void;
onToggleThreadList: () => void;
isMaximized?: boolean; // floating:大窗状态
onToggleMaximize?: () => void; // floating:大窗/小窗切换
showHome?: boolean; // floating:聊天态显示回到首页;整屏容器态显示「返回聊天」
onHome?: () => void; // 回到首页(新会话);整屏容器态为 closePanel 返回聊天
artifactCount?: number; // 会话产物数量(artifactPreview 开启时提供)
onToggleArtifactDrawer?: () => void; // 打开/关闭产物预览抽屉
panelTitle?: string; // 整屏容器态:面板标题(内置 Header 居中显示;自定义 Header 据此自行渲染)
}function MyHeader({ title, onClose, onToggleThreadList }: HeaderProps) {
return (
<div
style={{
display: "flex",
alignItems: "center",
padding: 12,
background: "#111",
}}
>
<span style={{ flex: 1, color: "#fff", fontWeight: 600 }}>{title}</span>
<button onClick={onToggleThreadList}>历史</button>
<button onClick={onClose}>关闭</button>
</div>
);
}Message(重点:完整自定义消息渲染)
替换后接管整条消息的渲染,可完全自定义布局。slots 会透传进来,方便复用内置/自定义的 Part 组件。
export interface MessageProps {
message: UIMessage;
isStreaming: boolean;
isLast: boolean;
avatar?: AvatarConfig;
slots: Slots;
onCopy: () => void;
onRegenerate: () => void;
onFeedback?: (messageId: string, feedback: "like" | "dislike") => void;
}function MyMessage({
message,
isStreaming,
isLast,
avatar,
slots,
onCopy,
onRegenerate,
onFeedback,
}: MessageProps) {
const isUser = message.role === "user";
return (
<div
style={{
display: "flex",
gap: 12,
justifyContent: isUser ? "flex-end" : "flex-start",
}}
>
{!isUser && <div>{avatar?.assistant ?? "A"}</div>}
<div style={{ maxWidth: "75%" }}>
{message.parts.map((part, idx) => {
switch (part.type) {
case "text":
return <div key={idx}>{part.content}</div>;
case "thinking":
return (
<div key={idx} style={{ fontStyle: "italic" }}>
{part.content}
</div>
);
case "tool_call":
return <div key={idx}>[调用 {part.toolName}]</div>;
case "tool_result":
return <div key={idx}>[结果] {part.content}</div>;
case "error":
return (
<div key={idx} style={{ color: "red" }}>
{part.content}
</div>
);
}
})}
{isStreaming && isLast && !isUser && <span>|</span>}
{!isUser && !isStreaming && (
<slots.ActionBar
message={message}
onCopy={onCopy}
onRegenerate={onRegenerate}
onFeedback={onFeedback}
/>
)}
</div>
{isUser && <div>{avatar?.user ?? "U"}</div>}
</div>
);
}Composer
export interface ComposerProps {
status: ChatStatus;
onSend: (content: string, attachments?: ChatAttachment[]) => void;
onStop: () => void;
}function MyComposer({ status, onSend, onStop }: ComposerProps) {
const [value, setValue] = useState("");
const streaming = status === "streaming";
return (
<div style={{ display: "flex", gap: 8, padding: 12 }}>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
style={{ flex: 1 }}
/>
{streaming ? (
<button onClick={onStop}>停止</button>
) : (
<button
onClick={() => {
onSend(value);
setValue("");
}}
>
发送
</button>
)}
</div>
);
}ActionBar(含 onFeedback)
export interface ActionBarProps {
message: UIMessage;
onCopy: () => void;
onRegenerate?: () => void;
// 传整条 message(内置 ActionBar 的做法)或 messageId / 本地 id 字符串均可
onFeedback?: (
message: UIMessage | string,
feedback: "like" | "dislike",
options?: { reason?: number; remark?: string },
) => void;
}function MyActionBar({
message,
onCopy,
onRegenerate,
onFeedback,
}: ActionBarProps) {
return (
<div style={{ display: "flex", gap: 8 }}>
<button onClick={onCopy}>复制</button>
<button onClick={onRegenerate}>重新生成</button>
<button onClick={() => onFeedback?.(message.id, "like")}>👍</button>
<button onClick={() => onFeedback?.(message.id, "dislike")}>👎</button>
</div>
);
}ThreadList(浮窗内嵌会话侧边栏 + 全屏模式侧边栏)
在 floating 模式下作为窗口内嵌侧边栏渲染(小窗 200px / 大窗 220px,随「历史记录」按钮开合);在 fullpage 模式下作为左侧侧边栏渲染(220px,可收起为图标栏)。
export interface ThreadListProps {
conversations: Conversation[];
activeSessionId: string | null;
onSwitch: (sessionId: string) => void;
onNew: () => void;
onDelete: (sessionId: string) => void;
onRename: (sessionId: string, title: string) => void;
hasMore?: boolean;
onLoadMore?: () => void;
}function MyThreadList({
conversations,
activeSessionId,
onSwitch,
onNew,
onDelete,
onRename,
}: ThreadListProps) {
return (
<div>
<button onClick={onNew}>+ 新会话</button>
{conversations.map((conv) => (
<div
key={conv.sessionId}
onClick={() => onSwitch(conv.sessionId)}
style={{ fontWeight: conv.sessionId === activeSessionId ? 700 : 400 }}
>
{conv.title || "新会话"}
<button
onClick={(e) => {
e.stopPropagation();
onRename(conv.sessionId, prompt("新标题") || "");
}}
>
改
</button>
<button
onClick={(e) => {
e.stopPropagation();
onDelete(conv.sessionId);
}}
>
删
</button>
</div>
))}
</div>
);
}ThinkingPart / TextPart / ToolCallPart / ToolResultPart / ErrorPart
export interface ThinkingPartProps {
content: string;
}
export interface TextPartProps {
content: string;
}
export interface ToolCallPartProps {
toolName: string;
toolCallId: string;
args: string;
}
export interface ToolResultPartProps {
toolName: string;
toolCallId: string;
content: string;
artifacts?: Artifact[]; // 仅供自定义 slot 取用;内置组件不再内联渲染文件卡片(已上移至消息级)
renderMode?: string;
html?: string;
artifactPreview?: boolean;
}
export interface ErrorPartProps {
content: string;
onRetry?: () => void;
}function MyThinkingPart({ content }: ThinkingPartProps) {
return (
<div style={{ borderLeft: "3px solid #a855f7", padding: 6 }}>{content}</div>
);
}
function MyTextPart({ content }: TextPartProps) {
return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>;
}
function MyToolCallPart({ toolName, args }: ToolCallPartProps) {
return (
<div style={{ border: "1px solid #f59e0b" }}>
[调用 {toolName}] {args}
</div>
);
}
function MyToolResultPart({ toolName, content }: ToolResultPartProps) {
return (
<div style={{ background: "#ecfdf5" }}>
[{toolName} 结果] {content}
</div>
);
}
function MyErrorPart({ content, onRetry }: ErrorPartProps) {
return (
<div style={{ color: "red" }}>
{content}
{onRetry && <button onClick={onRetry}>重试</button>}
</div>
);
}AppendContainer(首屏业务定制 + 整屏容器)
首屏(无消息时)在 WelcomeScreen 之下预留一块业务定制区,浮窗与全屏模式共用;未配置则不产生任何 DOM。
用法就一句话:点一下,openPanel(action)——可视区始终在 chatPanel 内(link 除外)。
export type PanelContent = ComponentType<PageProps> | ReactElement; // 组件类型,或带参数的元素
export type PanelAction =
| { type: 'page'; component: PanelContent; title?: string } // 整屏容器
| { type: 'link'; url: string; target?: '_blank' | '_self' }; // 外链
export interface AppendContainerProps { openPanel: (action: PanelAction) => void }
export interface PageProps { close: () => void }function ReportPage({ close }: PageProps) {
return (
<div style={{ padding: 16 }}>
报告内容……
<button onClick={close}>返回</button>
</div>
);
}
function MyHomeBlock({ openPanel }: AppendContainerProps) {
return (
<>
<button onClick={() => openPanel({ type: "page", component: ReportPage, title: "全景报告" })}>
整屏容器
</button>
<button onClick={() => openPanel({ type: "link", url: "https://example.com" })}>
新窗口外链
</button>
</>
);
}
mount("#app", { sdk, slots: { AppendContainer: MyHomeBlock } });要开「第 3 条记录」这类带参数的页面,直接给元素,SDK 用 cloneElement 补上 close:
// 元素形态下 close 声明为可选,才能写 <DetailPage id={3} />(运行时 SDK 必然注入)
function DetailPage({ id, close }: { id: number; close?: () => void }) {
return <button onClick={close}>详情 #{id} 返回</button>;
}
openPanel({ type: "page", component: <DetailPage id={3} />, title: "详情 #3" });给组件类型(component: DetailPage)时 close 是必填 prop,同样由 SDK 注入。
连定制区都不想写?首屏「快速入口」吃同一个 action,纯配置即可接入(title 缺省用 label):
mount("#app", {
sdk,
quickLinks: [
{ label: "全景报告", action: { type: "page", component: ReportPage } },
{ label: "帮助文档", action: { type: "link", url: "https://example.com/help" } },
],
});点击优先级:onClick > action > prompt。
整屏容器行为:
| 项 | 表现 |
| --- | --- |
| 呈现 | 替换「内容区 + 输入区」,Header 保留(小窗仍可拖拽);link 走 window.open,不占状态 |
| 标题 | 无独立标题栏(纯白背景);title 传入时显示在 Header 居中(floating 内置 Header / fullpage 均适用,自定义 slots.Header 收 panelTitle 自行渲染) |
| 退出 | Header 的 home 图标(tooltip「返回聊天」)/ 业务组件 close() / Esc;关闭聊天面板时一并清掉 |
| 窗口几何 | 保持打开前窗态,小窗不自动升大窗;内容在容器内滚动(业务页不写滚动容器) |
| 栈 | 单层不叠栈,容器内部导航由业务组件自己管 |
注意:浮层组件渲染在 SDK 自己的 createRoot 树内,拿不到宿主的 React Context / Router。组件引用直接给,不需要注册表——类型即校验。
8.4 事件钩子(EventHooks)
通过 hooks 监听组件生命周期与交互事件:
export interface EventHooks {
onOpen?: () => void;
onClose?: () => void;
onMessageSend?: (message: string) => void;
onStreamStart?: (sessionId: string) => void;
onStreamEnd?: (sessionId: string) => void;
onError?: (error: Error) => void;
onConversationChange?: (sessionId: string) => void;
}mount("#chat-root", {
sdk,
hooks: {
onOpen: () => console.log("面板已打开"),
onClose: () => console.log("面板已关闭"),
onMessageSend: (message) => console.log("用户发送:", message),
onStreamStart: (sessionId) =>
console.log("开始流式输出,会话:", sessionId),
onStreamEnd: (sessionId) => console.log("输出结束,会话:", sessionId),
onError: (error) => console.error("发生错误:", error.message),
onConversationChange: (sessionId) => console.log("切换会话:", sessionId),
},
});点赞 / 踩由内置
ActionBar组件通过ChatState.feedback()/cancelFeedback()自动处理,无需额外配置(见 5.9)。
8.5 定制示例
完整的自定义 Header + Message + ActionBar + WelcomeScreen:
import React, { useState } from "react";
import { createSuperAgent } from "super-agent-sdk";
import { mount, Icon } from "super-agent-sdk/widget";
import type {
HeaderProps,
MessageProps,
ActionBarProps,
WelcomeScreenProps,
} from "super-agent-sdk/widget";
const sdk = createSuperAgent({
botCode: "tech-researcher",
tokenGateway: "/api/gateway",
});
sdk.setToken("demo_token");
function CustomHeader({ title, onClose, onToggleThreadList }: HeaderProps) {
return (
<div
style={{
display: "flex",
alignItems: "center",
padding: "12px 16px",
background: "#0f172a",
color: "#fff",
}}
>
<Icon name="bot" size={18} color="#fff" />
<span style={{ flex: 1, marginLeft: 10, fontWeight: 700 }}>{title}</span>
<button onClick={onToggleThreadList}>
<Icon name="menu" size={16} />
</button>
<button onClick={onClose}>
<Icon name="x" size={16} />
</button>
</div>
);
}
function CustomMessage({
message,
isStreaming,
isLast,
avatar,
slots,
onCopy,
onRegenerate,
onFeedback,
}: MessageProps) {
const isUser = message.role === "user";
return (
<div
style={{
display: "flex",
gap: 12,
justifyContent: isUser ? "flex-end" : "flex-start",
padding: "4px 0",
}}
>
{!isUser && (
<div
style={{
width: 36,
height: 36,
borderRadius: 12,
background: "#6366f1",
}}
>
{avatar?.assistant || "A"}
</div>
)}
<div style={{ maxWidth: "75%" }}>
{message.parts.map((part, idx) => {
switch (part.type) {
case "text":
return (
<div key={idx} style={{ whiteSpace: "pre-wrap" }}>
{part.content}
</div>
);
case "thinking":
return (
<div
key={idx}
style={{ fontStyle: "italic", color: "#7c3aed" }}
>
{part.content}
</div>
);
case "tool_call":
return <div key={idx}>[调用 {part.toolName}]</div>;
case "tool_result":
return <div key={idx}>[结果] {part.content}</div>;
case "error":
return (
<div key={idx} style={{ color: "#b91c1c" }}>
{part.content}
</div>
);
}
})}
{isStreaming && isLast && !isUser && <span>|</span>}
{!isUser && !isStreaming && message.parts.length > 0 && (
<slots.ActionBar
message={message}
onCopy={onCopy}
onRegenerate={onRegenerate}
onFeedback={onFeedback}
/>
)}
</div>
{isUser && (
<div
style={{
width: 36,
height: 36,
borderRadius: 12,
background: "#475569",
}}
>
{avatar?.user || "U"}
</div>
)}
</div>
);
}
function CustomActionBar({
message,
onCopy,
onRegenerate,
onFeedback,
}: ActionBarProps) {
const [liked, setLiked] = useState<"like" | "dislike" | null>(null);
return (
<div style={{ display: "flex", gap: 8, marginTop: 6 }}>
<button onClick={onCopy}>复制</button>
<button onClick={onRegenerate}>重新生成</button>
<button
onClick={() => {
const v = liked === "like" ? null : "like";
setLiked(v);
if (v) onFeedback?.(message.id, v);
}}
style={{ color: liked === "like" ? "#6366f1" : "#9ca3af" }}
>
👍
</button>
<button
onClick={() => {
const v = liked === "dislike" ? null : "dislike";
setLiked(v);
if (v) onFeedback?.(message.id, v);
}}
style={{ color: liked === "dislike" ? "#6366f1" : "#9ca3af" }}
>
👎
</button>
</div>
);
}
function CustomWelcomeScreen({
message,
suggestedPrompts,
onPromptClick,
}: WelcomeScreenProps) {
return (
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<Icon name="sparkles" size={32} color="#6366f1" />
<p>{message || "有什么可以帮你的?"}</p>
{suggestedPrompts?.map((prompt) => (
<button key={prompt} onClick={() => onPromptClick(prompt)}>
{prompt}
</button>
))}
</div>
);
}
mount("#chat-root", {
sdk,
mode: "fullpage",
title: "AI 助手",
welcomeMessage: "你好!我是 AI 助手",
suggestedPrompts: ["查询订单", "写一段代码"],
slots: {
Header: CustomHeader,
Message: CustomMessage,
ActionBar: CustomActionBar,
WelcomeScreen: CustomWelcomeScreen,
},
}).open();9. 会话管理
9.1 SDK 层面
| 方法 | 签名 | 说明 |
| -------------------- | ----------------------------------------------- | ------------------------------------ |
| createSession | () => Promise<string> | 创建新会话,返回 sessionId |
| listConversations | (params?) => Promise<ListConversationsResult> | 获取会话列表(分页) |
| renameConversation | (sessionId, title) => Promise<void> | 重命名会话 |
| deleteConversation | (sessionId) => Promise<void> | 删除会话 |
| getMessages | (sessionId) => Promise<UIMessage[]> | 获取会话历史消息(已转换为 UI 结构) |
const sessionId = await sdk.createSession();
const { items, total } = await sdk.listConversations({ page: 1, size: 20 });
const messages = await sdk.getMessages(items[0].sessionId);
await sdk.renameConversation(items[0].sessionId, "新标题");
await sdk.deleteConversation(items[0].sessionId);9.2 组件内置能力
- 自动加载:挂载后自动调用
listConversations拉取会话列表(每页 20 条)。 - 滚动加载:会话列表滚动到底部时自动加载下一页,支持任意数量的会话。
- 切换:点击会话项加载该会话历史消息并展示。
- 新建:点击「新会话」清空当前消息,下次发送时自动调用
createSession()创建新会话。 - 重命名:
fullpage侧边栏或自定义ThreadList中调用renameConversation。 - 删除:悬停会话项后点击删除按钮。
- 本地持久化:最近一次活跃会话写入
localStorage,键为sa_active_conversation_{botCode}(按 Bot 隔离,切换 Bot 后互不串扰)。
10. 中断恢复
10.1 停止生成
SDK 提供双信号停止机制,确保多实例部署下也能可靠停止生成:
abort():断开 SSE 连接,前端即时停止接收POST /chat/stop:通过 Redis Pub/Sub 广播取消信号到实际执行的后端实例
Widget 内置的停止按钮已自动执行双信号(abort() + stopGeneration()),无需手动处理。
纯 API 模式
const controller = sdk.chat({
sessionId,
message: "...",
stream: true,
onMessage: (event) => {
if (event.type === "stop") {
// 用户主动停止,event.content 为已生成的半截回复全文
console.log("已停止,半截回复:", event.content);
}
},
onDone,
});
// 停止生成:双信号并发
controller.abort();
await sdk.stopGeneration(sessionId);stopGeneration()
sdk.stopGeneration(sessionId: string): Promise<void>调用 POST /chat/stop,广播取消信号。返回 { stopped: "pending" } 表示已受理,实际停止异步发生。SDK 不应阻塞等待——以 SSE 流的 stop/done 事件或本地 abort 作为停止完成的判定。
半截回复(含思考与正文)会保留:既写入数据库(刷新可回显),也写入 checkpoint(影响下一轮对话上下文)。
10.2 中断请求
chat() 返回 AbortController,可随时中断:
const controller = sdk.chat({
sessionId,
message: "...",
stream: true,
onDone,
});
controller.abort();也可在 ChatOptions.signal 传入外部 AbortSignal,SDK 会将其与内部信号合并,任一触发即中断:
const ac = new AbortController();
sdk.chat({ sessionId, message: "...", signal: ac.signal });
ac.abort();10.3 会话恢复
页面刷新或组件重新挂载时,会话状态自动恢复:
- 挂载时从
localStorage读取上次活跃会话 ID(键sa_active_conversation_{botCode})。 - 若该会话仍在列表中,自动调用
getMessages拉取历史消息并渲染。 - 若存储的会话已不存在(被删除),则自动清空并回退到「新会话」。
整个过程对用户透明,无需额外配置。
11. 消息类型
11.1 UIMessage
export interface UIMessage {
id: string; // 渲染 key:历史加载为表主键,流式为客户端临时 id(不用于 feedback)
role: "user" | "assistant";
parts: MessagePart[];
timestamp: number;
messageId?: string; // 后端消息标识(= conversation_messages.message_id),feedback/cancelFeedback 按它定位
roundId?: string; // 本轮对话 id(历史分组 roundId;后端为 null 时 SDK 随机生成)
artifacts?: Artifact[]; // done 汇总的本轮文件产物(历史取分组 artifacts),消息末尾渲染文件卡片
}产物渲染:文件产物(
generate_file交付的 html/md/pdf/docx)统一挂在 assistant 消息的artifacts上,渲染在该消息末尾为文件卡片(点击开预览抽屉)。流式来自done事件,历史来自 分组接口的组级artifacts(与done同构)。tool_resultpart 不再内联渲染文件卡片。
11.2 MessagePart
export type MessagePart =
| { type: "text"; content: string }
| { type: "thinking"; content: string }
| { type: "tool_call"; toolName: string; toolCallId: string; args: string }
| {
type: "tool_result";
toolName: string;
toolCallId: string;
content: string;
artifacts?: Artifact[];
renderMode?: string;
html?: string;
markdown?: string;
defaultExpanded?: boolean; // config.default_expanded=true 或 renderMode==="html":默认展开
}
| { type: "error"; content: string }
| { type: "attachment"; attachment: ChatAttachment } // 用户消息携带的附件 chip
| {
type: "interrupt";
interrupt: InterruptEvent;
resolved?: boolean;
response?: InterruptResponse;
};| type | 字段 | 说明 |
| ------------- | ------------------------------------------------------------------------- | ------------------------------- |
| text | content | 文本 / Markdown 内容 |
| thinking | content | 思考过程(reasoning) |
| tool_call | toolName, toolCallId, args | 工具调用,args 为 JSON 字符串 |
| tool_result | toolName, toolCallId, content, artifacts?, renderMode?, html?, markdown?, defaultExpanded? | 工具返回结果(文件产物已上移至消息级 UIMessage.artifacts,part 级 artifacts 仅供自定义 slot 取用);defaultExpanded=true 结果块默认展开(缺省收起),来源为工具配置 config.default_expanded 或 renderMode==="html" |
| error | content | 错误信息 |
| attachment | attachment | 用户消息附件 chip(发送时本地构造 / 历史加载由后端映射,有 url 时可下载) |
| interrupt | interrupt, resolved?, response? | 中断交互卡片(HITL) |
11.3 ChatEvent(流式事件)
export interface ChatEvent {
type:
| "user"
| "thinking"
| "ai"
| "tool_call"
| "tool_result"
| "done"
| "stop"
| "error"
| "interrupt";
content: string;
toolName?: string;
toolCallId?: string;
args?: string; // JSON.stringify 后的字符串
sessionId?: string;
messageId?: string; // done/stop 事件携带的后端消息标识(= conversation_messages.message_id,feedback 按它定位消息)
interrupt?: InterruptEvent; // interrupt 事件携带中断详情
artifacts?: Artifact[]; // tool_result 顶层产物;done 事件汇总本轮产物(Widget 挂到消息级渲染文件卡片)
renderMode?: string; // "html" 时工具结果为富文本
html?: string; // renderMode==="html" 时的 HTML 内容
markdown?: string; // renderMode==="markdown" 时的 markdown 内容
defaultExpanded?: boolean; // tool_result 顶层 default_expanded:结果块默认展开(缺省收起)
roundId?: string; // session.start/done 携带,本轮对话 id
}12. Human-in-the-Loop
Agent 执行中需要人类介入时(确认敏感操作、选择方案、补充信息、审核内容等),SDK 提供统一的中断交互机制。
12.1 核心流程
Agent 执行中
→ 后端发送 SSE interrupt 事件
→ SDK 渲染中断交互卡片(按 interruptType 分发)
→ 用户完成操作
→ SDK 调用 respondInterrupt() 回传响应
→ Agent 基于响应继续执行12.2 interrupt 事件
{
"type": "interrupt",
"interruptId": "int_001",
"interruptType": "confirm",
"content": "即将执行转账 ¥500,确认执行?",
"options": ["确认", "取消"]
}12.3 响应中断
await sdk.respondInterrupt(interruptId, sessionId, {
action: "confirm",
});12.4 Widget 自动处理
Widget 内置中断卡片渲染,收到 interrupt 事件后自动展示对应交互卡片,用户操作后自动回传,无需业务方处理:
mount("#chat", { sdk }).open();
// 遇到 interrupt 时:自动渲染卡片 → 用户点击 → 自动 respondInterrupt → Agent 继续interrupt 帧是流的暂停点(后端不发
done),respondInterrupt()后的续事件在同一 SSE 流上返回;续流结束(或嵌套 interrupt 再次暂停)后,输入框均可正常继续发送——挂起/续流状态由 Widget 自动管理。pending interrupt 期间在输入框直接发文本(不点卡片)也是"作答":Widget 自动路由到
POST /chat/interrupt/{interruptId}/respond(action=submit+ 原文),而非新开/chat轮;答案回填到中断卡片,不另插用户气泡(与历史加载口径一致)。
也可通过插槽自定义中断卡片:
mount("#chat", {
sdk,
slots: {
InterruptCard: MyInterruptCard, // 自定义全部类型
},
});12.5 纯 API 模式手动处理
sdk.chat({
sessionId,
message: "帮我转账 ¥500",
onMessage: (e) => {
if (e.type === "interrupt" && e.interrupt) {
// 自定义 UI 处理
const ok = window.confirm(e.interrupt.content);
sdk.respondInterrupt(e.interrupt.interruptId, sessionId, {
action: ok ? "confirm" : "cancel",
});
}
},
});12.6 interruptType 一览
| interruptType | 交互 | action | value 示例 |
| ------------- | -------- | --------------------- | ----------------------- |
| confirm | 二次确认 | confirm / cancel | — |
| select | 单选 | select | "flight_a" |
| multiSelect | 多选 | submit | ["name", "phone"] |
| input | 文本输入 | submit | "123456" |
| form | 表单 | submit | { name: "张三" } |
| review | 内容审阅 | approve / reject | 修改后内容 |
| approve | 审批流 | approve / reject | 驳回原因 |
| upload | 文件上传 | submit | { fileId, fileName } |
| image | 图片选择 | select / reject | 图片索引 |
| auth | 授权请求 | authorized / skip | — |
| captcha | 人机验证 | verified | "captcha_token" |
| decision | 流程分支 | decide | "retry" 等 |
| rating | 评分 | rate | 4 |
| date | 日期选择 | submit | "2026-08-21T14:00:00" |
| location | 位置选择 | submit | { address, lat, lng } |
form的字段定义见FormField(types.ts):type支持text/number/password/textarea/select/multiSelect/date;options支持字符串数组或{ value, label }数组。已回答的历史 interrupt 以禁用态回显用户答案。
13. 高级用法
13.1 纯 API 模式(不用 Widget)
不挂载组件,仅使用 API 层构建自己的 UI:
import { createSuperAgent } from "super-agent-sdk";
const sdk = createSuperAgent({
botCode: "tech-researcher",
tokenGateway: "/api/gateway",
});
sdk.setToken("<TOKEN>");
const sessionId = await sdk.createSession();
let full = "";
sdk.chat({
sessionId,
message: "你好",
onMessage: (e) => {
if (e.type === "ai") full += e.content;
},
onDone: ({ sessionId, content }) => console.log(sessionId, content),
onError: (err) => console.error(err),
});13.2 使用 Hooks
从 super-agent-sdk/widget 导出 useChat、useConversations,可在自己的 React 应用中复用:
export function useChat(options: UseChatOptions): UseChatReturn;
interface UseChatOptions {
sdk: SuperAgentSDK;
sessionId: string | null;
onSessionCreated?: (sessionId: string) => void;
onStreamStart?: (sessionId: string) => void;
onStreamEnd?: (sessionId: string) => void;
onError?: (error: Error) => void;
onMessageSend?: (message: string) => void;
}
interface UseChatReturn {
messages: UIMessage[];
status: ChatStatus;
error: Error | null;
sendMessage: (content: string) => void;
stop: () => void;
regenerate: () => void;
setMessages: (messages: UIMessage[]) => void;
respondToInterrupt: (
interruptId: string,
response: InterruptResponse,
) => void;
}export function useConversations(
options: UseConversationsOptions,
): UseConversationsReturn;
interface UseConversationsOptions {
sdk: SuperAgentSDK;
onConversationChange?: (sessionId: string) => void;
enabled?: boolean; // 默认 true,false 时不自动加载
}
interface UseConversationsReturn {
conversations: Conversation[];
activeConversationId: string | null;
loading: boolean;
hasMore: boolean;
loadConversations: () => Promise<void>;
loadMore: () => Promise<void>;
switchConversation: (sessionId: string) => Promise<UIMessage[]>;
newConversation: () => Promise<void>;
deleteConversation: (sessionId: string) => Promise<void>;
renameConversation: (sessionId: string, title: string) => Promise<void>;
setActiveConversationId: (id: string | null) => void;
}function MyChat({ sdk }: { sdk: SuperAgentSDK }) {
const conv = useConversations({ sdk });
const chat = useChat({ sdk, sessionId: conv.activeConversationId });
return (
<div>
{chat.messages.map((m) => (
<div key={m.id}>
{m.role}:{" "}
{m.parts.map((p) => (p.type === "text" ? p.content : "")).join("")}
</div>
))}
<button onClick={() => chat.sendMessage("你好")}>发送</button>
</div>
);
}
useChat在sessionId为空时会自动调用sdk.createSession()创建新会话,无需手动处理。
13.3 Token 续期
请求返回 401 时,SDK 自动重新调用 POST {tokenGateway}/superagent/v2/token/generate 获取新 token。
13.4 停止与取消请求
chat() 返回 AbortController;或传入外部 AbortSignal(SDK 会合并内部信号,任一触发即中断)。
推荐使用双信号机制停止生成(Widget 内置停止按钮已自动处理):
const controller = sdk.chat({
sessionId,
message: "...",
stream: true,
onDone,
});
// 双信号停止:abort 断 SSE + POST /chat/stop 广播后端取消
controller.abort();
await sdk.stopGeneration(sessionId);纯客户端取消(不通知后端):
controller.abort();
// 或使用外部 signal
const ac = new AbortController();
sdk.chat({ sessionId, message: "...", signal: ac.signal });
ac.abort();