@glodon-aiot/bot-client-sdk 3.25.1-alpha.7 → 3.26.0-alpha.4

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.
Files changed (70) hide show
  1. package/README.md +543 -98
  2. package/dist/es/AgentSession.mjs +230 -165
  3. package/dist/es/BotClient.mjs +186 -136
  4. package/dist/es/Config.mjs +111 -26
  5. package/dist/es/Session.mjs +12 -0
  6. package/dist/es/api/index.mjs +42 -33
  7. package/dist/es/index.mjs +68 -9
  8. package/dist/es/localFiles/adapters/FileSystemAccessAdapter.mjs +174 -0
  9. package/dist/es/localFiles/adapters/HostBridgeAdapter.mjs +172 -0
  10. package/dist/es/localFiles/adapters/MemoryWorkspaceAdapter.mjs +162 -0
  11. package/dist/es/localFiles/adapters/WorkspaceFileAdapter.mjs +50 -0
  12. package/dist/es/localFiles/adapters/fsaTypes.mjs +9 -0
  13. package/dist/es/localFiles/constants.mjs +47 -0
  14. package/dist/es/localFiles/createLocalFilesPlugin.mjs +255 -0
  15. package/dist/es/localFiles/createLocalPythonPlugin.mjs +196 -0
  16. package/dist/es/localFiles/errors.mjs +57 -0
  17. package/dist/es/localFiles/host/connectHostBridge.mjs +50 -0
  18. package/dist/es/localFiles/host/protocol.mjs +26 -0
  19. package/dist/es/localFiles/limits.mjs +22 -0
  20. package/dist/es/localFiles/schemas.mjs +327 -0
  21. package/dist/es/localFiles/security/downloadUrlPolicy.mjs +39 -0
  22. package/dist/es/localFiles/security/glob.mjs +28 -0
  23. package/dist/es/localFiles/security/pathPolicy.mjs +59 -0
  24. package/dist/es/localFiles/security/sensitiveFiles.mjs +115 -0
  25. package/dist/es/localFiles/tools/readTools.mjs +333 -0
  26. package/dist/es/localFiles/tools/runtime.mjs +108 -0
  27. package/dist/es/localFiles/tools/transferTools.mjs +228 -0
  28. package/dist/es/localFiles/tools/writeTools.mjs +173 -0
  29. package/dist/es/localFiles/transfer/SessionFileTransfer.mjs +111 -0
  30. package/dist/es/localFiles/validate.mjs +66 -0
  31. package/dist/es/localFiles/workspace/LocalWorkspace.mjs +339 -0
  32. package/dist/es/localFiles/workspace/handleStore.mjs +79 -0
  33. package/dist/es/localFiles/workspace/watchDirectory.mjs +38 -0
  34. package/dist/es/package.json.mjs +7 -4
  35. package/dist/lib/index.js +9 -4
  36. package/dist/src/AgentSession.d.ts +9 -0
  37. package/dist/src/BotClient.d.ts +12 -2
  38. package/dist/src/Config.d.ts +22 -0
  39. package/dist/src/Session.d.ts +5 -1
  40. package/dist/src/api/index.d.ts +3 -1
  41. package/dist/src/index.d.ts +4 -2
  42. package/dist/src/localFiles/adapters/FileSystemAccessAdapter.d.ts +17 -0
  43. package/dist/src/localFiles/adapters/HostBridgeAdapter.d.ts +24 -0
  44. package/dist/src/localFiles/adapters/MemoryWorkspaceAdapter.d.ts +18 -0
  45. package/dist/src/localFiles/adapters/WorkspaceFileAdapter.d.ts +26 -0
  46. package/dist/src/localFiles/adapters/fsaTypes.d.ts +39 -0
  47. package/dist/src/localFiles/constants.d.ts +50 -0
  48. package/dist/src/localFiles/createLocalFilesPlugin.d.ts +11 -0
  49. package/dist/src/localFiles/createLocalPythonPlugin.d.ts +12 -0
  50. package/dist/src/localFiles/errors.d.ts +34 -0
  51. package/dist/src/localFiles/host/connectHostBridge.d.ts +6 -0
  52. package/dist/src/localFiles/host/protocol.d.ts +33 -0
  53. package/dist/src/localFiles/index.d.ts +21 -0
  54. package/dist/src/localFiles/limits.d.ts +3 -0
  55. package/dist/src/localFiles/schemas.d.ts +30 -0
  56. package/dist/src/localFiles/security/downloadUrlPolicy.d.ts +4 -0
  57. package/dist/src/localFiles/security/glob.d.ts +3 -0
  58. package/dist/src/localFiles/security/pathPolicy.d.ts +14 -0
  59. package/dist/src/localFiles/security/sensitiveFiles.d.ts +23 -0
  60. package/dist/src/localFiles/tools/readTools.d.ts +7 -0
  61. package/dist/src/localFiles/tools/runtime.d.ts +43 -0
  62. package/dist/src/localFiles/tools/transferTools.d.ts +4 -0
  63. package/dist/src/localFiles/tools/writeTools.d.ts +4 -0
  64. package/dist/src/localFiles/transfer/SessionFileTransfer.d.ts +40 -0
  65. package/dist/src/localFiles/types.d.ts +306 -0
  66. package/dist/src/localFiles/validate.d.ts +2 -0
  67. package/dist/src/localFiles/workspace/LocalWorkspace.d.ts +99 -0
  68. package/dist/src/localFiles/workspace/handleStore.d.ts +11 -0
  69. package/dist/src/localFiles/workspace/watchDirectory.d.ts +7 -0
  70. package/package.json +7 -4
package/README.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # @glodon-aiot/bot-client-sdk
2
2
 
3
- AIoT Agent 客户端 JavaScript SDK,提供与 AIoT 平台交互的能力,支持会话管理和消息处理。
3
+ AIoT 应用客户端 SDK,支持 Agent 应用和对话型应用。会话、消息、产出物和客户端插件的执行由这个包负责,界面由接入方实现。下面按界面需要实现的部分来写。
4
+
5
+ | 能力 | 适用应用 | 运行环境 | 见 |
6
+ | --- | --- | --- | --- |
7
+ | 会话与流式对话 | Agent、对话型 | 浏览器、端侧宿主 | [对话界面](#对话界面) |
8
+ | 会话产出物 | Agent、对话型 | 浏览器、端侧宿主 | [产出物](#产出物) |
9
+ | 业务插件、本地文件 | Agent | 浏览器、端侧宿主 | [Agent 插件](#agent-插件) |
10
+ | 本地 Python | Agent | **仅端侧宿主** | [本地 Python](#本地-python) |
11
+
12
+ 浏览器指现代浏览器。端侧宿主是内嵌此页面、负责读写本地工程和运行脚本的桌面程序,见[宿主通道](#宿主通道)。
4
13
 
5
14
  ## 安装
6
15
 
@@ -10,148 +19,584 @@ yarn add @glodon-aiot/bot-client-sdk
10
19
  npm install @glodon-aiot/bot-client-sdk
11
20
  ```
12
21
 
13
- ## 核心概念
22
+ ```ts
23
+ import { BotClient } from '@glodon-aiot/bot-client-sdk';
24
+ ```
14
25
 
15
- ### BotClient
16
- 主客户端类,负责与 AIoT 平台建立连接和管理会话。
26
+ 类型、工作区、工具 Schema 和错误码都从包入口导出。
17
27
 
18
- ### Session
19
- 会话类,分为两种类型:
20
- - `AgentSession`: 代理会话
21
- - `DialogSession`: 对话会话
28
+ ## 创建客户端
22
29
 
23
- ## 基本使用
30
+ ```ts
31
+ const client = new BotClient(token, {
32
+ env: 'dev', // 'dev' | 'test' | 'prod' | 'local',决定默认 API 地址
33
+ // apiRoot: 'https://example.com/api/cvforce', // 传入后覆盖 env 对应的地址
34
+ stream: true, // 省略时 Agent 和对话型应用默认流式
35
+ // plugins、builtinPlugins 见「Agent 插件」
36
+ });
24
37
 
25
- ### 初始化客户端
38
+ client.addEventListener('error', (error) => {
39
+ // 配置问题从这里抛出,例如插件编码冲突、backend 写错
40
+ console.error(error);
41
+ });
26
42
 
27
- ```javascript
28
- import BotClient from '@glodon-aiot/bot-client-sdk';
43
+ client.addEventListener('ready', () => {
44
+ // 应用信息已加载。loadSession、sessions、application 需要等这个事件
45
+ });
46
+ ```
29
47
 
30
- const token = 'your-aiot-token'; // AIoT 平台令牌
31
- const config = {
32
- apiRoot: 'https://api.example.com' // AIoT API 根地址
33
- };
48
+ `token` 是 AIoT 资源令牌,其中需要带有应用 ID。应用类型由平台上的应用决定,`loadSession` 会按类型创建对应的会话。
34
49
 
35
- const client = new BotClient(token, config);
50
+ ## 对话界面
36
51
 
37
- // 监听客户端就绪事件
38
- client.addEventListener('ready', () => {
39
- console.log('Client is ready');
40
- });
52
+ 界面自己保存渲染状态,数据以 `session.messages` 为准。下面的事件都带当前消息或消息列表,收到后重绘即可。
53
+
54
+ ```ts
55
+ const session = await client.loadSession(sessionId); // 空字符串表示新建会话
56
+
57
+ session.addEventListener('history:loaded', () => render(session.messages));
58
+ session.addEventListener('message:new', () => render(session.messages));
59
+ session.addEventListener('message:updated', () => render(session.messages));
60
+ session.addEventListener('message:content', () => render(session.messages)); // 流式增量
61
+ session.addEventListener('answer:finish', () => render(session.messages));
62
+ session.addEventListener('message:error', (error) => showError(error));
63
+ session.addEventListener('message:action', () => render(session.messages)); // 工具调用步骤
64
+
65
+ await session.fetchMessages(); // 拉取历史,结果写入 session.messages
66
+ await session.send({ text: '你好' }, { stream: true });
67
+
68
+ // 停止回答。进行中的客户端插件调用会被取消,确认弹窗应随之关闭
69
+ session.suspend();
41
70
  ```
42
71
 
43
- ### 会话管理
72
+ 会话列表:
44
73
 
45
- ```javascript
46
- // 获取会话列表
74
+ ```ts
47
75
  const sessions = await client.getSessions();
76
+ const current = await client.loadSession(id); // 同时把它设为活跃会话
77
+ await client.removeSession(id, true);
78
+ ```
48
79
 
49
- // 创建新会话
50
- const newSession = await client.loadSession('', {
51
- name: 'New Session'
52
- });
80
+ | 事件 | 界面上做什么 |
81
+ | --- | --- |
82
+ | `history:loaded` | 用 `session.messages` 渲染历史 |
83
+ | `message:new` / `message:updated` | 重绘消息列表 |
84
+ | `message:content` | 流式更新当前回答 |
85
+ | `message:action` | 刷新当前回答里的工具步骤,见[展示工具步骤](#展示工具步骤) |
86
+ | `answer:finish` | 结束「正在回答」,允许再次发送 |
87
+ | `message:error` / `error` | 展示错误 |
88
+
89
+ 发送开始后 `session.frozen === true`,此时 `send` 会失败,输入框保持禁用,直到 `answer:finish`。
90
+
91
+ ## 产出物
92
+
93
+ 任务结束后可以拉取这次会话留下的文件。列表是会话级的,不是某一条消息的附件。没有产出物时返回空数组,不抛错。
94
+
95
+ ```ts
96
+ import type { ISessionArtifact } from '@glodon-aiot/bot-client-sdk';
97
+
98
+ const artifacts: ISessionArtifact[] = await session.getArtifacts();
99
+ // 或按会话 id。省略 id 时用当前活跃会话
100
+ const same = await client.getArtifacts(session.id);
101
+ ```
53
102
 
54
- // 激活会话
55
- client.activeSession = newSession.id;
103
+ | 字段 | 含义 |
104
+ | --- | --- |
105
+ | `name` | 文件名 |
106
+ | `size` | 字节数 |
107
+ | `url` | 可访问地址 |
108
+
109
+ `Session.getArtifacts()` 使用该会话自己的应用 id 和会话 id,两者缺失时抛错。`BotClient.getArtifacts(sessionId?)` 要等 `ready`。不传 `sessionId` 且没有活跃会话时抛错。已加载的会话走 `Session.getArtifacts()`,否则直接请求平台。
110
+
111
+ 一般在任务成功结束(`index === -1` 且 `code === 200`)和用户中止回答(`index === -2`)后调用。
112
+
113
+ ## Agent 插件
114
+
115
+ Agent 应用里,平台上标记为**客户端执行**的插件函数由 SDK 在页面里调用,结果再回传给平台。对话型应用不调用客户端插件。
116
+
117
+ 插件分两类,都在创建客户端时配置:
118
+
119
+ | 配置 | 内容 | `pluginCode` |
120
+ | --- | --- | --- |
121
+ | `plugins` | 接入方自己实现的业务插件 | 平台分配,作为 key |
122
+ | `builtinPlugins` | SDK 内置的本地文件工具和 `run_python` | 固定 `buildin_tools`,不用传 |
123
+
124
+ ```ts
125
+ const client = new BotClient(token, {
126
+ env: 'dev',
127
+ plugins: {
128
+ biz_plugin_code: {
129
+ query_project: async (params, callback, context) => {
130
+ const data = await queryProject(params, context.signal);
131
+ callback({ code: 200, message: '查询到的项目信息', data });
132
+ },
133
+ },
134
+ },
135
+ builtinPlugins: {
136
+ localFiles: { enabled: true },
137
+ localPython: { enabled: true },
138
+ },
139
+ });
56
140
  ```
57
141
 
58
- ### 发送消息
142
+ ### 业务插件
143
+
144
+ `plugins` 的 key 是平台上的 `pluginCode`,值是 `functionCode` 到函数的映射。SDK 收到客户端执行的调用时,以 `(params, callback, context)` 调用对应函数:
145
+
146
+ | 参数 | 含义 |
147
+ | --- | --- |
148
+ | `params` | 模型传入的参数,已解析为对象 |
149
+ | `callback` | 回传结果,`{ code, message, data? }`。`message` 会交给模型理解结果。只认第一次调用 |
150
+ | `context` | `requestId`、`applicationId`、`sessionId`、`pluginCode`、`functionCode`、`signal`(类型 `ToolContext`) |
151
+
152
+ 停止回答或会话失活时 `signal` 变为 aborted,之后的 `callback` 不再回传。函数抛错或返回的 Promise 被拒绝时回传 500。平台调用了没有注册的插件或函数时回传 404。
153
+
154
+ ### 内置插件
155
+
156
+ `localFiles` 和 `localPython` 在平台上是同一个客户端插件,`pluginCode` 为 `buildin_tools`,两个开关只决定客户端注册哪些函数。某项 `enabled` 不为 `true` 时,对应函数不注册。`client.localWorkspace` 只在 `localFiles.enabled === true` 时存在。
59
157
 
60
- ```javascript
61
- const session = await client.loadSession('session-id');
158
+ 业务 `plugins` 里如果已经有 `buildin_tools` 这个 key,内置工具不会注册,`error` 事件会带上原因。同一页面只支持一个启用了内置插件的 `BotClient`(插件表是全局配置)。
62
159
 
63
- // 发送文本消息
64
- await session.send({
65
- text: 'Hello, world!'
66
- }, {
67
- stream: true // 启用流式响应
160
+ 平台上注册该插件时,`functionCode` 不可改名。参数定义从 `BUILTIN_TOOL_SCHEMAS` 读取:
161
+
162
+ | functionCode | 名称 | 执行前需要确认 | 运行环境 |
163
+ | --- | --- | --- | --- |
164
+ | `list_directory` | 列出目录 | 否 | 浏览器、端侧宿主 |
165
+ | `read_file` | 读取文件 | 否 | 浏览器、端侧宿主 |
166
+ | `stat_file` | 查看文件信息 | 否 | 浏览器、端侧宿主 |
167
+ | `search_files` | 查找文件 | 否 | 浏览器、端侧宿主 |
168
+ | `search_text` | 搜索文本 | 否 | 浏览器、端侧宿主 |
169
+ | `apply_patch` | 修改文件 | 是 | 浏览器、端侧宿主 |
170
+ | `write_file` | 写入文件 | 是 | 浏览器、端侧宿主 |
171
+ | `upload_file` | 上传文件到会话 | 是 | 浏览器、端侧宿主 |
172
+ | `download_file` | 下载文件到工作区 | 是 | 浏览器、端侧宿主 |
173
+ | `run_python` | 运行 Python 脚本 | 是,且与文件确认分开 | **仅端侧宿主** |
174
+
175
+ ### 展示工具步骤
176
+
177
+ `message:action` 里出现内置工具步骤时,用已注册的插件编码和 `BUILTIN_TOOL_SCHEMAS` 换成中文名,只展示相对路径和参数,不展示工程的绝对路径:
178
+
179
+ ```ts
180
+ import {
181
+ BUILTIN_TOOL_SCHEMAS,
182
+ getRegisteredLocalFilesPluginCode,
183
+ getRegisteredLocalPythonPluginCode,
184
+ } from '@glodon-aiot/bot-client-sdk';
185
+
186
+ const pluginCodes = [getRegisteredLocalFilesPluginCode(), getRegisteredLocalPythonPluginCode()];
187
+ const schema = pluginCodes.includes(pluginCode)
188
+ ? BUILTIN_TOOL_SCHEMAS[funcCode as keyof typeof BUILTIN_TOOL_SCHEMAS]
189
+ : undefined;
190
+ // schema 存在时,展示 schema.name 和参数里的 path / destPath
191
+ ```
192
+
193
+ 业务插件的步骤按接入方自己的规则展示。
194
+
195
+ ## 本地工作文件夹
196
+
197
+ 内置插件 `localFiles`。应用中的助手只能访问一个文件夹:网页里由用户选择,或由宿主指定。读、搜、改、上传、下载是 SDK 内置的 9 个工具。界面负责四件事:让用户授权文件夹(宿主接入时改为等待宿主推送)、在写入和传输前确认、展示进度、展示并刷新目录。
198
+
199
+ 网页里由用户选择文件夹时依赖 File System Access API,只在 Chromium 内核(Chrome、Edge、WebView2)的安全上下文(https 或 localhost)中可用。端侧宿主自己掌握工程目录时,页面不再弹选择框,见[宿主通道](#宿主通道)。
200
+
201
+ 每个工具都有 `grantId`,固定为 `current-workspace`(常量 `WORKSPACE_GRANT_ID`)。模型漏传时 SDK 会补上;传了其他值返回 403。
202
+
203
+ ### 开启
204
+
205
+ ```ts
206
+ const client = new BotClient(token, {
207
+ env: 'dev',
208
+ builtinPlugins: {
209
+ localFiles: {
210
+ enabled: true,
211
+ // 'file-system-access'(默认):网页里弹出目录选择框
212
+ // 'host-bridge':目录由宿主推送,页面不弹选择框
213
+ backend: 'file-system-access',
214
+ onApprovalRequired: async (request) => openApprovalDialog(request),
215
+ // 只能追加,不能放宽基线。见下文「安全与限额」
216
+ sensitiveRulesAppend: [],
217
+ downloadUrlAllowlistAppend: [],
218
+ // 只能调小,大于默认值的项按默认值处理
219
+ limits: { maxReadBytes: 512 * 1024 },
220
+ // 为 true 时 search_text 可以不指定子目录、搜整个工作区。默认 false
221
+ allowWorkspaceWideTextSearch: false,
222
+ },
223
+ },
68
224
  });
69
225
 
70
- // 监听消息事件
71
- session.addEventListener('message:new', (message) => {
72
- console.log('New message:', message);
226
+ const workspace = client.localWorkspace;
227
+ ```
228
+
229
+ 确认回调可以写在配置里,也可以等界面挂载后再 `workspace.setApprovalHandler`。没有回调时,`apply_patch`、`write_file`、`upload_file`、`download_file` 一律返回 403(`approval_unavailable`),不会写盘、不会传输。返回值只有严格的 `true` 会继续执行。
230
+
231
+ `transfer` 可替换上传和下载与平台文件服务的交互。默认实现走会话文件接口。一般集成不用改。
232
+
233
+ ### 九个工具
234
+
235
+ 路径一律相对工作区根,用 `/` 分隔。绝对路径、盘符、`..`、反斜杠、URL 编码遍历返回 403(`path_outside_workspace`)。
236
+
237
+ **`list_directory`** 只列一层直接子项。目录在前、名称升序,然后分页。`path` 省略或 `"."` 表示根。`offset` 默认 0,`limit` 默认 200、最大 1000。返回 `entries`、`total`、`hasMore`。敏感文件仍在列表里,并带 `sensitive: true`。
238
+
239
+ **`read_file`** 按行读文本。`startLine` / `endLine` 从 1 计、含端点。`encoding` 为 `utf-8`(默认)、`gbk` 或 `gb18030`。单次最多返回 `limits.maxReadBytes`(默认 1 MiB),超出时结果带 `truncated: true`。完整扫描过文件时返回 `totalLines` 和 `hash`(`sha256:<hex>`),后续修改要用这个 hash。二进制文件返回 400。
240
+
241
+ **`stat_file`** 返回 `kind`、`size`、`lastModified`、`sensitive`。`includeHash: true` 时对不超过传输上限的文件计算 hash。不返回内容。
242
+
243
+ **`search_files`** 按文件名或 glob 找文件。`*`、`?`、`**` 可用。模式不含 `/` 时匹配文件名,含 `/` 时匹配相对 `path` 的路径。`path` 默认根,`maxDepth` 默认 8、最大 16,`maxResults` 默认并最多 200。跳过 `.git`、`node_modules`、`.svn`、`.idea`、`.vscode`,以及 `~$*`、`.~lock*`、`*.tmp`。敏感文件不出现在结果里。
244
+
245
+ **`search_text`** 在指定文件或目录里搜普通文本,返回文件、行号、列号和该行内容。`caseSensitive` 默认否。`glob` 可限制文件名,`contextLines` 为 0–3。`isRegex: true` 返回 501。默认必须把 `path` 指到子目录或文件;要搜整个工作区,配置 `allowWorkspaceWideTextSearch: true`。单文件超过 1 MiB 的不扫,超过 500 字的行会截断,最多扫描 2000 个文件。敏感文件和上面那些忽略目录同样跳过。
246
+
247
+ **`apply_patch`** 对已有 UTF-8 文件做查找替换。必须带 `expectedHash`。`edits` 按顺序应用,每条 `oldText` 在文件中只能出现一次,除非 `replaceAll: true`。执行前确认。确认前、确认后各校验一次 hash,期间文件被外部改过则返回 409,`data.currentHash` 是当前值。先写临时副本,失败或取消时原文件不变。
248
+
249
+ **`write_file`** 新建或整体覆盖。`content` 是完整 UTF-8 文本。覆盖已有文件必须带 `expectedHash`;新建不要带。`createDirectories: true` 时才会创建尚不存在的父目录,默认否(父目录不存在返回 404)。执行前确认。同样先写临时副本。修改已有文件优先用 `apply_patch`。
250
+
251
+ **`upload_file`** 把工作区里的一个文件上传到平台文件服务。`target` 只支持 `session`(默认),其他值返回 501。单文件不超过 50 MB。执行前确认。成功返回 `fileId`、`name`、`size`、`url`。`url` 交给模型使用。返回即视为成功,不等待平台解析完成。敏感文件不能上传。
252
+
253
+ **`download_file`** 把文件保存到 `destPath`。`source.type` 三选一:
254
+
255
+ | type | 还要带 | 含义 |
256
+ | --- | --- | --- |
257
+ | `sessionFile` | `fileId` | 当前会话里的文件 |
258
+ | `platformFile` | `fileId` | `upload_file` 返回的 `fileId` |
259
+ | `url` | `url` | 完整 https 地址 |
260
+
261
+ 只传 `type`、不传 `fileId` 或 `url` 会失败。`overwrite` 默认否,目标已存在返回 409。覆盖时可再带 `expectedHash`。单文件不超过 50 MB。执行前确认。URL 规则见下文。
262
+
263
+ ### 浏览器里授权
264
+
265
+ `backend` 为 `file-system-access`(默认)时,`BotClient` 构造时会自己 `restore()`,刷新页面后能恢复上次的文件夹。界面订阅 `workspace:changed`,按 `status` 画按钮。`pick()` 和 `requestPermission()` 必须在点击回调里同步调用,中间不能有 `await`,否则浏览器会拒绝弹出选择框。
266
+
267
+ ```ts
268
+ workspace.on('workspace:changed', ({ status, displayName }) => {
269
+ renderFolder(status, displayName);
73
270
  });
271
+
272
+ selectButton.onclick = () => {
273
+ workspace.pick().catch(console.error);
274
+ };
275
+
276
+ restoreButton.onclick = () => {
277
+ workspace.requestPermission().catch(console.error);
278
+ };
279
+
280
+ closeButton.onclick = () => {
281
+ workspace.clear().catch(console.error);
282
+ };
74
283
  ```
75
284
 
76
- ## 事件系统
285
+ | status | 含义 | 按钮 |
286
+ | --- | --- | --- |
287
+ | `unsupported` | 当前环境没有目录选择能力 | 提示不可用 |
288
+ | `none` | 还没选过 | 「选择工作文件夹」→ `pick()` |
289
+ | `prompt` | 有上次的文件夹,需要重新授权(例如浏览器重启后) | 「恢复访问」→ `requestPermission()` |
290
+ | `granted` | 可用,`displayName` 是文件夹名 | 「更换」→ `pick()`,「关闭」→ `clear()` |
291
+ | `denied` | 用户拒绝了授权 | 「选择工作文件夹」→ `pick()` |
292
+
293
+ `pick()` 在用户取消选择框时返回 `false`,状态保持不变。`displayName` 只有文件夹名,没有绝对路径。
294
+
295
+ 第一次选择前先给用户看一段外发说明:助手会读取该文件夹,内容可能发送给模型;修改、上传、下载前会再确认。SDK 只记住「已经看过」,文案由界面决定。看过之后直接 `pick()`。
296
+
297
+ ```ts
298
+ selectButton.onclick = () => {
299
+ if (disclosureAcked) {
300
+ workspace.pick().catch(console.error);
301
+ return;
302
+ }
303
+ openDisclosure({
304
+ onContinue: () => {
305
+ // 仍然要在这次点击里同步调用 pick
306
+ workspace.pick().catch(console.error);
307
+ disclosureAcked = true;
308
+ void workspace.acknowledgeDisclosure();
309
+ },
310
+ });
311
+ };
312
+
313
+ const disclosureAcked = await workspace.isDisclosureAcknowledged();
314
+ ```
77
315
 
78
- ### BotClient 事件
79
- - `ready`: 客户端准备就绪
80
- - `application:loaded`: 应用信息加载完成
316
+ 说明弹窗的「继续」按钮同样不能先 `await acknowledgeDisclosure()` 再 `pick()`。`acknowledgeDisclosure` 按 `applicationId` 记住,同一应用下不再重复提示。
317
+
318
+ ### 目录树
319
+
320
+ 授权之后用 `workspace.adapter` 列目录。`status !== 'granted'` 时 `adapter` 为 `null`。根目录传空字符串。返回的条目只有 `name`、`kind`(`file` | `directory`)、可选的 `size` 和 `lastModified`,路径要自己拼。目录树按展开加载:展开哪一层,就列哪一层。
321
+
322
+ ```ts
323
+ async function loadDirectory(dir: string) {
324
+ const adapter = workspace.adapter;
325
+ if (!adapter) return [];
326
+ try {
327
+ const entries = await adapter.listEntries(dir);
328
+ return entries.map((entry) => {
329
+ const path = dir ? `${dir}/${entry.name}` : entry.name;
330
+ return {
331
+ path,
332
+ ...entry,
333
+ sensitive: entry.kind === 'file' && workspace.sensitive.isSensitive(path),
334
+ };
335
+ });
336
+ } catch (error) {
337
+ if (isNotAllowed(error)) void workspace.handlePermissionLost();
338
+ throw error;
339
+ }
340
+ }
341
+
342
+ function isNotAllowed(error: unknown) {
343
+ return !!error && typeof error === 'object' && (error as { name?: string }).name === 'NotAllowedError';
344
+ }
345
+ ```
81
346
 
82
- ### Session 事件
83
- - `message:new`: 收到新消息
84
- - `message:updated`: 消息更新
85
- - `history:loaded`: 历史消息加载完成
86
- - `data:updated`: 会话数据更新
347
+ 目录会变。SDK 把需要重读的目录合成 `files:changed` 再发出(大约 150 毫秒内的多次变化并成一次):
87
348
 
88
- ## 高级功能
349
+ | 来源 | `paths` |
350
+ | --- | --- |
351
+ | 浏览器 `FileSystemObserver`(Chrome / Edge) | 发生变化的那些目录。根目录是 `""` |
352
+ | 宿主推送 `fs-changed` | 宿主给的相对目录 |
353
+ | `run_python` 结束 | `[]`,表示重读根目录和当前已经展开的每一层 |
89
354
 
90
- ### 流式消息处理
355
+ ```ts
356
+ const expanded = new Set<string>();
91
357
 
92
- ```javascript
93
- session.addEventListener('message:content', (content) => {
94
- // 处理流式消息内容
95
- console.log('Stream content:', content);
358
+ workspace.on('files:changed', (event) => {
359
+ const dirs = event.paths.length ? event.paths : ['', ...expanded];
360
+ dirs.forEach((dir) => void loadDirectory(dir));
96
361
  });
362
+ ```
97
363
 
98
- await session.send({
99
- text: 'Streaming question'
100
- }, {
101
- stream: true
364
+ `apply_patch`、`write_file`、`download_file` 成功时还会发 `tool:finish`。没有 `FileSystemObserver` 的环境靠它刷新被改文件的父目录。窗口重新获得焦点时,再重读根目录和已展开目录,用来补上观察器没有覆盖到的改动(例如在资源管理器里改了 `.git` 或 `node_modules` 里面的文件;宿主默认不监视这三个目录)。
365
+
366
+ ```ts
367
+ window.addEventListener('focus', () => {
368
+ if (document.visibilityState === 'hidden') return;
369
+ ['', ...expanded].forEach((dir) => void loadDirectory(dir));
370
+ });
371
+
372
+ workspace.on('tool:finish', (event) => {
373
+ if (!['apply_patch', 'write_file', 'download_file'].includes(event.functionCode)) return;
374
+ const slash = event.path?.lastIndexOf('/') ?? -1;
375
+ const dir = !event.path || slash < 0 ? '' : event.path.slice(0, slash);
376
+ void loadDirectory(dir);
102
377
  });
103
378
  ```
104
379
 
105
- ### 会话配置
380
+ 目录树只做浏览。文件内容由应用中的助手通过工具读取,界面不要另开一条读取通道把内容发给模型。
381
+
382
+ ### 确认弹窗
383
+
384
+ `onApprovalRequired` 收到的 `ApprovalRequest`:
385
+
386
+ | 字段 | 用途 |
387
+ | --- | --- |
388
+ | `functionCode` | `apply_patch` / `write_file` / `upload_file` / `download_file` |
389
+ | `path` | 工作区相对路径 |
390
+ | `workspaceName` | 文件夹名 |
391
+ | `preview` | 给用户看的内容,见下表 |
392
+ | `signal` | 停止回答、切换会话、更换或关闭文件夹、确认超时(默认 5 分钟)时变为 aborted |
393
+
394
+ `signal` abort 时关掉弹窗并 resolve `false`。返回值只有严格的 `true` 会继续执行;`false`、抛错、超时都按拒绝处理,磁盘上的原文件保持不变。
395
+
396
+ ```ts
397
+ import type { ApprovalRequest } from '@glodon-aiot/bot-client-sdk';
398
+
399
+ function openApprovalDialog(request: ApprovalRequest): Promise<boolean> {
400
+ return new Promise((resolve) => {
401
+ let settled = false;
402
+ const finish = (approved: boolean) => {
403
+ if (settled) return;
404
+ settled = true;
405
+ closeDialog();
406
+ resolve(approved);
407
+ };
408
+ request.signal.addEventListener('abort', () => finish(false), { once: true });
409
+ showDialog(request, finish);
410
+ });
411
+ }
412
+ ```
413
+
414
+ `preview.type` 决定弹窗内容:
415
+
416
+ | preview | 展示 |
417
+ | --- | --- |
418
+ | `{ type: 'text', created: true, after }` | 新建文件,展示 `after` |
419
+ | `{ type: 'text', created: false, before, after }` | 修改文件,按行对比 `before` 和 `after` |
420
+ | `{ type: 'upload', name, size, target: 'session' }` | 文件名、大小、工作区路径,目标是当前会话 |
421
+ | `{ type: 'download', source, name?, size?, overwrite }` | `source.label`(「会话文件」或 URL 主机名)、文件名、大小、保存路径、是否覆盖 |
422
+
423
+ `tool:approval-required` 只有 `requestId`、`functionCode`、`path`、`approvalId`,不含文件内容。预览文本只出现在 `request.preview` 里。
106
424
 
107
- ```javascript
108
- // 设置会话知识库
109
- await session.setKnowledges([{id: 'knowledge-1'}]);
425
+ 界面如果晚于 `BotClient` 挂载,在挂载前不要让回调一直 pending:还没有弹窗时直接返回 `false`,挂载后再 `setApprovalHandler`。
110
426
 
111
- // 设置提示变量
112
- await session.setPromptVariables([
113
- {key: 'var1', value: 'value1'}
114
- ]);
427
+ ### 进度与未授权
115
428
 
116
- // 设置网络访问
117
- await session.setNetOpen(true);
429
+ 上传、下载过程订阅 `tool:progress`。`stage` 为 `reading` | `uploading` | `downloading` | `writing` | `done`,用 `loaded` / `total` 画进度。`stage === 'done'`,以及 `tool:finish`、`tool:error`、`tool:cancelled` 时把这条进度拿掉。
430
+
431
+ 应用中的助手在未授权时调用工具,会收到 `tool:error`,`reason === 'workspace_not_granted'`。这时把「选择工作文件夹」或「恢复访问」亮出来。`status` 变为 `granted` 后关掉这个提示。授权后不要自动重放刚才失败的调用。
432
+
433
+ 工作区事件一览(载荷里没有文件内容):
434
+
435
+ | 事件 | 载荷要点 |
436
+ | --- | --- |
437
+ | `workspace:changed` | `status`、`displayName` |
438
+ | `files:changed` | `paths`:要重读的相对目录;空数组表示根目录和已展开的目录 |
439
+ | `tool:start` | `requestId`、`functionCode`、`path` |
440
+ | `tool:approval-required` | 同上,另有 `approvalId` |
441
+ | `tool:progress` | 同上,另有 `stage`、`loaded`、`total` |
442
+ | `tool:finish` | 同上,另有 `code` |
443
+ | `tool:error` | 同上,另有 `code`、`message`、`reason` |
444
+ | `tool:cancelled` | `requestId`、`functionCode`、`path` |
445
+
446
+ `workspace.on` 返回取消订阅的函数。
447
+
448
+ ## 本地 Python
449
+
450
+ > 只能在端侧宿主里使用。普通浏览器里不会运行脚本,调用返回 501。
451
+
452
+ 内置插件 `localPython`。使用前需同时满足:
453
+
454
+ - 页面运行在端侧宿主里,且宿主按[宿主通道](#宿主通道)实现了 `run_python`;
455
+ - `localFiles.enabled: true` 且 `backend: 'host-bridge'`;
456
+ - 配置了 `localPython.onApprovalRequired`。
457
+
458
+ 没有启用本地工作区时,脚本工具不会注册,`error` 事件里会说明原因。`run_python` 和文件工具在同一个客户端插件里,但文件工具已经授权,也不能代替这一次脚本确认。
459
+
460
+ ```ts
461
+ builtinPlugins: {
462
+ localFiles: {
463
+ enabled: true,
464
+ backend: 'host-bridge',
465
+ onApprovalRequired: async (request) => askUser(request),
466
+ },
467
+ localPython: {
468
+ enabled: true, // 仅端侧宿主可用,需配合 backend: 'host-bridge'
469
+ onApprovalRequired: async (request) => askUser(request),
470
+ },
471
+ }
118
472
  ```
119
473
 
120
- ## 开发指南
474
+ 平台上的函数名是 `run_python`。参数:
475
+
476
+ | 字段 | 含义 |
477
+ | --- | --- |
478
+ | `grantId` | 固定 `current-workspace`,漏传会补上 |
479
+ | `path` | 工程内已有的 `.py`,相对工程根 |
480
+ | `args` | 字符串数组。最多 16 个。每个参数只能含字母、数字、`_`、`.`、`/`、`:`、`@`、`=`、`+`、`-`、`,`,不能有空格或 shell 语法 |
481
+ | `timeoutMs` | 1000–120000,默认 20000 |
121
482
 
122
- ### 依赖安装
483
+ 确认请求是 `PythonApprovalRequest`:`path`、`args`、`workspaceName`、`signal`。与文件确认分开,每次运行都要单独问一次。没有回调、用户拒绝、或脚本命中敏感规则时返回 403,不会启动进程。
123
484
 
124
- `yarn install`
485
+ 成功时 `data` 为 `{ exitCode, timedOut, stdout, stderr }`。`exitCode !== 0` 仍然是工具成功(`code === 0`),脚本自己的失败在 `exitCode` 里。超时后 `timedOut === true`。同一时间只能跑一个脚本,再调返回 429。
125
486
 
126
- ### 运行示例
487
+ 脚本可能改了工程里的文件。运行结束后 SDK 发出 `files:changed`,`paths` 为空,目录树应重读当前看得到的各层。
127
488
 
128
- `yarn start`
489
+ 沙箱由宿主启动:工程目录可读写,解释器只读,运行时不能联网,也不能访问工程和解释器以外的文件。工程根下的 `sandbox-requirements.txt` 会在跑脚本前安装(每行一个 PyPI 包,最多 20 行)。参照实现是仓库里的 `examples/qt-workspace-host`。
129
490
 
130
- ### 贡献代码
491
+ ## 宿主通道
131
492
 
132
- 1. 切换到 master 分支: `git switch master`
133
- 2. 拉取最新代码: `git pull`
134
- 3. 创建新分支: `git switch -c feature/xxx`
135
- 4. 提交代码:
136
- ```bash
137
- git add .
138
- git commit -m "feat: your feature"
139
- git push
140
- ```
141
- 5. 创建 PR 合并到 develop 分支
493
+ 端侧宿主(下文简称宿主)是内嵌此页面的桌面程序,例如用 WebView2 加载页面的客户端。本地 Python 只能通过它运行。
142
494
 
143
- ### 代码提交规范
495
+ `localFiles.backend` 为 `host-bridge` 时,页面通过 `window.chrome.webview` 和宿主说话。通道名固定 `glodon-workspace`。消息里只有相对路径和文件夹名,绝对路径留在宿主内。页面上不再出现「选择工作文件夹」。宿主还没推送目录时,`status` 为 `unsupported`。
496
+
497
+ 页面加载后先发:
498
+
499
+ ```json
500
+ { "channel": "glodon-workspace", "type": "hello" }
501
+ ```
144
502
 
145
- 遵循 Conventional Commits 规范:
146
- - `feat`: 新功能
147
- - `fix`: bug 修复
148
- - `docs`: 文档更新
149
- - `style`: 代码样式
150
- - `refactor`: 代码重构
151
- - `test`: 测试相关
152
- - `chore`: 构建/工具变更
503
+ 宿主立刻回当前工程。之后每次打开或关闭工程再推一次:
153
504
 
154
- 示例:
505
+ ```json
506
+ { "channel": "glodon-workspace", "type": "workspace", "granted": true, "displayName": "sample-project" }
155
507
  ```
156
- git commit -m "feat: add new session API"
157
- git commit -m "fix: message streaming issue"
508
+
509
+ `granted` 为 `false` 时页面断开工作区。`displayName` 只放文件夹名。
510
+
511
+ 目录有增删时再推一条。`paths` 是发生变化的目录,相对工程根,根目录用 `""`。空数组表示让页面重读已展开的全部目录。短时间内多次变化可以合成一条。
512
+
513
+ ```json
514
+ { "channel": "glodon-workspace", "type": "fs-changed", "paths": ["notes", ""] }
515
+ ```
516
+
517
+ 脚本跑完、用户在资源管理器里改文件,都应推送。Qt 示例用目录监视做这件事,默认不监视 `.git`、`node_modules`、`__pycache__` 内部。
518
+
519
+ 其余请求都带 `id` 和 `op`。路径用 `/`,不能含 `..`、盘符、反斜杠或绝对路径。不合法时回 `{ "id", "ok": false, "name": "SecurityError" }`,不要访问磁盘。
520
+
521
+ | op | 页面发来的字段 | 成功时宿主回 |
522
+ | --- | --- | --- |
523
+ | `list` | `path`,根目录是 `""` 或 `"."` | `entries`: `{ name, kind: "file" \| "directory", size?, lastModified? }[]` |
524
+ | `stat` | `path` | `entry`:同样的字段 |
525
+ | `read` | `path` | `contentBase64`。单文件不超过 50 MB |
526
+ | `write` | `path`、`contentBase64`、`createDirectories` | `{ "ok": true }`。父目录不存在且 `createDirectories` 不是 `true` 时回 `NotFoundError` |
527
+ | `run_python` | `path`、`args`、`timeoutMs` | `exitCode`、`timedOut`、`stdout`、`stderr` |
528
+
529
+ 失败时统一 `{ "id", "ok": false, "name" }`:
530
+
531
+ | name | 何时 |
532
+ | --- | --- |
533
+ | `NotAllowedError` | 还没有工程目录,或文件打不开 |
534
+ | `SecurityError` | 路径越出工程目录 |
535
+ | `NotFoundError` | 目标不存在 |
536
+ | `TypeMismatchError` | 把文件当目录,或脚本不是 `.py` |
537
+ | `QuotaExceededError` | 读写超过 50 MB |
538
+ | `NoModificationAllowedError` | 写入没有完成 |
539
+ | `AbortError` | 已经有一个脚本在跑,或调用超时 |
540
+
541
+ `lastModified` 用 Unix 毫秒。`size` 用字节数。搜索、按行修改、上传、下载仍由页面完成,底层落到上面的 `list` / `read` / `write`。宿主不要自己实现那 9 个工具名。
542
+
543
+ ## 安全与限额
544
+
545
+ 敏感文件不可读、不可写、不可上传、不可运行,也不出现在搜索结果里。`list_directory` / `stat_file` 仍返回它们,并标记 `sensitive: true`。目录树上用 `workspace.sensitive.isSensitive(path)` 做同样的标记。命中时工具返回 403,`reason` 为 `sensitive_file`,`data.ruleId` 是规则 ID。
546
+
547
+ 基线(`SDK_SENSITIVE_BASELINE`)始终生效,`sensitiveRulesAppend` 只能再加规则:
548
+
549
+ | 匹配 | 规则 |
550
+ | --- | --- |
551
+ | 文件名 | `.env*`、`*.pem`、`*.key`、`*.p12`、`*.pfx`、`id_rsa*`、`id_ed25519*`、`id_ecdsa*`、`.credentials`、`.npmrc`、`.pypirc` |
552
+ | 整段相对路径 | `**/.aws/credentials`、`**/.ssh/**` |
553
+
554
+ 追加规则的 `pattern` 是 `/` 分隔的 glob。`match` 省略时只比较文件名,`path` 时比较整段相对路径。`operations` 可限制为 `read` / `write` / `search` 的子集,省略则三种都拦。`source` 取 `product` 或 `enterprise`。
555
+
556
+ 下载 URL 只允许 https,且域名是允许列表中的某一项或其子域。默认只有 `glodon.com`。`downloadUrlAllowlistAppend` 追加域名后缀。IP、localhost、带用户名密码的地址会被拒绝;重定向后的地址同样校验,不通过则不写盘。请求不带 Cookie。
557
+
558
+ `limits` 各项只能调小(`DEFAULT_LIMITS`):
559
+
560
+ | 项 | 默认 |
561
+ | --- | --- |
562
+ | `maxReadBytes` | 1 MiB |
563
+ | `maxWriteBytes` | 5 MiB |
564
+ | `maxTransferBytes` | 50 MB |
565
+ | `maxSearchResults` | 200 |
566
+ | `maxConcurrentRequests` | 4 |
567
+ | `toolTimeoutMs` | 30 秒 |
568
+ | `searchTimeoutMs` | 60 秒 |
569
+ | `transferTimeoutMs` | 10 分钟 |
570
+ | `approvalTimeoutMs` | 5 分钟,不计入工具超时 |
571
+
572
+ 停止回答会取消进行中的调用,确认框应关闭,结果不再回传。更换或关闭文件夹同样取消进行中的调用。
573
+
574
+ 已知限制:File System Access API 不暴露符号链接目标,链接的处理以浏览器行为为准。`upload_file` 返回 `fileId` 即视为成功。
575
+
576
+ ## 错误码
577
+
578
+ 工具结果是 `{ code, message, data? }`。界面主要看 `tool:error` 的 `code` 和 `reason`。
579
+
580
+ | code | 含义 |
581
+ | --- | --- |
582
+ | 0 | 成功 |
583
+ | 400 | 参数错误、二进制文件、非 UTF-8、`search_text` 的范围是整个工作区 |
584
+ | 403 | 未授权(`workspace_not_granted`)、越界(`path_outside_workspace`)、敏感文件(`sensitive_file`)、用户拒绝(`user_rejected`)、没有确认回调(`approval_unavailable`)、下载地址不允许(`url_not_allowed`) |
585
+ | 404 | 文件或目录不存在 |
586
+ | 408 | 超时 |
587
+ | 409 | `expectedHash` 不一致(`data.currentHash` 为当前值),或下载目标已存在 |
588
+ | 413 | 超过大小上限 |
589
+ | 429 | 并发过多,或已有一个脚本在运行 |
590
+ | 499 | 已取消(停止回答、更换文件夹) |
591
+ | 500 | 内部异常。消息里不含路径和调用栈 |
592
+ | 501 | 当前不支持:正则搜索、非 session 的上传目标、网页环境里的 `run_python` |
593
+
594
+ ## 开发
595
+
596
+ ```bash
597
+ yarn install
598
+ yarn test
599
+ yarn start
600
+ ```
601
+
602
+ 提交信息使用 Conventional Commits:`feat`、`fix`、`docs`、`refactor`、`test`、`chore`。