super-agent-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1316 -0
- package/dist/index.cjs +5 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.mjs +345 -0
- package/dist/widget.cjs +1124 -0
- package/dist/widget.d.ts +394 -0
- package/dist/widget.mjs +2077 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
# @super-agent/sdk
|
|
2
|
+
|
|
3
|
+
Super Agent Web SDK —— 为 `super-agent-service` 提供的一站式浏览器端集成方案:既包含纯 JS 的 API 调用层(鉴权、SSE 流式解析、会话与消息接口),也包含开箱即用的嵌入式 React 聊天组件。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
1. [概述](#1-概述)
|
|
8
|
+
2. [安装](#2-安装)
|
|
9
|
+
3. [快速开始](#3-快速开始)
|
|
10
|
+
4. [SDK 初始化](#4-sdk-初始化)
|
|
11
|
+
5. [API 参考](#5-api-参考)
|
|
12
|
+
6. [聊天组件](#6-聊天组件)
|
|
13
|
+
7. [展示模式](#7-展示模式)
|
|
14
|
+
8. [定制化](#8-定制化)
|
|
15
|
+
9. [会话管理](#9-会话管理)
|
|
16
|
+
10. [中断恢复](#10-中断恢复)
|
|
17
|
+
11. [消息类型](#11-消息类型)
|
|
18
|
+
12. [高级用法](#12-高级用法)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. 概述
|
|
23
|
+
|
|
24
|
+
`@super-agent/sdk` 提供两个入口:
|
|
25
|
+
|
|
26
|
+
| 入口 | 说明 | 依赖 |
|
|
27
|
+
| ------------------------- | ---------------------------------------------------------- | ---------- |
|
|
28
|
+
| `@super-agent/sdk` | 纯 JS API 调用层,封装鉴权、SSE 流式解析、会话与消息接口 | 无 UI 依赖 |
|
|
29
|
+
| `@super-agent/sdk/widget` | 可嵌入式 React 聊天组件 | React 18+ |
|
|
30
|
+
|
|
31
|
+
核心能力:
|
|
32
|
+
|
|
33
|
+
- **纯 API 模式**:`createSuperAgent()` 返回轻量 SDK 实例,提供流式聊天、会话列表、历史消息、Token 刷新等能力。
|
|
34
|
+
- **嵌入式组件**:`mount()` 挂载 React 聊天组件。
|
|
35
|
+
- **两种展示模式**:`floating`(右下角气泡 + 弹窗面板)与 `fullpage`(整页 DeepSeek 风格布局)。
|
|
36
|
+
- **会话持久化**:自动加载会话列表、`localStorage` 记住最近会话、刷新后自动恢复。
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. 安装
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @super-agent/sdk
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
纯 API 模式无需任何额外依赖。若使用聊天组件(widget),需同时安装 React:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm add react react-dom
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> `react` / `react-dom` 以 `peerDependencies`(可选)声明,版本要求 `>=18.0.0`。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. 快速开始
|
|
57
|
+
|
|
58
|
+
最简 5 行接入:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { createSuperAgent } from "@super-agent/sdk";
|
|
62
|
+
import { mount } from "@super-agent/sdk/widget";
|
|
63
|
+
|
|
64
|
+
const sdk = createSuperAgent({ baseUrl: "/api/v1", botId: 1 });
|
|
65
|
+
// Token 自动获取,无需手动设置
|
|
66
|
+
const widget = mount("#chat", { sdk });
|
|
67
|
+
widget.open();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
挂载完成后,页面右下角出现聊天气泡,点击展开面板即可开始对话。
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 4. SDK 初始化
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
function createSuperAgent(config: SDKConfig): SuperAgentSDK;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
export interface SDKConfig {
|
|
82
|
+
baseUrl: string; // e.g. "https://agent.example.com/api/v1"
|
|
83
|
+
botId: number; // bot ID
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
只需提供 `baseUrl` 与 `botId` 即可。SDK 不要求在创建时传入 `appId` / `token`——组件挂载后会自动调用 `sdk.getToken()` 获取凭证并内部保存,无需手动设置。
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const sdk = createSuperAgent({
|
|
91
|
+
baseUrl: "https://agent.example.com/api/v1",
|
|
92
|
+
botId: 1,
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
凭证获取流程:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
createSuperAgent({ baseUrl, botId })
|
|
100
|
+
↓
|
|
101
|
+
Widget 挂载时自动调用 sdk.getToken()
|
|
102
|
+
↓
|
|
103
|
+
POST /token { botId }
|
|
104
|
+
↓
|
|
105
|
+
后端返回 { app_id, token }
|
|
106
|
+
↓
|
|
107
|
+
SDK 内部自动保存凭证
|
|
108
|
+
↓
|
|
109
|
+
ready
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
出于安全考虑,`app_key` 应永远留在后端,浏览器端只持有短时有效的 Token。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 5. API 参考
|
|
117
|
+
|
|
118
|
+
> **字段命名映射**:后端接口返回的是 snake_case 字段,SDK 内部统一转换为 camelCase 后对外暴露。
|
|
119
|
+
>
|
|
120
|
+
> - 后端返回 `thread_id`(snake_case)→ SDK 内部映射为 `sessionId`
|
|
121
|
+
> - 后端返回 `bot_id` → SDK 映射为 `botId`
|
|
122
|
+
> - SDK 所有公开 API 均使用 camelCase(`sessionId`、`botId`)
|
|
123
|
+
> - SDK 发起请求时同样使用 camelCase(如 `{ botId, sessionId, message }`)
|
|
124
|
+
|
|
125
|
+
### 5.1 SDK 实例
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
export interface SuperAgentSDK {
|
|
129
|
+
createSession(): Promise<string>;
|
|
130
|
+
chat(options: ChatOptions): AbortController;
|
|
131
|
+
listConversations(
|
|
132
|
+
params?: ListConversationsParams,
|
|
133
|
+
): Promise<ListConversationsResult>;
|
|
134
|
+
renameConversation(sessionId: string, title: string): Promise<void>;
|
|
135
|
+
deleteConversation(sessionId: string): Promise<void>;
|
|
136
|
+
getMessages(sessionId: string): Promise<UIMessage[]>;
|
|
137
|
+
getToken(): Promise<{ appId: string; token: string }>;
|
|
138
|
+
setToken(appId: string, token: string): void;
|
|
139
|
+
feedback(messageId: string, type: "like" | "dislike"): void;
|
|
140
|
+
onFeedback?: (messageId: string, type: "like" | "dislike") => void;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 5.2 createSession()
|
|
145
|
+
|
|
146
|
+
创建新会话,返回 `sessionId`。聊天前必须先创建会话。
|
|
147
|
+
|
|
148
|
+
- SDK 调用 `POST /chat/sessions`,请求体为 `{ botId }`
|
|
149
|
+
- 后端返回 `{ session_id: "..." }`
|
|
150
|
+
- SDK 解析后返回 `sessionId` 字符串
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const sessionId = await sdk.createSession();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 5.3 chat()
|
|
157
|
+
|
|
158
|
+
流式聊天。**`sessionId` 为必填**,必须先调用 `createSession()` 获取。返回 `AbortController`,可随时中断。
|
|
159
|
+
|
|
160
|
+
- SDK 发送 `POST /chat`,请求体为 `{ botId, sessionId, message, stream }`
|
|
161
|
+
- 后端 SSE 在 `done` 事件中返回 `session_id` → SDK 暴露为 `sessionId`
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
export interface ChatOptions {
|
|
165
|
+
sessionId: string; // 必填 — 先调用 createSession()
|
|
166
|
+
message: string;
|
|
167
|
+
stream?: boolean; // 默认 true
|
|
168
|
+
onMessage?: (event: ChatEvent) => void;
|
|
169
|
+
onError?: (error: Error) => void;
|
|
170
|
+
onDone?: (result: { sessionId: string; content: string }) => void;
|
|
171
|
+
signal?: AbortSignal;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const sessionId = await sdk.createSession();
|
|
177
|
+
|
|
178
|
+
const controller = sdk.chat({
|
|
179
|
+
sessionId,
|
|
180
|
+
message: "帮我写一段排序算法",
|
|
181
|
+
stream: true, // 默认 true
|
|
182
|
+
onMessage: (event) => {
|
|
183
|
+
switch (event.type) {
|
|
184
|
+
case "thinking":
|
|
185
|
+
console.log("[思考]", event.content);
|
|
186
|
+
break;
|
|
187
|
+
case "ai":
|
|
188
|
+
process.stdout.write(event.content); // 增量输出
|
|
189
|
+
break;
|
|
190
|
+
case "tool_call":
|
|
191
|
+
console.log("[调用工具]", event.toolName, event.args);
|
|
192
|
+
break;
|
|
193
|
+
case "tool_result":
|
|
194
|
+
console.log("[工具结果]", event.content);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
onDone: ({ sessionId, content }) => {
|
|
199
|
+
console.log("\n完成,sessionId =", sessionId);
|
|
200
|
+
},
|
|
201
|
+
onError: (error) => {
|
|
202
|
+
console.error("出错:", error.message);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 取消:controller.abort();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`stream: false` 时走非流式接口,`onDone` 收到完整回复:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
sdk.chat({
|
|
213
|
+
sessionId,
|
|
214
|
+
message: "你好",
|
|
215
|
+
stream: false,
|
|
216
|
+
onDone: ({ sessionId, content }) => console.log("完整回复:", content),
|
|
217
|
+
onError: (error) => console.error(error),
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 5.4 listConversations()
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
export interface ListConversationsParams {
|
|
225
|
+
page?: number; // 默认 1
|
|
226
|
+
size?: number; // 默认 20
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface ListConversationsResult {
|
|
230
|
+
items: Conversation[];
|
|
231
|
+
total: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface Conversation {
|
|
235
|
+
sessionId: string;
|
|
236
|
+
botId: number;
|
|
237
|
+
title: string | null;
|
|
238
|
+
createTime: string;
|
|
239
|
+
updateTime: string;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const { items, total } = await sdk.listConversations({ page: 1, size: 20 });
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- SDK 调用 `GET /chat/conversations?botId=1&page=1&size=20`
|
|
248
|
+
- 后端返回的每一项使用 `thread_id`、`bot_id`、`create_time`、`update_time` 字段
|
|
249
|
+
- SDK 将其映射为 `Conversation { sessionId, botId, title, createTime, updateTime }`
|
|
250
|
+
|
|
251
|
+
### 5.5 getMessages()
|
|
252
|
+
|
|
253
|
+
拉取会话历史消息,已转换为 UI 结构:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const messages: UIMessage[] = await sdk.getMessages(sessionId);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 5.6 renameConversation() / deleteConversation()
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
await sdk.renameConversation(sessionId, "关于部署的讨论");
|
|
263
|
+
await sdk.deleteConversation(sessionId);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 5.7 getToken()
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
sdk.getToken(): Promise<{ appId: string; token: string }>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
自动获取并保存访问凭证:
|
|
273
|
+
|
|
274
|
+
- SDK 调用 `POST /token`,请求体为 `{ botId }`(无需鉴权头)
|
|
275
|
+
- 后端返回 `{ app_id, token }`
|
|
276
|
+
- SDK 自动将凭证保存在内部
|
|
277
|
+
- Widget 在挂载时自动调用此方法 —— 通常无需手动调用
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const { appId, token } = await sdk.getToken();
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 5.8 setToken()
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
sdk.setToken(appId: string, token: string): void
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
手动设置凭证(用于凭证由外部获取、或测试等场景)。正常情况下无需调用,Widget 挂载时会自动通过 `getToken()` 获取凭证。
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
sdk.setToken("<APP_ID>", "<TOKEN>");
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 5.9 feedback() / onFeedback
|
|
296
|
+
|
|
297
|
+
点赞 / 踩:调用 `sdk.feedback()`,SDK 会回调你注册的 `sdk.onFeedback`。
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
sdk.onFeedback = (messageId, type) => {
|
|
301
|
+
// type: 'like' | 'dislike'
|
|
302
|
+
console.log(`Feedback: ${type} on message ${messageId}`);
|
|
303
|
+
// 上报到自己的业务后端
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
sdk.feedback("msg_123", "like");
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
组件内置的点赞 / 踩按钮也会经过 `sdk.feedback()`,因此只需注册一次 `onFeedback` 即可统一接收所有反馈。
|
|
310
|
+
|
|
311
|
+
### 5.10 后端接口对照
|
|
312
|
+
|
|
313
|
+
SDK 方法 → 后端接口的完整映射:
|
|
314
|
+
|
|
315
|
+
| SDK 方法 | HTTP 请求 | 请求体(camelCase) | 后端返回(snake_case) | SDK 返回 |
|
|
316
|
+
| --------------------- | ------------------------- | --------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------ |
|
|
317
|
+
| `getToken()` | `POST /token` | `{ botId }` | `{ app_id, token }` | `{ appId, token }` |
|
|
318
|
+
| `createSession()` | `POST /chat/sessions` | `{ botId }` | `{ session_id }` | `sessionId` 字符串 |
|
|
319
|
+
| `chat()` | `POST /chat` | `{ botId, sessionId, message, stream }` | SSE 流,`done` 事件含 `session_id` | `onDone` 中暴露 `sessionId` |
|
|
320
|
+
| `listConversations()` | `GET /chat/conversations` | 查询参数 `botId`、`page`、`size` | `{ thread_id, bot_id, create_time, update_time }[]` | `Conversation { sessionId, botId, title, createTime, updateTime }` |
|
|
321
|
+
|
|
322
|
+
字段命名转换规则:
|
|
323
|
+
|
|
324
|
+
| 后端(snake_case) | SDK(camelCase) |
|
|
325
|
+
| ------------------ | ---------------- |
|
|
326
|
+
| `thread_id` | `sessionId` |
|
|
327
|
+
| `bot_id` | `botId` |
|
|
328
|
+
| `create_time` | `createTime` |
|
|
329
|
+
| `update_time` | `updateTime` |
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 6. 聊天组件
|
|
334
|
+
|
|
335
|
+
组件通过 `mount()` 从 `@super-agent/sdk/widget` 导入。
|
|
336
|
+
|
|
337
|
+
### 6.1 mount()
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
export function mount(
|
|
341
|
+
target: string | HTMLElement,
|
|
342
|
+
options: MountOptions,
|
|
343
|
+
): WidgetInstance;
|
|
344
|
+
|
|
345
|
+
export interface MountOptions {
|
|
346
|
+
sdk: SuperAgentSDK;
|
|
347
|
+
mode?: WidgetMode; // 默认 'floating'
|
|
348
|
+
theme?: ThemeConfig;
|
|
349
|
+
slots?: Slots;
|
|
350
|
+
hooks?: EventHooks;
|
|
351
|
+
welcomeMessage?: string;
|
|
352
|
+
suggestedPrompts?: string[];
|
|
353
|
+
title?: string; // 标题,默认 "AI Assistant"
|
|
354
|
+
sidebarDefaultOpen?: boolean; // fullpage 模式:侧边栏初始展开(默认 true)
|
|
355
|
+
avatar?: AvatarConfig; // 自定义头像
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export interface WidgetInstance {
|
|
359
|
+
open(): void;
|
|
360
|
+
close(): void;
|
|
361
|
+
destroy(): void;
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { createSuperAgent } from "@super-agent/sdk";
|
|
367
|
+
import { mount } from "@super-agent/sdk/widget";
|
|
368
|
+
|
|
369
|
+
const sdk = createSuperAgent({ baseUrl: "/api/v1", botId: 1 });
|
|
370
|
+
|
|
371
|
+
const widget = mount("#chat-root", {
|
|
372
|
+
sdk,
|
|
373
|
+
mode: "floating",
|
|
374
|
+
title: "AI 助手",
|
|
375
|
+
welcomeMessage: "你好!有什么可以帮你的?",
|
|
376
|
+
suggestedPrompts: ["帮我写周报", "解释一下这段代码"],
|
|
377
|
+
theme: { primaryColor: "#6366f1" },
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
widget.open(); // 展开面板
|
|
381
|
+
widget.close(); // 收起面板
|
|
382
|
+
widget.destroy(); // 卸载并移除 DOM
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
> 挂载后默认是收起状态,需调用 `widget.open()` 展开。
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 7. 展示模式
|
|
390
|
+
|
|
391
|
+
通过 `mode` 切换两种展示模式:
|
|
392
|
+
|
|
393
|
+
| 模式 | 说明 |
|
|
394
|
+
| ------------------ | ---------------------------------------------- |
|
|
395
|
+
| `floating`(默认) | 右下角悬浮气泡,点击展开弹窗面板 |
|
|
396
|
+
| `fullpage` | 整页布局,DeepSeek 风格左侧会话栏 + 右侧聊天区 |
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
mount("#chat-root", { sdk, mode: "fullpage" });
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`fullpage` 模式下,可通过 `sidebarDefaultOpen` 控制侧边栏初始展开状态:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
mount("#chat-root", { sdk, mode: "fullpage", sidebarDefaultOpen: false });
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## 8. 定制化
|
|
411
|
+
|
|
412
|
+
### 8.1 主题定制
|
|
413
|
+
|
|
414
|
+
通过 `ThemeConfig` 定制外观,组件内部将其转换为 CSS 变量。
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
export interface ThemeConfig {
|
|
418
|
+
primaryColor?: string; // 默认 #6366f1
|
|
419
|
+
backgroundColor?: string; // 默认 #ffffff
|
|
420
|
+
fontFamily?: string; // 默认 system-ui, -apple-system, sans-serif
|
|
421
|
+
borderRadius?: number; // 默认 16 (px)
|
|
422
|
+
panelWidth?: number; // 默认 380 (px)
|
|
423
|
+
panelHeight?: number; // 默认 520 (px)
|
|
424
|
+
zIndex?: number; // 默认 9999
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
对应的 CSS 变量:
|
|
429
|
+
|
|
430
|
+
| 变量 | 默认值 | 对应字段 |
|
|
431
|
+
| ------------------- | -------------------------------------- | ----------------- |
|
|
432
|
+
| `--sa-primary` | `#6366f1` | `primaryColor` |
|
|
433
|
+
| `--sa-bg` | `#ffffff` | `backgroundColor` |
|
|
434
|
+
| `--sa-font` | `system-ui, -apple-system, sans-serif` | `fontFamily` |
|
|
435
|
+
| `--sa-radius` | `16px` | `borderRadius` |
|
|
436
|
+
| `--sa-panel-width` | `380px` | `panelWidth` |
|
|
437
|
+
| `--sa-panel-height` | `520px` | `panelHeight` |
|
|
438
|
+
| `--sa-z-index` | `9999` | `zIndex` |
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
mount("#chat-root", {
|
|
442
|
+
sdk,
|
|
443
|
+
theme: {
|
|
444
|
+
primaryColor: "#10b981",
|
|
445
|
+
backgroundColor: "#f9fafb",
|
|
446
|
+
fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif",
|
|
447
|
+
borderRadius: 12,
|
|
448
|
+
panelWidth: 400,
|
|
449
|
+
panelHeight: 600,
|
|
450
|
+
zIndex: 10000,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### 8.2 头像定制
|
|
456
|
+
|
|
457
|
+
通过 `avatar` 配置助手与用户的头像,支持 URL、emoji 或文本(以 `http`、`/`、`data:` 开头视为图片 URL,否则作为 emoji/文本渲染):
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
export interface AvatarConfig {
|
|
461
|
+
assistant?: string; // URL、emoji 或文本
|
|
462
|
+
user?: string;
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
mount("#chat-root", {
|
|
468
|
+
sdk,
|
|
469
|
+
avatar: {
|
|
470
|
+
assistant: "🤖",
|
|
471
|
+
user: "https://cdn.example.com/user-avatar.png",
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### 8.3 组件插槽(Slots)
|
|
477
|
+
|
|
478
|
+
通过 `slots` 替换内置子组件。所有插槽 Props 均从 `@super-agent/sdk/widget` 导出。
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
export interface Slots {
|
|
482
|
+
Trigger?: ComponentType<TriggerProps>;
|
|
483
|
+
Header?: ComponentType<HeaderProps>;
|
|
484
|
+
Message?: ComponentType<MessageProps>;
|
|
485
|
+
Composer?: ComponentType<ComposerProps>;
|
|
486
|
+
ActionBar?: ComponentType<ActionBarProps>;
|
|
487
|
+
ThreadList?: ComponentType<ThreadListProps>;
|
|
488
|
+
ThinkingPart?: ComponentType<ThinkingPartProps>;
|
|
489
|
+
TextPart?: ComponentType<TextPartProps>;
|
|
490
|
+
ToolCallPart?: ComponentType<ToolCallPartProps>;
|
|
491
|
+
ToolResultPart?: ComponentType<ToolResultPartProps>;
|
|
492
|
+
ErrorPart?: ComponentType<ErrorPartProps>;
|
|
493
|
+
WelcomeScreen?: ComponentType<WelcomeScreenProps>;
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
| 插槽 | Props 接口 | 说明 |
|
|
498
|
+
| ---------------- | --------------------- | ------------------------------------------- |
|
|
499
|
+
| `Trigger` | `TriggerProps` | 悬浮触发按钮 |
|
|
500
|
+
| `Header` | `HeaderProps` | 面板标题栏 |
|
|
501
|
+
| `Message` | `MessageProps` | 单条消息容器(完整自定义消息渲染) |
|
|
502
|
+
| `Composer` | `ComposerProps` | 输入框 / 发送栏 |
|
|
503
|
+
| `ActionBar` | `ActionBarProps` | 消息操作栏(复制 / 重试 / 反馈) |
|
|
504
|
+
| `ThreadList` | `ThreadListProps` | 会话列表(浮窗模式覆盖层 / 全屏模式侧边栏) |
|
|
505
|
+
| `ThinkingPart` | `ThinkingPartProps` | 思考过程块 |
|
|
506
|
+
| `TextPart` | `TextPartProps` | 文本块 |
|
|
507
|
+
| `ToolCallPart` | `ToolCallPartProps` | 工具调用卡片 |
|
|
508
|
+
| `ToolResultPart` | `ToolResultPartProps` | 工具返回卡片 |
|
|
509
|
+
| `ErrorPart` | `ErrorPartProps` | 错误提示块 |
|
|
510
|
+
| `WelcomeScreen` | `WelcomeScreenProps` | 空会话欢迎页 |
|
|
511
|
+
|
|
512
|
+
#### Trigger
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
export interface TriggerProps {
|
|
516
|
+
isOpen: boolean;
|
|
517
|
+
onClick: () => void;
|
|
518
|
+
unreadCount?: number;
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
```tsx
|
|
523
|
+
function MyTrigger({ isOpen, onClick, unreadCount }: TriggerProps) {
|
|
524
|
+
return (
|
|
525
|
+
<button
|
|
526
|
+
onClick={onClick}
|
|
527
|
+
style={{ position: "fixed", right: 24, bottom: 24 }}
|
|
528
|
+
>
|
|
529
|
+
{isOpen ? "关闭" : "聊天"}
|
|
530
|
+
{unreadCount ? <span>{unreadCount}</span> : null}
|
|
531
|
+
</button>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### Header
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
export interface HeaderProps {
|
|
540
|
+
title: string;
|
|
541
|
+
onClose: () => void;
|
|
542
|
+
onToggleThreadList: () => void;
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
function MyHeader({ title, onClose, onToggleThreadList }: HeaderProps) {
|
|
548
|
+
return (
|
|
549
|
+
<div
|
|
550
|
+
style={{
|
|
551
|
+
display: "flex",
|
|
552
|
+
alignItems: "center",
|
|
553
|
+
padding: 12,
|
|
554
|
+
background: "#111",
|
|
555
|
+
}}
|
|
556
|
+
>
|
|
557
|
+
<span style={{ flex: 1, color: "#fff", fontWeight: 600 }}>{title}</span>
|
|
558
|
+
<button onClick={onToggleThreadList}>历史</button>
|
|
559
|
+
<button onClick={onClose}>关闭</button>
|
|
560
|
+
</div>
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
#### Message(重点:完整自定义消息渲染)
|
|
566
|
+
|
|
567
|
+
替换后接管整条消息的渲染,可完全自定义布局。`slots` 会透传进来,方便复用内置/自定义的 Part 组件。
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
export interface MessageProps {
|
|
571
|
+
message: UIMessage;
|
|
572
|
+
isStreaming: boolean;
|
|
573
|
+
isLast: boolean;
|
|
574
|
+
avatar?: AvatarConfig;
|
|
575
|
+
slots: Slots;
|
|
576
|
+
onCopy: () => void;
|
|
577
|
+
onRegenerate: () => void;
|
|
578
|
+
onFeedback?: (messageId: string, feedback: "like" | "dislike") => void;
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
583
|
+
function MyMessage({
|
|
584
|
+
message,
|
|
585
|
+
isStreaming,
|
|
586
|
+
isLast,
|
|
587
|
+
avatar,
|
|
588
|
+
slots,
|
|
589
|
+
onCopy,
|
|
590
|
+
onRegenerate,
|
|
591
|
+
onFeedback,
|
|
592
|
+
}: MessageProps) {
|
|
593
|
+
const isUser = message.role === "user";
|
|
594
|
+
return (
|
|
595
|
+
<div
|
|
596
|
+
style={{
|
|
597
|
+
display: "flex",
|
|
598
|
+
gap: 12,
|
|
599
|
+
justifyContent: isUser ? "flex-end" : "flex-start",
|
|
600
|
+
}}
|
|
601
|
+
>
|
|
602
|
+
{!isUser && <div>{avatar?.assistant ?? "A"}</div>}
|
|
603
|
+
<div style={{ maxWidth: "75%" }}>
|
|
604
|
+
{message.parts.map((part, idx) => {
|
|
605
|
+
switch (part.type) {
|
|
606
|
+
case "text":
|
|
607
|
+
return <div key={idx}>{part.content}</div>;
|
|
608
|
+
case "thinking":
|
|
609
|
+
return (
|
|
610
|
+
<div key={idx} style={{ fontStyle: "italic" }}>
|
|
611
|
+
{part.content}
|
|
612
|
+
</div>
|
|
613
|
+
);
|
|
614
|
+
case "tool_call":
|
|
615
|
+
return <div key={idx}>[调用 {part.toolName}]</div>;
|
|
616
|
+
case "tool_result":
|
|
617
|
+
return <div key={idx}>[结果] {part.content}</div>;
|
|
618
|
+
case "error":
|
|
619
|
+
return (
|
|
620
|
+
<div key={idx} style={{ color: "red" }}>
|
|
621
|
+
{part.content}
|
|
622
|
+
</div>
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
})}
|
|
626
|
+
{isStreaming && isLast && !isUser && <span>|</span>}
|
|
627
|
+
{!isUser && !isStreaming && (
|
|
628
|
+
<slots.ActionBar
|
|
629
|
+
message={message}
|
|
630
|
+
onCopy={onCopy}
|
|
631
|
+
onRegenerate={onRegenerate}
|
|
632
|
+
onFeedback={onFeedback}
|
|
633
|
+
/>
|
|
634
|
+
)}
|
|
635
|
+
</div>
|
|
636
|
+
{isUser && <div>{avatar?.user ?? "U"}</div>}
|
|
637
|
+
</div>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
#### Composer
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
export interface ComposerProps {
|
|
646
|
+
status: ChatStatus;
|
|
647
|
+
onSend: (content: string) => void;
|
|
648
|
+
onStop: () => void;
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
```tsx
|
|
653
|
+
function MyComposer({ status, onSend, onStop }: ComposerProps) {
|
|
654
|
+
const [value, setValue] = useState("");
|
|
655
|
+
const streaming = status === "streaming";
|
|
656
|
+
return (
|
|
657
|
+
<div style={{ display: "flex", gap: 8, padding: 12 }}>
|
|
658
|
+
<input
|
|
659
|
+
value={value}
|
|
660
|
+
onChange={(e) => setValue(e.target.value)}
|
|
661
|
+
style={{ flex: 1 }}
|
|
662
|
+
/>
|
|
663
|
+
{streaming ? (
|
|
664
|
+
<button onClick={onStop}>停止</button>
|
|
665
|
+
) : (
|
|
666
|
+
<button
|
|
667
|
+
onClick={() => {
|
|
668
|
+
onSend(value);
|
|
669
|
+
setValue("");
|
|
670
|
+
}}
|
|
671
|
+
>
|
|
672
|
+
发送
|
|
673
|
+
</button>
|
|
674
|
+
)}
|
|
675
|
+
</div>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
#### ActionBar(含 onFeedback)
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
export interface ActionBarProps {
|
|
684
|
+
message: UIMessage;
|
|
685
|
+
onCopy: () => void;
|
|
686
|
+
onRegenerate: () => void;
|
|
687
|
+
onFeedback?: (messageId: string, feedback: "like" | "dislike") => void;
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
```tsx
|
|
692
|
+
function MyActionBar({
|
|
693
|
+
message,
|
|
694
|
+
onCopy,
|
|
695
|
+
onRegenerate,
|
|
696
|
+
onFeedback,
|
|
697
|
+
}: ActionBarProps) {
|
|
698
|
+
return (
|
|
699
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
700
|
+
<button onClick={onCopy}>复制</button>
|
|
701
|
+
<button onClick={onRegenerate}>重新生成</button>
|
|
702
|
+
<button onClick={() => onFeedback?.(message.id, "like")}>👍</button>
|
|
703
|
+
<button onClick={() => onFeedback?.(message.id, "dislike")}>👎</button>
|
|
704
|
+
</div>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
#### ThreadList(浮窗模式覆盖层 + 全屏模式侧边栏)
|
|
710
|
+
|
|
711
|
+
在 `floating` 模式下作为覆盖层渲染;在 `fullpage` 模式下作为侧边栏渲染。
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
export interface ThreadListProps {
|
|
715
|
+
conversations: Conversation[];
|
|
716
|
+
activeSessionId: string | null;
|
|
717
|
+
onSwitch: (sessionId: string) => void;
|
|
718
|
+
onNew: () => void;
|
|
719
|
+
onDelete: (sessionId: string) => void;
|
|
720
|
+
onRename: (sessionId: string, title: string) => void;
|
|
721
|
+
}
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
```tsx
|
|
725
|
+
function MyThreadList({
|
|
726
|
+
conversations,
|
|
727
|
+
activeSessionId,
|
|
728
|
+
onSwitch,
|
|
729
|
+
onNew,
|
|
730
|
+
onDelete,
|
|
731
|
+
onRename,
|
|
732
|
+
}: ThreadListProps) {
|
|
733
|
+
return (
|
|
734
|
+
<div>
|
|
735
|
+
<button onClick={onNew}>+ 新会话</button>
|
|
736
|
+
{conversations.map((conv) => (
|
|
737
|
+
<div
|
|
738
|
+
key={conv.sessionId}
|
|
739
|
+
onClick={() => onSwitch(conv.sessionId)}
|
|
740
|
+
style={{ fontWeight: conv.sessionId === activeSessionId ? 700 : 400 }}
|
|
741
|
+
>
|
|
742
|
+
{conv.title || "新会话"}
|
|
743
|
+
<button
|
|
744
|
+
onClick={(e) => {
|
|
745
|
+
e.stopPropagation();
|
|
746
|
+
onRename(conv.sessionId, prompt("新标题") || "");
|
|
747
|
+
}}
|
|
748
|
+
>
|
|
749
|
+
改
|
|
750
|
+
</button>
|
|
751
|
+
<button
|
|
752
|
+
onClick={(e) => {
|
|
753
|
+
e.stopPropagation();
|
|
754
|
+
onDelete(conv.sessionId);
|
|
755
|
+
}}
|
|
756
|
+
>
|
|
757
|
+
删
|
|
758
|
+
</button>
|
|
759
|
+
</div>
|
|
760
|
+
))}
|
|
761
|
+
</div>
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
#### ThinkingPart / TextPart / ToolCallPart / ToolResultPart / ErrorPart
|
|
767
|
+
|
|
768
|
+
```ts
|
|
769
|
+
export interface ThinkingPartProps {
|
|
770
|
+
content: string;
|
|
771
|
+
}
|
|
772
|
+
export interface TextPartProps {
|
|
773
|
+
content: string;
|
|
774
|
+
}
|
|
775
|
+
export interface ToolCallPartProps {
|
|
776
|
+
toolName: string;
|
|
777
|
+
toolCallId: string;
|
|
778
|
+
args: string;
|
|
779
|
+
}
|
|
780
|
+
export interface ToolResultPartProps {
|
|
781
|
+
toolName: string;
|
|
782
|
+
toolCallId: string;
|
|
783
|
+
content: string;
|
|
784
|
+
}
|
|
785
|
+
export interface ErrorPartProps {
|
|
786
|
+
content: string;
|
|
787
|
+
onRetry?: () => void;
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
```tsx
|
|
792
|
+
function MyThinkingPart({ content }: ThinkingPartProps) {
|
|
793
|
+
return (
|
|
794
|
+
<div style={{ borderLeft: "3px solid #a855f7", padding: 6 }}>{content}</div>
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function MyTextPart({ content }: TextPartProps) {
|
|
799
|
+
return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function MyToolCallPart({ toolName, args }: ToolCallPartProps) {
|
|
803
|
+
return (
|
|
804
|
+
<div style={{ border: "1px solid #f59e0b" }}>
|
|
805
|
+
[调用 {toolName}] {args}
|
|
806
|
+
</div>
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function MyToolResultPart({ toolName, content }: ToolResultPartProps) {
|
|
811
|
+
return (
|
|
812
|
+
<div style={{ background: "#ecfdf5" }}>
|
|
813
|
+
[{toolName} 结果] {content}
|
|
814
|
+
</div>
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function MyErrorPart({ content, onRetry }: ErrorPartProps) {
|
|
819
|
+
return (
|
|
820
|
+
<div style={{ color: "red" }}>
|
|
821
|
+
{content}
|
|
822
|
+
{onRetry && <button onClick={onRetry}>重试</button>}
|
|
823
|
+
</div>
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### 8.4 事件钩子(EventHooks)
|
|
829
|
+
|
|
830
|
+
通过 `hooks` 监听组件生命周期与交互事件:
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
export interface EventHooks {
|
|
834
|
+
onOpen?: () => void;
|
|
835
|
+
onClose?: () => void;
|
|
836
|
+
onMessageSend?: (message: string) => void;
|
|
837
|
+
onStreamStart?: (sessionId: string) => void;
|
|
838
|
+
onStreamEnd?: (sessionId: string) => void;
|
|
839
|
+
onError?: (error: Error) => void;
|
|
840
|
+
onConversationChange?: (sessionId: string) => void;
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
mount("#chat-root", {
|
|
846
|
+
sdk,
|
|
847
|
+
hooks: {
|
|
848
|
+
onOpen: () => console.log("面板已打开"),
|
|
849
|
+
onClose: () => console.log("面板已关闭"),
|
|
850
|
+
onMessageSend: (message) => console.log("用户发送:", message),
|
|
851
|
+
onStreamStart: (sessionId) =>
|
|
852
|
+
console.log("开始流式输出,会话:", sessionId),
|
|
853
|
+
onStreamEnd: (sessionId) => console.log("输出结束,会话:", sessionId),
|
|
854
|
+
onError: (error) => console.error("发生错误:", error.message),
|
|
855
|
+
onConversationChange: (sessionId) => console.log("切换会话:", sessionId),
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
> 点赞 / 踩的反馈回调不在 `hooks` 中,而是在 `sdk.onFeedback`(见 [5.9](#59-feedback--onfeedback))。
|
|
861
|
+
|
|
862
|
+
### 8.5 定制示例
|
|
863
|
+
|
|
864
|
+
完整的自定义 `Header` + `Message` + `ActionBar` + `WelcomeScreen`:
|
|
865
|
+
|
|
866
|
+
```tsx
|
|
867
|
+
import React, { useState } from "react";
|
|
868
|
+
import { createSuperAgent } from "@super-agent/sdk";
|
|
869
|
+
import { mount, Icon } from "@super-agent/sdk/widget";
|
|
870
|
+
import type {
|
|
871
|
+
HeaderProps,
|
|
872
|
+
MessageProps,
|
|
873
|
+
ActionBarProps,
|
|
874
|
+
WelcomeScreenProps,
|
|
875
|
+
} from "@super-agent/sdk/widget";
|
|
876
|
+
|
|
877
|
+
const sdk = createSuperAgent({ baseUrl: "/api/v1", botId: 1 });
|
|
878
|
+
sdk.setToken("demo_app", "demo_token");
|
|
879
|
+
|
|
880
|
+
function CustomHeader({ title, onClose, onToggleThreadList }: HeaderProps) {
|
|
881
|
+
return (
|
|
882
|
+
<div
|
|
883
|
+
style={{
|
|
884
|
+
display: "flex",
|
|
885
|
+
alignItems: "center",
|
|
886
|
+
padding: "12px 16px",
|
|
887
|
+
background: "#0f172a",
|
|
888
|
+
color: "#fff",
|
|
889
|
+
}}
|
|
890
|
+
>
|
|
891
|
+
<Icon name="bot" size={18} color="#fff" />
|
|
892
|
+
<span style={{ flex: 1, marginLeft: 10, fontWeight: 700 }}>{title}</span>
|
|
893
|
+
<button onClick={onToggleThreadList}>
|
|
894
|
+
<Icon name="menu" size={16} />
|
|
895
|
+
</button>
|
|
896
|
+
<button onClick={onClose}>
|
|
897
|
+
<Icon name="x" size={16} />
|
|
898
|
+
</button>
|
|
899
|
+
</div>
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function CustomMessage({
|
|
904
|
+
message,
|
|
905
|
+
isStreaming,
|
|
906
|
+
isLast,
|
|
907
|
+
avatar,
|
|
908
|
+
slots,
|
|
909
|
+
onCopy,
|
|
910
|
+
onRegenerate,
|
|
911
|
+
onFeedback,
|
|
912
|
+
}: MessageProps) {
|
|
913
|
+
const isUser = message.role === "user";
|
|
914
|
+
return (
|
|
915
|
+
<div
|
|
916
|
+
style={{
|
|
917
|
+
display: "flex",
|
|
918
|
+
gap: 12,
|
|
919
|
+
justifyContent: isUser ? "flex-end" : "flex-start",
|
|
920
|
+
padding: "4px 0",
|
|
921
|
+
}}
|
|
922
|
+
>
|
|
923
|
+
{!isUser && (
|
|
924
|
+
<div
|
|
925
|
+
style={{
|
|
926
|
+
width: 36,
|
|
927
|
+
height: 36,
|
|
928
|
+
borderRadius: 12,
|
|
929
|
+
background: "#6366f1",
|
|
930
|
+
}}
|
|
931
|
+
>
|
|
932
|
+
{avatar?.assistant || "A"}
|
|
933
|
+
</div>
|
|
934
|
+
)}
|
|
935
|
+
<div style={{ maxWidth: "75%" }}>
|
|
936
|
+
{message.parts.map((part, idx) => {
|
|
937
|
+
switch (part.type) {
|
|
938
|
+
case "text":
|
|
939
|
+
return (
|
|
940
|
+
<div key={idx} style={{ whiteSpace: "pre-wrap" }}>
|
|
941
|
+
{part.content}
|
|
942
|
+
</div>
|
|
943
|
+
);
|
|
944
|
+
case "thinking":
|
|
945
|
+
return (
|
|
946
|
+
<div
|
|
947
|
+
key={idx}
|
|
948
|
+
style={{ fontStyle: "italic", color: "#7c3aed" }}
|
|
949
|
+
>
|
|
950
|
+
{part.content}
|
|
951
|
+
</div>
|
|
952
|
+
);
|
|
953
|
+
case "tool_call":
|
|
954
|
+
return <div key={idx}>[调用 {part.toolName}]</div>;
|
|
955
|
+
case "tool_result":
|
|
956
|
+
return <div key={idx}>[结果] {part.content}</div>;
|
|
957
|
+
case "error":
|
|
958
|
+
return (
|
|
959
|
+
<div key={idx} style={{ color: "#b91c1c" }}>
|
|
960
|
+
{part.content}
|
|
961
|
+
</div>
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
})}
|
|
965
|
+
{isStreaming && isLast && !isUser && <span>|</span>}
|
|
966
|
+
{!isUser && !isStreaming && message.parts.length > 0 && (
|
|
967
|
+
<slots.ActionBar
|
|
968
|
+
message={message}
|
|
969
|
+
onCopy={onCopy}
|
|
970
|
+
onRegenerate={onRegenerate}
|
|
971
|
+
onFeedback={onFeedback}
|
|
972
|
+
/>
|
|
973
|
+
)}
|
|
974
|
+
</div>
|
|
975
|
+
{isUser && (
|
|
976
|
+
<div
|
|
977
|
+
style={{
|
|
978
|
+
width: 36,
|
|
979
|
+
height: 36,
|
|
980
|
+
borderRadius: 12,
|
|
981
|
+
background: "#475569",
|
|
982
|
+
}}
|
|
983
|
+
>
|
|
984
|
+
{avatar?.user || "U"}
|
|
985
|
+
</div>
|
|
986
|
+
)}
|
|
987
|
+
</div>
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function CustomActionBar({
|
|
992
|
+
message,
|
|
993
|
+
onCopy,
|
|
994
|
+
onRegenerate,
|
|
995
|
+
onFeedback,
|
|
996
|
+
}: ActionBarProps) {
|
|
997
|
+
const [liked, setLiked] = useState<"like" | "dislike" | null>(null);
|
|
998
|
+
return (
|
|
999
|
+
<div style={{ display: "flex", gap: 8, marginTop: 6 }}>
|
|
1000
|
+
<button onClick={onCopy}>复制</button>
|
|
1001
|
+
<button onClick={onRegenerate}>重新生成</button>
|
|
1002
|
+
<button
|
|
1003
|
+
onClick={() => {
|
|
1004
|
+
const v = liked === "like" ? null : "like";
|
|
1005
|
+
setLiked(v);
|
|
1006
|
+
if (v) onFeedback?.(message.id, v);
|
|
1007
|
+
}}
|
|
1008
|
+
style={{ color: liked === "like" ? "#6366f1" : "#9ca3af" }}
|
|
1009
|
+
>
|
|
1010
|
+
👍
|
|
1011
|
+
</button>
|
|
1012
|
+
<button
|
|
1013
|
+
onClick={() => {
|
|
1014
|
+
const v = liked === "dislike" ? null : "dislike";
|
|
1015
|
+
setLiked(v);
|
|
1016
|
+
if (v) onFeedback?.(message.id, v);
|
|
1017
|
+
}}
|
|
1018
|
+
style={{ color: liked === "dislike" ? "#6366f1" : "#9ca3af" }}
|
|
1019
|
+
>
|
|
1020
|
+
👎
|
|
1021
|
+
</button>
|
|
1022
|
+
</div>
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function CustomWelcomeScreen({
|
|
1027
|
+
message,
|
|
1028
|
+
suggestedPrompts,
|
|
1029
|
+
onPromptClick,
|
|
1030
|
+
}: WelcomeScreenProps) {
|
|
1031
|
+
return (
|
|
1032
|
+
<div
|
|
1033
|
+
style={{
|
|
1034
|
+
flex: 1,
|
|
1035
|
+
display: "flex",
|
|
1036
|
+
flexDirection: "column",
|
|
1037
|
+
alignItems: "center",
|
|
1038
|
+
justifyContent: "center",
|
|
1039
|
+
}}
|
|
1040
|
+
>
|
|
1041
|
+
<Icon name="sparkles" size={32} color="#6366f1" />
|
|
1042
|
+
<p>{message || "有什么可以帮你的?"}</p>
|
|
1043
|
+
{suggestedPrompts?.map((prompt) => (
|
|
1044
|
+
<button key={prompt} onClick={() => onPromptClick(prompt)}>
|
|
1045
|
+
{prompt}
|
|
1046
|
+
</button>
|
|
1047
|
+
))}
|
|
1048
|
+
</div>
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
mount("#chat-root", {
|
|
1053
|
+
sdk,
|
|
1054
|
+
mode: "fullpage",
|
|
1055
|
+
title: "AI 助手",
|
|
1056
|
+
welcomeMessage: "你好!我是 AI 助手",
|
|
1057
|
+
suggestedPrompts: ["查询订单", "写一段代码"],
|
|
1058
|
+
slots: {
|
|
1059
|
+
Header: CustomHeader,
|
|
1060
|
+
Message: CustomMessage,
|
|
1061
|
+
ActionBar: CustomActionBar,
|
|
1062
|
+
WelcomeScreen: CustomWelcomeScreen,
|
|
1063
|
+
},
|
|
1064
|
+
}).open();
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## 9. 会话管理
|
|
1070
|
+
|
|
1071
|
+
### 9.1 SDK 层面
|
|
1072
|
+
|
|
1073
|
+
| 方法 | 签名 | 说明 |
|
|
1074
|
+
| -------------------- | ----------------------------------------------- | ------------------------------------ |
|
|
1075
|
+
| `createSession` | `() => Promise<string>` | 创建新会话,返回 `sessionId` |
|
|
1076
|
+
| `listConversations` | `(params?) => Promise<ListConversationsResult>` | 获取会话列表(分页) |
|
|
1077
|
+
| `renameConversation` | `(sessionId, title) => Promise<void>` | 重命名会话 |
|
|
1078
|
+
| `deleteConversation` | `(sessionId) => Promise<void>` | 删除会话 |
|
|
1079
|
+
| `getMessages` | `(sessionId) => Promise<UIMessage[]>` | 获取会话历史消息(已转换为 UI 结构) |
|
|
1080
|
+
|
|
1081
|
+
```ts
|
|
1082
|
+
const sessionId = await sdk.createSession();
|
|
1083
|
+
const { items, total } = await sdk.listConversations({ page: 1, size: 20 });
|
|
1084
|
+
const messages = await sdk.getMessages(items[0].sessionId);
|
|
1085
|
+
await sdk.renameConversation(items[0].sessionId, "新标题");
|
|
1086
|
+
await sdk.deleteConversation(items[0].sessionId);
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
### 9.2 组件内置能力
|
|
1090
|
+
|
|
1091
|
+
- **自动加载**:挂载后自动调用 `listConversations` 拉取会话列表。
|
|
1092
|
+
- **切换**:点击会话项加载该会话历史消息并展示。
|
|
1093
|
+
- **新建**:点击「新会话」清空当前消息,下次发送时自动调用 `createSession()` 创建新会话。
|
|
1094
|
+
- **重命名**:`fullpage` 侧边栏或自定义 `ThreadList` 中调用 `renameConversation`。
|
|
1095
|
+
- **删除**:悬停会话项后点击删除按钮。
|
|
1096
|
+
- **本地持久化**:最近一次活跃会话写入 `localStorage`,键为 `sa_active_conversation`。
|
|
1097
|
+
|
|
1098
|
+
---
|
|
1099
|
+
|
|
1100
|
+
## 10. 中断恢复
|
|
1101
|
+
|
|
1102
|
+
### 10.1 中断请求
|
|
1103
|
+
|
|
1104
|
+
`chat()` 返回 `AbortController`,可随时中断:
|
|
1105
|
+
|
|
1106
|
+
```ts
|
|
1107
|
+
const controller = sdk.chat({
|
|
1108
|
+
sessionId,
|
|
1109
|
+
message: "...",
|
|
1110
|
+
stream: true,
|
|
1111
|
+
onDone,
|
|
1112
|
+
});
|
|
1113
|
+
controller.abort();
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
也可在 `ChatOptions.signal` 传入外部 `AbortSignal`,SDK 会将其与内部信号合并,任一触发即中断:
|
|
1117
|
+
|
|
1118
|
+
```ts
|
|
1119
|
+
const ac = new AbortController();
|
|
1120
|
+
sdk.chat({ sessionId, message: "...", signal: ac.signal });
|
|
1121
|
+
ac.abort();
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### 10.2 会话恢复
|
|
1125
|
+
|
|
1126
|
+
页面刷新或组件重新挂载时,会话状态自动恢复:
|
|
1127
|
+
|
|
1128
|
+
1. 挂载时从 `localStorage` 读取上次活跃会话 ID(键 `sa_active_conversation`)。
|
|
1129
|
+
2. 若该会话仍在列表中,自动调用 `getMessages` 拉取历史消息并渲染。
|
|
1130
|
+
3. 若存储的会话已不存在(被删除),则自动清空并回退到「新会话」。
|
|
1131
|
+
|
|
1132
|
+
整个过程对用户透明,无需额外配置。
|
|
1133
|
+
|
|
1134
|
+
---
|
|
1135
|
+
|
|
1136
|
+
## 11. 消息类型
|
|
1137
|
+
|
|
1138
|
+
### 11.1 UIMessage
|
|
1139
|
+
|
|
1140
|
+
```ts
|
|
1141
|
+
export interface UIMessage {
|
|
1142
|
+
id: string;
|
|
1143
|
+
role: "user" | "assistant";
|
|
1144
|
+
parts: MessagePart[];
|
|
1145
|
+
timestamp: number;
|
|
1146
|
+
}
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### 11.2 MessagePart
|
|
1150
|
+
|
|
1151
|
+
```ts
|
|
1152
|
+
export type MessagePart =
|
|
1153
|
+
| { type: "text"; content: string }
|
|
1154
|
+
| { type: "thinking"; content: string }
|
|
1155
|
+
| { type: "tool_call"; toolName: string; toolCallId: string; args: string }
|
|
1156
|
+
| {
|
|
1157
|
+
type: "tool_result";
|
|
1158
|
+
toolName: string;
|
|
1159
|
+
toolCallId: string;
|
|
1160
|
+
content: string;
|
|
1161
|
+
}
|
|
1162
|
+
| { type: "error"; content: string };
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
| type | 字段 | 说明 |
|
|
1166
|
+
| ------------- | ----------------------------------- | ------------------------------- |
|
|
1167
|
+
| `text` | `content` | 文本 / Markdown 内容 |
|
|
1168
|
+
| `thinking` | `content` | 思考过程(reasoning) |
|
|
1169
|
+
| `tool_call` | `toolName`, `toolCallId`, `args` | 工具调用,`args` 为 JSON 字符串 |
|
|
1170
|
+
| `tool_result` | `toolName`, `toolCallId`, `content` | 工具返回结果 |
|
|
1171
|
+
| `error` | `content` | 错误信息 |
|
|
1172
|
+
|
|
1173
|
+
### 11.3 ChatEvent(流式事件)
|
|
1174
|
+
|
|
1175
|
+
```ts
|
|
1176
|
+
export interface ChatEvent {
|
|
1177
|
+
type:
|
|
1178
|
+
| "user"
|
|
1179
|
+
| "thinking"
|
|
1180
|
+
| "ai"
|
|
1181
|
+
| "tool_call"
|
|
1182
|
+
| "tool_result"
|
|
1183
|
+
| "done"
|
|
1184
|
+
| "error";
|
|
1185
|
+
content: string;
|
|
1186
|
+
toolName?: string;
|
|
1187
|
+
toolCallId?: string;
|
|
1188
|
+
args?: string; // JSON.stringify 后的字符串
|
|
1189
|
+
sessionId?: string;
|
|
1190
|
+
}
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
---
|
|
1194
|
+
|
|
1195
|
+
## 12. 高级用法
|
|
1196
|
+
|
|
1197
|
+
### 12.1 纯 API 模式(不用 Widget)
|
|
1198
|
+
|
|
1199
|
+
不挂载组件,仅使用 API 层构建自己的 UI:
|
|
1200
|
+
|
|
1201
|
+
```ts
|
|
1202
|
+
import { createSuperAgent } from "@super-agent/sdk";
|
|
1203
|
+
|
|
1204
|
+
const sdk = createSuperAgent({ baseUrl: "/api/v1", botId: 1 });
|
|
1205
|
+
sdk.setToken("<APP_ID>", "<TOKEN>");
|
|
1206
|
+
|
|
1207
|
+
const sessionId = await sdk.createSession();
|
|
1208
|
+
|
|
1209
|
+
let full = "";
|
|
1210
|
+
sdk.chat({
|
|
1211
|
+
sessionId,
|
|
1212
|
+
message: "你好",
|
|
1213
|
+
onMessage: (e) => {
|
|
1214
|
+
if (e.type === "ai") full += e.content;
|
|
1215
|
+
},
|
|
1216
|
+
onDone: ({ sessionId, content }) => console.log(sessionId, content),
|
|
1217
|
+
onError: (err) => console.error(err),
|
|
1218
|
+
});
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
### 12.2 使用 Hooks
|
|
1222
|
+
|
|
1223
|
+
从 `@super-agent/sdk/widget` 导出 `useChat`、`useConversations`,可在自己的 React 应用中复用:
|
|
1224
|
+
|
|
1225
|
+
```ts
|
|
1226
|
+
export function useChat(options: UseChatOptions): UseChatReturn;
|
|
1227
|
+
|
|
1228
|
+
interface UseChatOptions {
|
|
1229
|
+
sdk: SuperAgentSDK;
|
|
1230
|
+
sessionId: string | null;
|
|
1231
|
+
onSessionCreated?: (sessionId: string) => void;
|
|
1232
|
+
onStreamStart?: (sessionId: string) => void;
|
|
1233
|
+
onStreamEnd?: (sessionId: string) => void;
|
|
1234
|
+
onError?: (error: Error) => void;
|
|
1235
|
+
onMessageSend?: (message: string) => void;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
interface UseChatReturn {
|
|
1239
|
+
messages: UIMessage[];
|
|
1240
|
+
status: ChatStatus;
|
|
1241
|
+
error: Error | null;
|
|
1242
|
+
sendMessage: (content: string) => void;
|
|
1243
|
+
stop: () => void;
|
|
1244
|
+
regenerate: () => void;
|
|
1245
|
+
setMessages: (messages: UIMessage[]) => void;
|
|
1246
|
+
}
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
```ts
|
|
1250
|
+
export function useConversations(
|
|
1251
|
+
options: UseConversationsOptions,
|
|
1252
|
+
): UseConversationsReturn;
|
|
1253
|
+
|
|
1254
|
+
interface UseConversationsOptions {
|
|
1255
|
+
sdk: SuperAgentSDK;
|
|
1256
|
+
onConversationChange?: (sessionId: string) => void;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
interface UseConversationsReturn {
|
|
1260
|
+
conversations: Conversation[];
|
|
1261
|
+
activeConversationId: string | null;
|
|
1262
|
+
loading: boolean;
|
|
1263
|
+
loadConversations: () => Promise<void>;
|
|
1264
|
+
switchConversation: (sessionId: string) => Promise<UIMessage[]>;
|
|
1265
|
+
newConversation: () => void;
|
|
1266
|
+
deleteConversation: (sessionId: string) => Promise<void>;
|
|
1267
|
+
renameConversation: (sessionId: string, title: string) => Promise<void>;
|
|
1268
|
+
setActiveConversationId: (id: string | null) => void;
|
|
1269
|
+
}
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
```tsx
|
|
1273
|
+
function MyChat({ sdk }: { sdk: SuperAgentSDK }) {
|
|
1274
|
+
const conv = useConversations({ sdk });
|
|
1275
|
+
const chat = useChat({ sdk, sessionId: conv.activeConversationId });
|
|
1276
|
+
|
|
1277
|
+
return (
|
|
1278
|
+
<div>
|
|
1279
|
+
{chat.messages.map((m) => (
|
|
1280
|
+
<div key={m.id}>
|
|
1281
|
+
{m.role}:{" "}
|
|
1282
|
+
{m.parts.map((p) => (p.type === "text" ? p.content : "")).join("")}
|
|
1283
|
+
</div>
|
|
1284
|
+
))}
|
|
1285
|
+
<button onClick={() => chat.sendMessage("你好")}>发送</button>
|
|
1286
|
+
</div>
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
> `useChat` 在 `sessionId` 为空时会自动调用 `sdk.createSession()` 创建新会话,无需手动处理。
|
|
1292
|
+
|
|
1293
|
+
### 12.3 Token 续期
|
|
1294
|
+
|
|
1295
|
+
请求返回 `401` 时,SDK 会自动重新调用 `POST /token` 获取新凭证并重试(并发请求合并为一次刷新),无需手动处理。
|
|
1296
|
+
|
|
1297
|
+
### 12.4 取消请求
|
|
1298
|
+
|
|
1299
|
+
`chat()` 返回 `AbortController`;或传入外部 `AbortSignal`(SDK 会合并内部信号,任一触发即中断):
|
|
1300
|
+
|
|
1301
|
+
```ts
|
|
1302
|
+
const controller = sdk.chat({
|
|
1303
|
+
sessionId,
|
|
1304
|
+
message: "...",
|
|
1305
|
+
stream: true,
|
|
1306
|
+
onDone,
|
|
1307
|
+
});
|
|
1308
|
+
// 稍后取消
|
|
1309
|
+
controller.abort();
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
```ts
|
|
1313
|
+
const ac = new AbortController();
|
|
1314
|
+
sdk.chat({ sessionId, message: "...", signal: ac.signal });
|
|
1315
|
+
ac.abort();
|
|
1316
|
+
```
|