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.md +700 -109
- package/README.zh-CN.md +699 -108
- package/dist/index.d.mts +8 -33
- package/dist/index.d.ts +8 -33
- package/dist/index.js +72 -10
- package/dist/index.mjs +72 -10
- package/package.json +13 -4
- package/src/bridge.ts +1 -1
- package/src/index.ts +4 -3
- package/src/modules/platform.ts +14 -0
- package/src/modules/ui.ts +2 -2
- package/src/types.ts +6 -38
- package/src/modules/skills.ts +0 -14
package/README.zh-CN.md
CHANGED
|
@@ -1,10 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ChatableX Web SDK
|
|
2
2
|
|
|
3
3
|
**[English](README.md)** | 简体中文
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
86
|
+
最小集成——在 WebUI 中响应 LLM 工具调用:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
18
89
|
import { ChatableX } from 'chatablex-web-sdk';
|
|
19
90
|
|
|
20
|
-
|
|
21
|
-
const sdk = await ChatableX.init({
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
发布前执行 `npm run build`。ChatableX 客户端加载的是 `dist/index.html`,而非开发服务器(除非你配置了远程 `webui.entry` URL)。
|
|
36
177
|
|
|
178
|
+
### 推荐项目结构
|
|
37
179
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
194
|
+
---
|
|
48
195
|
|
|
49
|
-
##
|
|
196
|
+
## 架构说明
|
|
50
197
|
|
|
51
|
-
|
|
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
|
-
|
|
236
|
+
**响应(Flutter → JS):**
|
|
62
237
|
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
+
订阅宿主推送的事件。每次订阅也会向宿主发送 `events.subscribe`,告知宿主需要转发对应事件。
|
|
84
404
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
405
|
+
| 方法 | 说明 |
|
|
406
|
+
|------|------|
|
|
407
|
+
| `on(eventType, callback)` | 通用订阅。返回 `unsubscribe` 函数。 |
|
|
408
|
+
| `onAiResponse(callback)` | `'aiResponse'` 的简写。 |
|
|
409
|
+
| `onToolExecution(callback)` | `'toolExecution'` 的简写。 |
|
|
410
|
+
| `onUserMessage(callback)` | `'userMessage'` 的简写。 |
|
|
89
411
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// 组件卸载时取消订阅,避免泄漏
|
|
100
|
-
// unsubUser(); unsubStream(); unsubAi();
|
|
418
|
+
// 组件卸载时清理
|
|
419
|
+
unsub();
|
|
101
420
|
```
|
|
102
421
|
|
|
103
|
-
|
|
422
|
+
> **注意:** `unsubscribe()` 仅移除本地监听器。当前 SDK 版本不会通过 `events.unsubscribe` 通知宿主。
|
|
423
|
+
|
|
424
|
+
---
|
|
104
425
|
|
|
105
426
|
### `sdk.ai`
|
|
106
427
|
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
+
从 WebUI 驱动宿主原生 UI。
|
|
124
453
|
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
462
|
+
```ts
|
|
463
|
+
const ok = await sdk.ui.showConfirm('删除', '此操作不可撤销。');
|
|
129
464
|
if (!ok) return;
|
|
130
465
|
|
|
131
|
-
|
|
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
|
-
|
|
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:
|
|
489
|
+
const KEY = 'my-app:draft';
|
|
144
490
|
|
|
145
|
-
await sdk.storage.set(KEY, {
|
|
146
|
-
const
|
|
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
|
-
|
|
496
|
+
存储键在宿主侧按工具实例隔离。
|
|
497
|
+
|
|
498
|
+
---
|
|
151
499
|
|
|
152
|
-
|
|
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
|
-
|
|
512
|
+
const result = await sdk.tools.execute('fetch-doc', { url: 'https://...' });
|
|
513
|
+
if (!result.success) throw new Error(result.error);
|
|
514
|
+
```
|
|
157
515
|
|
|
158
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|