npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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. 概述
  2. 安装
  3. 快速开始
  4. SDK 初始化
  5. API 参考
  6. 聊天组件
  7. 展示模式
  8. 定制化
  9. 会话管理
  10. 中断恢复
  11. 消息类型
  12. Human-in-the-Loop
  13. 高级用法

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
        ↓
ready

tokenGateway 用于拼接 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 管线处理,需要满足:

  1. 宿主 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 源码
  ],
  // ...
};
  1. 无需在宿主 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 #e3e1d8 hairline 边框(浅色暖调、横向滚动)、引用块 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 提供双信号停止机制,确保多实例部署下也能可靠停止生成:

  1. abort():断开 SSE 连接,前端即时停止接收
  2. 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 会话恢复

页面刷新或组件重新挂载时,会话状态自动恢复:

  1. 挂载时从 localStorage 读取上次活跃会话 ID(键 sa_active_conversation_{botCode})。
  2. 若该会话仍在列表中,自动调用 getMessages 拉取历史消息并渲染。
  3. 若存储的会话已不存在(被删除),则自动清空并回退到「新会话」。

整个过程对用户透明,无需额外配置。


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_result part 不再内联渲染文件卡片。

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 以禁用态回显用户答案。

每个 interruptType 的完整字段定义见 API 文档 与 HITL 设计文档。


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