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

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 (56) hide show
  1. package/README.md +105 -76
  2. package/dist/es/Config.mjs +63 -85
  3. package/dist/es/builtinTools/browserRuntime/BrowserCommandRunner.mjs +201 -0
  4. package/dist/es/builtinTools/browserRuntime/PythonRunner.mjs +356 -0
  5. package/dist/es/builtinTools/browserRuntime/WorkspaceBashFs.mjs +350 -0
  6. package/dist/es/builtinTools/browserRuntime/assets.mjs +9 -0
  7. package/dist/es/builtinTools/browserRuntime/loadBash.mjs +17 -0
  8. package/dist/es/builtinTools/browserRuntime/pyodideRunner.py.mjs +96 -0
  9. package/dist/es/builtinTools/browserRuntime/pyodideWorker.mjs +242 -0
  10. package/dist/es/builtinTools/command.mjs +56 -0
  11. package/dist/es/builtinTools/constants.mjs +10 -0
  12. package/dist/es/builtinTools/createBuiltinToolsPlugin.mjs +236 -0
  13. package/dist/es/builtinTools/overview.mjs +77 -0
  14. package/dist/es/builtinTools/paths.mjs +63 -0
  15. package/dist/es/builtinTools/schemas.mjs +216 -0
  16. package/dist/es/builtinTools/tools.mjs +471 -0
  17. package/dist/es/index.mjs +49 -48
  18. package/dist/es/localFiles/adapters/FileSystemAccessAdapter.mjs +193 -103
  19. package/dist/es/localFiles/adapters/HostBridgeAdapter.mjs +138 -77
  20. package/dist/es/localFiles/adapters/MemoryWorkspaceAdapter.mjs +132 -97
  21. package/dist/es/localFiles/adapters/WorkspaceFileAdapter.mjs +32 -28
  22. package/dist/es/localFiles/constants.mjs +1 -1
  23. package/dist/es/localFiles/host/connectHostBridge.mjs +22 -18
  24. package/dist/es/localFiles/host/protocol.mjs +6 -5
  25. package/dist/es/localFiles/schemas.mjs +45 -60
  26. package/dist/es/localFiles/validate.mjs +37 -37
  27. package/dist/es/localFiles/workspace/LocalWorkspace.mjs +71 -52
  28. package/dist/es/package.json.mjs +33 -27
  29. package/dist/lib/index.js +372 -9
  30. package/dist/src/Config.d.ts +2 -2
  31. package/dist/src/builtinTools/browserRuntime/BrowserCommandRunner.d.ts +26 -0
  32. package/dist/src/builtinTools/browserRuntime/PythonRunner.d.ts +67 -0
  33. package/dist/src/builtinTools/browserRuntime/WorkspaceBashFs.d.ts +51 -0
  34. package/dist/src/builtinTools/browserRuntime/assets.d.ts +13 -0
  35. package/dist/src/builtinTools/browserRuntime/loadBash.d.ts +3 -0
  36. package/dist/src/builtinTools/browserRuntime/pyodideWorker.d.ts +44 -0
  37. package/dist/src/builtinTools/command.d.ts +26 -0
  38. package/dist/src/builtinTools/constants.d.ts +17 -0
  39. package/dist/src/builtinTools/createBuiltinToolsPlugin.d.ts +11 -0
  40. package/dist/src/builtinTools/overview.d.ts +5 -0
  41. package/dist/src/builtinTools/paths.d.ts +15 -0
  42. package/dist/src/builtinTools/schemas.d.ts +3 -0
  43. package/dist/src/builtinTools/tools.d.ts +60 -0
  44. package/dist/src/localFiles/adapters/FileSystemAccessAdapter.d.ts +4 -1
  45. package/dist/src/localFiles/adapters/HostBridgeAdapter.d.ts +20 -1
  46. package/dist/src/localFiles/adapters/MemoryWorkspaceAdapter.d.ts +4 -1
  47. package/dist/src/localFiles/adapters/WorkspaceFileAdapter.d.ts +14 -0
  48. package/dist/src/localFiles/adapters/fsaTypes.d.ts +2 -0
  49. package/dist/src/localFiles/constants.d.ts +2 -1
  50. package/dist/src/localFiles/host/protocol.d.ts +13 -0
  51. package/dist/src/localFiles/index.d.ts +3 -2
  52. package/dist/src/localFiles/schemas.d.ts +2 -3
  53. package/dist/src/localFiles/types.d.ts +14 -1
  54. package/dist/src/localFiles/workspace/LocalWorkspace.d.ts +12 -0
  55. package/package.json +8 -2
  56. package/dist/es/localFiles/createLocalPythonPlugin.mjs +0 -196
package/README.md CHANGED
@@ -7,9 +7,12 @@ AIoT 应用客户端 SDK,支持 Agent 应用和对话型应用。会话、消
7
7
  | 会话与流式对话 | Agent、对话型 | 浏览器、端侧宿主 | [对话界面](#对话界面) |
8
8
  | 会话产出物 | Agent、对话型 | 浏览器、端侧宿主 | [产出物](#产出物) |
9
9
  | 业务插件、本地文件 | Agent | 浏览器、端侧宿主 | [Agent 插件](#agent-插件) |
10
- | 本地 Python | Agent | **仅端侧宿主** | [本地 Python](#本地-python) |
10
+ | 命令与脚本(shell / run_file) | Agent | 浏览器运行时或端侧宿主沙箱 | [命令和脚本](#命令和脚本) |
11
+ | 本地 Python(`localPython` / `run_python`) | Agent | **优先端侧宿主**;浏览器可用 Pyodide | [命令和脚本](#命令和脚本) |
11
12
 
12
- 浏览器指现代浏览器。端侧宿主是内嵌此页面、负责读写本地工程和运行脚本的桌面程序,见[宿主通道](#宿主通道)。
13
+ 浏览器指现代浏览器(命令运行时另需能加载公司 OBS 上的 just-bash / Pyodide)。端侧宿主是内嵌此页面、负责读写本地工程并启动 `@glodon-aiot/workspace-sandbox` 的桌面程序,见[宿主通道](#宿主通道)。
14
+
15
+ 集成方总览见仓库内 [`bot-client-ui/docs/agent-integration-handbook.md`](../bot-client-ui/docs/agent-integration-handbook.md)。
13
16
 
14
17
  ## 安装
15
18
 
@@ -119,7 +122,7 @@ Agent 应用里,平台上标记为**客户端执行**的插件函数由 SDK
119
122
  | 配置 | 内容 | `pluginCode` |
120
123
  | --- | --- | --- |
121
124
  | `plugins` | 接入方自己实现的业务插件 | 平台分配,作为 key |
122
- | `builtinPlugins` | SDK 内置的本地文件工具和 `run_python` | 固定 `buildin_tools`,不用传 |
125
+ | `builtinPlugins` | SDK 内置的工作区、命令和技能工具 | 固定 `builtin_tools`,不用传 |
123
126
 
124
127
  ```ts
125
128
  const client = new BotClient(token, {
@@ -153,24 +156,27 @@ const client = new BotClient(token, {
153
156
 
154
157
  ### 内置插件
155
158
 
156
- `localFiles` 和 `localPython` 在平台上是同一个客户端插件,`pluginCode` 为 `buildin_tools`,两个开关只决定客户端注册哪些函数。某项 `enabled` 不为 `true` 时,对应函数不注册。`client.localWorkspace` 只在 `localFiles.enabled === true` 时存在。
159
+ `localFiles` 或 `localPython` 任一启用后,SDK 把同一套内置工具注册到 `pluginCode` `builtin_tools`。`client.localWorkspace` 只在 `localFiles.enabled === true` 时存在。
160
+
161
+ `shell` / `run_file` / `run_skill_script` 按[命令和脚本](#命令和脚本)选择执行器:注入的 `commandRunner` → 宿主声明 `sandbox-exec`(协议一致)→ 浏览器 just-bash / Pyodide(默认开启)→ 否则 501。
157
162
 
158
- 业务 `plugins` 里如果已经有 `buildin_tools` 这个 key,内置工具不会注册,`error` 事件会带上原因。同一页面只支持一个启用了内置插件的 `BotClient`(插件表是全局配置)。
163
+ 业务 `plugins` 里如果已经有 `builtin_tools` 这个 key,内置工具不会注册,`error` 事件会带上原因。同一页面只支持一个启用了内置插件的 `BotClient`(插件表是全局配置)。
159
164
 
160
165
  平台上注册该插件时,`functionCode` 不可改名。参数定义从 `BUILTIN_TOOL_SCHEMAS` 读取:
161
166
 
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 脚本 | 是,且与文件确认分开 | **仅端侧宿主** |
167
+ | functionCode | 名称 | 运行环境 |
168
+ | --- | --- | --- |
169
+ | `shell` | 执行命令 | 浏览器运行时或端侧宿主沙箱 |
170
+ | `read_file` | 读取文件 | 浏览器、端侧宿主 |
171
+ | `write_file` | 写入文件 | 浏览器、端侧宿主 |
172
+ | `append_file` | 追加文件 | 浏览器、端侧宿主 |
173
+ | `run_file` | 执行文件 | 浏览器运行时(`.py` / `.sh`)或端侧宿主(另含 `.js`) |
174
+ | `edit_file` | 修改文件 | 浏览器、端侧宿主 |
175
+ | `grep` | 搜索文件 | 浏览器、端侧宿主 |
176
+ | `find` | 查找文件 | 浏览器、端侧宿主 |
177
+ | `load_skill` | 加载技能 | 浏览器、端侧宿主 |
178
+ | `read_skill_resource` | 读取技能资源 | 浏览器、端侧宿主 |
179
+ | `run_skill_script` | 运行技能脚本 | 同 `run_file` |
174
180
 
175
181
  ### 展示工具步骤
176
182
 
@@ -194,11 +200,16 @@ const schema = pluginCodes.includes(pluginCode)
194
200
 
195
201
  ## 本地工作文件夹
196
202
 
197
- 内置插件 `localFiles`。应用中的助手只能访问一个文件夹:网页里由用户选择,或由宿主指定。读、搜、改、上传、下载是 SDK 内置的 9 个工具。界面负责四件事:让用户授权文件夹(宿主接入时改为等待宿主推送)、在写入和传输前确认、展示进度、展示并刷新目录。
203
+ 内置插件 `localFiles`。应用中的助手只能访问一个文件夹:网页里由用户选择,或由宿主指定。
198
204
 
199
- 网页里由用户选择文件夹时依赖 File System Access API,只在 Chromium 内核(Chrome、Edge、WebView2)的安全上下文(https 或 localhost)中可用。端侧宿主自己掌握工程目录时,页面不再弹选择框,见[宿主通道](#宿主通道)。
205
+ **路径约定(以当前 Schema / 代码为准):**
206
+
207
+ - 工具参数使用工作区内以 `/` 开头的绝对路径,或相对工作区根的相对路径,例如 `/说明.md`、`notes/a.md`。
208
+ - **不要**使用或新建 `input`、`output`、`uploads`、`tmp` 作为约定根目录。
209
+ - 技能在 `skills/<名称>/` 下,只通过技能工具访问。
210
+ - `/dev`、`/usr`、`/bin` 仍是系统路径。写进脚本文件里的路径请用相对路径。
200
211
 
201
- 每个工具都有 `grantId`,固定为 `current-workspace`(常量 `WORKSPACE_GRANT_ID`)。模型漏传时 SDK 会补上;传了其他值返回 403。
212
+ 网页里由用户选择文件夹时依赖 File System Access API,只在 Chromium 内核(Chrome、Edge、WebView2)的安全上下文(https 或 localhost)中可用。端侧宿主自己掌握工程目录时,页面不再弹选择框,见[宿主通道](#宿主通道)。
202
213
 
203
214
  ### 开启
204
215
 
@@ -219,6 +230,13 @@ const client = new BotClient(token, {
219
230
  limits: { maxReadBytes: 512 * 1024 },
220
231
  // 为 true 时 search_text 可以不指定子目录、搜整个工作区。默认 false
221
232
  allowWorkspaceWideTextSearch: false,
233
+ // 无宿主沙箱时,是否在页面里跑 shell / python(默认 true)
234
+ commandRuntime: {
235
+ browser: {
236
+ enabled: true,
237
+ // bashModuleURL / pyodideIndexURL 可覆盖公司 OBS 默认地址
238
+ },
239
+ },
222
240
  },
223
241
  },
224
242
  });
@@ -226,39 +244,33 @@ const client = new BotClient(token, {
226
244
  const workspace = client.localWorkspace;
227
245
  ```
228
246
 
229
- 确认回调可以写在配置里,也可以等界面挂载后再 `workspace.setApprovalHandler`。没有回调时,`apply_patch`、`write_file`、`upload_file`、`download_file` 一律返回 403(`approval_unavailable`),不会写盘、不会传输。返回值只有严格的 `true` 会继续执行。
230
-
231
- `transfer` 可替换上传和下载与平台文件服务的交互。默认实现走会话文件接口。一般集成不用改。
247
+ `transfer` 仍可供旧的上传下载实现使用。当前注册的内置工具不走这套传输。宿主沙箱是否启用由宿主 `capabilities` 声明决定,不在 `commandRuntime` 里配置。
232
248
 
233
- ### 九个工具
249
+ ### 十一个工具
234
250
 
235
- 路径一律相对工作区根,用 `/` 分隔。绝对路径、盘符、`..`、反斜杠、URL 编码遍历返回 403(`path_outside_workspace`)。
251
+ 路径见上文约定。`..`、反斜杠、越出工作区的路径返回 403。
236
252
 
237
- **`list_directory`** 只列一层直接子项。目录在前、名称升序,然后分页。`path` 省略或 `"."` 表示根。`offset` 默认 0,`limit` 默认 200、最大 1000。返回 `entries`、`total`、`hasMore`。敏感文件仍在列表里,并带 `sensitive: true`。
253
+ **`read_file`** 读取 UTF-8 文本。`start_line` / `end_line` 从 1 计、含端点。不超过 81920 字节时按原文返回。超出后,JSON、XML、SVG、CSV、TSV 返回结构概览和短预览,`truncated` 为 true;指定了行范围则返回该片段。PDF 和图片只返回摘要,不返回正文。
238
254
 
239
- **`read_file`** 按行读文本。`startLine` / `endLine` 从 1 计、含端点。`encoding` 为 `utf-8`(默认)、`gbk` 或 `gb18030`。单次最多返回 `limits.maxReadBytes`(默认 1 MiB),超出时结果带 `truncated: true`。完整扫描过文件时返回 `totalLines` 和 `hash`(`sha256:<hex>`),后续修改要用这个 hash。二进制文件返回 400。
255
+ **`write_file`** 覆盖写入。`content` 按 UTF-8 计不超过 81920 字节,超出返回 413。父目录会自动创建。需确认。
240
256
 
241
- **`stat_file`** 返回 `kind`、`size`、`lastModified`、`sensitive`。`includeHash: true` 时对不超过传输上限的文件计算 hash。不返回内容。
257
+ **`append_file`** 把文本接到文件末尾,文件不存在时创建。追加本身不设字节上限。
242
258
 
243
- **`search_files`** 按文件名或 glob 找文件。`*`、`?`、`**` 可用。模式不含 `/` 时匹配文件名,含 `/` 时匹配相对 `path` 的路径。`path` 默认根,`maxDepth` 默认 8、最大 16,`maxResults` 默认并最多 200。跳过 `.git`、`node_modules`、`.svn`、`.idea`、`.vscode`,以及 `~$*`、`.~lock*`、`*.tmp`。敏感文件不出现在结果里。
259
+ **`edit_file`** 把文件中第一处与 `old_text` 完全相同的内容换成 `new_text`。找不到时,`message` 里带最接近片段的起始行号和原文。
244
260
 
245
- **`search_text`** 在指定文件或目录里搜普通文本,返回文件、行号、列号和该行内容。`caseSensitive` 默认否。`glob` 可限制文件名,`contextLines` 为 0–3。`isRegex: true` 返回 501。默认必须把 `path` 指到子目录或文件;要搜整个工作区,配置 `allowWorkspaceWideTextSearch: true`。单文件超过 1 MiB 的不扫,超过 500 字的行会截断,最多扫描 2000 个文件。敏感文件和上面那些忽略目录同样跳过。
261
+ **`grep`** 在目录中递归搜索,`pattern` 按 grep 基本正则。`data.matches` 是带路径和行号的文本。
246
262
 
247
- **`apply_patch`** 对已有 UTF-8 文件做查找替换。必须带 `expectedHash`。`edits` 按顺序应用,每条 `oldText` 在文件中只能出现一次,除非 `replaceAll: true`。执行前确认。确认前、确认后各校验一次 hash,期间文件被外部改过则返回 409,`data.currentHash` 是当前值。先写临时副本,失败或取消时原文件不变。
263
+ **`find`** 按文件名通配查找,`name` 等同 `find -name`。`data.files` 是匹配路径,一行一个。
248
264
 
249
- **`write_file`** 新建或整体覆盖。`content` 是完整 UTF-8 文本。覆盖已有文件必须带 `expectedHash`;新建不要带。`createDirectories: true` 时才会创建尚不存在的父目录,默认否(父目录不存在返回 404)。执行前确认。同样先写临时副本。修改已有文件优先用 `apply_patch`。
265
+ **`shell`** 执行一条 shell 命令(每次新 shell,只保留上次 cwd)。起始目录是工作区根。命令不超过 20000 字。执行器见[命令和脚本](#命令和脚本)。工具 `code` 为 0 时进程退出码在 `data.exitCode`。
250
266
 
251
- **`upload_file`** 把工作区里的一个文件上传到平台文件服务。`target` 只支持 `session`(默认),其他值返回 501。单文件不超过 50 MB。执行前确认。成功返回 `fileId`、`name`、`size`、`url`。`url` 交给模型使用。返回即视为成功,不等待平台解析完成。敏感文件不能上传。
267
+ **`run_file`** 执行已存在的脚本。浏览器:`.py`(Pyodide)、`.sh`(just-bash);`.js` / `node` 明确不支持。宿主沙箱:另支持 `.js`(沙箱自带 Node)。
252
268
 
253
- **`download_file`** 把文件保存到 `destPath`。`source.type` 三选一:
269
+ **`load_skill`** 读取 `skills/<名称>/SKILL.md`。
254
270
 
255
- | type | 还要带 | 含义 |
256
- | --- | --- | --- |
257
- | `sessionFile` | `fileId` | 当前会话里的文件 |
258
- | `platformFile` | `fileId` | `upload_file` 返回的 `fileId` |
259
- | `url` | `url` | 完整 https 地址 |
271
+ **`read_skill_resource`** 读取该技能目录内的相对路径。
260
272
 
261
- 只传 `type`、不传 `fileId` 或 `url` 会失败。`overwrite` 默认否,目标已存在返回 409。覆盖时可再带 `expectedHash`。单文件不超过 50 MB。执行前确认。URL 规则见下文。
273
+ **`run_skill_script`** 执行技能脚本,`args` 是位置参数数组。工作目录为工作区根(与当前命令执行器一致)。
262
274
 
263
275
  ### 浏览器里授权
264
276
 
@@ -445,52 +457,53 @@ function openApprovalDialog(request: ApprovalRequest): Promise<boolean> {
445
457
 
446
458
  `workspace.on` 返回取消订阅的函数。
447
459
 
448
- ## 本地 Python
460
+ ## 命令和脚本
449
461
 
450
- > 只能在端侧宿主里使用。普通浏览器里不会运行脚本,调用返回 501。
462
+ `shell`、`run_file`、`run_skill_script` 共用同一套执行器选择(`runnerFor`),成功时 `data` 为 `{ exitCode, stdout, stderr, cwd? }`。`exitCode !== 0` 仍是工具成功(`code === 0`)。跨命令只保留 cwd,不保留环境变量。
451
463
 
452
- 内置插件 `localPython`。使用前需同时满足:
464
+ ### 执行器选择顺序
453
465
 
454
- - 页面运行在端侧宿主里,且宿主按[宿主通道](#宿主通道)实现了 `run_python`;
455
- - `localFiles.enabled: true` 且 `backend: 'host-bridge'`;
456
- - 配置了 `localPython.onApprovalRequired`。
466
+ 1. **注入的** `LocalWorkspace.commandRunner`(测试 / 自定义)
467
+ 2. **宿主沙箱**:`backend: 'host-bridge'`,且 `workspace` 消息声明 `capabilities` 含 `sandbox-exec`,且 `sandboxProtocol === 1` → 经宿主转发 `@glodon-aiot/workspace-sandbox`(MXC 隔离)
468
+ 3. **浏览器运行时**:`commandRuntime.browser.enabled` 默认为 `true` → just-bash + Pyodide Worker
469
+ 4. 以上皆不可用 → **501**
457
470
 
458
- 没有启用本地工作区时,脚本工具不会注册,`error` 事件里会说明原因。`run_python` 和文件工具在同一个客户端插件里,但文件工具已经授权,也不能代替这一次脚本确认。
471
+ 协议版本不一致时:记一次 `error` 事件日志,**回退浏览器运行时**,不硬失败。未声明 `sandbox-exec` 的旧宿主不再使用其裸 `exec`。
459
472
 
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
- }
472
- ```
473
+ 参照宿主:`examples/qt-workspace-host`。
473
474
 
474
- 平台上的函数名是 `run_python`。参数:
475
+ ### 浏览器运行时
475
476
 
476
- | 字段 | 含义 |
477
+ | 能力 | 行为 |
477
478
  | --- | --- |
478
- | `grantId` | 固定 `current-workspace`,漏传会补上 |
479
- | `path` | 工程内已有的 `.py`,相对工程根 |
480
- | `args` | 字符串数组。最多 16 个。每个参数只能含字母、数字、`_`、`.`、`/`、`:`、`@`、`=`、`+`、`-`、`,`,不能有空格或 shell 语法 |
481
- | `timeoutMs` | 1000–120000,默认 20000 |
479
+ | shell | 按需 `import(bashModuleURL)` 加载 just-bash,文件系统接到当前 `WorkspaceFileAdapter` |
480
+ | Python | Blob Module Worker 加载 Pyodide;白名单包:numpy、pandas、matplotlib、pillow、lxml |
481
+ | Node / `.js` | 明确报错,提示改用 python3 或 shell |
482
+ | pip / 任意第三方 | 不支持 |
483
+
484
+ 默认 CDN(路径带版本,长缓存;可用配置覆盖):
482
485
 
483
- 确认请求是 `PythonApprovalRequest`:`path`、`args`、`workspaceName`、`signal`。与文件确认分开,每次运行都要单独问一次。没有回调、用户拒绝、或脚本命中敏感规则时返回 403,不会启动进程。
486
+ | 配置项 | 默认 |
487
+ | --- | --- |
488
+ | `commandRuntime.browser.bashModuleURL` | `https://cv-cdn.obs.cn-north-4.myhuaweicloud.com/glodon/libs/just-bash/3.6.0/just-bash.mjs` |
489
+ | `commandRuntime.browser.pyodideIndexURL` | `https://cv-cdn.obs.cn-north-4.myhuaweicloud.com/glodon/libs/pyodide/314.0.7/` |
484
490
 
485
- 成功时 `data` 为 `{ exitCode, timedOut, stdout, stderr }`。`exitCode !== 0` 仍然是工具成功(`code === 0`),脚本自己的失败在 `exitCode` 里。超时后 `timedOut === true`。同一时间只能跑一个脚本,再调返回 429。
491
+ **CSP:** 放行 OBS 的 `script-src` / `connect-src`;Pyodide Worker 需 `worker-src blob:`。
486
492
 
487
- 脚本可能改了工程里的文件。运行结束后 SDK 发出 `files:changed`,`paths` 为空,目录树应重读当前看得到的各层。
493
+ 浏览器运行时用于能力对齐与防误用,**不是**与宿主 MXC 同级的强隔离。
488
494
 
489
- 沙箱由宿主启动:工程目录可读写,解释器只读,运行时不能联网,也不能访问工程和解释器以外的文件。工程根下的 `sandbox-requirements.txt` 会在跑脚本前安装(每行一个 PyPI 包,最多 20 行)。参照实现是仓库里的 `examples/qt-workspace-host`。
495
+ **打包注意:** just-bash / Pyodide **不打进** SDK npm 包。请使用包的 **ES 入口**(`import` / `exports.import`)。CJS 产物里动态 `import(url)` 的 `webpackIgnore` 注释可能被压缩去掉,webpack 从 CJS 入口打包时可能错误解析该导入。
496
+
497
+ ### 宿主沙箱(摘要)
498
+
499
+ - 宿主启动一次 `workspace-sandbox --serve`,每请求一行 JSON;文件 IO 在该进程直读写,命令进 MXC
500
+ - 随安装包分发:`Resources/node/`(Node 24)+ `Resources/workspace-sandbox/`(`yarn bundle --platform …` 产物),可选 `python/`
501
+ - 输出做路径反向替换,页面看不到工程绝对路径
502
+ - 细节与 Windows 步骤见 [`local-workspace-windows-host.md`](../bot-client-ui/docs/local-workspace-windows-host.md)
490
503
 
491
504
  ## 宿主通道
492
505
 
493
- 端侧宿主(下文简称宿主)是内嵌此页面的桌面程序,例如用 WebView2 加载页面的客户端。本地 Python 只能通过它运行。
506
+ 端侧宿主(下文简称宿主)是内嵌此页面的桌面程序,例如用 WebView2 加载页面的客户端。
494
507
 
495
508
  `localFiles.backend` 为 `host-bridge` 时,页面通过 `window.chrome.webview` 和宿主说话。通道名固定 `glodon-workspace`。消息里只有相对路径和文件夹名,绝对路径留在宿主内。页面上不再出现「选择工作文件夹」。宿主还没推送目录时,`status` 为 `unsupported`。
496
509
 
@@ -503,10 +516,23 @@ builtinPlugins: {
503
516
  宿主立刻回当前工程。之后每次打开或关闭工程再推一次:
504
517
 
505
518
  ```json
506
- { "channel": "glodon-workspace", "type": "workspace", "granted": true, "displayName": "sample-project" }
519
+ {
520
+ "channel": "glodon-workspace",
521
+ "type": "workspace",
522
+ "granted": true,
523
+ "displayName": "sample-project",
524
+ "capabilities": ["fs-mutate", "sandbox-exec"],
525
+ "sandboxVersion": "0.1.0",
526
+ "sandboxProtocol": 1
527
+ }
507
528
  ```
508
529
 
509
- `granted` 为 `false` 时页面断开工作区。`displayName` 只放文件夹名。
530
+ `granted` 为 `false` 时页面断开工作区。`displayName` 只放文件夹名。找不到 Node / 沙箱包时 **不要** 声明 `fs-mutate` / `sandbox-exec`。
531
+
532
+ | capability | 含义 |
533
+ | --- | --- |
534
+ | `sandbox-exec` | 支持 `exec`(经 workspace-sandbox);须同时带 `sandboxProtocol: 1` |
535
+ | `fs-mutate` | 支持 `mkdir` / `remove` / `move` |
510
536
 
511
537
  目录有增删时再推一条。`paths` 是发生变化的目录,相对工程根,根目录用 `""`。空数组表示让页面重读已展开的全部目录。短时间内多次变化可以合成一条。
512
538
 
@@ -516,7 +542,7 @@ builtinPlugins: {
516
542
 
517
543
  脚本跑完、用户在资源管理器里改文件,都应推送。Qt 示例用目录监视做这件事,默认不监视 `.git`、`node_modules`、`__pycache__` 内部。
518
544
 
519
- 其余请求都带 `id` 和 `op`。路径用 `/`,不能含 `..`、盘符、反斜杠或绝对路径。不合法时回 `{ "id", "ok": false, "name": "SecurityError" }`,不要访问磁盘。
545
+ 其余请求都带 `id` 和 `op`。路径用 `/`,不能含 `..`、盘符、反斜杠或绝对路径。不合法时回 `{ "id", "ok": false, "name": "SecurityError" }`,不要访问磁盘。宿主补上本地 `root` 后转发给 `workspace-sandbox --serve`,按 `id` 回传。
520
546
 
521
547
  | op | 页面发来的字段 | 成功时宿主回 |
522
548
  | --- | --- | --- |
@@ -524,6 +550,8 @@ builtinPlugins: {
524
550
  | `stat` | `path` | `entry`:同样的字段 |
525
551
  | `read` | `path` | `contentBase64`。单文件不超过 50 MB |
526
552
  | `write` | `path`、`contentBase64`、`createDirectories` | `{ "ok": true }`。父目录不存在且 `createDirectories` 不是 `true` 时回 `NotFoundError` |
553
+ | `mkdir` / `remove` / `move` | 路径规则同 write;`move` 用 `from`/`to`;需 `fs-mutate` | `{ "ok": true }` |
554
+ | `exec` | `protocol`(当前为 1)、`cwd`(工作区相对目录)、`command` 或 `argv`、`timeoutMs` | `exitCode`、`stdout`、`stderr`、`cwd?`、`timedOut?` |
527
555
  | `run_python` | `path`、`args`、`timeoutMs` | `exitCode`、`timedOut`、`stdout`、`stderr` |
528
556
 
529
557
  失败时统一 `{ "id", "ok": false, "name" }`:
@@ -537,8 +565,9 @@ builtinPlugins: {
537
565
  | `QuotaExceededError` | 读写超过 50 MB |
538
566
  | `NoModificationAllowedError` | 写入没有完成 |
539
567
  | `AbortError` | 已经有一个脚本在跑,或调用超时 |
568
+ | `InvalidModificationError` | 非空目录删除未带 `recursive` 等 |
540
569
 
541
- `lastModified` 用 Unix 毫秒。`size` 用字节数。搜索、按行修改、上传、下载仍由页面完成,底层落到上面的 `list` / `read` / `write`。宿主不要自己实现那 9 个工具名。
570
+ `lastModified` 用 Unix 毫秒。`size` 用字节数。搜索、按行修改、上传、下载仍由页面完成,底层落到上面的 `list` / `read` / `write`。宿主不要自己实现那些工具名。文件 op 可并发;命令类同一时间只跑一个。
542
571
 
543
572
  ## 安全与限额
544
573
 
@@ -589,7 +618,7 @@ builtinPlugins: {
589
618
  | 429 | 并发过多,或已有一个脚本在运行 |
590
619
  | 499 | 已取消(停止回答、更换文件夹) |
591
620
  | 500 | 内部异常。消息里不含路径和调用栈 |
592
- | 501 | 当前不支持:正则搜索、非 session 的上传目标、网页环境里的 `run_python` |
621
+ | 501 | 无可用命令执行器(关闭浏览器运行时且无宿主沙箱 / 注入 runner) |
593
622
 
594
623
  ## 开发
595
624
 
@@ -1,22 +1,21 @@
1
- var O = Object.defineProperty, w = Object.defineProperties;
2
- var k = Object.getOwnPropertyDescriptors;
3
- var h = Object.getOwnPropertySymbols;
4
- var F = Object.prototype.hasOwnProperty, N = Object.prototype.propertyIsEnumerable;
5
- var a = (e, i, o) => i in e ? O(e, i, { enumerable: !0, configurable: !0, writable: !0, value: o }) : e[i] = o, c = (e, i) => {
6
- for (var o in i || (i = {}))
7
- F.call(i, o) && a(e, o, i[o]);
8
- if (h)
9
- for (var o of h(i))
10
- N.call(i, o) && a(e, o, i[o]);
1
+ var R = Object.defineProperty, _ = Object.defineProperties;
2
+ var B = Object.getOwnPropertyDescriptors;
3
+ var P = Object.getOwnPropertySymbols;
4
+ var w = Object.prototype.hasOwnProperty, O = Object.prototype.propertyIsEnumerable;
5
+ var C = (e, n, i) => n in e ? R(e, n, { enumerable: !0, configurable: !0, writable: !0, value: i }) : e[n] = i, r = (e, n) => {
6
+ for (var i in n || (n = {}))
7
+ w.call(n, i) && C(e, i, n[i]);
8
+ if (P)
9
+ for (var i of P(n))
10
+ O.call(n, i) && C(e, i, n[i]);
11
11
  return e;
12
- }, R = (e, i) => w(e, k(i));
13
- import f from "./Logger.mjs";
14
- import { LOCAL_PYTHON_FUNCTION_CODE as y, LOCAL_FILES_PLUGIN_CODES as A, LOCAL_PYTHON_PLUGIN_CODES as E } from "./localFiles/constants.mjs";
15
- import { createLocalFilesPlugin as I } from "./localFiles/createLocalFilesPlugin.mjs";
16
- import { createLocalPythonPlugin as $ } from "./localFiles/createLocalPythonPlugin.mjs";
17
- const q = new f("[Config] ");
18
- var _ = /* @__PURE__ */ ((e) => (e.dev = "https://aiot-dev.glodon.com/api/cvforcepd", e.test = "https://aiot-dev.glodon.com/api/cvforce", e.prod = "https://copilot.glodon.com/api/cvforce", e.local = "http://localhost:3000/api/cvforcepd", e))(_ || {});
19
- let n = {
12
+ }, L = (e, n) => _(e, B(n));
13
+ import g from "./Logger.mjs";
14
+ import { LOCAL_FILES_PLUGIN_CODES as k, LOCAL_PYTHON_PLUGIN_CODES as E } from "./localFiles/constants.mjs";
15
+ import { createBuiltinToolsPlugin as I } from "./builtinTools/createBuiltinToolsPlugin.mjs";
16
+ const S = new g("[Config] ");
17
+ var h = /* @__PURE__ */ ((e) => (e.dev = "https://aiot-dev.glodon.com/api/cvforcepd", e.test = "https://aiot-dev.glodon.com/api/cvforce", e.prod = "https://copilot.glodon.com/api/cvforce", e.local = "http://localhost:3000/api/cvforcepd", e))(h || {});
18
+ let o = {
20
19
  debug: !1,
21
20
  env: "prod",
22
21
  //AIoT 环境, 与apiRoot配对使用
@@ -31,95 +30,74 @@ let n = {
31
30
  mcp: void 0,
32
31
  loadRelatedQuesions: !0,
33
32
  stream: !1
34
- }, u, r, d;
35
- const v = /* @__PURE__ */ new Set();
36
- function T(e) {
37
- u !== e && (u = e, r = void 0, d = void 0, v.clear());
33
+ }, u, l;
34
+ const c = /* @__PURE__ */ new Set();
35
+ function Q(e) {
36
+ u !== e && (u = e, l = void 0, c.clear());
38
37
  }
39
- function j() {
40
- if (!r)
38
+ function Y() {
39
+ if (!l)
41
40
  return;
42
41
  const {
43
42
  pluginCode: e,
44
- table: i
45
- } = r, o = n.plugins[e], l = i[e];
46
- if (!(!o || o.list_directory !== l.list_directory))
43
+ table: n
44
+ } = l, i = o.plugins[e], t = n[e];
45
+ if (!(!i || i.shell !== t.shell))
47
46
  return e;
48
47
  }
49
- function z() {
50
- if (!d)
48
+ function j() {
49
+ if (!l)
51
50
  return;
52
51
  const {
53
52
  pluginCode: e,
54
- table: i
55
- } = d, o = n.plugins[e], l = i[e];
56
- if (!(!o || o[y] !== l[y]))
53
+ table: n
54
+ } = l, i = o.plugins[e], t = n[e];
55
+ if (!(!i || i.run_file !== t.run_file))
57
56
  return e;
58
57
  }
59
- function S(e, i) {
60
- return (e == null ? void 0 : e.pluginCode) || A[i];
58
+ function T(e, n) {
59
+ return (e == null ? void 0 : e.pluginCode) || k[n];
61
60
  }
62
- function D(e, i, o) {
63
- return (e == null ? void 0 : e.pluginCode) || o || E[i];
61
+ function N(e, n, i) {
62
+ return (e == null ? void 0 : e.pluginCode) || i || E[n];
64
63
  }
65
- function H() {
66
- var P, C, b;
67
- const e = (P = n.businessPlugins) != null ? P : {}, i = (C = n.builtinPlugins) == null ? void 0 : C.localFiles, o = (b = n.builtinPlugins) == null ? void 0 : b.localPython, l = [], g = c({}, e);
68
- let s, p = !1;
69
- if (i != null && i.enabled && u && (s = S(i, n.env), s ? e[s] ? l.push(`[LocalFiles] 业务插件中已存在 pluginCode「${s}」,已保留业务插件,内置文件工具未注册。`) : (((r == null ? void 0 : r.workspace) !== u || r.pluginCode !== s) && (r = {
64
+ function D() {
65
+ var v, b, p;
66
+ const e = (v = o.businessPlugins) != null ? v : {}, n = (b = o.builtinPlugins) == null ? void 0 : b.localFiles, i = (p = o.builtinPlugins) == null ? void 0 : p.localPython, t = [], d = r({}, e);
67
+ if (!!!(n != null && n.enabled || i != null && i.enabled))
68
+ return o.plugins = d, t;
69
+ if (!u)
70
+ return t.push("[BuiltinTools] 内置工具依赖已启用的本地工作区。工具未注册。"), o.plugins = d, t;
71
+ const f = n != null && n.enabled ? T(n, o.env) : void 0, s = f || (i != null && i.enabled ? N(i, o.env, f) : void 0);
72
+ return s ? e[s] ? t.push(`[BuiltinTools] 业务插件中已存在 pluginCode「${s}」,已保留业务插件,内置工具未注册。`) : (((l == null ? void 0 : l.workspace) !== u || l.pluginCode !== s) && (l = {
70
73
  workspace: u,
71
74
  pluginCode: s,
72
75
  table: I({
73
76
  pluginCode: s,
74
77
  workspace: u
75
78
  })
76
- }), g[s] = r.table[s], p = !0) : l.push(`[LocalFiles] 当前环境(env: ${n.env})没有默认的内置工具插件编码,请配置 builtinPlugins.localFiles.pluginCode。内置文件工具未注册。`)), o != null && o.enabled) {
77
- const t = D(o, n.env, s);
78
- if (!u)
79
- l.push("[LocalPython] 运行脚本依赖已启用的本地工作区。内置脚本工具未注册。");
80
- else if (!t)
81
- l.push(`[LocalPython] 当前环境(env: ${n.env})没有默认的内置工具插件编码,请配置 builtinPlugins.localPython.pluginCode,或与文件工具使用同一个 pluginCode。内置脚本工具未注册。`);
82
- else if (e[t])
83
- l.push(`[LocalPython] pluginCode「${t}」已被业务插件占用,已保留业务插件,内置脚本工具未注册。`);
84
- else if (g[t] && !(p && t === s))
85
- l.push(`[LocalPython] pluginCode「${t}」已被占用,已保留原插件,内置脚本工具未注册。`);
86
- else {
87
- ((d == null ? void 0 : d.workspace) !== u || d.pluginCode !== t || d.approve !== o.onApprovalRequired) && (d = {
88
- workspace: u,
89
- pluginCode: t,
90
- approve: o.onApprovalRequired,
91
- table: $({
92
- pluginCode: t,
93
- workspace: u,
94
- onApprovalRequired: o.onApprovalRequired
95
- })
96
- });
97
- const L = d.table[t];
98
- g[t] = p && t === s ? c(c({}, g[t]), L) : L;
99
- }
100
- }
101
- return n.plugins = g, l;
79
+ }), d[s] = l.table[s]) : t.push(`[BuiltinTools] 当前环境(env: ${o.env})没有默认的内置工具插件编码,请配置 builtinPlugins.localFiles.pluginCode。内置工具未注册。`), o.plugins = d, t;
102
80
  }
103
- function B() {
104
- return H().filter((e) => !v.has(e)).map((e) => (v.add(e), console.error(e), new Error(e)));
81
+ function G() {
82
+ return D().filter((e) => !c.has(e)).map((e) => (c.add(e), console.error(e), new Error(e)));
105
83
  }
106
- function J(e) {
107
- var i, o, l;
108
- f.debug = (e == null ? void 0 : e.debug) || !1, n = R(c(c({}, n), e), {
109
- apiRoot: (e == null ? void 0 : e.apiRoot) || n.apiRoot || (e != null && e.env ? _[e.env || "prod"] : n.apiRoot),
84
+ function q(e) {
85
+ var n, i, t;
86
+ g.debug = (e == null ? void 0 : e.debug) || !1, o = L(r(r({}, o), e), {
87
+ apiRoot: (e == null ? void 0 : e.apiRoot) || o.apiRoot || (e != null && e.env ? h[e.env || "prod"] : o.apiRoot),
110
88
  // 应用未注册插件时 param.plugins 为 undefined,避免覆盖默认空表
111
- businessPlugins: (o = (i = e == null ? void 0 : e.plugins) != null ? i : n.businessPlugins) != null ? o : {},
112
- builtinPlugins: (l = e == null ? void 0 : e.builtinPlugins) != null ? l : n.builtinPlugins
113
- }), B(), f.debug && q.log("updated", n);
89
+ businessPlugins: (i = (n = e == null ? void 0 : e.plugins) != null ? n : o.businessPlugins) != null ? i : {},
90
+ builtinPlugins: (t = e == null ? void 0 : e.builtinPlugins) != null ? t : o.builtinPlugins
91
+ }), G(), g.debug && S.log("updated", o);
114
92
  }
115
93
  export {
116
- _ as ApiUrl,
117
- n as configuration,
118
- J as configure,
119
- j as getRegisteredLocalFilesPluginCode,
120
- z as getRegisteredLocalPythonPluginCode,
121
- B as refreshBuiltinPlugins,
122
- S as resolveLocalFilesPluginCode,
123
- D as resolveLocalPythonPluginCode,
124
- T as setActiveLocalWorkspace
94
+ h as ApiUrl,
95
+ o as configuration,
96
+ q as configure,
97
+ Y as getRegisteredLocalFilesPluginCode,
98
+ j as getRegisteredLocalPythonPluginCode,
99
+ G as refreshBuiltinPlugins,
100
+ T as resolveLocalFilesPluginCode,
101
+ N as resolveLocalPythonPluginCode,
102
+ Q as setActiveLocalWorkspace
125
103
  };