@ljwei-stak/dsh-model-route-bot 0.66.4 → 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 (271) 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 +18 -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/package.json +1 -1
  178. package/packages/orchestration/src/binding-product.mjs +19 -0
  179. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  180. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  181. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  182. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  183. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  184. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  185. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  186. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  187. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  188. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  189. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  190. package/packages/orchestration/src/bot-loop.mjs +152 -19
  191. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  192. package/packages/orchestration/src/bot-policy.mjs +43 -4
  193. package/packages/orchestration/src/bot-profile.mjs +171 -0
  194. package/packages/orchestration/src/bot-routine.mjs +4 -4
  195. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  196. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  197. package/packages/orchestration/src/bot-serving.mjs +164 -0
  198. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  199. package/packages/orchestration/src/bot-settings.mjs +50 -11
  200. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  201. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  202. package/packages/orchestration/src/bot-threads.mjs +790 -0
  203. package/packages/orchestration/src/data-product-access.mjs +37 -0
  204. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  205. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  206. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  207. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  208. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  209. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  210. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  211. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  212. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  213. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  214. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  215. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  216. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  217. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  218. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  219. package/packages/orchestration/src/model-accounting.mjs +99 -35
  220. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  221. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  222. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  223. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  224. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  225. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  226. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  227. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  228. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  229. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  230. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  231. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  232. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  233. package/packages/orchestration/src/remote-machines.mjs +82 -14
  234. package/packages/orchestration/src/routine-execution.mjs +5 -3
  235. package/packages/orchestration/src/routine-session.mjs +19 -13
  236. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  237. package/packages/orchestration/src/task-journal.mjs +29 -0
  238. package/packages/orchestration/src/task-product.mjs +17 -0
  239. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  240. package/packages/orchestration/src/team-bridge.mjs +282 -0
  241. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  242. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  243. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  244. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  245. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  246. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  247. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  248. package/remote-computer-agent/README.md +39 -29
  249. package/remote-computer-agent/build.mjs +3 -0
  250. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  253. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  255. package/remote-computer-agent/dist/cli.mjs +1688 -168
  256. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  257. package/remote-computer-agent/dist/index.mjs +1579 -122
  258. package/remote-computer-agent/dist/index.mjs.map +4 -4
  259. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  260. package/remote-computer-agent/package-lock.json +308 -2
  261. package/remote-computer-agent/package.json +2 -1
  262. package/remote-computer-agent/pair-device.ps1 +20 -0
  263. package/remote-computer-agent/setup.ps1 +38 -16
  264. package/remote-computer-agent/src/cli.ts +24 -3
  265. package/remote-computer-agent/src/desktop-session.ts +11 -3
  266. package/remote-computer-agent/src/e2e-server.ts +81 -0
  267. package/remote-computer-agent/src/pairing-page.ts +35 -0
  268. package/remote-computer-agent/src/protocol.ts +25 -25
  269. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  270. package/remote-computer-agent/src/store.ts +20 -5
  271. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,84 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+ import { parseSecureInvite } from '../../packages/orchestration/src/pairing-invite.mjs'
3
+ import { UiIcon } from './ui-icons.jsx'
4
+ import { PairingQrReader } from './pairing-qr.jsx'
5
+
6
+ const errors = {
7
+ PAIRING_CODE_INVALID: '确认码不正确,请核对目标电脑上显示的六位数字。五次错误后邀请码失效。',
8
+ PAIRING_WINDOW_CLOSED: '邀请码已过期或已关闭,请在目标电脑生成新的邀请码。',
9
+ PAIRING_PEER_UNVERIFIED: '未能验证目标电脑。请检查地址和网络,并核对设备指纹;不会切换到明文连接。',
10
+ PAIRING_RESULT_UNKNOWN: '兑换结果尚未确认。保留这份邀请码,点击“继续核对原配对”;宿主会沿用原请求身份。',
11
+ PAIRING_RESULT_UNRECOVERABLE: '原请求的恢复窗口已结束。请在目标电脑检查已配对设备;若宿主没有保存凭据,使用“撤销旧配对后重新邀请”,再导入新邀请。',
12
+ PAIRING_DEVICE_ALREADY_COMMITTED: '目标电脑已登记这次配对,宿主未能保存可用凭据。请在目标电脑核对设备,运行 pair-device.cmd -ResetPairing 撤销旧配对并重新邀请;此操作会断开旧配对。',
13
+ PAIRING_CREDENTIAL_CHANGED: '宿主中已保存的凭据与原配对记录不一致。请核对目标电脑和宿主的设备登记后重新邀请。',
14
+ PAIRING_ALREADY_REDEEMED: '这份邀请码已经被兑换。请在目标电脑核对已配对设备。',
15
+ PAIRING_BINDING_REVIEW_REQUIRED: '设备已添加,请在电脑卡片中核对并完成 Bot 授权。',
16
+ PAIRING_DEVICE_LIMIT: '目标电脑的已配对设备已达上限,请先在目标电脑撤销不再使用的设备。',
17
+ }
18
+
19
+ export function SecurePairingSetup({ snapshot, dispatch, busy }) {
20
+ const [text,setText] = useState(''), [code,setCode] = useState(''), [working,setWorking] = useState(false)
21
+ const [failure,setFailure] = useState(null), [status,setStatus] = useState(''), [clock,setClock] = useState(Date.now())
22
+ const request = useRef(null), mounted = useRef(true), sequence = useRef(0), active = useRef(false), fileInput=useRef(null)
23
+ const importSequence = useRef(0)
24
+ useEffect(() => { mounted.current=true; return () => { mounted.current=false; sequence.current++;importSequence.current++ } }, [])
25
+ useEffect(() => { const timer=setInterval(()=>setClock(Date.now()),1000); return ()=>clearInterval(timer) }, [])
26
+ const parsed = useMemo(() => { if (!text.trim()) return {}; try { return { invite:parseSecureInvite(text) } } catch (error) { return { error:error.code } } },[text])
27
+ const invite = parsed.invite
28
+ const seconds = invite ? Math.max(0,Math.ceil((invite.expiresAt-clock)/1000)) : 0
29
+ const probeBotId = snapshot.mainBotId ?? snapshot.bots?.[0]?.id
30
+ const allowed = invite?.ceiling.hostManagedBots || invite?.ceiling.botIds.includes(probeBotId)
31
+ const recoverable = request.current !== null && Boolean(failure)
32
+ const ready = invite && /^\d{6}$/.test(code) && probeBotId && allowed && (seconds>0 || recoverable)
33
+ const change = value => { importSequence.current++;setText(value); request.current=null; setCode(''); setFailure(null); setStatus('') }
34
+ const submit = async () => {
35
+ if (!ready || active.current || busy) return
36
+ importSequence.current++;active.current=true;setWorking(true);setFailure(null);setStatus('')
37
+ const epoch=++sequence.current
38
+ try {
39
+ if (!request.current) request.current={requestId:[...crypto.getRandomValues(new Uint8Array(16))].map(byte=>byte.toString(16).padStart(2,'0')).join(''),code,invite,probeBotId,
40
+ expectedRevision:(snapshot.machines?.items??[]).find(item=>item.id===invite.machineId)?.revision??0}
41
+ const result=await dispatch('machine.pair.invite',request.current,{errorResult:true})
42
+ if (!result || result.ok===false) throw Object.assign(new Error('配对尚未确认。'),{code:result?.error?.code})
43
+ if (!mounted.current || epoch!==sequence.current) return
44
+ setStatus(result.mainBinding?.status==='blocked'
45
+ ? `设备已安全配对。${errors[result.mainBinding.code]??`Bot 绑定需要处理:${result.mainBinding.code}`}`
46
+ : '已验证设备并保存连接凭据,Bot 的授权范围已登记。')
47
+ setText('');setCode('');request.current=null
48
+ } catch(error) {
49
+ if (mounted.current && epoch===sequence.current) setFailure({code:error.code,message:errors[error.code]??String(error.message??error)})
50
+ if (error.code==='PAIRING_CODE_INVALID') request.current=null
51
+ } finally { active.current=false;if(mounted.current&&epoch===sequence.current)setWorking(false) }
52
+ }
53
+ const importFile = async event => {
54
+ const file=event.target.files?.[0];event.target.value=''
55
+ if(!file||active.current||recoverable||busy)return
56
+ const epoch=++importSequence.current
57
+ if(file.size>16384){setFailure({message:'邀请码文件超过 16 KB。'});return}
58
+ try{const value=await file.text();if(mounted.current&&epoch===importSequence.current&&!active.current&&!request.current)change(value)}
59
+ catch{if(mounted.current&&epoch===importSequence.current)setFailure({message:'无法读取邀请码文件。'})}
60
+ }
61
+ return <details className="bp-card bp-secure-pairing" open><summary><UiIcon name="computer" />安全配对电脑</summary>
62
+ <p className="bp-muted">在目标电脑生成两分钟邀请码,扫描或粘贴到这里,再核对确认码与设备指纹。连接后可从 Bot 聊天中的电脑入口打开桌面。</p>
63
+ <p className="bp-muted">安全配对需要同一预览版 Agent;已有长期配对文件可使用下方兼容入口。</p>
64
+ {(snapshot.availableActions??[]).includes('machine.pair.invite') ? <div className="bp-form">
65
+ <PairingQrReader onRead={change} disabled={working||recoverable||busy}/>
66
+ <label>设备邀请码<textarea rows={3} value={text} disabled={working||recoverable} onChange={event=>change(event.target.value)} autoComplete="off" spellCheck={false} placeholder="粘贴目标电脑生成的 dsh-pairing-invite JSON" /></label>
67
+ <div className="bp-actions"><button type="button" disabled={working||recoverable||busy} onClick={()=>fileInput.current?.click()}>导入邀请码文件</button><input ref={fileInput} className="bp-pairing-file" aria-label="邀请码文件" type="file" accept=".json,application/json" disabled={working||recoverable} onChange={event=>{void importFile(event)}} /></div>
68
+ {parsed.error&&<p className="bp-error" role="alert">无法识别这份邀请码。需要 Agent 生成的第 2 版安全配对文件。</p>}
69
+ {invite&&<section className="bp-pairing-identity" aria-label="核对设备身份"><strong>{invite.name}</strong><code>{invite.endpoint}</code>
70
+ <label>设备指纹<code>{invite.transport.fingerprint.match(/.{1,8}/g).join(' ')}</code></label>
71
+ <p>共享目录:{invite.ceiling.roots.join('、')};允许 {invite.ceiling.tools.length} 项能力。</p>
72
+ <details><summary>查看允许的能力</summary><p className="bp-muted">{invite.ceiling.tools.join('、')}</p></details>
73
+ <p className="bp-muted">{invite.ceiling.hostManagedBots?'目标电脑允许本宿主管理团队 Bot,仍受共享目录和能力范围限制。':`仅允许 Bot:${invite.ceiling.botIds.join('、')}`}</p>
74
+ {!allowed&&<p className="bp-error" role="alert">当前主 Bot 不在目标电脑允许的范围内。</p>}
75
+ <span role="timer">{seconds>0?`邀请码剩余 ${seconds} 秒`:recoverable?'邀请码已过期;可继续核对已保存凭据的原请求。':'邀请码已过期,请在目标电脑重新生成。'}</span>
76
+ </section>}
77
+ <label>目标电脑的六位确认码<input inputMode="numeric" autoComplete="one-time-code" maxLength={6} value={code} disabled={working||recoverable} onChange={event=>{importSequence.current++;setCode(event.target.value.replace(/\D/g,''));request.current=null;setFailure(null)}} placeholder="000000" /></label>
78
+ <div className="bp-actions"><button type="button" className="bp-button-primary" disabled={!ready||busy||working} onClick={()=>{void submit()}}>{working?'正在验证设备…':recoverable?'继续核对原配对':'验证并配对'}</button>
79
+ {recoverable&&<button type="button" disabled={working} onClick={()=>change('')}>清除本地表单</button>}</div>
80
+ {failure&&<p className="bp-error" role="alert">{failure.message}</p>}
81
+ {status&&<p role="status">{status}</p>}
82
+ </div>:<p className="bp-muted">宿主尚未提供安全配对操作,请更新 Host 与 Remote Agent。</p>}
83
+ </details>
84
+ }
@@ -3,6 +3,7 @@ import { TASK_STATES } from './api.mjs'
3
3
  import { botAttentionSummary, remoteUncertain } from './bot-attention.mjs'
4
4
  import { filterTasks, progressNote, taskProgress, taskIdOf, taskKey, taskTitle, TASK_FILTERS, TASK_EVENTS } from './task-progress.mjs'
5
5
  import { UiIcon } from './ui-icons.jsx'
6
+ import { taskBoard } from './workspace-model.mjs'
6
7
 
7
8
  const Tag = ({ children }) => <span className="bp-tag">{children}</span>
8
9
  const date = value => value === null || value === undefined || !Number.isFinite(new Date(value).getTime()) ? '未取得' : new Date(value).toLocaleString('zh-CN')
@@ -33,7 +34,7 @@ function RemoteNotice({ row }) {
33
34
  return remote.possiblyApplied === true ? <small>操作已停止,文件可能已改变;重试不会撤销已有变更,请先核对产物。</small> : null
34
35
  }
35
36
 
36
- function TaskDetail({ task, inspect, busy, actions, dispatch, onClose, bot, openSession }) {
37
+ function TaskDetail({ task, inspect, busy, actions, dispatch, onClose, bot, openSession, focusAttemptId }) {
37
38
  const [history, setHistory] = useState(null), [failure, setFailure] = useState(''), [loading, setLoading] = useState(false)
38
39
  const [reload, setReload] = useState(0)
39
40
  const ref = useRef(null)
@@ -54,11 +55,21 @@ function TaskDetail({ task, inspect, busy, actions, dispatch, onClose, bot, open
54
55
  return () => controller.abort()
55
56
  }, [key, task.updatedAt, inspect, actions.includes('task.inspect'), reload])
56
57
  const data = history?.task ?? task
58
+ const requestedAttempt = focusAttemptId && history?.attempts.find(item=>item.attemptId===focusAttemptId)
59
+ const missingAttempt = Boolean(history && focusAttemptId && !requestedAttempt)
60
+ const historical = Boolean(focusAttemptId && focusAttemptId!==data.attemptId)
61
+ const readOnlyDetail = Boolean(task.unloaded && !history || focusAttemptId && (!history || historical || missingAttempt))
62
+ useEffect(()=>{if(requestedAttempt)ref.current?.querySelector('[data-focused-attempt]')?.scrollIntoView?.({block:'nearest'})},[history,focusAttemptId])
57
63
  return <article className="bp-card bp-task-detail" aria-label="任务详情" ref={ref} tabIndex={-1}>
58
- <div className="bp-card-title"><div><h2>任务详情</h2><p>{taskTitle(task)}</p></div><button type="button" onClick={onClose}>关闭</button></div>
64
+ <div className="bp-card-title"><div><h2>任务详情</h2><p>{taskTitle(data)}</p></div><button type="button" onClick={onClose}>关闭</button></div>
59
65
  <div className="bp-actions"><Tag>{TASK_STATES[data.state] ?? data.state}</Tag><span>{bot?.name ?? task.botId ?? '未取得归属'}</span>
60
66
  {bot?.sessionId && openSession && <button type="button" onClick={() => openSession(bot.sessionId)}>打开 Bot 私聊</button>}
61
- <TaskActions row={data} actions={actions} dispatch={dispatch} busy={busy} /></div>
67
+ {!readOnlyDetail && <TaskActions row={data} actions={actions} dispatch={dispatch} busy={busy} />}</div>
68
+ {task.unloaded && <p className="bp-muted">原任务未包含在当前列表,详情通过原 Bot 与 Task 编号单独读取。</p>}
69
+ {missingAttempt && <p role="alert" className="bp-blocked">未找到所选的原执行尝试。请在原 Bot 会话核对执行记录。</p>}
70
+ {requestedAttempt && <section className="bp-blocked" aria-label="所选原执行尝试"><strong>第 {requestedAttempt.number} 次尝试 · {TASK_STATES[requestedAttempt.state]??requestedAttempt.state}</strong>
71
+ {historical && <p>此处定位历史尝试,当前任务已进入后续尝试。原记录只读。</p>}
72
+ <p>开始 {date(requestedAttempt.startedAt)} · 结束 {date(requestedAttempt.finishedAt)}</p><small className="bp-code">{requestedAttempt.attemptId} · {requestedAttempt.operationId}</small></section>}
62
73
  <RemoteNotice row={data} />
63
74
  <dl className="bp-facts"><Value label="创建">{date(data.createdAt)}</Value><Value label="最近更新">{date(data.updatedAt)}</Value><Value label="期限">{date(data.deadlineAt)}</Value>{data.attribution?.label && <Value label="执行进度">{data.attribution.label}</Value>}</dl>
64
75
  <h3>任务时间线</h3>
@@ -68,7 +79,7 @@ function TaskDetail({ task, inspect, busy, actions, dispatch, onClose, bot, open
68
79
  {!actions.includes('task.inspect') && <p className="bp-muted">此宿主尚未提供任务时间线,可在下方查看现有收据依据。</p>}
69
80
  {history && <>
70
81
  <p className="bp-muted">{history.attempts.length} 次执行尝试 · {history.timeline.total} 条日志{history.timeline.truncated ? `,显示最近 ${history.timeline.items.length} 条` : ''}。停止请求需等待原操作确认。</p>
71
- <ol className="bp-task-timeline">{history.timeline.items.map(event => <li key={event.seq}>
82
+ <ol className="bp-task-timeline">{history.timeline.items.map(event => <li key={event.seq} data-focused-attempt={focusAttemptId&&event.attemptId===focusAttemptId?'':undefined}>
72
83
  <span className="bp-timeline-dot" aria-hidden="true" /><div><strong>{TASK_EVENTS[event.kind] ?? '任务状态更新'}</strong>
73
84
  <small><time>{date(event.at)}</time>{event.attemptNumber != null ? ` · 第 ${event.attemptNumber} 次尝试` : ''}{event.outcome ? ` · ${TASK_STATES[event.outcome] ?? (event.outcome === 'unknown' ? '结果未知' : event.outcome)}` : ''}</small>
74
85
  {event.code && <small className="bp-code">{event.code}</small>}</div>
@@ -83,18 +94,29 @@ function TaskDetail({ task, inspect, busy, actions, dispatch, onClose, bot, open
83
94
  </article>
84
95
  }
85
96
 
86
- export function TaskWorkspace({ snapshot, dispatch, busy, scope, inspect, openSession, routingCard }) {
97
+ export function TaskWorkspace({ snapshot, dispatch, busy, scope, inspect, openSession, routingCard, focus }) {
87
98
  const progress = useMemo(() => taskProgress(snapshot), [snapshot])
88
99
  const [botId, setBotId] = useState(''), [status, setStatus] = useState('all'), [query, setQuery] = useState('')
89
100
  const [expanded, setExpanded] = useState(false), [selection, setSelection] = useState(null)
90
- useEffect(() => { setBotId(''); setStatus('all'); setQuery(''); setExpanded(false); setSelection(null) }, [scope])
101
+ const [view, setView] = useState('list'), [focusMissing, setFocusMissing] = useState(false)
102
+ useEffect(() => { setBotId(''); setStatus('all'); setQuery(''); setExpanded(false); setSelection(null); setFocusMissing(false) }, [scope])
103
+ useEffect(() => {
104
+ if (!focus) return
105
+ setQuery(''); setStatus(focus.filter ?? 'all'); setBotId(focus.taskId ? '' : focus.botId ?? '')
106
+ const target = focus.taskId ? progress.items.find(row => taskIdOf(row) === focus.taskId && row.botId === focus.botId) : null
107
+ const canReadMissing = focus.taskId && focus.botId && inspect && snapshot.availableActions?.includes('task.inspect') && snapshot.bots?.some(bot=>bot.id===focus.botId)
108
+ setSelection(target ? { scope, key: taskKey(target), attemptId:focus.attemptId } : canReadMissing ? {scope,key:taskKey(focus),attemptId:focus.attemptId,
109
+ lookup:{botId:focus.botId,taskId:focus.taskId,state:'unloaded',title:'原任务',unloaded:true,attempts:0}} : null)
110
+ setFocusMissing(Boolean(focus.taskId && !target && !canReadMissing))
111
+ }, [focus?.version, scope, Boolean(inspect), snapshot.availableActions?.includes('task.inspect')])
91
112
  const rows = filterTasks(progress, { botId, status, query })
92
- const task = selection && selection.scope === scope ? progress.items.find(row => taskKey(row) === selection.key) : null
113
+ const task = selection && selection.scope === scope ? progress.items.find(row => taskKey(row) === selection.key) ?? selection.lookup : null
93
114
  const actions = snapshot.availableActions ?? []
94
115
  const select = row => setSelection({ scope, key: taskKey(row) })
95
116
  const attention = expanded ? progress.attention : progress.attention.slice(0, 6)
96
117
  const [showPlans, setShowPlans] = useState(false)
97
118
  return <>
119
+ {focusMissing && <p className="bp-blocked" role="status">所选任务不在当前已加载的记录中。请刷新记录,或返回原 Bot 会话核对。</p>}
98
120
  <section className="bp-card bp-task-overview" aria-label="多 Bot 任务总览">
99
121
  <div className="bp-card-title"><div><h2>团队任务</h2><p className="bp-muted">按 Bot 查看进展,优先处理需要你介入的任务。</p></div><Tag>{progress.items.length} 条已加载</Tag></div>
100
122
  {snapshot.tasks.truncated && <p role="status" className="bp-blocked">已加载 {progress.items.length} / {snapshot.tasks.total} 条任务,优先显示待处理与进行中的记录。下方数量与筛选仅针对已加载任务。</p>}
@@ -117,11 +139,11 @@ export function TaskWorkspace({ snapshot, dispatch, busy, scope, inspect, openSe
117
139
  })}</ul>
118
140
  {progress.attention.length > 6 && <button type="button" aria-expanded={expanded} onClick={() => setExpanded(value => !value)}>{expanded ? '收起待处理事项' : `查看全部 ${progress.attention.length} 项待处理`}</button>}
119
141
  </section>}
120
- {task && <TaskDetail key={taskKey(task)} task={task} inspect={inspect} busy={busy} actions={actions} dispatch={dispatch} onClose={() => setSelection(null)} bot={snapshot.bots?.find(bot => bot.id === task.botId)} openSession={openSession} />}
121
- <section className="bp-card" aria-label="任务记录"><div className="bp-card-title"><h2>任务记录</h2><span className="bp-muted" role="status">显示 {rows.length} / {progress.items.length} 条</span></div>
142
+ {task && <TaskDetail key={taskKey(task)} task={task} focusAttemptId={selection.attemptId} inspect={inspect} busy={busy} actions={actions} dispatch={dispatch} onClose={() => setSelection(null)} bot={snapshot.bots?.find(bot => bot.id === task.botId)} openSession={openSession} />}
143
+ <section className="bp-card bp-task-records" aria-label="任务记录"><div className="bp-card-title"><div><h2>任务记录</h2><span className="bp-muted" role="status">显示 {rows.length} / {progress.items.length} 条</span></div><div className="bp-segmented" role="group" aria-label="任务显示方式"><button type="button" aria-pressed={view === 'list'} onClick={() => setView('list')}><UiIcon name="list" size={15} />列表</button><button type="button" aria-pressed={view === 'board'} onClick={() => setView('board')}><UiIcon name="board" size={15} />看板</button></div></div>
122
144
  <div className="bp-task-search"><label>搜索任务<input type="search" value={query} placeholder="任务名称、Bot 或任务 / 计划 / 操作编号" onChange={event => setQuery(event.target.value)} /></label>
123
145
  {(query || botId || status !== 'all') && <button type="button" onClick={() => { setQuery(''); setBotId(''); setStatus('all') }}>清除筛选</button>}</div>
124
- {!progress.items.length ? <p className="bp-empty">尚无持久任务。在 Main 私聊中提出目标后,可在这里查看工作包、执行尝试与收据。</p> : rows.length === 0 ? <p className="bp-empty">没有符合当前筛选的任务。可清除筛选查看其他记录。</p> : <div className="bp-table-wrap"><table>
146
+ {!progress.items.length ? <p className="bp-empty">尚无持久任务。在 Main 私聊中提出目标后,可在这里查看工作包、执行尝试与收据。</p> : rows.length === 0 ? <p className="bp-empty">没有符合当前筛选的任务。可清除筛选查看其他记录。</p> : view === 'board' ? <div className="bp-board" aria-label="任务看板">{taskBoard(progress, rows).map(lane => <section key={lane.id} className="bp-board-lane" aria-label={lane.title} data-lane={lane.id}><h3><UiIcon name={lane.icon} size={15} />{lane.title}<span>{lane.items.length}</span></h3><div className="bp-board-items">{lane.items.map(row => <article key={taskKey(row)} className="bp-board-card"><span className="bp-board-state">{TASK_STATES[row.state] ?? row.state}</span><button type="button" className="bp-text-button" onClick={() => select(row)}>{taskTitle(row)}</button><p>{snapshot.bots?.find(bot => bot.id === row.botId)?.name ?? row.botId ?? '归属未提供'}</p>{row.blockedReason && <small>{row.blockedReason}</small>}<RemoteNotice row={row} /><footer><time>{date(row.updatedAt)}</time><div className="bp-actions"><TaskActions row={row} actions={actions} dispatch={dispatch} busy={busy} /></div></footer></article>)}{!lane.items.length && <p className="bp-board-empty">暂无任务</p>}</div></section>)}</div> : <div className="bp-table-wrap"><table>
125
147
  <thead><tr><th>任务</th><th>Bot</th><th>状态</th><th>执行尝试</th><th>操作</th></tr></thead><tbody>{rows.map(row => <tr key={taskKey(row)}>
126
148
  <td><button className="bp-text-button" onClick={() => select(row)}>{taskTitle(row)}</button><small>更新 {date(row.updatedAt)}</small></td>
127
149
  <td>{snapshot.bots?.find(bot => bot.id === row.botId)?.name ?? row.botId ?? '未取得归属'}</td><td><Tag>{TASK_STATES[row.state] ?? row.state}</Tag>{progressNote(row, TASK_STATES[row.state] ?? row.state) && <small>{progressNote(row, TASK_STATES[row.state] ?? row.state)}</small>}{row.blockedReason && <small>{row.blockedReason}</small>}<RemoteNotice row={row} /></td>
@@ -1,6 +1,22 @@
1
1
  import React from 'react'
2
2
 
3
3
  const paths = {
4
+ home: ['m3 10 9-7 9 7v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Zm6 11v-8h6v8'],
5
+ inbox: ['M4 4h16l2 10v6H2v-6Zm-2 10h6l2 3h4l2-3h6'],
6
+ bell: ['M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4'],
7
+ clock: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 4v5l3 2'],
8
+ activity: ['M3 12h4l3-8 4 16 3-8h4'],
9
+ sun: ['M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5'],
10
+ moon: ['M20 14.5A8.5 8.5 0 0 1 9.5 4 8.5 8.5 0 1 0 20 14.5Z'],
11
+ memory: ['M9 4a3 3 0 0 0-5 3 4 4 0 0 0 0 7 4 4 0 0 0 5 5m6-15a3 3 0 0 1 5 3 4 4 0 0 1 0 7 4 4 0 0 1-5 5', 'M12 3v18m-5-11 5 2 5-2m-9 6 4-2 4 2'],
12
+ voice: ['M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3Zm-7 8v1a7 7 0 0 0 14 0v-1m-7 8v3m-4 0h8'],
13
+ keyboard: ['M3 5h18v14H3Zm4 4h1m3 0h1m3 0h1m-9 4h1m3 0h1m3 0h1m-9 3h10'],
14
+ hand: ['M7 11V5a2 2 0 0 1 4 0v5-7a2 2 0 0 1 4 0v7-5a2 2 0 0 1 4 0v7l1-2a2 2 0 0 1 3 2l-4 9H9l-6-7a2 2 0 0 1 3-3l3 3'],
15
+ fit: ['M8 3H3v5m13-5h5v5M3 16v5h5m13-5v5h-5M8 8h8v8H8Z'],
16
+ close: ['m6 6 12 12M6 18 18 6'],
17
+ board: ['M3 4h7v16H3Zm11 0h7v10h-7'],
18
+ list: ['M9 6h12M9 12h12M9 18h12M3 6h1m-1 6h1m-1 6h1'],
19
+ spark: ['m12 3 2.3 6.7L21 12l-6.7 2.3L12 21l-2.3-6.7L3 12l6.7-2.3Z'],
4
20
  bot: ['M8 4h8l4 4v8l-4 4H8l-4-4V8Z', 'M9 10v3m6-3v3'],
5
21
  team: ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2m20 0v-2a4 4 0 0 0-3-3.87', 'M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8m8-7.87a4 4 0 0 1 0 7.75'],
6
22
  tasks: ['M9 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-3', 'M9 3h6v4H9Zm-1 10 2 2 5-5m-7 8h8'],
@@ -12,6 +28,7 @@ const paths = {
12
28
  refresh: ['M20 7v5h-5M4 17v-5h5', 'M6.1 6.1A8 8 0 0 1 19.5 9M4.5 15a8 8 0 0 0 13.4 2.9'],
13
29
  search: ['M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14m5-2 5 5'],
14
30
  upload: ['M12 16V3m-5 5 5-5 5 5M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4'],
31
+ download: ['M12 3v13m-5-5 5 5 5-5M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3'],
15
32
  plus: ['M12 5v14M5 12h14'],
16
33
  file: ['M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Zm0 0v6h6', 'M8 13h8m-8 4h5'],
17
34
  arrow: ['M7 17 17 7M7 7h10v10'],
@@ -0,0 +1,43 @@
1
+ import React,{useEffect,useMemo,useState} from 'react'
2
+ import {money} from './api.mjs'
3
+ import {usageReport,usageCsv,usageDay,usageModel,USAGE_SOURCES,USAGE_STATES} from './usage-report.mjs'
4
+ import {UiIcon} from './ui-icons.jsx'
5
+
6
+ const quantity=value=>value===null?'未知':value.toLocaleString('zh-CN')
7
+ export function UsageExplorer({section,busy=false,scope}){
8
+ const [filter,setFilter]=useState({from:'',to:'',botId:'',source:'',status:'',groupBy:'none'}),[page,setPage]=useState(0),[message,setMessage]=useState('')
9
+ useEffect(()=>{setFilter({from:'',to:'',botId:'',source:'',status:'',groupBy:'none'});setPage(0);setMessage('')},[scope])
10
+ const items=section.items??[]
11
+ const report=useMemo(()=>usageReport(items,{...filter,timezone:section.timezone}),[items,filter,section.timezone])
12
+ const bots=useMemo(()=>[...new Map(items.map(row=>[row.botId??'',row.botName??row.botId??'未归属'])).entries()].sort((a,b)=>String(a[1]).localeCompare(String(b[1]))),[items])
13
+ const statuses=useMemo(()=>[...new Set(items.map(row=>row.status).filter(Boolean))].sort(),[items])
14
+ const pages=Math.max(1,Math.ceil(report.rows.length/100)),current=Math.min(page,pages-1),visible=report.rows.slice(current*100,(current+1)*100)
15
+ const change=(key,value)=>{setFilter(old=>({...old,[key]:value}));setPage(0);setMessage('')}
16
+ const exportCsv=()=>{
17
+ if(busy||report.error||!report.rows.length)return
18
+ try{
19
+ const href=URL.createObjectURL(new Blob([usageCsv(report)],{type:'text/csv;charset=utf-8'})),link=document.createElement('a')
20
+ link.href=href;link.download=`dsh-usage-${filter.from||'all'}-${filter.to||'all'}.csv`;link.hidden=true;document.body.append(link)
21
+ try{link.click()}finally{link.remove();setTimeout(()=>URL.revokeObjectURL(href),1000)}
22
+ setMessage(`已导出 ${report.rows.length} 条当前筛选的收据。`)
23
+ }catch{setMessage('浏览器未能导出文件,请检查下载权限后重试。')}
24
+ }
25
+ return <section className="bp-usage-explorer" aria-label="费用收据明细">
26
+ <div className="bp-usage-heading"><div><h2>收据明细</h2><p className="bp-muted">按调用开始时间筛选 · {report.timezone}。仅包含当前 Bot 的逐条调用收据;早期汇总归档与无归属费用见上方资金汇总。</p></div><button type="button" disabled={busy||Boolean(report.error)||!report.rows.length} onClick={exportCsv}><UiIcon name="download" size={15}/>导出 CSV</button></div>
27
+ <div className="bp-usage-filters">
28
+ <label>开始日期<input type="date" aria-label="收据开始日期" value={filter.from} onChange={event=>change('from',event.target.value)}/></label>
29
+ <label>结束日期<input type="date" aria-label="收据结束日期" value={filter.to} onChange={event=>change('to',event.target.value)}/></label>
30
+ <label>Bot<select aria-label="筛选收据 Bot" value={filter.botId} onChange={event=>change('botId',event.target.value)}><option value="">全部 Bot</option>{bots.filter(([id])=>id).map(([id,label])=><option key={id} value={id}>{label}</option>)}</select></label>
31
+ <label>来源<select aria-label="筛选收据来源" value={filter.source} onChange={event=>change('source',event.target.value)}><option value="">全部来源</option>{[...new Set(items.map(row=>row.source).filter(Boolean))].sort().map(source=><option key={source} value={source}>{USAGE_SOURCES[source]??source}</option>)}</select></label>
32
+ <label>状态<select aria-label="筛选收据状态" value={filter.status} onChange={event=>change('status',event.target.value)}><option value="">全部状态</option>{statuses.map(status=><option key={status} value={status}>{USAGE_STATES[status]??status}</option>)}</select></label>
33
+ <label>分组<select aria-label="收据分组" value={filter.groupBy} onChange={event=>change('groupBy',event.target.value)}>{[['none','不分组'],['bot','按 Bot'],['model','按模型'],['source','按来源'],['day','按日期']].map(([value,label])=><option key={value} value={value}>{label}</option>)}</select></label>
34
+ </div>
35
+ {report.error&&<p className="bp-error" role="alert">{report.error}</p>}
36
+ {(report.undatedExcluded>0||report.undated>0&&!filter.from&&!filter.to)&&<p className="bp-muted">{report.undatedExcluded?`${report.undatedExcluded} 条收据未报告日期,不计入所选日期范围。`:`${report.undated} 条收据未报告日期,保留在全部明细中。`}</p>}
37
+ <div className="bp-usage-totals" aria-label="当前筛选汇总"><span><strong>{report.summary.calls}</strong> 条收据</span><span>已知 API 金额 <strong>{money(report.summary.knownApiUsd)}</strong></span><span>金额未知 <strong>{report.summary.unknownApiCalls}</strong> 条</span><span>订阅调用 <strong>{report.summary.subscriptionCalls}</strong> 次</span><span>已知 Token {quantity(report.summary.calls>report.summary.unknownInputCalls?report.summary.inputTokens:null)} / {quantity(report.summary.calls>report.summary.unknownOutputCalls?report.summary.outputTokens:null)}{report.summary.unknownInputCalls+report.summary.unknownOutputCalls>0?'(含未报告项)':''}</span></div>
38
+ {report.groups.length>0&&<div className="bp-table-wrap bp-usage-groups"><table><thead><tr><th>分组</th><th>收据数</th><th>已知 API 金额</th><th>金额未知</th><th>订阅调用</th></tr></thead><tbody>{report.groups.map(group=><tr key={group.key}><td>{group.label}</td><td>{group.calls}</td><td>{money(group.knownApiUsd)}</td><td>{group.unknownApiCalls}</td><td>{group.subscriptionCalls}</td></tr>)}</tbody></table></div>}
39
+ {!visible.length?<p className="bp-empty">{items.length?'没有符合条件的收据。':'尚无费用收据。'}</p>:<div className="bp-table-wrap bp-usage-receipts"><table role="table"><thead><tr><th>日期 / Bot / 任务</th><th>来源</th><th>准确模型</th><th>金额</th><th>状态</th></tr></thead><tbody role="rowgroup">{visible.map((row,index)=><tr role="row" key={row.id??row.receiptId??index}><td role="cell" data-label="日期 / Bot / 任务">{usageDay(row,report.timezone)??'日期未报告'}<small>{row.botName??row.botId??'未归属'}</small><small>{row.taskId??row.runId}</small></td><td role="cell" data-label="来源">{USAGE_SOURCES[row.source]??row.source??'未报告'}</td><td role="cell" data-label="准确模型">{usageModel(row)}</td><td role="cell" data-label="金额">{row.billing==='subscription'?'订阅':money(row.amountUsd??row.costUsd)}</td><td role="cell" data-label="状态">{USAGE_STATES[row.status]??row.status??'未知'}{row.resolution&&<span className="bp-tag">{row.resolution.kind==='waive'?'已按零费用放行':`已确认 ${money(row.resolution.costUsd)}`}</span>}</td></tr>)}</tbody></table></div>}
40
+ <div className="bp-usage-pagination"><span>{report.rows.length} / {report.total} 条 · 第 {current+1} / {pages} 页</span><button type="button" disabled={current===0} onClick={()=>setPage(current-1)}>上一页</button><button type="button" disabled={current+1>=pages} onClick={()=>setPage(current+1)}>下一页</button></div>
41
+ {message&&<p role="status">{message}</p>}
42
+ </section>
43
+ }
@@ -0,0 +1,71 @@
1
+ export const USAGE_SOURCES = Object.freeze({bot:'Bot 对话',aux:'Bot 辅助调用','mr-api':'MR API',cli:'官方 CLI',review:'评审'})
2
+ export const USAGE_STATES = Object.freeze({admitted:'已准入',running:'执行中',reported:'已报告',settled:'已结算',unknown:'待核对'})
3
+ const quantity=value=>typeof value==='number'&&Number.isFinite(value)&&value>=0?value:null
4
+ const dateOf=row=>Number.isSafeInteger(row.createdAt)&&row.createdAt>=0?row.createdAt:Number.isSafeInteger(row.finishedAt)&&row.finishedAt>=0?row.finishedAt:null
5
+ const validDate=value=>!value||/^\d{4}-\d{2}-\d{2}$/.test(value)&&Number.isFinite(Date.parse(`${value}T00:00:00Z`))&&new Date(`${value}T00:00:00Z`).toISOString().slice(0,10)===value
6
+ const formatterCache=new Map()
7
+ export function usageTimezone(value){
8
+ if(formatterCache.has(value||'UTC'))return value||'UTC'
9
+ try{new Intl.DateTimeFormat('en',{timeZone:value||'UTC'}).format(0);return value||'UTC'}catch{return 'UTC'}
10
+ }
11
+ export function usageDay(row,timezone='UTC'){
12
+ const at=dateOf(row)
13
+ if(at===null||!Number.isFinite(new Date(at).getTime()))return null
14
+ const zone=usageTimezone(timezone)
15
+ if(!formatterCache.has(zone)){if(formatterCache.size>=32)formatterCache.delete(formatterCache.keys().next().value);formatterCache.set(zone,new Intl.DateTimeFormat('en',{timeZone:zone,year:'numeric',month:'2-digit',day:'2-digit'}))}
16
+ const parts=Object.fromEntries(formatterCache.get(zone).formatToParts(at).map(part=>[part.type,part.value]))
17
+ return `${parts.year.padStart(4,'0')}-${parts.month}-${parts.day}`
18
+ }
19
+ export const usageModel=row=>typeof row.model==='object'&&row.model?`${row.model.provider??row.provider??'未知来源'}/${row.model.model??row.model.id??'未知模型'}`:row.provider?`${row.provider}/${row.model??row.modelId??'未知模型'}`:String(row.model??row.modelId??'未报告')
20
+ const emptySummary=()=>({calls:0,knownApiUsd:null,knownApiCalls:0,amountOverflow:false,unknownApiCalls:0,subscriptionCalls:0,inputTokens:0,outputTokens:0,unknownInputCalls:0,unknownOutputCalls:0})
21
+ function accumulate(total,row){
22
+ total.calls++
23
+ const amount=quantity(row.amountUsd??row.costUsd)
24
+ if(row.billing==='subscription')total.subscriptionCalls++
25
+ else if(amount===null)total.unknownApiCalls++
26
+ else {
27
+ total.knownApiCalls++
28
+ if(!total.amountOverflow){const sum=(total.knownApiUsd??0)+amount;total.amountOverflow=!Number.isFinite(sum);total.knownApiUsd=total.amountOverflow?null:sum}
29
+ }
30
+ for(const [field,missing]of [['inputTokens','unknownInputCalls'],['outputTokens','unknownOutputCalls']]){
31
+ const value=Number.isSafeInteger(row[field])&&row[field]>=0?row[field]:null
32
+ if(value===null)total[missing]++
33
+ else total[field]=total[field]===null||!Number.isSafeInteger(total[field]+value)?null:total[field]+value
34
+ }
35
+ }
36
+
37
+ /** Analysis of public source receipts only; no budget, reservation or billing decisions. */
38
+ export function usageReport(items=[],{from='',to='',botId='',source='',status='',groupBy='none',timezone='UTC'}={}){
39
+ const error=!validDate(from)||!validDate(to)?'请选择有效的起止日期。':from&&to&&from>to?'开始日期不能晚于结束日期。':''
40
+ const zone=usageTimezone(timezone),rows=[],groups=new Map(),summary=emptySummary()
41
+ let undated=0,undatedExcluded=0
42
+ for(const row of Array.isArray(items)?items:[]){
43
+ if(!row||typeof row!=='object'||botId&&row.botId!==botId||source&&row.source!==source||status&&row.status!==status)continue
44
+ const day=usageDay(row,zone)
45
+ if(!day){undated++;if(from||to)undatedExcluded++}
46
+ if(error||(from||to)&&!day||from&&day<from||to&&day>to)continue
47
+ rows.push(row);accumulate(summary,row)
48
+ if(groupBy!=='none'){
49
+ const key=groupBy==='bot'?String(row.botId??''):groupBy==='model'?usageModel(row):groupBy==='source'?String(row.source??''):day??''
50
+ if(!groups.has(key))groups.set(key,{key,label:groupBy==='bot'?row.botName??row.botId??'未归属':groupBy==='source'?USAGE_SOURCES[row.source]??row.source??'未报告':groupBy==='day'?day??'日期未报告':key,...emptySummary()})
51
+ accumulate(groups.get(key),row)
52
+ }
53
+ }
54
+ rows.sort((a,b)=>(dateOf(b)??-1)-(dateOf(a)??-1)||(b.sequence??0)-(a.sequence??0))
55
+ return {rows,groups:[...groups.values()].sort((a,b)=>groupBy==='day'?b.key.localeCompare(a.key):b.calls-a.calls||a.key.localeCompare(b.key)),summary,timezone:zone,undated,undatedExcluded,error,total:Array.isArray(items)?items.length:0}
56
+ }
57
+
58
+ /** Quoting alone does not prevent spreadsheet formulas in a person's Bot/model name. */
59
+ export function csvCell(value){
60
+ let text=value===null||value===undefined?'':String(value).replaceAll('\0','')
61
+ if(/^[\s\uFEFF]*[=+\-@]/u.test(text)||/^[\t\r\n]/.test(text))text=`'${text}`
62
+ return `"${text.replaceAll('"','""')}"`
63
+ }
64
+ export function usageCsv(report){
65
+ const headers=['收据编号','Bot 编号','Bot 名称','任务编号','运行编号','调用开始时间','日期时区','来源','准确模型','计费方式','金额 USD(空白=未知或订阅)','金额状态','输入 Token(空白=未知)','输出 Token(空白=未知)','状态','核对结果']
66
+ const lines=report.rows.map(row=>{
67
+ const amount=quantity(row.amountUsd??row.costUsd),at=dateOf(row),dated=at!==null&&Number.isFinite(new Date(at).getTime())
68
+ return [row.id??row.receiptId,row.botId,row.botName,row.taskId,row.runId,dated?new Date(at).toISOString():'',report.timezone,USAGE_SOURCES[row.source]??row.source,usageModel(row),row.billing==='subscription'?'订阅':'API',row.billing==='subscription'?null:amount,row.billing==='subscription'?'订阅':amount===null?'未知':'已知',quantity(row.inputTokens),quantity(row.outputTokens),row.status,row.resolution?.kind??'']
69
+ })
70
+ return '\uFEFF'+[headers,...lines].map(cells=>cells.map(csvCell).join(',')).join('\r\n')+'\r\n'
71
+ }
@@ -0,0 +1,34 @@
1
+ body[data-bot-product-ui] .bp-usage-explorer { display:flex;flex-direction:column;gap:16px;min-width:0; }
2
+ body[data-bot-product-ui] .bp-usage-heading { display:flex;align-items:flex-start;gap:20px;justify-content:space-between; }
3
+ body[data-bot-product-ui] .bp-usage-heading > div { flex:1;min-width:0; }
4
+ body[data-bot-product-ui] .bp-usage-heading h2 { margin:0 0 6px;font-size:17px;font-weight:600; }
5
+ body[data-bot-product-ui] .bp-usage-heading p { margin:0;line-height:1.7; }
6
+ body[data-bot-product-ui] .bp-usage-heading > button { display:inline-flex;align-items:center;gap:7px;flex:none; }
7
+ body[data-bot-product-ui] .bp-usage-filters { display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;padding:16px;border:1px solid var(--bp-border);border-radius:14px;background:var(--bp-surface); }
8
+ body[data-bot-product-ui] .bp-usage-filters label { display:flex;flex-direction:column;gap:7px;min-width:0;font-size:12px;color:var(--bp-muted); }
9
+ body[data-bot-product-ui] .bp-usage-filters :is(input,select) { width:100%;min-width:0;min-height:36px;color:var(--bp-ink); }
10
+ body[data-bot-product-ui] .bp-usage-filters input[type=date] { color-scheme:light; }
11
+ body[data-bot-product-ui][data-bp-appearance=dark] .bp-usage-filters input[type=date],
12
+ body[data-bot-product-ui][data-ds-dark-theme]:not([data-bp-appearance=light]) .bp-usage-filters input[type=date] { color-scheme:dark; }
13
+ body[data-bot-product-ui] .bp-usage-totals { display:flex;flex-wrap:wrap;gap:10px 20px;font-size:12px;color:var(--bp-muted); }
14
+ body[data-bot-product-ui] .bp-usage-totals strong { color:var(--bp-ink);font-variant-numeric:tabular-nums;font-weight:600; }
15
+ body[data-bot-product-ui] .bp-usage-explorer td { overflow-wrap:anywhere; }
16
+ body[data-bot-product-ui] .bp-usage-pagination { display:flex;align-items:center;flex-wrap:wrap;gap:8px;font-size:12px;color:var(--bp-muted); }
17
+ body[data-bot-product-ui] .bp-usage-pagination > span { flex:1;min-width:100px; }
18
+ @container(max-width:600px) {
19
+ body[data-bot-product-ui] .bp-usage-filters { grid-template-columns:repeat(2,minmax(0,1fr));padding:12px; }
20
+ body[data-bot-product-ui] .bp-usage-heading { flex-direction:column;gap:12px; }
21
+ body[data-bot-product-ui] .bp-usage-groups table { min-width:520px; }
22
+ body[data-bot-product-ui] .bp-usage-receipts { border:0;background:transparent; }
23
+ body[data-bot-product-ui] .bp-usage-receipts :is(table,tbody) { display:block;width:100%; }
24
+ body[data-bot-product-ui] .bp-usage-receipts thead { position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%); }
25
+ body[data-bot-product-ui] .bp-usage-receipts tbody { display:flex;flex-direction:column;gap:12px; }
26
+ body[data-bot-product-ui] .bp-usage-receipts tbody tr { display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px 12px;padding:16px;border:1px solid var(--bp-border);border-radius:12px;background:var(--bp-surface); }
27
+ body[data-bot-product-ui] .bp-usage-receipts td { display:block;min-width:0;padding:0;border:0; }
28
+ body[data-bot-product-ui] .bp-usage-receipts td::before { display:block;content:attr(data-label);margin-bottom:5px;font-size:11px;color:var(--bp-muted); }
29
+ body[data-bot-product-ui] .bp-usage-receipts td:first-child { order:0; }
30
+ body[data-bot-product-ui] .bp-usage-receipts td:nth-child(4) { order:1; }
31
+ body[data-bot-product-ui] .bp-usage-receipts td:nth-child(3) { order:2;grid-column:1 / -1; }
32
+ body[data-bot-product-ui] .bp-usage-receipts td:nth-child(2) { order:3; }
33
+ body[data-bot-product-ui] .bp-usage-receipts td:nth-child(5) { order:4; }
34
+ }
@@ -0,0 +1,140 @@
1
+ // Pure helpers for the pinned web panel surface: the same closed input check the
2
+ // Host applies, the wording for every framing verdict, and the messages for every
3
+ // way a Host can refuse. Kept free of React so both the panel and the sidebar pin
4
+ // list — and their tests — read one definition.
5
+ //
6
+ // The client check is a mirror, never the authority: it exists so a typo is
7
+ // answered while the person is still typing, and the Host re-validates every save
8
+ // with the caller's real embedding origin.
9
+
10
+ export const MAX_WEB_PANELS = 6
11
+ export const MAX_WEB_PANEL_NAME_CHARS = 40
12
+ export const MAX_WEB_PANEL_NAME_BYTES = 120
13
+ export const MAX_WEB_PANEL_URL_BYTES = 2048
14
+
15
+ // The frame keeps its page's own origin, so it may run scripts and use its own
16
+ // storage. What it never gets is a way out: no top-level navigation, no pointer
17
+ // lock, no presentation. `allow-popups` stays because a dashboard's sign-in very
18
+ // often opens one.
19
+ export const WEB_PANEL_SANDBOX = 'allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads'
20
+ export const WEB_PANEL_REFERRER_POLICY = 'no-referrer'
21
+ export const WEB_PANEL_LOAD_TIMEOUT_MS = 10_000
22
+
23
+ const CONTROL = /[\u0000-\u001f\u007f-\u009f]/
24
+
25
+ // The client bundle has no Buffer, and the byte count is only a fast "this is
26
+ // absurdly long" answer for a person still typing; the Host measures for real.
27
+ const byteLength = text => {
28
+ let size = 0
29
+ for (let index = 0; index < text.length; index++) {
30
+ const point = text.codePointAt(index)
31
+ if (point < 0x80) size += 1
32
+ else if (point < 0x800) size += 2
33
+ else if (point < 0x10000) size += 3
34
+ else { size += 4; index++ }
35
+ }
36
+ return size
37
+ }
38
+
39
+ /** Mirrors `validateWebPanelInput` on the Host. */
40
+ export function validateWebPanelDraft(input, embeddingOrigin = '') {
41
+ const name = typeof input?.name === 'string' ? input.name.trim() : ''
42
+ const raw = typeof input?.url === 'string' ? input.url.trim() : ''
43
+ if (!name || name.length > MAX_WEB_PANEL_NAME_CHARS || CONTROL.test(name)) return { ok: false, error: 'name' }
44
+ let url
45
+ try { url = new URL(raw) } catch { return { ok: false, error: 'url' } }
46
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return { ok: false, error: 'url' }
47
+ if (url.username || url.password || !url.hostname) return { ok: false, error: 'url' }
48
+ if (byteLength(raw) > MAX_WEB_PANEL_URL_BYTES) return { ok: false, error: 'url' }
49
+ if (embeddingOrigin && sameOrigin(url, embeddingOrigin)) return { ok: false, error: 'self' }
50
+ return { ok: true, name, url: url.href }
51
+ }
52
+
53
+ function sameOrigin(url, origin) {
54
+ try { return new URL(origin).origin === url.origin } catch { return false }
55
+ }
56
+
57
+ export function panelHost(url) {
58
+ try { return new URL(url).host } catch { return '' }
59
+ }
60
+
61
+ /** The client's own embedding origin: the native shell's, or the browser's. */
62
+ export function clientOrigin(location = globalThis.location) {
63
+ try { return location?.origin ?? '' } catch { return '' }
64
+ }
65
+
66
+ /** A panel framed from this app's own origin would run with the app's access. */
67
+ export function isSelfOrigin(url, origin) {
68
+ if (!origin) return false
69
+ try { return new URL(url).origin === new URL(origin).origin } catch { return false }
70
+ }
71
+
72
+ export function panelsOf(snapshot) {
73
+ const section = snapshot?.webPanels
74
+ if (!section || section.status !== 'available' || !Array.isArray(section.items)) {
75
+ return { status: section?.status === 'blocked' ? 'blocked' : 'unavailable', items: [], revision: 0, limits: null, reason: section?.reason ?? '', nextAction: section?.nextAction ?? '' }
76
+ }
77
+ const items = section.items
78
+ .filter(panel => panel && typeof panel.id === 'string' && typeof panel.url === 'string')
79
+ .map((panel, index) => ({ id: panel.id, name: typeof panel.name === 'string' ? panel.name : panel.id,
80
+ url: panel.url, host: panelHost(panel.url), position: Number.isSafeInteger(panel.position) ? panel.position : index + 1 }))
81
+ .sort((left, right) => left.position - right.position)
82
+ return { status: 'available', items, revision: Number.isSafeInteger(section.revision) ? section.revision : 0,
83
+ limits: section.limits ?? null, reason: '', nextAction: '' }
84
+ }
85
+
86
+ /** The list in a new order after moving one panel by `delta`. */
87
+ export function movedOrder(panels, id, delta) {
88
+ const index = panels.findIndex(panel => panel.id === id)
89
+ const target = index + delta
90
+ if (index < 0 || target < 0 || target >= panels.length) return null
91
+ const ids = panels.map(panel => panel.id)
92
+ ;[ids[index], ids[target]] = [ids[target], ids[index]]
93
+ return ids
94
+ }
95
+
96
+ /** What a probe verdict means, in the words the panel shows. Never a server's own text. */
97
+ export function framingMessage(result) {
98
+ if (!result || result.state === 'checking') return { tone: 'idle', text: '正在向宿主确认该页面是否允许显示…' }
99
+ if (result.framing === 'blocked') return { tone: 'blocked', text: `${result.host || '该网站'} 不允许在其他应用中显示它。可以在浏览器中打开,或换一个允许嵌入的页面。` }
100
+ if (result.framing === 'allowed') return { tone: 'ok', text: '宿主确认该页面允许在此显示。浏览器仍可能因其他设置拒绝渲染。' }
101
+ const reason = result.reason
102
+ if (reason === 'timeout') return { tone: 'unknown', text: '无法确认:该页面响应超时。仍会尝试显示,浏览器可能可以打开它。' }
103
+ if (reason === 'redirect') return { tone: 'unknown', text: '无法确认:该页面会跳转,可能是浏览器先登录后才看到的页面。' }
104
+ if (reason === 'bad-status') return { tone: 'unknown', text: '无法确认:该页面没有返回可显示的内容。' }
105
+ if (reason === 'unreachable') return { tone: 'unknown', text: '无法确认:宿主暂时连不上该页面。仍会尝试显示。' }
106
+ if (reason === 'aborted') return { tone: 'idle', text: '' }
107
+ if (['invalid-url', 'unsupported-scheme', 'credentials-in-url'].includes(reason)) return { tone: 'blocked', text: '该地址不能作为网页面板:只支持不带登录信息的 http/https 地址。' }
108
+ return { tone: 'unknown', text: '无法确认该页面是否允许显示。仍会尝试显示。' }
109
+ }
110
+
111
+ /** A Host refusal, in the words the panel shows. Codes are the contract, not the text. */
112
+ export function actionError(error) {
113
+ const code = error?.code ?? ''
114
+ const text = ({
115
+ WEB_PANEL_NAME: '名称需为 1–40 个字符。',
116
+ WEB_PANEL_URL: '请填写完整的 http/https 地址,且不要带登录信息。',
117
+ WEB_PANEL_SELF_URL: '不能固定应用自身的页面。',
118
+ WEB_PANEL_LIMIT: `最多固定 ${MAX_WEB_PANELS} 个网页面板。`,
119
+ WEB_PANEL_NOT_FOUND: '该固定页面已不存在,列表已刷新。',
120
+ WEB_PANEL_REVISION_CONFLICT: '列表刚刚在别处被改过,已刷新;请重新操作。',
121
+ WEB_PANEL_ORDER_INVALID: '排序与当前列表不一致,已刷新;请重新排序。',
122
+ WEB_PANEL_INPUT: '请求格式不正确。',
123
+ WEB_PANEL_READBACK_MISMATCH: '宿主未确认这次修改,请刷新后重试。',
124
+ WEB_PANEL_PROBE_BUSY: '正在检测其他页面,请稍候再试。',
125
+ WEB_PANEL_STORE_CLOSED: '宿主正在关闭,请刷新后重试。',
126
+ REMOTE_OWNER_REQUIRED: '请重新登录宿主拥有者账号。',
127
+ PRODUCT_READ_ONLY: '宿主目前处于只读状态,暂不能修改。',
128
+ PRODUCT_ACTION_UNAVAILABLE: '当前宿主尚未接入固定网页面板,请更新宿主。',
129
+ })[code]
130
+ return { code, text: text ?? '宿主请求未完成,请刷新后重试。' }
131
+ }
132
+
133
+ /**
134
+ * Open a page in the user's own browser. Only ever called from a click, in a new
135
+ * browsing context, with no handle back to this window and no referrer sent.
136
+ */
137
+ export function openExternally(url, open = globalThis.open) {
138
+ if (typeof open !== 'function') return false
139
+ try { return open(url, '_blank', 'noopener,noreferrer') !== null } catch { return false }
140
+ }