chatablex-web-sdk 1.0.0 → 1.0.31

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