@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
@@ -4,11 +4,14 @@ import { connectDesktopStream } from './desktop-stream.mjs'
4
4
  import { createAcknowledgedRoutineRecorder } from './routine-recorder.mjs'
5
5
  import { RoutineCaptureControls } from './routines-panel.jsx'
6
6
  import { UiIcon } from './ui-icons.jsx'
7
+ import { DesktopTools } from './desktop-tools.jsx'
8
+ import { sendDesktopKeys, sendDesktopText } from './desktop-keyboard.mjs'
9
+ import { createDesktopTouchTracker } from './desktop-touch.mjs'
7
10
 
8
11
  const rootOf = machine => typeof machine?.roots?.[0] === 'string' ? machine.roots[0] : machine?.rootId ?? machine?.roots?.[0]?.id ?? ''
9
12
  const STATUS = { idle: '未连接', connecting: '连接中', authenticating: '验证中', connected: '已连接', releasing: '归还控制中', disconnected: '已断开', error: '连接异常' }
10
13
 
11
- export function DesktopViewer({ botId, machines, openSession, renewSession, closeSession, saveRoutine }) {
14
+ export function DesktopViewer({ botId, machines, openSession, renewSession, closeSession, saveRoutine, transport }) {
12
15
  const [machineId, setMachineId] = useState(machines[0]?.machineId ?? machines[0]?.id ?? '')
13
16
  const [displayId, setDisplayId] = useState('')
14
17
  const [displays, setDisplays] = useState([])
@@ -16,31 +19,44 @@ export function DesktopViewer({ botId, machines, openSession, renewSession, clos
16
19
  const [expanded, setExpanded] = useState(false)
17
20
  const [stats, setStats] = useState({ fps: 0, width: 0, height: 0 })
18
21
  const [text, setText] = useState('')
22
+ const [toolsOpen,setToolsOpen]=useState(false),[keyboardBusy,setKeyboardBusy]=useState(false),[keyboardError,setKeyboardError]=useState('')
23
+ const [fit,setFit]=useState(true),[fullscreen,setFullscreen]=useState(false)
19
24
  const [metadata, setMetadata] = useState({})
20
25
  const recorderRef = useRef(null)
21
26
  if (!recorderRef.current) recorderRef.current = createAcknowledgedRoutineRecorder()
22
27
  const recorder = recorderRef.current
23
28
  const canvasRef = useRef(null), largeRef = useRef(null), streamRef = useRef(null), ticketRef = useRef(null)
29
+ const pendingStop=useRef(null),stopAttempt=useRef(null)
30
+ const connecting=useRef(false)
24
31
  const generation = useRef(0), pendingFrame = useRef(null), drawing = useRef(false)
25
32
  const surface = useRef(null)
26
33
  const size = useRef({ width: 0, height: 0 }), rate = useRef({ at: performance.now(), count: 0 })
27
34
  const renewTimer = useRef(null), moveTimer = useRef(null), pendingMove = useRef(null)
28
35
  const [canControl, setCanControl] = useState(false)
36
+ const touches=useRef(null);if(!touches.current)touches.current=createDesktopTouchTracker()
37
+ const viewerRef=useRef(null),dialogRef=useRef(null),expandButton=useRef(null),toolsButton=useRef(null),keyboardPending=useRef(false),stateRef=useRef(state)
38
+ stateRef.current=state
29
39
  const closeRef = useRef(closeSession)
30
40
  closeRef.current = closeSession
31
41
  useEffect(() => { if (!machineId && machines[0]) setMachineId(machines[0].machineId ?? machines[0].id) }, [machines, machineId])
32
- const release = useCallback(() => { pendingMove.current = null; void streamRef.current?.releaseInput()?.catch(() => {}) }, [])
42
+ const release = useCallback((options={}) => {if(!options.keepTouches)touches.current.clear(); pendingMove.current = null; void streamRef.current?.releaseInput()?.catch(() => {}) }, [])
33
43
  const stop = useCallback(async () => {
34
44
  recorder.invalidate('ROUTINE_DESKTOP_DISCONNECTED')
35
45
  generation.current++; clearTimeout(renewTimer.current); renewTimer.current = null
46
+ setKeyboardBusy(false);keyboardPending.current=false
36
47
  if (moveTimer.current !== null) cancelAnimationFrame(moveTimer.current)
37
48
  moveTimer.current = null; pendingMove.current = null; pendingFrame.current = null
49
+ touches.current.clear()
38
50
  size.current = { width: 0, height: 0 }
39
51
  streamRef.current?.close(); streamRef.current = null
40
52
  const ticket = ticketRef.current; ticketRef.current = null
41
- if (ticket) {
42
- try { return await closeRef.current(botId, ticket.sessionId) }
43
- catch { return { stopConfirmed: false } }
53
+ if(ticket)pendingStop.current={botId,sessionId:ticket.sessionId,close:closeRef.current}
54
+ if(pendingStop.current){
55
+ if(stopAttempt.current)return stopAttempt.current
56
+ const target=pendingStop.current
57
+ const attempt=(async()=>{try{const result=await target.close(target.botId,target.sessionId);if(result?.stopConfirmed===true&&pendingStop.current===target)pendingStop.current=null;return result}catch{return {stopConfirmed:false}}})()
58
+ stopAttempt.current=attempt
59
+ try{return await attempt}finally{if(stopAttempt.current===attempt)stopAttempt.current=null}
44
60
  }
45
61
  return { stopConfirmed: true }
46
62
  }, [botId])
@@ -88,18 +104,25 @@ export function DesktopViewer({ botId, machines, openSession, renewSession, clos
88
104
  finally { drawing.current = false }
89
105
  }, [])
90
106
  const connect = useCallback(async () => {
91
- await stop()
107
+ if(connecting.current)return
108
+ connecting.current=true
109
+ try{
110
+ const stopped=await stop()
111
+ if(stopped?.stopConfirmed!==true){setState({status:'error',mode:'watch',error:'上一桌面尚未确认释放输入。请点击“断开”核对原会话,再连接电脑。'});return}
92
112
  const active = generation.current
93
113
  setState({ status: 'connecting', mode: 'watch' }); setStats({ fps: 0, width: 0, height: 0 })
94
114
  try {
95
115
  const ticket = await openSession(botId, machineId, { ...(displayId ? { displayId } : {}), mode: 'watch', fps: 15 })
96
- if (active !== generation.current) { await closeSession(botId, ticket.sessionId); return }
116
+ if (active !== generation.current) {pendingStop.current={botId,sessionId:ticket.sessionId,close:closeSession};const closed=await stop();if(closed?.stopConfirmed!==true)setState({status:'error',mode:'watch',error:'已取消连接,远端释放尚未确认,请核对原会话。'});return }
97
117
  ticketRef.current = ticket; setCanControl(ticket.canControl === true)
98
118
  if (ticket.displays?.length) setDisplays(ticket.displays)
99
- streamRef.current = connectDesktopStream({ ticket, onFrame: draw,
119
+ streamRef.current = connectDesktopStream({ ticket, botId, transport, onFrame: draw,
100
120
  onBeforeInput: (event, meta) => recorder.inputSent(event, meta),
101
121
  onInputAcknowledged: (event, meta) => recorder.inputAcknowledged(event, meta),
102
122
  onInputFailure: (event, meta) => recorder.inputFailed(event, meta),
123
+ onBeforeRelease: meta=>recorder.releaseSent(meta),
124
+ onReleaseAcknowledged: meta=>recorder.releaseAcknowledged(meta),
125
+ onReleaseFailure: meta=>recorder.releaseFailed(meta),
103
126
  onMetadata: value => {
104
127
  if (active !== generation.current) return
105
128
  if (value.displays) setDisplays(value.displays)
@@ -121,13 +144,34 @@ export function DesktopViewer({ botId, machines, openSession, renewSession, clos
121
144
  }
122
145
  renew()
123
146
  } catch (failure) { if (active === generation.current) setState({ status: 'error', mode: 'watch', error: String(failure?.message ?? failure) }) }
124
- }, [stop, botId, machineId, displayId, openSession, closeSession, renewSession, draw])
147
+ }finally{connecting.current=false}
148
+ }, [stop, botId, machineId, displayId, openSession, closeSession, renewSession, draw, transport])
125
149
  useEffect(() => { return () => { void stop() } }, [stop])
150
+ useEffect(()=>{const update=()=>setFullscreen(Boolean(document.fullscreenElement&&viewerRef.current?.contains(document.fullscreenElement)));document.addEventListener('fullscreenchange',update);return()=>document.removeEventListener('fullscreenchange',update)},[])
151
+ useEffect(()=>{
152
+ if(!expanded)return
153
+ if(surface.current&&largeRef.current){largeRef.current.width=surface.current.width;largeRef.current.height=surface.current.height;largeRef.current.getContext('2d')?.drawImage(surface.current,0,0)}
154
+ const previous=document.activeElement,overflow=document.body.style.overflow
155
+ document.body.style.overflow='hidden';dialogRef.current?.querySelector('button')?.focus()
156
+ return()=>{if(document.body.style.overflow==='hidden')document.body.style.overflow=overflow;if(previous?.isConnected)previous.focus()}
157
+ },[expanded])
126
158
  useEffect(() => {
127
159
  const lost = () => release()
128
160
  window.addEventListener('blur', lost); document.addEventListener('visibilitychange', lost)
129
161
  return () => { window.removeEventListener('blur', lost); document.removeEventListener('visibilitychange', lost) }
130
162
  }, [release])
163
+ useEffect(()=>{
164
+ const wheel=event=>{
165
+ if(stateRef.current.mode!=='control')return
166
+ event.preventDefault()
167
+ const scale=event.deltaMode===1?16:event.deltaMode===2?window.innerHeight:1
168
+ const deltaX=Math.max(-4800,Math.min(4800,Math.round(event.deltaX*scale))),deltaY=Math.max(-4800,Math.min(4800,Math.round(event.deltaY*scale)))
169
+ if(deltaX||deltaY)streamRef.current?.input({type:'wheel',deltaX,deltaY})
170
+ }
171
+ const canvases=[canvasRef.current,largeRef.current].filter(Boolean)
172
+ for(const canvas of canvases)canvas.addEventListener('wheel',wheel,{passive:false})
173
+ return()=>{for(const canvas of canvases)canvas.removeEventListener('wheel',wheel)}
174
+ },[expanded])
131
175
  const point = event => {
132
176
  const rect = event.currentTarget.getBoundingClientRect()
133
177
  try { return framePoint({ clientX: event.clientX - rect.left, clientY: event.clientY - rect.top, rect, frame: size.current }) }
@@ -135,53 +179,78 @@ export function DesktopViewer({ botId, machines, openSession, renewSession, clos
135
179
  }
136
180
  const pointer = (event, action) => {
137
181
  if (state.mode !== 'control') return
138
- const position = point(event); if (!position) return
182
+ if(action==='down'&&event.pointerType==='touch')try{event.currentTarget.setPointerCapture?.(event.pointerId)}catch{}
183
+ const gesture=touches.current.handle(action,event)
184
+ if(gesture.handled){event.preventDefault();if(gesture.release)release({keepTouches:true});if(gesture.wheel)streamRef.current?.input(gesture.wheel);return}
185
+ const position = point(event); if (!position) {if(action==='up'){event.preventDefault();release()};return}
139
186
  event.preventDefault()
140
187
  const packet = { type: 'pointer', action, ...position, button: ['left', 'middle', 'right'][event.button] ?? 'left' }
141
188
  if (action === 'move') {
142
189
  pendingMove.current = packet
143
190
  if (moveTimer.current === null) moveTimer.current = requestAnimationFrame(() => { moveTimer.current = null; if (pendingMove.current) streamRef.current?.input(pendingMove.current); pendingMove.current = null })
144
191
  } else {
145
- if (action === 'down') { event.currentTarget.focus({ preventScroll: true }); event.currentTarget.setPointerCapture?.(event.pointerId) }
192
+ if(moveTimer.current!==null){cancelAnimationFrame(moveTimer.current);moveTimer.current=null;if(pendingMove.current)streamRef.current?.input(pendingMove.current);pendingMove.current=null}
193
+ if (action === 'down') { event.currentTarget.focus({ preventScroll: true });try{event.currentTarget.setPointerCapture?.(event.pointerId)}catch{} }
146
194
  streamRef.current?.input(packet)
147
195
  }
148
196
  }
149
197
  const keyboard = (event, action) => {
150
- if (state.mode !== 'control' || event.isComposing) return
198
+ if (state.mode !== 'control' || event.isComposing || event.nativeEvent?.isComposing || event.keyCode===229) return
199
+ if((event.ctrlKey||event.metaKey)&&event.key.toLowerCase()==='v'){event.stopPropagation();return}
151
200
  event.preventDefault(); event.stopPropagation()
152
201
  const input = desktopKeyboardInput(event, action)
153
202
  if (input) streamRef.current?.input(input)
154
203
  }
155
- const screen = (ref, big = false) => <canvas ref={ref} tabIndex={0} aria-label="远程桌面,取得控制权后可用鼠标键盘操作" className={big ? 'bp-stream-canvas bp-stream-large' : 'bp-stream-canvas'}
204
+ const keyboardAction=async work=>{
205
+ if(keyboardPending.current||stateRef.current.status!=='connected'||stateRef.current.mode!=='control')return
206
+ const stream=streamRef.current,active=generation.current;keyboardPending.current=true;setKeyboardBusy(true);setKeyboardError('')
207
+ const current=()=>active===generation.current&&stream===streamRef.current&&stateRef.current.mode==='control'
208
+ try{await work(stream,current)}catch(failure){if(active===generation.current)setKeyboardError(`远端尚未确认这次输入:${failure.message}。请检查电脑,不会自动重发。`)}
209
+ finally{if(active===generation.current){keyboardPending.current=false;setKeyboardBusy(false)}}
210
+ }
211
+ const submitText=()=>{const value=text;void keyboardAction(async(stream,current)=>{await sendDesktopText(stream,value,current);if(current())setText(before=>before===value?'':before)})}
212
+ const localPaste=async()=>{const active=generation.current;try{if(!navigator.clipboard?.readText)throw new Error('请直接粘贴到上方文本框。');const value=await navigator.clipboard.readText();if(active!==generation.current)return;if(value.length>8192)throw new Error('本机文字超过 8192 字,请分段粘贴。');setText(value);setKeyboardError('')}catch(failure){if(active===generation.current)setKeyboardError(`无法读取本机剪贴板:${failure.message}`)}}
213
+ const closeTools=()=>{setToolsOpen(false);toolsButton.current?.focus()}
214
+ const toggleFullscreen=async()=>{try{if(document.fullscreenElement)await document.exitFullscreen();else await (dialogRef.current??viewerRef.current).requestFullscreen()}catch{setKeyboardError('无法进入系统全屏,请使用放大视图。');setToolsOpen(true)}}
215
+ const tools=<DesktopTools open={toolsOpen} onClose={closeTools} connected={state.status==='connected'} canInput={state.mode==='control'} text={text} onText={setText} onSend={submitText} onPaste={()=>{void localPaste()}}
216
+ onShortcut={keys=>{void keyboardAction((stream,current)=>sendDesktopKeys(stream,keys,current))}} working={keyboardBusy} error={keyboardError}/>
217
+ const screen = (ref, big = false) => <canvas ref={ref} tabIndex={0} aria-label="远程桌面,取得控制权后可用鼠标键盘操作" data-control={state.mode==='control'} className={big ? 'bp-stream-canvas bp-stream-large' : 'bp-stream-canvas'}
218
+ style={!fit&&size.current.width?{width:size.current.width,maxWidth:'none',maxHeight:'none'}:undefined}
156
219
  onPointerMove={event => pointer(event, 'move')} onPointerDown={event => pointer(event, 'down')} onPointerUp={event => pointer(event, 'up')}
157
220
  onPointerCancel={release} onLostPointerCapture={event => { if (event.buttons) release() }} onBlur={release} onContextMenu={event => event.preventDefault()}
221
+ onPointerLeave={event=>{if(event.buttons&&!event.currentTarget.hasPointerCapture?.(event.pointerId))release()}}
158
222
  onKeyDown={event => keyboard(event, 'down')} onKeyUp={event => keyboard(event, 'up')}
159
- onPaste={event => { if (state.mode === 'control') { event.preventDefault(); streamRef.current?.input({ type: 'text', text: event.clipboardData.getData('text/plain') }) } }}
160
- onWheel={event => { if (state.mode === 'control') { event.preventDefault(); streamRef.current?.input({ type: 'wheel', deltaX: Math.round(event.deltaX), deltaY: Math.round(event.deltaY) }) } }} />
223
+ onPaste={event => { if (state.mode === 'control') { event.preventDefault();const value=event.clipboardData.getData('text/plain');void keyboardAction((stream,current)=>sendDesktopText(stream,value,current)) } }}
224
+ />
161
225
  const controls = <div className="bp-actions">
162
226
  <button disabled={state.status !== 'connected' || state.mode === 'watch'} onClick={() => { release(); streamRef.current?.setMode('watch') }}>仅观看</button>
163
- <button disabled={state.status !== 'connected' || state.mode === 'control' || !canControl} onClick={() => streamRef.current?.setMode('control')}>取得控制权</button>
164
- <span role="status">{state.mode === 'control' ? '你正在控制这台电脑' : '仅观看'}</span></div>
165
- return <section className="bp-desktop-viewer"><div className="bp-card-title"><h3><UiIcon name="computer" size={16} /> 远程桌面</h3><span className="bp-desktop-status" data-connected={state.status === 'connected'}>{STATUS[state.status] ?? state.status}</span></div>
166
- <div className="bp-actions"><select aria-label="远程桌面电脑" value={machineId} onChange={event => { void stop(); setMachineId(event.target.value); setDisplayId(''); setState({ status: 'idle', mode: 'watch' }) }}>
227
+ <button className={state.mode==='control'?'':'bp-button-primary'} disabled={state.status !== 'connected' || state.mode === 'control' || !canControl} onClick={() => streamRef.current?.setMode('control')}>取得控制权</button>
228
+ <span className="bp-desktop-control-status" data-mode={state.mode} role="status">{state.mode === 'control' ? '你正在控制这台电脑' : '仅观看'}</span></div>
229
+ const toolbar=<div className="bp-desktop-toolbar">{controls}<div className="bp-desktop-view-tools"><button ref={toolsButton} type="button" aria-label="键盘与文字" aria-expanded={toolsOpen} onClick={()=>{release();setToolsOpen(value=>!value)}}><UiIcon name="keyboard"/></button>
230
+ <button type="button" aria-label={fit?'原始尺寸':'适应窗口'} title={fit?'原始尺寸':'适应窗口'} aria-pressed={!fit} onClick={()=>setFit(value=>!value)}><UiIcon name="fit"/></button>
231
+ {typeof document.documentElement.requestFullscreen==='function'&&<button type="button" aria-label={fullscreen?'退出系统全屏':'系统全屏'} title={fullscreen?'退出系统全屏':'系统全屏'} onClick={()=>{void toggleFullscreen()}}><UiIcon name="expand"/></button>}</div></div>
232
+ return <section ref={viewerRef} className="bp-desktop-viewer"><div className="bp-card-title"><h3><UiIcon name="computer" size={16} /> 远程桌面</h3><span className="bp-desktop-status" data-connected={state.status === 'connected'}>{STATUS[state.status] ?? state.status}</span></div>
233
+ <div className="bp-actions"><select aria-label="远程桌面电脑" value={machineId} onChange={event => { void stop(); setMachineId(event.target.value);setText('');setToolsOpen(false);setKeyboardError(''); setDisplayId(''); setState({ status: 'idle', mode: 'watch' }) }}>
167
234
  {machines.map(item => <option key={item.machineId ?? item.id} value={item.machineId ?? item.id}>{item.label ?? item.name ?? item.machineId ?? item.id}</option>)}</select>
168
235
  {displays.length > 1 && <select aria-label="远程显示器" value={displayId} onChange={event => { void stop(); setDisplayId(event.target.value); setState({ status: 'idle', mode: 'watch' }) }}>
169
236
  <option value="">主显示器</option>{displays.map(display => <option key={display.id ?? display.displayId} value={display.id ?? display.displayId}>{display.name ?? display.id ?? display.displayId}</option>)}</select>}
170
237
  <button className="bp-button-primary" disabled={!machineId || !botId || ['connecting', 'authenticating'].includes(state.status)} onClick={() => { void connect() }}>{state.status === 'connected' ? '重新连接' : '连接桌面'}</button>
171
- <button disabled={!ticketRef.current} onClick={() => { void stop().then(result => setState(result?.stopConfirmed === true ? { status: 'idle', mode: 'watch' }
238
+ <button disabled={!ticketRef.current&&!pendingStop.current} onClick={() => { void stop().then(result => setState(result?.stopConfirmed === true ? { status: 'idle', mode: 'watch' }
172
239
  : { status: 'error', mode: 'watch', error: '画面连接已关闭,但远端尚未确认释放输入,请检查远程电脑。' })) }}>断开</button></div>
173
- {state.error && <p className="bp-error" role="alert">{state.error}</p>}{controls}
174
- <div className="bp-stream-screen" data-connected={state.status === 'connected'} onDoubleClick={() => { if (state.mode === 'watch') setExpanded(true) }}>{screen(canvasRef)}
240
+ {state.error && <p className="bp-error" role="alert">{state.error}</p>}{!expanded&&toolbar}
241
+ <div className="bp-stream-screen" data-fit={fit} data-connected={state.status === 'connected'} onDoubleClick={() => { if (state.mode === 'watch') setExpanded(true) }}>{screen(canvasRef)}
175
242
  {state.status !== 'connected' && <div className="bp-desktop-idle"><UiIcon name="computer" size={34} /><strong>{['connecting', 'authenticating'].includes(state.status) ? '正在连接你的电脑…' : '你的电脑,就在这里'}</strong><span>连接后查看实时桌面,或取得鼠标键盘控制权</span></div>}
176
243
  </div>
177
- <div className="bp-actions"><small>{stats.width ? `${stats.width} × ${stats.height} · ${stats.fps} 帧/秒${stats.frameWidth && stats.frameWidth !== stats.width ? ` · 传输 ${stats.frameWidth} × ${stats.frameHeight}` : ''}` : '连接后显示整台电脑的桌面。'}</small><button onClick={() => setExpanded(true)}>放大</button></div>
178
- {state.mode === 'control' && <div className="bp-actions"><input aria-label="发送到远程电脑的文字" value={text} onChange={event => setText(event.target.value)} placeholder="中文或多行文字" />
179
- <button disabled={!text} onClick={() => { if (streamRef.current?.input({ type: 'text', text })) setText('') }}>发送文字</button></div>}
244
+ <div className="bp-actions"><small>{stats.width ? `${stats.width} × ${stats.height} · ${stats.fps} 帧/秒${stats.frameWidth && stats.frameWidth !== stats.width ? ` · 传输 ${stats.frameWidth} × ${stats.frameHeight}` : ''}` : '连接后显示整台电脑的桌面。'}</small><button ref={expandButton} onClick={() => setExpanded(true)}><UiIcon name="expand" size={15}/>放大</button></div>
245
+ {state.mode==='control'&&<p className="bp-desktop-touch-hint">手机单指操作,双指滚动;离开画面会释放按键和鼠标。</p>}
246
+ {!expanded&&tools}
180
247
  {typeof saveRoutine === 'function' && <RoutineCaptureControls recorder={recorder} sessionId={ticketRef.current?.sessionId} metadata={metadata}
181
248
  mode={state.mode} connected={state.status === 'connected'} drainInputs={() => streamRef.current?.drain()}
182
249
  releaseInput={() => streamRef.current?.releaseInput()} saveRoutine={routine => saveRoutine(botId, routine)} />}
183
- {expanded && <div className="bp-stream-overlay" role="dialog" aria-modal="true" aria-label="远程桌面放大"><div className="bp-stream-dialog"><div className="bp-actions">{controls}
184
- <button onClick={() => { release(); setExpanded(false) }}>关闭放大</button></div>{screen(largeRef, true)}</div></div>}
250
+ {expanded && <div className="bp-stream-overlay" role="dialog" aria-modal="true" aria-label="远程桌面放大" ref={dialogRef} onKeyDown={event=>{
251
+ if(event.key==='Escape'&&!event.nativeEvent.isComposing){event.preventDefault();event.stopPropagation();release();setToolsOpen(false);setExpanded(false)}
252
+ if(event.key==='Tab'){const focusable=[...dialogRef.current.querySelectorAll('button:not(:disabled),textarea,canvas,[tabindex="0"]')];const first=focusable[0],last=focusable.at(-1);if(event.shiftKey&&document.activeElement===first){event.preventDefault();last?.focus()}else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first?.focus()}}
253
+ }}><div className="bp-stream-dialog"><header className="bp-desktop-expanded-header"><strong><UiIcon name="computer"/>{machines.find(item=>(item.machineId??item.id)===machineId)?.label??'远程电脑'}</strong><span className="bp-desktop-status" data-connected={state.status==='connected'}>{STATUS[state.status]??state.status}</span><button onClick={() => { release();setToolsOpen(false);setExpanded(false) }}>关闭放大</button></header>{toolbar}<div className="bp-desktop-expanded-stage" data-fit={fit}>{screen(largeRef, true)}{tools}</div></div></div>}
185
254
  </section>
186
255
  }
187
256
 
@@ -192,10 +261,10 @@ export function DesktopPane({ bot }) {
192
261
  if (!api?.open) return <p className="bp-muted">桌面通道尚未就绪。</p>
193
262
  if (error) return <p role="alert">{error}</p>
194
263
  if (!machines.length) return <p className="bp-muted">在“远程电脑”中配对电脑并为这个 Bot 授权。</p>
195
- return <DesktopViewer key={bot.id} botId={bot.id} machines={machines} openSession={api.open} renewSession={api.renew} closeSession={api.close} saveRoutine={api.saveRoutine} />
264
+ return <DesktopViewer key={bot.id} botId={bot.id} machines={machines} openSession={api.open} renewSession={api.renew} closeSession={api.close} saveRoutine={api.saveRoutine} transport={api.transport} />
196
265
  }
197
266
 
198
- export function DesktopWorkbench({ snapshot, dispatch }) {
267
+ export function DesktopWorkbench({ snapshot, dispatch, transport }) {
199
268
  const bots = snapshot.bots ?? [], [botId, setBotId] = useState(bots[0]?.id ?? '')
200
269
  const machines = (snapshot.machines?.items ?? []).filter(item => item.status !== 'revoked')
201
270
  const current = useRef({ dispatch, machines }); current.current = { dispatch, machines }
@@ -205,5 +274,5 @@ export function DesktopWorkbench({ snapshot, dispatch }) {
205
274
  const saveRoutine = useCallback((id, routine) => current.current.dispatch('routine.save', { botId: id, routine }), [])
206
275
  if (!machines.length) return <section className="bp-card"><div className="bp-empty-state"><span className="bp-empty-icon"><UiIcon name="computer" size={29} /></span><strong>连接你的第一台电脑</strong><p>在自己的 Windows 或云电脑上运行 Agent,再导入配对文件,就能在这里查看和接管桌面。</p><div className="bp-actions"><a className="bp-download-link" href="https://github.com/Alice-Marx/dsh-model-router/releases/download/v0.63.0/dsh-remote-agent-0.1.0-windows-x64.zip" target="_blank" rel="noopener noreferrer"><UiIcon name="arrow" size={15} />下载 Windows Agent</a><button className="bp-button-primary" onClick={() => { const setup = document.querySelector('.bp-machine-setup'); if (setup) { setup.open = true; setup.scrollIntoView({ block: 'start', behavior: 'smooth' }) } }}>导入配对文件</button></div></div></section>
207
276
  return <section className="bp-card"><div className="bp-actions" style={{ marginBottom: 20 }}><label>连接身份<select value={botId} onChange={event => setBotId(event.target.value)}>{bots.map(bot => <option key={bot.id} value={bot.id}>{bot.name ?? bot.id}</option>)}</select></label></div>
208
- <DesktopViewer key={botId} botId={botId} machines={machines} openSession={openSession} renewSession={renewSession} closeSession={closeSession} saveRoutine={saveRoutine} /></section>
277
+ <DesktopViewer key={botId} botId={botId} machines={machines} openSession={openSession} renewSession={renewSession} closeSession={closeSession} saveRoutine={saveRoutine} transport={transport} /></section>
209
278
  }
@@ -1,8 +1,14 @@
1
1
  /** The viewer never receives a pairing secret. Its ticket belongs to one authenticated desktop session. */
2
- export function connectDesktopStream({ ticket, onFrame, onState, onMetadata, onBeforeInput, onInputAcknowledged, onInputFailure,
2
+ export function connectDesktopStream({ ticket, botId, transport, onFrame, onState, onMetadata, onBeforeInput, onInputAcknowledged, onInputFailure,onBeforeRelease,onReleaseAcknowledged,onReleaseFailure,
3
3
  WebSocketImpl = globalThis.WebSocket, ackTimeoutMs = 10000, now = Date.now }) {
4
- if (!ticket?.websocketUrl || typeof WebSocketImpl !== 'function') throw new Error('桌面连接不可用。')
5
- const socket = new WebSocketImpl(ticket.websocketUrl)
4
+ let websocketUrl=ticket?.websocketUrl
5
+ if(ticket?.websocketPath){
6
+ if(ticket.websocketPath!=='/bot-product/desktop/v1/socket'||!['http:','https:'].includes(globalThis.location?.protocol))throw new Error('此桌面连接需要已登录的网页宿主。')
7
+ const endpoint=new URL(ticket.websocketPath,globalThis.location.href);endpoint.protocol=endpoint.protocol==='https:'?'wss:':'ws:';websocketUrl=endpoint.href
8
+ }
9
+ if (ticket?.carrier!=='owner-http'&&(!websocketUrl || typeof WebSocketImpl !== 'function')) throw new Error('桌面连接不可用。')
10
+ if(ticket?.carrier==='owner-http'&&typeof transport!=='function')throw new Error('宿主桌面数据通道尚未就绪。')
11
+ const socket = ticket?.carrier==='owner-http'?createOwnerDesktopSocket({ticket,botId,dispatch:transport}):new WebSocketImpl(websocketUrl)
6
12
  socket.binaryType = 'arraybuffer'
7
13
  let closed = false, mode = 'watch', desiredMode = ticket.mode === 'control' ? 'control' : 'watch', nextId = 0
8
14
  let lastError = ''
@@ -20,6 +26,7 @@ export function connectDesktopStream({ ticket, onFrame, onState, onMetadata, onB
20
26
  if (!pending.delete(row.id)) return
21
27
  clearTimeout(row.timer)
22
28
  if (row.event) onInputFailure?.(row.event, { ...row.meta, failure })
29
+ else onReleaseFailure?.({...row.meta,failure})
23
30
  row.reject(Object.assign(new Error(failure.code), failure))
24
31
  }
25
32
  const failPending = code => { for (const row of [...pending.values()]) failed(row, { code, possiblyApplied: true }) }
@@ -36,6 +43,7 @@ export function connectDesktopStream({ ticket, onFrame, onState, onMetadata, onB
36
43
  pending.set(id, row)
37
44
  try {
38
45
  if (event) onBeforeInput?.(event, meta)
46
+ else onBeforeRelease?.(meta)
39
47
  if (!send({ type, id, ...(event ? { event } : {}) })) throw Object.assign(new Error('DESKTOP_DISCONNECTED'), { code: 'DESKTOP_DISCONNECTED' })
40
48
  } catch (error) { failed(row, { code: error.code ?? 'DESKTOP_DISCONNECTED', possiblyApplied: false }); return null }
41
49
  return promise
@@ -55,6 +63,7 @@ export function connectDesktopStream({ ticket, onFrame, onState, onMetadata, onB
55
63
  }
56
64
  pending.delete(row.id); clearTimeout(row.timer)
57
65
  if (row.event) onInputAcknowledged?.(row.event, { ...row.meta, acknowledgedAt: now() })
66
+ else onReleaseAcknowledged?.({...row.meta,acknowledgedAt:now(),ok:true})
58
67
  row.resolve({ ok: true, id: row.id }); return
59
68
  }
60
69
  if (value.type === 'ready') {
@@ -86,3 +95,4 @@ export function connectDesktopStream({ ticket, onFrame, onState, onMetadata, onB
86
95
  close() { if (closed) return; send({ type: 'close' }); closed = true; mode = 'watch'; failPending('DESKTOP_DISCONNECTED'); socket.close() },
87
96
  })
88
97
  }
98
+ import { createOwnerDesktopSocket } from './desktop-http-socket.mjs'
@@ -0,0 +1,19 @@
1
+ import React,{useEffect,useRef} from 'react'
2
+ import {DESKTOP_SHORTCUTS} from './desktop-keyboard.mjs'
3
+ import {UiIcon} from './ui-icons.jsx'
4
+
5
+ export function DesktopTools({open,onClose,connected,canInput,text,onText,onSend,onPaste,onShortcut,working,error}){
6
+ const panel=useRef(null)
7
+ useEffect(()=>{if(open)panel.current?.querySelector('textarea')?.focus()},[open])
8
+ if(!open)return null
9
+ return <section className="bp-desktop-tools-panel" ref={panel} aria-label="远程键盘与文字" onKeyDown={event=>{
10
+ if(event.key==='Escape'&&!event.nativeEvent.isComposing&&event.nativeEvent.keyCode!==229){event.preventDefault();event.stopPropagation();onClose()}
11
+ }}><header><strong><UiIcon name="keyboard" size={17}/>键盘与文字</strong><button type="button" onClick={onClose} aria-label="关闭键盘面板"><UiIcon name="close"/></button></header>
12
+ <label>发送到远程电脑<textarea rows={4} maxLength={8192} value={text} onChange={event=>onText(event.target.value)} placeholder="中文、多行文字或本机复制的内容" autoComplete="off" spellCheck={false}/></label>
13
+ <div className="bp-actions"><button type="button" className="bp-button-primary" disabled={!connected||!canInput||!text||working} onClick={onSend}>{working?'等待远端确认…':'发送文字'}</button><button type="button" disabled={working} onClick={onPaste}>读取本机剪贴板</button></div>
14
+ {!canInput&&<p className="bp-muted">取得控制权后可以发送文字和快捷键。</p>}
15
+ {error&&<p className="bp-error" role="alert">{error}</p>}
16
+ <div className="bp-desktop-shortcuts" role="group" aria-label="远程快捷键">{DESKTOP_SHORTCUTS.map(row=><button key={row.label} type="button" disabled={!connected||!canInput||working} onClick={()=>onShortcut(row.keys)}>{row.label}</button>)}</div>
17
+ <p className="bp-muted">本机文字会以键盘输入发送;快捷键“复制 / 粘贴”操作的是远端剪贴板。</p>
18
+ </section>
19
+ }
@@ -0,0 +1,27 @@
1
+ /** Once a gesture has two fingers, none of its remaining contacts may resume a held mouse drag. */
2
+ export function createDesktopTouchTracker(){
3
+ const points=new Map();let multiple=false,center=null
4
+ const average=()=>{const values=[...points.values()];return {x:values.reduce((sum,item)=>sum+item.x,0)/values.length,y:values.reduce((sum,item)=>sum+item.y,0)/values.length}}
5
+ const clear=()=>{points.clear();multiple=false;center=null}
6
+ return Object.freeze({clear,handle(action,event){
7
+ if(event.pointerType!=='touch')return {handled:false}
8
+ if(!Number.isFinite(event.clientX)||!Number.isFinite(event.clientY)||!Number.isSafeInteger(event.pointerId))return {handled:true,release:true}
9
+ if(action==='down'){
10
+ points.set(event.pointerId,{x:event.clientX,y:event.clientY})
11
+ if(points.size<2&&!multiple)return {handled:false}
12
+ const release=!multiple;multiple=true;center=average();return {handled:true,release}
13
+ }
14
+ if(action==='move'){
15
+ if(!points.has(event.pointerId))return {handled:multiple}
16
+ points.set(event.pointerId,{x:event.clientX,y:event.clientY})
17
+ if(!multiple)return {handled:false}
18
+ if(points.size<2)return {handled:true}
19
+ const next=average(),previous=center??next;center=next
20
+ const deltaX=Math.max(-480,Math.min(480,Math.round(previous.x-next.x))),deltaY=Math.max(-480,Math.min(480,Math.round(previous.y-next.y)))
21
+ return {handled:true,...(deltaX||deltaY?{wheel:{type:'wheel',deltaX,deltaY}}:{})}
22
+ }
23
+ const handled=multiple;points.delete(event.pointerId);center=points.size>=2?average():null
24
+ if(!points.size){multiple=false;center=null}
25
+ return {handled,release:handled&&!points.size}
26
+ }})
27
+ }
@@ -0,0 +1,41 @@
1
+ .bp-desktop-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; padding: 9px; border: 1px solid var(--bp-border); border-radius: 12px; background: var(--bp-subtle); }
2
+ .bp-desktop-toolbar > .bp-actions { margin: 0; gap: 7px; flex: 1; }
3
+ .bp-desktop-view-tools { display: flex; flex: none; gap: 4px; }
4
+ .bp-desktop-viewer .bp-desktop-view-tools button { width: 36px; min-width: 36px; height: 36px; padding: 7px; }
5
+ .bp-desktop-view-tools button[aria-pressed=true],.bp-desktop-view-tools button[aria-expanded=true] { border-color: var(--bp-solid); color: var(--bp-solid); background: var(--bp-hover); }
6
+ .bp-desktop-control-status { display: inline-flex; align-items: center; gap: 6px; margin-left: 4px; font-size: 11px; color: var(--bp-muted); }
7
+ .bp-desktop-control-status[data-mode=control] { color: var(--bp-solid); }
8
+ .bp-desktop-control-status[data-mode=control]::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
9
+ .bp-desktop-touch-hint { display: none; margin: 0; color: var(--bp-muted); font-size: 11px; }
10
+ @media(pointer:coarse) { .bp-desktop-touch-hint { display: block; } }
11
+ .bp-stream-canvas { touch-action: pan-y; }
12
+ .bp-stream-canvas[data-control=true] { touch-action: none; }
13
+ .bp-stream-screen[data-fit=false],.bp-desktop-expanded-stage[data-fit=false] { overflow: auto; align-items: flex-start; justify-content: flex-start; }
14
+ .bp-stream-screen[data-fit=false] .bp-stream-canvas { flex: none; }
15
+ .bp-desktop-tools-panel { min-width: 0; padding: 16px; border: 1px solid var(--bp-border); border-radius: 14px; background: var(--bp-surface); box-shadow: 0 12px 32px #00000014; }
16
+ .bp-desktop-tools-panel > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
17
+ .bp-desktop-tools-panel strong { display: flex; align-items: center; gap: 8px; }
18
+ .bp-desktop-tools-panel > label { display: grid; gap: 8px; margin-bottom: 12px; font-size: 12px; color: var(--bp-muted); }
19
+ .bp-desktop-viewer .bp-desktop-tools-panel textarea { width: 100%; min-height: 100px; max-height: 220px; min-width: 0; padding: 10px; resize: vertical; border: 1px solid var(--bp-border); border-radius: 9px; background: var(--bp-subtle); color: var(--bp-ink); font: inherit; }
20
+ .bp-desktop-shortcuts { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 7px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--bp-border); }
21
+ .bp-desktop-shortcuts button { overflow-wrap: anywhere; }
22
+ .bp-stream-overlay { padding: 16px; background: #101714; }
23
+ .bp-desktop-viewer :is(button,input,textarea,select,canvas) { -webkit-app-region: no-drag; }
24
+ .bp-stream-dialog { height: calc(100dvh - 32px); width: 100%; max-width: 1500px; min-height: 0; --bp-subtle: #1b2420; --bp-surface: #242e28; --bp-ink: #eef4ee; --bp-muted: #abb8ad; --bp-border: #425147; --bp-hover: #35483b; --bp-solid: #9acda7; --bp-on-solid: #142319; }
25
+ .bp-desktop-expanded-header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; min-height: 40px; }
26
+ .bp-desktop-expanded-header > strong { display: flex; align-items: center; gap: 9px; min-width: 0; margin-right: auto; }
27
+ .bp-desktop-expanded-stage { position: relative; flex: 1; min-height: 0; min-width: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; border: 1px solid var(--bp-border); border-radius: 12px; background: #101612; }
28
+ .bp-desktop-expanded-stage .bp-stream-large { width: 100%; height: 100%; max-height: none; object-fit: contain; flex: none; }
29
+ .bp-desktop-expanded-stage > .bp-desktop-tools-panel { position: absolute; z-index: 1; left: 16px; top: 16px; width: min(420px,calc(100% - 32px)); max-height: calc(100% - 32px); overflow: auto; }
30
+ .bp-stream-overlay:fullscreen { padding: 12px; }
31
+ .bp-stream-overlay:fullscreen .bp-stream-dialog { height: calc(100dvh - 24px); max-width: none; }
32
+ @media(max-width:600px){
33
+ .bp-desktop-toolbar { gap: 7px; }
34
+ .bp-desktop-toolbar > .bp-actions { flex-wrap: wrap; }
35
+ .bp-desktop-control-status { width: 100%; margin: 2px 4px; }
36
+ .bp-desktop-shortcuts { grid-template-columns: repeat(2,minmax(0,1fr)); }
37
+ .bp-stream-overlay { padding: 8px; }
38
+ .bp-stream-dialog { height: calc(100dvh - 16px); max-width: 100%; gap: 8px; }
39
+ .bp-desktop-viewer button { min-height: 40px; }
40
+ .bp-desktop-expanded-stage > .bp-desktop-tools-panel { left: 8px; top: 8px; width: calc(100% - 16px); max-height: calc(100% - 16px); }
41
+ }