@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0

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 (272) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +20 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -1,11 +1,28 @@
1
1
  # 统一 Bot 客户端
2
2
 
3
+ ## 工作空间重构(本地开发预览)
4
+
5
+ `workspace-navigation.mjs` 定义工作空间页面和按产品 API 隔离的 UI 深链、一次性目标转交与内存草稿;不存权限或执行凭据。`workspace-model.mjs` 从已提交投影生成总览、搜索与看板;未加载的任务不计入统计,不推算节省时间。
6
+
7
+ `workspace-ui.jsx` 提供首页、任务待办、成员目录、键盘搜索和外观控制。`workspace.css` 重排产品导航与信息层级,并在明暗主题、紧凑密度和窄屏下使用同一组语义颜色。产品页内 Ctrl/Cmd K 打开工作空间搜索;聊天中的原搜索继续由 Bot 界面处理。外观偏好保存在本机浏览器,目标文本仅在当前宿主 API 的内存里保留。
8
+
9
+ 新目标仍先检查候选与预览,再通过原始提交确认流程派发;看板不提供拖动改状态。任务链接由 Bot ID 与 Task ID 共同定位,电脑搜索只聚焦原电脑卡片,不自动连接或取得控制。
10
+
11
+ ```sh
12
+ node --test tests/workspace-model.test.mjs tests/workspace-ui.integration.test.mjs
13
+ node scripts/verify-workspace-browser.mjs
14
+ node scripts/verify-workspace-browser.mjs --serve
15
+ ```
16
+
17
+ 浏览器脚本运行真实产品组件与离线 fixture,截图明确标注为本地验证数据;它不证明实际 Harness Desktop 的完整安装、Electron 或用户 profile 已验收。
18
+
3
19
  `index.jsx` 在同一 ModuleLoader factory 中安装完整冻结 DS 界面和现有 MR 高级工作台。
4
20
  默认沿用 DS 的 Main、Bot/工作群、记忆、侧聊、语音、设置、能力与计划任务。
5
- 通过公共 sidebar.workspaces slot 包装原 TeamSidebar,在底部增加中文入口:
6
- Bot 绑定、任务、远程电脑、费用与用量、高级工作台。没有修改 vendor 或旧 MR 大面板。
21
+ 通过公共 sidebar.workspaces slot 包装原 TeamSidebar,联系人保留主区域,底部使用
22
+ ContactNavigation 的待办、工作空间和资源菜单。HeaderPill 通过自有 product-overlay
23
+ 增加会话工具,原注册组件身份、工作目标、审批、输入和执行授权继续由 native 路径负责。
7
24
 
8
- 四个产品页只读取宿主 `schemaVersion:1` 投影。缺后端、宿主读取错误、只读锁、未知费用、
25
+ 产品页读取宿主 `schemaVersion:1` 投影。缺后端、宿主读取错误、只读锁、未知费用、
9
26
  未在线/锁屏机器都有明确状态和下一步;列表不放示例 Bot、机器、任务或收据。
10
27
  Task 的核对/取消/重试只有宿主声明动作可用时显示,最终授权仍由宿主执行。
11
28
 
@@ -15,16 +32,23 @@ node scripts/integration-product-client.mjs --check
15
32
  node --test tests/bot-runtime-product.test.mjs tests/bot-runtime-ui.test.mjs
16
33
  ```
17
34
 
18
- 默认生成 `apps/integration-client/client.js`;`--output <path>` 可写入主包统一的
19
- `.dsh-plugin/client.js`。构建 API `bundleProductClient()` 返回 `{code,inputs,bytes}`,
35
+ 默认生成主包统一的 `.dsh-plugin/client.js`,`--output <path>` 可显式指定其他位置。
36
+ 构建 API `bundleProductClient()` 返回 `{code,inputs,bytes}`,
20
37
  `generateProductClient({output,check})` 可供主构建脚本调用。生成物只有一个 loader id
21
38
  `@ljwei-stak/dsh-model-route-bot`,没有单独 `ds-bot` 的 loader。纯路由包 `@ljwei-stak/dsh-model-router` 使用独立工作台入口,不导入此集成客户端。
22
39
  React、ReactDOM 与 Harness UI primitives 继续由宿主提供。
23
40
 
24
- 当前完整功能并集 bundle 约 2 MiB。旧 MR 的单独客户端保持原构建方式,是否切换默认
25
- 产物由主插件接入与产品验收决定。发布还需在主包的客户端依赖声明中包含 DS 公开
41
+ 实际 bundle 大小以构建结果为准。旧 MR 的单独客户端保持原构建方式。
42
+ 发布还需在主包的客户端依赖声明中包含 DS 公开
26
43
  `inject` 所需的 sessions、uiWorkspace、uiConversation、slots、theme、layout 等服务。
27
44
 
28
45
  组件 DOM 检查覆盖未知 Operation 原依据、核对请求使用原 taskId、同模型不同 Bot、
29
46
  未接入权限、未知金额及 Windows 锁屏原因。实际浏览器/Electron 的整体布局、键盘/窄屏
30
47
  和默认 profile 升级仍需单独验收;本目录的 DOM 检查不宣称这些已通过。
48
+
49
+ 安全配对使用 `secure-pairing.jsx` 与 `pairing-qr.jsx`,支持短期邀请、独立确认码、摄像头
50
+ 和本地二维码图片解码。jsQR 1.4.0 的 Apache-2.0 许可证随
51
+ `licenses/jsqr-1.4.0-LICENSE.txt` 分发;源码和作者见 https://github.com/cozmo/jsQR。
52
+ 二维码图像不会发送给模型或外部解码服务。摄像头仅在显式点击后开启。
53
+ 协议、Host 与 Agent 的实现及验证边界见
54
+ `docs/implementation/20261107-secure-pairing.md`;尚不代表全功能迁移或真实用户桌面验收完成。
@@ -1,11 +1,15 @@
1
- export const PRODUCT_PANELS = Object.freeze({ bindings: 'bot-product-bindings', tasks: 'bot-product-tasks', machines: 'bot-product-machines', library: 'bot-product-library', routine: 'bot-product-routine', usage: 'bot-product-usage' })
1
+ export { PRODUCT_PANELS } from './workspace-navigation.mjs'
2
2
 
3
3
  export function createProductApi(fetcher = globalThis.fetch, clientLocation = globalThis.location) {
4
+ const listeners = new Set()
4
5
  const call = async (action, payload, signal) => {
5
6
  // Desktop's protocol bridge removes Origin before forwarding to the HTTP
6
7
  // Host. Preserve only the known native viewer origin as a transport hint;
7
8
  // owner admission and Bot/machine authority still belong to the Host.
8
- const nativeDesktop = ['machine.desktop.open', 'machine.desktop.renew'].includes(action)
9
+ // The desktop bridge strips Origin, so the native viewer supplies its own. A web panel probe
10
+ // needs the same hint: it evaluates frame-ancestors against the origin the page is really
11
+ // framed from, and an HTTP Host origin must never stand in for dsh-app://app.
12
+ const nativeDesktop = ['machine.desktop.open', 'machine.desktop.renew', 'machine.desktop.poll', 'machine.desktop.send', 'webPanels.probe'].includes(action)
9
13
  && clientLocation?.protocol === 'dsh-app:' && clientLocation?.host === 'app'
10
14
  const response = await fetcher('api/bot-product', { method: 'POST', credentials: 'same-origin',
11
15
  headers: { 'content-type': 'application/json', ...(nativeDesktop ? { 'x-dsh-desktop-origin': 'dsh-app://app' } : {}) },
@@ -15,7 +19,14 @@ export function createProductApi(fetcher = globalThis.fetch, clientLocation = gl
15
19
  if (!response.ok || body.ok !== true) throw Object.assign(new Error(body.error?.message ?? `宿主请求失败(HTTP ${response.status})`), { code: body.error?.code })
16
20
  return body.value
17
21
  }
18
- return Object.freeze({ snapshot: signal => call('snapshot', undefined, signal), dispatch: (action, payload, signal) => call(action, payload, signal) })
22
+ return Object.freeze({ snapshot: signal => call('snapshot', undefined, signal), attention: signal => call('task.attention', {}, signal),
23
+ async dispatch(action, payload, signal) {
24
+ const value = await call(action, payload, signal)
25
+ for (const listener of [...listeners]) { try { listener(action) } catch {} }
26
+ return value
27
+ },
28
+ subscribeActions(listener) { listeners.add(listener); return () => listeners.delete(listener) },
29
+ })
19
30
  }
20
31
 
21
32
  export const TASK_STATES = Object.freeze({ queued: '排队中', ready:'等待执行', running: '执行中', cancelling: '停止中', cancelled: '已停止', succeeded: '已完成', failed: '失败',
@@ -1,118 +1,317 @@
1
- import React, { useEffect, useMemo, useState } from 'react'
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Blocked } from './panels.jsx'
3
+ import { UiIcon } from './ui-icons.jsx'
4
+ import bindingCss from './binding.css'
5
+ import { BotModelPicker } from './model-picker.jsx'
6
+ import { botModelStatus } from './bot-model-status.mjs'
7
+ import { MainRouteEditor } from './main-route-editor.jsx'
8
+ import { mainDefaultFields, mainRoutePayload, mainRouteProblems, startMainRoutes } from './main-route-model.mjs'
9
+ import {
10
+ accountFor, bindingProblems, buildBinding, defaultMode, EXECUTION_MODES, failureText, findModel, FUNDING_MODES,
11
+ MODE_HELP, MODE_LABEL, numberOrNull, routeOf, ROUTING_PRESETS, scopeAllows, seedForm,
12
+ toolCards, workspaceKey, workspaceLabel, workspaceOptions,
13
+ } from './binding-model.mjs'
3
14
 
4
- const route = model => model ? `${model.provider}/${model.model}` : ''
5
- const workspace = ref => ref ? `${ref.machineId}/${ref.rootId}` : ''
6
-
7
- export function BotBindingEditor({ bot, record, accounts, dispatch, busy }) {
15
+ /**
16
+ * Bot binding editor.
17
+ *
18
+ * The props contract is unchanged: `{ bot, record, accounts, dispatch, busy }`.
19
+ * `record` is the Host's committed read model (settings + trusted catalog); every
20
+ * choice offered here comes from that catalog, and a save always carries the
21
+ * revision it was read at, so the Host keeps authority over what is granted.
22
+ *
23
+ * Layout follows the Bot first: pick a provider, then an exact model, then the
24
+ * funding scope; workspace, capabilities and budgets stay available but folded.
25
+ * The official-tools module is a separate region because granting a CLI execution
26
+ * path is a different decision from choosing the model this Bot talks with.
27
+ */
28
+ export function BotBindingEditor({ bot, record, accounts, dispatch, busy, hostScope, onOpenTools }) {
29
+ const connection = hostScope ?? dispatch
30
+ const draftConnection = useRef(connection)
31
+ const catalog = record?.catalog
8
32
  const [form, setForm] = useState(null)
33
+ const [dirty, setDirty] = useState(false)
34
+ const [feedback, setFeedback] = useState(null)
35
+ const [working, setWorking] = useState('')
36
+ const seedKey = useRef('')
37
+ const seedRevision = useRef(null)
38
+ const saveToken = useRef(0)
39
+ const workingRef = useRef('')
40
+
41
+ useEffect(() => {
42
+ // The stylesheet ships with this component so the editor needs no parent wiring;
43
+ // index.jsx may concatenate it instead, but not both at once.
44
+ const style = document.createElement('style')
45
+ style.dataset.botBindingEditor = '1'
46
+ style.textContent = bindingCss
47
+ document.head.append(style)
48
+ return () => { saveToken.current++; workingRef.current = ''; style.remove() }
49
+ }, [])
50
+
9
51
  useEffect(() => {
10
- if (!record) { setForm(null); return }
11
- const value = record.settings
12
- const selectedWorkspace = value.workspaceRef ?? value.machineGrants[0] ?? null
13
- setForm({
14
- enabled: value.enabled, model: route(value.model), workspace: workspace(selectedWorkspace), workspaceMode: selectedWorkspace?.mode ?? 'read-only',
15
- capabilities: value.capabilities.length ? value.capabilities : record.catalog.capabilities.map(item => item.id),
16
- officialTools: value.officialTools, fundingMode: value.funding.mode,
17
- input: value.funding.pricing?.input ?? 1, output: value.funding.pricing?.output ?? 2,
18
- maxInputTokens: value.funding.request?.maxInputTokens ?? 100000, maxOutputTokens: value.funding.request?.maxOutputTokens ?? 4096,
19
- daily: value.funding.dailyBudgetUsd ?? 5, monthly: value.funding.monthlyBudgetUsd ?? 50,
20
- accountDaily: 10, accountMonthly: 100,
21
- dailyCalls: value.funding.subscription?.dailyCalls ?? 20, monthlyCalls: value.funding.subscription?.monthlyCalls ?? 200,
22
- maxConcurrentTasks: value.maxConcurrentTasks,
23
- // Loaded from committed settings so the form shows what is actually stored, not a default.
24
- routing: {
25
- preset: value.routing?.preset ?? '',
26
- dailyBudgetUsd: value.routing?.dailyBudgetUsd ?? '',
27
- monthlyBudgetUsd: value.routing?.monthlyBudgetUsd ?? '',
28
- },
29
- })
30
- }, [bot.id, record?.revision])
31
- const model = useMemo(() => record?.catalog.models.find(item => route(item) === form?.model) ?? null, [record, form?.model])
32
- const account = accounts.items.find(item => item.provider === model?.provider)
52
+ if (draftConnection.current !== connection) {
53
+ draftConnection.current = connection; seedKey.current = ''; saveToken.current++; workingRef.current = ''; setWorking(''); setFeedback(null)
54
+ }
55
+ if (!record) { saveToken.current++; seedKey.current = ''; seedRevision.current = null; setForm(null); return }
56
+ const botId = bot?.id ?? ''
57
+ const key = `${botId}#${record.revision ?? 0}`
58
+ if (seedKey.current === key) return
59
+ const sameBot = seedKey.current.split('#')[0] === botId
60
+ seedKey.current = key
61
+ // A late Host or Bot switch invalidates the in-flight save of the previous Bot.
62
+ if (!sameBot) { saveToken.current += 1; workingRef.current = ''; setWorking('') }
63
+ // A revision bump from another window must not silently drop unsaved work.
64
+ if (sameBot && dirty) {
65
+ setFeedback({ kind: 'notice', text: `宿主中的绑定已更新到修订号 ${record.revision ?? 0};你在此处未保存的修改仍然保留,保存时会按旧修订号提交,宿主可能拒绝。` })
66
+ return
67
+ }
68
+ seedRevision.current = record.revision
69
+ setForm(seedForm(record, accounts))
70
+ setDirty(false)
71
+ setFeedback(current => (current?.kind === 'saved' ? current : null))
72
+ }, [bot?.id, record, dirty, accounts, connection])
73
+
74
+ const edit = patch => {
75
+ setForm(current => (current ? { ...current, ...patch } : current))
76
+ setFeedback(null)
77
+ setDirty(true)
78
+ }
79
+
80
+ const model = form ? findModel(catalog, form.model) : null
81
+ const savedRoute = form?.model ?? ''
82
+ const savedMissing = Boolean(savedRoute) && !model
83
+ const machineOptions = useMemo(() => workspaceOptions(catalog), [catalog])
84
+ const [machineId, rootId] = (form?.workspace ?? '').split('/')
85
+ const machine = machineOptions.find(item => item.machineId === machineId) ?? null
86
+ const root = machine?.roots.find(item => item.rootId === rootId) ?? null
87
+ const account = accountFor(accounts, model?.provider)
88
+ const mainRoutes = bot?.main ? form?.mainRoutes : undefined
89
+ const scope = (catalog?.fundingScopes ?? []).find(item => item.id === account?.id) ?? null
90
+ const tools = useMemo(() => toolCards({ catalog, model, grants: form?.officialTools }), [catalog, model, form?.officialTools])
91
+ const problems = form ? bindingProblems({ form, model, account, machine, root, catalog, scope, mainRoutes: Boolean(mainRoutes) }) : []
92
+ if (mainRoutes) problems.push(...mainRouteProblems(mainRoutes, { defaultRoute: form.model, enabled: form.enabled, catalog, accounts }))
93
+ const staleTools = tools.filter(tool => tool.stale)
94
+ const hostModel = botModelStatus(bot, catalog?.models)
95
+
33
96
  if (!record || !form) return <Blocked value={{ reason: '此 Bot 尚未取得绑定目录。' }} />
34
- const change = (key, value) => setForm(current => ({ ...current, [key]: value }))
35
- const selectedMachine = form.workspace.split('/')[0]
36
- const selectedRoot = form.workspace.split('/').slice(1).join('/')
37
- // This Bot's own routing policy: empty means it inherits the global preset.
38
- const routing = form.routing ?? { preset: '', dailyBudgetUsd: '', monthlyBudgetUsd: '' }
39
- const enableAccount = () => dispatch('funding.account.save', { scopeId: account.id, expectedRevision: account.revision, enabled: true,
40
- dailyLimitUsd: Number(form.accountDaily), monthlyLimitUsd: Number(form.accountMonthly) })
41
- const save = event => {
97
+
98
+ const accountLimitsReady = numberOrNull(form.accountDaily) !== null && numberOrNull(form.accountMonthly) !== null
99
+
100
+ const setToolGrant = (tool, cliModel, checked) => {
101
+ const others = form.officialTools.filter(item => item.toolId !== tool.toolId)
102
+ edit({ officialTools: checked
103
+ ? [...others, { toolId: tool.toolId, cliModel, mode: tool.grant?.mode ?? defaultMode(tool.modes) }]
104
+ : others })
105
+ }
106
+ const setToolMode = (toolId, mode) => edit({ officialTools: form.officialTools.map(item => (item.toolId === toolId ? { ...item, mode } : item)) })
107
+ const dropStaleTools = () => edit({ officialTools: form.officialTools.filter(item => !staleTools.some(tool => tool.toolId === item.toolId)) })
108
+
109
+ const enableAccount = async (target, dailyLimitUsd, monthlyLimitUsd) => {
110
+ if (!target || workingRef.current) return
111
+ const token = saveToken.current
112
+ workingRef.current = 'funding'
113
+ setWorking('funding')
114
+ setFeedback(null)
115
+ try {
116
+ await dispatch('funding.account.save', { scopeId: target.id, expectedRevision: target.revision, enabled: true,
117
+ dailyLimitUsd, monthlyLimitUsd })
118
+ if (saveToken.current !== token) return
119
+ setFeedback({ kind: 'saved', text: `已启用 ${target.provider} 资金范围。保存绑定后生效。` })
120
+ } catch (error) {
121
+ if (saveToken.current !== token) return
122
+ setFeedback({ kind: 'error', text: failureText(error) })
123
+ } finally { if (saveToken.current === token) { workingRef.current = ''; setWorking('') } }
124
+ }
125
+
126
+ const save = async event => {
42
127
  event.preventDefault()
43
- const settings = {
44
- schemaVersion: 1, enabled: form.enabled,
45
- model: model ? { provider: model.provider, model: model.model } : null,
46
- officialTools: form.officialTools,
47
- machineGrants: selectedMachine && selectedRoot ? [{ machineId: selectedMachine, rootId: selectedRoot, mode: form.workspaceMode, capabilities: [] }] : [],
48
- capabilities: form.capabilities,
49
- workspaceRef: selectedMachine && selectedRoot ? { kind: record.catalog.machines.find(item => item.machineId === selectedMachine)?.location ?? 'local', machineId: selectedMachine, rootId: selectedRoot } : null,
50
- funding: {
51
- mode: form.fundingMode, scopeId: account?.enabled ? account.id : null,
52
- pricing: form.fundingMode === 'api' ? { input: Number(form.input), output: Number(form.output) } : null,
53
- request: form.fundingMode === 'api' ? { maxInputTokens: Number(form.maxInputTokens), maxOutputTokens: Number(form.maxOutputTokens) } : null,
54
- dailyBudgetUsd: form.fundingMode === 'api' ? Number(form.daily) : null,
55
- monthlyBudgetUsd: form.fundingMode === 'api' ? Number(form.monthly) : null,
56
- subscription: form.fundingMode === 'subscription' ? { dailyCalls: Number(form.dailyCalls), monthlyCalls: Number(form.monthlyCalls) } : null,
57
- },
58
- maxConcurrentTasks: Number(form.maxConcurrentTasks),
59
- // An empty preset inherits; a budget left blank means "no separate ceiling for this Bot".
60
- routing: routing.preset === '' && routing.dailyBudgetUsd === '' && routing.monthlyBudgetUsd === ''
61
- ? null
62
- : {
63
- preset: routing.preset === '' ? null : routing.preset,
64
- dailyBudgetUsd: routing.dailyBudgetUsd === '' ? null : Number(routing.dailyBudgetUsd),
65
- monthlyBudgetUsd: routing.monthlyBudgetUsd === '' ? null : Number(routing.monthlyBudgetUsd),
66
- },
67
- }
68
- return dispatch('binding.save', { botId: bot.id, expectedRevision: record.revision, settings })
128
+ // A second click in the same batch must not submit twice against the same revision.
129
+ if (workingRef.current || problems.length) return
130
+ const token = saveToken.current
131
+ const defaultForm = mainRoutes ? { ...form, ...mainDefaultFields(mainRoutes, form.model) } : form
132
+ const settings = buildBinding({ form: defaultForm, model, account, machine, root })
133
+ if (mainRoutes !== undefined) settings.mainRoutes = mainRoutePayload(mainRoutes)
134
+ workingRef.current = 'binding'
135
+ setWorking('binding')
136
+ setFeedback({ kind: 'saving', text: '正在提交到宿主校验…' })
137
+ try {
138
+ const expectedRevision = seedRevision.current
139
+ const result = await dispatch('binding.save', { botId: bot.id, expectedRevision, settings })
140
+ if (saveToken.current !== token) return
141
+ if (!Number.isSafeInteger(result?.revision) || result.revision <= expectedRevision || (result.botId && result.botId !== bot.id)) throw new Error('尚未收到当前 Bot 的保存确认,请刷新配置后核对。')
142
+ seedRevision.current = result?.revision ?? seedRevision.current
143
+ setDirty(false)
144
+ setFeedback({ kind: 'saved', text: `设置已保存。` })
145
+ } catch (error) {
146
+ if (saveToken.current !== token) return
147
+ setFeedback({ kind: 'error', text: failureText(error) })
148
+ } finally { if (saveToken.current === token) { workingRef.current = ''; setWorking('') } }
69
149
  }
70
- const cliMappings = (model?.cliModels ?? []).filter((item, index, items) => items.findIndex(other => other.toolId === item.toolId && other.model === item.model) === index)
71
- return <details className="bp-card" open={record.status !== 'available'}>
72
- <summary>配置并启用此 Bot</summary>
73
- <form className="bp-form" onSubmit={save}>
74
- <label>准确模型<select aria-label={`${bot.name} 的绑定模型`} value={form.model} onChange={event => { change('model', event.target.value); change('officialTools', []) }} required>
75
- <option value="" disabled>请选择</option>{record.catalog.models.map(item => <option key={route(item)} value={route(item)}>{route(item)}</option>)}
76
- </select></label>
77
- {model && account && !account.enabled && <aside className="bp-blocked"><strong>资金范围尚未启用</strong><p>{account.provider} 需要本地预算上限;它不是厂商余额。</p>
78
- <label>账户每日上限(USD)<input type="number" min="0" step="0.01" value={form.accountDaily} onChange={event => change('accountDaily', event.target.value)} /></label>
79
- <label>账户每月上限(USD)<input type="number" min="0" step="0.01" value={form.accountMonthly} onChange={event => change('accountMonthly', event.target.value)} /></label>
80
- <button type="button" disabled={busy} onClick={enableAccount}>启用 {account.provider} 资金范围</button></aside>}
81
- <label>工作区<select aria-label={`${bot.name} 的绑定工作区`} value={form.workspace} onChange={event => change('workspace', event.target.value)} required>
82
- <option value="" disabled>请选择</option>{record.catalog.machines.flatMap(machine => machine.roots.map(root => <option key={`${machine.machineId}/${root.rootId}`} value={`${machine.machineId}/${root.rootId}`}>{machine.label} / {root.label}</option>))}
83
- </select></label>
84
- <label>工作区权限<select value={form.workspaceMode} onChange={event => change('workspaceMode', event.target.value)}><option value="read-only">只读</option><option value="workspace-write">允许写入</option></select></label>
85
- <fieldset><legend>Bot 路由策略</legend>
86
- <p className="bp-muted">这个 Bot 派活给别人时用哪套取舍;留空表示沿用全局预设。</p>
87
- <label>预设<select aria-label={`${bot.name} 的路由预设`} value={routing.preset} onChange={event => change('routing', { ...routing, preset: event.target.value })}>
88
- <option value="">沿用全局</option><option value="economy">省钱优先</option><option value="balanced">均衡</option><option value="quality">效果优先</option>
89
- </select></label>
90
- <label>每日预算(USD)<input type="number" min="0" step="0.01" value={routing.dailyBudgetUsd} onChange={event => change('routing', { ...routing, dailyBudgetUsd: event.target.value })} placeholder="留空表示不单独限制" /></label>
91
- <label>每月预算(USD)<input type="number" min="0" step="0.01" value={routing.monthlyBudgetUsd} onChange={event => change('routing', { ...routing, monthlyBudgetUsd: event.target.value })} placeholder="留空表示不单独限制" /></label>
92
- </fieldset>
93
- <fieldset><legend>路由与任务能力</legend>{record.catalog.capabilities.map(capability => <label key={capability.id}><input type="checkbox" checked={form.capabilities.includes(capability.id)} onChange={event => change('capabilities', event.target.checked ? [...form.capabilities, capability.id] : form.capabilities.filter(id => id !== capability.id))} />{capability.label}</label>)}</fieldset>
94
- {cliMappings.length > 0 && <fieldset><legend>已核验官方 CLI</legend>{cliMappings.map(mapping => {
95
- const selected = form.officialTools.some(tool => tool.toolId === mapping.toolId && tool.cliModel === mapping.model)
96
- return <label key={`${mapping.toolId}/${mapping.model}`}><input type="checkbox" checked={selected} onChange={event => change('officialTools', event.target.checked
97
- ? [...form.officialTools, { toolId: mapping.toolId, cliModel: mapping.model, mode: 'read-only' }]
98
- : form.officialTools.filter(tool => !(tool.toolId === mapping.toolId && tool.cliModel === mapping.model)))} />{mapping.toolId} / {mapping.model}(只读)</label>
99
- })}</fieldset>}
100
- <label>资金渠道<select value={form.fundingMode} onChange={event => change('fundingMode', event.target.value)}><option value="api">API(金额)</option><option value="subscription">订阅(调用次数)</option></select></label>
101
- {form.fundingMode === 'api' ? <>
102
- <label>输入价格(USD / 百万 Token)<input type="number" min="0" step="0.000001" value={form.input} onChange={event => change('input', event.target.value)} required /></label>
103
- <label>输出价格(USD / 百万 Token)<input type="number" min="0" step="0.000001" value={form.output} onChange={event => change('output', event.target.value)} required /></label>
104
- <label>最大输入 Token<input type="number" min="1" step="1" value={form.maxInputTokens} onChange={event => change('maxInputTokens', event.target.value)} required /></label>
105
- <label>最大输出 Token<input type="number" min="1" step="1" value={form.maxOutputTokens} onChange={event => change('maxOutputTokens', event.target.value)} required /></label>
106
- <label>Bot 每日预算(USD)<input type="number" min="0" step="0.01" value={form.daily} onChange={event => change('daily', event.target.value)} required /></label>
107
- <label>Bot 每月预算(USD)<input type="number" min="0" step="0.01" value={form.monthly} onChange={event => change('monthly', event.target.value)} required /></label>
108
- </> : <>
109
- <label>每日订阅调用次数<input type="number" min="0" step="1" value={form.dailyCalls} onChange={event => change('dailyCalls', event.target.value)} required /></label>
110
- <label>每月订阅调用次数<input type="number" min="0" step="1" value={form.monthlyCalls} onChange={event => change('monthlyCalls', event.target.value)} required /></label>
111
- </>}
112
- <label>最大并发任务<input type="number" min="1" max="32" step="1" value={form.maxConcurrentTasks} onChange={event => change('maxConcurrentTasks', event.target.value)} required /></label>
113
- <label><input type="checkbox" checked={form.enabled} onChange={event => change('enabled', event.target.checked)} />启用此绑定(模型、工作区和资金范围必须完整)</label>
114
- <button type="submit" disabled={busy || form.enabled && (!model || !account?.enabled || !selectedMachine || !selectedRoot)}>保存绑定</button>
115
- {record.blockedReason && <Blocked value={record} />}
150
+
151
+ const fundingMode = FUNDING_MODES.find(item => item.value === form.fundingMode) ?? FUNDING_MODES[0]
152
+ const summary = [
153
+ ['准确模型', savedRoute || '未选择'],
154
+ ['资金范围', mainRoutes ? `${mainRoutes.routes.filter(route => route.enabled).length} 条路线 · 共享限额` : account ? `${account.provider} · ${form.fundingMode === 'api' ? 'API' : '订阅'}${account.enabled ? '' : '(未启用)'}` : '未匹配'],
155
+ ['工作区', machine && root ? workspaceLabel(machine, root) : '未选择'],
156
+ ['官方工具', form.officialTools.length ? form.officialTools.map(tool => `${tool.toolId}/${tool.cliModel}·${MODE_LABEL[tool.mode] ?? tool.mode}`).join(',') : bot?.main ? 'Main 全权限 · 待配置默认执行方式' : '未授予'],
157
+ ['绑定状态', record.status === 'available' ? '已启用' : '未启用'],
158
+ ]
159
+
160
+ return <div className="bb-root">
161
+ <header className="bb-head">
162
+ <p className="bb-eyebrow">Bot 绑定 · {bot?.name ?? ''}</p>
163
+ <h3 className="bb-title">模型与工具</h3>
164
+ <p className="bb-sub">{bot?.main ? '配置 Main 的模型路线、默认模型与官方工具。' : '选择默认模型,为这个 Bot 添加官方工具。'}</p>
165
+ {bot?.main && <p className="bb-note" role="note"><strong>Main · 全权限主控</strong> 可使用宿主全部已接入模型路线与工具。下方配置默认模型、工作区和资金。</p>}
166
+ <details className="bb-overview"><summary>配置概览<span>{form.enabled ? '已启用' : '未启用'}</span></summary><dl className="bb-summary">{summary.map(([label, value]) => <div key={label}><dt>{label}</dt><dd>{value}</dd></div>)}</dl></details>
167
+ </header>
168
+ {record.status !== 'available' && record.settings?.enabled && <Blocked value={{ reason: record.blockedReason ?? '此 Bot 的执行绑定尚未就绪。', nextAction: record.nextAction }} />}
169
+ <form className="bp-form bb-form" onSubmit={save}>
170
+ <section className="bb-model-card" aria-label="Bot 模型设置">
171
+ <BotModelPicker models={mainRoutes ? (catalog?.models ?? []).filter(item => mainRoutes.routes.some(route => route.enabled && routeOf(route) === routeOf(item))) : catalog?.models ?? []} value={form.model} disabled={Boolean(busy || working)}
172
+ note={form.enabled ? '保存后用于当前 Bot。' : '此 Bot 的绑定已停用。保存会保留选择,启用后用于对话。'}
173
+ onSelect={choice => {
174
+ const selected = findModel(catalog, routeOf(choice)), modes = selected?.fundingModes
175
+ edit({ provider: choice.provider, model: routeOf(choice), ...(modes && !modes.includes(form.fundingMode) ? { fundingMode: modes[0] } : {}) })
176
+ }}/>
177
+ <p className="bb-model-applied" data-model-different={hostModel.different ? '' : undefined} title={hostModel.title} role="status">{hostModel.appliedText}</p>
178
+ {bot?.main && !mainRoutes && <button type="button" className="bb-link" disabled={Boolean(busy || working)}
179
+ onClick={() => edit({ mainRoutes: startMainRoutes(form, model, account, record.settings?.funding) })}>配置多条模型路线</button>}
180
+ </section>
181
+ {mainRoutes ? <MainRouteEditor draft={mainRoutes} defaultRoute={form.model} catalog={catalog} accounts={accounts} busy={Boolean(busy || working)}
182
+ onChange={draft => edit({ mainRoutes: draft })}
183
+ onDefault={route => edit({ provider: route.provider, model: routeOf(route) })}
184
+ onEnableAccount={enableAccount}
185
+ onSingleRoute={() => edit({ ...mainDefaultFields(mainRoutes, form.model), mainRoutes: null })} /> :
186
+ <details className="bb-budget-config"><summary><UiIcon name="chart" size={16}/>费用与预算<span className="bb-pill">{account?.provider ?? '待配置'}</span></summary>
187
+ <ol className="bb-steps">
188
+ <li className="bb-step" data-state={model && account?.enabled ? 'done' : 'blocked'}>
189
+ <div className="bb-step-head"><UiIcon name="chart" size={17}/>
190
+ <div><strong>资金范围</strong><p>{fundingMode.help}</p></div>
191
+ <span className="bb-step-state"><span className="bb-pill" data-tone={account?.enabled ? 'current' : 'warn'}>{account ? `${account.provider}${account.enabled ? '' : ' · 未启用'}` : '无匹配范围'}</span></span></div>
192
+ <div className="bb-step-body">
193
+ <div className="bb-row">{FUNDING_MODES.map(item => <button key={item.value} type="button" className="bb-option bb-option-inline"
194
+ aria-pressed={form.fundingMode === item.value} disabled={Boolean(model?.fundingModes && !model.fundingModes.includes(item.value))} onClick={() => edit({ fundingMode: item.value })}>{item.label}</button>)}</div>
195
+ {!model && <p className="bb-note">先选择模型,宿主才能给出这个提供商的资金范围。</p>}
196
+ {model && !account && <p className="bb-warn">宿主目录里没有 {model.provider} 的资金范围。请刷新宿主或改选模型;未匹配范围时无法启用绑定。</p>}
197
+ {model && account && !account.enabled && <aside className="bp-blocked">
198
+ <strong>资金范围尚未启用</strong><p>{account.provider} 需要本地预算上限;它不是厂商余额。</p>
199
+ <div className="bb-fields">
200
+ <label className="bb-field"><span>账户每日上限(USD)</span><input type="number" min="0" step="0.01" value={form.accountDaily} onChange={event => edit({ accountDaily: event.target.value })} /></label>
201
+ <label className="bb-field"><span>账户每月上限(USD)</span><input type="number" min="0" step="0.01" value={form.accountMonthly} onChange={event => edit({ accountMonthly: event.target.value })} /></label>
202
+ </div>
203
+ <div className="bb-row"><button type="button" disabled={busy || Boolean(working) || !accountLimitsReady} onClick={() => enableAccount(account, numberOrNull(form.accountDaily), numberOrNull(form.accountMonthly))}>
204
+ {working === 'funding' ? '启用中…' : `启用 ${account.provider} 资金范围`}</button>
205
+ {!accountLimitsReady && <small className="bb-note">留空表示未配置,宿主会拒绝启用;0 表示禁止付费。</small>}</div>
206
+ </aside>}
207
+ {model && account?.enabled && scope && !scopeAllows(scope, form.fundingMode, routeOf(model)) && <p className="bb-warn">宿主发布该资金范围时不包含当前渠道或准确模型,保存会被拒绝。</p>}
208
+ {form.fundingMode === 'api' ? <>
209
+ <div className="bb-fields">
210
+ <label className="bb-field"><span>输入价格(USD / 百万 Token)</span><input type="number" min="0" step="0.000001" value={form.input} onChange={event => edit({ input: event.target.value })} /></label>
211
+ <label className="bb-field"><span>输出价格(USD / 百万 Token)</span><input type="number" min="0" step="0.000001" value={form.output} onChange={event => edit({ output: event.target.value })} /></label>
212
+ <label className="bb-field"><span>最大输入 Token</span><input type="number" min="1" step="1" value={form.maxInputTokens} onChange={event => edit({ maxInputTokens: event.target.value })} /></label>
213
+ <label className="bb-field"><span>最大输出 Token</span><input type="number" min="1" step="1" value={form.maxOutputTokens} onChange={event => edit({ maxOutputTokens: event.target.value })} /></label>
214
+ <label className="bb-field"><span>Bot 每日预算(USD)</span><input type="number" min="0" step="0.01" value={form.daily} onChange={event => edit({ daily: event.target.value })} /></label>
215
+ <label className="bb-field"><span>Bot 每月预算(USD)</span><input type="number" min="0" step="0.01" value={form.monthly} onChange={event => edit({ monthly: event.target.value })} /></label>
216
+ </div>
217
+ <p className="bb-note">价格与请求上界由 Bot 绑定自己承担,即使别的 Bot 用同一个模型。留空的字段会按“未配置”提交,不会变成 0。</p>
218
+ </> : <>
219
+ <div className="bb-fields">
220
+ <label className="bb-field"><span>每日订阅调用次数</span><input type="number" min="0" step="1" value={form.dailyCalls} onChange={event => edit({ dailyCalls: event.target.value })} /></label>
221
+ <label className="bb-field"><span>每月订阅调用次数</span><input type="number" min="0" step="1" value={form.monthlyCalls} onChange={event => edit({ monthlyCalls: event.target.value })} /></label>
222
+ </div>
223
+ <p className="bb-note">订阅按次数计数;不会自动回退到 API 计价。</p>
224
+ </>}
225
+ </div>
226
+ </li>
227
+ </ol>
228
+ </details>}
229
+
230
+ <details className="bb-fold">
231
+ <summary><UiIcon name="folder" size={16} />工作区与能力<small>{bot?.main ? 'Main 全权限' : machine && root ? `${workspaceLabel(machine, root)} · ${form.workspaceMode === 'read-only' ? '只读' : '可写'} · ${form.capabilities.length} 项能力` : '未选择'}</small></summary>
232
+ <div className="bb-fold-body">
233
+ <label className="bb-field bb-field-wide"><span>工作区(机器 / 根目录)</span>
234
+ <select aria-label={`${bot?.name ?? ''} 的绑定工作区`} value={form.workspace} onChange={event => edit({ workspace: event.target.value })}>
235
+ <option value="">请选择</option>
236
+ {machineOptions.flatMap(entry => entry.roots.map(entry2 => <option key={workspaceKey(entry.machineId, entry2.rootId)} value={workspaceKey(entry.machineId, entry2.rootId)}>{entry.label} / {entry2.label}</option>))}
237
+ </select></label>
238
+ {machine && root && <label className="bb-field bb-field-wide"><span>工作区权限</span>
239
+ <select aria-label={`${bot?.name ?? ''} 的工作区权限`} value={form.workspaceMode} onChange={event => edit({ workspaceMode: event.target.value })}>
240
+ {['read-only', 'workspace-write'].filter(mode => root.modes.includes(mode)).map(mode => <option key={mode} value={mode}>{mode === 'read-only' ? '只读' : '允许写入'}</option>)}
241
+ </select></label>}
242
+ {machine && root && !root.modes.includes(form.workspaceMode) && <p className="bb-warn">所选根目录已不支持当前权限模式,请重新选择。</p>}
243
+ {!bot?.main && <><div className="bb-row"><strong className="bb-note">路由与任务能力</strong>
244
+ <button type="button" className="bb-link" onClick={() => edit({ capabilities: (catalog?.capabilities ?? []).map(item => item.id) })}>全选</button>
245
+ <button type="button" className="bb-link" onClick={() => edit({ capabilities: [] })}>清空</button></div>
246
+ <p className="bb-note">留空表示不授予额外能力;此界面不会在打开时替你勾选。</p>
247
+ <div className="bb-list">{(catalog?.capabilities ?? []).map(item => <label key={item.id} className="bb-check">
248
+ <input type="checkbox" checked={form.capabilities.includes(item.id)} onChange={event => edit({ capabilities: event.target.checked ? [...form.capabilities, item.id] : form.capabilities.filter(id => id !== item.id) })} />
249
+ <span>{item.label}<small>{item.tools.join('、') || '无工具'}</small></span></label>)}</div></>}
250
+ <label className="bb-field bb-field-wide"><span>最大并发任务</span><input type="number" min="1" max="32" step="1" value={form.maxConcurrentTasks} onChange={event => edit({ maxConcurrentTasks: event.target.value })} /></label>
251
+ <fieldset className="bb-fieldset">
252
+ <legend className="bb-note">Bot 路由策略(这个 Bot 派活给别人时用哪套取舍)</legend>
253
+ <label className="bb-field bb-field-wide"><span>预设</span>
254
+ <select aria-label={`${bot?.name ?? ''} 的路由预设`} value={form.routing.preset} onChange={event => edit({ routing: { ...form.routing, preset: event.target.value } })}>
255
+ {ROUTING_PRESETS.map(item => <option key={item.value || 'global'} value={item.value}>{item.label}</option>)}</select></label>
256
+ <div className="bb-fields">
257
+ <label className="bb-field"><span>每日预算(USD)</span><input type="number" min="0" step="0.01" value={form.routing.dailyBudgetUsd} onChange={event => edit({ routing: { ...form.routing, dailyBudgetUsd: event.target.value } })} placeholder="留空表示不单独限制" /></label>
258
+ <label className="bb-field"><span>每月预算(USD)</span><input type="number" min="0" step="0.01" value={form.routing.monthlyBudgetUsd} onChange={event => edit({ routing: { ...form.routing, monthlyBudgetUsd: event.target.value } })} placeholder="留空表示不单独限制" /></label>
259
+ </div>
260
+ </fieldset>
261
+ </div>
262
+ </details>
263
+
264
+ <section className="bb-tools" aria-label={bot?.main ? '官方工具(默认执行配置)' : '官方工具(额外执行授权)'}>
265
+ <div className="bb-tools-head">
266
+ <strong><UiIcon name="settings" size={15} />官方工具</strong>
267
+ {typeof onOpenTools === 'function' && <button type="button" className="bb-link" onClick={() => {
268
+ const token = saveToken.current
269
+ Promise.resolve().then(onOpenTools).catch(() => { if (saveToken.current === token) setFeedback({ kind: 'error', text: '暂时无法打开官方工具设置,请重试。' }) })
270
+ }}>安装与登录<UiIcon name="arrow" size={13}/></button>}
271
+ <p>{bot?.main ? 'Main 已有全部工具权限;下方配置已核验的 CLI 模型与执行方式。' : '为这个 Bot 添加官方工具,选择允许使用的模型与执行方式。'}</p>
272
+ </div>
273
+ {tools.length === 0 ? <p className="bb-tool-empty">宿主目录里没有可授予的官方 CLI 工具。安装与登录状态在官方工具面板查看;未核验的身份不会在这里显示为可用。</p> : (
274
+ <ul className="bb-tool-list">{tools.map(tool => <li key={tool.toolId} className="bb-tool" data-granted={Boolean(tool.grant)} data-stale={tool.stale}>
275
+ <div className="bb-tool-head"><div className="bb-tool-title">
276
+ <strong>{tool.label}</strong>
277
+ <p>{tool.vendor ? `${tool.vendor} · ` : ''}{tool.source || '厂商官方 CLI'}</p></div>
278
+ <span className="bb-pill" data-tone={tool.grant ? (tool.stale ? 'warn' : 'ok') : 'muted'}>{bot?.main ? (tool.stale ? '配置待核验' : tool.grant ? '执行已配置' : '全权限 · 待配置') : tool.grant ? (tool.stale ? '授权已失效' : '已授予') : '未授予'}</span></div>
279
+ <div className="bb-tool-body">
280
+ {tool.grantTools.length > 0 && <details className="bb-tool-permissions"><summary>工具权限明细</summary><p className="bb-note">{tool.grantTools.join('、')}</p></details>}
281
+ {tool.identities.length === 0 ? <p className="bb-note">当前模型没有已核验的 {tool.toolId} CLI 身份,{bot?.main ? '需要先完成执行配置' : '宿主不会接受授予'}。请改选模型,或先在官方工具面板安装并核验该 CLI。</p> : (
282
+ <div className="bb-list">{tool.identities.map(identity => {
283
+ const checked = tool.grant?.cliModel === identity.model
284
+ return <label key={`${identity.toolId}/${identity.model}`} className="bb-check">
285
+ <input type="checkbox" checked={checked} onChange={event => setToolGrant(tool, identity.model, event.target.checked)} />
286
+ <span>{identity.toolId} / {identity.model}<small>宿主已核验的 CLI 模型名</small></span></label>
287
+ })}</div>)}
288
+ {tool.grant && <label className="bb-field bb-field-wide"><span>执行模式</span>
289
+ <select aria-label={`${tool.label} 的执行模式`} value={tool.grant.mode} onChange={event => setToolMode(tool.toolId, event.target.value)}>
290
+ {EXECUTION_MODES.filter(mode => tool.modes.includes(mode)).map(mode => <option key={mode} value={mode}>{MODE_LABEL[mode]}</option>)}</select>
291
+ <small>{MODE_HELP[tool.grant.mode] ?? ''}</small></label>}
292
+ {tool.grant && tool.stale && <p className="bb-warn">这个授权指向的 CLI 模型与当前模型尚未核验,宿主会拒绝保存。请移除,或改选已核验的身份。</p>}
293
+ </div></li>)}</ul>)}
294
+ <p className="bb-tools-foot">{bot?.main ? 'Main 的全权限不受普通 Bot 能力勾选限制。工具仍需安装、登录并核验 CLI 身份;这里保存默认执行配置。' : '每个工具只能保留一个已核验 CLI 身份;换模型不会自动移除或改指向旧授权。写入与 CLI 自身权限会扩大这个 Bot 的执行范围,请按需要选择。'}</p>
295
+ {staleTools.length > 0 && <div className="bb-row"><button type="button" className="bb-link" onClick={dropStaleTools}>移除 {staleTools.length} 项未核验授权</button></div>}
296
+ </section>
297
+
298
+ <div className="bb-save">
299
+ <label className="bb-check"><input type="checkbox" checked={form.enabled} onChange={event => edit({ enabled: event.target.checked })} />
300
+ <span>启用此绑定<small>模型、工作区、资金范围与必要预算必须完整;未启用时保持停用,不授予执行权。</small></span></label>
301
+ {problems.length > 0 && <div className="bb-danger"><strong>还不能保存</strong>
302
+ <ul>{problems.map(item => <li key={item}>{item}</li>)}</ul></div>}
303
+ <div className="bb-save-row">
304
+ <span className="bb-pill" data-tone={dirty ? 'warn' : 'muted'}>{dirty ? '有未保存修改' : '与宿主一致'}</span>
305
+ <span className="bb-pill">修订号 {record.revision ?? 0}</span>
306
+ <button type="submit" className="bp-button-primary" disabled={busy || Boolean(working) || problems.length > 0}>
307
+ {working === 'binding' ? '保存中…' : '保存绑定'}</button>
308
+ </div>
309
+ {feedback && <p className="bb-feedback" data-kind={feedback.kind} role="status">{feedback.text}</p>}
310
+ {dirty && seedRevision.current !== record.revision && <button type="button" className="bb-link" onClick={() => {
311
+ saveToken.current++; workingRef.current = ''; setWorking(''); seedRevision.current = record.revision
312
+ setForm(seedForm(record, accounts)); setDirty(false); setFeedback(null)
313
+ }}>放弃草稿并载入最新设置</button>}
314
+ </div>
116
315
  </form>
117
- </details>
316
+ </div>
118
317
  }