@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
@@ -0,0 +1,224 @@
1
+ const failure = (code, message = code) => Object.assign(new Error(message), { code })
2
+ const BOT = /^[-A-Za-z0-9_.]{1,128}$/
3
+ const HOST = /^host-[0-9a-f]{16}$/
4
+ const KEY = /^[-A-Za-z0-9_]{16,128}$/
5
+ const STATES = new Set(['accepted', 'running', 'succeeded', 'unknown', 'refused', 'cancelled'])
6
+ const withoutProgress = status => {
7
+ if (!status) return status
8
+ const { progress, ...stored } = status
9
+ return stored
10
+ }
11
+ export const independentChatStorageKey = (hostId, botId) => `bot-product:independent-chat:1:${hostId}:${botId}`
12
+ export const independentChatNonce = () => {
13
+ if (typeof globalThis.crypto?.randomUUID !== 'function') throw failure('CHAT_NONCE_UNAVAILABLE', '浏览器无法生成安全请求编号。')
14
+ return globalThis.crypto.randomUUID()
15
+ }
16
+ export function validateChatRequest(value, botId, requestKey) {
17
+ if (value?.schemaVersion !== 1 || value.botId !== botId || !KEY.test(value.requestKey ?? '') || (requestKey && value.requestKey !== requestKey)
18
+ || !STATES.has(value.state) || typeof value.pending !== 'boolean' || typeof value.finished !== 'boolean' || value.stopConfirmed !== false) throw failure('CHAT_IDENTITY_MISMATCH', '宿主返回的请求身份或状态无效,请保留原编号核对。')
19
+ const progress = value.progress
20
+ if (value.operationKind !== undefined && !['send', 'new-conversation'].includes(value.operationKind)) throw failure('CHAT_IDENTITY_MISMATCH')
21
+ if (progress != null && (value.operationKind === 'new-conversation' || !value.pending || !['accepted', 'running'].includes(value.state) || !Number.isSafeInteger(progress.step) || progress.step < 1 || progress.step > 64
22
+ || !Number.isSafeInteger(progress.sequence) || progress.sequence < 1 || typeof progress.text !== 'string' || progress.text.length > 64000 || typeof progress.truncated !== 'boolean')) throw failure('CHAT_PROGRESS_INVALID', '宿主返回的生成预览格式无效。')
23
+ return value
24
+ }
25
+ export function validateChatPage(value, botId) {
26
+ if (value?.schemaVersion !== 1 || value.botId !== botId || !Array.isArray(value.items) || value.items.length > 100
27
+ || (value.nextCursor !== null && typeof value.nextCursor !== 'string')
28
+ || value.items.some(row => !row || typeof row.messageId !== 'string' || !row.messageId || typeof row.content !== 'string'
29
+ || !['user', 'assistant', 'tool', 'system'].includes(row.role) || row.contextId !== undefined && (typeof row.contextId !== 'string' || !row.contextId || row.contextId.length > 128))
30
+ || new Set(value.items.map(row => row.messageId)).size !== value.items.length) throw failure('CHAT_PAGE_INVALID', '会话页缺少稳定消息编号,已拒绝显示可能错位的内容。')
31
+ return value
32
+ }
33
+
34
+ /** One mounted, validated Host/Bot. This metadata never authenticates an owner.
35
+ * A persisted uncertain key is inspected only. send() is reachable solely from
36
+ * the owner's explicit submit, after its nonce and exact text are durable.
37
+ */
38
+ export function createIndependentChatController({ api, hostId, botId, storage = globalThis.localStorage, pollMs = 1500, timeoutMs = 8000,
39
+ nonce = independentChatNonce, onHostChanged } = {}) {
40
+ if (!HOST.test(hostId ?? '') || !BOT.test(botId ?? '') || typeof api?.dispatch !== 'function') throw failure('CHAT_SCOPE_INVALID')
41
+ const key = independentChatStorageKey(hostId, botId), listeners = new Set(), calls = new Set()
42
+ let live = false, retired = false, timer, polling = false, lock = false, pageLock = false, revision = 0, pageLoaded = false, exhausted = false
43
+ let nextNonce = nonce()
44
+ if (!KEY.test(nextNonce ?? '')) throw failure('CHAT_NONCE_INVALID')
45
+ let state = { hostId, botId, draft: '', pending: null, lastRequest: null, view: null, items: [], nextCursor: null,
46
+ loading: true, sending: false, loadingOlder: false, error: '', storageError: '', acknowledgedRequestKey: null }
47
+ const publish = patch => { if (retired) return; state = { ...state, ...patch }; for (const listener of listeners) listener() }
48
+ const persist = patch => {
49
+ const next = { ...state, ...patch }
50
+ try {
51
+ const pending = next.pending ? { ...next.pending, status: withoutProgress(next.pending.status) } : null
52
+ storage.setItem(key, JSON.stringify({ schemaVersion: 1, hostId, botId, draft: next.draft, pending, acknowledgedRequestKey: next.acknowledgedRequestKey }))
53
+ publish({ ...patch, storageError: '' }); return true
54
+ } catch { publish({ storageError: '无法保存本机草稿与请求编号,请恢复浏览器存储后再发送。' }); return false }
55
+ }
56
+ try {
57
+ const saved = JSON.parse(storage.getItem(key) ?? 'null')
58
+ if (saved?.schemaVersion === 1 && saved.hostId === hostId && saved.botId === botId) {
59
+ const pending = saved.pending && KEY.test(saved.pending.requestKey ?? '') && typeof saved.pending.input === 'string' && saved.pending.input.length <= 64000
60
+ ? { ...saved.pending, status: withoutProgress(saved.pending.status) } : null
61
+ state = { ...state, draft: typeof saved.draft === 'string' ? saved.draft.slice(0, 64000) : '', pending, acknowledgedRequestKey: KEY.test(saved.acknowledgedRequestKey ?? '') ? saved.acknowledgedRequestKey : null }
62
+ }
63
+ } catch { state.storageError = '无法读取本机草稿与请求编号;请修复浏览器存储后再发送。' }
64
+ const call = async (action, payload) => {
65
+ const controller = new AbortController(); calls.add(controller)
66
+ let alarm, abort
67
+ const stopped = new Promise((_, reject) => {
68
+ abort = () => reject(failure('CHAT_TRANSPORT_UNKNOWN', '连接中断,发送结果尚未确认;只核对原请求,不会自动重发。'))
69
+ controller.signal.addEventListener('abort', abort, { once: true })
70
+ alarm = setTimeout(() => controller.abort(), timeoutMs)
71
+ })
72
+ try { return await Promise.race([Promise.resolve().then(() => api.dispatch(action, payload, controller.signal)), stopped]) }
73
+ finally { clearTimeout(alarm); calls.delete(controller); controller.signal.removeEventListener('abort', abort) }
74
+ }
75
+ const assertHost = async () => {
76
+ const namespace = await call('ui.namespace', {})
77
+ if (!live) throw failure('CHAT_SCOPE_RETIRED')
78
+ if (namespace?.schemaVersion !== 1 || !HOST.test(namespace.hostId ?? '')) throw failure('CHAT_SCOPE_INVALID', '宿主没有确认界面存储标识。')
79
+ if (namespace.hostId !== hostId) {
80
+ publish({ view: null, error: '宿主连接已切换,正在重新读取联系人与草稿。' })
81
+ live = false; revision++; clearTimeout(timer)
82
+ for (const controller of calls) controller.abort()
83
+ onHostChanged?.()
84
+ throw failure('CHAT_SCOPE_RETIRED')
85
+ }
86
+ }
87
+ const applyRequest = (value, requestKey) => {
88
+ const request = validateChatRequest(value, botId, requestKey)
89
+ const held = state.pending
90
+ if (held && held.requestKey !== request.requestKey) return
91
+ const operationKind = request.operationKind ?? held?.operationKind ?? 'send'
92
+ if (held?.operationKind && held.operationKind !== operationKind) throw failure('CHAT_IDENTITY_MISMATCH')
93
+ const pending = request.state === 'succeeded' ? null : { requestKey: request.requestKey,
94
+ operationKind, input: held?.input ?? request.recoveryInput ?? '', accepted: request.durableInput === true, status: request }
95
+ const clearDraft = operationKind === 'send' && request.durableInput === true && held && state.draft === held.input
96
+ persist({ pending, ...(clearDraft ? { draft: '' } : {}), lastRequest: request,
97
+ ...(request.state === 'succeeded' ? { acknowledgedRequestKey: request.requestKey } : {}) })
98
+ if (request.state === 'succeeded') nextNonce = nonce()
99
+ }
100
+ const loadMessages = async (cursor, older = false) => {
101
+ const page = validateChatPage(await call('independentChat.messages', { botId, limit: 40, ...(cursor ? { cursor } : {}) }), botId)
102
+ if (!live) return
103
+ const seen = new Set()
104
+ const combined = older ? [...page.items, ...state.items] : [...state.items.filter(row => !page.items.some(next => next.messageId === row.messageId)), ...page.items]
105
+ publish({ items: combined.filter(row => { if (seen.has(row.messageId)) return false; seen.add(row.messageId); return true }),
106
+ ...((older || !pageLoaded) ? { nextCursor: exhausted ? null : page.nextCursor } : {}) })
107
+ pageLoaded = true
108
+ if (older && page.nextCursor === null) exhausted = true
109
+ }
110
+ const refresh = async () => {
111
+ if (!live || polling || lock) return
112
+ polling = true
113
+ const mine = revision
114
+ try {
115
+ await assertHost()
116
+ if (!live || mine !== revision) return
117
+ const view = await call('independentChat.view', { botId })
118
+ if (!live || mine !== revision) return
119
+ if (view?.schemaVersion !== 1 || view.botId !== botId || !['independent', 'integration', null].includes(view.mode) || typeof view.available !== 'boolean') throw failure('CHAT_VIEW_INVALID', '宿主聊天状态无效。')
120
+ publish({ view, error: '' })
121
+ if (state.pending) {
122
+ const requestKey = state.pending.requestKey
123
+ try {
124
+ const value = await call('independentChat.inspect', { botId, requestKey })
125
+ if (!live || mine !== revision) return
126
+ applyRequest(value, requestKey)
127
+ } catch (error) {
128
+ if (live && mine === revision && error?.code === 'INDEPENDENT_CHAT_REQUEST_NOT_FOUND' && state.pending?.requestKey === requestKey) persist({ pending: { ...state.pending, notFound: true } })
129
+ throw error
130
+ }
131
+ } else if (view.pendingRequest || (view.latestRequest && view.latestRequest.requestKey !== state.acknowledgedRequestKey)) {
132
+ applyRequest(view.pendingRequest ?? view.latestRequest)
133
+ }
134
+ if (live && mine === revision) await loadMessages()
135
+ if (live && mine === revision) publish({ loading: false })
136
+ } catch (error) { if (live && mine === revision) publish({ loading: false, view: null, error: String(error?.message ?? error) }) }
137
+ finally {
138
+ polling = false
139
+ if (live) { clearTimeout(timer); timer = setTimeout(refresh, pollMs) }
140
+ }
141
+ }
142
+ return Object.freeze({
143
+ subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
144
+ getSnapshot: () => state,
145
+ start() { if (retired || live) return; live = true; void refresh() },
146
+ dispose() { live = false; retired = true; revision++; clearTimeout(timer); for (const controller of calls) controller.abort(); listeners.clear() },
147
+ setDraft(text) { if (!retired && typeof text === 'string') persist({ draft: text.slice(0, 64000) }) },
148
+ async send() {
149
+ if (!live || lock || state.pending || !state.view?.available || !state.draft.trim()) return
150
+ if (new TextEncoder().encode(state.draft).length > 64000) { publish({ error: '消息超过 64,000 字节,请缩短后发送。' }); return }
151
+ lock = true; revision++
152
+ let requestKey
153
+ try {
154
+ await assertHost()
155
+ if (!live) return
156
+ requestKey = nextNonce
157
+ if (!KEY.test(requestKey)) throw failure('CHAT_NONCE_INVALID')
158
+ const pending = { requestKey, input: state.draft, accepted: false, status: null }
159
+ // Must complete before the first network send. Double click and Enter
160
+ // share this synchronous lock and this one durable nonce.
161
+ if (!persist({ pending, sending: true, error: '' })) return
162
+ const value = await call('independentChat.send', { botId, requestKey, input: pending.input })
163
+ if (live) applyRequest(value, requestKey)
164
+ } catch (error) { if (live) publish({ error: String(error?.message ?? error) }) }
165
+ finally { lock = false; if (live) { publish({ sending: false }); clearTimeout(timer); timer = setTimeout(refresh, 0) } }
166
+ },
167
+ async cancel() {
168
+ if (!live || lock || !state.pending) return
169
+ lock = true; revision++
170
+ const requestKey = state.pending.requestKey
171
+ try { await assertHost(); if (!live) return; const value = await call('independentChat.cancel', { botId, requestKey }); if (live) applyRequest(value, requestKey) }
172
+ catch (error) { if (live) publish({ error: String(error?.message ?? error) }) }
173
+ finally { lock = false; if (live) { clearTimeout(timer); timer = setTimeout(refresh, 0) } }
174
+ },
175
+ async newConversation() {
176
+ if (!live || lock || state.pending || !state.view?.newConversationAvailable || state.storageError) return
177
+ lock = true; revision++
178
+ try {
179
+ await assertHost()
180
+ if (!live) return
181
+ const requestKey = nextNonce
182
+ if (!KEY.test(requestKey)) throw failure('CHAT_NONCE_INVALID')
183
+ if (!persist({ pending: { requestKey, input: '', operationKind: 'new-conversation', accepted: false, status: null }, sending: true, error: '' })) return
184
+ const value = await call('independentChat.newConversation', { botId, requestKey })
185
+ if (live) applyRequest(value, requestKey)
186
+ } catch (error) { if (live) publish({ error: String(error?.message ?? error) }) }
187
+ finally { lock = false; if (live) { publish({ sending: false }); clearTimeout(timer); timer = setTimeout(refresh, 0) } }
188
+ },
189
+ inspect() { clearTimeout(timer); void refresh() },
190
+ restore() {
191
+ const held = state.pending
192
+ if (!live || lock || !held || held.status?.pending !== false || held.status?.finished !== true) return
193
+ if (held.operationKind === 'new-conversation') return false
194
+ const recovered = held.status.recoveryInput ?? held.input
195
+ if (state.draft && state.draft !== recovered) {
196
+ publish({ error: '输入框已有未发送文字,请先复制或清空,再恢复原消息。' })
197
+ return false
198
+ }
199
+ // Restore is a human choice. It only fills the composer. The next send
200
+ // is a new explicit intent, after the old outcome is shown to the owner.
201
+ persist({ draft: recovered, pending: null, lastRequest: held.status, acknowledgedRequestKey: held.requestKey })
202
+ nextNonce = nonce()
203
+ },
204
+ releaseNotFound() {
205
+ // An unacknowledged transport request can be explicitly released only
206
+ // after the authoritative journal answers not-found, never on a timeout.
207
+ if (state.pending?.notFound === true) {
208
+ if (state.pending.operationKind === 'new-conversation') return false
209
+ if (state.draft && state.draft !== state.pending.input) {
210
+ publish({ error: '输入框已有未发送文字,请先复制或清空,再恢复原消息。' })
211
+ return false
212
+ }
213
+ persist({ draft: state.pending.input, pending: null, acknowledgedRequestKey: state.pending.requestKey }); nextNonce = nonce()
214
+ }
215
+ },
216
+ async older() {
217
+ if (!live || pageLock || !state.nextCursor) return
218
+ pageLock = true; publish({ loadingOlder: true })
219
+ try { await loadMessages(state.nextCursor, true) } catch (error) { if (live) publish({ error: String(error?.message ?? error) }) }
220
+ finally { pageLock = false; if (live) publish({ loadingOlder: false }) }
221
+ },
222
+ reloadMessages() { pageLoaded = false; exhausted = false; publish({ items: [], nextCursor: null }); clearTimeout(timer); void refresh() },
223
+ })
224
+ }
@@ -0,0 +1,61 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+ import { UiIcon } from './ui-icons.jsx'
3
+ import { openIndependentChat } from './independent-chat-navigation.mjs'
4
+
5
+ let nextScope = 0
6
+ export function readWithDeadline(work, controller, ms = 8000) {
7
+ let timer, abort
8
+ const expired = new Promise((_, reject) => {
9
+ abort = () => reject(new Error('当前宿主没有及时回答,请重试连接。'))
10
+ controller.signal.addEventListener('abort', abort, { once: true })
11
+ timer = setTimeout(() => controller.abort(), ms)
12
+ })
13
+ return Promise.race([Promise.resolve().then(work), expired]).finally(() => { clearTimeout(timer); controller.signal.removeEventListener('abort', abort) })
14
+ }
15
+
16
+ /** Native navigation remains usable without loading the independent transcript renderer. */
17
+ export function IndependentChatEntry({ productApi, botId, onOpen, openPanel }) {
18
+ const scope = useMemo(() => ++nextScope, [productApi, botId])
19
+ return <ChatEntry key={scope} productApi={productApi} botId={botId} onOpen={onOpen} openPanel={openPanel}/>
20
+ }
21
+ function ChatEntry({ productApi, botId, onOpen, openPanel }) {
22
+ const [independent, setIndependent] = useState(false)
23
+ useEffect(() => {
24
+ if (!botId || !productApi?.dispatch) return
25
+ let live = true, timer, controller
26
+ const load = async () => {
27
+ controller = new AbortController()
28
+ try { const view = await readWithDeadline(() => productApi.dispatch('independentChat.view', { botId }, controller.signal), controller); if (live) setIndependent(view?.botId === botId && view.mode === 'independent') }
29
+ catch { if (live) setIndependent(false) }
30
+ finally { if (live) timer = setTimeout(load, 4000) }
31
+ }
32
+ void load()
33
+ return () => { live = false; controller?.abort(); clearTimeout(timer) }
34
+ }, [productApi, botId])
35
+ if (!independent) return null
36
+ return <button className="ic-native-entry" type="button" title="此 Bot 已使用独立运行时,请在独立聊天页继续发送" onClick={() => typeof onOpen === 'function' ? onOpen(botId) : openIndependentChat(productApi, openPanel, botId)}><UiIcon name="bot" size={16}/><span>打开独立聊天</span><UiIcon name="arrow" size={14}/></button>
37
+ }
38
+
39
+ export function BotConversationLink({ productApi, bot, openSession, onOpenIndependent, available }) {
40
+ const [error, setError] = useState(''), [busy, setBusy] = useState(false)
41
+ const current = useRef({ live: false, epoch: 0, controller: null, pending: false })
42
+ useEffect(() => { const held = current.current; held.live = true; held.epoch++; held.pending = false; setError(''); setBusy(false); return () => { held.live = false; held.epoch++; held.controller?.abort() } }, [productApi, bot.id])
43
+ const open = async () => {
44
+ const held = current.current, epoch = held.epoch
45
+ if (held.pending || !held.live) return
46
+ held.pending = true
47
+ const controller = new AbortController(); held.controller = controller
48
+ setBusy(true); setError('')
49
+ try {
50
+ if (!available) { openSession?.(bot.sessionId); return }
51
+ const view = await readWithDeadline(() => productApi.dispatch('independentChat.view', { botId: bot.id }, controller.signal), controller)
52
+ if (!held.live || held.epoch !== epoch) return
53
+ if (view?.botId !== bot.id) throw new Error('宿主返回的 Bot 身份不匹配。')
54
+ if (view.mode === 'independent') onOpenIndependent?.(bot.id)
55
+ else if (view.mode === 'integration') openSession?.(bot.sessionId)
56
+ else throw new Error('宿主未确认当前运行时,请检查连接后重试。')
57
+ } catch (cause) { if (held.live && held.epoch === epoch) setError(String(cause?.message ?? cause)) }
58
+ finally { if (held.live && held.epoch === epoch) { held.pending = false; setBusy(false) } }
59
+ }
60
+ return <div><button className="bp-button-primary" type="button" disabled={busy} onClick={() => { void open() }}>{busy ? '正在打开…' : '打开私聊'}<UiIcon name="arrow" size={15}/></button>{error && <p className="bp-error" role="alert">{error}</p>}</div>
61
+ }
@@ -0,0 +1,21 @@
1
+ export const INDEPENDENT_CHAT_PANEL = 'bot-product-independentChat'
2
+ const scopes = new WeakMap()
3
+ export function independentChatNavigation(api) {
4
+ if (scopes.has(api)) return scopes.get(api)
5
+ let snapshot = null, version = 0
6
+ const listeners = new Set()
7
+ const navigation = Object.freeze({
8
+ subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
9
+ getSnapshot: () => snapshot,
10
+ select(botId) {
11
+ if (typeof botId !== 'string' || !/^[-A-Za-z0-9_.]{1,128}$/.test(botId)) throw new TypeError('CHAT_BOT_REQUIRED')
12
+ snapshot = Object.freeze({ botId, version: ++version })
13
+ for (const listener of listeners) listener()
14
+ },
15
+ })
16
+ scopes.set(api, navigation); return navigation
17
+ }
18
+ export function openIndependentChat(api, openPanel, botId) {
19
+ independentChatNavigation(api).select(botId)
20
+ return openPanel?.(INDEPENDENT_CHAT_PANEL)
21
+ }
@@ -0,0 +1,148 @@
1
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
2
+ import { ComposerDraftHost } from './composer-draft-host.jsx'
3
+ import { createIndependentChatController } from './independent-chat-controller.mjs'
4
+ import { independentChatNavigation } from './independent-chat-navigation.mjs'
5
+ import { readWithDeadline } from './independent-chat-entry.jsx'
6
+ export { IndependentChatEntry, BotConversationLink } from './independent-chat-entry.jsx'
7
+ import { BotSettingsDialog } from './bot-settings-dialog.jsx'
8
+ import { UiIcon } from './ui-icons.jsx'
9
+ import { botModelStatus } from './bot-model-status.mjs'
10
+ import { requestWorkbenchView } from '../../.dsh-plugin/client/workbench-state.mjs'
11
+ import { IndependentRichMessage } from './independent-chat-rich.jsx'
12
+ import { independentToolRelations } from './independent-chat-rich.mjs'
13
+ import { NativeActivityCardView, useNativeActivity } from './native-activity.jsx'
14
+
15
+ let nextScope = 0
16
+ export function IndependentChatPage(props) {
17
+ const scope = useMemo(() => ++nextScope, [props.productApi])
18
+ return <ChatDirectory key={scope} {...props}/>
19
+ }
20
+
21
+ function ChatDirectory({ productApi, openPanel, openSession }) {
22
+ const navigation = useMemo(() => independentChatNavigation(productApi), [productApi])
23
+ const route = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot, navigation.getSnapshot)
24
+ const [directory, setDirectory] = useState(null), [error, setError] = useState(''), [attempt, setAttempt] = useState(0)
25
+ const reconnect = useCallback(() => { setDirectory(null); setAttempt(value => value + 1) }, [])
26
+ useEffect(() => {
27
+ let live = true, timer, controller
28
+ const load = async () => {
29
+ controller = new AbortController()
30
+ try {
31
+ const snapshot = await readWithDeadline(() => productApi.snapshot(controller.signal), controller)
32
+ if (!live) return
33
+ if (snapshot?.schemaVersion !== 1 || !Array.isArray(snapshot.bots)) throw new Error('宿主未返回 Bot 联系人。')
34
+ setDirectory(snapshot); setError('')
35
+ } catch (cause) { if (live) setError(String(cause?.message ?? cause)) }
36
+ finally { if (live) timer = setTimeout(load, 4000) }
37
+ }
38
+ void load()
39
+ return () => { live = false; controller?.abort(); clearTimeout(timer) }
40
+ }, [productApi, attempt])
41
+ const bots = directory?.bots ?? [], selected = route?.botId ?? directory?.mainBotId ?? bots[0]?.id
42
+ const bot = bots.find(row => row.id === selected)
43
+ return <section className="ic-shell" aria-label="独立 Bot 聊天">
44
+ <aside className="ic-contacts"><div className="ic-contact-title"><UiIcon name="team" size={19}/><strong>Bot 伙伴</strong></div>
45
+ <nav aria-label="独立聊天联系人">{bots.map(row => <button key={row.id} type="button" aria-current={row.id === selected ? 'page' : undefined} onClick={() => navigation.select(row.id)}><span className="ic-avatar"><UiIcon name="bot" size={21}/></span><span><strong>{row.name || row.id}</strong><small>{row.role || '和你的 Bot 一起工作'}</small></span></button>)}</nav>
46
+ <button className="ic-team-link" type="button" onClick={() => openPanel?.('bot-product-bindings')}>管理团队<UiIcon name="chevron" size={14}/></button>
47
+ </aside>
48
+ <div className="ic-conversation">{error ? <div className="ic-alert" role="alert">无法读取当前宿主联系人:{error}<button type="button" onClick={() => { setDirectory(null); setAttempt(value => value + 1) }}>重试连接</button></div>
49
+ : !directory ? <p className="ic-placeholder" role="status">正在连接 Bot 伙伴…</p>
50
+ : !bot ? <p className="ic-placeholder" role="status">当前宿主中没有这个 Bot。请从联系人重新选择。</p>
51
+ : <ComposerDraftHost productApi={productApi} attempt={attempt}>{({ hostId, status, error: namespaceError }) => status === 'ready'
52
+ ? <ChatConversation key={`${hostId}:${bot.id}`} productApi={productApi} hostId={hostId} bot={bot} readOnly={directory.readOnly} nativeActivityAvailable={directory.availableActions?.includes('nativeActivity.list') === true} openSession={openSession} openPanel={openPanel} onHostChanged={reconnect}/>
53
+ : <div className="ic-placeholder" role="status">{status === 'error' ? <>{namespaceError}<button type="button" onClick={() => setAttempt(value => value + 1)}>重试连接</button></> : '正在确认当前宿主…'}</div>}</ComposerDraftHost>}
54
+ </div>
55
+ </section>
56
+ }
57
+
58
+ const requestText = request => {
59
+ if (request?.operationKind === 'new-conversation') {
60
+ if (request.state === 'succeeded') return '新对话已开启,旧记录已保留。'
61
+ if (['accepted', 'running'].includes(request.state)) return '正在开启新对话,核对旧会话的停止状态…'
62
+ return '新对话结果尚未确认,请核对原操作;旧聊天记录保留。'
63
+ }
64
+ if (!request) return '发送结果尚未确认。请核对原请求;不会自动重发。'
65
+ if (request.state === 'succeeded') return request.cancelRequested ? '回复已提交;之前已请求停止。' : '回复已提交。'
66
+ if (request.cancelRequested) return request.finished ? '本地回合已结算,已请求停止;工具停止尚未确认。' : '已请求停止,正在核对回合结果;工具停止尚未确认。'
67
+ if (request.state === 'accepted') return '消息已保存,等待 Bot 回复…'
68
+ if (request.state === 'running') return 'Bot 正在处理…'
69
+ if (request.state === 'unknown') return request.pending ? '原宿主的回合可能仍在运行,请核对原请求。' : '回合结果未知,请检查已有消息与工具记录后再决定是否恢复文字。'
70
+ if (request.state === 'refused') return '宿主拒绝了这次发送,可以恢复文字后修改。'
71
+ return '本地回合已结算,可以恢复文字;工具停止尚未确认。'
72
+ }
73
+
74
+ function ChatConversation({ productApi, hostId, bot, readOnly, nativeActivityAvailable, openSession, openPanel, onHostChanged }) {
75
+ const controller = useMemo(() => createIndependentChatController({ api: productApi, hostId, botId: bot.id, onHostChanged }), [productApi, hostId, bot.id, onHostChanged])
76
+ const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot)
77
+ const [settings, setSettings] = useState(false), [following, setFollowing] = useState(true)
78
+ const scroll = useRef(null), input = useRef(null), composition = useRef(false), olderPosition = useRef(null)
79
+ useEffect(() => { controller.start(); return () => controller.dispose() }, [controller])
80
+ const pending = state.pending, request = pending?.status ?? state.lastRequest
81
+ const progress = !state.loading && !state.error && state.view?.available && pending?.status?.state === 'running' && !pending.status.cancelRequested ? pending.status.progress : null
82
+ const contextOperation = pending?.operationKind === 'new-conversation' || request?.operationKind === 'new-conversation'
83
+ const knownPending = pending && ['accepted', 'running'].includes(pending.status?.state) && !pending.status.cancelRequested
84
+ const composingNext = knownPending && !contextOperation
85
+ const showRequest = Boolean(pending || (request && (request.state !== 'succeeded' || request.cancelRequested)))
86
+ const model = botModelStatus(bot)
87
+ const toolRelations = useMemo(() => independentToolRelations(state.items), [state.items])
88
+ const canSend = state.view?.available === true && !state.loading && !readOnly && !pending && !state.sending && !state.storageError
89
+ const context = state.view?.context
90
+ // Native tool activity for the context that is actually open. A context change
91
+ // retires the previous read, so another conversation's records never appear here.
92
+ const nativeActivity = useNativeActivity({ productApi, botId: bot.id, mode: 'independent', contextId: context?.contextId, enabled: nativeActivityAvailable })
93
+ const contextBoundary = () => <div className="ic-context-boundary" role="separator" aria-label="新对话,旧聊天记录保留"><span>新对话 · 旧记录保留</span></div>
94
+ useLayoutEffect(() => {
95
+ const node = scroll.current
96
+ if (!node) return
97
+ if (olderPosition.current !== null && !state.loadingOlder) { node.scrollTop += node.scrollHeight - olderPosition.current; olderPosition.current = null }
98
+ else if (following) node.scrollTop = node.scrollHeight
99
+ }, [state.items, state.loadingOlder, progress?.sequence, following, nativeActivity.activity?.revision ?? 0])
100
+ useLayoutEffect(() => {
101
+ const node = scroll.current
102
+ if (!node || typeof ResizeObserver === 'undefined') return
103
+ const observer = new ResizeObserver(() => {
104
+ if (following && olderPosition.current === null) node.scrollTop = node.scrollHeight
105
+ })
106
+ observer.observe(node)
107
+ if (node.firstElementChild) observer.observe(node.firstElementChild)
108
+ return () => observer.disconnect()
109
+ }, [following])
110
+ const submit = () => { if (canSend && !composition.current) void controller.send() }
111
+ return <>
112
+ <header className="ic-header"><span className="ic-avatar"><UiIcon name="bot" size={22}/></span><div className="ic-heading"><strong>{bot.name || bot.id}</strong><button type="button" className="ic-model" title={`独立聊天 · ${model.title}`} aria-label={`宿主已保存模型:${model.selected ?? '未选择'},打开模型与工具设置`} data-model-different={model.different ? '' : undefined} onClick={() => setSettings(true)}><UiIcon name="spark" size={12}/><span>宿主已选:{model.selectedLabel ?? '未选择模型'}</span><UiIcon name="chevron" size={10}/></button></div><div className="ic-header-actions">
113
+ {typeof state.view?.newConversationAvailable === 'boolean' && <button type="button" title="开启新对话,保留旧聊天记录" aria-label="开启新对话" disabled={!canSend || !state.view.newConversationAvailable} onClick={() => void controller.newConversation()}><UiIcon name="plus" size={18}/></button>}
114
+ {bot.sessionId && <button type="button" title="打开此 Bot 的原生会话" aria-label="打开此 Bot 的原生会话" onClick={() => openSession?.(bot.sessionId)}><UiIcon name="list" size={18}/></button>}
115
+ <button type="button" title="Bot 设置" aria-label="Bot 设置" onClick={() => setSettings(true)}><UiIcon name="settings" size={18}/></button>
116
+ </div></header>
117
+ {readOnly && <div className="ic-alert" role="alert">当前宿主为只读:{String(readOnly)}</div>}
118
+ {state.view && !state.view.available && <div className="ic-alert" role="status">{state.view.mode === 'integration' ? '这个 Bot 使用原生会话。独立聊天记录保留为只读。' : '当前宿主未完整启用独立聊天,请检查运行时配置。'}{state.view.mode === 'integration' && bot.sessionId && <button type="button" onClick={() => openSession?.(bot.sessionId)}>回到原生会话</button>}</div>}
119
+ <div ref={scroll} className="ic-transcript" role="log" aria-label={`${bot.name || bot.id} 的独立聊天记录`} aria-live="polite" onScroll={() => { const node = scroll.current; setFollowing(node.scrollHeight - node.scrollTop - node.clientHeight < 90) }}>
120
+ <div className="ic-message-column">{state.nextCursor && <button type="button" className="ic-load-older" disabled={state.loadingOlder} onClick={() => { olderPosition.current = scroll.current.scrollHeight; void controller.older() }}>{state.loadingOlder ? '正在读取…' : '加载较早消息'}</button>}
121
+ {state.items.length === 0 && <div className="ic-empty"><span className="ic-avatar ic-avatar-large"><UiIcon name="bot" size={33}/></span><strong>{bot.name || bot.id}</strong><p>{state.loading ? '正在读取聊天记录…' : '告诉我你想做什么,我们一起开始。'}</p></div>}
122
+ {state.items.map((message, index) => <React.Fragment key={message.messageId}>{index > 0 && message.contextId && message.contextId !== state.items[index - 1].contextId && contextBoundary()}<IndependentRichMessage message={message} botName={bot.name} calls={toolRelations.get(message.messageId)}/></React.Fragment>)}
123
+ {context?.sequence > 0 && !context.resetPending && state.items.at(-1)?.contextId !== context.contextId && contextBoundary()}
124
+ <NativeActivityCardView activity={nativeActivity.activity} controller={nativeActivity.controller} announce={false}/>
125
+ {progress?.text && <article className="ic-streaming" aria-label="正在生成的回复" aria-busy="true"><div className="ic-streaming-caption"><span className="ic-streaming-dot"/>正在生成 · 第 {progress.step} 步 · 尚未完成</div><div className="ic-streaming-text">{progress.text}</div>{progress.truncated && <small>预览已达到显示上限,完整回复会在完成后显示。</small>}</article>}
126
+ </div>
127
+ </div>
128
+ {!following && <button type="button" className="ic-scroll-latest" onClick={() => { setFollowing(true); scroll.current?.scrollTo({ top: scroll.current.scrollHeight, behavior: 'smooth' }) }}>回到最新<UiIcon name="chevron" size={13}/></button>}
129
+ <div className="ic-composer-area">
130
+ {(state.error || state.storageError) && <div className="ic-alert" role="alert"><span>{state.storageError || state.error}</span><button type="button" onClick={() => controller.inspect()}>核对原请求</button><button type="button" onClick={() => controller.reloadMessages()}>刷新聊天记录</button></div>}
131
+ {showRequest && <div className={`ic-request${knownPending ? ' ic-request-working' : ''}`} role="status"><span>{requestText(pending ? { ...pending.status, operationKind: pending.operationKind } : request)}</span>{!knownPending && <small>请求编号:{pending?.requestKey ?? request.requestKey}</small>}
132
+ {pending?.status?.errorCode && <small>{pending.status.errorCode}</small>}
133
+ <div className="ic-request-actions">{pending && !knownPending && <button type="button" onClick={() => controller.inspect()}>核对原请求</button>}
134
+ {!contextOperation && pending?.status?.finished === true && pending.status.pending === false && <button type="button" onClick={() => { controller.restore(); input.current?.focus() }}>恢复文字到输入框</button>}
135
+ {!contextOperation && pending?.notFound && <button type="button" onClick={() => controller.releaseNotFound()}>宿主无此请求,恢复文字</button>}
136
+ {!contextOperation && pending?.status?.pending === true && !pending.status.cancelRequested && <button type="button" className="ic-stop" disabled={state.sending} onClick={() => { void controller.cancel() }}><UiIcon name="stop" size={13}/>请求停止</button>}
137
+ </div></div>}
138
+ <form className="ic-composer" onSubmit={event => { event.preventDefault(); submit() }}>
139
+ <label className="ic-sr-only" htmlFor={`independent-message-${bot.id}`}>发消息给 {bot.name || bot.id}</label>
140
+ <textarea ref={input} id={`independent-message-${bot.id}`} value={state.draft} placeholder={composingNext ? 'Bot 正在回复,可先写下一条…' : pending ? '原请求尚未核对,已暂停新的发送' : '发消息…'} rows={2} disabled={!state.view?.available || Boolean(readOnly)} onChange={event => controller.setDraft(event.target.value)} onCompositionStart={() => { composition.current = true }} onCompositionEnd={() => { composition.current = false }} onKeyDown={event => { if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && !composition.current) { event.preventDefault(); submit() } }}/>
141
+ <div className="ic-composer-footer"><span>文字消息 · Shift + Enter 换行</span><button type="submit" aria-label="发送消息" disabled={!canSend || !state.draft.trim()}><UiIcon name="arrow" size={18}/><span>发送</span></button></div>
142
+ </form>
143
+ <p className="ic-capability-note">此入口发送文字;附件、语音与记忆视图请打开原生会话。<button type="button" onClick={() => openPanel?.('bot-product-machines')}>电脑</button></p>
144
+ </div>
145
+ {settings && <BotSettingsDialog productApi={productApi} botId={bot.id} onClose={() => setSettings(false)} onOpenTools={typeof openPanel === 'function' ? async () => { requestWorkbenchView('tools'); await openPanel('model-router-galgame'); setSettings(false) } : undefined}/>}
146
+ </>
147
+ }
148
+
@@ -0,0 +1,64 @@
1
+ import React, { memo, useEffect, useRef, useState } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { MarkdownText, MarkdownDelegateProvider, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
4
+ import { copyRichText, INDEPENDENT_MARKDOWN_LABELS, richToolArguments } from './independent-chat-rich.mjs'
5
+
6
+ /**
7
+ * The public Host renderer owns parsing, GFM, math, safe URLs and highlighting.
8
+ * An empty nested delegate deliberately replaces enclosing file capabilities:
9
+ * an authored path never gains an opener or image resolver from another panel.
10
+ */
11
+ export const IndependentRichMessage = memo(function IndependentRichMessage({ message, botName, calls = [] }) {
12
+ const text = typeof message.content === 'string' ? message.content : ''
13
+ const [feedback, setFeedback] = useState(null), [codeFeedback, setCodeFeedback] = useState(null)
14
+ const body = useRef(null), scope = useRef({ live: false, revision: 0, copying: false })
15
+ useEffect(() => {
16
+ const current = scope.current; current.live = true; current.revision++; current.copying = false
17
+ setFeedback(null); setCodeFeedback(null)
18
+ return () => { current.live = false; current.revision++; current.copying = false }
19
+ }, [message.messageId, text])
20
+ const copy = async (source, codeTarget = null) => {
21
+ const current = scope.current, revision = current.revision
22
+ if (!current.live || current.copying) return
23
+ current.copying = true
24
+ const focused = document.activeElement
25
+ const ok = await copyRichText(source, writeClipboard)
26
+ if (!current.live || current.revision !== revision) return
27
+ current.copying = false
28
+ const value = { ok, text: ok ? codeTarget ? '代码已复制' : '消息原文已复制' : '复制失败,请展开消息原文后手动复制。' }
29
+ if (codeTarget?.isConnected) setCodeFeedback({ ...value, target: codeTarget })
30
+ else setFeedback(value)
31
+ // The public helper's execCommand fallback temporarily focuses a textarea.
32
+ // Return to this message's own control rather than disturbing the composer.
33
+ if (focused?.isConnected && typeof focused.focus === 'function') focused.focus({ preventScroll: true })
34
+ }
35
+ const copyCode = event => {
36
+ if (!(event.target instanceof Element)) return
37
+ const button = event.target.closest('button'), block = button?.closest('.md-code-block')
38
+ if (!block || !body.current?.contains(block)) return
39
+ // These are public SDK 0.2.1-rc.4's observable code-card hooks. If a future
40
+ // Host changes them, do not guess from translated button text: the always
41
+ // present exact-message copy/raw controls still work independently.
42
+ const banner = button.closest('[data-code-block-banner]'), source = block.querySelector('[data-code-block-content] pre')
43
+ if (!banner || !source) return
44
+ event.preventDefault(); event.stopPropagation()
45
+ void copy(source.textContent ?? '', banner)
46
+ }
47
+ const roleLabel = message.role === 'user' ? '你' : message.role === 'assistant' ? botName || 'Bot' : message.role === 'tool' ? message.name || '工具结果' : '上下文记录'
48
+ return <article className={`ic-message ic-message-${message.role}`} data-message-id={message.messageId}>
49
+ <div className="ic-message-meta"><strong>{roleLabel}</strong>{message.at != null && <time>{new Date(message.at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}</time>}</div>
50
+ {message.role === 'tool' ? <details className="ic-rich-tool-result"><summary>工具结果{message.toolCallId ? ` · ${message.toolCallId}` : ' · 未记录调用编号'}</summary><pre>{text}</pre>{message.isError === true && <p className="ic-rich-tool-error">记录为错误结果</p>}</details>
51
+ : message.role === 'system' ? <details className="ic-rich-context"><summary>查看上下文记录</summary><pre>{text}</pre></details>
52
+ : text ? <div ref={body} className="ic-bubble ic-rich-body" onClickCapture={copyCode}>
53
+ <MarkdownDelegateProvider><MarkdownText text={text} labels={INDEPENDENT_MARKDOWN_LABELS} variant="body"/></MarkdownDelegateProvider>
54
+ {codeFeedback?.target.isConnected && createPortal(<span className="ic-rich-code-feedback" role="status" data-copy-ok={codeFeedback.ok}>{codeFeedback.text}</span>, codeFeedback.target)}
55
+ </div> : null}
56
+ {calls.length > 0 && <details className="ic-rich-tool-calls"><summary>工具调用 · {calls.length} 项</summary><p className="ic-rich-tool-note">这里显示已记录的调用与返回;结果记录不代表任务完成或已停止。</p>
57
+ {calls.map((call, index) => <section key={`${call.id ?? 'unrecorded'}:${index}`} data-tool-call-id={call.id}>
58
+ <h4>{call.name || '未记录名称'}</h4><dl><dt>调用编号</dt><dd>{call.id || '未记录'}</dd><dt>参数</dt><dd><pre>{richToolArguments(call)}</pre></dd><dt>返回记录</dt><dd>{call.results?.length ? call.results.map(result => <div key={result.messageId} data-tool-result-id={result.messageId}><small>{result.isError === true ? '记录为错误结果' : '已记录返回'} · {result.messageId}</small><pre>{result.content}</pre></div>) : <span>本页尚未记录对应返回;不能据此判断执行结果。</span>}</dd></dl>
59
+ </section>)}
60
+ </details>}
61
+ {message.modelSource && <small className="ic-message-source">{typeof message.modelSource === 'string' ? message.modelSource : `${message.modelSource.provider ?? ''}/${message.modelSource.model ?? ''}`}</small>}
62
+ <div className="ic-rich-message-actions"><button type="button" aria-label="复制消息原文" onClick={() => { void copy(text) }}>复制原文</button><details className="ic-rich-raw"><summary>消息原文</summary><textarea readOnly aria-label="消息原文(可选择复制)" value={text} spellCheck={false}/></details>{feedback && <span role="status" data-copy-ok={feedback.ok}>{feedback.text}</span>}</div>
63
+ </article>
64
+ })
@@ -0,0 +1,30 @@
1
+ /** Match only the nearest preceding recorded call, including reused ids in later batches. */
2
+ export function independentToolRelations(messages = []) {
3
+ const owners = new Map(), result = new Map()
4
+ for (const message of messages) {
5
+ if (message.role === 'assistant') {
6
+ const calls = (Array.isArray(message.toolCalls) ? message.toolCalls : message.toolCall ? [message.toolCall] : []).map(call => ({ ...call, results: [] }))
7
+ result.set(message.messageId, calls)
8
+ for (const call of calls) if (typeof call.id === 'string' && call.id) owners.set(call.id, call)
9
+ }
10
+ const callId = message.toolCallId ?? message.toolCall?.id
11
+ if (message.role === 'tool' && typeof callId === 'string') owners.get(callId)?.results.push(message)
12
+ }
13
+ return result
14
+ }
15
+
16
+ export function richToolArguments(call) {
17
+ const value = call && Object.hasOwn(call, 'args') ? call.args : call?.arguments
18
+ if (value === undefined) return '未记录参数'
19
+ if (typeof value === 'string') return value
20
+ try { return JSON.stringify(value, null, 2) ?? '未记录参数' } catch { return '参数无法显示' }
21
+ }
22
+
23
+ /** Success is established only by the actual clipboard operation's return value. */
24
+ export async function copyRichText(text, writer) {
25
+ try { return await writer(text) === true } catch { return false }
26
+ }
27
+
28
+ export const INDEPENDENT_MARKDOWN_LABELS = Object.freeze({
29
+ code: Object.freeze({ copyLabel: '复制代码', copiedLabel: '代码已复制' }), footnotes: '注释',
30
+ })