chatablex-web-sdk 1.0.0 → 1.0.3

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.zh-CN.md CHANGED
@@ -1,10 +1,62 @@
1
- # chatablex-web-sdk
1
+ # ChatableX Web SDK
2
2
 
3
3
  **[English](README.md)** | 简体中文
4
4
 
5
- 用于构建 **ChatableX AI App** WebUI 应用的运行时 SDK
6
-
7
- 与仅提供类型的包不同,本 SDK 包含将 Web 应用连接到 ChatableX Flutter 宿主的真实桥接运行时。你必须将其作为依赖安装.
5
+ **用于构建 ChatableX AI App WebUI 扩展的官方运行时 SDK。**
6
+
7
+ `chatablex-web-sdk` 是将你的 Web 应用连接到 **ChatableX 桌面客户端**(Flutter WebView 宿主)的官方 JavaScript/TypeScript 库。它不是纯类型包,而是包含真实桥接运行时:RPC 请求/响应、事件订阅、工具执行回调。
8
+
9
+ 你的 WebUI 运行在 WebView 中。许多能力——原生对话框、文件选择、与会话对齐的存储、走宿主 AI 管线的对话——仅靠浏览器 API 难以实现或体验不一致。本 SDK 将它们封装为带类型的 Promise API。
10
+
11
+ ---
12
+
13
+ ## 目录
14
+
15
+ - [环境要求](#环境要求)
16
+ - [安装](#安装)
17
+ - [快速开始](#快速开始)
18
+ - [项目配置](#项目配置)
19
+ - [架构说明](#架构说明)
20
+ - [核心概念:工具执行](#核心概念工具执行)
21
+ - [API 参考](#api-参考)
22
+ - [ChatableX(入口)](#chatablex入口)
23
+ - [sdk.tool](#sdktool)
24
+ - [sdk.events](#sdkevents)
25
+ - [sdk.ai](#sdkai)
26
+ - [sdk.ui](#sdkui)
27
+ - [sdk.storage](#sdkstorage)
28
+ - [sdk.tools](#sdktools)
29
+ - [sdk.platform](#sdkplatform)
30
+ - [事件参考](#事件参考)
31
+ - [权限声明](#权限声明)
32
+ - [宿主能力矩阵](#宿主能力矩阵)
33
+ - [本地开发](#本地开发)
34
+ - [框架集成](#框架集成)
35
+ - [TypeScript 类型](#typescript-类型)
36
+ - [最佳实践](#最佳实践)
37
+ - [故障排查](#故障排查)
38
+ - [官方示例](#官方示例)
39
+ - [版本说明](#版本说明)
40
+ - [许可证](#许可证)
41
+
42
+ ---
43
+
44
+ ## 环境要求
45
+
46
+ | 要求 | 说明 |
47
+ |------|------|
48
+ | **ChatableX 客户端** | 支持 WebView 桥接的桌面应用(Flutter 宿主) |
49
+ | **扩展模式** | `manifest.json` 中 `execution_mode: "webapp"` |
50
+ | **Node.js** | ≥ 16(用于构建 WebUI) |
51
+ | **构建产物** | `webui.entry` 须指向 `./dist/index.html`(Vite 或同类工具) |
52
+ | **SDK 安装** | **必须** `npm install chatablex-web-sdk`——宿主**不会**自动注入 SDK |
53
+
54
+ 平台从你的扩展中消费两样东西:
55
+
56
+ 1. **构建产物**:`chatablex.webapp.webui.entry` 指向的文件(通常为 `./dist/index.html`)
57
+ 2. **桥接调用**:通过本 SDK(`ChatableX.init`、`sdk.tool.onExecute` 等)
58
+
59
+ ---
8
60
 
9
61
  ## 安装
10
62
 
@@ -12,188 +64,727 @@
12
64
  npm install chatablex-web-sdk
13
65
  ```
14
66
 
67
+ 在 monorepo 中本地联调:
68
+
69
+ ```bash
70
+ npm install ../chatablex-web-sdk
71
+ # 或
72
+ npm install file:../chatablex-web-sdk
73
+ ```
74
+
75
+ **包导出**(ESM + CJS + TypeScript 声明):
76
+
77
+ ```ts
78
+ import { ChatableX } from 'chatablex-web-sdk';
79
+ import type { ChatableXSDK, ToolResult, ChatResponse } from 'chatablex-web-sdk';
80
+ ```
81
+
82
+ ---
83
+
15
84
  ## 快速开始
16
85
 
17
- ```tsx
86
+ 最小集成——在 WebUI 中响应 LLM 工具调用:
87
+
88
+ ```ts
18
89
  import { ChatableX } from 'chatablex-web-sdk';
19
90
 
20
- // 初始化 —— 连接 Flutter WebView 宿主
21
- const sdk = await ChatableX.init({ appId: 'my-app', debug: true });
91
+ async function main() {
92
+ const sdk = await ChatableX.init({
93
+ appId: 'my-counter-app', // 须与 manifest.json 的 "id" 一致
94
+ debug: true,
95
+ });
22
96
 
23
- // 注册工具处理器(当 LLM 调用你的工具时触发)
24
- sdk.tool.onExecute(async (params) => {
25
- const { action, value } = params;
26
- // ... 执行操作、更新 UI ...
27
- return { success: true, result: 42 };
28
- });
97
+ sdk.tool.onExecute(async (params) => {
98
+ const { action, value } = params;
99
+
100
+ if (action === 'increment') {
101
+ const next = (Number(value) || 0) + 1;
102
+ return { success: true, data: { value: next } };
103
+ }
104
+
105
+ return { success: false, error: `Unknown action: ${action}` };
106
+ });
107
+ }
108
+
109
+ main().catch(console.error);
29
110
  ```
30
111
 
31
- ## SDK 模块是做什么的?
112
+ **你不需要用到每一个模块。** 生产环境的最小集成通常只需 `sdk.tool`。按需添加 `sdk.storage`、`sdk.events`、`sdk.ui` 等。
113
+
114
+ ---
115
+
116
+ ## 项目配置
117
+
118
+ ### manifest.json(webapp 扩展)
119
+
120
+ ```json
121
+ {
122
+ "id": "my-counter-app",
123
+ "name": "Counter App",
124
+ "version": "1.0.0",
125
+ "type": "app",
126
+ "execution_mode": "webapp",
127
+ "return_direct": true,
128
+ "permissions": ["notification"],
129
+ "tools": [
130
+ {
131
+ "name": "counter_control",
132
+ "description": "控制计数器组件",
133
+ "inputSchema": {
134
+ "type": "object",
135
+ "properties": {
136
+ "action": { "type": "string", "enum": ["increment", "decrement", "get"] },
137
+ "value": { "type": "number" }
138
+ },
139
+ "required": ["action"]
140
+ }
141
+ }
142
+ ],
143
+ "chatablex": {
144
+ "webapp": {
145
+ "webui": {
146
+ "entry": "./dist/index.html"
147
+ }
148
+ }
149
+ }
150
+ }
151
+ ```
32
152
 
33
- `ChatableX.init()` 返回的 `sdk` 是一个**按职责划分的 API 集合**。WebUI 跑在 WebView 里,很多能力(系统对话框、文件选择、与主聊天同步的存储、走宿主模型的对话等)在浏览器里要么不好做、要么和 Flutter 主应用割裂。这些模块都通过 **JS ↔ Flutter 桥** 调用宿主已实现的能力。
153
+ | 字段 | 规则 |
154
+ |------|------|
155
+ | `id` | 必须等于 `ChatableX.init({ appId })` |
156
+ | `execution_mode` | 必须为 `"webapp"` |
157
+ | `webui.entry` | 相对路径 → 本地 HTTP 服务;`https://` → 远程 URL |
158
+ | `tools[]` | 声明 LLM 可调用的函数;宿主将参数转发给 `sdk.tool.onExecute` |
159
+ | `permissions` | 控制宿主侧 API 访问——见[权限声明](#权限声明) |
160
+
161
+ ### package.json 脚本
162
+
163
+ ```json
164
+ {
165
+ "scripts": {
166
+ "dev": "vite",
167
+ "build": "vite build",
168
+ "preview": "vite preview"
169
+ },
170
+ "dependencies": {
171
+ "chatablex-web-sdk": "^1.0.0"
172
+ }
173
+ }
174
+ ```
34
175
 
35
- **你不一定要用到每一个模块**:最小集成往往只需要 `sdk.tool`(响应 LLM 调用);其余按需选用。
176
+ 发布前执行 `npm run build`。ChatableX 客户端加载的是 `dist/index.html`,而非开发服务器(除非你配置了远程 `webui.entry` URL)。
36
177
 
178
+ ### 推荐项目结构
37
179
 
38
- | 命名空间 | 用途(为什么要单独拆出来) |
39
- |----------|---------------------------|
40
- | **`sdk.tool`** | 注册工具执行逻辑:LLM 调用你的工具时由宿主把参数传进来,你在 WebUI 里算完再返回结果。这是 AI App 的**核心**入口。 |
41
- | **`sdk.events`** | 订阅宿主侧事件(如用户消息、流式内容),让 WebUI 与当前会话状态对齐。 |
42
- | **`sdk.ai`** | WebUI 内向**宿主同一条 AI 管线**发消息或拉取会话上下文(`chat` / `getContext` 等),避免你在页面里自己再接一套模型。 |
43
- | **`sdk.ui`** | 用**宿主原生 UI** 做提示、确认框、选文件、通知主界面刷新等,体验和权限与桌面客户端一致。 |
44
- | **`sdk.storage`** | 键值存储走**宿主侧**,便于与 App 其它部分共享状态、持久化,而不只存在页面的 `localStorage` 里。 |
45
- | **`sdk.tools` / `sdk.skills`** | 列举或调用平台上其它工具、技能,做编排或联动。 |
180
+ ```
181
+ my-app/
182
+ ├── manifest.json # 扩展元数据
183
+ ├── package.json
184
+ ├── index.html # Vite 入口 HTML
185
+ ├── src/
186
+ │ ├── main.ts # ChatableX.init() + 应用启动
187
+ │ ├── app.ts # UI 逻辑
188
+ │ └── bridge.ts # 可选:工具路由辅助
189
+ ├── dist/ # 构建产物(宿主加载)
190
+ │ └── index.html
191
+ └── vite.config.ts
192
+ ```
46
193
 
47
- 下面「API」各节包含:**典型场景**(什么时候用)+ **示例代码**(怎么接)。
194
+ ---
48
195
 
49
- ## API
196
+ ## 架构说明
50
197
 
51
- ### `ChatableX.init(config)`
198
+ ```
199
+ ┌──────────────────────────────────────────────────────────────┐
200
+ │ 你的 Web 应用(React / Vue / Svelte / 原生 JS) │
201
+ │ import { ChatableX } from 'chatablex-web-sdk' │
202
+ └────────────────────────────┬─────────────────────────────────┘
203
+
204
+
205
+ ┌──────────────────────────────────────────────────────────────┐
206
+ │ chatablex-web-sdk │
207
+ │ │
208
+ │ Bridge(RPC + 事件) │
209
+ │ JS → 宿主 : window.ChatableXBridge.postMessage(JSON) │
210
+ │ 宿主 → JS : window.ChatableXReceive(JSON) │
211
+ │ │
212
+ │ 模块:tool · events · ai · ui · storage · tools · │
213
+ │ tools · platform │
214
+ └────────────────────────────┬─────────────────────────────────┘
215
+ │ WebView JavaScriptChannel
216
+
217
+ ┌──────────────────────────────────────────────────────────────┐
218
+ │ ChatableX Flutter 客户端 │
219
+ │ 聊天 UI · SSE 流 · Agent 图 · SQLite 存储 │
220
+ └──────────────────────────────────────────────────────────────┘
221
+ ```
52
222
 
223
+ ### 桥接协议
53
224
 
54
- | 选项 | 类型 | 默认值 | 说明 |
55
- | --------- | ------- | ----- | ------------------------------------- |
56
- | `appId` | string | — | **必填。**须与 `manifest.json` 中的 `id` 一致。 |
57
- | `debug` | boolean | false | 是否在控制台打印调试日志。 |
58
- | `timeout` | number | 10000 | 握手超时时间(毫秒)。 |
225
+ **请求(JS Flutter):**
59
226
 
227
+ ```json
228
+ {
229
+ "id": "ctx_1_1718200000000",
230
+ "method": "storage.get",
231
+ "params": { "key": "filters" },
232
+ "timestamp": 1718200000000
233
+ }
234
+ ```
60
235
 
61
- 返回 `Promise<ChatableXSDK>`。
236
+ **响应(Flutter → JS):**
62
237
 
63
- ### `sdk.tool`
238
+ ```json
239
+ {
240
+ "type": "response",
241
+ "id": "ctx_1_1718200000000",
242
+ "success": true,
243
+ "data": { "projectId": "p1" }
244
+ }
245
+ ```
246
+
247
+ **事件推送(Flutter → JS):**
248
+
249
+ ```json
250
+ {
251
+ "type": "event",
252
+ "eventType": "toolExecution",
253
+ "data": { "action": "increment", "_requestId": "texec_1_...", "_toolName": "counter_control" }
254
+ }
255
+ ```
64
256
 
65
- **典型场景**:用户在聊天里触发你的 AI App,或模型根据上下文调用你的工具;宿主把 JSON 参数推进 WebView,你要执行逻辑并把结果返回给会话。
257
+ **工具结果(JS Flutter,即发即忘):**
258
+
259
+ ```json
260
+ {
261
+ "method": "tool.executeResult",
262
+ "params": {
263
+ "_requestId": "texec_1_...",
264
+ "success": true,
265
+ "data": { "value": 42 }
266
+ }
267
+ }
268
+ ```
269
+
270
+ > `tool.executeResult` **不使用** RPC 的 `id` 字段。宿主通过 `_requestId` 关联结果。这是因为 WebView 的 `evaluateJavaScript` 无法 await Promise。
271
+
272
+ ### 初始化流程
273
+
274
+ 1. 你的 bundle 在 WebView 中加载。
275
+ 2. 调用 `ChatableX.init({ appId })`。
276
+ 3. SDK 安装 `window.ChatableXReceive`。
277
+ 4. SDK 等待 `window.ChatableXBridge`(由 Flutter 设置)。
278
+ 5. SDK 发送 `sdk_init` 握手 → 宿主返回工具元数据。
279
+ 6. SDK 暴露 `window.ChatableX` 并返回 `sdk` 对象。
280
+
281
+ ---
282
+
283
+ ## 核心概念:工具执行
284
+
285
+ 这是 AI App 的**主要集成路径**。当 LLM 调用你的工具时,宿主将参数推入 WebUI 并等待结果。
286
+
287
+ ```
288
+ LLM(Agent) Flutter 宿主 你的 WebUI(SDK)
289
+ │ │ │
290
+ │ frontend_tool_call │ │
291
+ │────────────────────>│ │
292
+ │ │ event: toolExecution │
293
+ │ │ { ...args, _requestId } │
294
+ │ │─────────────────────────>│
295
+ │ │ │ onExecute(params)
296
+ │ │ │ → 你的业务逻辑
297
+ │ │ tool.executeResult │
298
+ │ │<─────────────────────────│
299
+ │ tool-result POST │ │
300
+ │<────────────────────│ │
301
+ │ Agent 继续推理 │ │
302
+ ```
303
+
304
+ ### 处理器契约
66
305
 
67
306
  ```ts
68
307
  sdk.tool.onExecute(async (params) => {
69
- const { action, rowId } = params as { action?: string; rowId?: string };
70
- if (action === 'delete') {
71
- await deleteRow(rowId);
72
- return { success: true, message: '已删除' };
308
+ // params 包含 LLM 参数 + 宿主元数据:
309
+ // _toolName — 被调用的 manifest 工具名(string)
310
+ // _requestId — 关联 ID(string,由宿主设置)
311
+
312
+ return {
313
+ success: true, // 必填
314
+ data: { /* 任意 */ }, // 可选,返回给 LLM
315
+ error: '原因', // 可选,success 为 false 时
316
+ };
317
+ });
318
+ ```
319
+
320
+ | 返回字段 | 类型 | 说明 |
321
+ |----------|------|------|
322
+ | `success` | `boolean` | 操作是否成功 |
323
+ | `data` | `unknown` | 传给 LLM / 会话的载荷(任意可 JSON 序列化的值) |
324
+ | `error` | `string` | `success: false` 时的可读错误信息 |
325
+
326
+ **规则:**
327
+
328
+ - 通过 `onExecute` 注册**一个**处理器。再次调用会**覆盖**之前的处理器。
329
+ - 处理器抛出的异常会被捕获并转为 `{ success: false, error: message }`。
330
+ - 未注册处理器时,宿主收到 `{ success: false, error: 'No execute handler registered' }`。
331
+ - 多工具扩展务必按 `params._toolName` 路由(参考下方示例)。
332
+ - 若 30 秒内未收到 `tool.executeResult`,宿主会超时。
333
+
334
+ ### 多工具路由示例
335
+
336
+ ```ts
337
+ sdk.tool.onExecute(async (params) => {
338
+ const toolName = typeof params._toolName === 'string' ? params._toolName : '';
339
+
340
+ switch (toolName) {
341
+ case 'counter_control':
342
+ return handleCounter(params);
343
+ case 'export_data':
344
+ return handleExport(params);
345
+ default:
346
+ return { success: false, error: `Unknown tool: ${toolName}` };
73
347
  }
74
- return { success: false, error: 'unknown action' };
75
348
  });
349
+ ```
350
+
351
+ ---
352
+
353
+ ## API 参考
354
+
355
+ ### ChatableX(入口)
356
+
357
+ #### `ChatableX.init(config): Promise<ChatableXSDK>`
358
+
359
+ 初始化 SDK 并连接 Flutter 宿主。
360
+
361
+ | 选项 | 类型 | 默认值 | 说明 |
362
+ |------|------|--------|------|
363
+ | `appId` | `string` | — | **必填。** 须与 `manifest.json` 的 `id` 一致。 |
364
+ | `debug` | `boolean` | `false` | 将桥接日志输出到 `console`。 |
365
+ | `timeout` | `number` | `10000` | 等待 `ChatableXBridge` 的超时时间(毫秒)。 |
366
+
367
+ 返回单例。后续 `init()` 调用返回同一实例(以首次 `appId` 为准)。
368
+
369
+ 若在 `timeout` 内 `ChatableXBridge` 不可用则抛出异常。
76
370
 
77
- // 展示 manifest 里的名称、版本等(握手后由宿主填充)
371
+ #### `ChatableX.getInstance(): ChatableXSDK`
372
+
373
+ 返回当前实例。若尚未调用 `init()` 则抛出异常。
374
+
375
+ #### `ChatableX.isReady(): boolean`
376
+
377
+ 首次 `init()` 成功后为 `true`。
378
+
379
+ #### `ChatableX.version: string`
380
+
381
+ 当前 SDK 版本(如 `"1.0.0"`)。
382
+
383
+ ---
384
+
385
+ ### `sdk.tool`
386
+
387
+ 注册并查询扩展的工具执行处理器。
388
+
389
+ | 方法 | 签名 | 说明 |
390
+ |------|------|------|
391
+ | `onExecute` | `(handler) => void` | 注册 LLM 工具处理器。**webapp 扩展必填。** |
392
+ | `getInfo` | `() => ToolInfo` | 握手后由宿主填充的元数据(`id`、`name`、`version`、`description`)。 |
393
+
394
+ ```ts
78
395
  const info = sdk.tool.getInfo();
396
+ // { id: 'my-app', name: 'My App', version: '1.0.0', description: '...' }
79
397
  ```
80
398
 
399
+ ---
400
+
81
401
  ### `sdk.events`
82
402
 
83
- **典型场景**:侧边 WebUI 要做「实时仪表盘」——主窗口里用户发了新消息、AI 流式输出、或其它工具执行完成时,你的面板要同步高亮或刷新。
403
+ 订阅宿主推送的事件。每次订阅也会向宿主发送 `events.subscribe`,告知宿主需要转发对应事件。
84
404
 
85
- ```ts
86
- const unsubUser = sdk.events.onUserMessage(({ message }) => {
87
- appendActivityFeed(`用户:${message}`);
88
- });
405
+ | 方法 | 说明 |
406
+ |------|------|
407
+ | `on(eventType, callback)` | 通用订阅。返回 `unsubscribe` 函数。 |
408
+ | `onAiResponse(callback)` | `'aiResponse'` 的简写。 |
409
+ | `onToolExecution(callback)` | `'toolExecution'` 的简写。 |
410
+ | `onUserMessage(callback)` | `'userMessage'` 的简写。 |
89
411
 
90
- const unsubStream = sdk.events.on('streamingContent', ({ content, finished }) => {
91
- setPartialReply(content);
412
+ ```ts
413
+ const unsub = sdk.events.on('streamingContent', ({ content, finished }) => {
414
+ appendToken(content);
92
415
  if (finished) setLoading(false);
93
416
  });
94
417
 
95
- const unsubAi = sdk.events.onAiResponse((data) => {
96
- setLastReply(data.content);
97
- });
98
-
99
- // 组件卸载时取消订阅,避免泄漏
100
- // unsubUser(); unsubStream(); unsubAi();
418
+ // 组件卸载时清理
419
+ unsub();
101
420
  ```
102
421
 
103
- 事件名还可选:`toolExecution`、`close` 等(与宿主实现一致)。
422
+ > **注意:** `unsubscribe()` 仅移除本地监听器。当前 SDK 版本不会通过 `events.unsubscribe` 通知宿主。
423
+
424
+ ---
104
425
 
105
426
  ### `sdk.ai`
106
427
 
107
- **典型场景**:在工具面板里提供「针对当前会话再问一句」按钮——走宿主已配置的模型与上下文,而不是在页面里单独接第三方 API;或拉取会话上下文做摘要展示。
428
+ WebUI 调用宿主的 AI 管线。须在 `manifest.json` 中声明 `ai_chat` 权限。
429
+
430
+ | 方法 | 签名 | 说明 |
431
+ |------|------|------|
432
+ | `chat` | `(message, options?) => Promise<ChatResponse>` | 通过宿主 AI 栈发送消息。 |
433
+ | `chatStream` | `(message, options?) => Promise<unknown>` | 发起流式对话。Token 通过 `sdk.events.on('streamingContent')` 推送。 |
434
+ | `getContext` | `() => Promise<SessionContext>` | 获取当前会话元数据和消息列表。 |
108
435
 
109
436
  ```ts
110
- const reply = await sdk.ai.chat('把上一条用户消息总结成三点', {
437
+ const reply = await sdk.ai.chat('总结最近三条消息', {
438
+ sessionId: '可选覆盖',
111
439
  stream: false,
112
440
  });
113
441
 
114
442
  const ctx = await sdk.ai.getContext();
115
- if (ctx.name) setSessionTitle(ctx.name); // 部分字段视宿主实现而定
116
-
117
- // 流式由宿主推送;也可按需调用 chatStream(视宿主支持而定)
118
- await sdk.ai.chatStream('写一段简短回复', { stream: true });
443
+ console.log(ctx.messages.length, ctx.activeTools);
119
444
  ```
120
445
 
446
+ **`ChatOptions`:** `sessionId`、`context`、`tools`、`skills`、`stream`。
447
+
448
+ ---
449
+
121
450
  ### `sdk.ui`
122
451
 
123
- **典型场景**:危险操作要用**系统级确认框**;完成长任务后用**原生通知**;需要访问用户文件时用**宿主文件选择器**;操作结束后让主窗口**刷新消息列表或关闭 WebUI**。
452
+ WebUI 驱动宿主原生 UI。
124
453
 
125
- ```ts
126
- await sdk.ui.showNotification('导出完成', 'success');
454
+ | 方法 | 签名 | 权限 | 说明 |
455
+ |------|------|------|------|
456
+ | `showNotification` | `(message, type?) => Promise<void>` | `notification` | 吐司:`info` \| `success` \| `warning` \| `error`。 |
457
+ | `showConfirm` | `(title, message) => Promise<boolean>` | — | 原生确认框。确认返回 `true`。 |
458
+ | `pickFile` | `(options?) => Promise<string \| null>` | `file_access` | 打开原生文件选择器。取消返回 `null`。 |
459
+ | `openTab` | `(config) => Promise<void>` | — | 请求在宿主中打开新标签页。 |
460
+ | `updateState` | `(state) => Promise<void>` | — | 通知宿主刷新 UI(如 `{ refreshMessages: true }`)。 |
127
461
 
128
- const ok = await sdk.ui.showConfirm('删除记录', '此操作不可撤销,是否继续?');
462
+ ```ts
463
+ const ok = await sdk.ui.showConfirm('删除', '此操作不可撤销。');
129
464
  if (!ok) return;
130
465
 
131
- const path = await sdk.ui.pickFile({ type: 'image' });
132
- if (path) await uploadPreview(path);
133
-
466
+ await sdk.ui.showNotification('已保存', 'success');
134
467
  await sdk.ui.updateState({ refreshMessages: true });
135
- // await sdk.ui.openTab({ title: '详情', type: 'custom', data: { id: 'x' } });
136
468
  ```
137
469
 
470
+ **`FilePickerOptions`:** `type`(`any` \| `image` \| `video` \| `audio` \| `custom`)、`multiple`、`allowedExtensions`。
471
+
472
+ **`TabConfig`:** `id`、`title`、`type`(`chat` \| `tool` \| `skill` \| `custom`),可选 `icon`、`data`。
473
+
474
+ > **仅宿主支持:** `ui.saveFile`(原生另存为对话框)已在 Flutter 宿主实现,但尚未封装到本 SDK。高级集成可通过原始 `ChatableXBridge.postMessage` 调用。
475
+
476
+ ---
477
+
138
478
  ### `sdk.storage`
139
479
 
140
- **典型场景**:记住用户在面板里的筛选条件、布局或草稿;数据存在**宿主侧**,可和 App 其它部分一致、也比仅 `localStorage` 更符合桌面端预期。
480
+ 由宿主持久化的键值存储(SQLite,按工具隔离)。需要数据在 WebView 重置后保留、或与桌面端对齐时,应使用此模块而非 `localStorage`。
481
+
482
+ | 方法 | 签名 | 说明 |
483
+ |------|------|------|
484
+ | `get` | `<T>(key) => Promise<T \| null>` | 读取值。不存在时返回 `null`。 |
485
+ | `set` | `<T>(key, value) => Promise<void>` | 写入可 JSON 序列化的值。 |
486
+ | `delete` | `(key) => Promise<void>` | 删除键。 |
141
487
 
142
488
  ```ts
143
- const KEY = 'my-app:filters';
489
+ const KEY = 'my-app:draft';
144
490
 
145
- await sdk.storage.set(KEY, { projectId: 'p1', sort: 'date' });
146
- const filters = await sdk.storage.get<{ projectId: string; sort: string }>(KEY);
491
+ await sdk.storage.set(KEY, { title: '草稿', nodes: [] });
492
+ const draft = await sdk.storage.get<{ title: string }>(KEY);
147
493
  await sdk.storage.delete(KEY);
148
494
  ```
149
495
 
150
- ### `sdk.tools` / `sdk.skills`
496
+ 存储键在宿主侧按工具实例隔离。
497
+
498
+ ---
151
499
 
152
- **典型场景**:面板上的「一键流程」——按固定顺序调用**其它已安装工具**;或展示表单让用户填变量后执行**技能**(技能内部可编排多个工具)。敏感步骤可用 `executeWithConfirm` 让宿主先弹确认。
500
+ ### `sdk.tools`
501
+
502
+ 从 WebUI 列举并调用**其他**平台工具。
503
+
504
+ | 方法 | 签名 | 说明 |
505
+ |------|------|------|
506
+ | `list` | `() => Promise<ToolInfo[]>` | 列出可用工具。 |
507
+ | `execute` | `(toolId, params) => Promise<ToolResult>` | 立即调用工具。 |
508
+ | `executeWithConfirm` | `(toolId, params) => Promise<ToolResult>` | 经宿主确认框后调用。 |
153
509
 
154
510
  ```ts
155
511
  const tools = await sdk.tools.list();
156
- setToolPicker(tools.filter((t) => t.id !== sdk.tool.getInfo().id));
512
+ const result = await sdk.tools.execute('fetch-doc', { url: 'https://...' });
513
+ if (!result.success) throw new Error(result.error);
514
+ ```
157
515
 
158
- const step1 = await sdk.tools.execute('fetch-doc', { url });
159
- if (!step1.success) throw new Error(step1.error);
160
- const step2 = await sdk.tools.execute('summarize', { text: step1.data });
516
+ > 指令型扩展(`execution_mode: "skill"`)通过在对话中激活并注入系统提示词使用,不再有单独的 SDK 模块。若 WebUI 需编排其他扩展,请用 `sdk.tools`。
161
517
 
162
- // 删除类等高风险:走宿主确认
163
- await sdk.tools.executeWithConfirm('delete-backup', { id: backupId });
518
+ ---
164
519
 
165
- const skills = await sdk.skills.list();
166
- const skillResult = await sdk.skills.execute('weekly-report-skill', {
167
- week: '2026-W13',
168
- department: 'sales',
169
- });
520
+ ### `sdk.platform`
521
+
522
+ 平台级工具方法。
523
+
524
+ | 方法 | 签名 | 说明 |
525
+ |------|------|------|
526
+ | `openInBrowser` | `(targetUrl) => Promise<void>` | 在系统浏览器中打开 URL,并传递鉴权信息。 |
527
+
528
+ ```ts
529
+ await sdk.platform.openInBrowser('https://docs.example.com/guide');
530
+ ```
531
+
532
+ `targetUrl` 为空或仅空白字符时抛出异常。
533
+
534
+ ---
535
+
536
+ ## 事件参考
537
+
538
+ | 事件 | 载荷 | 触发时机 |
539
+ |------|------|----------|
540
+ | `toolExecution` | `{ toolCall, result? }` 或原始参数 + `_requestId` | LLM 调用工具;也用于内部 `onExecute` 分发 |
541
+ | `aiResponse` | `ChatResponse` | 宿主会话中 AI 回复完成 |
542
+ | `streamingContent` | `{ content, finished? }` | 流式生成过程中的 token/片段 |
543
+ | `userMessage` | `{ message, timestamp }` | 用户在主聊天窗口发送消息 |
544
+ | `close` | `{ toolId }` | WebUI 即将关闭 |
545
+
546
+ 在事件触发前完成订阅。在框架清理钩子(`useEffect` 返回、`onUnmounted` 等)中调用返回的 `unsubscribe()`。
547
+
548
+ ---
549
+
550
+ ## 权限声明
551
+
552
+ 在 `manifest.json` → `permissions[]` 中声明。宿主会拒绝未授权的 API 调用。
553
+
554
+ | manifest 值 | 受控 SDK API | 说明 |
555
+ |-------------|--------------|------|
556
+ | `ai_chat` | `sdk.ai.*` | 访问宿主 AI 管线 |
557
+ | `file_access` | `sdk.ui.pickFile` | 原生文件选择器 |
558
+ | `notification` | `sdk.ui.showNotification` | 系统通知 |
559
+ | `network` | (宿主级) | 扩展的网络访问 |
560
+ | `system_command` | (宿主级) | 执行系统命令 |
561
+
562
+ 被拒绝时,RPC 调用会以 `Error: Permission denied: <permission>` 拒绝。
563
+
564
+ ---
565
+
566
+ ## 宿主能力矩阵
567
+
568
+ SDK 方法是薄 RPC 封装。部分宿主处理器已完整实现,部分返回 stub。请据此规划扩展功能。
569
+
570
+ | SDK 方法 | 宿主状态 | 说明 |
571
+ |----------|----------|------|
572
+ | `sdk.tool.onExecute` | **生产可用** | 核心路径,完整支持 |
573
+ | `sdk.storage.*` | **生产可用** | 按工具隔离的 SQLite |
574
+ | `sdk.ui.showNotification` | **生产可用** | 需要 `notification` |
575
+ | `sdk.ui.showConfirm` | **生产可用** | |
576
+ | `sdk.ui.pickFile` | **生产可用** | 需要 `file_access` |
577
+ | `sdk.ui.updateState` | **生产可用** | 委托给宿主 |
578
+ | `sdk.platform.openInBrowser` | **生产可用** | 鉴权传递 |
579
+ | `sdk.ai.chat` | **生产可用** | 需要 `ai_chat` + delegate |
580
+ | `sdk.ai.getContext` | **部分实现** | 返回最小上下文 |
581
+ | `sdk.ai.chatStream` | **部分实现** | 返回 `{ streaming: true }`;token 走事件 |
582
+ | `sdk.events.*` | **生产可用** | |
583
+ | `sdk.tools.list` | **Stub** | 返回 `[]` |
584
+ | `sdk.tools.execute` | **Delegate** | 需要宿主 delegate |
585
+ | `sdk.ui.openTab` | **Stub** | 返回成功,无实际操作 |
586
+ | `ui.saveFile`(原始调用) | **生产可用** | 仅宿主——尚未封装到 SDK |
587
+
588
+ ---
589
+
590
+ ## 本地开发
591
+
592
+ WebUI 应能在普通浏览器中开发 UI。检测宿主环境,不在 ChatableX 内时跳过 SDK 初始化。
593
+
594
+ ```ts
595
+ function isInsideChatableX(): boolean {
596
+ return typeof window.ChatableXBridge === 'object' && window.ChatableXBridge !== null;
597
+ }
598
+
599
+ async function bootstrap() {
600
+ if (isInsideChatableX()) {
601
+ const sdk = await ChatableX.init({ appId: 'my-app', debug: true });
602
+ sdk.tool.onExecute(handleTool);
603
+ } else {
604
+ console.log('不在 ChatableX 内运行 — SDK 未激活');
605
+ // 使用 mock、本地状态或手动测试触发器
606
+ }
607
+
608
+ mountApp();
609
+ }
610
+ ```
611
+
612
+ **建议:**
613
+
614
+ - 用 `npm run dev`(Vite)在浏览器中快速迭代。
615
+ - 用 `npm run build` + 在 ChatableX 中加载做集成测试。
616
+ - 宿主通过 `http://127.0.0.1:<端口>/` 为本地扩展提供 `dist/` 服务。
617
+ - 开发时设置 `debug: true` 查看桥接日志。
618
+
619
+ ---
620
+
621
+ ## 框架集成
622
+
623
+ ### React
624
+
625
+ ```tsx
626
+ import { useEffect, useRef } from 'react';
627
+ import { ChatableX, type ChatableXSDK } from 'chatablex-web-sdk';
628
+
629
+ export function useChatableX(appId: string) {
630
+ const sdkRef = useRef<ChatableXSDK | null>(null);
631
+
632
+ useEffect(() => {
633
+ let cancelled = false;
634
+ let unsubStream: (() => void) | undefined;
635
+
636
+ (async () => {
637
+ if (!window.ChatableXBridge) return;
638
+ const sdk = await ChatableX.init({ appId });
639
+ if (cancelled) return;
640
+ sdkRef.current = sdk;
641
+
642
+ sdk.tool.onExecute(async (params) => {
643
+ // 处理工具调用
644
+ return { success: true };
645
+ });
646
+
647
+ unsubStream = sdk.events.on('streamingContent', (data) => {
648
+ // 更新状态
649
+ });
650
+ })();
651
+
652
+ return () => {
653
+ cancelled = true;
654
+ unsubStream?.();
655
+ };
656
+ }, [appId]);
657
+
658
+ return sdkRef;
659
+ }
660
+ ```
661
+
662
+ ### Vue 3
663
+
664
+ ```ts
665
+ import { onMounted, onUnmounted, shallowRef } from 'vue';
666
+ import { ChatableX, type ChatableXSDK } from 'chatablex-web-sdk';
667
+
668
+ export function useChatableX(appId: string) {
669
+ const sdk = shallowRef<ChatableXSDK | null>(null);
670
+ let unsub: (() => void) | undefined;
671
+
672
+ onMounted(async () => {
673
+ if (!window.ChatableXBridge) return;
674
+ sdk.value = await ChatableX.init({ appId });
675
+ sdk.value.tool.onExecute(handleTool);
676
+ unsub = sdk.value.events.onAiResponse(handleAiResponse);
677
+ });
678
+
679
+ onUnmounted(() => unsub?.());
680
+
681
+ return { sdk };
682
+ }
683
+ ```
684
+
685
+ ---
686
+
687
+ ## TypeScript 类型
688
+
689
+ 所有公开类型均已导出:
690
+
691
+ ```ts
692
+ import type {
693
+ ChatableXSDK,
694
+ ChatableXInitConfig,
695
+ ToolInfo,
696
+ ToolResult,
697
+ ToolExecuteHandler,
698
+ ChatResponse,
699
+ ChatOptions,
700
+ SessionContext,
701
+ EventType,
702
+ EventCallbackMap,
703
+ NotificationType,
704
+ FilePickerOptions,
705
+ TabConfig,
706
+ StateUpdate,
707
+ Unsubscribe,
708
+ } from 'chatablex-web-sdk';
170
709
  ```
171
710
 
172
- ## 架构
711
+ 初始化后的全局 `window` 增强:
173
712
 
713
+ | 全局变量 | 设置方 | 用途 |
714
+ |----------|--------|------|
715
+ | `window.ChatableX` | SDK | 活跃的 `ChatableXSDK` 实例 |
716
+ | `window.ChatableXReceive` | SDK | 宿主 → JS 消息接收器 |
717
+ | `window.ChatableXBridge` | Flutter | JS → 宿主 `postMessage` 通道 |
718
+ | `window.__CHATABLEX_DISPATCH__` | SDK | 直接工具分发(高级用法) |
719
+
720
+ ---
721
+
722
+ ## 最佳实践
723
+
724
+ 1. **在应用启动时调用一次 `init()`**,先于处理器注册。
725
+ 2. **`appId` 与 manifest `id` 保持一致**——不一致会导致存储和路由的隐蔽问题。
726
+ 3. **多 `tools[]` 时按 `_toolName` 路由**。
727
+ 4. **返回结构化的 `data`**——LLM 在会话上下文中读取工具结果。
728
+ 5. **持久化用 `sdk.storage`**——需要与宿主对齐时不要依赖 `localStorage`。
729
+ 6. **卸载时取消事件订阅**——避免 SPA 导航中重复注册处理器。
730
+ 7. **用 `isInsideChatableX()` 守卫**——使 `npm run dev` 无需桌面客户端即可运行。
731
+ 8. **发布前构建**——宿主加载的是 `dist/`,不是 TypeScript 源码。
732
+ 9. **提前声明权限**——不要在 manifest 中缺少权限的情况下调用受限 API。
733
+ 10. **保持处理器快速**——宿主对工具执行有 30 秒超时。
734
+
735
+ ---
736
+
737
+ ## 故障排查
738
+
739
+ | 现象 | 可能原因 | 解决办法 |
740
+ |------|----------|----------|
741
+ | `ChatableXBridge not available` | 页面在 ChatableX 外加载,或 init 早于通道注册 | 用 `isInsideChatableX()` 守卫;DOM 就绪后再 `init()` |
742
+ | `ChatableX SDK not initialised` | 在 `init()` 前调用 `getInstance()` | 先 await `init()` |
743
+ | 工具调用挂起 30 秒后失败 | 未注册 `onExecute`,或未发送 `tool.executeResult` | 确认 `init()` 完成且处理器已设置 |
744
+ | `Permission denied` | manifest 缺少权限 | 添加 `ai_chat`、`file_access` 或 `notification` |
745
+ | `sdk_init handshake failed` | 宿主桥未就绪(非致命) | SDK 会以默认元数据继续;检查 `debug: true` 日志 |
746
+ | storage 返回 `null` | 首次读取或键名错误 | 首次访问时正常;检查键名拼写 |
747
+ | 开发正常、ChatableX 中空白 | 未构建或 `webui.entry` 错误 | 执行 `npm run build`;确认 `dist/index.html` 存在 |
748
+ | 第二次 `init()` 被忽略 | 单例设计 | 重启 WebView 才能以不同 `appId` 重新初始化 |
749
+
750
+ **调试清单:**
751
+
752
+ ```ts
753
+ await ChatableX.init({ appId: 'my-app', debug: true });
754
+ console.log('SDK ready:', ChatableX.isReady());
755
+ console.log('Tool info:', ChatableX.getInstance().tool.getInfo());
174
756
  ```
175
- Your App (React/Vue/Vanilla)
176
- │ import { ChatableX } from 'chatablex-web-sdk'
177
-
178
-
179
- ┌─────────────────────────────────────┐
180
- │ chatablex-web-sdk(本包) │
181
- │ │
182
- │ 桥接层: │
183
- │ JS → Flutter: ChatableXBridge │
184
- │ FlutterJS: ChatableXReceive │
185
- │ │
186
- │ 模块:tool, events, ai, ui, │
187
- │ storage, tools, skills │
188
- └──────────────┬──────────────────────┘
189
- │ WebView Bridge
190
-
191
- ┌─────────────────────────────────────┐
192
- │ ChatableX Flutter 客户端 │
193
- │ (承载聊天 UI、SSE 流、Agent) │
194
- └─────────────────────────────────────┘
757
+
758
+ ---
759
+
760
+ ## 官方示例
761
+
762
+ [`examples/`](examples/) 目录下的可运行示例。每个示例均含单元测试、桥接集成测试,以及可装入 ChatableX 的 `dist/` 构建产物。
763
+
764
+ | 应用 | 框架 | 工具 | 演示流程 |
765
+ |------|------|------|----------|
766
+ | [counter-app](examples/counter-app/) | React | `counter_control` | `get` `increment` → `get` |
767
+ | [todo-app](examples/todo-app/) | Vue 3 | `todo_control` | `get` → `add` → `get`(`sdk.storage` 持久化) |
768
+
769
+ ```bash
770
+ npm run test:examples # 运行全部示例测试
771
+ npm run build:examples # 构建两个 dist/
195
772
  ```
196
773
 
774
+ 两个工具均提供 **`get` action**,要求 LLM 在修改前先读取真实状态——多轮对话演示时避免幻觉和工具漏调。
775
+
776
+ ---
777
+
778
+ ## 版本说明
779
+
780
+ | SDK 版本 | npm 标签 | 说明 |
781
+ |----------|----------|------|
782
+ | `1.0.0` | `latest` | 当前稳定版 |
783
+
784
+ 桥接方法名或 `tool.executeResult` 结构的破坏性变更将触发主版本号升级。各 ChatableX 客户端发行版中的 Flutter 宿主是协议的权威来源。
785
+
786
+ ---
787
+
197
788
  ## 许可证
198
789
 
199
- MIT
790
+ MIT © ChatableX Team