botmux 3.14.0 → 3.16.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/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/destroy-result.d.ts +159 -0
- package/dist/adapters/backend/destroy-result.d.ts.map +1 -0
- package/dist/adapters/backend/destroy-result.js +223 -0
- package/dist/adapters/backend/destroy-result.js.map +1 -0
- package/dist/adapters/backend/herdr-backend.d.ts +4 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +23 -1
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/mojo-backend.d.ts +577 -0
- package/dist/adapters/backend/mojo-backend.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-backend.js +2471 -0
- package/dist/adapters/backend/mojo-backend.js.map +1 -0
- package/dist/adapters/backend/mojo-budgets.d.ts +21 -0
- package/dist/adapters/backend/mojo-budgets.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-budgets.js +22 -0
- package/dist/adapters/backend/mojo-budgets.js.map +1 -0
- package/dist/adapters/backend/mojo-isolated-workspace.d.ts +69 -0
- package/dist/adapters/backend/mojo-isolated-workspace.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-isolated-workspace.js +213 -0
- package/dist/adapters/backend/mojo-isolated-workspace.js.map +1 -0
- package/dist/adapters/backend/mojo-process-tree.d.ts +292 -0
- package/dist/adapters/backend/mojo-process-tree.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-process-tree.js +486 -0
- package/dist/adapters/backend/mojo-process-tree.js.map +1 -0
- package/dist/adapters/backend/mojo-types.d.ts +542 -0
- package/dist/adapters/backend/mojo-types.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-types.js +682 -0
- package/dist/adapters/backend/mojo-types.js.map +1 -0
- package/dist/adapters/backend/reproduce-command.js +3 -3
- package/dist/adapters/backend/reproduce-command.js.map +1 -1
- package/dist/adapters/backend/riff-backend.js +1 -1
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/sandbox.d.ts +44 -7
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +30 -7
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/backend/session-backend-selector.d.ts +14 -1
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +82 -10
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +30 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +60 -0
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +108 -6
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zmx-backend.d.ts +11 -0
- package/dist/adapters/backend/zmx-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zmx-backend.js +16 -1
- package/dist/adapters/backend/zmx-backend.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +2 -2
- package/dist/adapters/cli/codex-app.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +7 -0
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/copilot.d.ts +3 -1
- package/dist/adapters/cli/copilot.d.ts.map +1 -1
- package/dist/adapters/cli/copilot.js +23 -5
- package/dist/adapters/cli/copilot.js.map +1 -1
- package/dist/adapters/cli/cursor.d.ts.map +1 -1
- package/dist/adapters/cli/cursor.js +26 -4
- package/dist/adapters/cli/cursor.js.map +1 -1
- package/dist/adapters/cli/dsh.d.ts.map +1 -1
- package/dist/adapters/cli/dsh.js +5 -1
- package/dist/adapters/cli/dsh.js.map +1 -1
- package/dist/adapters/cli/grok.d.ts.map +1 -1
- package/dist/adapters/cli/grok.js +24 -5
- package/dist/adapters/cli/grok.js.map +1 -1
- package/dist/adapters/cli/kimi.d.ts.map +1 -1
- package/dist/adapters/cli/kimi.js +19 -1
- package/dist/adapters/cli/kimi.js.map +1 -1
- package/dist/adapters/cli/mojo.d.ts +28 -0
- package/dist/adapters/cli/mojo.d.ts.map +1 -0
- package/dist/adapters/cli/mojo.js +58 -0
- package/dist/adapters/cli/mojo.js.map +1 -0
- package/dist/adapters/cli/pi.d.ts +20 -5
- package/dist/adapters/cli/pi.d.ts.map +1 -1
- package/dist/adapters/cli/pi.js +27 -5
- package/dist/adapters/cli/pi.js.map +1 -1
- package/dist/adapters/cli/read-isolation.d.ts +251 -1
- package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
- package/dist/adapters/cli/read-isolation.js +254 -1
- package/dist/adapters/cli/read-isolation.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +13 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +19 -1
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/runner-input.d.ts +3 -3
- package/dist/adapters/cli/runner-input.d.ts.map +1 -1
- package/dist/adapters/cli/runner-input.js +4 -3
- package/dist/adapters/cli/runner-input.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +3 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +51 -30
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +33 -4
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +42 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +54 -4
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/arg-utils.d.ts +5 -0
- package/dist/cli/arg-utils.d.ts.map +1 -1
- package/dist/cli/arg-utils.js +7 -0
- package/dist/cli/arg-utils.js.map +1 -1
- package/dist/cli/bots-list-output.d.ts +130 -2
- package/dist/cli/bots-list-output.d.ts.map +1 -1
- package/dist/cli/bots-list-output.js +173 -27
- package/dist/cli/bots-list-output.js.map +1 -1
- package/dist/cli/daemon-lifecycle-env.d.ts +10 -3
- package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
- package/dist/cli/daemon-lifecycle-env.js +38 -11
- package/dist/cli/daemon-lifecycle-env.js.map +1 -1
- package/dist/cli/dashboard-command.d.ts +15 -0
- package/dist/cli/dashboard-command.d.ts.map +1 -1
- package/dist/cli/dashboard-command.js +22 -0
- package/dist/cli/dashboard-command.js.map +1 -1
- package/dist/cli/log-tail.d.ts +54 -0
- package/dist/cli/log-tail.d.ts.map +1 -0
- package/dist/cli/log-tail.js +167 -0
- package/dist/cli/log-tail.js.map +1 -0
- package/dist/cli/pm2-env.d.ts +16 -0
- package/dist/cli/pm2-env.d.ts.map +1 -0
- package/dist/cli/pm2-env.js +63 -0
- package/dist/cli/pm2-env.js.map +1 -0
- package/dist/cli/pm2-fleet-lock.d.ts +26 -0
- package/dist/cli/pm2-fleet-lock.d.ts.map +1 -0
- package/dist/cli/pm2-fleet-lock.js +50 -0
- package/dist/cli/pm2-fleet-lock.js.map +1 -0
- package/dist/cli/pm2-god-retirement.d.ts +59 -0
- package/dist/cli/pm2-god-retirement.d.ts.map +1 -0
- package/dist/cli/pm2-god-retirement.js +95 -0
- package/dist/cli/pm2-god-retirement.js.map +1 -0
- package/dist/cli/session-list-wake.d.ts +41 -0
- package/dist/cli/session-list-wake.d.ts.map +1 -0
- package/dist/cli/session-list-wake.js +102 -0
- package/dist/cli/session-list-wake.js.map +1 -0
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +26 -0
- package/dist/cli/workflow.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +941 -118
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +261 -49
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/core/cli-runtime-update.d.ts +8 -0
- package/dist/core/cli-runtime-update.d.ts.map +1 -1
- package/dist/core/cli-runtime-update.js +127 -8
- package/dist/core/cli-runtime-update.js.map +1 -1
- package/dist/core/close-residual.d.ts +64 -0
- package/dist/core/close-residual.d.ts.map +1 -0
- package/dist/core/close-residual.js +93 -0
- package/dist/core/close-residual.js.map +1 -0
- package/dist/core/closed-session-card.d.ts.map +1 -1
- package/dist/core/closed-session-card.js +11 -3
- package/dist/core/closed-session-card.js.map +1 -1
- package/dist/core/command-discovery.d.ts.map +1 -1
- package/dist/core/command-discovery.js +4 -1
- package/dist/core/command-discovery.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +129 -27
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts +3 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +230 -1
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/daemon-background-close.d.ts +19 -0
- package/dist/core/daemon-background-close.d.ts.map +1 -0
- package/dist/core/daemon-background-close.js +39 -0
- package/dist/core/daemon-background-close.js.map +1 -0
- package/dist/core/dashboard-command/overview.d.ts +4 -0
- package/dist/core/dashboard-command/overview.d.ts.map +1 -1
- package/dist/core/dashboard-command/overview.js +8 -1
- package/dist/core/dashboard-command/overview.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +415 -42
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +4 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +3 -0
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/dashboard-url.d.ts +37 -0
- package/dist/core/dashboard-url.d.ts.map +1 -1
- package/dist/core/dashboard-url.js +71 -0
- package/dist/core/dashboard-url.js.map +1 -1
- package/dist/core/deferred-schedule-settlement.d.ts +15 -1
- package/dist/core/deferred-schedule-settlement.d.ts.map +1 -1
- package/dist/core/deferred-schedule-settlement.js +4 -1
- package/dist/core/deferred-schedule-settlement.js.map +1 -1
- package/dist/core/device-isolation-daemon.d.ts +91 -1
- package/dist/core/device-isolation-daemon.d.ts.map +1 -1
- package/dist/core/device-isolation-daemon.js +346 -2
- package/dist/core/device-isolation-daemon.js.map +1 -1
- package/dist/core/local-terminal-opener.d.ts.map +1 -1
- package/dist/core/local-terminal-opener.js +5 -1
- package/dist/core/local-terminal-opener.js.map +1 -1
- package/dist/core/loopback-target.d.ts +14 -0
- package/dist/core/loopback-target.d.ts.map +1 -0
- package/dist/core/loopback-target.js +20 -0
- package/dist/core/loopback-target.js.map +1 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +16 -2
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/mojo-containment-command.d.ts +20 -0
- package/dist/core/mojo-containment-command.d.ts.map +1 -0
- package/dist/core/mojo-containment-command.js +266 -0
- package/dist/core/mojo-containment-command.js.map +1 -0
- package/dist/core/mojo-containment.d.ts +575 -0
- package/dist/core/mojo-containment.d.ts.map +1 -0
- package/dist/core/mojo-containment.js +1158 -0
- package/dist/core/mojo-containment.js.map +1 -0
- package/dist/core/mojo-launcher-env-quarantine.d.ts +30 -0
- package/dist/core/mojo-launcher-env-quarantine.d.ts.map +1 -0
- package/dist/core/mojo-launcher-env-quarantine.js +175 -0
- package/dist/core/mojo-launcher-env-quarantine.js.map +1 -0
- package/dist/core/mojo-session-identity.d.ts +3 -0
- package/dist/core/mojo-session-identity.d.ts.map +1 -0
- package/dist/core/mojo-session-identity.js +126 -0
- package/dist/core/mojo-session-identity.js.map +1 -0
- package/dist/core/persistent-backend.d.ts +32 -8
- package/dist/core/persistent-backend.d.ts.map +1 -1
- package/dist/core/persistent-backend.js +55 -17
- package/dist/core/persistent-backend.js.map +1 -1
- package/dist/core/plugins/mcp/gateway.d.ts +14 -1
- package/dist/core/plugins/mcp/gateway.d.ts.map +1 -1
- package/dist/core/plugins/mcp/gateway.js +70 -3
- package/dist/core/plugins/mcp/gateway.js.map +1 -1
- package/dist/core/plugins/mcp/host.d.ts +2 -0
- package/dist/core/plugins/mcp/host.d.ts.map +1 -1
- package/dist/core/plugins/mcp/host.js +2 -0
- package/dist/core/plugins/mcp/host.js.map +1 -1
- package/dist/core/plugins/pm2.d.ts.map +1 -1
- package/dist/core/plugins/pm2.js +16 -3
- package/dist/core/plugins/pm2.js.map +1 -1
- package/dist/core/plugins/service-manager.d.ts +9 -0
- package/dist/core/plugins/service-manager.d.ts.map +1 -1
- package/dist/core/plugins/service-manager.js +33 -16
- package/dist/core/plugins/service-manager.js.map +1 -1
- package/dist/core/preview-port-owner.d.ts +109 -0
- package/dist/core/preview-port-owner.d.ts.map +1 -0
- package/dist/core/preview-port-owner.js +320 -0
- package/dist/core/preview-port-owner.js.map +1 -0
- package/dist/core/remote-cli-ids.d.ts +5 -0
- package/dist/core/remote-cli-ids.d.ts.map +1 -0
- package/dist/core/remote-cli-ids.js +24 -0
- package/dist/core/remote-cli-ids.js.map +1 -0
- package/dist/core/{riff-shutdown-detach.d.ts → remote-shutdown-detach.d.ts} +40 -39
- package/dist/core/remote-shutdown-detach.d.ts.map +1 -0
- package/dist/core/{riff-shutdown-detach.js → remote-shutdown-detach.js} +85 -83
- package/dist/core/remote-shutdown-detach.js.map +1 -0
- package/dist/core/{riff-worker-shutdown-readiness.d.ts → remote-worker-shutdown-readiness.d.ts} +5 -4
- package/dist/core/remote-worker-shutdown-readiness.d.ts.map +1 -0
- package/dist/core/{riff-worker-shutdown-readiness.js → remote-worker-shutdown-readiness.js} +4 -3
- package/dist/core/remote-worker-shutdown-readiness.js.map +1 -0
- package/dist/core/reply-target.d.ts +16 -1
- package/dist/core/reply-target.d.ts.map +1 -1
- package/dist/core/reply-target.js +61 -0
- package/dist/core/reply-target.js.map +1 -1
- package/dist/core/session-group-birth.d.ts +26 -0
- package/dist/core/session-group-birth.d.ts.map +1 -1
- package/dist/core/session-group-birth.js +81 -4
- package/dist/core/session-group-birth.js.map +1 -1
- package/dist/core/session-manager.d.ts +15 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +259 -5
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-model.d.ts +46 -0
- package/dist/core/session-model.d.ts.map +1 -0
- package/dist/core/session-model.js +42 -0
- package/dist/core/session-model.js.map +1 -0
- package/dist/core/session-owner-reminder.d.ts +63 -0
- package/dist/core/session-owner-reminder.d.ts.map +1 -0
- package/dist/core/session-owner-reminder.js +196 -0
- package/dist/core/session-owner-reminder.js.map +1 -0
- package/dist/core/session-preview-registry.d.ts +53 -0
- package/dist/core/session-preview-registry.d.ts.map +1 -0
- package/dist/core/session-preview-registry.js +82 -0
- package/dist/core/session-preview-registry.js.map +1 -0
- package/dist/core/session-preview.d.ts +114 -0
- package/dist/core/session-preview.d.ts.map +1 -0
- package/dist/core/session-preview.js +184 -0
- package/dist/core/session-preview.js.map +1 -0
- package/dist/core/session-rename.d.ts.map +1 -1
- package/dist/core/session-rename.js +5 -3
- package/dist/core/session-rename.js.map +1 -1
- package/dist/core/session-wake-deadline.d.ts +5 -0
- package/dist/core/session-wake-deadline.d.ts.map +1 -0
- package/dist/core/session-wake-deadline.js +13 -0
- package/dist/core/session-wake-deadline.js.map +1 -0
- package/dist/core/shutdown-budgets.d.ts +7 -6
- package/dist/core/shutdown-budgets.d.ts.map +1 -1
- package/dist/core/shutdown-budgets.js +11 -10
- package/dist/core/shutdown-budgets.js.map +1 -1
- package/dist/core/skills/frontmatter.d.ts.map +1 -1
- package/dist/core/skills/frontmatter.js +6 -1
- package/dist/core/skills/frontmatter.js.map +1 -1
- package/dist/core/terminal-control-grant.d.ts +74 -0
- package/dist/core/terminal-control-grant.d.ts.map +1 -0
- package/dist/core/terminal-control-grant.js +183 -0
- package/dist/core/terminal-control-grant.js.map +1 -0
- package/dist/core/terminal-proxy.d.ts.map +1 -1
- package/dist/core/terminal-proxy.js +4 -5
- package/dist/core/terminal-proxy.js.map +1 -1
- package/dist/core/terminal-write-auth.d.ts +4 -11
- package/dist/core/terminal-write-auth.d.ts.map +1 -1
- package/dist/core/terminal-write-auth.js +19 -16
- package/dist/core/terminal-write-auth.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +36 -29
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +88 -10
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +9 -7
- package/dist/core/types.js.map +1 -1
- package/dist/core/workbench-link.d.ts +81 -0
- package/dist/core/workbench-link.d.ts.map +1 -0
- package/dist/core/workbench-link.js +109 -0
- package/dist/core/workbench-link.js.map +1 -0
- package/dist/core/worker-pool.d.ts +312 -11
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +2515 -217
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +447 -116
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +33 -3
- package/dist/dashboard/aggregator.d.ts.map +1 -1
- package/dist/dashboard/aggregator.js +93 -17
- package/dist/dashboard/aggregator.js.map +1 -1
- package/dist/dashboard/auth-session-connections.d.ts +59 -0
- package/dist/dashboard/auth-session-connections.d.ts.map +1 -0
- package/dist/dashboard/auth-session-connections.js +109 -0
- package/dist/dashboard/auth-session-connections.js.map +1 -0
- package/dist/dashboard/auth.d.ts +99 -1
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +176 -64
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +5 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +4 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/control-audit.d.ts +112 -0
- package/dist/dashboard/control-audit.d.ts.map +1 -0
- package/dist/dashboard/control-audit.js +195 -0
- package/dist/dashboard/control-audit.js.map +1 -0
- package/dist/dashboard/control-csrf.d.ts +123 -0
- package/dist/dashboard/control-csrf.d.ts.map +1 -0
- package/dist/dashboard/control-csrf.js +392 -0
- package/dist/dashboard/control-csrf.js.map +1 -0
- package/dist/dashboard/daemon-reconcile.d.ts +26 -0
- package/dist/dashboard/daemon-reconcile.d.ts.map +1 -0
- package/dist/dashboard/daemon-reconcile.js +46 -0
- package/dist/dashboard/daemon-reconcile.js.map +1 -0
- package/dist/dashboard/debug-terminal.d.ts.map +1 -1
- package/dist/dashboard/debug-terminal.js +18 -4
- package/dist/dashboard/debug-terminal.js.map +1 -1
- package/dist/dashboard/events-sse.d.ts +60 -0
- package/dist/dashboard/events-sse.d.ts.map +1 -0
- package/dist/dashboard/events-sse.js +63 -0
- package/dist/dashboard/events-sse.js.map +1 -0
- package/dist/dashboard/h5-auth.d.ts +256 -0
- package/dist/dashboard/h5-auth.d.ts.map +1 -0
- package/dist/dashboard/h5-auth.js +825 -0
- package/dist/dashboard/h5-auth.js.map +1 -0
- package/dist/dashboard/managed-spawn.d.ts +36 -0
- package/dist/dashboard/managed-spawn.d.ts.map +1 -0
- package/dist/dashboard/managed-spawn.js +169 -0
- package/dist/dashboard/managed-spawn.js.map +1 -0
- package/dist/dashboard/preview-content-capability.d.ts +41 -0
- package/dist/dashboard/preview-content-capability.d.ts.map +1 -0
- package/dist/dashboard/preview-content-capability.js +127 -0
- package/dist/dashboard/preview-content-capability.js.map +1 -0
- package/dist/dashboard/preview-contract.d.ts +85 -0
- package/dist/dashboard/preview-contract.d.ts.map +1 -0
- package/dist/dashboard/preview-contract.js +198 -0
- package/dist/dashboard/preview-contract.js.map +1 -0
- package/dist/dashboard/preview-guard-page.d.ts +52 -0
- package/dist/dashboard/preview-guard-page.d.ts.map +1 -0
- package/dist/dashboard/preview-guard-page.js +162 -0
- package/dist/dashboard/preview-guard-page.js.map +1 -0
- package/dist/dashboard/preview-interaction.d.ts +57 -0
- package/dist/dashboard/preview-interaction.d.ts.map +1 -0
- package/dist/dashboard/preview-interaction.js +198 -0
- package/dist/dashboard/preview-interaction.js.map +1 -0
- package/dist/dashboard/preview-proxy.d.ts +113 -0
- package/dist/dashboard/preview-proxy.d.ts.map +1 -0
- package/dist/dashboard/preview-proxy.js +543 -0
- package/dist/dashboard/preview-proxy.js.map +1 -0
- package/dist/dashboard/public-redact.d.ts +23 -0
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +90 -21
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/request-identity.d.ts +77 -0
- package/dist/dashboard/request-identity.d.ts.map +1 -0
- package/dist/dashboard/request-identity.js +72 -0
- package/dist/dashboard/request-identity.js.map +1 -0
- package/dist/dashboard/session-cleanup.d.ts +5 -0
- package/dist/dashboard/session-cleanup.d.ts.map +1 -1
- package/dist/dashboard/session-cleanup.js +4 -0
- package/dist/dashboard/session-cleanup.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +5 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +14 -0
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/standing-link.d.ts +76 -0
- package/dist/dashboard/standing-link.d.ts.map +1 -0
- package/dist/dashboard/standing-link.js +111 -0
- package/dist/dashboard/standing-link.js.map +1 -0
- package/dist/dashboard/terminal-control.d.ts +105 -0
- package/dist/dashboard/terminal-control.d.ts.map +1 -0
- package/dist/dashboard/terminal-control.js +296 -0
- package/dist/dashboard/terminal-control.js.map +1 -0
- package/dist/dashboard/terminal-front-proxy.d.ts +55 -0
- package/dist/dashboard/terminal-front-proxy.d.ts.map +1 -0
- package/dist/dashboard/terminal-front-proxy.js +397 -0
- package/dist/dashboard/terminal-front-proxy.js.map +1 -0
- package/dist/dashboard/terminal-view-capability.d.ts +97 -0
- package/dist/dashboard/terminal-view-capability.d.ts.map +1 -0
- package/dist/dashboard/terminal-view-capability.js +145 -0
- package/dist/dashboard/terminal-view-capability.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-api.d.ts +59 -0
- package/dist/dashboard/web/agent-workbench-api.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-api.js +227 -0
- package/dist/dashboard/web/agent-workbench-api.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.d.ts +59 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.js +195 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance.d.ts +222 -0
- package/dist/dashboard/web/agent-workbench-appearance.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance.js +499 -0
- package/dist/dashboard/web/agent-workbench-appearance.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-capabilities.d.ts +13 -0
- package/dist/dashboard/web/agent-workbench-capabilities.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-capabilities.js +24 -0
- package/dist/dashboard/web/agent-workbench-capabilities.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-chat.d.ts +90 -0
- package/dist/dashboard/web/agent-workbench-chat.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-chat.js +168 -0
- package/dist/dashboard/web/agent-workbench-chat.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-page.d.ts +3 -0
- package/dist/dashboard/web/agent-workbench-dock-page.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-page.js +23 -0
- package/dist/dashboard/web/agent-workbench-dock-page.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-view.d.ts +22 -0
- package/dist/dashboard/web/agent-workbench-dock-view.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-view.js +76 -0
- package/dist/dashboard/web/agent-workbench-dock-view.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-model.d.ts +196 -0
- package/dist/dashboard/web/agent-workbench-model.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-model.js +529 -0
- package/dist/dashboard/web/agent-workbench-model.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-page.d.ts +3 -0
- package/dist/dashboard/web/agent-workbench-page.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-page.js +27 -0
- package/dist/dashboard/web/agent-workbench-page.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-panes.d.ts +49 -0
- package/dist/dashboard/web/agent-workbench-panes.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-panes.js +468 -0
- package/dist/dashboard/web/agent-workbench-panes.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-session-list.d.ts +42 -0
- package/dist/dashboard/web/agent-workbench-session-list.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-session-list.js +353 -0
- package/dist/dashboard/web/agent-workbench-session-list.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-storage.d.ts +49 -0
- package/dist/dashboard/web/agent-workbench-storage.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-storage.js +197 -0
- package/dist/dashboard/web/agent-workbench-storage.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-touch.d.ts +46 -0
- package/dist/dashboard/web/agent-workbench-touch.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-touch.js +96 -0
- package/dist/dashboard/web/agent-workbench-touch.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-view.d.ts +34 -0
- package/dist/dashboard/web/agent-workbench-view.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-view.js +439 -0
- package/dist/dashboard/web/agent-workbench-view.js.map +1 -0
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +63 -2
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +18 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +362 -36
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +8 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/control-csrf.d.ts +4 -0
- package/dist/dashboard/web/control-csrf.d.ts.map +1 -0
- package/dist/dashboard/web/control-csrf.js +26 -0
- package/dist/dashboard/web/control-csrf.js.map +1 -0
- package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-routes.js +2 -0
- package/dist/dashboard/web/dashboard-routes.js.map +1 -1
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +11 -2
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +140 -6
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/overview-page.d.ts.map +1 -1
- package/dist/dashboard/web/overview-page.js +3 -2
- package/dist/dashboard/web/overview-page.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +41 -1
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +5 -1
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +3 -0
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +56 -10
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +4 -2
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/web/store.d.ts +27 -2
- package/dist/dashboard/web/store.d.ts.map +1 -1
- package/dist/dashboard/web/store.js +43 -4
- package/dist/dashboard/web/store.js.map +1 -1
- package/dist/dashboard/web/ui.d.ts +3 -0
- package/dist/dashboard/web/ui.d.ts.map +1 -1
- package/dist/dashboard/web/ui.js +11 -0
- package/dist/dashboard/web/ui.js.map +1 -1
- package/dist/dashboard/web/update-action.d.ts +12 -0
- package/dist/dashboard/web/update-action.d.ts.map +1 -1
- package/dist/dashboard/web/update-action.js +11 -0
- package/dist/dashboard/web/update-action.js.map +1 -1
- package/dist/dashboard/workbench-doctor.d.ts +114 -0
- package/dist/dashboard/workbench-doctor.d.ts.map +1 -0
- package/dist/dashboard/workbench-doctor.js +620 -0
- package/dist/dashboard/workbench-doctor.js.map +1 -0
- package/dist/dashboard/workbench-ticket.d.ts +165 -0
- package/dist/dashboard/workbench-ticket.d.ts.map +1 -0
- package/dist/dashboard/workbench-ticket.js +495 -0
- package/dist/dashboard/workbench-ticket.js.map +1 -0
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/agent-workbench-dock-page-IKAC5CFL.js +1 -0
- package/dist/dashboard-web/chunks/agent-workbench-page-M53UN5EB.js +1 -0
- package/dist/dashboard-web/chunks/bot-defaults-page-YKE5SDZF.js +10 -0
- package/dist/dashboard-web/chunks/chunk-22WIADUT.js +2 -0
- package/dist/dashboard-web/chunks/chunk-24ONYQMR.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-6O7KXFD2.js → chunk-2W6SCN3M.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-4XNJX6B3.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-Q2MT4VWH.js → chunk-F6KNPF4R.js} +11 -9
- package/dist/dashboard-web/chunks/chunk-FXW3V4JR.js +1 -0
- package/dist/dashboard-web/chunks/chunk-G5ZS2YUS.js +1 -0
- package/dist/dashboard-web/chunks/chunk-KTQ7B6ZR.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-U3ZFSO7E.js → chunk-NACP7BMR.js} +1 -1
- package/dist/dashboard-web/chunks/{connectors-page-2KZO5KSJ.js → connectors-page-VOISXXII.js} +1 -1
- package/dist/dashboard-web/chunks/{feedback-page-CLVTOLLO.js → feedback-page-RN6HRZMS.js} +1 -1
- package/dist/dashboard-web/chunks/groups-page-UQT6DCB7.js +8 -0
- package/dist/dashboard-web/chunks/{insights-page-4RPU2CRU.js → insights-page-HJFDYR5N.js} +3 -3
- package/dist/dashboard-web/chunks/{monitor-room-OTKQ2CXK.js → monitor-room-V7PCP47V.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-IJPUB6JT.js → monitoring-page-NKEKVQAP.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-QSR3J44Y.js → office-page-GVJF73XW.js} +1 -1
- package/dist/dashboard-web/chunks/overview-page-KHLV6VTI.js +1 -0
- package/dist/dashboard-web/chunks/{plugin-page-VEGTCEAS.js → plugin-page-J3OF5IY4.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-6NWJMOB5.js → roles-page-WQIRSOLZ.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-6I3LNXD4.js → schedules-page-J2DEY2X6.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-7BQFEA5P.js +8 -0
- package/dist/dashboard-web/chunks/settings-page-BJEIOFJH.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-M2Q6L5CB.js → skills-page-HGIFR3IU.js} +2 -2
- package/dist/dashboard-web/chunks/{team-federation-page-7I7FDE3B.js → team-federation-page-MR5ZPL4B.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-HQXRL65L.js → v3-page-GBR5JWPE.js} +2 -2
- package/dist/dashboard-web/chunks/{whiteboards-page-LQKC4TAZ.js → whiteboards-page-QAZM4BN3.js} +1 -1
- package/dist/dashboard-web/index.html +8 -1
- package/dist/dashboard-web/style.css +2006 -10
- package/dist/dashboard.js +1017 -264
- package/dist/dashboard.js.map +1 -1
- package/dist/dsh-runner.js +8 -1
- package/dist/dsh-runner.js.map +1 -1
- package/dist/global-config.d.ts +30 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +33 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +46 -27
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +43 -27
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +1 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +22 -6
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +118 -24
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +15 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +30 -0
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +48 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +138 -41
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/identity-cache.d.ts +14 -6
- package/dist/im/lark/identity-cache.d.ts.map +1 -1
- package/dist/im/lark/identity-cache.js +69 -26
- package/dist/im/lark/identity-cache.js.map +1 -1
- package/dist/im/lark/lark-hosts.d.ts +13 -0
- package/dist/im/lark/lark-hosts.d.ts.map +1 -1
- package/dist/im/lark/lark-hosts.js +16 -0
- package/dist/im/lark/lark-hosts.js.map +1 -1
- package/dist/im/lark/overview-card.d.ts +22 -0
- package/dist/im/lark/overview-card.d.ts.map +1 -1
- package/dist/im/lark/overview-card.js +53 -3
- package/dist/im/lark/overview-card.js.map +1 -1
- package/dist/im/lark/sessions-card.d.ts.map +1 -1
- package/dist/im/lark/sessions-card.js +26 -3
- package/dist/im/lark/sessions-card.js.map +1 -1
- package/dist/im/lark/workflow-slash-command.d.ts +6 -0
- package/dist/im/lark/workflow-slash-command.d.ts.map +1 -1
- package/dist/im/lark/workflow-slash-command.js +7 -0
- package/dist/im/lark/workflow-slash-command.js.map +1 -1
- package/dist/index-daemon.js +27 -4
- package/dist/index-daemon.js.map +1 -1
- package/dist/index-dashboard.d.ts +3 -0
- package/dist/index-dashboard.d.ts.map +1 -0
- package/dist/index-dashboard.js +94 -0
- package/dist/index-dashboard.js.map +1 -0
- package/dist/platform/team-agents-client.d.ts +199 -0
- package/dist/platform/team-agents-client.d.ts.map +1 -0
- package/dist/platform/team-agents-client.js +263 -0
- package/dist/platform/team-agents-client.js.map +1 -0
- package/dist/platform/tunnel-client.d.ts +6 -0
- package/dist/platform/tunnel-client.d.ts.map +1 -1
- package/dist/platform/tunnel-client.js.map +1 -1
- package/dist/services/async-trigger-state.d.ts +7 -3
- package/dist/services/async-trigger-state.d.ts.map +1 -1
- package/dist/services/async-trigger-state.js +28 -0
- package/dist/services/async-trigger-state.js.map +1 -1
- package/dist/services/async-trigger-store.d.ts +13 -4
- package/dist/services/async-trigger-store.d.ts.map +1 -1
- package/dist/services/async-trigger-store.js +55 -8
- package/dist/services/async-trigger-store.js.map +1 -1
- package/dist/services/bot-config-store.d.ts +1 -11
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +27 -3
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/bot-profile-store.d.ts +17 -2
- package/dist/services/bot-profile-store.d.ts.map +1 -1
- package/dist/services/bot-profile-store.js +93 -12
- package/dist/services/bot-profile-store.js.map +1 -1
- package/dist/services/bridge-fallback-gate.d.ts +15 -7
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
- package/dist/services/bridge-fallback-gate.js +43 -16
- package/dist/services/bridge-fallback-gate.js.map +1 -1
- package/dist/services/bridge-turn-queue.d.ts +10 -1
- package/dist/services/bridge-turn-queue.d.ts.map +1 -1
- package/dist/services/bridge-turn-queue.js +29 -9
- package/dist/services/bridge-turn-queue.js.map +1 -1
- package/dist/services/claude-transcript.d.ts +19 -0
- package/dist/services/claude-transcript.d.ts.map +1 -1
- package/dist/services/claude-transcript.js +57 -5
- package/dist/services/claude-transcript.js.map +1 -1
- package/dist/services/codex-app-threads.d.ts +2 -0
- package/dist/services/codex-app-threads.d.ts.map +1 -1
- package/dist/services/codex-app-threads.js +1 -1
- package/dist/services/codex-app-threads.js.map +1 -1
- package/dist/services/codex-reasoning-effort.d.ts +8 -0
- package/dist/services/codex-reasoning-effort.d.ts.map +1 -1
- package/dist/services/codex-reasoning-effort.js +24 -0
- package/dist/services/codex-reasoning-effort.js.map +1 -1
- package/dist/services/cursor-resume-policy.d.ts.map +1 -1
- package/dist/services/cursor-resume-policy.js +15 -3
- package/dist/services/cursor-resume-policy.js.map +1 -1
- package/dist/services/open-platform-rename.d.ts.map +1 -1
- package/dist/services/open-platform-rename.js +3 -2
- package/dist/services/open-platform-rename.js.map +1 -1
- package/dist/services/ordinary-turn-recovery.d.ts +76 -0
- package/dist/services/ordinary-turn-recovery.d.ts.map +1 -0
- package/dist/services/ordinary-turn-recovery.js +349 -0
- package/dist/services/ordinary-turn-recovery.js.map +1 -0
- package/dist/services/peer-cross-ref-store.d.ts +8 -0
- package/dist/services/peer-cross-ref-store.d.ts.map +1 -0
- package/dist/services/peer-cross-ref-store.js +95 -0
- package/dist/services/peer-cross-ref-store.js.map +1 -0
- package/dist/services/pi-transcript.d.ts.map +1 -1
- package/dist/services/pi-transcript.js +10 -6
- package/dist/services/pi-transcript.js.map +1 -1
- package/dist/services/resume-fresh-policy.d.ts +17 -0
- package/dist/services/resume-fresh-policy.d.ts.map +1 -0
- package/dist/services/resume-fresh-policy.js +24 -0
- package/dist/services/resume-fresh-policy.js.map +1 -0
- package/dist/services/session-group-title.d.ts +1 -0
- package/dist/services/session-group-title.d.ts.map +1 -1
- package/dist/services/session-group-title.js +16 -1
- package/dist/services/session-group-title.js.map +1 -1
- package/dist/services/session-groups-store.d.ts +30 -0
- package/dist/services/session-groups-store.d.ts.map +1 -1
- package/dist/services/session-groups-store.js +26 -4
- package/dist/services/session-groups-store.js.map +1 -1
- package/dist/services/session-owner-notification.d.ts +9 -0
- package/dist/services/session-owner-notification.d.ts.map +1 -0
- package/dist/services/session-owner-notification.js +10 -0
- package/dist/services/session-owner-notification.js.map +1 -0
- package/dist/services/session-owner-reminder-config-store.d.ts +9 -0
- package/dist/services/session-owner-reminder-config-store.d.ts.map +1 -0
- package/dist/services/session-owner-reminder-config-store.js +24 -0
- package/dist/services/session-owner-reminder-config-store.js.map +1 -0
- package/dist/services/session-owner-reminder-store.d.ts +5 -0
- package/dist/services/session-owner-reminder-store.d.ts.map +1 -0
- package/dist/services/session-owner-reminder-store.js +54 -0
- package/dist/services/session-owner-reminder-store.js.map +1 -0
- package/dist/services/session-store.d.ts +98 -21
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +406 -48
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/structured-bridge-clis.d.ts +15 -2
- package/dist/services/structured-bridge-clis.d.ts.map +1 -1
- package/dist/services/structured-bridge-clis.js +16 -1
- package/dist/services/structured-bridge-clis.js.map +1 -1
- package/dist/services/transcript-resolver.d.ts +1 -1
- package/dist/services/transcript-resolver.d.ts.map +1 -1
- package/dist/services/transcript-resolver.js +6 -1
- package/dist/services/transcript-resolver.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +7 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-herdr-integrations.d.ts +13 -0
- package/dist/setup/ensure-herdr-integrations.d.ts.map +1 -1
- package/dist/setup/ensure-herdr-integrations.js +21 -3
- package/dist/setup/ensure-herdr-integrations.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +1 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +107 -7
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/skills/definitions.d.ts +17 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +78 -8
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/injection-mode.d.ts +24 -3
- package/dist/skills/injection-mode.d.ts.map +1 -1
- package/dist/skills/injection-mode.js +64 -15
- package/dist/skills/injection-mode.js.map +1 -1
- package/dist/skills/installer.d.ts +18 -0
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +45 -1
- package/dist/skills/installer.js.map +1 -1
- package/dist/types.d.ts +222 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/bot-routing.d.ts +1 -1
- package/dist/utils/bot-routing.d.ts.map +1 -1
- package/dist/utils/bot-routing.js.map +1 -1
- package/dist/utils/child-env.d.ts +92 -2
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +195 -1
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/cli-usage-limit.d.ts +25 -14
- package/dist/utils/cli-usage-limit.d.ts.map +1 -1
- package/dist/utils/cli-usage-limit.js +25 -15
- package/dist/utils/cli-usage-limit.js.map +1 -1
- package/dist/utils/daemon-rejection-guard.d.ts +35 -0
- package/dist/utils/daemon-rejection-guard.d.ts.map +1 -0
- package/dist/utils/daemon-rejection-guard.js +101 -0
- package/dist/utils/daemon-rejection-guard.js.map +1 -0
- package/dist/utils/dashboard-env.d.ts +53 -0
- package/dist/utils/dashboard-env.d.ts.map +1 -0
- package/dist/utils/dashboard-env.js +116 -0
- package/dist/utils/dashboard-env.js.map +1 -0
- package/dist/utils/global-install.d.ts.map +1 -1
- package/dist/utils/global-install.js +61 -5
- package/dist/utils/global-install.js.map +1 -1
- package/dist/utils/install-diagnostics.d.ts +14 -7
- package/dist/utils/install-diagnostics.d.ts.map +1 -1
- package/dist/utils/install-diagnostics.js +20 -11
- package/dist/utils/install-diagnostics.js.map +1 -1
- package/dist/utils/local-dev-update.d.ts +132 -0
- package/dist/utils/local-dev-update.d.ts.map +1 -0
- package/dist/utils/local-dev-update.js +176 -0
- package/dist/utils/local-dev-update.js.map +1 -0
- package/dist/utils/pending-input-queue.d.ts +12 -1
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/worker.js +1517 -236
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +68 -68
- package/dist/workflows/events/payloads.d.ts +14 -14
- package/dist/workflows/events/schema.d.ts +312 -312
- package/dist/workflows/migration/v2-run-archive-schema.d.ts +40 -40
- package/dist/workflows/v3/cli-run.d.ts.map +1 -1
- package/dist/workflows/v3/cli-run.js +7 -0
- package/dist/workflows/v3/cli-run.js.map +1 -1
- package/dist/workflows/v3/goal-cli.d.ts +3 -0
- package/dist/workflows/v3/goal-cli.d.ts.map +1 -1
- package/dist/workflows/v3/goal-cli.js +11 -0
- package/dist/workflows/v3/goal-cli.js.map +1 -1
- package/dist/workflows/v3/run-envelope.d.ts +144 -144
- package/package.json +7 -2
- package/dist/cli/pm2-god-admission.d.ts +0 -8
- package/dist/cli/pm2-god-admission.d.ts.map +0 -1
- package/dist/cli/pm2-god-admission.js +0 -25
- package/dist/cli/pm2-god-admission.js.map +0 -1
- package/dist/core/riff-shutdown-detach.d.ts.map +0 -1
- package/dist/core/riff-shutdown-detach.js.map +0 -1
- package/dist/core/riff-worker-shutdown-readiness.d.ts.map +0 -1
- package/dist/core/riff-worker-shutdown-readiness.js.map +0 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-4RENHE7J.js +0 -10
- package/dist/dashboard-web/chunks/chunk-2WQDS65D.js +0 -1
- package/dist/dashboard-web/chunks/chunk-G5XGBCM6.js +0 -1
- package/dist/dashboard-web/chunks/chunk-WLIVBPJC.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-HLASRTSY.js +0 -8
- package/dist/dashboard-web/chunks/overview-page-6OK63V4V.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-B2TXGJDO.js +0 -8
- package/dist/dashboard-web/chunks/settings-page-6KJADSC6.js +0 -2
package/dist/dashboard.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
// src/dashboard.ts
|
|
2
|
-
import { createServer, get as httpGet
|
|
3
|
-
import { createServer as createTcpServer
|
|
2
|
+
import { createServer, get as httpGet } from 'node:http';
|
|
3
|
+
import { createServer as createTcpServer } from 'node:net';
|
|
4
4
|
import { readFileSync, existsSync, mkdirSync, readdirSync, statSync, createReadStream, realpathSync, } from 'node:fs';
|
|
5
5
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
6
6
|
import { join, dirname, extname, resolve, relative, isAbsolute } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { randomBytes } from 'node:crypto';
|
|
9
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
10
10
|
import { logger } from './utils/logger.js';
|
|
11
11
|
import { gracefulProcessExitCode } from './pm2-graceful-exit.js';
|
|
12
12
|
import { config, isWildcardBindHost } from './config.js';
|
|
13
13
|
import { listenWithProbe } from './utils/listen-with-probe.js';
|
|
14
|
-
import { parseCookie, buildSetCookie, verifyHmac, cliAuthBind,
|
|
14
|
+
import { parseCookie, buildSetCookie, verifyHmac, cliAuthBind, projectWorkbenchOperationCapabilities, previewInteractionWriteAllowed, loadPersistedToken, loadOrCreatePersistedToken, rotatePersistedToken, loadDashboardSecret, loadOrCreateDashboardSecret, describeDashboardTokenError, } from './dashboard/auth.js';
|
|
15
|
+
import { resolveDashboardIdentity, resolveDashboardRequestGate, } from './dashboard/request-identity.js';
|
|
16
|
+
import { AuthSessionConnectionRegistry } from './dashboard/auth-session-connections.js';
|
|
17
|
+
import { createDashboardEventsStream } from './dashboard/events-sse.js';
|
|
18
|
+
import { ControlCsrfTokens, guardControlRequest, injectControlCsrfMeta, managementUpgradeOrigin, } from './dashboard/control-csrf.js';
|
|
15
19
|
import { DaemonRegistry, botsRosterSignature } from './dashboard/registry.js';
|
|
16
20
|
import { Aggregator, subscribeDaemon } from './dashboard/aggregator.js';
|
|
21
|
+
import { reconcileDaemonSnapshot } from './dashboard/daemon-reconcile.js';
|
|
17
22
|
import { createSessionPresentationCoordinator } from './dashboard/session-presentation.js';
|
|
18
23
|
import { compactGroupsMatrix, createGroupsMatrixSnapshot, enrichSessionsWithGroupNames, roleWriteShouldInvalidate, } from './dashboard/groups-matrix-snapshot.js';
|
|
19
24
|
import { parseDashboardAskAnswerRequest, proxyDashboardAskAnswer, } from './dashboard/desktop-asks.js';
|
|
20
25
|
import { createDebugTerminalManager } from './dashboard/debug-terminal.js';
|
|
26
|
+
import { createSessionPreviewProxy } from './dashboard/preview-proxy.js';
|
|
27
|
+
import { mintPreviewContentCapability, verifyPreviewContentCapability, } from './dashboard/preview-content-capability.js';
|
|
28
|
+
import { previewDescriptorFromRow, previewTeardownForDaemonEvent, projectSessionDetailForBrowser, projectSessionPreviewEventForBrowser, projectSessionPreviewsForBrowser, resolveSessionPreviewForProxy, } from './dashboard/preview-contract.js';
|
|
29
|
+
import { sameSessionPreviewTarget, sessionPreviewTargetStillOwned, } from './core/session-preview.js';
|
|
21
30
|
import { pickCreatorForGroup } from './dashboard/operator-selector.js';
|
|
22
31
|
import { buildTeamGroupCreatePayload, planGroupCreator } from './dashboard/team-group.js';
|
|
23
32
|
import { jsonRes } from './dashboard/http.js';
|
|
@@ -26,7 +35,7 @@ import { defaultRunsDir as v3RunsDir } from './workflows/v3/ops-projection.js';
|
|
|
26
35
|
import { verifyWorkflowDaemonIpcResponse, workflowDaemonIpcHeaders, WORKFLOW_DAEMON_IPC_ROUTE_PREFIX, } from './workflows/v3/daemon-ipc-auth.js';
|
|
27
36
|
import { handleDashboardTriggerApi } from './dashboard/trigger-api.js';
|
|
28
37
|
import { handleConnectorApi } from './dashboard/connector-api.js';
|
|
29
|
-
import {
|
|
38
|
+
import { projectSessionEventForAudience, projectSessionsForAudience, redactGroupsForPublic, redactSchedulesForPublic, redactSettingsForPublic, sessionBoardAudienceFor, } from './dashboard/public-redact.js';
|
|
30
39
|
import { handleWebhookRoute } from './dashboard/webhook-routes.js';
|
|
31
40
|
import { handleFeedbackAnalyticsApi } from './dashboard/feedback-analytics-api.js';
|
|
32
41
|
import { FeedbackAnalyticsService } from './services/feedback-analytics.js';
|
|
@@ -36,33 +45,48 @@ import { resolveLiveBotTransport } from './services/team-roster.js';
|
|
|
36
45
|
import { handleFederationSpokeApi, syncAllMemberships, autoBindOwnerIfUnambiguous } from './dashboard/federation-spoke-api.js';
|
|
37
46
|
import { BotOnboardingManager } from './dashboard/bot-onboarding.js';
|
|
38
47
|
import { FeishuLoginManager } from './dashboard/feishu-login.js';
|
|
48
|
+
import { createDashboardH5AuthController, DashboardSessionStore, resolveDashboardH5AuthConfig, } from './dashboard/h5-auth.js';
|
|
49
|
+
import { FileControlAuditSink } from './dashboard/control-audit.js';
|
|
50
|
+
import { TerminalControlManager, terminalControlTtlFromEnv, } from './dashboard/terminal-control.js';
|
|
51
|
+
import { PreviewInteractionManager } from './dashboard/preview-interaction.js';
|
|
52
|
+
import { createPreviewGuardPage } from './dashboard/preview-guard-page.js';
|
|
53
|
+
import { handleWorkbenchDoctor } from './dashboard/workbench-doctor.js';
|
|
54
|
+
import { handleWorkbenchTicketRedemption, revokeWorkbenchTicketsOutsideGeneration, workbenchTicketGeneration, } from './dashboard/workbench-ticket.js';
|
|
55
|
+
import { handleWorkbenchStandingLink } from './dashboard/standing-link.js';
|
|
56
|
+
import { createTerminalFrontProxy } from './dashboard/terminal-front-proxy.js';
|
|
57
|
+
import { centralViewLinkPath, mintTerminalViewCapability, terminalViewCapabilityAuthSession, terminalViewForwardProof, upstreamWorkerViewGeneration, } from './dashboard/terminal-view-capability.js';
|
|
39
58
|
import { CLI_SELECT_OPTIONS, resolveCliSelection, isTtadkWrapper, ttadkAcceptsModel, TTADK_DEFAULT_MODEL, TTADK_MODEL_SUGGESTIONS, } from './setup/cli-selection.js';
|
|
40
59
|
import { checkCliAvailability } from './setup/cli-availability.js';
|
|
41
60
|
import { invalidWorkingDirs } from './utils/working-dir.js';
|
|
42
61
|
import { invalidateGlobalConfigCache, mergeDashboardConfig, mergeGlobalConfig, readGlobalConfig } from './global-config.js';
|
|
43
62
|
import { hostLocalTimeZone, scheduleTimeZone } from './utils/timezone.js';
|
|
44
|
-
import { buildDashboardUrls, buildPlatformDashboardLoginUrl, } from './core/dashboard-url.js';
|
|
63
|
+
import { buildDashboardUrls, buildPlatformDashboardLoginUrl, workbenchEntryUrl, } from './core/dashboard-url.js';
|
|
45
64
|
import { resolveBotmuxDataDir } from './core/data-dir.js';
|
|
65
|
+
import { parseCloseResidual } from './core/close-residual.js';
|
|
46
66
|
import { dashboardSecretPath } from './core/dashboard-secret.js';
|
|
47
67
|
import { getGitRepoInfo } from './core/session-row-enrichment.js';
|
|
48
68
|
import { deleteWhiteboard, listWhiteboards, readWhiteboard, whiteboardEnabled } from './services/whiteboard-store.js';
|
|
49
|
-
import { isLocalDevInstall, botmuxVersionAt,
|
|
50
|
-
import { checkNode, detectBotmuxInstalls, resolveCurrentVersion } from './utils/install-diagnostics.js';
|
|
69
|
+
import { isLocalDevInstall, botmuxVersionAt, botmuxCliEntryAt, botmuxInstallRoot } from './utils/install-info.js';
|
|
70
|
+
import { checkNode, detectBotmuxInstalls, resolveCurrentVersion, resolveCurrentVersionAt } from './utils/install-diagnostics.js';
|
|
51
71
|
import { fetchLatestVersion, fetchReleasesSince, fetchRollbackVersions, compareVersions, isCanonicalStableVersion, isNewerVersion, } from './core/update-check.js';
|
|
52
72
|
import { GITHUB_REPO } from './core/restart-report.js';
|
|
53
73
|
import { DEFAULT_OVERLOAD_THRESHOLDS } from './core/host-overload-alert.js';
|
|
54
|
-
import { spawnDetachedRestart, globalInstallUpdateLockTarget
|
|
74
|
+
import { spawnDetachedRestart, globalInstallUpdateLockTarget } from './core/maintenance.js';
|
|
75
|
+
import { resolveLocalDevCheckoutDir, resolveLocalDevRestartTarget, isGitWorktree, gitPorcelainStatus, gitHeadSha, localDevUpdateSteps, } from './utils/local-dev-update.js';
|
|
55
76
|
import { detectGlobalInstallManager, formatGlobalInstallCommand, resolveGlobalInstallPlan, tryResolveGlobalInstallPlan, withGlobalInstallRegistry, UnsupportedGlobalInstallError, } from './utils/global-install.js';
|
|
56
77
|
import { filterCliRuntimeUpdateEntriesForTargets, listCliRuntimeUpdateEntries, selectCodexRuntimeUpdateTargets, } from './core/cli-runtime-update.js';
|
|
57
78
|
import { claimRestartLease, clearRestartIntent, clearRestartLease, hasActiveRestartLease, writeManualIntentIfAbsent, writeRestartIntent, } from './services/restart-intent-store.js';
|
|
58
79
|
import { withFileLock } from './utils/file-lock.js';
|
|
59
80
|
import { evaluateRestartShutdownPreflight } from './cli/restart-shutdown-preflight.js';
|
|
60
|
-
|
|
81
|
+
// Host children the dashboard forks (start/stop-bot, global install). They live
|
|
82
|
+
// in their own module because every one of them must run on a REDACTED env —
|
|
83
|
+
// see dashboard/managed-spawn.ts.
|
|
84
|
+
import { runGlobalInstall, runLocalDevStep, spawnStartBotLive, spawnStopBotLive } from './dashboard/managed-spawn.js';
|
|
61
85
|
import { applySettingsWrite, defaultSettingsWriteApplierDeps, hasResolvedCodexNotifierRecipient, resolveCodexNotifierRecipientView, } from './dashboard/settings-write-applier.js';
|
|
62
86
|
import { addBotsToGroup, bindOncall, disbandGroup, leaveGroup, unbindOncall, } from './dashboard/groups-action-helpers.js';
|
|
63
87
|
import { createDaemonInternalApi } from './dashboard/daemon-internal-api.js';
|
|
64
88
|
import { listTeamReports, readTeamBoard, setTeamBoardEntry } from './services/team-board-store.js';
|
|
65
|
-
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
89
|
+
import { ALL_CLI_IDS, createCliAdapterSync } from './adapters/cli/registry.js';
|
|
66
90
|
import { hd2dAssetPath, hd2dStatus, startHd2dDownload } from './dashboard/hd2d-assets.js';
|
|
67
91
|
import { buildSkillInstallAuditSummary, installLocalSkillLinks, readSkillRegistry, removeInstalledSkill, removeInstalledSkills, sweepStoreTrash, updateInstalledSkillAsync, } from './services/skill-registry-store.js';
|
|
68
92
|
import { readSkillPackRegistry } from './services/skill-pack-store.js';
|
|
@@ -90,6 +114,7 @@ import { readPlatformBinding } from './platform/binding.js';
|
|
|
90
114
|
import { startPlatformTunnelClient } from './platform/tunnel-client.js';
|
|
91
115
|
import { applyPlatformTeamSync, getPlatformTeamSyncRev, listPlatformTeams } from './services/platform-team-store.js';
|
|
92
116
|
import { getBotUnionId } from './services/bot-union-ids-store.js';
|
|
117
|
+
import { getBotSpecialties } from './services/bot-profile-store.js';
|
|
93
118
|
import { cleanupIdleSessions, parseIdleCleanupHours } from './dashboard/session-cleanup.js';
|
|
94
119
|
import { compatMachineIdForAuthenticatedRequest, handleDesktopCompat, } from './dashboard/compat.js';
|
|
95
120
|
import { isDashboardChunkJsPath, missingDashboardChunkModule } from './dashboard/stale-chunk-module.js';
|
|
@@ -171,6 +196,116 @@ function currentDashboardToken() {
|
|
|
171
196
|
// EADDRINUSE probe). Used for token-bearing URLs and persisted for the CLI.
|
|
172
197
|
let boundDashboardPort = config.dashboard.port;
|
|
173
198
|
const SECRET = loadOrCreateSecret();
|
|
199
|
+
const dashboardControlAudit = new FileControlAuditSink();
|
|
200
|
+
const dashboardH5AuthConfig = resolveDashboardH5AuthConfig();
|
|
201
|
+
const dashboardSessions = new DashboardSessionStore({ ttlMs: dashboardH5AuthConfig.sessionTtlMs });
|
|
202
|
+
const dashboardH5Auth = createDashboardH5AuthController({
|
|
203
|
+
config: dashboardH5AuthConfig,
|
|
204
|
+
sessions: dashboardSessions,
|
|
205
|
+
audit: dashboardControlAudit,
|
|
206
|
+
});
|
|
207
|
+
const terminalControl = new TerminalControlManager({
|
|
208
|
+
secret: SECRET,
|
|
209
|
+
audit: dashboardControlAudit,
|
|
210
|
+
ttlMs: terminalControlTtlFromEnv(),
|
|
211
|
+
});
|
|
212
|
+
const previewInteraction = new PreviewInteractionManager({ audit: dashboardControlAudit });
|
|
213
|
+
function legacyDashboardAuthSessionId(token) {
|
|
214
|
+
return createHmac('sha256', SECRET)
|
|
215
|
+
.update('botmux-legacy-dashboard-session-id-v1\0')
|
|
216
|
+
.update(token)
|
|
217
|
+
.digest('base64url');
|
|
218
|
+
}
|
|
219
|
+
/** Stable per-machine scope for platform-dashboard actors. Shared between
|
|
220
|
+
* identity resolution and the read-capability liveness check so the two can
|
|
221
|
+
* never drift apart on the authSessionId format. */
|
|
222
|
+
function platformDashboardActorScope(machineId) {
|
|
223
|
+
return createHmac('sha256', SECRET)
|
|
224
|
+
.update('botmux-platform-dashboard-actor-v1\0')
|
|
225
|
+
.update(machineId)
|
|
226
|
+
.digest('base64url');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* P1-5 liveness for a bound terminal read capability: `false` means the auth
|
|
230
|
+
* session it was minted under is over (H5 logout/expiry, dashboard token
|
|
231
|
+
* rotation, platform unbind), so the front proxy refuses the capability even
|
|
232
|
+
* though its signature/expiry would still verify at the worker.
|
|
233
|
+
*/
|
|
234
|
+
function terminalAuthSessionLive(authSessionId) {
|
|
235
|
+
if (dashboardSessions.liveAuthSession(authSessionId))
|
|
236
|
+
return true;
|
|
237
|
+
const activeToken = currentDashboardToken();
|
|
238
|
+
if (activeToken && authSessionId === legacyDashboardAuthSessionId(activeToken))
|
|
239
|
+
return true;
|
|
240
|
+
const binding = readPlatformBinding();
|
|
241
|
+
if (binding) {
|
|
242
|
+
const scope = platformDashboardActorScope(binding.machineId);
|
|
243
|
+
if (authSessionId === `${scope}:owner`
|
|
244
|
+
|| authSessionId === `${scope}:teammate`
|
|
245
|
+
|| authSessionId === `${scope}:guest`) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
/** P1-8:预览内容凭据所属的认证会话(校验失败 → null,不予绑定)。 */
|
|
252
|
+
function previewContentCapabilityAuthSession(capability, sessionId) {
|
|
253
|
+
const verified = verifyPreviewContentCapability(SECRET, capability, sessionId);
|
|
254
|
+
return verified.ok ? verified.claims.authSessionId : null;
|
|
255
|
+
}
|
|
256
|
+
/** P1-7:身份判定只此一处(legacy > 平台角色 > H5),门禁选择也只读它的结论。 */
|
|
257
|
+
function dashboardRequestIdentity(req) {
|
|
258
|
+
return resolveDashboardIdentity({
|
|
259
|
+
legacyCookie: parseCookie(req.headers.cookie),
|
|
260
|
+
// The persisted file is the active-token authority (currentDashboardToken);
|
|
261
|
+
// there is no module-level mirror to compare against any more.
|
|
262
|
+
activeToken: currentDashboardToken(),
|
|
263
|
+
roleHeader: req.headers['x-botmux-role'],
|
|
264
|
+
platformMachineId: readPlatformBinding()?.machineId ?? null,
|
|
265
|
+
platformActorScope: platformDashboardActorScope,
|
|
266
|
+
legacyAuthSessionId: legacyDashboardAuthSessionId,
|
|
267
|
+
h5: dashboardH5Auth.resolve(req),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/** P1-8:authSession → 已建立的长连接(/events SSE、Preview SSE/长响应、
|
|
271
|
+
* Preview WS)。身份一结束就遍历关闭,不等对端自己断。 */
|
|
272
|
+
const authSessionConnections = new AuthSessionConnectionRegistry();
|
|
273
|
+
/** P1-11:控制类端点的一次性 CSRF 票据(页面加载现签、绑定认证会话)。 */
|
|
274
|
+
const controlCsrfTokens = new ControlCsrfTokens();
|
|
275
|
+
/**
|
|
276
|
+
* 身份结束的统一收口:H5 logout/到期、legacy token rotate、平台解绑三条来源都
|
|
277
|
+
* 走这里,少走一条就等于留一扇后窗(P1-5 关写租约/读 socket,P1-8 关其余长连接,
|
|
278
|
+
* P1-11 作废该会话签出的 CSRF 票据)。
|
|
279
|
+
*/
|
|
280
|
+
function endDashboardAuthSession(authSessionId) {
|
|
281
|
+
terminalControl.releaseByAuthSession(authSessionId);
|
|
282
|
+
previewInteraction.relockAuthSession(authSessionId);
|
|
283
|
+
authSessionConnections.closeAuthSession(authSessionId);
|
|
284
|
+
controlCsrfTokens.revokeAuthSession(authSessionId);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* P1-8 平台解绑/改绑:`botmux bind|unbind` 先写 platform.json 再捅
|
|
288
|
+
* `/__cli/reload-binding`,所以进入 handler 时磁盘上已经是**新**值——想知道刚
|
|
289
|
+
* 才被吊销的是谁,只能由本进程自己记住上一次认可的 machineId。
|
|
290
|
+
*/
|
|
291
|
+
let observedPlatformMachineId = readPlatformBinding()?.machineId ?? null;
|
|
292
|
+
function syncPlatformBindingRevocation() {
|
|
293
|
+
const current = readPlatformBinding()?.machineId ?? null;
|
|
294
|
+
if (observedPlatformMachineId && observedPlatformMachineId !== current) {
|
|
295
|
+
const scope = platformDashboardActorScope(observedPlatformMachineId);
|
|
296
|
+
for (const role of ['owner', 'teammate', 'guest']) {
|
|
297
|
+
endDashboardAuthSession(`${scope}:${role}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
observedPlatformMachineId = current;
|
|
301
|
+
}
|
|
302
|
+
dashboardSessions.onEnd(identity => {
|
|
303
|
+
// Ends BOTH capabilities of the authentication (P1-5): write leases AND every
|
|
304
|
+
// read socket the auth session opened (bound view-link capabilities included,
|
|
305
|
+
// via the front proxy's read-socket index). New connections with a capability
|
|
306
|
+
// minted under this authSessionId are refused by terminalAuthSessionLive.
|
|
307
|
+
endDashboardAuthSession(identity.authSessionId);
|
|
308
|
+
});
|
|
174
309
|
function tcpPortAvailable(host, port) {
|
|
175
310
|
return new Promise((resolve) => {
|
|
176
311
|
const probe = createTcpServer();
|
|
@@ -222,6 +357,154 @@ function verifyDashboardBinding(port) {
|
|
|
222
357
|
mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
223
358
|
const registry = new DaemonRegistry(REGISTRY_DIR);
|
|
224
359
|
const aggregator = new Aggregator();
|
|
360
|
+
/**
|
|
361
|
+
* P1-13:一个会话的预览目标失效时的收口动作(本进程侧)。
|
|
362
|
+
*
|
|
363
|
+
* 「失效」= 目标没了或换了主人:worker 换代 / 切 CLI / 会话关闭 / 端口被别的进程接管。
|
|
364
|
+
* 这三件事必须一起做,少一件就留一扇后窗:
|
|
365
|
+
* 1. 断掉该会话已经建立的预览 SSE / 长响应 / WebSocket——它们握手时是合法的,此刻
|
|
366
|
+
* 仍在把一个**不再属于这个会话**的进程的内容送进浏览器;
|
|
367
|
+
* 2. 收回该会话上的交互租约(resume 出来的新一代不得继承旧的「交互模式」授权);
|
|
368
|
+
* 3. 让持有会话的 daemon 清掉 previewTarget 并广播 `preview: null`——代理进程只有
|
|
369
|
+
* procfs 的只读视角,改不了会话行。
|
|
370
|
+
*/
|
|
371
|
+
const previewInvalidationsInFlight = new Set();
|
|
372
|
+
function teardownSessionPreview(sessionId) {
|
|
373
|
+
authSessionConnections.closeSessionStreams(sessionId);
|
|
374
|
+
previewInteraction.relockSession(sessionId);
|
|
375
|
+
}
|
|
376
|
+
function invalidateStalePreviewTarget(sessionId, ownerLarkAppId, staleTarget) {
|
|
377
|
+
teardownSessionPreview(sessionId);
|
|
378
|
+
if (!ownerLarkAppId || previewInvalidationsInFlight.has(sessionId))
|
|
379
|
+
return;
|
|
380
|
+
// 每个会话同一时刻只发一次清理请求:刷新一次页面就是几十条子资源请求,逐条捅
|
|
381
|
+
// daemon 会把一次失效放大成一场风暴。拒绝本身已经生效,这里只是让状态收敛。
|
|
382
|
+
previewInvalidationsInFlight.add(sessionId);
|
|
383
|
+
// P1-3:指名要作废的是判定失效的**那一次注册**。这条 DELETE 跨进程飞过去的途中,
|
|
384
|
+
// 会话完全可以合法地重注册一个新目标;不带 revision 的清空会把它一起抹掉。
|
|
385
|
+
const path = `/api/sessions/${encodeURIComponent(sessionId)}/preview`
|
|
386
|
+
+ `?expectedRegisteredAt=${encodeURIComponent(staleTarget.registeredAt)}`;
|
|
387
|
+
void proxyToDaemon(ownerLarkAppId, path, { method: 'DELETE' })
|
|
388
|
+
.catch(() => { })
|
|
389
|
+
.finally(() => previewInvalidationsInFlight.delete(sessionId));
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* 每一跳预览请求的判定入口(HTTP、WebSocket 升级、guard 页面共用同一条)。
|
|
393
|
+
*
|
|
394
|
+
* P1-12:归属复核只读 /proc/net/tcp{,6} 与 /proc/<pid>/stat(全体可读),所以即便
|
|
395
|
+
* 代理进程与 daemon 不是同一个用户也能在每次落地前重新核验。
|
|
396
|
+
*/
|
|
397
|
+
function resolveDashboardSessionPreview(sessionId) {
|
|
398
|
+
const owner = aggregator.ownerOf(sessionId);
|
|
399
|
+
return resolveSessionPreviewForProxy({
|
|
400
|
+
row: aggregator.getSession(sessionId),
|
|
401
|
+
sessionId,
|
|
402
|
+
ownerLarkAppId: owner,
|
|
403
|
+
daemonOnline: !!owner && !!registry.getByAppId(owner),
|
|
404
|
+
isTargetOwned: target => sessionPreviewTargetStillOwned(target),
|
|
405
|
+
onStaleTarget: (staleSessionId, staleTarget) => invalidateStalePreviewTarget(staleSessionId, owner, staleTarget),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
const sessionPreviewProxy = createSessionPreviewProxy({
|
|
409
|
+
// Preview HTTP/WS never accepts ?t=. The user must first establish either
|
|
410
|
+
// the legacy management cookie or an allow-listed short H5 session.
|
|
411
|
+
authenticated: req => dashboardRequestIdentity(req) !== null,
|
|
412
|
+
resolve: resolveDashboardSessionPreview,
|
|
413
|
+
// P0: the sandboxed content stream is an opaque origin, so it carries no
|
|
414
|
+
// cookie of any kind. Its path-scoped capability is the only credential —
|
|
415
|
+
// signature + session binding + expiry here, plus central revocation
|
|
416
|
+
// (logout / token rotation / platform unbind) through the same auth-session
|
|
417
|
+
// liveness the bound terminal read capability uses.
|
|
418
|
+
verifyContentCapability: (capability, sessionId) => {
|
|
419
|
+
const verified = verifyPreviewContentCapability(SECRET, capability, sessionId);
|
|
420
|
+
return verified.ok && terminalAuthSessionLive(verified.claims.authSessionId);
|
|
421
|
+
},
|
|
422
|
+
// P1-8: preview SSE / long responses / WebSocket bridges outlive the handshake
|
|
423
|
+
// that authorised them. Index each stream under its auth session so logout /
|
|
424
|
+
// rotation / unbind tears it down immediately. The content path carries no
|
|
425
|
+
// cookie, so its owner is the capability's own authSessionId.
|
|
426
|
+
// P1-13:同一条流再按 sessionId 建第二个索引。预览目标失效(换代 / 关闭 / 端口易主)
|
|
427
|
+
// 时身份仍然有效,只能靠这个索引定点断流。
|
|
428
|
+
// P1-4:这里同时是「登记点」和「最后一次判定点」。授权发生在拨号之前,而 dev
|
|
429
|
+
// server 的握手最长可以拖 45 秒;这段窗口里的登出/到期/rotate/解绑扫描不到一条
|
|
430
|
+
// 还没入索引的流。所以登记前把身份重新解一遍并复核存活:解不出身份(cookie 那条
|
|
431
|
+
// 路的会话已经没了)或已不存活,一律 fail closed,由代理销毁上游、不回 101/200。
|
|
432
|
+
// P1-1:同一段窗口里换靶也要 fail closed。身份没变、目标却已经换代 / 切 CLI /
|
|
433
|
+
// 端口易主时,旧流握完手照样能拿到 200/101,还会被**重新登记**进索引——换靶那一刻
|
|
434
|
+
// 的 teardown 扫的是索引,扫不到一条还没入索引的流。所以登记前把目标重解一遍
|
|
435
|
+
// (`resolveDashboardSessionPreview` 内含每跳的 owner 复核)并与拨号时那个比指纹:
|
|
436
|
+
// 不是同一次注册就不登记、不回 101/200,由代理销毁上游。
|
|
437
|
+
bindStream: (req, ctx, close) => {
|
|
438
|
+
const authSessionId = ctx.contentCapability
|
|
439
|
+
? previewContentCapabilityAuthSession(ctx.contentCapability, ctx.sessionId)
|
|
440
|
+
: dashboardRequestIdentity(req)?.authSessionId ?? null;
|
|
441
|
+
if (!authSessionId || !terminalAuthSessionLive(authSessionId))
|
|
442
|
+
return false;
|
|
443
|
+
const current = resolveDashboardSessionPreview(ctx.sessionId);
|
|
444
|
+
if (!current.ok || !sameSessionPreviewTarget(current.target, ctx.target))
|
|
445
|
+
return false;
|
|
446
|
+
return authSessionConnections.register(authSessionId, close, ctx.sessionId);
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
/**
|
|
450
|
+
* P1-13:daemon 侧生命周期事件 → 预览收口。
|
|
451
|
+
*
|
|
452
|
+
* daemon 在每个权威换代边界广播 `preview: null`(worker 换代 / suspend / exit /
|
|
453
|
+
* close),会话彻底结束时广播 `session.exited`。中央 Dashboard 订阅这些事件,把本地
|
|
454
|
+
* 还挂着的预览长连接断掉、交互租约收回——否则「服务端已经没有目标了,浏览器那条流
|
|
455
|
+
* 还在流」,而且 resume 之后旧的交互授权会直接落到新一代 CLI 上。
|
|
456
|
+
*
|
|
457
|
+
* P1-1:`session.spawned` 也要认。daemon 重启期间广播的 `preview: null` 是丢的(事件
|
|
458
|
+
* 总线没有 replay buffer),重连之后只以 spawned 重放形式补齐。判据与记忆都在
|
|
459
|
+
* `previewTeardownForDaemonEvent` 里——只有目标指纹**确实变了**才收口,否则每次 SSE
|
|
460
|
+
* 重连都会误杀全部预览长连接。
|
|
461
|
+
*/
|
|
462
|
+
const lastSeenPreviewFingerprints = new Map();
|
|
463
|
+
aggregator.on(ev => {
|
|
464
|
+
const sessionId = previewTeardownForDaemonEvent(ev, lastSeenPreviewFingerprints);
|
|
465
|
+
if (sessionId)
|
|
466
|
+
teardownSessionPreview(sessionId);
|
|
467
|
+
});
|
|
468
|
+
const previewGuardPage = createPreviewGuardPage({
|
|
469
|
+
authenticated: req => dashboardRequestIdentity(req) !== null,
|
|
470
|
+
resolve: resolveDashboardSessionPreview,
|
|
471
|
+
mintContentCapability: (req, sessionId) => {
|
|
472
|
+
const identity = dashboardRequestIdentity(req);
|
|
473
|
+
return identity
|
|
474
|
+
? mintPreviewContentCapability(SECRET, sessionId, {
|
|
475
|
+
userId: identity.userId,
|
|
476
|
+
authSessionId: identity.authSessionId,
|
|
477
|
+
expiresAt: identity.expiresAt,
|
|
478
|
+
})
|
|
479
|
+
: null;
|
|
480
|
+
},
|
|
481
|
+
// P1-11: the guard shell is same-origin with the dashboard and POSTs
|
|
482
|
+
// unlock/activity/lock itself, so it needs its own control ticket.
|
|
483
|
+
mintCsrfToken: req => {
|
|
484
|
+
const identity = dashboardRequestIdentity(req);
|
|
485
|
+
return identity ? controlCsrfTokens.mint(identity.authSessionId) : null;
|
|
486
|
+
},
|
|
487
|
+
// P2:解锁按钮按能力渲染,与工作台面板用同一份投影(canInteract)。平台
|
|
488
|
+
// teammate/guest 这类 previewCapability=readonly 的身份,解锁 POST 本来就会被
|
|
489
|
+
// 下面的 preview-interaction 路由 403;壳里不再画那个按钮,避免「点了才知道
|
|
490
|
+
// 没权限」。这只是不渲染一个必然失败的入口,服务端门禁一分未松。
|
|
491
|
+
canInteract: req => projectWorkbenchOperationCapabilities(dashboardRequestIdentity(req)).canInteract,
|
|
492
|
+
});
|
|
493
|
+
const terminalFrontProxy = createTerminalFrontProxy({
|
|
494
|
+
resolvePort: sessionId => aggregator.terminalProxyPortOf(sessionId),
|
|
495
|
+
resolveActor: dashboardRequestIdentity,
|
|
496
|
+
control: terminalControl,
|
|
497
|
+
// P1-5: bound `?viewToken=` capabilities are refused once the auth session
|
|
498
|
+
// they were minted under ended, and their bridged sockets are indexed so
|
|
499
|
+
// logout/expiry closes them immediately (see dashboardSessions.onEnd).
|
|
500
|
+
viewCapabilityAuthSession: (sessionId, viewToken) => terminalViewCapabilityAuthSession(SECRET, sessionId, viewToken),
|
|
501
|
+
isAuthSessionLive: terminalAuthSessionLive,
|
|
502
|
+
// P1-5: this proxy is the ONLY consumer allowed to spend a view capability.
|
|
503
|
+
// The countersignature proves the loopback hop passed through here — and
|
|
504
|
+
// therefore through the liveness check above — so a raw view URL aimed at the
|
|
505
|
+
// worker port or the daemon's own `/s/` proxy is refused by the worker.
|
|
506
|
+
viewCapabilityForwardProof: viewToken => terminalViewForwardProof(SECRET, viewToken),
|
|
507
|
+
});
|
|
225
508
|
const sessionPresentation = createSessionPresentationCoordinator(aggregator, getGitRepoInfo);
|
|
226
509
|
const groupsMatrixSnapshot = createGroupsMatrixSnapshot(buildGroupsMatrix, {
|
|
227
510
|
onRefreshError: error => logger.warn(`[dashboard] groups matrix refresh failed: ${String(error)}`),
|
|
@@ -328,119 +611,6 @@ function resolveScheduleOwner(id) {
|
|
|
328
611
|
const primary = registry.list().find(d => d.botIndex === 0);
|
|
329
612
|
return primary?.larkAppId;
|
|
330
613
|
}
|
|
331
|
-
/**
|
|
332
|
-
* Bring a freshly-onboarded bot online without a fleet-wide restart by spawning
|
|
333
|
-
* `botmux start-bot <appId> --json` (see cli.ts:ensureBotDaemonStarted). The new
|
|
334
|
-
* daemon is forked+supervised by pm2 (reparented off this process), self-registers
|
|
335
|
-
* and opens its Feishu WSClient, then publishes a descriptor the DaemonRegistry
|
|
336
|
-
* auto-discovers — so no dashboard reload is needed either. Runs `botmux` on the
|
|
337
|
-
* SAME host as the dashboard (shared pm2 home / bots.json — the documented
|
|
338
|
-
* dashboard↔daemon co-location assumption). Resolves best-effort; the caller
|
|
339
|
-
* falls back to the restart hint on failure.
|
|
340
|
-
*/
|
|
341
|
-
function spawnStartBotLive(appId) {
|
|
342
|
-
return new Promise((resolve) => {
|
|
343
|
-
let out = '';
|
|
344
|
-
let err = '';
|
|
345
|
-
let settled = false;
|
|
346
|
-
const done = (r) => { if (!settled) {
|
|
347
|
-
settled = true;
|
|
348
|
-
resolve(r);
|
|
349
|
-
} };
|
|
350
|
-
try {
|
|
351
|
-
const child = spawn(process.execPath, [botmuxCliEntry(), 'start-bot', appId, '--json'], {
|
|
352
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
353
|
-
env: process.env,
|
|
354
|
-
// Run from HOME, not the dashboard's cwd (pm2 `cwd: PKG_ROOT`): a global
|
|
355
|
-
// package update replaces that dir, so a still-running dashboard would spawn
|
|
356
|
-
// start-bot in a deleted directory (uv_cwd/ENOENT). See globalInstallUpdateCwd.
|
|
357
|
-
cwd: globalInstallUpdateCwd(),
|
|
358
|
-
});
|
|
359
|
-
const timer = setTimeout(() => {
|
|
360
|
-
try {
|
|
361
|
-
child.kill('SIGKILL');
|
|
362
|
-
}
|
|
363
|
-
catch { /* already gone */ }
|
|
364
|
-
done({ ok: false, message: 'start-bot 超时(30s)' });
|
|
365
|
-
}, 30_000);
|
|
366
|
-
timer.unref?.();
|
|
367
|
-
child.stdout?.on('data', (d) => { out += String(d); });
|
|
368
|
-
child.stderr?.on('data', (d) => { err += String(d); });
|
|
369
|
-
child.on('error', (e) => {
|
|
370
|
-
clearTimeout(timer);
|
|
371
|
-
done({ ok: false, message: e instanceof Error ? e.message : String(e) });
|
|
372
|
-
});
|
|
373
|
-
child.on('exit', (code) => {
|
|
374
|
-
clearTimeout(timer);
|
|
375
|
-
// `start-bot --json` prints a single StartBotLiveResult object; prefer its
|
|
376
|
-
// own message/processName over the raw exit code.
|
|
377
|
-
let parsed;
|
|
378
|
-
try {
|
|
379
|
-
parsed = JSON.parse(out.trim());
|
|
380
|
-
}
|
|
381
|
-
catch { /* non-JSON → fall through */ }
|
|
382
|
-
if (code === 0) {
|
|
383
|
-
done({ ok: true, message: parsed?.processName ? `${parsed.processName} 已上线` : undefined });
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
done({ ok: false, message: parsed?.message || err.trim() || `start-bot 退出码 ${code}` });
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
catch (e) {
|
|
391
|
-
done({ ok: false, message: e instanceof Error ? e.message : String(e) });
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
function spawnStopBotLive(appId) {
|
|
396
|
-
return new Promise((resolve) => {
|
|
397
|
-
let out = '';
|
|
398
|
-
let err = '';
|
|
399
|
-
let settled = false;
|
|
400
|
-
const done = (r) => { if (!settled) {
|
|
401
|
-
settled = true;
|
|
402
|
-
resolve(r);
|
|
403
|
-
} };
|
|
404
|
-
try {
|
|
405
|
-
const child = spawn(process.execPath, [botmuxCliEntry(), 'stop-bot', appId, '--json'], {
|
|
406
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
407
|
-
env: process.env,
|
|
408
|
-
cwd: globalInstallUpdateCwd(),
|
|
409
|
-
});
|
|
410
|
-
const timer = setTimeout(() => {
|
|
411
|
-
try {
|
|
412
|
-
child.kill('SIGKILL');
|
|
413
|
-
}
|
|
414
|
-
catch { /* already gone */ }
|
|
415
|
-
done({ ok: false, message: 'stop-bot 超时(30s)' });
|
|
416
|
-
}, 30_000);
|
|
417
|
-
timer.unref?.();
|
|
418
|
-
child.stdout?.on('data', (d) => { out += String(d); });
|
|
419
|
-
child.stderr?.on('data', (d) => { err += String(d); });
|
|
420
|
-
child.on('error', (e) => {
|
|
421
|
-
clearTimeout(timer);
|
|
422
|
-
done({ ok: false, message: e instanceof Error ? e.message : String(e) });
|
|
423
|
-
});
|
|
424
|
-
child.on('exit', (code) => {
|
|
425
|
-
clearTimeout(timer);
|
|
426
|
-
let parsed;
|
|
427
|
-
try {
|
|
428
|
-
parsed = JSON.parse(out.trim());
|
|
429
|
-
}
|
|
430
|
-
catch { /* non-JSON */ }
|
|
431
|
-
if (code === 0) {
|
|
432
|
-
done({ ok: true, message: parsed?.processName ? `${parsed.processName} 已停止` : undefined });
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
done({ ok: false, message: parsed?.message || err.trim() || `stop-bot 退出码 ${code}` });
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
catch (e) {
|
|
440
|
-
done({ ok: false, message: e instanceof Error ? e.message : String(e) });
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
614
|
const botOnboarding = new BotOnboardingManager({
|
|
445
615
|
botsJsonPath: BOTS_JSON_PATH,
|
|
446
616
|
stopBotLive: spawnStopBotLive,
|
|
@@ -1025,6 +1195,7 @@ function resolveDashboardSettings() {
|
|
|
1025
1195
|
localDevInstall: isLocalDevInstall(),
|
|
1026
1196
|
autoUpdateSupported: lastSuccessfulUpdatePlan !== undefined || tryResolveGlobalInstallPlan() !== null,
|
|
1027
1197
|
whiteboard: { enabled: global.whiteboard?.enabled === true },
|
|
1198
|
+
workflow: { enabled: global.workflow?.enabled !== false }, // default ON
|
|
1028
1199
|
remoteAccess: global.remoteAccess === true,
|
|
1029
1200
|
scheduleTimeZone: global.scheduleTimeZone ?? null,
|
|
1030
1201
|
hostTimeZone: hostLocalTimeZone(),
|
|
@@ -1102,6 +1273,14 @@ let updateInFlight = false;
|
|
|
1102
1273
|
// the successful plan (including its stable package root) so follow-up status,
|
|
1103
1274
|
// update, and restart requests do not reuse the removed old runtime realpath.
|
|
1104
1275
|
let lastSuccessfulUpdatePlan;
|
|
1276
|
+
// Local-dev counterpart: the checkout a successful /api/update/run built, and
|
|
1277
|
+
// its post-build HEAD. Pinned so the follow-up /api/update/restart applies THIS
|
|
1278
|
+
// build's target — not a wrapper that a concurrent `pnpm use:here` in another
|
|
1279
|
+
// worktree may have re-pointed between the two requests (run builds B, wrapper
|
|
1280
|
+
// flips to C, restart would otherwise restart C or fall back to A). Cleared
|
|
1281
|
+
// once consumed by a restart. A plain "restart" (no preceding run) still
|
|
1282
|
+
// resolves the wrapper live.
|
|
1283
|
+
let pendingLocalDevRestart;
|
|
1105
1284
|
// Cache the upstream version/changelog lookups so the nav-badge check + the
|
|
1106
1285
|
// Settings card don't hammer the npm registry / GitHub on every page load.
|
|
1107
1286
|
// GitHub's unauthenticated API is only 60 req/h per IP, so caching the changelog
|
|
@@ -1181,54 +1360,72 @@ function currentInstalledVersion() {
|
|
|
1181
1360
|
return version === '0.0.0' ? resolveCurrentVersion() : version;
|
|
1182
1361
|
}
|
|
1183
1362
|
/**
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1363
|
+
* Local-dev update: git-clean check (fail closed) → git pull --ff-only →
|
|
1364
|
+
* pnpm build, all in the checkout the global wrapper points at. Mirrors the CLI
|
|
1365
|
+
* `cmdUpgradeLocalDev` via the shared local-dev-update helpers. Returns the
|
|
1366
|
+
* checkout dir, its version before/after, and whether HEAD advanced; the caller
|
|
1367
|
+
* applies the restart through the existing lease/intent path. A successful
|
|
1368
|
+
* build always requires a restart to take effect (dist/ is regenerated), which
|
|
1369
|
+
* the caller signals independently of `changed`. Rejects with a stable `code`
|
|
1370
|
+
* on the recoverable, UI-actionable failures.
|
|
1189
1371
|
*/
|
|
1190
|
-
function
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
}, 180_000);
|
|
1206
|
-
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
1207
|
-
child.on('exit', (code) => {
|
|
1208
|
-
clearTimeout(timer);
|
|
1209
|
-
if (code === 0)
|
|
1210
|
-
resolve();
|
|
1211
|
-
else
|
|
1212
|
-
reject(new Error(`${plan.manager} exited ${code}: ${tail.trim().slice(-500)}`));
|
|
1372
|
+
async function runLocalDevUpdate() {
|
|
1373
|
+
const dir = resolveLocalDevCheckoutDir();
|
|
1374
|
+
if (!isGitWorktree(dir)) {
|
|
1375
|
+
throw Object.assign(new Error(`${dir} is not a git worktree`), { code: 'not_a_worktree', dir });
|
|
1376
|
+
}
|
|
1377
|
+
let status;
|
|
1378
|
+
try {
|
|
1379
|
+
status = gitPorcelainStatus(dir);
|
|
1380
|
+
}
|
|
1381
|
+
catch (e) {
|
|
1382
|
+
throw Object.assign(new Error(e instanceof Error ? e.message : String(e)), { code: 'git_status_failed', dir });
|
|
1383
|
+
}
|
|
1384
|
+
if (status) {
|
|
1385
|
+
throw Object.assign(new Error('working tree has uncommitted changes'), {
|
|
1386
|
+
code: 'dirty_worktree', dir, status,
|
|
1213
1387
|
});
|
|
1214
|
-
}
|
|
1388
|
+
}
|
|
1389
|
+
const before = gitHeadSha(dir);
|
|
1390
|
+
const oldVersion = resolveCurrentVersionAt(dir);
|
|
1391
|
+
for (const { command, args } of localDevUpdateSteps()) {
|
|
1392
|
+
await runLocalDevStep(dir, command, args);
|
|
1393
|
+
}
|
|
1394
|
+
const after = gitHeadSha(dir);
|
|
1395
|
+
const newVersion = resolveCurrentVersionAt(dir);
|
|
1396
|
+
return { dir, changed: before === '' || after === '' ? true : before !== after, oldVersion, newVersion, head: after };
|
|
1215
1397
|
}
|
|
1216
1398
|
/**
|
|
1217
1399
|
* Attach to one daemon: hydrate its sessions/schedules into the aggregator,
|
|
1218
|
-
* THEN open the SSE subscription.
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1400
|
+
* THEN open the SSE subscription.
|
|
1401
|
+
*
|
|
1402
|
+
* The subscription runs a snapshot barrier (subscribeDaemon's `onConnected`):
|
|
1403
|
+
* after every stream establishment — the first included — and BEFORE any
|
|
1404
|
+
* frame is read, we install a fresh authoritative snapshot while incoming
|
|
1405
|
+
* frames stay queued in the stream; frames then apply on top in order. This
|
|
1406
|
+
* gives two guarantees at once:
|
|
1407
|
+
*
|
|
1408
|
+
* 1. No reverse clobber: the barrier snapshot is installed before any frame
|
|
1409
|
+
* is applied, so a slow snapshot response can never overwrite state that
|
|
1410
|
+
* a faster SSE event already delivered (a naive post-subscribe hydrate
|
|
1411
|
+
* would).
|
|
1412
|
+
* 2. No forward gap: events fired between step 1 below and the stream
|
|
1413
|
+
* handshake are picked up by the barrier snapshot, and events missed
|
|
1414
|
+
* during a drop are recovered by the barrier re-run on reconnect.
|
|
1415
|
+
*
|
|
1416
|
+
* The blocking hydrate in step 1 still matters: it populates the cache
|
|
1417
|
+
* before the dashboard starts serving, and keeps a daemon's last-known
|
|
1418
|
+
* state visible even if its SSE stream never connects.
|
|
1221
1419
|
*
|
|
1222
1420
|
* Idempotent: a second call for the same daemon while one is in flight is a
|
|
1223
|
-
* no-op;
|
|
1224
|
-
* restarts and we want to refresh its slice of the cache).
|
|
1421
|
+
* no-op; the subscription itself is installed once.
|
|
1225
1422
|
*/
|
|
1226
1423
|
async function attachDaemon(d) {
|
|
1227
1424
|
if (attaching.has(d.larkAppId))
|
|
1228
1425
|
return;
|
|
1229
1426
|
attaching.add(d.larkAppId);
|
|
1230
1427
|
try {
|
|
1231
|
-
// 1.
|
|
1428
|
+
// 1. Blocking snapshot (see above)
|
|
1232
1429
|
try {
|
|
1233
1430
|
const [sRes, schRes] = await Promise.all([
|
|
1234
1431
|
fetchDaemonIpc(d.ipcPort, '/api/sessions'),
|
|
@@ -1240,20 +1437,42 @@ async function attachDaemon(d) {
|
|
|
1240
1437
|
aggregator.hydrateSessions(d.larkAppId, rows);
|
|
1241
1438
|
for (const row of rows)
|
|
1242
1439
|
sessionPresentation.schedule(d.larkAppId, row);
|
|
1243
|
-
aggregator.hydrateSchedules(sch.schedules ?? []);
|
|
1440
|
+
aggregator.hydrateSchedules(d.larkAppId, sch.schedules ?? []);
|
|
1244
1441
|
}
|
|
1245
1442
|
catch (e) {
|
|
1246
1443
|
logger.warn(`[dashboard] hydrate ${d.larkAppId}: ${e.message ?? e}`);
|
|
1247
1444
|
}
|
|
1248
|
-
// 2. Open SSE subscription if not already (idempotent)
|
|
1445
|
+
// 2. Open SSE subscription if not already (idempotent). The barrier
|
|
1446
|
+
// below runs inside subscribeDaemon, after the stream is established.
|
|
1249
1447
|
if (!subs.has(d.larkAppId)) {
|
|
1250
|
-
subs.set(d.larkAppId, subscribeDaemon(d, aggregator, e => logger.warn(`[aggregator] ${d.larkAppId}: ${e.message}`), (_url, init) => fetchDaemonIpc(d.ipcPort, '/api/events', init)
|
|
1448
|
+
subs.set(d.larkAppId, subscribeDaemon(d, aggregator, e => logger.warn(`[aggregator] ${d.larkAppId}: ${e.message}`), (_url, init) => fetchDaemonIpc(d.ipcPort, '/api/events', init),
|
|
1449
|
+
// Snapshot barrier: install an authoritative snapshot before any
|
|
1450
|
+
// frame is read. Frames arriving during this fetch stay queued in
|
|
1451
|
+
// the stream and apply afterwards, so the snapshot can never
|
|
1452
|
+
// clobber fresher SSE state; on reconnect it recovers missed
|
|
1453
|
+
// events. The subscription signal is the generation arbitration:
|
|
1454
|
+
// if aborted mid-flight (daemon offline, newer generation), the
|
|
1455
|
+
// snapshot is discarded instead of clobbering the new generation.
|
|
1456
|
+
signal => reconcileDaemon(d, signal)));
|
|
1251
1457
|
}
|
|
1252
1458
|
}
|
|
1253
1459
|
finally {
|
|
1254
1460
|
attaching.delete(d.larkAppId);
|
|
1255
1461
|
}
|
|
1256
1462
|
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Reconcile one daemon's snapshot into the aggregator (subscribeDaemon
|
|
1465
|
+
* barrier). Thin wrapper over reconcileDaemonSnapshot that also schedules
|
|
1466
|
+
* presentation enrichment for the session rows.
|
|
1467
|
+
*/
|
|
1468
|
+
async function reconcileDaemon(d, signal) {
|
|
1469
|
+
const snapshot = await reconcileDaemonSnapshot(d, aggregator, signal);
|
|
1470
|
+
if (!snapshot)
|
|
1471
|
+
return;
|
|
1472
|
+
for (const row of snapshot.sessions) {
|
|
1473
|
+
sessionPresentation.schedule(d.larkAppId, row);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1257
1476
|
function syncSubscriptions() {
|
|
1258
1477
|
const daemons = registry.list();
|
|
1259
1478
|
const online = new Set(daemons.map(d => d.larkAppId));
|
|
@@ -1396,7 +1615,7 @@ function injectDevReload(html) {
|
|
|
1396
1615
|
const snippet = devReloadSnippet();
|
|
1397
1616
|
return html.includes('</body>') ? html.replace('</body>', `${snippet}\n</body>`) : `${html}\n${snippet}`;
|
|
1398
1617
|
}
|
|
1399
|
-
function serveStatic(req, res, pathname) {
|
|
1618
|
+
function serveStatic(req, res, pathname, options = {}) {
|
|
1400
1619
|
const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
|
1401
1620
|
const fp = resolve(WEB_DIR, rel);
|
|
1402
1621
|
const webRoot = resolve(WEB_DIR);
|
|
@@ -1413,13 +1632,17 @@ function serveStatic(req, res, pathname) {
|
|
|
1413
1632
|
// and can be cached immutably once the current app.js points at them.
|
|
1414
1633
|
const immutableChunk = relToRoot.startsWith('chunks/') || relToRoot.startsWith('chunks\\');
|
|
1415
1634
|
const etag = `W/"${st.size.toString(16)}-${Math.floor(st.mtimeMs).toString(16)}"`;
|
|
1416
|
-
const
|
|
1635
|
+
const isIndex = relToRoot === 'index.html';
|
|
1636
|
+
const devIndex = isIndex && dashboardDevReloadEnabled();
|
|
1637
|
+
// 注入 CSRF 票据的壳是每次请求现生成的:ETag 只反映磁盘文件,走 304 会让浏览
|
|
1638
|
+
// 器复用上一次(可能已随认证结束作废)的票据,所以这条路径不缓存、不 304。
|
|
1639
|
+
const dynamicIndex = devIndex || (isIndex && !!options.injectHtml);
|
|
1417
1640
|
const headers = {
|
|
1418
1641
|
'content-type': MIME[extname(fp)] ?? 'application/octet-stream',
|
|
1419
|
-
'cache-control':
|
|
1642
|
+
'cache-control': dynamicIndex ? 'no-store' : immutableChunk ? 'public, max-age=31536000, immutable' : 'no-cache',
|
|
1420
1643
|
etag,
|
|
1421
1644
|
};
|
|
1422
|
-
if (!
|
|
1645
|
+
if (!dynamicIndex && req.headers['if-none-match'] === etag) {
|
|
1423
1646
|
res.writeHead(304, headers);
|
|
1424
1647
|
res.end();
|
|
1425
1648
|
return true;
|
|
@@ -1429,8 +1652,13 @@ function serveStatic(req, res, pathname) {
|
|
|
1429
1652
|
res.end();
|
|
1430
1653
|
return true;
|
|
1431
1654
|
}
|
|
1432
|
-
if (
|
|
1433
|
-
|
|
1655
|
+
if (dynamicIndex) {
|
|
1656
|
+
let html = readFileSync(fp, 'utf8');
|
|
1657
|
+
if (options.injectHtml)
|
|
1658
|
+
html = options.injectHtml(html);
|
|
1659
|
+
if (devIndex)
|
|
1660
|
+
html = injectDevReload(html);
|
|
1661
|
+
res.end(html);
|
|
1434
1662
|
}
|
|
1435
1663
|
else {
|
|
1436
1664
|
res.end(readFileSync(fp));
|
|
@@ -1911,6 +2139,7 @@ function configuredBotAgentFields() {
|
|
|
1911
2139
|
wrapperCli: b.wrapperCli,
|
|
1912
2140
|
model: b.model,
|
|
1913
2141
|
reasoningEffort: b.reasoningEffort,
|
|
2142
|
+
turnTimeoutMs: b.turnTimeoutMs,
|
|
1914
2143
|
}]));
|
|
1915
2144
|
}
|
|
1916
2145
|
catch {
|
|
@@ -1928,6 +2157,7 @@ function withConfiguredCliId(bot, ids) {
|
|
|
1928
2157
|
wrapperCli: bot.wrapperCli || fallback?.wrapperCli,
|
|
1929
2158
|
model: bot.model || fallback?.model,
|
|
1930
2159
|
reasoningEffort: bot.reasoningEffort || fallback?.reasoningEffort,
|
|
2160
|
+
turnTimeoutMs: bot.turnTimeoutMs ?? fallback?.turnTimeoutMs,
|
|
1931
2161
|
};
|
|
1932
2162
|
}
|
|
1933
2163
|
function liveBots() {
|
|
@@ -2205,9 +2435,11 @@ async function closeSessionsMatching(pred) {
|
|
|
2205
2435
|
body = JSON.parse(text);
|
|
2206
2436
|
}
|
|
2207
2437
|
catch { /* tolerate */ }
|
|
2438
|
+
const residual = body?.ok ? parseCloseResidual(body) : undefined;
|
|
2208
2439
|
return {
|
|
2209
2440
|
sessionId: s.sessionId,
|
|
2210
2441
|
ok: !!body?.ok,
|
|
2442
|
+
...(residual ? { residual } : {}),
|
|
2211
2443
|
error: body?.ok ? undefined : (body?.error ?? `http_${upstream.status}`),
|
|
2212
2444
|
};
|
|
2213
2445
|
}
|
|
@@ -2331,8 +2563,9 @@ function dashboardSkillCliIds() {
|
|
|
2331
2563
|
const ids = new Set();
|
|
2332
2564
|
// Always scan all known CLI skill dirs, not just configured bots — users may
|
|
2333
2565
|
// want to discover codex/trae/... skills even before creating a bot for them.
|
|
2334
|
-
|
|
2335
|
-
|
|
2566
|
+
// Derived from the closed Record<CliId,…> in the registry — a hand-typed
|
|
2567
|
+
// literal here silently omitted reasonix and mojo, hiding their skill dirs.
|
|
2568
|
+
for (const cliId of ALL_CLI_IDS)
|
|
2336
2569
|
ids.add(cliId);
|
|
2337
2570
|
try {
|
|
2338
2571
|
for (const cliId of configuredCliIds().values())
|
|
@@ -2492,14 +2725,51 @@ async function dashboardSkillReferencesMany(skillNames) {
|
|
|
2492
2725
|
async function dashboardSkillReferences(skillName) {
|
|
2493
2726
|
return (await dashboardSkillReferencesMany([skillName])).get(skillName) ?? { bots: [], packs: [] };
|
|
2494
2727
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2728
|
+
function dashboardControlJson(res, status, body) {
|
|
2729
|
+
res.writeHead(status, {
|
|
2730
|
+
'content-type': 'application/json; charset=utf-8',
|
|
2731
|
+
'cache-control': 'no-store',
|
|
2732
|
+
'referrer-policy': 'no-referrer',
|
|
2733
|
+
});
|
|
2734
|
+
res.end(JSON.stringify(body));
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* P1-11:控制类端点的跨站伪造门禁。返回 false 表示已经写完 403 响应,调用方直接
|
|
2738
|
+
* return。只作用于「有副作用且可被无 body 表单触发」的 POST:终端接管/释放、
|
|
2739
|
+
* 预览交互 unlock/activity/lock、话题定位。
|
|
2740
|
+
*
|
|
2741
|
+
* GET 不进门禁(读没有副作用,且壳/前端的状态轮询本身就是 GET);Preview 自身的
|
|
2742
|
+
* 不透明来源请求走 preview 专用路径的路径内凭据,不经过这里。
|
|
2743
|
+
*/
|
|
2744
|
+
function enforceControlCsrf(req, res, identity) {
|
|
2745
|
+
if ((req.method ?? 'GET').toUpperCase() === 'GET')
|
|
2746
|
+
return true;
|
|
2747
|
+
const verdict = guardControlRequest({
|
|
2748
|
+
headers: req.headers,
|
|
2749
|
+
authSessionId: identity.authSessionId,
|
|
2750
|
+
tokens: controlCsrfTokens,
|
|
2751
|
+
});
|
|
2752
|
+
if (verdict.ok)
|
|
2753
|
+
return true;
|
|
2754
|
+
dashboardControlJson(res, verdict.status, { ok: false, error: verdict.error });
|
|
2755
|
+
return false;
|
|
2756
|
+
}
|
|
2757
|
+
function terminalControlAvailability(sessionId) {
|
|
2758
|
+
const row = aggregator.getSession(sessionId);
|
|
2759
|
+
if (!row || !aggregator.ownerOf(sessionId))
|
|
2760
|
+
return { ok: false, status: 404, error: 'unknown_session' };
|
|
2761
|
+
if (row.status === 'closed')
|
|
2762
|
+
return { ok: false, status: 409, error: 'session_not_active' };
|
|
2763
|
+
if (typeof row.riffAccessUrl === 'string' && row.riffAccessUrl) {
|
|
2764
|
+
return { ok: false, status: 409, error: 'terminal_external_only' };
|
|
2765
|
+
}
|
|
2766
|
+
const owner = aggregator.ownerOf(sessionId);
|
|
2767
|
+
if (!owner || !registry.getByAppId(owner))
|
|
2768
|
+
return { ok: false, status: 503, error: 'daemon_offline' };
|
|
2769
|
+
if (!aggregator.terminalProxyPortOf(sessionId) || typeof row.webPort !== 'number') {
|
|
2770
|
+
return { ok: false, status: 409, error: 'terminal_unavailable' };
|
|
2771
|
+
}
|
|
2772
|
+
return { ok: true };
|
|
2503
2773
|
}
|
|
2504
2774
|
/**
|
|
2505
2775
|
* Read every currently-online daemon directly for the public dashboard
|
|
@@ -2578,6 +2848,19 @@ const server = createServer(async (req, res) => {
|
|
|
2578
2848
|
handleDesktopCompat(req, res, url, { machineId: compatMachineId ?? undefined });
|
|
2579
2849
|
return;
|
|
2580
2850
|
}
|
|
2851
|
+
// Configurable Feishu/Lark H5 passwordless entry and code exchange. It is
|
|
2852
|
+
// self-authenticating and must run before the ordinary Dashboard cookie
|
|
2853
|
+
// gate; no authorization code or session capability is ever put in a URL.
|
|
2854
|
+
if (await dashboardH5Auth.handle(req, res, url))
|
|
2855
|
+
return;
|
|
2856
|
+
// Session Web preview is an authenticated, same-origin reverse proxy to an
|
|
2857
|
+
// agent-registered literal loopback target. It owns its auth gate because
|
|
2858
|
+
// WebSocket upgrades do not pass through decideDashboardAuth; using one
|
|
2859
|
+
// manager for HTTP + WS keeps the cookie/ownership/SSRF contract identical.
|
|
2860
|
+
if (previewGuardPage.handle(req, res, url))
|
|
2861
|
+
return;
|
|
2862
|
+
if (await sessionPreviewProxy.handleHttp(req, res, url))
|
|
2863
|
+
return;
|
|
2581
2864
|
// Web terminal reverse-proxy: `/s/<sessionId>/*` → the owning bot daemon's
|
|
2582
2865
|
// terminal proxy. The central platform only tunnels the dashboard port, so
|
|
2583
2866
|
// terminal links served under the machine subdomain
|
|
@@ -2588,25 +2871,13 @@ const server = createServer(async (req, res) => {
|
|
|
2588
2871
|
// response straight back. Mounted before the dashboard auth gate because the
|
|
2589
2872
|
// worker independently requires a view/write capability or authenticated
|
|
2590
2873
|
// dashboard cookie before serving either HTTP or WebSocket terminal data.
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
}
|
|
2598
|
-
const upstream = httpRequest({ host: '127.0.0.1', port: tport, method: req.method, path: req.url, headers: req.headers }, (up) => {
|
|
2599
|
-
res.writeHead(up.statusCode ?? 502, up.headers);
|
|
2600
|
-
up.pipe(res);
|
|
2601
|
-
});
|
|
2602
|
-
upstream.on('error', () => {
|
|
2603
|
-
if (!res.headersSent)
|
|
2604
|
-
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
2605
|
-
res.end('terminal proxy error');
|
|
2606
|
-
});
|
|
2607
|
-
req.pipe(upstream);
|
|
2874
|
+
// Since P1-5 this hop is also LOAD-BEARING for revocation, not just
|
|
2875
|
+
// reachability: a signed view capability is checked here against live auth
|
|
2876
|
+
// sessions and countersigned for the worker, which refuses one that arrived
|
|
2877
|
+
// any other way. That is why the view-link API hands out a same-origin path
|
|
2878
|
+
// instead of the daemon/worker origin — this is the only door.
|
|
2879
|
+
if (terminalFrontProxy.handleHttp(req, res, url))
|
|
2608
2880
|
return;
|
|
2609
|
-
}
|
|
2610
2881
|
if (await handleWebhookRoute(req, res, url, {
|
|
2611
2882
|
proxyToDaemon,
|
|
2612
2883
|
createLifecycleGroup: createLifecycleGroupForWebhook,
|
|
@@ -2677,8 +2948,27 @@ const server = createServer(async (req, res) => {
|
|
|
2677
2948
|
const gate = verifyCliRequest(req, url.pathname);
|
|
2678
2949
|
if (!gate.ok)
|
|
2679
2950
|
return jsonRes(res, gate.status, gate.body);
|
|
2951
|
+
const previousToken = currentDashboardToken();
|
|
2680
2952
|
try {
|
|
2681
2953
|
const token = rotatePersistedToken(TOKEN_PATH);
|
|
2954
|
+
// The previous link is dead once the write lands: drop terminal control
|
|
2955
|
+
// leases, re-lock preview interaction, and (P1-8) close every long-lived
|
|
2956
|
+
// connection the old link opened — otherwise the old management SSE
|
|
2957
|
+
// keeps streaming rows minted AFTER the rotation, including freshly
|
|
2958
|
+
// issued `riffAccessUrl` write credentials.
|
|
2959
|
+
// Ordered after the durable write so a failed rotation keeps both the
|
|
2960
|
+
// old token and its live grants intact.
|
|
2961
|
+
if (previousToken && previousToken !== token) {
|
|
2962
|
+
endDashboardAuthSession(legacyDashboardAuthSessionId(previousToken));
|
|
2963
|
+
}
|
|
2964
|
+
// P1-6: the same reasoning covers the workbench entry tickets sitting in
|
|
2965
|
+
// Feishu card history. They redeem into "whatever token is active now",
|
|
2966
|
+
// so without this a ticket leaked BEFORE the rotation would hand out the
|
|
2967
|
+
// freshly minted management cookie — rotation would protect nothing in
|
|
2968
|
+
// exactly the case it is used for. Ticket verification independently
|
|
2969
|
+
// requires the bound generation to still be current, so this call is the
|
|
2970
|
+
// cleanup (drop dead rows from the shared file), not the guarantee.
|
|
2971
|
+
revokeWorkbenchTicketsOutsideGeneration(workbenchTicketGeneration(token));
|
|
2682
2972
|
return jsonRes(res, 200, dashboardUrlsFor(token));
|
|
2683
2973
|
}
|
|
2684
2974
|
catch (e) {
|
|
@@ -2737,29 +3027,60 @@ const server = createServer(async (req, res) => {
|
|
|
2737
3027
|
/* ignore */
|
|
2738
3028
|
}
|
|
2739
3029
|
platformTunnel = null;
|
|
3030
|
+
// P1-8:解绑/改绑之后旧平台身份的短请求立刻 401,但它建立的 SSE / Preview
|
|
3031
|
+
// 长连接不会自己断,三个角色作用域在这里一起收口。
|
|
3032
|
+
syncPlatformBindingRevocation();
|
|
2740
3033
|
startPlatformTunnelIfBound();
|
|
2741
3034
|
return jsonRes(res, 200, { ok: true });
|
|
2742
3035
|
}
|
|
2743
3036
|
const activeToken = currentDashboardToken();
|
|
2744
|
-
const
|
|
3037
|
+
const requestIdentity = dashboardRequestIdentity(req);
|
|
2745
3038
|
const globalDashboardConfig = readGlobalConfig().dashboard;
|
|
2746
|
-
const
|
|
3039
|
+
const publicReadOnly = globalDashboardConfig?.publicReadOnly
|
|
3040
|
+
?? config.dashboard.publicReadOnly;
|
|
3041
|
+
// P1-7:门禁选择与身份判定同源。旧代码在这里另算一遍 `h5Identity`,于是
|
|
3042
|
+
// 「legacy owner + H5 cookie 并存」的浏览器被判成 workbench-only,管理读写
|
|
3043
|
+
// 全 401,连正确的 `?t=` 也被一起清掉(详见 request-identity.ts 顶注)。
|
|
3044
|
+
// Only the local legacy Dashboard cookie is management authority. Platform
|
|
3045
|
+
// identities — owner included — retain terminal/preview capability through
|
|
3046
|
+
// signed proxy grants, but cannot cross into host administration APIs.
|
|
3047
|
+
const { legacyAuthed, workbenchOnlyIdentity, decision } = resolveDashboardRequestGate({
|
|
2747
3048
|
method: req.method ?? 'GET',
|
|
2748
3049
|
pathname: url.pathname,
|
|
2749
3050
|
hasTokenParam: url.searchParams.has('t'),
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
3051
|
+
identity: requestIdentity,
|
|
3052
|
+
tokenFromRequest: authedToken(req, url, activeToken),
|
|
3053
|
+
activeToken,
|
|
3054
|
+
publicReadOnly,
|
|
3055
|
+
});
|
|
3056
|
+
// `authed` is deliberately the local management capability, not merely a
|
|
3057
|
+
// valid Workbench/platform identity. Privileged mutations and management
|
|
3058
|
+
// reads (settings, schedules, groups) therefore cannot be widened by H5
|
|
3059
|
+
// authentication.
|
|
3060
|
+
const authed = legacyAuthed;
|
|
3061
|
+
// The session board is the one surface where `!authed` must NOT mean
|
|
3062
|
+
// "anonymous". `/api/sessions` and `/events` are exactly the two paths
|
|
3063
|
+
// workbenchH5Capability grants as `workbench.view`, and the same identity
|
|
3064
|
+
// holds `preview.view`/`preview.operate`; reusing the anonymous projection
|
|
3065
|
+
// here deleted the `preview` descriptor those capabilities operate on, so
|
|
3066
|
+
// the mobile Workbench and the Dock showed 「无网页预览」 unconditionally.
|
|
3067
|
+
// The three-way audience keeps management/anonymous behavior byte-identical
|
|
3068
|
+
// and only restores display fields for an authenticated Workbench viewer —
|
|
3069
|
+
// the Riff sandbox bearer write URL stays stripped (see public-redact.ts).
|
|
3070
|
+
const sessionBoardAudience = sessionBoardAudienceFor({
|
|
3071
|
+
legacyAuthed,
|
|
3072
|
+
workbenchIdentity: workbenchOnlyIdentity,
|
|
2754
3073
|
});
|
|
2755
|
-
// `authed` is consumed by route handlers that distinguish the public-read
|
|
2756
|
-
// carve-out from a valid management cookie (notably v3 run details).
|
|
2757
|
-
const authed = !!presentedToken && presentedToken === activeToken && !!activeToken;
|
|
2758
3074
|
if (decision.kind === 'deny401') {
|
|
2759
3075
|
const loginUrl = buildPlatformDashboardLoginUrl();
|
|
2760
3076
|
res.writeHead(401, {
|
|
2761
3077
|
'content-type': 'text/html; charset=utf-8',
|
|
2762
3078
|
'cache-control': 'no-store',
|
|
3079
|
+
// A valid H5/platform Workbench identity is expected to be denied by
|
|
3080
|
+
// management-only endpoints such as /api/settings. Let the SPA tell
|
|
3081
|
+
// that narrow denial from an expired identity, otherwise its global
|
|
3082
|
+
// fetch wrapper covers a healthy Workbench with the login overlay.
|
|
3083
|
+
...(workbenchOnlyIdentity ? { 'x-botmux-auth-scope': 'workbench' } : {}),
|
|
2763
3084
|
...(loginUrl ? { 'x-botmux-login-url': loginUrl } : {}),
|
|
2764
3085
|
});
|
|
2765
3086
|
res.end('<h1>Token expired</h1><p>Run <code>botmux dashboard</code> to get a fresh URL.</p>');
|
|
@@ -2773,6 +3094,20 @@ const server = createServer(async (req, res) => {
|
|
|
2773
3094
|
res.end();
|
|
2774
3095
|
return;
|
|
2775
3096
|
}
|
|
3097
|
+
// P2-1:飞书卡片「打开工作台」按钮的短时票据兑换,紧挨上面的 ?t= set-cookie
|
|
3098
|
+
// 流程——语义同款:验票通过就种同一个 legacy cookie,再 302 进工作台。票据由
|
|
3099
|
+
// /dashboard 卡片构建时现 mint(TTL 30 分钟、可多端重复打开,落盘只存 hash,
|
|
3100
|
+
// 见 dashboard/workbench-ticket.ts),长期管理 token 从此不再写进持久化卡片;
|
|
3101
|
+
// 无效/过期回一个无凭据中文提示页。该 GET 在 decideDashboardAuth 里与静态壳
|
|
3102
|
+
// 同级放行(票据本身就是凭证),其余方法不豁免。P1-10:正因为它在 auth gate
|
|
3103
|
+
// 之前放行,端点自带每 IP + 全局限流,IP 口径与 H5 兑换口共用同一份可信代理
|
|
3104
|
+
// 配置——否则两个公开面对同一个 `x-forwarded-for` 会得出不同结论。
|
|
3105
|
+
if (handleWorkbenchTicketRedemption(req, res, url, {
|
|
3106
|
+
activeToken: () => activeToken,
|
|
3107
|
+
trustedProxyHops: dashboardH5AuthConfig.trustedProxyHops,
|
|
3108
|
+
})) {
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
2776
3111
|
if (url.pathname === '/api/workflows' || url.pathname.startsWith('/api/workflows/')) {
|
|
2777
3112
|
return jsonRes(res, 410, {
|
|
2778
3113
|
ok: false,
|
|
@@ -2780,6 +3115,128 @@ const server = createServer(async (req, res) => {
|
|
|
2780
3115
|
message: 'v2 workflow dashboard APIs are retired; use /api/v3/runs for v3 run visibility',
|
|
2781
3116
|
});
|
|
2782
3117
|
}
|
|
3118
|
+
// Authenticated, non-secret metadata used only to build Feishu appCenter
|
|
3119
|
+
// and >=350px sidebar AppLinks. App secret and allowlist never cross this
|
|
3120
|
+
// projection; the route is intentionally absent from public-read allowlists.
|
|
3121
|
+
if (req.method === 'GET' && url.pathname === '/api/workbench/h5-context') {
|
|
3122
|
+
return jsonRes(res, 200, {
|
|
3123
|
+
ok: true,
|
|
3124
|
+
h5: {
|
|
3125
|
+
enabled: dashboardH5AuthConfig.enabled,
|
|
3126
|
+
appId: dashboardH5AuthConfig.appId,
|
|
3127
|
+
brand: dashboardH5AuthConfig.brand,
|
|
3128
|
+
entryPath: dashboardH5AuthConfig.entryPath,
|
|
3129
|
+
},
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
// P1-4:最小操作能力集投影。前端只据此渲染操作入口(定位 / 接管输入 / 开启
|
|
3133
|
+
// 交互),投影函数复算的是本文件三条真实路由的同一套门禁(路由级 auth 决策 +
|
|
3134
|
+
// terminalCapability/previewCapability 角色检查),见 auth.ts 的函数注释。
|
|
3135
|
+
// 匿名请求到不了这里(该路径不在 publicReadOnly 白名单,decideDashboardAuth
|
|
3136
|
+
// 已 401);前端把任何非 200/缺字段一律回落为全 false。注意 canControl 只描述
|
|
3137
|
+
// 无显式 token 的默认能力——显式 write token 走终端前置代理的独立授权(P1-6),
|
|
3138
|
+
// 与本投影无关。
|
|
3139
|
+
if (req.method === 'GET' && url.pathname === '/api/workbench/capabilities') {
|
|
3140
|
+
return jsonRes(res, 200, {
|
|
3141
|
+
ok: true,
|
|
3142
|
+
capabilities: projectWorkbenchOperationCapabilities(requestIdentity),
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
// owner 在工作台内自取常驻链接(`<base>/workbench?t=<当前活跃 token>`)。
|
|
3146
|
+
// 只有本机完整管理身份能取:上面的门禁已经把 workbench-only / 平台角色 /
|
|
3147
|
+
// 匿名 deny401,处理器再自己判一次 kind === 'legacy-dashboard'(两层独立,
|
|
3148
|
+
// 见 dashboard/standing-link.ts 顶注)。同源校验 + no-store + 每次落一条
|
|
3149
|
+
// `auth.standing_link_issued` 审计;token 现读落盘的活跃值,所以
|
|
3150
|
+
// `dashboard rotate` 之后这里自然发新链接。
|
|
3151
|
+
if (handleWorkbenchStandingLink(req, res, url, {
|
|
3152
|
+
identity: requestIdentity,
|
|
3153
|
+
activeToken: () => activeToken,
|
|
3154
|
+
standingLinkUrl: token => workbenchEntryUrl(dashboardUrlsFor(token).url),
|
|
3155
|
+
audit: dashboardControlAudit,
|
|
3156
|
+
})) {
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
// Server-authoritative terminal control lease. The API returns only mode
|
|
3160
|
+
// and timestamps; its signed read/write grant stays inside the central
|
|
3161
|
+
// proxy and is never placed in a URL or response body.
|
|
3162
|
+
let controlMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/control(?:\/(takeover|release))?$/);
|
|
3163
|
+
if (controlMatch) {
|
|
3164
|
+
if (!requestIdentity) {
|
|
3165
|
+
return dashboardControlJson(res, 401, { ok: false, error: 'authentication_required' });
|
|
3166
|
+
}
|
|
3167
|
+
if (!enforceControlCsrf(req, res, requestIdentity))
|
|
3168
|
+
return;
|
|
3169
|
+
let sessionId;
|
|
3170
|
+
try {
|
|
3171
|
+
sessionId = decodeURIComponent(controlMatch[1]);
|
|
3172
|
+
}
|
|
3173
|
+
catch {
|
|
3174
|
+
return dashboardControlJson(res, 400, { ok: false, error: 'invalid_session_id' });
|
|
3175
|
+
}
|
|
3176
|
+
const availability = terminalControlAvailability(sessionId);
|
|
3177
|
+
if (!availability.ok) {
|
|
3178
|
+
return dashboardControlJson(res, availability.status, { ok: false, error: availability.error });
|
|
3179
|
+
}
|
|
3180
|
+
const action = controlMatch[2];
|
|
3181
|
+
if (req.method === 'GET' && !action) {
|
|
3182
|
+
return dashboardControlJson(res, 200, { ok: true, ...terminalControl.state(requestIdentity, sessionId) });
|
|
3183
|
+
}
|
|
3184
|
+
if (requestIdentity.terminalCapability === 'readonly') {
|
|
3185
|
+
return dashboardControlJson(res, 403, { ok: false, error: 'terminal_operation_forbidden' });
|
|
3186
|
+
}
|
|
3187
|
+
if (req.method === 'POST' && action === 'takeover') {
|
|
3188
|
+
const result = terminalControl.takeover(requestIdentity, sessionId);
|
|
3189
|
+
const status = result.ok ? 200 : result.error === 'control_busy' ? 409 : 401;
|
|
3190
|
+
return dashboardControlJson(res, status, result.ok ? { ...result, owned: true } : { ok: false, error: result.error });
|
|
3191
|
+
}
|
|
3192
|
+
if (req.method === 'POST' && action === 'release') {
|
|
3193
|
+
const result = terminalControl.release(requestIdentity, sessionId);
|
|
3194
|
+
return dashboardControlJson(res, result.ok ? 200 : 403, result.ok ? { ...result, owned: false } : { ok: false, error: result.error });
|
|
3195
|
+
}
|
|
3196
|
+
return dashboardControlJson(res, 405, { ok: false, error: 'method_not_allowed' });
|
|
3197
|
+
}
|
|
3198
|
+
// Preview interaction is separately scoped per authenticated browser
|
|
3199
|
+
// session. Default is always the visibly labelled preview overlay; unlock
|
|
3200
|
+
// and activity are explicit, and the server hard-relocks after 15m idle.
|
|
3201
|
+
controlMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/preview-interaction(?:\/(unlock|activity|lock))?$/);
|
|
3202
|
+
if (controlMatch) {
|
|
3203
|
+
if (!requestIdentity) {
|
|
3204
|
+
return dashboardControlJson(res, 401, { ok: false, error: 'authentication_required' });
|
|
3205
|
+
}
|
|
3206
|
+
if (!enforceControlCsrf(req, res, requestIdentity))
|
|
3207
|
+
return;
|
|
3208
|
+
let sessionId;
|
|
3209
|
+
try {
|
|
3210
|
+
sessionId = decodeURIComponent(controlMatch[1]);
|
|
3211
|
+
}
|
|
3212
|
+
catch {
|
|
3213
|
+
return dashboardControlJson(res, 400, { ok: false, error: 'invalid_session_id' });
|
|
3214
|
+
}
|
|
3215
|
+
const resolution = resolveDashboardSessionPreview(sessionId);
|
|
3216
|
+
if (!resolution.ok) {
|
|
3217
|
+
return dashboardControlJson(res, resolution.status, { ok: false, error: resolution.error });
|
|
3218
|
+
}
|
|
3219
|
+
const action = controlMatch[2];
|
|
3220
|
+
if (req.method === 'GET' && !action) {
|
|
3221
|
+
return dashboardControlJson(res, 200, { ok: true, ...previewInteraction.state(requestIdentity, sessionId) });
|
|
3222
|
+
}
|
|
3223
|
+
// 唯一权威的角色门禁。guard 壳与工作台是否渲染解锁按钮,走的是同一个
|
|
3224
|
+
// previewInteractionWriteAllowed(经 canInteract 投影),所以「按能力隐藏
|
|
3225
|
+
// 按钮」永远只是把一次必然 403 的点击省掉,不会替代这里的检查。
|
|
3226
|
+
if (!previewInteractionWriteAllowed(requestIdentity)) {
|
|
3227
|
+
return dashboardControlJson(res, 403, { ok: false, error: 'preview_operation_forbidden' });
|
|
3228
|
+
}
|
|
3229
|
+
if (req.method === 'POST' && action === 'unlock') {
|
|
3230
|
+
return dashboardControlJson(res, 200, { ok: true, ...previewInteraction.unlock(requestIdentity, sessionId) });
|
|
3231
|
+
}
|
|
3232
|
+
if (req.method === 'POST' && action === 'activity') {
|
|
3233
|
+
return dashboardControlJson(res, 200, { ok: true, ...previewInteraction.activity(requestIdentity, sessionId) });
|
|
3234
|
+
}
|
|
3235
|
+
if (req.method === 'POST' && action === 'lock') {
|
|
3236
|
+
return dashboardControlJson(res, 200, { ok: true, ...previewInteraction.lock(requestIdentity, sessionId) });
|
|
3237
|
+
}
|
|
3238
|
+
return dashboardControlJson(res, 405, { ok: false, error: 'method_not_allowed' });
|
|
3239
|
+
}
|
|
2783
3240
|
if (url.pathname.startsWith('/api/feedback/analytics/')) {
|
|
2784
3241
|
await handleFeedbackAnalyticsApi(req, res, url, { service: analyticsService() });
|
|
2785
3242
|
return;
|
|
@@ -2815,6 +3272,10 @@ const server = createServer(async (req, res) => {
|
|
|
2815
3272
|
if (url.pathname === '/api/debug-terminal'
|
|
2816
3273
|
|| url.pathname.startsWith('/api/debug-terminal/')
|
|
2817
3274
|
|| url.pathname.startsWith('/debug-terminal/')) {
|
|
3275
|
+
// H5 sessions are scoped to Dashboard/workbench control. They never
|
|
3276
|
+
// inherit the legacy owner's unrestricted debug shell.
|
|
3277
|
+
if (!legacyAuthed)
|
|
3278
|
+
return jsonRes(res, 403, { ok: false, error: 'legacy_owner_required' });
|
|
2818
3279
|
if (debugTerminalManager.handleHttp(req, res, url))
|
|
2819
3280
|
return;
|
|
2820
3281
|
}
|
|
@@ -2828,6 +3289,54 @@ const server = createServer(async (req, res) => {
|
|
|
2828
3289
|
res.end();
|
|
2829
3290
|
return;
|
|
2830
3291
|
}
|
|
3292
|
+
// Fragment-free entry points. The card's terminal AppLink works with a plain
|
|
3293
|
+
// `/s/<id>?token=` URL; ours carried `#/agent-workbench`, and a fragment is
|
|
3294
|
+
// the one structural difference between the two. Clients that re-encode or
|
|
3295
|
+
// truncate an AppLink's `url` lose it and land on the Dashboard home, so
|
|
3296
|
+
// offer a path that survives regardless.
|
|
3297
|
+
if ((req.method === 'GET' || req.method === 'HEAD')
|
|
3298
|
+
&& (url.pathname === '/workbench' || url.pathname === '/workbench/dock')) {
|
|
3299
|
+
const target = url.pathname === '/workbench/dock' ? '#/agent-workbench-dock' : '#/agent-workbench';
|
|
3300
|
+
const token = url.searchParams.get('t');
|
|
3301
|
+
const query = token ? `?t=${encodeURIComponent(token)}` : '';
|
|
3302
|
+
res.writeHead(302, { location: `/${query}${target}`, 'cache-control': 'no-store' });
|
|
3303
|
+
res.end();
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
// Self-service diagnostics for the same phone that cannot render the
|
|
3307
|
+
// Workbench terminal. Desktop browsers and Chromium's mobile emulation both
|
|
3308
|
+
// succeed, so the failing device has to report its own conditions: which
|
|
3309
|
+
// build it cached, whether its cookie rides along, whether the terminal's
|
|
3310
|
+
// HTTP and WebSocket hops are reachable from its network. Zero external
|
|
3311
|
+
// resources and no SPA bundle — it must open precisely when the SPA cannot,
|
|
3312
|
+
// which is also why it is allow-listed beside the static shell in
|
|
3313
|
+
// `decideDashboardAuth`. It probes only the visitor's own reachability and
|
|
3314
|
+
// echoes no token or secret.
|
|
3315
|
+
if (handleWorkbenchDoctor(req, res, url))
|
|
3316
|
+
return;
|
|
3317
|
+
// Installable Workbench: Feishu has no way to pin a custom app into its
|
|
3318
|
+
// mobile tab bar, so the closest thing to a permanent entry is the phone's
|
|
3319
|
+
// own home screen. A manifest makes "add to home screen" launch straight
|
|
3320
|
+
// into the session list, standalone and chrome-less.
|
|
3321
|
+
if ((req.method === 'GET' || req.method === 'HEAD') && url.pathname === '/workbench.webmanifest') {
|
|
3322
|
+
const manifest = {
|
|
3323
|
+
name: 'Botmux Workbench',
|
|
3324
|
+
short_name: 'Workbench',
|
|
3325
|
+
start_url: '/#/agent-workbench',
|
|
3326
|
+
scope: '/',
|
|
3327
|
+
display: 'standalone',
|
|
3328
|
+
orientation: 'any',
|
|
3329
|
+
background_color: '#080b10',
|
|
3330
|
+
theme_color: '#080b10',
|
|
3331
|
+
icons: [
|
|
3332
|
+
{ src: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
|
|
3333
|
+
{ src: '/favicon.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
|
3334
|
+
],
|
|
3335
|
+
};
|
|
3336
|
+
res.writeHead(200, { 'content-type': 'application/manifest+json', 'cache-control': 'no-cache' });
|
|
3337
|
+
res.end(JSON.stringify(manifest));
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
2831
3340
|
// ─── Static frontend (index.html + /assets/* + /game/* + root icons) ───
|
|
2832
3341
|
if ((req.method === 'GET' || req.method === 'HEAD') &&
|
|
2833
3342
|
(url.pathname === '/' ||
|
|
@@ -2853,7 +3362,13 @@ const server = createServer(async (req, res) => {
|
|
|
2853
3362
|
: url.pathname === '/favicon.ico'
|
|
2854
3363
|
? '/favicon.png'
|
|
2855
3364
|
: url.pathname;
|
|
2856
|
-
if (serveStatic(req, res, lookupPath
|
|
3365
|
+
if (serveStatic(req, res, lookupPath, {
|
|
3366
|
+
// P1-11:只给已认证身份签票据;匿名 public-read 壳不含票据,控制类端点
|
|
3367
|
+
// 对它本来就 401/403。
|
|
3368
|
+
injectHtml: requestIdentity
|
|
3369
|
+
? html => injectControlCsrfMeta(html, controlCsrfTokens.mint(requestIdentity.authSessionId))
|
|
3370
|
+
: undefined,
|
|
3371
|
+
}))
|
|
2857
3372
|
return;
|
|
2858
3373
|
if (serveMissingDashboardChunkModule(req, res, lookupPath))
|
|
2859
3374
|
return;
|
|
@@ -2904,8 +3419,9 @@ const server = createServer(async (req, res) => {
|
|
|
2904
3419
|
? { ...s, botName: n }
|
|
2905
3420
|
: s;
|
|
2906
3421
|
}), groupsMatrixSnapshot.peekPresentation());
|
|
3422
|
+
const browserSessions = projectSessionPreviewsForBrowser(sessions);
|
|
2907
3423
|
return jsonRes(res, 200, {
|
|
2908
|
-
sessions:
|
|
3424
|
+
sessions: projectSessionsForAudience(browserSessions, sessionBoardAudience),
|
|
2909
3425
|
});
|
|
2910
3426
|
}
|
|
2911
3427
|
// Desktop / operator UI: aggregate pending ask-hooks across daemons.
|
|
@@ -2987,9 +3503,14 @@ const server = createServer(async (req, res) => {
|
|
|
2987
3503
|
// else (incl. an unparseable/missing body) as a failure rather than a
|
|
2988
3504
|
// silent success.
|
|
2989
3505
|
const ok = upstream.ok && parsed?.ok === true;
|
|
3506
|
+
// A residual is NOT a failure (the row closed) but must not be counted
|
|
3507
|
+
// as a clean close either: an idle/workerless mojo row can carry a
|
|
3508
|
+
// parked lineage, so this path really does produce them.
|
|
3509
|
+
const residual = ok ? parseCloseResidual(parsed) : undefined;
|
|
2990
3510
|
return {
|
|
2991
3511
|
sessionId: s.sessionId,
|
|
2992
3512
|
ok,
|
|
3513
|
+
...(residual ? { residual } : {}),
|
|
2993
3514
|
error: ok ? undefined : (parsed?.error ?? `http_${upstream.status}`),
|
|
2994
3515
|
};
|
|
2995
3516
|
}
|
|
@@ -3115,6 +3636,7 @@ const server = createServer(async (req, res) => {
|
|
|
3115
3636
|
...(entry.installTarget ? { installTarget: entry.installTarget } : {}),
|
|
3116
3637
|
lastCheckedAt: entry.lastCheckedAt,
|
|
3117
3638
|
}));
|
|
3639
|
+
const localDev = isLocalDevInstall();
|
|
3118
3640
|
return jsonRes(res, 200, {
|
|
3119
3641
|
current,
|
|
3120
3642
|
latest,
|
|
@@ -3122,7 +3644,11 @@ const server = createServer(async (req, res) => {
|
|
|
3122
3644
|
behind: !!latest && isNewerVersion(latest, current),
|
|
3123
3645
|
cliBehind: cliUpdates.some((entry) => entry.updateAvailable),
|
|
3124
3646
|
cliUpdates,
|
|
3125
|
-
localDevInstall:
|
|
3647
|
+
localDevInstall: localDev,
|
|
3648
|
+
// Local-dev can self-update via git pull + build only when the checkout
|
|
3649
|
+
// the wrapper points at is a real git worktree; otherwise the button
|
|
3650
|
+
// stays disabled (there is nothing to pull).
|
|
3651
|
+
localDevUpdatable: localDev && isGitWorktree(resolveLocalDevCheckoutDir()),
|
|
3126
3652
|
updateSupported: installPlan !== null,
|
|
3127
3653
|
updateManager: installPlan?.manager ?? installManager,
|
|
3128
3654
|
updateCommand: installPlan ? formatGlobalInstallCommand(installPlan) : null,
|
|
@@ -3149,8 +3675,70 @@ const server = createServer(async (req, res) => {
|
|
|
3149
3675
|
if (req.method === 'POST' && url.pathname === '/api/update/run') {
|
|
3150
3676
|
if (!authed)
|
|
3151
3677
|
return jsonRes(res, 401, { ok: false, error: 'unauthorized' });
|
|
3152
|
-
|
|
3153
|
-
|
|
3678
|
+
// 本地 checkout:走 git pull --ff-only + pnpm build(与 CLI cmdUpgradeLocalDev
|
|
3679
|
+
// 共用 local-dev-update 逻辑),而不是全局包管理器安装。重启仍走下方
|
|
3680
|
+
// /api/update/restart 的 lease/intent 路径。
|
|
3681
|
+
if (isLocalDevInstall()) {
|
|
3682
|
+
const node = checkNode();
|
|
3683
|
+
if (!node.ok)
|
|
3684
|
+
return jsonRes(res, 400, { ok: false, error: 'node_too_old', node });
|
|
3685
|
+
if (updateInFlight)
|
|
3686
|
+
return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
|
|
3687
|
+
updateInFlight = true;
|
|
3688
|
+
let acquired = false;
|
|
3689
|
+
let blockedByRestart = false;
|
|
3690
|
+
let result;
|
|
3691
|
+
try {
|
|
3692
|
+
await withFileLock(globalInstallUpdateLockTarget(), async () => {
|
|
3693
|
+
acquired = true;
|
|
3694
|
+
if (hasActiveRestartLease()) {
|
|
3695
|
+
blockedByRestart = true;
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
result = await runLocalDevUpdate();
|
|
3699
|
+
}, { maxWaitMs: 2_000 });
|
|
3700
|
+
}
|
|
3701
|
+
catch (e) {
|
|
3702
|
+
if (!acquired)
|
|
3703
|
+
return jsonRes(res, 409, { ok: false, error: 'update_in_flight' });
|
|
3704
|
+
const code = e.code;
|
|
3705
|
+
if (code === 'dirty_worktree') {
|
|
3706
|
+
return jsonRes(res, 409, {
|
|
3707
|
+
ok: false, error: 'dirty_worktree',
|
|
3708
|
+
detail: e.status ?? '',
|
|
3709
|
+
dir: e.dir ?? '',
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
if (code === 'not_a_worktree') {
|
|
3713
|
+
return jsonRes(res, 400, { ok: false, error: 'not_a_worktree', dir: e.dir ?? '' });
|
|
3714
|
+
}
|
|
3715
|
+
return jsonRes(res, 500, { ok: false, error: 'install_failed', detail: e instanceof Error ? e.message : String(e) });
|
|
3716
|
+
}
|
|
3717
|
+
finally {
|
|
3718
|
+
updateInFlight = false;
|
|
3719
|
+
}
|
|
3720
|
+
if (blockedByRestart)
|
|
3721
|
+
return jsonRes(res, 409, { ok: false, error: 'restart_in_flight' });
|
|
3722
|
+
// Pin THIS build's checkout + HEAD so the follow-up restart applies it,
|
|
3723
|
+
// even if the wrapper is re-pointed by a concurrent `use:here` before the
|
|
3724
|
+
// user confirms the restart. Consumed (and re-verified) in /api/update/restart.
|
|
3725
|
+
if (result)
|
|
3726
|
+
pendingLocalDevRestart = { dir: result.dir, head: result.head };
|
|
3727
|
+
return jsonRes(res, 200, {
|
|
3728
|
+
ok: true,
|
|
3729
|
+
// Versions of the checkout we actually updated (may differ from the
|
|
3730
|
+
// running process's install root when wrapper→B, dashboard runs A).
|
|
3731
|
+
oldVersion: result?.oldVersion ?? '',
|
|
3732
|
+
newVersion: result?.newVersion ?? '',
|
|
3733
|
+
// changed = HEAD advanced (or version string changed) — for display.
|
|
3734
|
+
changed: result?.changed === true || result?.oldVersion !== result?.newVersion,
|
|
3735
|
+
// A successful build regenerates dist/, so a restart is ALWAYS needed
|
|
3736
|
+
// to apply it — independent of whether HEAD moved (the checkout may
|
|
3737
|
+
// have been pulled already and only needed a build).
|
|
3738
|
+
restartRequired: true,
|
|
3739
|
+
localDev: true,
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3154
3742
|
let installPlan;
|
|
3155
3743
|
try {
|
|
3156
3744
|
const packageRoot = lastSuccessfulUpdatePlan?.activePackageRoot ?? botmuxInstallRoot();
|
|
@@ -3412,6 +4000,33 @@ const server = createServer(async (req, res) => {
|
|
|
3412
4000
|
}
|
|
3413
4001
|
catch { /* empty / bad body → plain restart */ }
|
|
3414
4002
|
const upd = body.update && typeof body.update === 'object' ? body.update : null;
|
|
4003
|
+
// Resolve the local-dev restart target BEFORE claiming the lease so a
|
|
4004
|
+
// fail-closed drift check can't leave a dangling lease. Prefer the plan a
|
|
4005
|
+
// preceding /api/update/run pinned (dir + post-build HEAD); a plain manual
|
|
4006
|
+
// restart (no pending plan) resolves the wrapper live. Verify the target's
|
|
4007
|
+
// dist/cli.js exists and — for a pinned plan — that HEAD hasn't moved since
|
|
4008
|
+
// the build; on drift/absence fail closed rather than restart the wrong tree.
|
|
4009
|
+
let localDevRestartError;
|
|
4010
|
+
let localDevTarget;
|
|
4011
|
+
if (isLocalDevInstall()) {
|
|
4012
|
+
const pinned = pendingLocalDevRestart;
|
|
4013
|
+
pendingLocalDevRestart = undefined; // consume regardless of outcome
|
|
4014
|
+
const decision = resolveLocalDevRestartTarget(pinned, resolveLocalDevCheckoutDir(), {
|
|
4015
|
+
cliEntryExists: (dir) => existsSync(botmuxCliEntryAt(dir)),
|
|
4016
|
+
headOf: (dir) => gitHeadSha(dir),
|
|
4017
|
+
});
|
|
4018
|
+
if (decision.action === 'fail') {
|
|
4019
|
+
localDevRestartError = { status: 409, body: { ok: false, error: decision.reason, dir: decision.dir } };
|
|
4020
|
+
}
|
|
4021
|
+
else if (decision.action === 'restart') {
|
|
4022
|
+
localDevTarget = decision.dir;
|
|
4023
|
+
}
|
|
4024
|
+
else {
|
|
4025
|
+
localDevTarget = undefined; // fallback-running-root
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
if (localDevRestartError)
|
|
4029
|
+
return jsonRes(res, localDevRestartError.status, localDevRestartError.body);
|
|
3415
4030
|
let acquired = false;
|
|
3416
4031
|
let leaseId = null;
|
|
3417
4032
|
let activePackageRoot;
|
|
@@ -3443,7 +4058,15 @@ const server = createServer(async (req, res) => {
|
|
|
3443
4058
|
});
|
|
3444
4059
|
return;
|
|
3445
4060
|
}
|
|
3446
|
-
|
|
4061
|
+
// Local-dev restarts from the target resolved above (pinned build's
|
|
4062
|
+
// checkout, verified present + at the built HEAD); undefined falls back
|
|
4063
|
+
// to this dashboard process's own cli.js via spawnDetachedRestart.
|
|
4064
|
+
if (isLocalDevInstall()) {
|
|
4065
|
+
activePackageRoot = localDevTarget;
|
|
4066
|
+
}
|
|
4067
|
+
else {
|
|
4068
|
+
activePackageRoot = (lastSuccessfulUpdatePlan ?? tryResolveGlobalInstallPlan())?.activePackageRoot;
|
|
4069
|
+
}
|
|
3447
4070
|
// Send acknowledgement while holding the lock, then release immediately.
|
|
3448
4071
|
// The lease itself prevents concurrent restarts — no need to hold the
|
|
3449
4072
|
// lock across the network round-trip waiting for res.finish.
|
|
@@ -3484,7 +4107,14 @@ const server = createServer(async (req, res) => {
|
|
|
3484
4107
|
if (req.method === 'GET' && url.pathname === '/api/skills') {
|
|
3485
4108
|
return jsonRes(res, 200, dashboardSkillsPayload());
|
|
3486
4109
|
}
|
|
3487
|
-
|
|
4110
|
+
// Batch skill removal. POST /api/skills/remove is the canonical route the
|
|
4111
|
+
// dashboard UI calls: the payload (names[], force) must travel in the body,
|
|
4112
|
+
// and DELETE bodies are dropped by the platform dashboard proxy (it assumes
|
|
4113
|
+
// DELETE carries no body, forwards content-length but never pipes the bytes,
|
|
4114
|
+
// so readJsonBody hangs until the outer gateway returns 504). DELETE
|
|
4115
|
+
// /api/skills stays as an alias for direct/scripted callers.
|
|
4116
|
+
if ((req.method === 'DELETE' && url.pathname === '/api/skills')
|
|
4117
|
+
|| (req.method === 'POST' && url.pathname === '/api/skills/remove')) {
|
|
3488
4118
|
let parsed;
|
|
3489
4119
|
try {
|
|
3490
4120
|
parsed = await readJsonBody(req);
|
|
@@ -4048,6 +4678,9 @@ const server = createServer(async (req, res) => {
|
|
|
4048
4678
|
if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(close|locate|resume|restart|start)$/))) {
|
|
4049
4679
|
const sid = decodeURIComponent(m[1]);
|
|
4050
4680
|
const op = m[2];
|
|
4681
|
+
// P1-11:locate 与接管/解锁同属工作台三项操作能力,同样是无 body POST。
|
|
4682
|
+
if (op === 'locate' && requestIdentity && !enforceControlCsrf(req, res, requestIdentity))
|
|
4683
|
+
return;
|
|
4051
4684
|
const owner = aggregator.ownerOf(sid);
|
|
4052
4685
|
if (!owner)
|
|
4053
4686
|
return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
|
|
@@ -4145,9 +4778,20 @@ const server = createServer(async (req, res) => {
|
|
|
4145
4778
|
if (!owner)
|
|
4146
4779
|
return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
|
|
4147
4780
|
const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}`, { method: 'GET' });
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4781
|
+
const raw = await upstream.text();
|
|
4782
|
+
if (!upstream.ok) {
|
|
4783
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
4784
|
+
res.end(raw);
|
|
4785
|
+
return;
|
|
4786
|
+
}
|
|
4787
|
+
let body;
|
|
4788
|
+
try {
|
|
4789
|
+
body = JSON.parse(raw);
|
|
4790
|
+
}
|
|
4791
|
+
catch {
|
|
4792
|
+
return jsonRes(res, 502, { ok: false, error: 'invalid_daemon_response' });
|
|
4793
|
+
}
|
|
4794
|
+
return jsonRes(res, upstream.status, projectSessionDetailForBrowser(body));
|
|
4151
4795
|
}
|
|
4152
4796
|
// 异步 trigger 结果轮询(asyncReturnSessionId 模式的权威查询端点)。
|
|
4153
4797
|
// 四态 running/completed/failed/not_found,daemon 重启后从持久化结果兜底
|
|
@@ -4191,6 +4835,11 @@ const server = createServer(async (req, res) => {
|
|
|
4191
4835
|
// so decideDashboardAuth has already 401'd unauthenticated callers before we
|
|
4192
4836
|
// get here — the token only reaches authenticated dashboard sessions.
|
|
4193
4837
|
if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/write-link$/))) {
|
|
4838
|
+
// Short H5 sessions use the tokenless /control/takeover lease. Returning
|
|
4839
|
+
// the legacy stable capability here would bypass release/expiry/disconnect
|
|
4840
|
+
// enforcement and leak a token into browser-visible JSON.
|
|
4841
|
+
if (!legacyAuthed)
|
|
4842
|
+
return dashboardControlJson(res, 403, { ok: false, error: 'control_takeover_required' });
|
|
4194
4843
|
const sid = decodeURIComponent(m[1]);
|
|
4195
4844
|
const owner = aggregator.ownerOf(sid);
|
|
4196
4845
|
if (!owner)
|
|
@@ -4200,9 +4849,71 @@ const server = createServer(async (req, res) => {
|
|
|
4200
4849
|
res.end(await upstream.text());
|
|
4201
4850
|
return;
|
|
4202
4851
|
}
|
|
4852
|
+
// Read-only web-terminal link. The Workbench terminal pane uses it so the
|
|
4853
|
+
// frame authenticates by capability instead of by Dashboard cookie, which
|
|
4854
|
+
// a Feishu WebView's WebSocket does not carry.
|
|
4855
|
+
//
|
|
4856
|
+
// Two things about the daemon's answer never reach the browser (P1-5):
|
|
4857
|
+
// • its `?viewToken=` is the worker's per-boot card token, unbound to the
|
|
4858
|
+
// requesting authentication. It is consumed here — converted to a
|
|
4859
|
+
// one-way worker generation id — and REPLACED with a short-lived signed
|
|
4860
|
+
// read grant bound to sessionId + authSessionId + expiresAt + that
|
|
4861
|
+
// generation + `audience: central`;
|
|
4862
|
+
// • its ORIGIN is the daemon terminal proxy / worker port, both network
|
|
4863
|
+
// reachable and both blind to logout. We answer with a same-origin
|
|
4864
|
+
// relative path instead, so the only entry point the browser ever learns
|
|
4865
|
+
// is this dashboard's front proxy — the one place that checks auth
|
|
4866
|
+
// session liveness and countersigns the hop for the worker.
|
|
4867
|
+
// A view capability can never send input, so this stays safe for any
|
|
4868
|
+
// identity allowed to observe the session; unauthenticated callers were
|
|
4869
|
+
// already rejected by the auth decision above (no public allow-list).
|
|
4870
|
+
if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/view-link$/))) {
|
|
4871
|
+
const sid = decodeURIComponent(m[1]);
|
|
4872
|
+
const owner = aggregator.ownerOf(sid);
|
|
4873
|
+
if (!owner)
|
|
4874
|
+
return jsonRes(res, 404, { ok: false, error: 'unknown_session' });
|
|
4875
|
+
const upstream = await proxyToDaemon(owner, `/api/sessions/${sid}/view-link`, { method: 'GET' });
|
|
4876
|
+
if (upstream.status !== 200) {
|
|
4877
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
4878
|
+
res.end(await upstream.text());
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
let upstreamUrl;
|
|
4882
|
+
try {
|
|
4883
|
+
upstreamUrl = JSON.parse(await upstream.text()).url;
|
|
4884
|
+
}
|
|
4885
|
+
catch {
|
|
4886
|
+
upstreamUrl = undefined;
|
|
4887
|
+
}
|
|
4888
|
+
// No generation ⇒ no pinned capability. Fail closed rather than mint one
|
|
4889
|
+
// that would outlive the worker boot it was meant for.
|
|
4890
|
+
const generation = upstreamWorkerViewGeneration(SECRET, upstreamUrl);
|
|
4891
|
+
const minted = requestIdentity && generation
|
|
4892
|
+
? mintTerminalViewCapability(SECRET, sid, requestIdentity, generation)
|
|
4893
|
+
: null;
|
|
4894
|
+
const rewritten = minted ? centralViewLinkPath(sid, minted.token) : null;
|
|
4895
|
+
// Fail closed rather than fall back to the unbound upstream token/origin.
|
|
4896
|
+
if (!minted || !rewritten)
|
|
4897
|
+
return jsonRes(res, 502, { ok: false, error: 'view_link_unavailable' });
|
|
4898
|
+
return jsonRes(res, 200, { ok: true, url: rewritten, expiresAt: minted.expiresAt });
|
|
4899
|
+
}
|
|
4900
|
+
// Browser-safe preview metadata. The literal loopback host/port remains in
|
|
4901
|
+
// the aggregator only; this authenticated API returns a same-origin path.
|
|
4902
|
+
if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/preview$/))) {
|
|
4903
|
+
const sid = decodeURIComponent(m[1]);
|
|
4904
|
+
const resolution = resolveDashboardSessionPreview(sid);
|
|
4905
|
+
if (!resolution.ok)
|
|
4906
|
+
return jsonRes(res, resolution.status, { ok: false, error: resolution.error });
|
|
4907
|
+
const preview = previewDescriptorFromRow(aggregator.getSession(sid));
|
|
4908
|
+
if (!preview)
|
|
4909
|
+
return jsonRes(res, 404, { ok: false, error: 'preview_not_registered' });
|
|
4910
|
+
return jsonRes(res, 200, { ok: true, preview });
|
|
4911
|
+
}
|
|
4203
4912
|
// Dashboard「复现命令」:透传到 owning daemon 取该 session 的真实 CLI 调用。
|
|
4204
4913
|
// 与 write-link 同样只在管理 cookie(写权限)下可达:命令含 token/凭证。
|
|
4205
4914
|
if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/spawn-command$/))) {
|
|
4915
|
+
if (!legacyAuthed)
|
|
4916
|
+
return dashboardControlJson(res, 403, { ok: false, error: 'legacy_owner_required' });
|
|
4206
4917
|
const sid = decodeURIComponent(m[1]);
|
|
4207
4918
|
const owner = aggregator.ownerOf(sid);
|
|
4208
4919
|
if (!owner)
|
|
@@ -4735,6 +5446,7 @@ const server = createServer(async (req, res) => {
|
|
|
4735
5446
|
wrapperCli: j.wrapperCli || d.wrapperCli,
|
|
4736
5447
|
model: j.model || d.model,
|
|
4737
5448
|
reasoningEffort: j.reasoningEffort || d.reasoningEffort,
|
|
5449
|
+
turnTimeoutMs: typeof j.turnTimeoutMs === 'number' ? j.turnTimeoutMs : d.turnTimeoutMs,
|
|
4738
5450
|
}, j);
|
|
4739
5451
|
}
|
|
4740
5452
|
catch (e) {
|
|
@@ -5378,6 +6090,24 @@ const server = createServer(async (req, res) => {
|
|
|
5378
6090
|
message: loginRequired ? '尚未获得飞书标签权限,请点击「立即授权」按钮进行授权。' : '没有可用于读取标签的飞书机器人。',
|
|
5379
6091
|
});
|
|
5380
6092
|
}
|
|
6093
|
+
// PUT /api/bots/:appId/session-owner-reminder — per-Bot periodic owner
|
|
6094
|
+
// reminder policy. The owning daemon validates, persists, and hot-applies.
|
|
6095
|
+
let mBotOwnerReminder;
|
|
6096
|
+
if (req.method === 'PUT' && (mBotOwnerReminder = url.pathname.match(/^\/api\/bots\/([^/]+)\/session-owner-reminder$/))) {
|
|
6097
|
+
const appId = decodeURIComponent(mBotOwnerReminder[1]);
|
|
6098
|
+
const chunks = [];
|
|
6099
|
+
for await (const c of req)
|
|
6100
|
+
chunks.push(c);
|
|
6101
|
+
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
6102
|
+
const upstream = await proxyToDaemon(appId, `/api/bot-session-owner-reminder`, {
|
|
6103
|
+
method: 'PUT',
|
|
6104
|
+
headers: { 'content-type': 'application/json' },
|
|
6105
|
+
body: raw,
|
|
6106
|
+
});
|
|
6107
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
6108
|
+
res.end(await upstream.text());
|
|
6109
|
+
return;
|
|
6110
|
+
}
|
|
5381
6111
|
// Create a new chat — pick a creator from the user-selected larkAppIds
|
|
5382
6112
|
// (Feishu makes the calling bot the implicit first member, so picking
|
|
5383
6113
|
// anything else would silently add an unwanted bot). Auto-invite the
|
|
@@ -5654,14 +6384,41 @@ const server = createServer(async (req, res) => {
|
|
|
5654
6384
|
'connection': 'keep-alive',
|
|
5655
6385
|
});
|
|
5656
6386
|
res.write('retry: 5000\n\n');
|
|
6387
|
+
// P1-8:这条流的寿命必须等于建立它的那次认证的寿命。建流时登记进
|
|
6388
|
+
// authSession→长连接索引(logout/rotate/解绑时被主动 destroy),并在每帧
|
|
6389
|
+
// 推送前复核身份仍有效——否则 rotate 之后新签的 `riffAccessUrl`(Riff 沙箱
|
|
6390
|
+
// 写凭据)会顺着这条老连接送给上一任持有者。匿名 public-read 连接没有认证
|
|
6391
|
+
// 会话,行为不变。
|
|
6392
|
+
// P1-14 姊妹项:这条流的能力口径必须与 REST 同源。Workbench-only 身份对
|
|
6393
|
+
// `GET /api/schedules` 是明确的 401,`/events` 就不能把同一份排程(含
|
|
6394
|
+
// prompt / workingDir / fired 的 error 原文)换个管子送出去——过滤在
|
|
6395
|
+
// createDashboardEventsStream 内部按 audience 执行,见 events-sse.ts。
|
|
6396
|
+
const eventsAudience = authed
|
|
6397
|
+
? 'management'
|
|
6398
|
+
: workbenchOnlyIdentity ? 'workbench' : 'anonymous';
|
|
6399
|
+
const stream = createDashboardEventsStream({
|
|
6400
|
+
res,
|
|
6401
|
+
authSessionId: requestIdentity?.authSessionId ?? null,
|
|
6402
|
+
audience: eventsAudience,
|
|
6403
|
+
isAuthSessionLive: terminalAuthSessionLive,
|
|
6404
|
+
bind: (authSessionId, close) => authSessionConnections.register(authSessionId, close),
|
|
6405
|
+
});
|
|
5657
6406
|
const off = aggregator.on(ev => {
|
|
5658
|
-
//
|
|
5659
|
-
//
|
|
5660
|
-
//
|
|
5661
|
-
|
|
5662
|
-
let body =
|
|
5663
|
-
|
|
5664
|
-
|
|
6407
|
+
// Session rows follow the same three-way audience as GET /api/sessions,
|
|
6408
|
+
// or a Workbench viewer would receive the preview descriptor on the
|
|
6409
|
+
// initial REST fetch and then lose it on the first live patch.
|
|
6410
|
+
const projectedBody = projectSessionPreviewEventForBrowser(ev.type, ev.body);
|
|
6411
|
+
let body = projectSessionEventForAudience(ev.type, projectedBody, sessionBoardAudience);
|
|
6412
|
+
// Schedules stay on the MANAGEMENT gate, mirroring the GET
|
|
6413
|
+
// /api/schedules carve-out: schedule events carry the full task object
|
|
6414
|
+
// (prompt = business instructions, workingDir = repo/customer path) and
|
|
6415
|
+
// `/api/schedules` is not a Workbench capability, so widening this to
|
|
6416
|
+
// `sessionBoardAudience` would let an H5 identity read over `/events`
|
|
6417
|
+
// what the REST route refuses it.
|
|
6418
|
+
// Workbench-only identities never reach this redaction at all — the
|
|
6419
|
+
// stream drops every `schedule.*` frame for them (P1-14 sibling); this
|
|
6420
|
+
// branch is the ANONYMOUS publicReadOnly path, which the REST route
|
|
6421
|
+
// does serve, redacted the same way.
|
|
5665
6422
|
if (!authed && (ev.type === 'schedule.created' || ev.type === 'schedule.updated')) {
|
|
5666
6423
|
const b = body;
|
|
5667
6424
|
body = {
|
|
@@ -5670,10 +6427,10 @@ const server = createServer(async (req, res) => {
|
|
|
5670
6427
|
...(b.patch ? { patch: { ...b.patch, prompt: undefined, workingDir: undefined } } : {}),
|
|
5671
6428
|
};
|
|
5672
6429
|
}
|
|
5673
|
-
|
|
6430
|
+
stream.write(ev.type, { larkAppId: ev.larkAppId, body });
|
|
5674
6431
|
});
|
|
5675
6432
|
const hb = setInterval(() => {
|
|
5676
|
-
|
|
6433
|
+
stream.write('heartbeat', { ts: Date.now() });
|
|
5677
6434
|
}, 15_000);
|
|
5678
6435
|
// Push a bots.changed frame whenever the online bot roster actually
|
|
5679
6436
|
// changes (bot added / removed / renamed / re-indexed) so the Bot 配置
|
|
@@ -5686,9 +6443,9 @@ const server = createServer(async (req, res) => {
|
|
|
5686
6443
|
if (sig === lastRoster)
|
|
5687
6444
|
return;
|
|
5688
6445
|
lastRoster = sig;
|
|
5689
|
-
|
|
6446
|
+
stream.write('bots.changed', { body: { signature: sig } });
|
|
5690
6447
|
});
|
|
5691
|
-
res.on('close', () => { off(); offRoster(); clearInterval(hb); });
|
|
6448
|
+
res.on('close', () => { off(); offRoster(); clearInterval(hb); stream.dispose(); });
|
|
5692
6449
|
return;
|
|
5693
6450
|
}
|
|
5694
6451
|
// Public API + static frontend land in Task 17 / 18. For now: 404.
|
|
@@ -5744,47 +6501,35 @@ oauthCallbackServer.listen(9768, '127.0.0.1', () => {
|
|
|
5744
6501
|
server.on('upgrade', (req, clientSocket, head) => {
|
|
5745
6502
|
try {
|
|
5746
6503
|
const rawUrl = req.url ?? '/';
|
|
6504
|
+
// Preview 的 WS 必须第一个判:沙箱化之后它来自不透明来源(`Origin: null`),
|
|
6505
|
+
// 凭据是路径里的 content capability,绝不能被下面的管理类 Origin 校验误杀。
|
|
6506
|
+
if (sessionPreviewProxy.handleUpgrade(req, clientSocket, head))
|
|
6507
|
+
return;
|
|
6508
|
+
// P1-11:管理类 WS(终端 / 调试终端)升级不经 HTTP 门禁,浏览器对 WS 握手
|
|
6509
|
+
// 一定带 Origin,所以「带了但对不上(含 null)」一律拒——同站兄弟子域和
|
|
6510
|
+
// localhost 其它端口正是 SameSite=Lax 挡不住的那一类。
|
|
6511
|
+
const upgradeOrigin = managementUpgradeOrigin(req.headers);
|
|
6512
|
+
if (!upgradeOrigin.ok) {
|
|
6513
|
+
const body = JSON.stringify({ ok: false, error: upgradeOrigin.error });
|
|
6514
|
+
clientSocket.end([
|
|
6515
|
+
'HTTP/1.1 403 Forbidden',
|
|
6516
|
+
'content-type: application/json; charset=utf-8',
|
|
6517
|
+
'cache-control: no-store',
|
|
6518
|
+
'connection: close',
|
|
6519
|
+
`content-length: ${Buffer.byteLength(body)}`,
|
|
6520
|
+
'',
|
|
6521
|
+
body,
|
|
6522
|
+
].join('\r\n'));
|
|
6523
|
+
return;
|
|
6524
|
+
}
|
|
5747
6525
|
// 调试终端 WS(owner-only):manager 内部自校验管理 cookie。命中即接管。
|
|
5748
6526
|
if (rawUrl.startsWith('/debug-terminal/')) {
|
|
5749
6527
|
if (debugTerminalManager.handleUpgrade(req, clientSocket, head))
|
|
5750
6528
|
return;
|
|
5751
6529
|
}
|
|
5752
|
-
if (
|
|
5753
|
-
return
|
|
5754
|
-
|
|
5755
|
-
// Strip query/hash before extracting the sessionId path segment.
|
|
5756
|
-
const pathname = rawUrl.split(/[?#]/)[0];
|
|
5757
|
-
const sessionId = parseTerminalSessionId(pathname);
|
|
5758
|
-
const tport = sessionId ? aggregator.terminalProxyPortOf(sessionId) : undefined;
|
|
5759
|
-
if (!tport)
|
|
5760
|
-
return clientSocket.destroy();
|
|
5761
|
-
const upstream = netConnect(tport, '127.0.0.1', () => {
|
|
5762
|
-
// rawHeaders is a flat [k, v, k, v, ...] list — preserves casing/duplicates.
|
|
5763
|
-
const lines = [`${req.method} ${req.url} HTTP/1.1`];
|
|
5764
|
-
const rh = req.rawHeaders;
|
|
5765
|
-
for (let i = 0; i + 1 < rh.length; i += 2)
|
|
5766
|
-
lines.push(`${rh[i]}: ${rh[i + 1]}`);
|
|
5767
|
-
lines.push('', '');
|
|
5768
|
-
upstream.write(lines.join('\r\n'));
|
|
5769
|
-
if (head?.length)
|
|
5770
|
-
upstream.write(head);
|
|
5771
|
-
upstream.pipe(clientSocket);
|
|
5772
|
-
clientSocket.pipe(upstream);
|
|
5773
|
-
});
|
|
5774
|
-
const cleanup = () => {
|
|
5775
|
-
try {
|
|
5776
|
-
upstream.destroy();
|
|
5777
|
-
}
|
|
5778
|
-
catch { /* ignore */ }
|
|
5779
|
-
try {
|
|
5780
|
-
clientSocket.destroy();
|
|
5781
|
-
}
|
|
5782
|
-
catch { /* ignore */ }
|
|
5783
|
-
};
|
|
5784
|
-
upstream.on('error', cleanup);
|
|
5785
|
-
clientSocket.on('error', cleanup);
|
|
5786
|
-
upstream.on('close', () => clientSocket.destroy());
|
|
5787
|
-
clientSocket.on('close', () => upstream.destroy());
|
|
6530
|
+
if (terminalFrontProxy.handleUpgrade(req, clientSocket, head))
|
|
6531
|
+
return;
|
|
6532
|
+
clientSocket.destroy();
|
|
5788
6533
|
}
|
|
5789
6534
|
catch {
|
|
5790
6535
|
try {
|
|
@@ -5894,11 +6639,12 @@ function readPlatformBotsInfo() {
|
|
|
5894
6639
|
// Merge per-bot team-visibility config (showInTeam) from bots.json by
|
|
5895
6640
|
// larkAppId so the platform team page can hide bots. Default: showInTeam =
|
|
5896
6641
|
// true (shown). bots.json may be unreadable from the dashboard process →
|
|
5897
|
-
// fall back to the default.
|
|
6642
|
+
// fall back to the default. apiOnly is read from the same config to derive
|
|
6643
|
+
// `mentionable` (a core-only bot has no Feishu transport → can't be @-ed).
|
|
5898
6644
|
const cfgByAppId = new Map();
|
|
5899
6645
|
try {
|
|
5900
6646
|
for (const cfg of loadBotConfigs()) {
|
|
5901
|
-
cfgByAppId.set(cfg.larkAppId, { showInTeam: cfg.showInTeam });
|
|
6647
|
+
cfgByAppId.set(cfg.larkAppId, { showInTeam: cfg.showInTeam, apiOnly: cfg.apiOnly });
|
|
5902
6648
|
}
|
|
5903
6649
|
}
|
|
5904
6650
|
catch {
|
|
@@ -5917,6 +6663,13 @@ function readPlatformBotsInfo() {
|
|
|
5917
6663
|
// 自家消息回声学到的租户稳定 union_id(可能尚未学到 → undefined)。
|
|
5918
6664
|
// 平台聚合团队 roster 用,见 bot-union-ids-store / platform-team-store。
|
|
5919
6665
|
unionId: e.larkAppId ? getBotUnionId(config.session.dataDir, e.larkAppId) : undefined,
|
|
6666
|
+
// 团队维度 Agent 互查(additive,交接契约 §端点2 / register|heartbeat):
|
|
6667
|
+
// · specialties:owner 预配的专长标签(bot-profiles),发现/拉群匹配依据,仅展示不可信。
|
|
6668
|
+
// · mentionable:是否有飞书传输身份能被 @(core-only/apiOnly → false)。cfg 读不到
|
|
6669
|
+
// 时保守按可传输(true),与 team-bot-directory「undefined 按可传输」同源
|
|
6670
|
+
// (fail-open 仅在本机自报、无跨部署放大风险;真正的 no-transport 由 apiOnly 明示)。
|
|
6671
|
+
specialties: e.larkAppId ? getBotSpecialties(config.session.dataDir, e.larkAppId) : [],
|
|
6672
|
+
mentionable: cfg?.apiOnly !== true,
|
|
5920
6673
|
};
|
|
5921
6674
|
})
|
|
5922
6675
|
.filter((b) => b.appId);
|