@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.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +20 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- 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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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();
|
|
160
|
-
|
|
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
|
-
|
|
166
|
-
<
|
|
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>}{
|
|
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)}
|
|
178
|
-
{state.mode
|
|
179
|
-
|
|
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="远程桌面放大"
|
|
184
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
+
}
|