botmux 3.4.3 → 3.5.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 +10 -7
- package/README.md +11 -6
- package/dist/adapters/backend/herdr-backend.d.ts +43 -6
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +362 -28
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/reproduce-command.d.ts +39 -0
- package/dist/adapters/backend/reproduce-command.d.ts.map +1 -0
- package/dist/adapters/backend/reproduce-command.js +63 -0
- package/dist/adapters/backend/reproduce-command.js.map +1 -0
- package/dist/adapters/backend/session-backend-selector.d.ts +10 -1
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +73 -4
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +16 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/cli/pi-initial-prompt.d.ts.map +1 -1
- package/dist/adapters/cli/pi-initial-prompt.js +5 -1
- package/dist/adapters/cli/pi-initial-prompt.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +5 -0
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +3 -3
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/adapters/hook-installer.d.ts +7 -0
- package/dist/adapters/hook-installer.d.ts.map +1 -1
- package/dist/adapters/hook-installer.js +56 -0
- package/dist/adapters/hook-installer.js.map +1 -1
- package/dist/bot-registry.d.ts +13 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +118 -2
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/capabilities.d.ts +24 -0
- package/dist/cli/capabilities.d.ts.map +1 -0
- package/dist/cli/capabilities.js +25 -0
- package/dist/cli/capabilities.js.map +1 -0
- package/dist/cli/create-group-resolver.d.ts +24 -0
- package/dist/cli/create-group-resolver.d.ts.map +1 -1
- package/dist/cli/create-group-resolver.js +23 -0
- package/dist/cli/create-group-resolver.js.map +1 -1
- package/dist/cli/daemon-lifecycle-env.d.ts +5 -7
- package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
- package/dist/cli/daemon-lifecycle-env.js +13 -7
- package/dist/cli/daemon-lifecycle-env.js.map +1 -1
- package/dist/cli/exact-chat-grant-client.d.ts +48 -0
- package/dist/cli/exact-chat-grant-client.d.ts.map +1 -0
- package/dist/cli/exact-chat-grant-client.js +108 -0
- package/dist/cli/exact-chat-grant-client.js.map +1 -0
- package/dist/cli/exact-chat-grant.d.ts +18 -0
- package/dist/cli/exact-chat-grant.d.ts.map +1 -0
- package/dist/cli/exact-chat-grant.js +97 -0
- package/dist/cli/exact-chat-grant.js.map +1 -0
- package/dist/cli.d.ts +10 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +770 -126
- package/dist/cli.js.map +1 -1
- package/dist/codex-rpc-engine.d.ts +26 -0
- package/dist/codex-rpc-engine.d.ts.map +1 -1
- package/dist/codex-rpc-engine.js +113 -2
- package/dist/codex-rpc-engine.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -0
- package/dist/config.js.map +1 -1
- package/dist/core/bot-live-control.d.ts +41 -0
- package/dist/core/bot-live-control.d.ts.map +1 -0
- package/dist/core/bot-live-control.js +135 -0
- package/dist/core/bot-live-control.js.map +1 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +28 -8
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/daemon-config-fence.d.ts +8 -0
- package/dist/core/daemon-config-fence.d.ts.map +1 -0
- package/dist/core/daemon-config-fence.js +13 -0
- package/dist/core/daemon-config-fence.js.map +1 -0
- package/dist/core/dashboard-events.d.ts +5 -0
- package/dist/core/dashboard-events.d.ts.map +1 -1
- package/dist/core/dashboard-events.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +21 -2
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +232 -5
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dispatch-registry.d.ts +12 -0
- package/dist/core/dispatch-registry.d.ts.map +1 -0
- package/dist/core/dispatch-registry.js +33 -0
- package/dist/core/dispatch-registry.js.map +1 -0
- package/dist/core/dispatch.d.ts +99 -0
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +136 -0
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +10 -3
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/persistent-backend.d.ts +16 -2
- package/dist/core/persistent-backend.d.ts.map +1 -1
- package/dist/core/persistent-backend.js +40 -0
- package/dist/core/persistent-backend.js.map +1 -1
- package/dist/core/session-discovery.d.ts +8 -1
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +140 -8
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +12 -8
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-title.d.ts +8 -0
- package/dist/core/session-title.d.ts.map +1 -1
- package/dist/core/session-title.js +54 -0
- package/dist/core/session-title.js.map +1 -1
- package/dist/core/terminal-write-auth.d.ts +15 -0
- package/dist/core/terminal-write-auth.d.ts.map +1 -1
- package/dist/core/terminal-write-auth.js +20 -0
- 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 +3 -2
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +33 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +27 -8
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +389 -19
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +4 -2
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +397 -26
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts +127 -1
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +1104 -25
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/debug-terminal.d.ts +18 -0
- package/dist/dashboard/debug-terminal.d.ts.map +1 -0
- package/dist/dashboard/debug-terminal.js +338 -0
- package/dist/dashboard/debug-terminal.js.map +1 -0
- package/dist/dashboard/public-redact.d.ts +2 -0
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +12 -3
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/registry.d.ts +8 -0
- package/dist/dashboard/registry.d.ts.map +1 -1
- package/dist/dashboard/registry.js +13 -0
- package/dist/dashboard/registry.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +47 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +102 -1
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +2 -0
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +1 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +24 -5
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +15 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +19 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +76 -6
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/icon-tooltip.d.ts +3 -0
- package/dist/dashboard/web/icon-tooltip.d.ts.map +1 -0
- package/dist/dashboard/web/icon-tooltip.js +108 -0
- package/dist/dashboard/web/icon-tooltip.js.map +1 -0
- package/dist/dashboard/web/sessions-kanban.js +2 -2
- package/dist/dashboard/web/sessions-kanban.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +48 -9
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +46 -2
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +78 -0
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +88 -1
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/store.d.ts +2 -0
- package/dist/dashboard/web/store.d.ts.map +1 -1
- package/dist/dashboard/web/store.js +13 -1
- package/dist/dashboard/web/store.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-O376WSER.js +5 -0
- package/dist/dashboard-web/chunks/{chunk-JNRSPKLS.js → chunk-EJDYUINZ.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-CUV65BIQ.js → chunk-H7P5UV4W.js} +6 -6
- package/dist/dashboard-web/chunks/{chunk-P6GO5KT4.js → chunk-QCPPKCX3.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-UBOLHQCK.js → chunk-WXPMNEYV.js} +1 -1
- package/dist/dashboard-web/chunks/{connectors-page-KTC2QWNW.js → connectors-page-IR54AOAI.js} +1 -1
- package/dist/dashboard-web/chunks/{groups-page-B3EKGOLY.js → groups-page-3P4TLUMF.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-X6RXNCWX.js → insights-page-TDCZV56A.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-LLRA5LQW.js → monitor-room-QWCWL3SX.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-7MT5FEOS.js → monitoring-page-IOWFSNVC.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-KD46D5TD.js → office-page-5AUPEHZP.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-JUWNH32O.js → overview-page-QJMI6NLW.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-P6F7NLOI.js → roles-page-WJVB52ET.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-XIHWZST4.js → schedules-page-RVRWHP4V.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-KWLGBX6G.js +3 -0
- package/dist/dashboard-web/chunks/settings-page-TNZEPC7V.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-A46KVWBL.js → skills-page-7YT5TIX5.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-SDUC56DF.js → team-federation-page-NJZKTU7A.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-J5JOTUSQ.js → v3-page-NQ73UA2Q.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-U7LSOAWI.js → whiteboards-page-DSM3MDMN.js} +1 -1
- package/dist/dashboard-web/style.css +198 -7
- package/dist/dashboard.js +294 -7
- package/dist/dashboard.js.map +1 -1
- package/dist/features/codex-notifier/adoption.d.ts +11 -0
- package/dist/features/codex-notifier/adoption.d.ts.map +1 -0
- package/dist/features/codex-notifier/adoption.js +11 -0
- package/dist/features/codex-notifier/adoption.js.map +1 -0
- package/dist/features/codex-notifier/app-opener.d.ts +21 -0
- package/dist/features/codex-notifier/app-opener.d.ts.map +1 -0
- package/dist/features/codex-notifier/app-opener.js +33 -0
- package/dist/features/codex-notifier/app-opener.js.map +1 -0
- package/dist/features/codex-notifier/card-action.d.ts +27 -0
- package/dist/features/codex-notifier/card-action.d.ts.map +1 -0
- package/dist/features/codex-notifier/card-action.js +84 -0
- package/dist/features/codex-notifier/card-action.js.map +1 -0
- package/dist/features/codex-notifier/card.d.ts +12 -0
- package/dist/features/codex-notifier/card.d.ts.map +1 -0
- package/dist/features/codex-notifier/card.js +150 -0
- package/dist/features/codex-notifier/card.js.map +1 -0
- package/dist/features/codex-notifier/codex-context.d.ts +12 -0
- package/dist/features/codex-notifier/codex-context.d.ts.map +1 -0
- package/dist/features/codex-notifier/codex-context.js +161 -0
- package/dist/features/codex-notifier/codex-context.js.map +1 -0
- package/dist/features/codex-notifier/config.d.ts +9 -0
- package/dist/features/codex-notifier/config.d.ts.map +1 -0
- package/dist/features/codex-notifier/config.js +11 -0
- package/dist/features/codex-notifier/config.js.map +1 -0
- package/dist/features/codex-notifier/confirmed-turn.d.ts +20 -0
- package/dist/features/codex-notifier/confirmed-turn.d.ts.map +1 -0
- package/dist/features/codex-notifier/confirmed-turn.js +130 -0
- package/dist/features/codex-notifier/confirmed-turn.js.map +1 -0
- package/dist/features/codex-notifier/emitter.d.ts +13 -0
- package/dist/features/codex-notifier/emitter.d.ts.map +1 -0
- package/dist/features/codex-notifier/emitter.js +33 -0
- package/dist/features/codex-notifier/emitter.js.map +1 -0
- package/dist/features/codex-notifier/event-store.d.ts +29 -0
- package/dist/features/codex-notifier/event-store.d.ts.map +1 -0
- package/dist/features/codex-notifier/event-store.js +217 -0
- package/dist/features/codex-notifier/event-store.js.map +1 -0
- package/dist/features/codex-notifier/event.d.ts +46 -0
- package/dist/features/codex-notifier/event.d.ts.map +1 -0
- package/dist/features/codex-notifier/event.js +188 -0
- package/dist/features/codex-notifier/event.js.map +1 -0
- package/dist/features/codex-notifier/hook-cli.d.ts +23 -0
- package/dist/features/codex-notifier/hook-cli.d.ts.map +1 -0
- package/dist/features/codex-notifier/hook-cli.js +130 -0
- package/dist/features/codex-notifier/hook-cli.js.map +1 -0
- package/dist/features/codex-notifier/hook-installer.d.ts +19 -0
- package/dist/features/codex-notifier/hook-installer.d.ts.map +1 -0
- package/dist/features/codex-notifier/hook-installer.js +158 -0
- package/dist/features/codex-notifier/hook-installer.js.map +1 -0
- package/dist/features/codex-notifier/index.d.ts +21 -0
- package/dist/features/codex-notifier/index.d.ts.map +1 -0
- package/dist/features/codex-notifier/index.js +19 -0
- package/dist/features/codex-notifier/index.js.map +1 -0
- package/dist/features/codex-notifier/internal-turn.d.ts +5 -0
- package/dist/features/codex-notifier/internal-turn.d.ts.map +1 -0
- package/dist/features/codex-notifier/internal-turn.js +43 -0
- package/dist/features/codex-notifier/internal-turn.js.map +1 -0
- package/dist/features/codex-notifier/outbox-worker.d.ts +71 -0
- package/dist/features/codex-notifier/outbox-worker.d.ts.map +1 -0
- package/dist/features/codex-notifier/outbox-worker.js +290 -0
- package/dist/features/codex-notifier/outbox-worker.js.map +1 -0
- package/dist/features/codex-notifier/outbox.d.ts +25 -0
- package/dist/features/codex-notifier/outbox.d.ts.map +1 -0
- package/dist/features/codex-notifier/outbox.js +225 -0
- package/dist/features/codex-notifier/outbox.js.map +1 -0
- package/dist/features/codex-notifier/paths.d.ts +7 -0
- package/dist/features/codex-notifier/paths.d.ts.map +1 -0
- package/dist/features/codex-notifier/paths.js +20 -0
- package/dist/features/codex-notifier/paths.js.map +1 -0
- package/dist/features/codex-notifier/screen-lock.d.ts +12 -0
- package/dist/features/codex-notifier/screen-lock.d.ts.map +1 -0
- package/dist/features/codex-notifier/screen-lock.js +47 -0
- package/dist/features/codex-notifier/screen-lock.js.map +1 -0
- package/dist/features/codex-notifier/side-conversation-monitor.d.ts +102 -0
- package/dist/features/codex-notifier/side-conversation-monitor.d.ts.map +1 -0
- package/dist/features/codex-notifier/side-conversation-monitor.js +774 -0
- package/dist/features/codex-notifier/side-conversation-monitor.js.map +1 -0
- package/dist/features/codex-notifier/types.d.ts +42 -0
- package/dist/features/codex-notifier/types.d.ts.map +1 -0
- package/dist/features/codex-notifier/types.js +2 -0
- package/dist/features/codex-notifier/types.js.map +1 -0
- package/dist/features/codex-notifier/worker-lock.d.ts +8 -0
- package/dist/features/codex-notifier/worker-lock.d.ts.map +1 -0
- package/dist/features/codex-notifier/worker-lock.js +108 -0
- package/dist/features/codex-notifier/worker-lock.js.map +1 -0
- package/dist/global-config.d.ts +13 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +30 -0
- package/dist/global-config.js.map +1 -1
- package/dist/im/lark/card-builder.js +2 -2
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +2 -0
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +165 -34
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +62 -4
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +237 -29
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +20 -30
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/services/codex-app-threads.d.ts +47 -0
- package/dist/services/codex-app-threads.d.ts.map +1 -1
- package/dist/services/codex-app-threads.js +502 -10
- package/dist/services/codex-app-threads.js.map +1 -1
- package/dist/services/exact-chat-grant.d.ts +83 -0
- package/dist/services/exact-chat-grant.d.ts.map +1 -0
- package/dist/services/exact-chat-grant.js +249 -0
- package/dist/services/exact-chat-grant.js.map +1 -0
- package/dist/services/group-creator.d.ts +11 -0
- package/dist/services/group-creator.d.ts.map +1 -1
- package/dist/services/group-creator.js +18 -9
- package/dist/services/group-creator.js.map +1 -1
- package/dist/services/local-cli-opener.d.ts.map +1 -1
- package/dist/services/local-cli-opener.js +10 -5
- package/dist/services/local-cli-opener.js.map +1 -1
- package/dist/services/traex-user-input.d.ts +19 -0
- package/dist/services/traex-user-input.d.ts.map +1 -0
- package/dist/services/traex-user-input.js +57 -0
- package/dist/services/traex-user-input.js.map +1 -0
- package/dist/setup/open-platform-automation.d.ts +24 -3
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +45 -8
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/setup/verify-permissions.d.ts +18 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -1
- package/dist/setup/verify-permissions.js +60 -0
- package/dist/setup/verify-permissions.js.map +1 -1
- package/dist/types.d.ts +70 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/stuck-detector.d.ts +45 -0
- package/dist/utils/stuck-detector.d.ts.map +1 -0
- package/dist/utils/stuck-detector.js +188 -0
- package/dist/utils/stuck-detector.js.map +1 -0
- package/dist/utils/stuck-key-guard.d.ts +75 -0
- package/dist/utils/stuck-key-guard.d.ts.map +1 -0
- package/dist/utils/stuck-key-guard.js +77 -0
- package/dist/utils/stuck-key-guard.js.map +1 -0
- package/dist/worker.js +705 -93
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-B5AY4B2J.js +0 -5
- package/dist/dashboard-web/chunks/sessions-page-2VECQ27T.js +0 -3
- package/dist/dashboard-web/chunks/settings-page-HMIUE2JA.js +0 -2
|
@@ -1,18 +1,96 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, rmSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
3
4
|
import { readBotsJsonOrEmpty, writeBotsJsonAtomic } from '../setup/bots-store.js';
|
|
4
5
|
import { atomicWriteFileSync } from '../utils/atomic-write.js';
|
|
5
6
|
import { logger } from '../utils/logger.js';
|
|
6
7
|
import { normalizeBotConfig, findInvalidAllowedUserEntries, hasOwnerEntry } from '../setup/bot-config-editor.js';
|
|
7
8
|
import { tryRegisterApp } from '../setup/register-app.js';
|
|
8
|
-
import { validateCredentials, buildRemainingSteps } from '../setup/verify-permissions.js';
|
|
9
|
+
import { validateCredentials, readCriticalScopesFromApplicationInfo, buildRemainingSteps, } from '../setup/verify-permissions.js';
|
|
9
10
|
import { resolveSetupAppName } from '../setup/app-name.js';
|
|
10
|
-
import { automateOpenPlatformSetup, createFeishuOpenPlatformApp, inspectCachedFeishuOpenPlatformSession, } from '../setup/open-platform-automation.js';
|
|
11
|
+
import { automateOpenPlatformSetup, BOT_BASELINE_APP_EVENTS, BOT_BASELINE_CALLBACKS, createFeishuOpenPlatformApp, inspectCachedFeishuOpenPlatformSession, } from '../setup/open-platform-automation.js';
|
|
11
12
|
import { sdkDomain } from '../im/lark/lark-hosts.js';
|
|
12
13
|
import * as Lark from '@larksuiteoapi/node-sdk';
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
14
15
|
const QRCode = require('qrcode-terminal/vendor/QRCode');
|
|
15
16
|
const QRErrorCorrectLevel = require('qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel');
|
|
17
|
+
const MANAGED_VERIFIED_EVENT_COUNT = BOT_BASELINE_APP_EVENTS.length + BOT_BASELINE_CALLBACKS.length;
|
|
18
|
+
function hasExactManagedAutomationAck(result) {
|
|
19
|
+
return result.ok
|
|
20
|
+
&& result.eventMode === 4
|
|
21
|
+
&& result.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
|
|
22
|
+
&& typeof result.versionId === 'string'
|
|
23
|
+
&& result.versionId.trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
function hasExactManagedPermissionAck(permission) {
|
|
26
|
+
return permission?.eventMode === 4
|
|
27
|
+
&& permission.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
|
|
28
|
+
&& typeof permission.versionId === 'string'
|
|
29
|
+
&& permission.versionId.trim().length > 0;
|
|
30
|
+
}
|
|
31
|
+
function managedActivationStateForSnapshot(job) {
|
|
32
|
+
if (job.criticalScopeActivationRequired !== true
|
|
33
|
+
|| !job.appId
|
|
34
|
+
|| !job.workingDir
|
|
35
|
+
|| job.platformQrScanConfirmedAt === undefined
|
|
36
|
+
|| !hasExactManagedPermissionAck(job.permission)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (job.status === 'failed'
|
|
40
|
+
&& job.recoveryOfJobId
|
|
41
|
+
&& job.error === 'permission_recovery_failed'
|
|
42
|
+
&& job.permission?.ok === false
|
|
43
|
+
&& job.permission.reason === 'scope_mapping_failed') {
|
|
44
|
+
return 'scope_propagation_pending';
|
|
45
|
+
}
|
|
46
|
+
if (job.status === 'completed'
|
|
47
|
+
&& job.permission?.ok === true
|
|
48
|
+
&& job.activationPending === true
|
|
49
|
+
&& job.activationCommitting === true) {
|
|
50
|
+
return 'activation_committing';
|
|
51
|
+
}
|
|
52
|
+
if (job.status === 'completed'
|
|
53
|
+
&& job.permission?.ok === true
|
|
54
|
+
&& job.activationPending === true) {
|
|
55
|
+
return 'initial_scope_propagation_pending';
|
|
56
|
+
}
|
|
57
|
+
if (job.status === 'completed'
|
|
58
|
+
&& job.permission?.ok === true
|
|
59
|
+
&& job.activationPending !== true
|
|
60
|
+
&& job.liveStarted === true) {
|
|
61
|
+
return 'completed';
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
function isExactManagedActivationMarker(value, appId, jobId) {
|
|
66
|
+
return !!value
|
|
67
|
+
&& typeof value === 'object'
|
|
68
|
+
&& !Array.isArray(value)
|
|
69
|
+
&& value.appId === appId
|
|
70
|
+
&& typeof value.jobId === 'string'
|
|
71
|
+
&& (jobId === undefined || value.jobId === jobId);
|
|
72
|
+
}
|
|
73
|
+
function managedActivationMarker(bot) {
|
|
74
|
+
if (!bot || typeof bot !== 'object' || Array.isArray(bot))
|
|
75
|
+
return undefined;
|
|
76
|
+
const record = bot;
|
|
77
|
+
const appId = typeof record.larkAppId === 'string' ? record.larkAppId : '';
|
|
78
|
+
const markers = [
|
|
79
|
+
['deactivating', record.activationDeactivating],
|
|
80
|
+
['starting', record.activationStarting],
|
|
81
|
+
['committed', record.activationCommitted],
|
|
82
|
+
].filter(([, value]) => value !== undefined);
|
|
83
|
+
if (markers.length !== 1)
|
|
84
|
+
return undefined;
|
|
85
|
+
const [phase, value] = markers[0];
|
|
86
|
+
if (!isExactManagedActivationMarker(value, appId))
|
|
87
|
+
return undefined;
|
|
88
|
+
return {
|
|
89
|
+
phase,
|
|
90
|
+
appId,
|
|
91
|
+
jobId: value.jobId,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
16
94
|
function svgEscape(value) {
|
|
17
95
|
return value.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
18
96
|
}
|
|
@@ -52,10 +130,19 @@ export class BotOnboardingManager {
|
|
|
52
130
|
registerApp;
|
|
53
131
|
validateCredentials;
|
|
54
132
|
automateOpenPlatform;
|
|
133
|
+
verifyCriticalScopes;
|
|
134
|
+
criticalScopeStableReads;
|
|
135
|
+
criticalScopeMaxAttempts;
|
|
136
|
+
criticalScopePollIntervalMs;
|
|
55
137
|
renderQrDataUrl;
|
|
56
138
|
now;
|
|
57
139
|
startBotLive;
|
|
58
140
|
pendingStorePath;
|
|
141
|
+
stopBotLive;
|
|
142
|
+
permissionRecoveryStorePath;
|
|
143
|
+
scopePropagationFlights = new Map();
|
|
144
|
+
activationStartupReconciliation = Promise.resolve();
|
|
145
|
+
permissionRecoveryStateError;
|
|
59
146
|
constructor(opts) {
|
|
60
147
|
this.opts = opts;
|
|
61
148
|
// 生产默认走单次 Web session;旧单测/外部注入若只给 registerApp,则明确
|
|
@@ -65,11 +152,33 @@ export class BotOnboardingManager {
|
|
|
65
152
|
this.registerApp = opts.registerApp ?? tryRegisterApp;
|
|
66
153
|
this.validateCredentials = opts.validateCredentials ?? validateCredentials;
|
|
67
154
|
this.automateOpenPlatform = opts.automateOpenPlatform ?? automateOpenPlatformSetup;
|
|
155
|
+
this.verifyCriticalScopes = opts.verifyCriticalScopes ?? readCriticalScopesFromApplicationInfo;
|
|
156
|
+
this.criticalScopeStableReads = Number.isInteger(opts.criticalScopeStableReads)
|
|
157
|
+
&& (opts.criticalScopeStableReads ?? 0) > 0
|
|
158
|
+
? opts.criticalScopeStableReads
|
|
159
|
+
: 3;
|
|
160
|
+
const configuredMaxAttempts = Number.isInteger(opts.criticalScopeMaxAttempts)
|
|
161
|
+
&& (opts.criticalScopeMaxAttempts ?? 0) > 0
|
|
162
|
+
? opts.criticalScopeMaxAttempts
|
|
163
|
+
: 12;
|
|
164
|
+
this.criticalScopeMaxAttempts = Math.max(this.criticalScopeStableReads, configuredMaxAttempts);
|
|
165
|
+
this.criticalScopePollIntervalMs = Number.isInteger(opts.criticalScopePollIntervalMs)
|
|
166
|
+
&& (opts.criticalScopePollIntervalMs ?? -1) >= 0
|
|
167
|
+
? opts.criticalScopePollIntervalMs
|
|
168
|
+
: 5_000;
|
|
68
169
|
this.renderQrDataUrl = opts.renderQrDataUrl ?? renderQrSvgDataUrl;
|
|
69
170
|
this.now = opts.now ?? (() => Date.now());
|
|
70
171
|
this.startBotLive = opts.startBotLive;
|
|
71
172
|
this.pendingStorePath = opts.pendingStorePath ?? `${opts.botsJsonPath}.onboarding-pending.json`;
|
|
173
|
+
this.stopBotLive = opts.stopBotLive;
|
|
174
|
+
this.permissionRecoveryStorePath = opts.permissionRecoveryStorePath ?? `${opts.botsJsonPath}.permission-recoveries.json`;
|
|
72
175
|
this.restorePendingJobs();
|
|
176
|
+
this.restorePermissionRecoveryJobs();
|
|
177
|
+
this.requireDurableManagedInitialJobs();
|
|
178
|
+
// A dashboard crash after PM2 has accepted the start but before the
|
|
179
|
+
// durable marker is cleared must converge to pending, never assume the
|
|
180
|
+
// daemon is safe to keep online.
|
|
181
|
+
this.activationStartupReconciliation = this.reconcileInterruptedManagedActivations();
|
|
73
182
|
}
|
|
74
183
|
/**
|
|
75
184
|
* 恢复 owner 待确认任务。凭证只存在 0600 私有文件和内存中,公开 job snapshot
|
|
@@ -137,21 +246,640 @@ export class BotOnboardingManager {
|
|
|
137
246
|
logger.warn(`[bot-onboarding] 无法持久化 owner 待确认任务: ${err?.message ?? String(err)}`);
|
|
138
247
|
}
|
|
139
248
|
}
|
|
249
|
+
restorePermissionRecoveryJobs() {
|
|
250
|
+
if (!existsSync(this.permissionRecoveryStorePath))
|
|
251
|
+
return;
|
|
252
|
+
let parsed;
|
|
253
|
+
try {
|
|
254
|
+
parsed = JSON.parse(readFileSync(this.permissionRecoveryStorePath, 'utf8'));
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
this.permissionRecoveryStateError = `permission recovery ledger is unreadable: ${err instanceof Error ? err.message : String(err)}`;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.jobs) || parsed.jobs.length > 128) {
|
|
261
|
+
this.permissionRecoveryStateError = 'permission recovery ledger schema is invalid';
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const records = parsed.jobs;
|
|
265
|
+
const restored = [];
|
|
266
|
+
const ids = new Set();
|
|
267
|
+
for (const raw of records) {
|
|
268
|
+
const isRecovery = (typeof raw?.recoveryOfJobId === 'string'
|
|
269
|
+
&& raw.recoveryOfJobId.length > 0);
|
|
270
|
+
const isManagedInitial = (!isRecovery
|
|
271
|
+
&& ['initial_scope_propagation_pending', 'activation_committing', 'completed'].includes(raw?.managedActivationState));
|
|
272
|
+
if (!raw || typeof raw !== 'object'
|
|
273
|
+
|| typeof raw.id !== 'string'
|
|
274
|
+
|| (isRecovery
|
|
275
|
+
? !/^botperm_[A-Za-z0-9_-]{1,160}$/.test(raw.id)
|
|
276
|
+
: !/^bot_[A-Za-z0-9_-]{1,160}$/.test(raw.id))
|
|
277
|
+
|| (!isRecovery && !isManagedInitial)
|
|
278
|
+
|| ids.has(raw.id)
|
|
279
|
+
|| typeof raw.appId !== 'string' || !raw.appId
|
|
280
|
+
|| typeof raw.workingDir !== 'string' || !raw.workingDir
|
|
281
|
+
|| !Number.isFinite(raw.createdAt) || !Number.isFinite(raw.updatedAt)
|
|
282
|
+
|| (isRecovery
|
|
283
|
+
? (!Number.isInteger(raw.recoveryAttempt) || raw.recoveryAttempt < 1)
|
|
284
|
+
: raw.recoveryAttempt !== undefined)
|
|
285
|
+
|| !['starting', 'configuring_permissions', 'waiting_for_platform_scan', 'completed', 'failed'].includes(raw.status)
|
|
286
|
+
|| (isManagedInitial && raw.status !== 'completed')
|
|
287
|
+
|| !['feishu', 'lark'].includes(raw.brand)
|
|
288
|
+
|| (raw.previousRecoveryJobId !== undefined
|
|
289
|
+
&& (!isRecovery || typeof raw.previousRecoveryJobId !== 'string'))
|
|
290
|
+
|| (raw.platformQrScanConfirmedAt !== undefined
|
|
291
|
+
&& (!Number.isInteger(raw.platformQrScanConfirmedAt) || raw.platformQrScanConfirmedAt <= 0))
|
|
292
|
+
|| (raw.activationDeactivating !== undefined && raw.activationDeactivating !== true)
|
|
293
|
+
|| (raw.activationCommitting !== undefined && raw.activationCommitting !== true)) {
|
|
294
|
+
this.permissionRecoveryStateError = 'permission recovery ledger contains an invalid record';
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
ids.add(raw.id);
|
|
298
|
+
const status = ['completed', 'failed'].includes(raw.status)
|
|
299
|
+
? raw.status
|
|
300
|
+
: 'failed';
|
|
301
|
+
const snapshot = {
|
|
302
|
+
id: raw.id,
|
|
303
|
+
status,
|
|
304
|
+
createdAt: raw.createdAt,
|
|
305
|
+
updatedAt: this.now(),
|
|
306
|
+
appId: raw.appId,
|
|
307
|
+
brand: raw.brand === 'lark' ? 'lark' : 'feishu',
|
|
308
|
+
cliId: 'traex',
|
|
309
|
+
workingDir: raw.workingDir,
|
|
310
|
+
registrationMode: 'compat',
|
|
311
|
+
...(isRecovery
|
|
312
|
+
? {
|
|
313
|
+
recoveryOfJobId: raw.recoveryOfJobId,
|
|
314
|
+
recoveryAttempt: raw.recoveryAttempt,
|
|
315
|
+
...(typeof raw.previousRecoveryJobId === 'string'
|
|
316
|
+
? { previousRecoveryJobId: raw.previousRecoveryJobId }
|
|
317
|
+
: {}),
|
|
318
|
+
}
|
|
319
|
+
: {}),
|
|
320
|
+
...(raw.criticalScopeActivationRequired === true
|
|
321
|
+
? { criticalScopeActivationRequired: true }
|
|
322
|
+
: {}),
|
|
323
|
+
...(raw.activationPending === true
|
|
324
|
+
|| raw.managedActivationState === 'initial_scope_propagation_pending'
|
|
325
|
+
? { activationPending: true }
|
|
326
|
+
: {}),
|
|
327
|
+
...(raw.activationDeactivating === true
|
|
328
|
+
? { activationDeactivating: true, activationPending: true }
|
|
329
|
+
: {}),
|
|
330
|
+
...(raw.activationCommitting === true
|
|
331
|
+
? { activationCommitting: true, activationPending: true }
|
|
332
|
+
: {}),
|
|
333
|
+
...(Number.isInteger(raw.platformQrScanConfirmedAt)
|
|
334
|
+
? { platformQrScanConfirmedAt: raw.platformQrScanConfirmedAt }
|
|
335
|
+
: {}),
|
|
336
|
+
...(raw.managedActivationState === 'initial_scope_propagation_pending'
|
|
337
|
+
? {
|
|
338
|
+
permission: {
|
|
339
|
+
ok: true,
|
|
340
|
+
eventMode: raw.managedActivationAck?.eventMode,
|
|
341
|
+
verifiedEventCount: raw.managedActivationAck?.verifiedEventCount,
|
|
342
|
+
versionId: raw.managedActivationAck?.versionId,
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
: {}),
|
|
346
|
+
...(raw.managedActivationState === 'scope_propagation_pending'
|
|
347
|
+
? {
|
|
348
|
+
permission: {
|
|
349
|
+
ok: false,
|
|
350
|
+
reason: 'scope_mapping_failed',
|
|
351
|
+
message: '关键权限发布后仍在传播,等待 botmux 精确回读并启动',
|
|
352
|
+
eventMode: raw.managedActivationAck?.eventMode,
|
|
353
|
+
verifiedEventCount: raw.managedActivationAck?.verifiedEventCount,
|
|
354
|
+
versionId: raw.managedActivationAck?.versionId,
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
: {}),
|
|
358
|
+
...(raw.managedActivationState === 'activation_committing'
|
|
359
|
+
? {
|
|
360
|
+
activationPending: true,
|
|
361
|
+
activationCommitting: true,
|
|
362
|
+
permission: {
|
|
363
|
+
ok: true,
|
|
364
|
+
eventMode: raw.managedActivationAck?.eventMode,
|
|
365
|
+
verifiedEventCount: raw.managedActivationAck?.verifiedEventCount,
|
|
366
|
+
versionId: raw.managedActivationAck?.versionId,
|
|
367
|
+
},
|
|
368
|
+
}
|
|
369
|
+
: {}),
|
|
370
|
+
...(raw.managedActivationState === 'completed'
|
|
371
|
+
? {
|
|
372
|
+
liveStarted: true,
|
|
373
|
+
activationPending: false,
|
|
374
|
+
permission: {
|
|
375
|
+
ok: true,
|
|
376
|
+
eventMode: raw.managedActivationAck?.eventMode,
|
|
377
|
+
verifiedEventCount: raw.managedActivationAck?.verifiedEventCount,
|
|
378
|
+
versionId: raw.managedActivationAck?.versionId,
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
: {}),
|
|
382
|
+
...(status === 'failed'
|
|
383
|
+
? {
|
|
384
|
+
error: raw.status === 'failed' && typeof raw.error === 'string'
|
|
385
|
+
? raw.error
|
|
386
|
+
: 'permission_recovery_interrupted',
|
|
387
|
+
message: raw.status === 'failed' && typeof raw.message === 'string'
|
|
388
|
+
? raw.message
|
|
389
|
+
: 'Dashboard restarted before permission recovery reached a terminal result',
|
|
390
|
+
}
|
|
391
|
+
: {}),
|
|
392
|
+
};
|
|
393
|
+
if ((raw.managedActivationState !== undefined
|
|
394
|
+
&& ![
|
|
395
|
+
'initial_scope_propagation_pending',
|
|
396
|
+
'scope_propagation_pending',
|
|
397
|
+
'activation_committing',
|
|
398
|
+
'completed',
|
|
399
|
+
].includes(raw.managedActivationState))
|
|
400
|
+
|| (raw.managedActivationState === 'initial_scope_propagation_pending'
|
|
401
|
+
&& (raw.status !== 'completed'
|
|
402
|
+
|| raw.activationPending !== true))
|
|
403
|
+
|| (raw.managedActivationState === 'scope_propagation_pending'
|
|
404
|
+
&& (!isRecovery
|
|
405
|
+
|| raw.status !== 'failed'
|
|
406
|
+
|| raw.error !== 'permission_recovery_failed'))
|
|
407
|
+
|| (raw.managedActivationState === 'activation_committing'
|
|
408
|
+
&& (raw.status !== 'completed'
|
|
409
|
+
|| raw.activationPending !== true
|
|
410
|
+
|| raw.activationDeactivating === true
|
|
411
|
+
|| raw.activationCommitting !== true))
|
|
412
|
+
|| (raw.managedActivationState === 'completed'
|
|
413
|
+
&& raw.status !== 'completed')
|
|
414
|
+
|| ((raw.managedActivationState !== undefined)
|
|
415
|
+
!== (raw.criticalScopeActivationRequired === true
|
|
416
|
+
&& Number.isInteger(raw.platformQrScanConfirmedAt)
|
|
417
|
+
&& raw.managedActivationAck?.eventMode === 4
|
|
418
|
+
&& raw.managedActivationAck?.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
|
|
419
|
+
&& typeof raw.managedActivationAck?.versionId === 'string'
|
|
420
|
+
&& raw.managedActivationAck.versionId.trim().length > 0))) {
|
|
421
|
+
this.permissionRecoveryStateError = 'permission recovery ledger contains an invalid activation ACK';
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
restored.push(snapshot);
|
|
425
|
+
}
|
|
426
|
+
const groups = new Map();
|
|
427
|
+
for (const snapshot of restored.filter(item => item.recoveryOfJobId)) {
|
|
428
|
+
const key = JSON.stringify([snapshot.recoveryOfJobId, snapshot.workingDir, snapshot.appId]);
|
|
429
|
+
const group = groups.get(key) ?? [];
|
|
430
|
+
group.push(snapshot);
|
|
431
|
+
groups.set(key, group);
|
|
432
|
+
}
|
|
433
|
+
for (const group of groups.values()) {
|
|
434
|
+
group.sort((a, b) => (a.recoveryAttempt ?? 0) - (b.recoveryAttempt ?? 0));
|
|
435
|
+
for (let index = 0; index < group.length; index += 1) {
|
|
436
|
+
if (group[index].recoveryAttempt !== index + 1
|
|
437
|
+
|| (index === 0 && group[index].previousRecoveryJobId !== undefined)
|
|
438
|
+
|| (index > 0 && group[index].previousRecoveryJobId !== group[index - 1].id)) {
|
|
439
|
+
this.permissionRecoveryStateError = 'permission recovery ledger lineage is ambiguous';
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
for (const snapshot of restored) {
|
|
446
|
+
if (snapshot.error === 'permission_recovery_interrupted') {
|
|
447
|
+
rmSync(join(dirname(this.opts.botsJsonPath), 'onboarding-sessions', `${snapshot.id}.json`), { force: true });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
this.permissionRecoveryStateError = `interrupted owner session could not be cleaned: ${err instanceof Error ? err.message : String(err)}`;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
for (const snapshot of restored) {
|
|
456
|
+
this.jobs.set(snapshot.id, snapshot);
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
this.persistPermissionRecoveryJobs();
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
this.permissionRecoveryStateError = `permission recovery ledger cannot be updated: ${err instanceof Error ? err.message : String(err)}`;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
persistPermissionRecoveryJobs() {
|
|
466
|
+
const jobs = [...this.jobs.values()]
|
|
467
|
+
.filter(job => ((job.recoveryOfJobId && job.appId && job.workingDir && job.recoveryAttempt)
|
|
468
|
+
|| managedActivationStateForSnapshot(job) !== undefined))
|
|
469
|
+
.sort((a, b) => a.updatedAt - b.updatedAt)
|
|
470
|
+
.slice(-128)
|
|
471
|
+
.map(job => {
|
|
472
|
+
const managedActivationState = managedActivationStateForSnapshot(job);
|
|
473
|
+
return {
|
|
474
|
+
id: job.id,
|
|
475
|
+
status: job.status,
|
|
476
|
+
createdAt: job.createdAt,
|
|
477
|
+
updatedAt: job.updatedAt,
|
|
478
|
+
appId: job.appId,
|
|
479
|
+
brand: job.brand,
|
|
480
|
+
workingDir: job.workingDir,
|
|
481
|
+
recoveryOfJobId: job.recoveryOfJobId,
|
|
482
|
+
recoveryAttempt: job.recoveryAttempt,
|
|
483
|
+
previousRecoveryJobId: job.previousRecoveryJobId,
|
|
484
|
+
criticalScopeActivationRequired: job.criticalScopeActivationRequired,
|
|
485
|
+
activationPending: job.activationPending,
|
|
486
|
+
...(job.activationDeactivating === true
|
|
487
|
+
? { activationDeactivating: true }
|
|
488
|
+
: {}),
|
|
489
|
+
...(job.activationCommitting === true
|
|
490
|
+
? { activationCommitting: true }
|
|
491
|
+
: {}),
|
|
492
|
+
platformQrScanConfirmedAt: job.platformQrScanConfirmedAt,
|
|
493
|
+
error: job.error,
|
|
494
|
+
message: job.message,
|
|
495
|
+
...(managedActivationState && hasExactManagedPermissionAck(job.permission)
|
|
496
|
+
? {
|
|
497
|
+
managedActivationState,
|
|
498
|
+
managedActivationAck: {
|
|
499
|
+
eventMode: job.permission.eventMode,
|
|
500
|
+
verifiedEventCount: job.permission.verifiedEventCount,
|
|
501
|
+
versionId: job.permission.versionId,
|
|
502
|
+
},
|
|
503
|
+
}
|
|
504
|
+
: {}),
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
if (jobs.length === 0)
|
|
508
|
+
return;
|
|
509
|
+
atomicWriteFileSync(this.permissionRecoveryStorePath, `${JSON.stringify({ version: 1, jobs }, null, 2)}\n`, { mode: 0o600 });
|
|
510
|
+
}
|
|
511
|
+
savePermissionRecoveryJobsBestEffort() {
|
|
512
|
+
try {
|
|
513
|
+
this.persistPermissionRecoveryJobs();
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
logger.warn(`[bot-onboarding] 无法更新权限恢复 lineage: ${err?.message ?? String(err)}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* A managed initial ACK is restart authority, not optional UI history.
|
|
521
|
+
* Refuse activation when its first durable write cannot be proven.
|
|
522
|
+
*/
|
|
523
|
+
requireDurableManagedInitialJobs() {
|
|
524
|
+
if (this.permissionRecoveryStateError)
|
|
525
|
+
return;
|
|
526
|
+
const hasPendingInitial = [...this.jobs.values()].some(job => (!job.recoveryOfJobId
|
|
527
|
+
&& managedActivationStateForSnapshot(job) === 'initial_scope_propagation_pending'));
|
|
528
|
+
if (!hasPendingInitial)
|
|
529
|
+
return;
|
|
530
|
+
try {
|
|
531
|
+
this.persistPermissionRecoveryJobs();
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
this.permissionRecoveryStateError = `managed initial activation ledger could not be persisted: ${err instanceof Error ? err.message : String(err)}`;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
140
537
|
/**
|
|
141
538
|
* Best-effort auto-start of the just-persisted bot's daemon (no fleet restart).
|
|
142
539
|
* Records the outcome on the job snapshot so the frontend shows "已自动上线"
|
|
143
540
|
* instead of the restart hint. Never throws.
|
|
144
541
|
*/
|
|
145
|
-
async runLiveStart(id, appId) {
|
|
542
|
+
async runLiveStart(id, appId, recordOutcome = true) {
|
|
146
543
|
if (!this.startBotLive)
|
|
147
|
-
return;
|
|
544
|
+
return { attempted: false };
|
|
148
545
|
try {
|
|
149
546
|
const r = await this.startBotLive(appId);
|
|
150
|
-
|
|
547
|
+
if (recordOutcome) {
|
|
548
|
+
this.patch(id, { liveStarted: r.ok, liveStartMessage: r.message });
|
|
549
|
+
}
|
|
550
|
+
return { attempted: true, ok: r.ok, message: r.message };
|
|
151
551
|
}
|
|
152
552
|
catch (err) {
|
|
153
|
-
|
|
553
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
554
|
+
if (recordOutcome) {
|
|
555
|
+
this.patch(id, { liveStarted: false, liveStartMessage: message });
|
|
556
|
+
}
|
|
557
|
+
return { attempted: true, ok: false, message };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async runLiveStop(id, appId) {
|
|
561
|
+
if (!this.stopBotLive)
|
|
562
|
+
return { attempted: false };
|
|
563
|
+
try {
|
|
564
|
+
const r = await this.stopBotLive(appId);
|
|
565
|
+
this.patch(id, { liveStopped: r.ok, liveStopMessage: r.message });
|
|
566
|
+
return { attempted: true, ok: r.ok, message: r.message };
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
570
|
+
this.patch(id, { liveStopped: false, liveStopMessage: message });
|
|
571
|
+
return { attempted: true, ok: false, message };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async stableCriticalScopeReadback(id, appId, appSecret, brand) {
|
|
575
|
+
let consecutiveComplete = 0;
|
|
576
|
+
let readback = {
|
|
577
|
+
ok: false,
|
|
578
|
+
error: 'network',
|
|
579
|
+
message: '尚未读取权限状态',
|
|
580
|
+
};
|
|
581
|
+
for (let attempt = 1; attempt <= this.criticalScopeMaxAttempts; attempt += 1) {
|
|
582
|
+
try {
|
|
583
|
+
readback = await this.verifyCriticalScopes(appId, appSecret, brand);
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
readback = {
|
|
587
|
+
ok: false,
|
|
588
|
+
error: 'network',
|
|
589
|
+
message: err instanceof Error ? err.message : String(err),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (readback.ok && readback.missingCritical.length === 0) {
|
|
593
|
+
consecutiveComplete += 1;
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
consecutiveComplete = 0;
|
|
597
|
+
}
|
|
598
|
+
this.patch(id, {
|
|
599
|
+
permissionStatusMsg: consecutiveComplete > 0
|
|
600
|
+
? `正在确认 9 项核心权限稳定生效(${consecutiveComplete}/${this.criticalScopeStableReads})`
|
|
601
|
+
: `正在等待 9 项核心权限生效(${attempt}/${this.criticalScopeMaxAttempts})`,
|
|
602
|
+
});
|
|
603
|
+
if (consecutiveComplete >= this.criticalScopeStableReads) {
|
|
604
|
+
return { ready: true, readback };
|
|
605
|
+
}
|
|
606
|
+
if (attempt < this.criticalScopeMaxAttempts
|
|
607
|
+
&& this.criticalScopePollIntervalMs > 0) {
|
|
608
|
+
await new Promise(resolve => {
|
|
609
|
+
setTimeout(resolve, this.criticalScopePollIntervalMs);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return { ready: false, readback };
|
|
614
|
+
}
|
|
615
|
+
async activateRecoveredBot(id, appId, addedBotIndex, ensureLiveStart = false) {
|
|
616
|
+
const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
617
|
+
const current = bots[addedBotIndex];
|
|
618
|
+
if (!current
|
|
619
|
+
|| typeof current !== 'object'
|
|
620
|
+
|| current.larkAppId !== appId
|
|
621
|
+
|| current.activationStarting !== undefined
|
|
622
|
+
|| current.activationCommitted !== undefined
|
|
623
|
+
|| current.activationCommitting !== undefined) {
|
|
624
|
+
throw new Error('permission_recovery_activation_target_drift');
|
|
625
|
+
}
|
|
626
|
+
// Existing pre-gate bots were already daemon-visible. Their exact App may
|
|
627
|
+
// recover permissions without performing a second activation transition.
|
|
628
|
+
if (current.activationPending !== true && !ensureLiveStart) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
this.beginManagedActivation(id, appId, addedBotIndex);
|
|
632
|
+
const live = await this.runLiveStart(id, appId, false);
|
|
633
|
+
if (live.attempted && live.ok === true) {
|
|
634
|
+
try {
|
|
635
|
+
this.patch(id, {
|
|
636
|
+
activationPending: true,
|
|
637
|
+
activationCommitting: true,
|
|
638
|
+
liveStarted: false,
|
|
639
|
+
liveStartMessage: live.message,
|
|
640
|
+
});
|
|
641
|
+
this.persistPermissionRecoveryJobs();
|
|
642
|
+
this.commitManagedActivation(id, appId, addedBotIndex);
|
|
643
|
+
this.completeManagedActivation(id, appId, addedBotIndex);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
const stopped = await this.runLiveStop(id, appId);
|
|
648
|
+
if (stopped.attempted && stopped.ok === true) {
|
|
649
|
+
this.restoreManagedActivationPending(id, appId, addedBotIndex);
|
|
650
|
+
}
|
|
651
|
+
throw err;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// An exit/timeout from start-bot is an unknown PM2 outcome. Stop and
|
|
655
|
+
// re-read the exact App before returning it to pending; otherwise a daemon
|
|
656
|
+
// could remain online while the durable config says it is blocked.
|
|
657
|
+
const stopped = await this.runLiveStop(id, appId);
|
|
658
|
+
if (!stopped.attempted || stopped.ok !== true) {
|
|
659
|
+
throw new Error(`permission_recovery_activation_stop_not_acknowledged${stopped.message ? `: ${stopped.message}` : ''}`);
|
|
660
|
+
}
|
|
661
|
+
this.restoreManagedActivationPending(id, appId, addedBotIndex);
|
|
662
|
+
throw new Error(`permission_recovery_activation_not_acknowledged${live.message ? `: ${live.message}` : ''}`);
|
|
663
|
+
}
|
|
664
|
+
beginManagedActivation(id, appId, addedBotIndex) {
|
|
665
|
+
const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
666
|
+
const current = bots[addedBotIndex];
|
|
667
|
+
if (!current
|
|
668
|
+
|| typeof current !== 'object'
|
|
669
|
+
|| current.larkAppId !== appId
|
|
670
|
+
|| current.activationPending !== true) {
|
|
671
|
+
throw new Error('permission_recovery_activation_target_drift');
|
|
672
|
+
}
|
|
673
|
+
const next = [...bots];
|
|
674
|
+
const activating = {
|
|
675
|
+
...current,
|
|
676
|
+
activationStarting: { appId, jobId: id },
|
|
677
|
+
};
|
|
678
|
+
delete activating.activationPending;
|
|
679
|
+
delete activating.activationDeactivating;
|
|
680
|
+
delete activating.activationCommitting;
|
|
681
|
+
next[addedBotIndex] = activating;
|
|
682
|
+
writeBotsJsonAtomic(this.opts.botsJsonPath, next);
|
|
683
|
+
}
|
|
684
|
+
commitManagedActivation(id, appId, addedBotIndex) {
|
|
685
|
+
const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
686
|
+
const current = bots[addedBotIndex];
|
|
687
|
+
if (!current
|
|
688
|
+
|| typeof current !== 'object'
|
|
689
|
+
|| current.larkAppId !== appId
|
|
690
|
+
|| current.activationPending === true
|
|
691
|
+
|| !isExactManagedActivationMarker(current.activationStarting, appId, id)) {
|
|
692
|
+
throw new Error('permission_recovery_activation_ack_target_drift');
|
|
693
|
+
}
|
|
694
|
+
const next = [...bots];
|
|
695
|
+
const ready = { ...current, activationCommitted: { appId, jobId: id } };
|
|
696
|
+
delete ready.activationStarting;
|
|
697
|
+
next[addedBotIndex] = ready;
|
|
698
|
+
writeBotsJsonAtomic(this.opts.botsJsonPath, next);
|
|
699
|
+
}
|
|
700
|
+
restoreManagedActivationPending(id, appId, addedBotIndex) {
|
|
701
|
+
const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
702
|
+
const current = bots[addedBotIndex];
|
|
703
|
+
if (!current
|
|
704
|
+
|| typeof current !== 'object'
|
|
705
|
+
|| current.larkAppId !== appId
|
|
706
|
+
|| (!isExactManagedActivationMarker(current.activationStarting, appId, id)
|
|
707
|
+
&& !isExactManagedActivationMarker(current.activationDeactivating, appId, id)
|
|
708
|
+
&& !isExactManagedActivationMarker(current.activationCommitted, appId, id))) {
|
|
709
|
+
throw new Error('permission_recovery_activation_rollback_target_drift');
|
|
710
|
+
}
|
|
711
|
+
const next = [...bots];
|
|
712
|
+
const pending = { ...current, activationPending: true };
|
|
713
|
+
delete pending.activationStarting;
|
|
714
|
+
delete pending.activationDeactivating;
|
|
715
|
+
delete pending.activationCommitted;
|
|
716
|
+
next[addedBotIndex] = pending;
|
|
717
|
+
writeBotsJsonAtomic(this.opts.botsJsonPath, next);
|
|
718
|
+
this.patch(id, {
|
|
719
|
+
activationPending: true,
|
|
720
|
+
activationDeactivating: false,
|
|
721
|
+
activationCommitting: false,
|
|
722
|
+
liveStarted: false,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
completeManagedActivation(id, appId, addedBotIndex) {
|
|
726
|
+
const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
727
|
+
const current = bots[addedBotIndex];
|
|
728
|
+
if (!current
|
|
729
|
+
|| typeof current !== 'object'
|
|
730
|
+
|| current.larkAppId !== appId
|
|
731
|
+
|| current.activationPending === true
|
|
732
|
+
|| current.activationStarting !== undefined
|
|
733
|
+
|| current.activationDeactivating !== undefined
|
|
734
|
+
|| !isExactManagedActivationMarker(current.activationCommitted, appId, id)) {
|
|
735
|
+
throw new Error('permission_recovery_activation_completion_target_drift');
|
|
736
|
+
}
|
|
737
|
+
this.patch(id, {
|
|
738
|
+
activationPending: false,
|
|
739
|
+
activationCommitting: false,
|
|
740
|
+
liveStarted: true,
|
|
741
|
+
});
|
|
742
|
+
this.persistPermissionRecoveryJobs();
|
|
743
|
+
const next = [...bots];
|
|
744
|
+
const ready = { ...current };
|
|
745
|
+
delete ready.activationCommitted;
|
|
746
|
+
next[addedBotIndex] = ready;
|
|
747
|
+
writeBotsJsonAtomic(this.opts.botsJsonPath, next);
|
|
748
|
+
}
|
|
749
|
+
clearManagedActivationCommitted(id, appId, addedBotIndex) {
|
|
750
|
+
const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
751
|
+
const current = bots[addedBotIndex];
|
|
752
|
+
if (!current
|
|
753
|
+
|| typeof current !== 'object'
|
|
754
|
+
|| current.larkAppId !== appId
|
|
755
|
+
|| !isExactManagedActivationMarker(current.activationCommitted, appId, id)) {
|
|
756
|
+
throw new Error('permission_recovery_activation_committed_target_drift');
|
|
757
|
+
}
|
|
758
|
+
const next = [...bots];
|
|
759
|
+
const ready = { ...current };
|
|
760
|
+
delete ready.activationCommitted;
|
|
761
|
+
next[addedBotIndex] = ready;
|
|
762
|
+
writeBotsJsonAtomic(this.opts.botsJsonPath, next);
|
|
763
|
+
}
|
|
764
|
+
async reconcileInterruptedManagedActivations() {
|
|
765
|
+
const targets = readBotsJsonOrEmpty(this.opts.botsJsonPath).flatMap((bot, index) => {
|
|
766
|
+
const marker = managedActivationMarker(bot);
|
|
767
|
+
if (!marker) {
|
|
768
|
+
if (bot
|
|
769
|
+
&& typeof bot === 'object'
|
|
770
|
+
&& (bot.activationDeactivating !== undefined
|
|
771
|
+
|| bot.activationStarting !== undefined
|
|
772
|
+
|| bot.activationCommitted !== undefined
|
|
773
|
+
|| bot.activationCommitting !== undefined)) {
|
|
774
|
+
this.permissionRecoveryStateError = 'managed activation marker is invalid';
|
|
775
|
+
}
|
|
776
|
+
return [];
|
|
777
|
+
}
|
|
778
|
+
if (marker.phase === 'deactivating'
|
|
779
|
+
|| marker.phase === 'starting'
|
|
780
|
+
|| marker.phase === 'committed') {
|
|
781
|
+
return [{ index, ...marker }];
|
|
782
|
+
}
|
|
783
|
+
if (!marker.appId) {
|
|
784
|
+
this.permissionRecoveryStateError = 'managed activation marker is invalid';
|
|
785
|
+
return [];
|
|
786
|
+
}
|
|
787
|
+
return [];
|
|
788
|
+
});
|
|
789
|
+
if (this.permissionRecoveryStateError)
|
|
790
|
+
return;
|
|
791
|
+
for (const target of targets) {
|
|
792
|
+
try {
|
|
793
|
+
const job = this.jobs.get(target.jobId);
|
|
794
|
+
if (target.phase === 'committed'
|
|
795
|
+
&& job?.status === 'completed'
|
|
796
|
+
&& job.activationPending !== true
|
|
797
|
+
&& job.liveStarted === true) {
|
|
798
|
+
this.clearManagedActivationCommitted(target.jobId, target.appId, target.index);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const stopped = await this.runLiveStop(target.jobId, target.appId);
|
|
802
|
+
if (!stopped.attempted || stopped.ok !== true) {
|
|
803
|
+
this.permissionRecoveryStateError = `managed activation stop could not be confirmed for ${target.appId}`;
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
this.restoreManagedActivationPending(target.jobId, target.appId, target.index);
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
this.permissionRecoveryStateError = `managed activation rollback failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (const job of this.jobs.values()) {
|
|
814
|
+
if (job.activationCommitting !== true
|
|
815
|
+
|| job.status !== 'completed'
|
|
816
|
+
|| !job.appId
|
|
817
|
+
|| !job.workingDir) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const matches = readBotsJsonOrEmpty(this.opts.botsJsonPath).flatMap((bot, index) => (bot
|
|
821
|
+
&& typeof bot === 'object'
|
|
822
|
+
&& bot.larkAppId === job.appId
|
|
823
|
+
&& bot.cliId === 'traex'
|
|
824
|
+
&& bot.defaultWorkingDir === job.workingDir
|
|
825
|
+
&& bot.activationPending !== true
|
|
826
|
+
&& bot.activationStarting === undefined
|
|
827
|
+
&& bot.activationDeactivating === undefined
|
|
828
|
+
&& isExactManagedActivationMarker(bot.activationCommitted, job.appId, job.id)
|
|
829
|
+
? [index]
|
|
830
|
+
: []));
|
|
831
|
+
if (matches.length !== 1) {
|
|
832
|
+
this.permissionRecoveryStateError = `managed activation commit could not be reconciled for ${job.appId}`;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
this.completeManagedActivation(job.id, job.appId, matches[0]);
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
this.permissionRecoveryStateError = `managed activation commit reconciliation failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
holdRecoveryActivation(id, appId, addedBotIndex) {
|
|
845
|
+
const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
846
|
+
const current = bots[addedBotIndex];
|
|
847
|
+
if (!current
|
|
848
|
+
|| typeof current !== 'object'
|
|
849
|
+
|| current.larkAppId !== appId) {
|
|
850
|
+
throw new Error('permission_recovery_activation_target_drift');
|
|
851
|
+
}
|
|
852
|
+
if (current.activationStarting !== undefined
|
|
853
|
+
|| current.activationCommitted !== undefined
|
|
854
|
+
|| (current.activationDeactivating !== undefined
|
|
855
|
+
&& !isExactManagedActivationMarker(current.activationDeactivating, appId, id))) {
|
|
856
|
+
throw new Error('permission_recovery_activation_target_drift');
|
|
857
|
+
}
|
|
858
|
+
const next = [...bots];
|
|
859
|
+
next[addedBotIndex] = {
|
|
860
|
+
...current,
|
|
861
|
+
activationPending: true,
|
|
862
|
+
activationDeactivating: { appId, jobId: id },
|
|
863
|
+
};
|
|
864
|
+
writeBotsJsonAtomic(this.opts.botsJsonPath, next);
|
|
865
|
+
this.patch(id, { activationPending: true, activationDeactivating: true });
|
|
866
|
+
this.persistPermissionRecoveryJobs();
|
|
867
|
+
}
|
|
868
|
+
markManagedDeactivated(id, appId, addedBotIndex) {
|
|
869
|
+
const bots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
870
|
+
const current = bots[addedBotIndex];
|
|
871
|
+
if (!current
|
|
872
|
+
|| typeof current !== 'object'
|
|
873
|
+
|| current.larkAppId !== appId
|
|
874
|
+
|| !isExactManagedActivationMarker(current.activationDeactivating, appId, id)) {
|
|
875
|
+
throw new Error('permission_recovery_deactivation_target_drift');
|
|
154
876
|
}
|
|
877
|
+
const next = [...bots];
|
|
878
|
+
const pending = { ...current, activationPending: true };
|
|
879
|
+
delete pending.activationDeactivating;
|
|
880
|
+
next[addedBotIndex] = pending;
|
|
881
|
+
writeBotsJsonAtomic(this.opts.botsJsonPath, next);
|
|
882
|
+
this.patch(id, { activationPending: true, activationDeactivating: false });
|
|
155
883
|
}
|
|
156
884
|
start(input = {}) {
|
|
157
885
|
const id = `bot_${Math.random().toString(36).slice(2)}_${this.now().toString(36)}`;
|
|
@@ -166,6 +894,221 @@ export class BotOnboardingManager {
|
|
|
166
894
|
});
|
|
167
895
|
return { id, done };
|
|
168
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* Resume only Open Platform authorization for one already-persisted Space
|
|
899
|
+
* Agent. The exact neutral working directory is the durable target anchor:
|
|
900
|
+
* zero or multiple matching bots fail closed, and this path never creates an
|
|
901
|
+
* App or registers another Bot. A managed caller may place the exact existing
|
|
902
|
+
* row behind the critical-scope activation marker before the fresh QR.
|
|
903
|
+
*/
|
|
904
|
+
startPermissionRecovery(input) {
|
|
905
|
+
if (this.permissionRecoveryStateError) {
|
|
906
|
+
return { ok: false, error: 'permission_recovery_state_unavailable' };
|
|
907
|
+
}
|
|
908
|
+
const configuredBots = readBotsJsonOrEmpty(this.opts.botsJsonPath);
|
|
909
|
+
if (configuredBots.some((bot) => (bot?.activationDeactivating !== undefined
|
|
910
|
+
|| bot?.activationStarting !== undefined
|
|
911
|
+
|| bot?.activationCommitted !== undefined))) {
|
|
912
|
+
return { ok: false, error: 'permission_recovery_state_unavailable' };
|
|
913
|
+
}
|
|
914
|
+
const candidates = configuredBots.flatMap((bot, index) => {
|
|
915
|
+
if (!bot
|
|
916
|
+
|| typeof bot !== 'object'
|
|
917
|
+
|| bot.cliId !== 'traex'
|
|
918
|
+
|| bot.defaultWorkingDir !== input.workingDir) {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
const appId = typeof bot.larkAppId === 'string' ? bot.larkAppId.trim() : '';
|
|
922
|
+
const appSecret = typeof bot.larkAppSecret === 'string' ? bot.larkAppSecret : '';
|
|
923
|
+
if (!appId || !appSecret || !Array.isArray(bot.allowedUsers) || !hasOwnerEntry(bot.allowedUsers))
|
|
924
|
+
return [];
|
|
925
|
+
return [{ appId, appSecret, brand: bot.brand === 'lark' ? 'lark' : 'feishu', index }];
|
|
926
|
+
});
|
|
927
|
+
if (candidates.length === 0)
|
|
928
|
+
return { ok: false, error: 'permission_recovery_target_missing' };
|
|
929
|
+
if (candidates.length !== 1)
|
|
930
|
+
return { ok: false, error: 'permission_recovery_target_ambiguous' };
|
|
931
|
+
if (!input.predecessorJobId || !input.workingDir || !input.expectedAppId || candidates[0].appId !== input.expectedAppId) {
|
|
932
|
+
return { ok: false, error: 'permission_recovery_target_invalid' };
|
|
933
|
+
}
|
|
934
|
+
const lineage = [...this.jobs.values()].filter(job => job.recoveryOfJobId === input.predecessorJobId
|
|
935
|
+
&& job.workingDir === input.workingDir
|
|
936
|
+
&& job.appId === input.expectedAppId).sort((a, b) => (a.recoveryAttempt ?? 0) - (b.recoveryAttempt ?? 0));
|
|
937
|
+
const latest = lineage.at(-1);
|
|
938
|
+
if (!input.priorRecoveryJobId) {
|
|
939
|
+
if (latest)
|
|
940
|
+
return { ok: true, job: { id: latest.id, done: Promise.resolve() } };
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
if (!latest || latest.id !== input.priorRecoveryJobId) {
|
|
944
|
+
return { ok: false, error: 'permission_recovery_target_invalid' };
|
|
945
|
+
}
|
|
946
|
+
if (!['completed', 'failed'].includes(latest.status)) {
|
|
947
|
+
return { ok: true, job: { id: latest.id, done: Promise.resolve() } };
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const target = candidates[0];
|
|
951
|
+
const id = `botperm_${Math.random().toString(36).slice(2)}_${this.now().toString(36)}`;
|
|
952
|
+
const createdAt = this.now();
|
|
953
|
+
this.jobs.set(id, {
|
|
954
|
+
id,
|
|
955
|
+
status: 'starting',
|
|
956
|
+
createdAt,
|
|
957
|
+
updatedAt: createdAt,
|
|
958
|
+
appId: target.appId,
|
|
959
|
+
brand: target.brand,
|
|
960
|
+
cliId: 'traex',
|
|
961
|
+
workingDir: input.workingDir,
|
|
962
|
+
registrationMode: 'compat',
|
|
963
|
+
recoveryOfJobId: input.predecessorJobId,
|
|
964
|
+
recoveryAttempt: (latest?.recoveryAttempt ?? 0) + 1,
|
|
965
|
+
...(input.requireCriticalScopesBeforeActivation === true
|
|
966
|
+
? { criticalScopeActivationRequired: true }
|
|
967
|
+
: {}),
|
|
968
|
+
...(latest ? { previousRecoveryJobId: latest.id } : {}),
|
|
969
|
+
});
|
|
970
|
+
try {
|
|
971
|
+
this.persistPermissionRecoveryJobs();
|
|
972
|
+
}
|
|
973
|
+
catch {
|
|
974
|
+
this.jobs.delete(id);
|
|
975
|
+
this.permissionRecoveryStateError = 'permission recovery intent could not be persisted';
|
|
976
|
+
return { ok: false, error: 'permission_recovery_state_unavailable' };
|
|
977
|
+
}
|
|
978
|
+
const done = this.runPermissionRecovery(id, target.appId, target.appSecret, target.brand, target.index, input.requireCriticalScopesBeforeActivation === true).catch(err => {
|
|
979
|
+
this.patch(id, {
|
|
980
|
+
status: 'failed',
|
|
981
|
+
error: 'permission_recovery_failed',
|
|
982
|
+
message: err instanceof Error ? err.message : String(err),
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
return { ok: true, job: { id, done } };
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Finish only the exact activation-pending tail of an already-scanned
|
|
989
|
+
* managed initial/recovery job. This never opens SSO, creates an App, or
|
|
990
|
+
* issues another owner QR. botmux itself re-reads stable scopes, removes
|
|
991
|
+
* activationPending, and requires an idempotent exact-daemon start ACK
|
|
992
|
+
* before returning success.
|
|
993
|
+
*/
|
|
994
|
+
async completeScopePropagation(input) {
|
|
995
|
+
await this.activationStartupReconciliation;
|
|
996
|
+
if (this.permissionRecoveryStateError) {
|
|
997
|
+
return { ok: false, error: 'permission_recovery_state_unavailable' };
|
|
998
|
+
}
|
|
999
|
+
const job = this.jobs.get(input.jobId);
|
|
1000
|
+
if (!job)
|
|
1001
|
+
return { ok: false, error: 'permission_recovery_target_missing' };
|
|
1002
|
+
const exactTarget = (job.workingDir === input.workingDir
|
|
1003
|
+
&& job.appId === input.expectedAppId
|
|
1004
|
+
&& job.criticalScopeActivationRequired === true);
|
|
1005
|
+
if (!exactTarget) {
|
|
1006
|
+
return { ok: false, error: 'permission_recovery_target_invalid' };
|
|
1007
|
+
}
|
|
1008
|
+
if (job.status === 'completed'
|
|
1009
|
+
&& job.activationPending !== true
|
|
1010
|
+
&& job.liveStarted === true
|
|
1011
|
+
&& job.permission?.ok === true
|
|
1012
|
+
&& job.permission.eventMode === 4
|
|
1013
|
+
&& job.permission.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
|
|
1014
|
+
&& typeof job.permission.versionId === 'string'
|
|
1015
|
+
&& job.permission.versionId.trim().length > 0) {
|
|
1016
|
+
return { ok: true };
|
|
1017
|
+
}
|
|
1018
|
+
const exactAck = (job.permission?.eventMode === 4
|
|
1019
|
+
&& job.permission.verifiedEventCount === MANAGED_VERIFIED_EVENT_COUNT
|
|
1020
|
+
&& typeof job.permission.versionId === 'string'
|
|
1021
|
+
&& job.permission.versionId.trim().length > 0);
|
|
1022
|
+
const initialPropagationPending = (job.status === 'completed'
|
|
1023
|
+
&& job.activationPending === true
|
|
1024
|
+
&& job.liveStarted !== true
|
|
1025
|
+
&& job.permission?.ok === true
|
|
1026
|
+
&& job.activationCommitting !== true);
|
|
1027
|
+
const recoveryPropagationPending = (job.status === 'failed'
|
|
1028
|
+
&& job.error === 'permission_recovery_failed'
|
|
1029
|
+
&& job.permission?.ok === false
|
|
1030
|
+
&& job.permission.reason === 'scope_mapping_failed');
|
|
1031
|
+
if (job.platformQrScanConfirmedAt === undefined
|
|
1032
|
+
|| !exactAck
|
|
1033
|
+
|| (!initialPropagationPending && !recoveryPropagationPending)) {
|
|
1034
|
+
return { ok: false, error: 'permission_recovery_target_invalid' };
|
|
1035
|
+
}
|
|
1036
|
+
const existingFlight = this.scopePropagationFlights.get(job.id);
|
|
1037
|
+
if (existingFlight)
|
|
1038
|
+
return existingFlight;
|
|
1039
|
+
const flight = this.finishScopePropagation(job, input);
|
|
1040
|
+
this.scopePropagationFlights.set(job.id, flight);
|
|
1041
|
+
try {
|
|
1042
|
+
return await flight;
|
|
1043
|
+
}
|
|
1044
|
+
finally {
|
|
1045
|
+
if (this.scopePropagationFlights.get(job.id) === flight) {
|
|
1046
|
+
this.scopePropagationFlights.delete(job.id);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async finishScopePropagation(job, input) {
|
|
1051
|
+
const managedPermission = job.permission;
|
|
1052
|
+
if (!managedPermission) {
|
|
1053
|
+
return { ok: false, error: 'permission_recovery_target_invalid' };
|
|
1054
|
+
}
|
|
1055
|
+
const candidates = readBotsJsonOrEmpty(this.opts.botsJsonPath).flatMap((bot, index) => {
|
|
1056
|
+
if (!bot
|
|
1057
|
+
|| typeof bot !== 'object'
|
|
1058
|
+
|| bot.cliId !== 'traex'
|
|
1059
|
+
|| bot.defaultWorkingDir !== input.workingDir
|
|
1060
|
+
|| bot.larkAppId !== input.expectedAppId
|
|
1061
|
+
|| typeof bot.larkAppSecret !== 'string'
|
|
1062
|
+
|| !bot.larkAppSecret
|
|
1063
|
+
|| !Array.isArray(bot.allowedUsers)
|
|
1064
|
+
|| !hasOwnerEntry(bot.allowedUsers)) {
|
|
1065
|
+
return [];
|
|
1066
|
+
}
|
|
1067
|
+
return [{
|
|
1068
|
+
index,
|
|
1069
|
+
appSecret: bot.larkAppSecret,
|
|
1070
|
+
brand: bot.brand === 'lark' ? 'lark' : 'feishu',
|
|
1071
|
+
}];
|
|
1072
|
+
});
|
|
1073
|
+
if (candidates.length === 0) {
|
|
1074
|
+
return { ok: false, error: 'permission_recovery_target_missing' };
|
|
1075
|
+
}
|
|
1076
|
+
if (candidates.length !== 1) {
|
|
1077
|
+
return { ok: false, error: 'permission_recovery_target_ambiguous' };
|
|
1078
|
+
}
|
|
1079
|
+
const target = candidates[0];
|
|
1080
|
+
const stable = await this.stableCriticalScopeReadback(job.id, input.expectedAppId, target.appSecret, target.brand);
|
|
1081
|
+
if (!stable.ready) {
|
|
1082
|
+
return { ok: false, error: 'permission_recovery_scopes_pending' };
|
|
1083
|
+
}
|
|
1084
|
+
const permissionAck = {
|
|
1085
|
+
ok: true,
|
|
1086
|
+
sessionFile: 'permission-recovery-ledger',
|
|
1087
|
+
sessionSource: 'qr_login',
|
|
1088
|
+
cookieCount: 0,
|
|
1089
|
+
scopeCount: 9,
|
|
1090
|
+
skippedScopeCount: 0,
|
|
1091
|
+
subscribedEventCount: MANAGED_VERIFIED_EVENT_COUNT,
|
|
1092
|
+
missingVcEvents: [],
|
|
1093
|
+
eventModeReady: true,
|
|
1094
|
+
eventMode: managedPermission.eventMode,
|
|
1095
|
+
verifiedEventCount: managedPermission.verifiedEventCount,
|
|
1096
|
+
versionId: managedPermission.versionId,
|
|
1097
|
+
};
|
|
1098
|
+
this.finalizePermissions(job.id, input.expectedAppId, target.brand, target.index, permissionAck, 'completed');
|
|
1099
|
+
try {
|
|
1100
|
+
await this.activateRecoveredBot(job.id, input.expectedAppId, target.index, true);
|
|
1101
|
+
}
|
|
1102
|
+
catch {
|
|
1103
|
+
return { ok: false, error: 'permission_recovery_activation_failed' };
|
|
1104
|
+
}
|
|
1105
|
+
this.patch(job.id, {
|
|
1106
|
+
error: undefined,
|
|
1107
|
+
message: undefined,
|
|
1108
|
+
remainingSteps: undefined,
|
|
1109
|
+
});
|
|
1110
|
+
return { ok: true };
|
|
1111
|
+
}
|
|
169
1112
|
get(id) {
|
|
170
1113
|
const job = this.jobs.get(id);
|
|
171
1114
|
return job ? { ...job } : undefined;
|
|
@@ -183,7 +1126,23 @@ export class BotOnboardingManager {
|
|
|
183
1126
|
const current = this.jobs.get(id);
|
|
184
1127
|
if (!current)
|
|
185
1128
|
return;
|
|
186
|
-
|
|
1129
|
+
const next = { ...current, ...patch, updatedAt: this.now() };
|
|
1130
|
+
this.jobs.set(id, next);
|
|
1131
|
+
if (current.recoveryOfJobId
|
|
1132
|
+
|| next.recoveryOfJobId
|
|
1133
|
+
|| managedActivationStateForSnapshot(current) !== undefined
|
|
1134
|
+
|| managedActivationStateForSnapshot(next) !== undefined) {
|
|
1135
|
+
this.savePermissionRecoveryJobsBestEffort();
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
confirmPlatformQrScan(id, confirmedAt) {
|
|
1139
|
+
const current = this.jobs.get(id);
|
|
1140
|
+
if (!current || current.platformQrScanConfirmedAt !== undefined)
|
|
1141
|
+
return;
|
|
1142
|
+
if (!Number.isInteger(confirmedAt) || confirmedAt <= 0) {
|
|
1143
|
+
throw new Error('platform_qr_scan_confirmation_invalid');
|
|
1144
|
+
}
|
|
1145
|
+
this.patch(id, { platformQrScanConfirmedAt: confirmedAt });
|
|
187
1146
|
}
|
|
188
1147
|
async run(id, input = {}) {
|
|
189
1148
|
// Freeze the resolved name before any asynchronous work. Later bot list
|
|
@@ -191,6 +1150,9 @@ export class BotOnboardingManager {
|
|
|
191
1150
|
const appName = resolveSetupAppName(input.appName, readBotsJsonOrEmpty(this.opts.botsJsonPath).length);
|
|
192
1151
|
this.patch(id, {
|
|
193
1152
|
registrationMode: input.registrationMode ?? 'web',
|
|
1153
|
+
...(input.requireCriticalScopesBeforeActivation
|
|
1154
|
+
? { criticalScopeActivationRequired: true }
|
|
1155
|
+
: {}),
|
|
194
1156
|
// SDK/Lark compatibility mode cannot apply a custom application name, so
|
|
195
1157
|
// do not claim that the resolved Feishu name was used.
|
|
196
1158
|
...(input.registrationMode === 'compat' ? {} : { appName }),
|
|
@@ -291,7 +1253,18 @@ export class BotOnboardingManager {
|
|
|
291
1253
|
cliId,
|
|
292
1254
|
workingDir,
|
|
293
1255
|
registrationMode: input.registrationMode ?? 'web',
|
|
1256
|
+
requireVerifiedEvents: input.requireCriticalScopesBeforeActivation === true,
|
|
294
1257
|
});
|
|
1258
|
+
// Every managed initial App first reaches one durable activation-pending
|
|
1259
|
+
// snapshot. Even when scopes are immediately stable, starting here would
|
|
1260
|
+
// leave a crash window between the exact PM2 ACK and persistence of the
|
|
1261
|
+
// owner-session event/version ACK. The Web helper always completes this
|
|
1262
|
+
// same job through the singleflight scope-propagation endpoint.
|
|
1263
|
+
const activationPending = input.requireCriticalScopesBeforeActivation === true;
|
|
1264
|
+
if (activationPending) {
|
|
1265
|
+
bot.activationPending = true;
|
|
1266
|
+
this.patch(id, { activationPending: true });
|
|
1267
|
+
}
|
|
295
1268
|
// 关键顺序:先确认 owner, 再决定是否落盘 + 终态。completed 必须意味着「bots.json
|
|
296
1269
|
// 里这个 bot 带着至少一个 owner」, 绝不产出空 allowedUsers 的可启动 bot。
|
|
297
1270
|
let ownerEntry;
|
|
@@ -310,7 +1283,9 @@ export class BotOnboardingManager {
|
|
|
310
1283
|
}
|
|
311
1284
|
if (ownerEntry) {
|
|
312
1285
|
const addedBotIndex = this.persistBot({ ...bot, allowedUsers: [ownerEntry] });
|
|
313
|
-
|
|
1286
|
+
if (!activationPending) {
|
|
1287
|
+
await this.runLiveStart(id, result.appId);
|
|
1288
|
+
}
|
|
314
1289
|
this.finalizePermissions(id, result.appId, result.brand, addedBotIndex, auto, 'completed');
|
|
315
1290
|
}
|
|
316
1291
|
else {
|
|
@@ -392,8 +1367,14 @@ export class BotOnboardingManager {
|
|
|
392
1367
|
// 校验通过才落盘:此刻 bot 第一次进入 bots.json, 且带着非空 allowedUsers。
|
|
393
1368
|
const addedBotIndex = this.persistBot({ ...pending, allowedUsers: entries });
|
|
394
1369
|
this.pendingBots.delete(id);
|
|
395
|
-
|
|
1370
|
+
if (pending.activationPending === true) {
|
|
1371
|
+
this.patch(id, { activationPending: true });
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1374
|
+
await this.runLiveStart(id, appId);
|
|
1375
|
+
}
|
|
396
1376
|
this.patch(id, { status: 'completed', addedBotIndex });
|
|
1377
|
+
this.requireDurableManagedInitialJobs();
|
|
397
1378
|
this.savePendingJobs();
|
|
398
1379
|
return { ok: true };
|
|
399
1380
|
}
|
|
@@ -409,8 +1390,27 @@ export class BotOnboardingManager {
|
|
|
409
1390
|
cliId: meta.cliId,
|
|
410
1391
|
workingDir: meta.workingDir,
|
|
411
1392
|
});
|
|
1393
|
+
const callbacks = {
|
|
1394
|
+
onQrCode: (info) => {
|
|
1395
|
+
this.patch(id, {
|
|
1396
|
+
status: 'waiting_for_platform_scan',
|
|
1397
|
+
platformQrDataUrl: this.renderQrDataUrl(info.qrPayload),
|
|
1398
|
+
});
|
|
1399
|
+
},
|
|
1400
|
+
onQrScanConfirmed: (info) => {
|
|
1401
|
+
this.confirmPlatformQrScan(id, info.confirmedAt);
|
|
1402
|
+
},
|
|
1403
|
+
onStatus: (msg) => {
|
|
1404
|
+
// onStatus follows onQrCode while polling; keep the QR visible.
|
|
1405
|
+
this.patch(id, { permissionStatusMsg: msg });
|
|
1406
|
+
},
|
|
1407
|
+
};
|
|
412
1408
|
try {
|
|
413
|
-
|
|
1409
|
+
if (meta.registrationMode === 'compat') {
|
|
1410
|
+
const sessionFilePath = join(dirname(this.opts.botsJsonPath), 'onboarding-sessions', `${id}.json`);
|
|
1411
|
+
return await this.runOwnerPermissionAutomation(id, appId, brand, callbacks, sessionFilePath, meta.requireVerifiedEvents);
|
|
1412
|
+
}
|
|
1413
|
+
const first = await this.automateOpenPlatform({
|
|
414
1414
|
appId,
|
|
415
1415
|
brand,
|
|
416
1416
|
// The Feishu primary path must never surprise the user with a second
|
|
@@ -418,19 +1418,10 @@ export class BotOnboardingManager {
|
|
|
418
1418
|
// manual recovery. Only explicit compatibility mode may scan again.
|
|
419
1419
|
disableQrLogin: meta.registrationMode === 'web',
|
|
420
1420
|
disableBytedcliFallback: meta.registrationMode === 'web',
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
status: 'waiting_for_platform_scan',
|
|
424
|
-
platformQrDataUrl: this.renderQrDataUrl(info.qrPayload),
|
|
425
|
-
});
|
|
426
|
-
},
|
|
427
|
-
onStatus: msg => {
|
|
428
|
-
// 只更新进度文案, 不动 status——onStatus 在 onQrCode 之后的轮询里就会
|
|
429
|
-
// 触发 ('等待飞书扫码'), 若把 status 拨回 configuring_permissions 会瞬间
|
|
430
|
-
// 把刚弹出的第二个二维码 (waiting_for_platform_scan) 盖掉.
|
|
431
|
-
this.patch(id, { permissionStatusMsg: msg });
|
|
432
|
-
},
|
|
1421
|
+
requireVerifiedEvents: meta.requireVerifiedEvents,
|
|
1422
|
+
...callbacks,
|
|
433
1423
|
});
|
|
1424
|
+
return first;
|
|
434
1425
|
}
|
|
435
1426
|
catch (err) {
|
|
436
1427
|
// automation 不应抛 (内部已结构化返回), 兜底当作失败 → 手动步骤.
|
|
@@ -441,11 +1432,88 @@ export class BotOnboardingManager {
|
|
|
441
1432
|
};
|
|
442
1433
|
}
|
|
443
1434
|
}
|
|
1435
|
+
async runPermissionRecovery(id, appId, appSecret, brand, addedBotIndex, requireCriticalScopesBeforeActivation) {
|
|
1436
|
+
if (requireCriticalScopesBeforeActivation) {
|
|
1437
|
+
this.holdRecoveryActivation(id, appId, addedBotIndex);
|
|
1438
|
+
const stopped = await this.runLiveStop(id, appId);
|
|
1439
|
+
if (!stopped.attempted || stopped.ok !== true) {
|
|
1440
|
+
throw new Error(`permission_recovery_deactivation_not_acknowledged${stopped.message ? `: ${stopped.message}` : ''}`);
|
|
1441
|
+
}
|
|
1442
|
+
this.markManagedDeactivated(id, appId, addedBotIndex);
|
|
1443
|
+
}
|
|
1444
|
+
this.patch(id, { status: 'configuring_permissions' });
|
|
1445
|
+
const sessionFilePath = join(dirname(this.opts.botsJsonPath), 'onboarding-sessions', `${id}.json`);
|
|
1446
|
+
const auto = await this.runOwnerPermissionAutomation(id, appId, brand, {
|
|
1447
|
+
onQrCode: (info) => {
|
|
1448
|
+
this.patch(id, {
|
|
1449
|
+
status: 'waiting_for_platform_scan',
|
|
1450
|
+
platformQrDataUrl: this.renderQrDataUrl(info.qrPayload),
|
|
1451
|
+
});
|
|
1452
|
+
},
|
|
1453
|
+
onQrScanConfirmed: (info) => {
|
|
1454
|
+
this.confirmPlatformQrScan(id, info.confirmedAt);
|
|
1455
|
+
},
|
|
1456
|
+
onStatus: (msg) => this.patch(id, { permissionStatusMsg: msg }),
|
|
1457
|
+
}, sessionFilePath, requireCriticalScopesBeforeActivation);
|
|
1458
|
+
if (!auto.ok) {
|
|
1459
|
+
this.finalizePermissions(id, appId, brand, addedBotIndex, auto, 'failed', 'permission_recovery_failed');
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (requireCriticalScopesBeforeActivation && !hasExactManagedAutomationAck(auto)) {
|
|
1463
|
+
this.finalizePermissions(id, appId, brand, addedBotIndex, {
|
|
1464
|
+
ok: false,
|
|
1465
|
+
reason: 'event_verification_failed',
|
|
1466
|
+
message: '受管权限恢复缺少同一 owner 会话的事件与版本精确 ACK',
|
|
1467
|
+
}, 'failed', 'permission_recovery_failed');
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (requireCriticalScopesBeforeActivation
|
|
1471
|
+
&& this.jobs.get(id)?.platformQrScanConfirmedAt === undefined) {
|
|
1472
|
+
throw new Error('platform_qr_scan_not_confirmed');
|
|
1473
|
+
}
|
|
1474
|
+
const stableScopeReadback = await this.stableCriticalScopeReadback(id, appId, appSecret, brand);
|
|
1475
|
+
const scopeReadback = stableScopeReadback.readback;
|
|
1476
|
+
if (!stableScopeReadback.ready) {
|
|
1477
|
+
const failed = {
|
|
1478
|
+
ok: false,
|
|
1479
|
+
reason: scopeReadback.ok ? 'scope_mapping_failed' : 'api_error',
|
|
1480
|
+
message: scopeReadback.ok
|
|
1481
|
+
? scopeReadback.missingCritical.length > 0
|
|
1482
|
+
? `关键权限回读仍缺失: ${scopeReadback.missingCritical.map(scope => scope.name).join(', ')}`
|
|
1483
|
+
: `9 项核心权限尚未连续稳定生效(需要连续 ${this.criticalScopeStableReads} 次)`
|
|
1484
|
+
: `关键权限回读失败: ${scopeReadback.message}`,
|
|
1485
|
+
eventMode: auto.eventMode,
|
|
1486
|
+
verifiedEventCount: auto.verifiedEventCount,
|
|
1487
|
+
versionId: auto.versionId,
|
|
1488
|
+
};
|
|
1489
|
+
this.finalizePermissions(id, appId, brand, addedBotIndex, failed, 'failed', 'permission_recovery_failed');
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
this.finalizePermissions(id, appId, brand, addedBotIndex, auto, 'completed');
|
|
1493
|
+
await this.activateRecoveredBot(id, appId, addedBotIndex);
|
|
1494
|
+
}
|
|
1495
|
+
async runOwnerPermissionAutomation(id, appId, brand, callbacks, sessionFilePath, requireVerifiedEvents) {
|
|
1496
|
+
try {
|
|
1497
|
+
return await this.automateOpenPlatform({
|
|
1498
|
+
appId,
|
|
1499
|
+
brand,
|
|
1500
|
+
sessionFilePath,
|
|
1501
|
+
forceQrLogin: true,
|
|
1502
|
+
disableQrLogin: false,
|
|
1503
|
+
disableBytedcliFallback: true,
|
|
1504
|
+
requireVerifiedEvents,
|
|
1505
|
+
...callbacks,
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
finally {
|
|
1509
|
+
rmSync(sessionFilePath, { force: true });
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
444
1512
|
/**
|
|
445
1513
|
* 统一落终态:completed (已落盘 + 有 owner) 或 needs_owner (尚未落盘、待用户手动填)。
|
|
446
1514
|
* needs_owner 时 addedBotIndex 为 undefined——bot 还没进 bots.json, 没有行号。
|
|
447
1515
|
*/
|
|
448
|
-
finalizePermissions(id, appId, brand, addedBotIndex, auto, status) {
|
|
1516
|
+
finalizePermissions(id, appId, brand, addedBotIndex, auto, status, error) {
|
|
449
1517
|
const permission = auto.ok
|
|
450
1518
|
? {
|
|
451
1519
|
ok: true,
|
|
@@ -453,15 +1521,26 @@ export class BotOnboardingManager {
|
|
|
453
1521
|
skippedScopeCount: auto.skippedScopeCount,
|
|
454
1522
|
versionId: auto.versionId,
|
|
455
1523
|
scopeWarning: auto.scopeWarning,
|
|
1524
|
+
eventMode: auto.eventMode,
|
|
1525
|
+
verifiedEventCount: auto.verifiedEventCount,
|
|
456
1526
|
}
|
|
457
|
-
: {
|
|
1527
|
+
: {
|
|
1528
|
+
ok: false,
|
|
1529
|
+
reason: auto.reason,
|
|
1530
|
+
message: auto.message,
|
|
1531
|
+
eventMode: auto.eventMode,
|
|
1532
|
+
verifiedEventCount: auto.verifiedEventCount,
|
|
1533
|
+
versionId: auto.versionId,
|
|
1534
|
+
};
|
|
458
1535
|
this.patch(id, {
|
|
459
1536
|
status,
|
|
460
1537
|
...(addedBotIndex !== undefined ? { addedBotIndex } : {}),
|
|
461
1538
|
platformQrDataUrl: undefined,
|
|
462
1539
|
permission,
|
|
1540
|
+
...(error ? { error } : {}),
|
|
463
1541
|
...(auto.ok ? {} : { remainingSteps: buildRemainingSteps(appId, brand) }),
|
|
464
1542
|
});
|
|
1543
|
+
this.requireDurableManagedInitialJobs();
|
|
465
1544
|
}
|
|
466
1545
|
}
|
|
467
1546
|
/**
|