botmux 2.47.0 → 2.47.2
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 -5
- package/README.md +10 -5
- package/dist/adapters/adopt-route.d.ts +63 -0
- package/dist/adapters/adopt-route.d.ts.map +1 -0
- package/dist/adapters/adopt-route.js +195 -0
- package/dist/adapters/adopt-route.js.map +1 -0
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +11 -0
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +11 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +17 -1
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +36 -9
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.d.ts.map +1 -1
- package/dist/adapters/cli/coco.js +26 -1
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts +4 -0
- package/dist/adapters/cli/codex-app.d.ts.map +1 -0
- package/dist/adapters/cli/codex-app.js +72 -0
- package/dist/adapters/cli/codex-app.js.map +1 -0
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +34 -17
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/cursor.d.ts.map +1 -1
- package/dist/adapters/cli/cursor.js +58 -12
- package/dist/adapters/cli/cursor.js.map +1 -1
- package/dist/adapters/cli/gemini.d.ts.map +1 -1
- package/dist/adapters/cli/gemini.js +5 -1
- package/dist/adapters/cli/gemini.js.map +1 -1
- package/dist/adapters/cli/hermes.d.ts +4 -0
- package/dist/adapters/cli/hermes.d.ts.map +1 -0
- package/dist/adapters/cli/hermes.js +40 -0
- package/dist/adapters/cli/hermes.js.map +1 -0
- package/dist/adapters/cli/mira.d.ts +4 -0
- package/dist/adapters/cli/mira.d.ts.map +1 -0
- package/dist/adapters/cli/mira.js +67 -0
- package/dist/adapters/cli/mira.js.map +1 -0
- package/dist/adapters/cli/mtr.d.ts +5 -0
- package/dist/adapters/cli/mtr.d.ts.map +1 -0
- package/dist/adapters/cli/mtr.js +62 -0
- package/dist/adapters/cli/mtr.js.map +1 -0
- package/dist/adapters/cli/opencode.d.ts.map +1 -1
- package/dist/adapters/cli/opencode.js +19 -1
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +5 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +22 -2
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +1 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +2 -1
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +35 -2
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/adapters/hook-command.d.ts +18 -0
- package/dist/adapters/hook-command.d.ts.map +1 -0
- package/dist/adapters/hook-command.js +38 -0
- package/dist/adapters/hook-command.js.map +1 -0
- package/dist/adapters/hook-installer.d.ts +14 -0
- package/dist/adapters/hook-installer.d.ts.map +1 -0
- package/dist/adapters/hook-installer.js +192 -0
- package/dist/adapters/hook-installer.js.map +1 -0
- package/dist/bot-registry.d.ts +59 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +67 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/bots-list-output.d.ts +8 -0
- package/dist/cli/bots-list-output.d.ts.map +1 -1
- package/dist/cli/bots-list-output.js +9 -0
- package/dist/cli/bots-list-output.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +603 -106
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.d.ts +3 -0
- package/dist/codex-app-runner.d.ts.map +1 -0
- package/dist/codex-app-runner.js +512 -0
- package/dist/codex-app-runner.js.map +1 -0
- package/dist/config.d.ts +11 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -4
- package/dist/config.js.map +1 -1
- package/dist/core/ask-api.d.ts +47 -0
- package/dist/core/ask-api.d.ts.map +1 -0
- package/dist/core/ask-api.js +139 -0
- package/dist/core/ask-api.js.map +1 -0
- package/dist/core/ask-args.d.ts +53 -0
- package/dist/core/ask-args.d.ts.map +1 -0
- package/dist/core/ask-args.js +122 -0
- package/dist/core/ask-args.js.map +1 -0
- package/dist/core/ask-broker.d.ts +98 -0
- package/dist/core/ask-broker.d.ts.map +1 -0
- package/dist/core/ask-broker.js +329 -0
- package/dist/core/ask-broker.js.map +1 -0
- package/dist/core/ask-hook/claude-code.d.ts +50 -0
- package/dist/core/ask-hook/claude-code.d.ts.map +1 -0
- package/dist/core/ask-hook/claude-code.js +145 -0
- package/dist/core/ask-hook/claude-code.js.map +1 -0
- package/dist/core/ask-hook/codex.d.ts +43 -0
- package/dist/core/ask-hook/codex.d.ts.map +1 -0
- package/dist/core/ask-hook/codex.js +69 -0
- package/dist/core/ask-hook/codex.js.map +1 -0
- package/dist/core/ask-hook/opencode.d.ts +41 -0
- package/dist/core/ask-hook/opencode.d.ts.map +1 -0
- package/dist/core/ask-hook/opencode.js +108 -0
- package/dist/core/ask-hook/opencode.js.map +1 -0
- package/dist/core/ask-hook/registry.d.ts +3 -0
- package/dist/core/ask-hook/registry.d.ts.map +1 -0
- package/dist/core/ask-hook/registry.js +12 -0
- package/dist/core/ask-hook/registry.js.map +1 -0
- package/dist/core/ask-hook/types.d.ts +26 -0
- package/dist/core/ask-hook/types.d.ts.map +1 -0
- package/dist/core/ask-hook/types.js +2 -0
- package/dist/core/ask-hook/types.js.map +1 -0
- package/dist/core/ask-types.d.ts +146 -0
- package/dist/core/ask-types.d.ts.map +1 -0
- package/dist/core/ask-types.js +18 -0
- package/dist/core/ask-types.js.map +1 -0
- package/dist/core/command-handler.d.ts +29 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +787 -312
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +2 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +222 -2
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/role-resolver.d.ts +17 -1
- package/dist/core/role-resolver.d.ts.map +1 -1
- package/dist/core/role-resolver.js +64 -10
- package/dist/core/role-resolver.js.map +1 -1
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +19 -5
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +37 -20
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/trigger-session.d.ts +9 -0
- package/dist/core/trigger-session.d.ts.map +1 -0
- package/dist/core/trigger-session.js +158 -0
- package/dist/core/trigger-session.js.map +1 -0
- package/dist/core/types.d.ts +5 -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 +141 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +543 -24
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +224 -60
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/auth.d.ts +6 -1
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +9 -1
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/connector-api.d.ts +3 -0
- package/dist/dashboard/connector-api.d.ts.map +1 -0
- package/dist/dashboard/connector-api.js +351 -0
- package/dist/dashboard/connector-api.js.map +1 -0
- package/dist/dashboard/federated-group-core.d.ts +54 -0
- package/dist/dashboard/federated-group-core.d.ts.map +1 -0
- package/dist/dashboard/federated-group-core.js +165 -0
- package/dist/dashboard/federated-group-core.js.map +1 -0
- package/dist/dashboard/federation-api.d.ts +42 -0
- package/dist/dashboard/federation-api.d.ts.map +1 -0
- package/dist/dashboard/federation-api.js +408 -0
- package/dist/dashboard/federation-api.js.map +1 -0
- package/dist/dashboard/federation-spoke-api.d.ts +76 -0
- package/dist/dashboard/federation-spoke-api.d.ts.map +1 -0
- package/dist/dashboard/federation-spoke-api.js +618 -0
- package/dist/dashboard/federation-spoke-api.js.map +1 -0
- package/dist/dashboard/team-group.d.ts +18 -0
- package/dist/dashboard/team-group.d.ts.map +1 -0
- package/dist/dashboard/team-group.js +7 -0
- package/dist/dashboard/team-group.js.map +1 -0
- package/dist/dashboard/trigger-api.d.ts +13 -0
- package/dist/dashboard/trigger-api.d.ts.map +1 -0
- package/dist/dashboard/trigger-api.js +77 -0
- package/dist/dashboard/trigger-api.js.map +1 -0
- package/dist/dashboard/web/app.js +8 -0
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +205 -21
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/connectors.d.ts +2 -0
- package/dist/dashboard/web/connectors.d.ts.map +1 -0
- package/dist/dashboard/web/connectors.js +187 -0
- package/dist/dashboard/web/connectors.js.map +1 -0
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +43 -5
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +4 -0
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/team-federation.d.ts +3 -0
- package/dist/dashboard/web/team-federation.d.ts.map +1 -0
- package/dist/dashboard/web/team-federation.js +487 -0
- package/dist/dashboard/web/team-federation.js.map +1 -0
- package/dist/dashboard/web/workflows.js +3 -3
- package/dist/dashboard/web/workflows.js.map +1 -1
- package/dist/dashboard/webhook-routes.d.ts +19 -0
- package/dist/dashboard/webhook-routes.d.ts.map +1 -0
- package/dist/dashboard/webhook-routes.js +321 -0
- package/dist/dashboard/webhook-routes.js.map +1 -0
- package/dist/dashboard/workflow-api.d.ts +8 -1
- package/dist/dashboard/workflow-api.d.ts.map +1 -1
- package/dist/dashboard/workflow-api.js +19 -4
- package/dist/dashboard/workflow-api.js.map +1 -1
- package/dist/dashboard-web/app.js +539 -375
- package/dist/dashboard-web/index.html +3 -1
- package/dist/dashboard-web/style.css +22 -0
- package/dist/dashboard.js +199 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +104 -11
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +104 -11
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/ask-card.d.ts +55 -0
- package/dist/im/lark/ask-card.d.ts.map +1 -0
- package/dist/im/lark/ask-card.js +328 -0
- package/dist/im/lark/ask-card.js.map +1 -0
- package/dist/im/lark/card-builder.d.ts +108 -3
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +480 -50
- 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 +241 -18
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +83 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +286 -70
- 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 +29 -4
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts +2 -1
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +3 -2
- package/dist/im/lark/grant-command.js.map +1 -1
- package/dist/im/lark/identity-cache.d.ts.map +1 -1
- package/dist/im/lark/identity-cache.js +3 -3
- package/dist/im/lark/identity-cache.js.map +1 -1
- package/dist/im/lark/md-card.d.ts +20 -2
- package/dist/im/lark/md-card.d.ts.map +1 -1
- package/dist/im/lark/md-card.js +49 -17
- package/dist/im/lark/md-card.js.map +1 -1
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +87 -31
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/workflow-card-handler.d.ts +2 -2
- package/dist/im/lark/workflow-card-handler.d.ts.map +1 -1
- package/dist/im/lark/workflow-card-handler.js +12 -1
- package/dist/im/lark/workflow-card-handler.js.map +1 -1
- package/dist/im/lark/workflow-progress-card.d.ts.map +1 -1
- package/dist/im/lark/workflow-progress-card.js +53 -0
- package/dist/im/lark/workflow-progress-card.js.map +1 -1
- package/dist/mira-output.d.ts +3 -0
- package/dist/mira-output.d.ts.map +1 -0
- package/dist/mira-output.js +136 -0
- package/dist/mira-output.js.map +1 -0
- package/dist/mira-runner.d.ts +3 -0
- package/dist/mira-runner.d.ts.map +1 -0
- package/dist/mira-runner.js +534 -0
- package/dist/mira-runner.js.map +1 -0
- package/dist/services/bot-owner-store.d.ts +28 -0
- package/dist/services/bot-owner-store.d.ts.map +1 -0
- package/dist/services/bot-owner-store.js +82 -0
- package/dist/services/bot-owner-store.js.map +1 -0
- package/dist/services/bot-profile-store.d.ts +16 -0
- package/dist/services/bot-profile-store.d.ts.map +1 -0
- package/dist/services/bot-profile-store.js +98 -0
- package/dist/services/bot-profile-store.js.map +1 -0
- package/dist/services/brand-store.d.ts +15 -0
- package/dist/services/brand-store.d.ts.map +1 -0
- package/dist/services/brand-store.js +47 -0
- package/dist/services/brand-store.js.map +1 -0
- package/dist/services/card-prefs-store.d.ts +20 -0
- package/dist/services/card-prefs-store.d.ts.map +1 -0
- package/dist/services/card-prefs-store.js +82 -0
- package/dist/services/card-prefs-store.js.map +1 -0
- package/dist/services/codex-bridge-queue.d.ts +1 -0
- package/dist/services/codex-bridge-queue.d.ts.map +1 -1
- package/dist/services/codex-bridge-queue.js +23 -0
- package/dist/services/codex-bridge-queue.js.map +1 -1
- package/dist/services/codex-transcript.d.ts +1 -0
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/services/connector-store.d.ts +58 -0
- package/dist/services/connector-store.d.ts.map +1 -0
- package/dist/services/connector-store.js +79 -0
- package/dist/services/connector-store.js.map +1 -0
- package/dist/services/deployment-identity.d.ts +22 -0
- package/dist/services/deployment-identity.d.ts.map +1 -0
- package/dist/services/deployment-identity.js +67 -0
- package/dist/services/deployment-identity.js.map +1 -0
- package/dist/services/federation-membership-store.d.ts +23 -0
- package/dist/services/federation-membership-store.d.ts.map +1 -0
- package/dist/services/federation-membership-store.js +66 -0
- package/dist/services/federation-membership-store.js.map +1 -0
- package/dist/services/federation-roster.d.ts +54 -0
- package/dist/services/federation-roster.d.ts.map +1 -0
- package/dist/services/federation-roster.js +51 -0
- package/dist/services/federation-roster.js.map +1 -0
- package/dist/services/federation-store.d.ts +76 -0
- package/dist/services/federation-store.d.ts.map +1 -0
- package/dist/services/federation-store.js +133 -0
- package/dist/services/federation-store.js.map +1 -0
- package/dist/services/grant-store.d.ts +12 -2
- package/dist/services/grant-store.d.ts.map +1 -1
- package/dist/services/grant-store.js +51 -4
- package/dist/services/grant-store.js.map +1 -1
- package/dist/services/group-creator.d.ts +10 -0
- package/dist/services/group-creator.d.ts.map +1 -1
- package/dist/services/group-creator.js +26 -1
- package/dist/services/group-creator.js.map +1 -1
- package/dist/services/groups-store.d.ts +30 -0
- package/dist/services/groups-store.d.ts.map +1 -1
- package/dist/services/groups-store.js +85 -12
- package/dist/services/groups-store.js.map +1 -1
- package/dist/services/hermes-transcript.d.ts +7 -0
- package/dist/services/hermes-transcript.d.ts.map +1 -0
- package/dist/services/hermes-transcript.js +117 -0
- package/dist/services/hermes-transcript.js.map +1 -0
- package/dist/services/invite-store.d.ts +28 -0
- package/dist/services/invite-store.d.ts.map +1 -0
- package/dist/services/invite-store.js +85 -0
- package/dist/services/invite-store.js.map +1 -0
- package/dist/services/pairing-store.d.ts +47 -0
- package/dist/services/pairing-store.d.ts.map +1 -0
- package/dist/services/pairing-store.js +132 -0
- package/dist/services/pairing-store.js.map +1 -0
- package/dist/services/project-scanner.d.ts +10 -0
- package/dist/services/project-scanner.d.ts.map +1 -1
- package/dist/services/project-scanner.js +11 -0
- package/dist/services/project-scanner.js.map +1 -1
- package/dist/services/relay-picker.d.ts +22 -0
- package/dist/services/relay-picker.d.ts.map +1 -0
- package/dist/services/relay-picker.js +62 -0
- package/dist/services/relay-picker.js.map +1 -0
- package/dist/services/send-policy.d.ts +55 -0
- package/dist/services/send-policy.d.ts.map +1 -0
- package/dist/services/send-policy.js +47 -0
- package/dist/services/send-policy.js.map +1 -0
- package/dist/services/session-store.js +1 -1
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/team-roster.d.ts +38 -0
- package/dist/services/team-roster.d.ts.map +1 -0
- package/dist/services/team-roster.js +82 -0
- package/dist/services/team-roster.js.map +1 -0
- package/dist/services/team-store.d.ts +54 -0
- package/dist/services/team-store.d.ts.map +1 -0
- package/dist/services/team-store.js +156 -0
- package/dist/services/team-store.js.map +1 -0
- package/dist/services/trigger-log-store.d.ts +46 -0
- package/dist/services/trigger-log-store.d.ts.map +1 -0
- package/dist/services/trigger-log-store.js +132 -0
- package/dist/services/trigger-log-store.js.map +1 -0
- package/dist/services/trigger-types.d.ts +57 -0
- package/dist/services/trigger-types.d.ts.map +1 -0
- package/dist/services/trigger-types.js +28 -0
- package/dist/services/trigger-types.js.map +1 -0
- package/dist/services/webhook-key.d.ts +16 -0
- package/dist/services/webhook-key.d.ts.map +1 -0
- package/dist/services/webhook-key.js +123 -0
- package/dist/services/webhook-key.js.map +1 -0
- package/dist/services/webhook-lifecycle-extractors.d.ts +15 -0
- package/dist/services/webhook-lifecycle-extractors.d.ts.map +1 -0
- package/dist/services/webhook-lifecycle-extractors.js +59 -0
- package/dist/services/webhook-lifecycle-extractors.js.map +1 -0
- package/dist/services/webhook-lifecycle-store.d.ts +45 -0
- package/dist/services/webhook-lifecycle-store.d.ts.map +1 -0
- package/dist/services/webhook-lifecycle-store.js +159 -0
- package/dist/services/webhook-lifecycle-store.js.map +1 -0
- package/dist/setup/bot-config-editor.d.ts +8 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +20 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-tmux.d.ts +0 -22
- package/dist/setup/ensure-tmux.d.ts.map +1 -1
- package/dist/setup/ensure-tmux.js +25 -1
- package/dist/setup/ensure-tmux.js.map +1 -1
- package/dist/setup/verify-permissions.d.ts.map +1 -1
- package/dist/setup/verify-permissions.js +15 -1
- package/dist/setup/verify-permissions.js.map +1 -1
- package/dist/skills/definitions.d.ts +2 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +178 -12
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/installer.d.ts +34 -0
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +119 -2
- package/dist/skills/installer.js.map +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/bot-routing.d.ts +50 -0
- package/dist/utils/bot-routing.d.ts.map +1 -1
- package/dist/utils/bot-routing.js +83 -0
- package/dist/utils/bot-routing.js.map +1 -1
- package/dist/utils/daemon-discovery.d.ts +11 -0
- package/dist/utils/daemon-discovery.d.ts.map +1 -0
- package/dist/utils/daemon-discovery.js +59 -0
- package/dist/utils/daemon-discovery.js.map +1 -0
- package/dist/utils/user-token.d.ts.map +1 -1
- package/dist/utils/user-token.js +0 -2
- package/dist/utils/user-token.js.map +1 -1
- package/dist/worker.js +233 -51
- package/dist/worker.js.map +1 -1
- package/dist/workflows/attempt-resume.d.ts.map +1 -1
- package/dist/workflows/attempt-resume.js +2 -2
- package/dist/workflows/attempt-resume.js.map +1 -1
- package/dist/workflows/definition.d.ts +412 -9
- package/dist/workflows/definition.d.ts.map +1 -1
- package/dist/workflows/definition.js +238 -3
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/events/payloads.d.ts +114 -11
- package/dist/workflows/events/payloads.d.ts.map +1 -1
- package/dist/workflows/events/payloads.js +46 -0
- package/dist/workflows/events/payloads.js.map +1 -1
- package/dist/workflows/events/replay.d.ts +21 -0
- package/dist/workflows/events/replay.d.ts.map +1 -1
- package/dist/workflows/events/replay.js +103 -0
- package/dist/workflows/events/replay.js.map +1 -1
- package/dist/workflows/events/schema.d.ts +1301 -606
- package/dist/workflows/events/schema.d.ts.map +1 -1
- package/dist/workflows/events/schema.js +37 -1
- package/dist/workflows/events/schema.js.map +1 -1
- package/dist/workflows/events/types.d.ts +5 -1
- package/dist/workflows/events/types.d.ts.map +1 -1
- package/dist/workflows/loader.d.ts +14 -0
- package/dist/workflows/loader.d.ts.map +1 -1
- package/dist/workflows/loader.js +27 -0
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/loop.js +58 -0
- package/dist/workflows/loop.js.map +1 -1
- package/dist/workflows/ops-projection.d.ts +58 -0
- package/dist/workflows/ops-projection.d.ts.map +1 -1
- package/dist/workflows/ops-projection.js +74 -0
- package/dist/workflows/ops-projection.js.map +1 -1
- package/dist/workflows/orchestrator.d.ts +65 -1
- package/dist/workflows/orchestrator.d.ts.map +1 -1
- package/dist/workflows/orchestrator.js +486 -74
- package/dist/workflows/orchestrator.js.map +1 -1
- package/dist/workflows/output-binding.d.ts +8 -1
- package/dist/workflows/output-binding.d.ts.map +1 -1
- package/dist/workflows/output-binding.js +75 -11
- package/dist/workflows/output-binding.js.map +1 -1
- package/dist/workflows/runtime.d.ts +1 -1
- package/dist/workflows/runtime.d.ts.map +1 -1
- package/dist/workflows/runtime.js +39 -4
- package/dist/workflows/runtime.js.map +1 -1
- package/dist/workflows/trigger-from-envelope.d.ts +13 -0
- package/dist/workflows/trigger-from-envelope.d.ts.map +1 -0
- package/dist/workflows/trigger-from-envelope.js +67 -0
- package/dist/workflows/trigger-from-envelope.js.map +1 -0
- package/dist/workflows/wait.d.ts +23 -2
- package/dist/workflows/wait.d.ts.map +1 -1
- package/dist/workflows/wait.js +39 -17
- package/dist/workflows/wait.js.map +1 -1
- package/package.json +1 -1
- package/dist/services/feishu-task-client.d.ts +0 -28
- package/dist/services/feishu-task-client.d.ts.map +0 -1
- package/dist/services/feishu-task-client.js +0 -123
- package/dist/services/feishu-task-client.js.map +0 -1
- package/dist/services/task-store.d.ts +0 -37
- package/dist/services/task-store.d.ts.map +0 -1
- package/dist/services/task-store.js +0 -115
- package/dist/services/task-store.js.map +0 -1
|
@@ -2,32 +2,41 @@
|
|
|
2
2
|
* Command handler — processes /slash commands from users.
|
|
3
3
|
* Extracted from daemon.ts for modularity.
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
5
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { join, resolve, basename } from 'node:path';
|
|
7
7
|
import { config } from '../config.js';
|
|
8
8
|
import { getBot, getAllBots, getBotOpenId } from '../bot-registry.js';
|
|
9
9
|
import * as sessionStore from '../services/session-store.js';
|
|
10
10
|
import * as scheduleStore from '../services/schedule-store.js';
|
|
11
|
-
import * as taskStore from '../services/task-store.js';
|
|
12
11
|
import * as scheduler from './scheduler.js';
|
|
13
|
-
import { scanMultipleProjects } from '../services/project-scanner.js';
|
|
12
|
+
import { scanMultipleProjects, describeProjectDir } from '../services/project-scanner.js';
|
|
14
13
|
import { buildRepoSelectCard, buildAdoptSelectCard, buildSessionClosedCard, getCliDisplayName } from '../im/lark/card-builder.js';
|
|
15
14
|
import { createCliAdapterSync } from '../adapters/cli/registry.js';
|
|
16
|
-
import { deleteMessage, sendMessage,
|
|
15
|
+
import { deleteMessage, sendMessage, listChatBotMembers, resolveUserUnionId, getChatModeStrict } from '../im/lark/client.js';
|
|
16
|
+
import { claimPairing } from '../services/pairing-store.js';
|
|
17
17
|
import { logger } from '../utils/logger.js';
|
|
18
|
-
import { killWorker, forkWorker, forkAdoptWorker, getCurrentCliVersion } from './worker-pool.js';
|
|
18
|
+
import { killWorker, forkWorker, forkAdoptWorker, getCurrentCliVersion, postFreshStreamingCard, postPrivateSnapshotCard, resolvePrivateCardAudience } from './worker-pool.js';
|
|
19
19
|
import { expandHome, getSessionWorkingDir, getProjectScanDirs, rememberLastCliInput } from './session-manager.js';
|
|
20
20
|
import { validateWorkingDir } from './working-dir.js';
|
|
21
21
|
import { discoverAdoptableSessions, validateAdoptTarget } from './session-discovery.js';
|
|
22
22
|
import { generateAuthUrl, getTokenStatus } from '../utils/user-token.js';
|
|
23
23
|
import { bindOncall, unbindOncall, getOncallStatus } from '../services/oncall-store.js';
|
|
24
24
|
import { invalidWorkingDirs } from '../utils/working-dir.js';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { writeRoleFile, deleteRoleFile, resolveRole, resolveTeamRoleFile, writeTeamRoleFile, deleteTeamRoleFile } from './role-resolver.js';
|
|
26
|
+
import { getBotCapability, setBotCapability, clearBotCapability } from '../services/bot-profile-store.js';
|
|
27
27
|
import { sessionKey, sessionAnchorId } from './types.js';
|
|
28
28
|
import { t, localeForBot } from '../i18n/index.js';
|
|
29
29
|
// ─── Exported constants ──────────────────────────────────────────────────────
|
|
30
|
-
export const DAEMON_COMMANDS = new Set(['/close', '/restart', '/status', '/help', '/cd', '/repo', '/
|
|
30
|
+
export const DAEMON_COMMANDS = new Set(['/close', '/restart', '/status', '/help', '/cd', '/repo', '/schedule', '/role', '/pair', '/login', '/adopt', '/oncall', '/group', '/g', '/relay', '/card']);
|
|
31
|
+
/**
|
|
32
|
+
* Daemon commands that act on the chat itself rather than opening a
|
|
33
|
+
* conversation. `/group` (`/g`) just creates a Lark group and replies once —
|
|
34
|
+
* no follow-up turns, no CLI worker. The new-topic spawn path normally
|
|
35
|
+
* pre-creates a sessionStore record so a command can attach state and keep
|
|
36
|
+
* card buttons routable, but for these that record is a phantom conversation
|
|
37
|
+
* that pollutes the dashboard's session list. Handle them without a session.
|
|
38
|
+
*/
|
|
39
|
+
export const SESSIONLESS_DAEMON_COMMANDS = new Set(['/group', '/g']);
|
|
31
40
|
/**
|
|
32
41
|
* Slash commands that are forwarded verbatim to the underlying CLI (e.g.
|
|
33
42
|
* Claude Code's `/compact`, `/model`, `/usage`). The daemon does NOT handle
|
|
@@ -47,6 +56,67 @@ const MULTILINE_COMMANDS = new Set(['/schedule', '/role']);
|
|
|
47
56
|
// `validateWorkingDir` now lives in ./working-dir.js (leaf module the CLI can
|
|
48
57
|
// import without the daemon graph); re-exported here for existing callers.
|
|
49
58
|
export { validateWorkingDir };
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a non-numeric `/repo <arg>` into a concrete repo path + display name.
|
|
61
|
+
* `arg` is either a path (absolute or relative) or a first-level project name
|
|
62
|
+
* under one of the bot's scan dirs — letting the user skip the selection card.
|
|
63
|
+
*
|
|
64
|
+
* Resolution:
|
|
65
|
+
* 1. Build candidate absolute paths — absolute / `~` taken as-is; relative or
|
|
66
|
+
* bare names resolved against each scan dir, then the daemon cwd (mirrors
|
|
67
|
+
* how the card's project list is rooted).
|
|
68
|
+
* 2. Prefer a candidate matching a scanned git project (carries a branch label).
|
|
69
|
+
* 3. For a bare name, also match a scanned project by basename (covers projects
|
|
70
|
+
* nested deeper than the scan-dir top level).
|
|
71
|
+
* 4. Fall back to any existing directory — lenient like `/cd`, whose trust model
|
|
72
|
+
* is "owner explicitly chose a dir"; the CLI already runs with full FS access.
|
|
73
|
+
* Returns null when nothing resolves to an existing directory.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveRepoSelection(repoArg, scanDirs) {
|
|
76
|
+
const existingScanDirs = scanDirs.filter((d) => existsSync(d));
|
|
77
|
+
const projects = existingScanDirs.length > 0 ? scanMultipleProjects(existingScanDirs) : [];
|
|
78
|
+
const isExplicitPath = repoArg.startsWith('/') ||
|
|
79
|
+
repoArg.startsWith('~') ||
|
|
80
|
+
repoArg.startsWith('.') ||
|
|
81
|
+
repoArg.includes('/');
|
|
82
|
+
const candidates = [];
|
|
83
|
+
if (repoArg.startsWith('/') || repoArg.startsWith('~')) {
|
|
84
|
+
candidates.push(resolve(expandHome(repoArg)));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
for (const d of scanDirs)
|
|
88
|
+
candidates.push(resolve(d, repoArg));
|
|
89
|
+
candidates.push(resolve(expandHome(repoArg))); // daemon-cwd fallback (matches /cd)
|
|
90
|
+
}
|
|
91
|
+
// 1) Exact scanned-project match — preferred, gives the "name (branch)" label.
|
|
92
|
+
for (const cand of candidates) {
|
|
93
|
+
const proj = projects.find((p) => resolve(p.path) === cand);
|
|
94
|
+
if (proj)
|
|
95
|
+
return { path: proj.path, displayName: `${proj.name} (${proj.branch})` };
|
|
96
|
+
}
|
|
97
|
+
// 2) Bare name → match a scanned project by basename.
|
|
98
|
+
if (!isExplicitPath) {
|
|
99
|
+
const byName = projects.find((p) => p.name === repoArg);
|
|
100
|
+
if (byName)
|
|
101
|
+
return { path: byName.path, displayName: `${byName.name} (${byName.branch})` };
|
|
102
|
+
}
|
|
103
|
+
// 3) Lenient fallback: any existing directory. Label it with a git ref when
|
|
104
|
+
// it's a repo (covers explicit paths outside the scan roots), else basename.
|
|
105
|
+
for (const cand of candidates) {
|
|
106
|
+
try {
|
|
107
|
+
if (!statSync(cand).isDirectory())
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue; // missing / not a dir — try next candidate
|
|
112
|
+
}
|
|
113
|
+
const desc = describeProjectDir(cand);
|
|
114
|
+
return desc
|
|
115
|
+
? { path: cand, displayName: `${desc.name} (${desc.branch})` }
|
|
116
|
+
: { path: cand, displayName: basename(cand) };
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
50
120
|
/**
|
|
51
121
|
* Parse a force-topic invocation: `/t [prompt]` or `/topic [prompt]`.
|
|
52
122
|
*
|
|
@@ -159,16 +229,69 @@ function invalidConfiguredWorkingDirs(ds, larkAppId) {
|
|
|
159
229
|
});
|
|
160
230
|
}
|
|
161
231
|
// ─── Schedule command ────────────────────────────────────────────────────────
|
|
162
|
-
async function handleRoleCommand(args, rootId, chatId, larkAppId, deps) {
|
|
232
|
+
async function handleRoleCommand(args, rootId, chatId, larkAppId, senderId, deps) {
|
|
163
233
|
const sessionReply = (rid, content, msgType) => deps.sessionReply(rid, content, msgType, larkAppId);
|
|
164
234
|
const trimmed = args.trim();
|
|
165
235
|
const loc = localeForBot(larkAppId);
|
|
166
|
-
|
|
236
|
+
const dataDir = config.session.dataDir;
|
|
237
|
+
// /role team [...] — manage the team-level (per-bot, cross-chat) role
|
|
238
|
+
const teamMatch = trimmed.match(/^team\b([\s\S]*)$/);
|
|
239
|
+
if (teamMatch) {
|
|
240
|
+
const teamArgs = teamMatch[1].trim();
|
|
241
|
+
const teamSet = teamArgs.match(/^set\s+([\s\S]+)/);
|
|
242
|
+
if (teamSet) {
|
|
243
|
+
const content = teamSet[1].trim();
|
|
244
|
+
if (!content) {
|
|
245
|
+
await sessionReply(rootId, t('role.set_empty', undefined, loc));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
writeTeamRoleFile(larkAppId, content);
|
|
249
|
+
await sessionReply(rootId, t('role.team_saved', { bytes: Buffer.byteLength(content, 'utf-8'), max: 4096 }, loc));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (teamArgs === 'delete' || teamArgs === '删除') {
|
|
253
|
+
await sessionReply(rootId, deleteTeamRoleFile(larkAppId) ? t('role.team_deleted', undefined, loc) : t('role.team_nothing', undefined, loc));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const content = resolveTeamRoleFile(larkAppId);
|
|
257
|
+
if (content) {
|
|
258
|
+
await sessionReply(rootId, `${t('role.team_current', undefined, loc)}\n\`\`\`markdown\n${content}\n\`\`\`\n${t('role.byte_count', { bytes: Buffer.byteLength(content, 'utf-8'), max: 4096 }, loc)}`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
await sessionReply(rootId, t('role.team_empty', undefined, loc));
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// /role cap [...] — manage the short capability label shown in the roster
|
|
266
|
+
const capMatch = trimmed.match(/^cap\b([\s\S]*)$/);
|
|
267
|
+
if (capMatch) {
|
|
268
|
+
const capArgs = capMatch[1].trim();
|
|
269
|
+
const capSet = capArgs.match(/^set\s+([\s\S]+)/);
|
|
270
|
+
if (capSet) {
|
|
271
|
+
const label = capSet[1].trim();
|
|
272
|
+
if (!label) {
|
|
273
|
+
await sessionReply(rootId, t('role.cap_set_empty', undefined, loc));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
setBotCapability(dataDir, larkAppId, label, senderId);
|
|
277
|
+
await sessionReply(rootId, t('role.cap_saved', { cap: getBotCapability(dataDir, larkAppId) ?? label }, loc));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (capArgs === 'clear' || capArgs === '清除') {
|
|
281
|
+
await sessionReply(rootId, clearBotCapability(dataDir, larkAppId) ? t('role.cap_cleared', undefined, loc) : t('role.cap_empty', undefined, loc));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const cap = getBotCapability(dataDir, larkAppId);
|
|
285
|
+
await sessionReply(rootId, cap ? t('role.cap_current', { cap }, loc) : t('role.cap_empty', undefined, loc));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// /role → show the EFFECTIVE role + where it comes from (chat override > team > none)
|
|
167
289
|
if (!trimmed) {
|
|
168
|
-
const content =
|
|
290
|
+
const { content, source } = resolveRole(larkAppId, chatId);
|
|
169
291
|
if (content) {
|
|
170
292
|
const len = Buffer.byteLength(content, 'utf-8');
|
|
171
|
-
|
|
293
|
+
const srcLabel = source === 'chat' ? t('role.src_chat', undefined, loc) : t('role.src_team', undefined, loc);
|
|
294
|
+
await sessionReply(rootId, `${t('role.current', undefined, loc)} ${srcLabel}\n\`\`\`markdown\n${content}\n\`\`\`\n${t('role.byte_count', { bytes: len, max: 4096 }, loc)}`);
|
|
172
295
|
}
|
|
173
296
|
else {
|
|
174
297
|
await sessionReply(rootId, t('role.empty', undefined, loc));
|
|
@@ -311,237 +434,6 @@ async function handleScheduleCommand(args, rootId, chatId, deps, larkAppId) {
|
|
|
311
434
|
// Unrecognized format
|
|
312
435
|
await sessionReply(rootId, t('schedule.parse_failed', undefined, loc));
|
|
313
436
|
}
|
|
314
|
-
function taskUsage() {
|
|
315
|
-
return [
|
|
316
|
-
'用法:',
|
|
317
|
-
'/task new <name> - 登记当前 thread/chat 的任务(需要已有 session)',
|
|
318
|
-
'/task list - 列出当前群的任务',
|
|
319
|
-
'/task status <task-id> - 查看任务状态',
|
|
320
|
-
'/task close <task-id> - 关闭任务登记(不关闭 session)',
|
|
321
|
-
'/task assign <task-id> @Bot - 指派任务并显式 @目标 bot',
|
|
322
|
-
].join('\n');
|
|
323
|
-
}
|
|
324
|
-
function formatTaskStatus(task) {
|
|
325
|
-
return [
|
|
326
|
-
`Task: ${task.taskId}`,
|
|
327
|
-
`Feishu Task: ${task.externalTaskId ?? '-'}`,
|
|
328
|
-
`Name: ${task.name}`,
|
|
329
|
-
`Status: ${task.status}`,
|
|
330
|
-
`Session: ${task.sessionId}`,
|
|
331
|
-
`Chat: ${task.chatId}`,
|
|
332
|
-
`Anchor: ${task.anchor}`,
|
|
333
|
-
`Bot app: ${task.larkAppId}`,
|
|
334
|
-
`Owner: ${task.ownerOpenId ? `<at id=${task.ownerOpenId}></at>` : '-'}`,
|
|
335
|
-
`Assignee: ${task.assigneeOpenId ? `<at id=${task.assigneeOpenId}></at>` : '-'}`,
|
|
336
|
-
task.externalSyncError ? `Feishu sync: ${task.externalSyncError}` : undefined,
|
|
337
|
-
`Created: ${task.createdAt}`,
|
|
338
|
-
`Updated: ${task.updatedAt}`,
|
|
339
|
-
].filter(Boolean).join('\n');
|
|
340
|
-
}
|
|
341
|
-
function buildTaskHandoffPost(task, assigneeOpenId) {
|
|
342
|
-
const content = [
|
|
343
|
-
[
|
|
344
|
-
{ tag: 'at', user_id: assigneeOpenId },
|
|
345
|
-
{ tag: 'text', text: ` 请接手任务 ${task.taskId}:${task.name}` },
|
|
346
|
-
],
|
|
347
|
-
[{ tag: 'text', text: `Task: ${task.taskId}` }],
|
|
348
|
-
[{ tag: 'text', text: `Feishu Task: ${task.externalTaskId ?? '-'}` }],
|
|
349
|
-
[{ tag: 'text', text: `Session: ${task.sessionId}` }],
|
|
350
|
-
[{ tag: 'text', text: `Chat: ${task.chatId}` }],
|
|
351
|
-
[{ tag: 'text', text: `Thread/anchor: ${task.anchor}` }],
|
|
352
|
-
task.ownerOpenId
|
|
353
|
-
? [{ tag: 'text', text: 'Owner: ' }, { tag: 'at', user_id: task.ownerOpenId }]
|
|
354
|
-
: [{ tag: 'text', text: 'Owner: -' }],
|
|
355
|
-
[{ tag: 'text', text: '请基于这个 task id 和对应 thread/session 上下文继续处理;这是显式 @ 指派,不依赖 lastCaller footer。' }],
|
|
356
|
-
];
|
|
357
|
-
return JSON.stringify({ zh_cn: { title: '', content } });
|
|
358
|
-
}
|
|
359
|
-
function taskSyncError(err) {
|
|
360
|
-
if (err instanceof FeishuTaskUnavailableError)
|
|
361
|
-
return err.message;
|
|
362
|
-
return err instanceof Error ? err.message : String(err);
|
|
363
|
-
}
|
|
364
|
-
async function tryCreateExternalTask(task) {
|
|
365
|
-
try {
|
|
366
|
-
const external = await createFeishuTask({
|
|
367
|
-
larkAppId: task.larkAppId,
|
|
368
|
-
summary: task.name,
|
|
369
|
-
localTaskId: task.taskId,
|
|
370
|
-
sessionId: task.sessionId,
|
|
371
|
-
chatId: task.chatId,
|
|
372
|
-
anchor: task.anchor,
|
|
373
|
-
ownerOpenId: task.ownerOpenId,
|
|
374
|
-
});
|
|
375
|
-
return taskStore.updateTaskFields(task.taskId, {
|
|
376
|
-
externalTaskId: external.guid,
|
|
377
|
-
externalTaskUrl: external.url,
|
|
378
|
-
externalSyncError: undefined,
|
|
379
|
-
}) ?? task;
|
|
380
|
-
}
|
|
381
|
-
catch (err) {
|
|
382
|
-
const message = taskSyncError(err);
|
|
383
|
-
logger.warn(`[task] Feishu task create failed for ${task.taskId}: ${message}`);
|
|
384
|
-
return taskStore.updateTaskFields(task.taskId, { externalSyncError: message }) ?? task;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
async function tryCompleteExternalTask(task) {
|
|
388
|
-
if (!task.externalTaskId)
|
|
389
|
-
return task;
|
|
390
|
-
try {
|
|
391
|
-
await completeFeishuTask(task.larkAppId, task.externalTaskId);
|
|
392
|
-
return taskStore.updateTaskFields(task.taskId, { externalSyncError: undefined }) ?? task;
|
|
393
|
-
}
|
|
394
|
-
catch (err) {
|
|
395
|
-
const message = taskSyncError(err);
|
|
396
|
-
logger.warn(`[task] Feishu task complete failed for ${task.taskId}: ${message}`);
|
|
397
|
-
return taskStore.updateTaskFields(task.taskId, { externalSyncError: message }) ?? task;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
async function tryAssignExternalTask(task, assigneeOpenId) {
|
|
401
|
-
if (!task.externalTaskId)
|
|
402
|
-
return task;
|
|
403
|
-
try {
|
|
404
|
-
await addFeishuTaskAssignee(task.larkAppId, task.externalTaskId, assigneeOpenId);
|
|
405
|
-
return taskStore.updateTaskFields(task.taskId, { externalSyncError: undefined }) ?? task;
|
|
406
|
-
}
|
|
407
|
-
catch (err) {
|
|
408
|
-
const message = taskSyncError(err);
|
|
409
|
-
logger.warn(`[task] Feishu task assign failed for ${task.taskId}: ${message}`);
|
|
410
|
-
return taskStore.updateTaskFields(task.taskId, { externalSyncError: message }) ?? task;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
function mentionedOpenIds(message) {
|
|
414
|
-
return (message.mentions ?? [])
|
|
415
|
-
.map(m => m.openId)
|
|
416
|
-
.filter((id) => !!id);
|
|
417
|
-
}
|
|
418
|
-
function isCurrentBotMention(openId, larkAppId) {
|
|
419
|
-
if (!larkAppId)
|
|
420
|
-
return false;
|
|
421
|
-
const self = getBotOpenId(larkAppId);
|
|
422
|
-
return !!self && self === openId;
|
|
423
|
-
}
|
|
424
|
-
async function handleTaskCommand(args, rootId, message, deps, larkAppId) {
|
|
425
|
-
const { activeSessions } = deps;
|
|
426
|
-
const sessionReply = (rid, content, msgType) => deps.sessionReply(rid, content, msgType, larkAppId);
|
|
427
|
-
const rawDs = larkAppId ? activeSessions.get(sessionKey(rootId, larkAppId)) : undefined;
|
|
428
|
-
const isEphemeralTaskCommandSession = !!rawDs && !rawDs.hasHistory && !rawDs.worker && rawDs.session.title.trim().startsWith('/task');
|
|
429
|
-
const commandChatId = rawDs?.chatId;
|
|
430
|
-
if (isEphemeralTaskCommandSession && larkAppId) {
|
|
431
|
-
activeSessions.delete(sessionKey(rootId, larkAppId));
|
|
432
|
-
sessionStore.closeSession(rawDs.session.sessionId);
|
|
433
|
-
}
|
|
434
|
-
const ds = isEphemeralTaskCommandSession ? undefined : rawDs;
|
|
435
|
-
const [subRaw, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
436
|
-
const sub = subRaw?.toLowerCase();
|
|
437
|
-
if (!sub || sub === 'help' || sub === '帮助') {
|
|
438
|
-
await sessionReply(rootId, taskUsage());
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
if (sub === 'new' || sub === '新建') {
|
|
442
|
-
const name = rest.join(' ').trim();
|
|
443
|
-
if (!name) {
|
|
444
|
-
await sessionReply(rootId, '请提供任务名称:/task new <name>');
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
if (!ds) {
|
|
448
|
-
await sessionReply(rootId, '当前没有可绑定的 session。请先在独立话题里 @Bot 发起任务,或用 /t <任务内容> 开一个独立任务。');
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
let task = taskStore.createTask({
|
|
452
|
-
name,
|
|
453
|
-
chatId: ds.chatId,
|
|
454
|
-
anchor: sessionAnchorId(ds),
|
|
455
|
-
sessionId: ds.session.sessionId,
|
|
456
|
-
larkAppId: ds.larkAppId,
|
|
457
|
-
ownerOpenId: message.senderId || ds.ownerOpenId || ds.session.ownerOpenId,
|
|
458
|
-
});
|
|
459
|
-
task = await tryCreateExternalTask(task);
|
|
460
|
-
const externalLine = task.externalTaskId
|
|
461
|
-
? `Feishu Task: ${task.externalTaskId}`
|
|
462
|
-
: `Feishu Task: 未创建(${task.externalSyncError ?? 'unknown'}),已降级为本地 task`;
|
|
463
|
-
await sessionReply(rootId, `✅ 已登记任务 ${task.taskId}\n${externalLine}\nName: ${task.name}\nSession: ${task.sessionId}\nAnchor: ${task.anchor}`);
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
if (sub === 'list' || sub === '列表') {
|
|
467
|
-
const chatId = ds?.chatId ?? commandChatId ?? (() => {
|
|
468
|
-
const all = taskStore.listTasks();
|
|
469
|
-
return all.find(t => t.anchor === rootId && t.larkAppId === larkAppId)?.chatId ?? all.find(t => t.anchor === rootId)?.chatId ?? rootId;
|
|
470
|
-
})();
|
|
471
|
-
const tasks = taskStore.listTasks({ chatId });
|
|
472
|
-
if (tasks.length === 0) {
|
|
473
|
-
await sessionReply(rootId, '当前群还没有登记 task。');
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
const lines = tasks.map(t => `${t.taskId} [${t.status}] ${t.name}\n feishu=${t.externalTaskId ?? '-'} session=${t.sessionId} anchor=${t.anchor} assignee=${t.assigneeOpenId ? `<at id=${t.assigneeOpenId}></at>` : '-'}`);
|
|
477
|
-
await sessionReply(rootId, `当前群任务:\n${lines.join('\n')}`);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
if (sub === 'status' || sub === '状态') {
|
|
481
|
-
const id = rest[0];
|
|
482
|
-
if (!id) {
|
|
483
|
-
await sessionReply(rootId, '请提供 task id:/task status <task-id>');
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
const task = taskStore.getTask(id);
|
|
487
|
-
if (!task) {
|
|
488
|
-
await sessionReply(rootId, `未找到 task:${id}`);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
await sessionReply(rootId, formatTaskStatus(task));
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
if (sub === 'close' || sub === '关闭') {
|
|
495
|
-
const id = rest[0];
|
|
496
|
-
if (!id) {
|
|
497
|
-
await sessionReply(rootId, '请提供 task id:/task close <task-id>');
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
let task = taskStore.closeTask(id);
|
|
501
|
-
if (!task) {
|
|
502
|
-
await sessionReply(rootId, `未找到 task:${id}`);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
task = await tryCompleteExternalTask(task);
|
|
506
|
-
const syncLine = task.externalTaskId
|
|
507
|
-
? (task.externalSyncError ? `\nFeishu Task 同步失败:${task.externalSyncError}` : '\nFeishu Task 已标记完成。')
|
|
508
|
-
: '';
|
|
509
|
-
await sessionReply(rootId, `✅ 已关闭任务 ${task.taskId}${syncLine}\n注意:关联 session 未关闭,如需关闭请在对应话题里使用 /close。`);
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
if (sub === 'assign' || sub === '指派') {
|
|
513
|
-
const id = rest[0];
|
|
514
|
-
if (!id) {
|
|
515
|
-
await sessionReply(rootId, '请提供 task id:/task assign <task-id> @Bot');
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
const candidates = mentionedOpenIds(message).filter(openId => !isCurrentBotMention(openId, larkAppId));
|
|
519
|
-
const assigneeOpenId = candidates[0];
|
|
520
|
-
if (!assigneeOpenId) {
|
|
521
|
-
await sessionReply(rootId, '请在 /task assign 命令里显式 @目标 Bot。');
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
let task = taskStore.assignTask(id, assigneeOpenId);
|
|
525
|
-
if (!task) {
|
|
526
|
-
await sessionReply(rootId, `未找到 task:${id}`);
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
task = await tryAssignExternalTask(task, assigneeOpenId);
|
|
530
|
-
const handoff = buildTaskHandoffPost(task, assigneeOpenId);
|
|
531
|
-
if (!larkAppId) {
|
|
532
|
-
await sessionReply(rootId, formatTaskStatus(task));
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
if (ds?.scope === 'chat' || task.anchor.startsWith('oc_')) {
|
|
536
|
-
await sendMessage(larkAppId, task.chatId, handoff, 'post');
|
|
537
|
-
}
|
|
538
|
-
else {
|
|
539
|
-
await replyMessage(larkAppId, task.anchor, handoff, 'post', true);
|
|
540
|
-
}
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
await sessionReply(rootId, taskUsage());
|
|
544
|
-
}
|
|
545
437
|
// ─── Main command handler ────────────────────────────────────────────────────
|
|
546
438
|
export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
547
439
|
const { activeSessions, getActiveCount, lastRepoScan } = deps;
|
|
@@ -630,38 +522,50 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
630
522
|
}
|
|
631
523
|
case '/repo': {
|
|
632
524
|
const repoArg = message.content.replace(/^\/repo\s*/, '').trim();
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
525
|
+
// First-spawn fork: consume the buffered prompt/attachments and start the
|
|
526
|
+
// CLI in whatever workingDir is currently set on the session. Shared by
|
|
527
|
+
// `commitRepoSelection` (a repo was named) and the bare-`/repo` launch
|
|
528
|
+
// (use the default workingDir) — both only run while `pendingRepo`.
|
|
529
|
+
const forkPendingCli = async (replyText) => {
|
|
530
|
+
const selfBot = getBot(ds.larkAppId);
|
|
531
|
+
const botCfg = selfBot.config;
|
|
532
|
+
ds.pendingRepo = false;
|
|
533
|
+
const pendingPrompt = ds.pendingPrompt ?? '';
|
|
534
|
+
// Was there an actual buffered user message to deliver? A session
|
|
535
|
+
// launched *via* `/repo` (the command itself is the first message) has
|
|
536
|
+
// none — so boot the CLI idle and let the user's NEXT message be the
|
|
537
|
+
// first prompt, instead of submitting an empty/boilerplate user_message.
|
|
538
|
+
const hasBufferedInput = pendingPrompt.trim().length > 0 ||
|
|
539
|
+
(ds.pendingAttachments?.length ?? 0) > 0 ||
|
|
540
|
+
(ds.pendingFollowUps?.length ?? 0) > 0;
|
|
541
|
+
if (hasBufferedInput) {
|
|
542
|
+
const { buildNewTopicPrompt, getAvailableBots } = await import('./session-manager.js');
|
|
543
|
+
const prompt = buildNewTopicPrompt(pendingPrompt, ds.session.sessionId, botCfg.cliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, loc, ds.pendingSender, { larkAppId, chatId: ds.chatId });
|
|
544
|
+
rememberLastCliInput(ds, pendingPrompt, prompt);
|
|
545
|
+
forkWorker(ds, prompt);
|
|
639
546
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
547
|
+
else {
|
|
548
|
+
// Empty initial prompt → worker spawns the CLI without submitting
|
|
549
|
+
// anything (see worker.ts: the init prompt is only queued when truthy).
|
|
550
|
+
forkWorker(ds, '', false);
|
|
643
551
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
552
|
+
ds.pendingPrompt = undefined;
|
|
553
|
+
ds.pendingAttachments = undefined;
|
|
554
|
+
ds.pendingMentions = undefined;
|
|
555
|
+
ds.pendingSender = undefined;
|
|
556
|
+
ds.pendingFollowUps = undefined;
|
|
557
|
+
await sessionReply(rootId, replyText);
|
|
558
|
+
};
|
|
559
|
+
// Shared commit path for an already-resolved repo: update the session's
|
|
560
|
+
// working dir, then either fork into the pending CLI (first spawn) or
|
|
561
|
+
// close + recreate the session (mid-session switch). Used by both the
|
|
562
|
+
// numeric `/repo <N>` form and the `/repo <path|name>` form.
|
|
563
|
+
const commitRepoSelection = async (selectedPath, displayName, how) => {
|
|
647
564
|
ds.workingDir = selectedPath;
|
|
648
565
|
ds.session.workingDir = selectedPath;
|
|
649
566
|
sessionStore.updateSession(ds.session);
|
|
650
567
|
if (ds.pendingRepo) {
|
|
651
|
-
|
|
652
|
-
const botCfg = selfBot.config;
|
|
653
|
-
ds.pendingRepo = false;
|
|
654
|
-
const { buildNewTopicPrompt, getAvailableBots } = await import('./session-manager.js');
|
|
655
|
-
const pendingPrompt = ds.pendingPrompt ?? '';
|
|
656
|
-
const prompt = buildNewTopicPrompt(pendingPrompt, ds.session.sessionId, botCfg.cliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, loc, ds.pendingSender, { larkAppId, chatId: ds.chatId });
|
|
657
|
-
rememberLastCliInput(ds, pendingPrompt, prompt);
|
|
658
|
-
ds.pendingPrompt = undefined;
|
|
659
|
-
ds.pendingAttachments = undefined;
|
|
660
|
-
ds.pendingMentions = undefined;
|
|
661
|
-
ds.pendingSender = undefined;
|
|
662
|
-
ds.pendingFollowUps = undefined;
|
|
663
|
-
forkWorker(ds, prompt);
|
|
664
|
-
await sessionReply(rootId, t('cmd.repo.selected_in_pending', { name: displayName }, loc));
|
|
568
|
+
await forkPendingCli(t('cmd.repo.selected_in_pending', { name: displayName }, loc));
|
|
665
569
|
}
|
|
666
570
|
else {
|
|
667
571
|
killWorker(ds);
|
|
@@ -681,7 +585,56 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
681
585
|
deleteMessage(ds.larkAppId, ds.repoCardMessageId);
|
|
682
586
|
ds.repoCardMessageId = undefined;
|
|
683
587
|
}
|
|
684
|
-
logger.info(`[${logTag}] Repo selected via
|
|
588
|
+
logger.info(`[${logTag}] Repo selected via ${how}: ${selectedPath}`);
|
|
589
|
+
};
|
|
590
|
+
// Numeric arg → pick by 1-based index from the last scan.
|
|
591
|
+
if (repoArg && ds && /^\d+$/.test(repoArg)) {
|
|
592
|
+
const repoIndex = parseInt(repoArg, 10);
|
|
593
|
+
const cached = lastRepoScan.get(ds.chatId);
|
|
594
|
+
if (!cached || cached.length === 0) {
|
|
595
|
+
await sessionReply(rootId, t('cmd.repo.no_prior_scan', undefined, loc));
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
if (repoIndex < 1 || repoIndex > cached.length) {
|
|
599
|
+
await sessionReply(rootId, t('cmd.repo.index_out_of_range', { max: cached.length }, loc));
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
const project = cached[repoIndex - 1];
|
|
603
|
+
await commitRepoSelection(project.path, `${project.name} (${project.branch})`, `/repo ${repoIndex}`);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
// Non-numeric arg → a path (relative/absolute) or first-level project
|
|
607
|
+
// name under workingDir; resolve it directly and skip the card.
|
|
608
|
+
if (repoArg && ds) {
|
|
609
|
+
const resolved = resolveRepoSelection(repoArg, getProjectScanDirs(ds));
|
|
610
|
+
if (!resolved) {
|
|
611
|
+
await sessionReply(rootId, t('cmd.repo.path_not_found', { arg: repoArg }, loc));
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
await commitRepoSelection(resolved.path, resolved.displayName, `/repo ${repoArg}`);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
// Bare `/repo` while a repo card is pending → launch right away in the
|
|
618
|
+
// default workingDir. This is the text-command twin of the card's
|
|
619
|
+
// "start directly" button (and replaces the old `/skip` command).
|
|
620
|
+
// Mid-session bare `/repo` (no pending) still falls through to the card.
|
|
621
|
+
if (!repoArg && ds?.pendingRepo) {
|
|
622
|
+
// Validate the configured workingDir before spawning — `forkWorker`
|
|
623
|
+
// doesn't, so a dead cwd would otherwise spawn-and-fail silently. Same
|
|
624
|
+
// guard the card path runs below. On failure we keep the pending state
|
|
625
|
+
// so the user can recover with `/repo <valid-path>` (no card here).
|
|
626
|
+
const invalidDirs = invalidConfiguredWorkingDirs(ds, ds.larkAppId ?? larkAppId);
|
|
627
|
+
if (invalidDirs.length > 0) {
|
|
628
|
+
await sessionReply(rootId, t('cmd.repo.working_dir_not_exist', { dirs: invalidDirs.map(d => `\`${d}\``).join(', ') }, loc));
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
const cwd = getSessionWorkingDir(ds);
|
|
632
|
+
await forkPendingCli(t('cmd.skip.opened', { cwd }, loc));
|
|
633
|
+
if (ds.repoCardMessageId) {
|
|
634
|
+
deleteMessage(ds.larkAppId, ds.repoCardMessageId);
|
|
635
|
+
ds.repoCardMessageId = undefined;
|
|
636
|
+
}
|
|
637
|
+
logger.info(`[${logTag}] Bare /repo while pending → launch in workingDir ${cwd}`);
|
|
685
638
|
break;
|
|
686
639
|
}
|
|
687
640
|
if (ds?.worker && !ds.worker.killed) {
|
|
@@ -713,34 +666,6 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
713
666
|
logger.info(`[${logTag}] Sent repo card with ${projects.length} project(s)`);
|
|
714
667
|
break;
|
|
715
668
|
}
|
|
716
|
-
case '/skip': {
|
|
717
|
-
if (ds?.pendingRepo) {
|
|
718
|
-
const selfBot = getBot(ds.larkAppId);
|
|
719
|
-
const botCfg = selfBot.config;
|
|
720
|
-
ds.pendingRepo = false;
|
|
721
|
-
const { buildNewTopicPrompt, getAvailableBots } = await import('./session-manager.js');
|
|
722
|
-
const pendingPrompt = ds.pendingPrompt ?? '';
|
|
723
|
-
const prompt = buildNewTopicPrompt(pendingPrompt, ds.session.sessionId, botCfg.cliId, botCfg.cliPathOverride, ds.pendingAttachments, ds.pendingMentions, await getAvailableBots(ds.larkAppId, ds.chatId), ds.pendingFollowUps, { name: selfBot.botName, openId: selfBot.botOpenId }, loc, ds.pendingSender, { larkAppId, chatId: ds.chatId });
|
|
724
|
-
rememberLastCliInput(ds, pendingPrompt, prompt);
|
|
725
|
-
ds.pendingPrompt = undefined;
|
|
726
|
-
ds.pendingAttachments = undefined;
|
|
727
|
-
ds.pendingMentions = undefined;
|
|
728
|
-
ds.pendingSender = undefined;
|
|
729
|
-
ds.pendingFollowUps = undefined;
|
|
730
|
-
forkWorker(ds, prompt);
|
|
731
|
-
const cwd = getSessionWorkingDir(ds);
|
|
732
|
-
await sessionReply(rootId, t('cmd.skip.opened', { cwd }, loc));
|
|
733
|
-
if (ds.repoCardMessageId) {
|
|
734
|
-
deleteMessage(ds.larkAppId, ds.repoCardMessageId);
|
|
735
|
-
ds.repoCardMessageId = undefined;
|
|
736
|
-
}
|
|
737
|
-
logger.info(`[${logTag}] Skip repo via /skip, spawning CLI in ${cwd}`);
|
|
738
|
-
}
|
|
739
|
-
else {
|
|
740
|
-
await sessionReply(rootId, t('cmd.skip.no_pending', undefined, loc));
|
|
741
|
-
}
|
|
742
|
-
break;
|
|
743
|
-
}
|
|
744
669
|
case '/status': {
|
|
745
670
|
if (ds) {
|
|
746
671
|
const alive = ds.worker && !ds.worker.killed;
|
|
@@ -775,12 +700,6 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
775
700
|
logger.info(`[${logTag}] Schedule command handled`);
|
|
776
701
|
break;
|
|
777
702
|
}
|
|
778
|
-
case '/task': {
|
|
779
|
-
const taskArgs = message.content.replace(/^\/task\s*/i, '');
|
|
780
|
-
await handleTaskCommand(taskArgs, rootId, message, deps, larkAppId);
|
|
781
|
-
logger.info(`[${logTag}] Task command handled`);
|
|
782
|
-
break;
|
|
783
|
-
}
|
|
784
703
|
case '/role': {
|
|
785
704
|
const chatId = ds?.chatId;
|
|
786
705
|
if (!chatId || !larkAppId) {
|
|
@@ -788,10 +707,35 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
788
707
|
break;
|
|
789
708
|
}
|
|
790
709
|
const roleArgs = message.content.replace(/^\/role\s*/, '');
|
|
791
|
-
await handleRoleCommand(roleArgs, rootId, chatId, larkAppId, deps);
|
|
710
|
+
await handleRoleCommand(roleArgs, rootId, chatId, larkAppId, message.senderId, deps);
|
|
792
711
|
logger.info(`[${logTag}] Role command handled`);
|
|
793
712
|
break;
|
|
794
713
|
}
|
|
714
|
+
case '/pair': {
|
|
715
|
+
const code = message.content.replace(/^\/pair\s*/, '').trim();
|
|
716
|
+
if (!larkAppId) {
|
|
717
|
+
await sessionReply(rootId, t('role.no_chat', undefined, loc));
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
if (!code) {
|
|
721
|
+
await sessionReply(rootId, t('pair.usage', undefined, loc));
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
// Resolve the sender's canonical union_id (best-effort) so the web
|
|
725
|
+
// session is keyed stably across apps; degrade to open_id-only.
|
|
726
|
+
const who = await resolveUserUnionId(larkAppId, message.senderId);
|
|
727
|
+
const result = claimPairing(config.session.dataDir, code, { openId: message.senderId, unionId: who.unionId, name: who.name, larkAppId });
|
|
728
|
+
if (result.ok)
|
|
729
|
+
await sessionReply(rootId, t('pair.ok', undefined, loc));
|
|
730
|
+
else if (result.reason === 'expired')
|
|
731
|
+
await sessionReply(rootId, t('pair.expired', undefined, loc));
|
|
732
|
+
else if (result.reason === 'already_claimed')
|
|
733
|
+
await sessionReply(rootId, t('pair.already', undefined, loc));
|
|
734
|
+
else
|
|
735
|
+
await sessionReply(rootId, t('pair.not_found', undefined, loc));
|
|
736
|
+
logger.info(`[${logTag}] Pair command handled: ${result.ok ? 'ok' : result.reason}`);
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
795
739
|
case '/login': {
|
|
796
740
|
const subCmd = message.content.replace(/^\/login\s*/, '').trim();
|
|
797
741
|
if (subCmd === 'status' || subCmd === '状态') {
|
|
@@ -948,7 +892,10 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
948
892
|
// • RESOLUTION (bot → larkAppId for the invite) uses the live roster
|
|
949
893
|
// listChatBotMembers(), failing CLOSED on any miss.
|
|
950
894
|
const mentions = message.mentions ?? [];
|
|
951
|
-
|
|
895
|
+
// `/group` runs without a pre-created session (see
|
|
896
|
+
// SESSIONLESS_DAEMON_COMMANDS), so the source chat comes from the
|
|
897
|
+
// message; fall back to the active session when invoked mid-session.
|
|
898
|
+
const sourceChatId = message.chatId ?? ds?.chatId;
|
|
952
899
|
const knownBotNames = globalKnownBotNames();
|
|
953
900
|
// Degraded-state guard: if the user @-mentioned someone but the global bot
|
|
954
901
|
// registry is empty (bots-info.json missing/corrupt/not-yet-written), we
|
|
@@ -1056,7 +1003,10 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
1056
1003
|
transferOwnerTo: senderOpenId,
|
|
1057
1004
|
notifyOwnerOpenId: senderOpenId,
|
|
1058
1005
|
});
|
|
1059
|
-
|
|
1006
|
+
// Prefer the shareable join link (others can click to *join*); fall
|
|
1007
|
+
// back to the member-only applink URL when Lark's link API failed.
|
|
1008
|
+
const applink = `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(result.chatId)}`;
|
|
1009
|
+
const link = result.shareLink ?? applink;
|
|
1060
1010
|
// Partial failures are non-fatal — the chat exists; surface them as
|
|
1061
1011
|
// hints so the user knows whether to expect to be auto-invited.
|
|
1062
1012
|
const hints = [];
|
|
@@ -1066,6 +1016,12 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
1066
1016
|
else if (result.transferError) {
|
|
1067
1017
|
hints.push(t('cmd.group.warn_transfer_failed', { reason: result.transferError }, loc));
|
|
1068
1018
|
}
|
|
1019
|
+
// Share-link fetch failed → the displayed link is the member-only
|
|
1020
|
+
// applink; warn the user so they don't expect non-members to join via it.
|
|
1021
|
+
if (!result.shareLink && result.shareLinkError) {
|
|
1022
|
+
logger.warn(`[${logTag}] /group share-link unavailable, using applink: ${result.shareLinkError}`);
|
|
1023
|
+
hints.push(t('cmd.group.warn_share_link_failed', undefined, loc));
|
|
1024
|
+
}
|
|
1069
1025
|
// List every bot in the new group (creator included), and warn about
|
|
1070
1026
|
// any Feishu rejected. Names come from the chat roster (members) since
|
|
1071
1027
|
// getBot() only knows this process's own bot in the one-daemon-per-bot
|
|
@@ -1092,6 +1048,527 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
1092
1048
|
}
|
|
1093
1049
|
break;
|
|
1094
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* `/relay --create <群名> @bot [@bot...]` — create a new chat, invite
|
|
1053
|
+
* the @-mentioned bots, then migrate every bot's session in this
|
|
1054
|
+
* thread (including the leader's) into the new chat.
|
|
1055
|
+
*
|
|
1056
|
+
* Two-path command:
|
|
1057
|
+
* • `--create` (PR2) — implemented below; creates a new chat.
|
|
1058
|
+
* • no flag (PR3) — picker card listing user's relayable sessions
|
|
1059
|
+
* in OTHER chats so the user can pull one into
|
|
1060
|
+
* the current chat. Stubbed for now.
|
|
1061
|
+
*
|
|
1062
|
+
* Leader election is `mentions[0]` (identical to /group). The leader
|
|
1063
|
+
* is the only daemon that:
|
|
1064
|
+
* 1. Creates the new chat (createGroupWithBots)
|
|
1065
|
+
* 2. Sends the M1 announcement message (its message_id becomes the
|
|
1066
|
+
* shared rootMessageId for all relayed sessions — multi-bot
|
|
1067
|
+
* sessions co-anchor on the same root via different larkAppIds)
|
|
1068
|
+
* 3. Transfers its own session (if any) via local transferSession()
|
|
1069
|
+
* 4. POSTs /api/sessions/migrate-to-chat to every peer daemon to
|
|
1070
|
+
* ask them to transfer their own session at the same anchor
|
|
1071
|
+
* 5. Aggregates results into a single reply in the source thread
|
|
1072
|
+
*
|
|
1073
|
+
* Owner-only: only the source session's `ownerOpenId` may invoke. Peers
|
|
1074
|
+
* enforce the same check independently inside the migrate endpoint.
|
|
1075
|
+
*
|
|
1076
|
+
* Failure mode: best-effort, no rollback. Peers that timeout / fail /
|
|
1077
|
+
* are offline simply appear in the report as "skipped". The new chat
|
|
1078
|
+
* and any successful transfers stand.
|
|
1079
|
+
*/
|
|
1080
|
+
case '/relay': {
|
|
1081
|
+
const argsLine = message.content.replace(/^\/relay\s*/i, '').trim();
|
|
1082
|
+
if (!/^--create\b/i.test(argsLine)) {
|
|
1083
|
+
// ── Pull picker ───────────────────────────────────────────────────
|
|
1084
|
+
// /relay (no flag) lives in the *target* chat — list the operator's
|
|
1085
|
+
// own active sessions in OTHER chats so they can pull one in.
|
|
1086
|
+
//
|
|
1087
|
+
// Filter:
|
|
1088
|
+
// • same bot (this larkAppId)
|
|
1089
|
+
// • session is active (has a worker / appears in activeSessions)
|
|
1090
|
+
// • session NOT in the current chat (can't relay to yourself)
|
|
1091
|
+
// • operator IS the session owner (owner-only access)
|
|
1092
|
+
//
|
|
1093
|
+
// The button's `target_chat_id` / `target_root_id` are the chat we're
|
|
1094
|
+
// pulling INTO (the chat hosting this command). card-handler uses
|
|
1095
|
+
// them to invoke transferSession after sending the M1 announcement.
|
|
1096
|
+
const operatorOpenId = message.senderId;
|
|
1097
|
+
if (!operatorOpenId) {
|
|
1098
|
+
await sessionReply(rootId, t('cmd.relay.no_sender', undefined, loc));
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
const myAppId = larkAppId ?? ds?.larkAppId;
|
|
1102
|
+
if (!myAppId) {
|
|
1103
|
+
await sessionReply(rootId, t('cmd.group.no_bot', undefined, loc));
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
const targetChatId = ds?.chatId;
|
|
1107
|
+
if (!targetChatId) {
|
|
1108
|
+
await sessionReply(rootId, t('cmd.relay.no_session', undefined, loc));
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
// ── Chat-type guard ───────────────────────────────────────────────
|
|
1112
|
+
// Picker mode only makes sense in regular group chats. p2p (1:1 with
|
|
1113
|
+
// bot) has no relay concept — there's no other participant to
|
|
1114
|
+
// collaborate with — and topic chats route per-thread, so a chat-
|
|
1115
|
+
// scope session pulled in would have no thread anchor.
|
|
1116
|
+
//
|
|
1117
|
+
// p2p is detectable from `ds.chatType` locally (cheap). Topic vs
|
|
1118
|
+
// regular group is NOT captured in chatType — both record 'group'
|
|
1119
|
+
// — so we hit the Lark API (getChatNameAndMode) to resolve the
|
|
1120
|
+
// mode. One API call per /relay invocation; picker is user-
|
|
1121
|
+
// triggered so latency is acceptable.
|
|
1122
|
+
if (ds?.chatType === 'p2p') {
|
|
1123
|
+
await sessionReply(rootId, t('cmd.relay.picker_p2p_unsupported', undefined, loc));
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
{
|
|
1127
|
+
const { getChatNameAndMode } = await import('../im/lark/client.js');
|
|
1128
|
+
const info = await getChatNameAndMode(myAppId, targetChatId).catch(() => null);
|
|
1129
|
+
if (info?.mode === 'p2p') {
|
|
1130
|
+
await sessionReply(rootId, t('cmd.relay.picker_p2p_unsupported', undefined, loc));
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
if (info?.mode === 'topic') {
|
|
1134
|
+
await sessionReply(rootId, t('cmd.relay.picker_topic_unsupported', undefined, loc));
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// ── Existing-session guard ────────────────────────────────────────
|
|
1139
|
+
// If this bot already runs a real session in the target chat, pulling
|
|
1140
|
+
// another session in would collide on sessionKey(targetChatId, larkAppId)
|
|
1141
|
+
// — Map.set would silently overwrite, orphaning the existing worker.
|
|
1142
|
+
// Refuse upfront with an actionable message.
|
|
1143
|
+
//
|
|
1144
|
+
// Scratch sessions (the placeholder a `/relay` typed in a fresh chat
|
|
1145
|
+
// gets routed through) are filtered by `!!c.worker` — they have no
|
|
1146
|
+
// worker process. We do NOT exclude `ds` by sessionId: when `/relay`
|
|
1147
|
+
// rides an EXISTING real session (daemon.ts:2034's "existing-session
|
|
1148
|
+
// DAEMON_COMMANDS" path skips the scratch and binds `ds` to the
|
|
1149
|
+
// chat's real session), `ds` itself IS the conflict — excluding it
|
|
1150
|
+
// would let the picker render and the user pick a remote session
|
|
1151
|
+
// that the eventual transferSession would have to refuse anyway.
|
|
1152
|
+
const conflict = [...activeSessions.values()].find(c => c.larkAppId === myAppId
|
|
1153
|
+
&& c.chatId === targetChatId
|
|
1154
|
+
// chat-scope only: thread-scope sessions (e.g. a `/t` force-topic
|
|
1155
|
+
// session in a regular group) live at a different sessionKey
|
|
1156
|
+
// anchor (rootMessageId), so they don't collide on transfer.
|
|
1157
|
+
// transferSession's own pre-flight (worker-pool.ts) and card-
|
|
1158
|
+
// handler's confirm both filter the same way; align here so the
|
|
1159
|
+
// picker doesn't false-positive a thread-scope live session.
|
|
1160
|
+
&& c.scope === 'chat'
|
|
1161
|
+
&& !!c.worker // real running session, not a placeholder
|
|
1162
|
+
);
|
|
1163
|
+
if (conflict) {
|
|
1164
|
+
await sessionReply(rootId, t('cmd.relay.target_has_session', { title: conflict.session.title || conflict.session.sessionId.substring(0, 8) }, loc));
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
// Shared candidate-collection logic — used here at initial render
|
|
1168
|
+
// and again in card-handler when the user clicks a card to switch
|
|
1169
|
+
// selection (the card re-render needs the same filtered list).
|
|
1170
|
+
// Filters out: other bots / current chat / non-owned / adopt
|
|
1171
|
+
// sessions. Resolves friendly chat names + modes in parallel.
|
|
1172
|
+
const { collectRelayPickerEntries } = await import('../services/relay-picker.js');
|
|
1173
|
+
const entries = await collectRelayPickerEntries(activeSessions, myAppId, targetChatId, operatorOpenId);
|
|
1174
|
+
const { buildRelayPickerCard } = await import('../im/lark/card-builder.js');
|
|
1175
|
+
const card = buildRelayPickerCard(entries, targetChatId, rootId, operatorOpenId, loc);
|
|
1176
|
+
await sessionReply(rootId, card, 'interactive');
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
const afterFlag = argsLine.replace(/^--create\s*/i, '').trim();
|
|
1180
|
+
const creatorAppId = larkAppId ?? ds?.larkAppId;
|
|
1181
|
+
if (!creatorAppId) {
|
|
1182
|
+
await sessionReply(rootId, t('cmd.group.no_bot', undefined, loc));
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
const senderOpenId = message.senderId;
|
|
1186
|
+
// Cross-app stable identity — peer daemons can't compare against
|
|
1187
|
+
// leader's open_id directly because the same user has a different
|
|
1188
|
+
// open_id in each bot's namespace. union_id is shared per tenant.
|
|
1189
|
+
// We pass it through the migrate-to-chat HTTP body; peers compare
|
|
1190
|
+
// against their session's `ownerUnionId` (with fallback to
|
|
1191
|
+
// open_id for sessions persisted before this field existed).
|
|
1192
|
+
const senderUnionId = message.senderUnionId;
|
|
1193
|
+
if (!senderOpenId) {
|
|
1194
|
+
await sessionReply(rootId, t('cmd.relay.no_sender', undefined, loc));
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
// `--create` must be invoked inside an existing thread — the source
|
|
1198
|
+
// anchor for peer transfers comes from `ds`. (Picker mode in PR3 is
|
|
1199
|
+
// allowed without a session.)
|
|
1200
|
+
if (!ds) {
|
|
1201
|
+
await sessionReply(rootId, t('cmd.relay.no_session', undefined, loc));
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
// Front-loaded guards — transferSession refuses adoptedFrom /
|
|
1205
|
+
// pendingRepo too, but only after createGroupWithBots has already
|
|
1206
|
+
// built a new chat. Failing here keeps relay clean and avoids
|
|
1207
|
+
// orphan-chat garbage when the operation can't possibly succeed.
|
|
1208
|
+
if (ds.session.adoptedFrom) {
|
|
1209
|
+
await sessionReply(rootId, t('cmd.relay.adopt_not_relayable', undefined, loc));
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
if (ds.pendingRepo) {
|
|
1213
|
+
await sessionReply(rootId, t('cmd.relay.not_started_yet', undefined, loc));
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
// ── Mention parsing & leader election (mirror of /group) ───────────
|
|
1217
|
+
const mentions = message.mentions ?? [];
|
|
1218
|
+
const knownBotNames = globalKnownBotNames();
|
|
1219
|
+
if (knownBotNames.size === 0 && mentions.some(m => !!m.name)) {
|
|
1220
|
+
logger.warn(`[${logTag}] /relay --create: global bot registry empty; cannot elect a creator`);
|
|
1221
|
+
await sessionReply(rootId, t('cmd.relay.resolve_failed', undefined, loc));
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
const botMentions = mentions.filter(m => m.name && knownBotNames.has(m.name.toLowerCase()));
|
|
1225
|
+
if (botMentions.length === 0) {
|
|
1226
|
+
await sessionReply(rootId, t('cmd.relay.no_mentions', undefined, loc));
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
// Am I `mentions[0]`?
|
|
1230
|
+
const firstBot = botMentions[0];
|
|
1231
|
+
const myOpenId = getBotOpenId(creatorAppId);
|
|
1232
|
+
const myName = getBot(creatorAppId).botName?.toLowerCase();
|
|
1233
|
+
const myNameAmbiguous = !!myName
|
|
1234
|
+
&& botMentions.filter(m => m.name?.toLowerCase() === myName).length > 1;
|
|
1235
|
+
const iAmFirstBot = (!!myOpenId && firstBot.openId === myOpenId) ||
|
|
1236
|
+
(!myOpenId && !!myName && !myNameAmbiguous && firstBot.name?.toLowerCase() === myName);
|
|
1237
|
+
if (!iAmFirstBot) {
|
|
1238
|
+
logger.info(`[${logTag}] /relay --create: not the first @-mentioned bot, staying silent`);
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
// Owner-only — only the source session owner may relay this session.
|
|
1242
|
+
if (ds.session.ownerOpenId && ds.session.ownerOpenId !== senderOpenId) {
|
|
1243
|
+
await sessionReply(rootId, t('cmd.relay.not_owner', undefined, loc));
|
|
1244
|
+
break;
|
|
1245
|
+
}
|
|
1246
|
+
// ── Resolve @-bots to larkAppIds via the source chat's bot roster ──
|
|
1247
|
+
const sourceChatId = ds.chatId;
|
|
1248
|
+
let members = [];
|
|
1249
|
+
try {
|
|
1250
|
+
members = await listChatBotMembers(creatorAppId, sourceChatId);
|
|
1251
|
+
}
|
|
1252
|
+
catch (e) {
|
|
1253
|
+
logger.warn(`[${logTag}] /relay --create: failed to list source chat members: ${e?.message ?? e}`);
|
|
1254
|
+
}
|
|
1255
|
+
const memberByOpenId = new Map(members.map(m => [m.openId, m]));
|
|
1256
|
+
const appIdToName = new Map();
|
|
1257
|
+
for (const m of members) {
|
|
1258
|
+
if (m.larkAppId && m.displayName)
|
|
1259
|
+
appIdToName.set(m.larkAppId, m.displayName);
|
|
1260
|
+
}
|
|
1261
|
+
const mentionedBotAppIds = [];
|
|
1262
|
+
const seenApp = new Set();
|
|
1263
|
+
let unresolved;
|
|
1264
|
+
for (const bm of botMentions) {
|
|
1265
|
+
const mem = bm.openId ? memberByOpenId.get(bm.openId) : undefined;
|
|
1266
|
+
if (!mem || !mem.larkAppId) {
|
|
1267
|
+
unresolved = bm.name;
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
if (!seenApp.has(mem.larkAppId)) {
|
|
1271
|
+
seenApp.add(mem.larkAppId);
|
|
1272
|
+
mentionedBotAppIds.push(mem.larkAppId);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (unresolved) {
|
|
1276
|
+
logger.warn(`[${logTag}] /relay --create: unresolved bot "${unresolved}"`);
|
|
1277
|
+
await sessionReply(rootId, t('cmd.relay.resolve_failed', undefined, loc));
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
// ── Group name extraction (mirror of /group) ───────────────────────
|
|
1281
|
+
let rawArgs = afterFlag;
|
|
1282
|
+
for (const m of mentions) {
|
|
1283
|
+
if (m.name)
|
|
1284
|
+
rawArgs = rawArgs.split(`@${m.name}`).join(' ');
|
|
1285
|
+
}
|
|
1286
|
+
const firstLine = rawArgs.split(/\r?\n/).map(s => s.trim()).find(Boolean) ?? '';
|
|
1287
|
+
const MAX_NAME = 50;
|
|
1288
|
+
let groupName;
|
|
1289
|
+
if (firstLine) {
|
|
1290
|
+
groupName = firstLine.length > MAX_NAME ? firstLine.slice(0, MAX_NAME) + '…' : firstLine;
|
|
1291
|
+
}
|
|
1292
|
+
else {
|
|
1293
|
+
const now = new Date();
|
|
1294
|
+
const ts = `${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
1295
|
+
groupName = t('cmd.relay.empty_group_name', { ts }, loc);
|
|
1296
|
+
}
|
|
1297
|
+
// ── Create the new chat ────────────────────────────────────────────
|
|
1298
|
+
const nameOf = (id) => appIdToName.get(id) ?? botDisplayName(id);
|
|
1299
|
+
let newChatId;
|
|
1300
|
+
let inviteLink;
|
|
1301
|
+
try {
|
|
1302
|
+
const { createGroupWithBots } = await import('../services/group-creator.js');
|
|
1303
|
+
const result = await createGroupWithBots({
|
|
1304
|
+
creatorLarkAppId: creatorAppId,
|
|
1305
|
+
larkAppIds: mentionedBotAppIds,
|
|
1306
|
+
name: groupName,
|
|
1307
|
+
userOpenIds: [senderOpenId],
|
|
1308
|
+
transferOwnerTo: senderOpenId,
|
|
1309
|
+
});
|
|
1310
|
+
newChatId = result.chatId;
|
|
1311
|
+
const applink = `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(result.chatId)}`;
|
|
1312
|
+
inviteLink = result.shareLink ?? applink;
|
|
1313
|
+
}
|
|
1314
|
+
catch (err) {
|
|
1315
|
+
logger.error(`[${logTag}] /relay --create: createGroup failed: ${err?.message ?? err}`);
|
|
1316
|
+
await sessionReply(rootId, t('cmd.relay.failed', { error: err?.message ?? String(err) }, loc));
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
// Snapshot the pre-transfer source anchor — peers locate their own
|
|
1320
|
+
// session by this value, and `transferSession()` will overwrite
|
|
1321
|
+
// `ds.session.rootMessageId` once it runs. Must capture BEFORE the
|
|
1322
|
+
// leader transfer call (caught in review).
|
|
1323
|
+
const sourceAnchor = ds.session.rootMessageId;
|
|
1324
|
+
// ── M1 deferred: post the announcement AFTER all transfers settle ──
|
|
1325
|
+
// Previous flow sent an optimistic "已接力" M1 before running any
|
|
1326
|
+
// transfer. When leader/peers later failed, that M1 was a lie — and
|
|
1327
|
+
// the --create path had no orphan-cleanup (picker path did).
|
|
1328
|
+
//
|
|
1329
|
+
// New flow: pass `newChatId` as a placeholder for targetRootMessageId
|
|
1330
|
+
// into transferSession. Chat-scope routing ignores rootMessageId
|
|
1331
|
+
// (worker-pool transferSession only stores it for audit/UX), so the
|
|
1332
|
+
// placeholder doesn't break routing. Once all outcomes are in, we
|
|
1333
|
+
// post the real M1 with success/failure breakdown, then patch the
|
|
1334
|
+
// leader's session.rootMessageId to that final M1 id. Peer sessions
|
|
1335
|
+
// keep newChatId as a cosmetic placeholder — fixing them would
|
|
1336
|
+
// require another round-trip; chat-scope doesn't actually care.
|
|
1337
|
+
const placeholderRootMessageId = newChatId;
|
|
1338
|
+
// Resolve friendly source-chat label for the M1 body — falls back to
|
|
1339
|
+
// raw chatId if Lark can't return a name. Mirrors picker-path
|
|
1340
|
+
// (card-handler.ts:341) so the message reads the same in both UX
|
|
1341
|
+
// entry points.
|
|
1342
|
+
const { getChatName } = await import('../im/lark/client.js');
|
|
1343
|
+
const sourceLabel = (await getChatName(creatorAppId, sourceChatId).catch(() => null)) ?? sourceChatId;
|
|
1344
|
+
// ── Step 1: leader transfers its own session (if any) ───────────────
|
|
1345
|
+
// Empty-leader handling: daemon auto-creates a placeholder ds for any
|
|
1346
|
+
// DAEMON_COMMAND (worker:null + hasHistory:false). If the user typed
|
|
1347
|
+
// `/relay --create` in a chat where they never actually chatted with
|
|
1348
|
+
// the bot, ds IS that placeholder — there's no real session to
|
|
1349
|
+
// migrate. Pre-Codex-review we'd happily transferSession the empty
|
|
1350
|
+
// shell and report "已就绪:leader" as a lie. Now we detect this,
|
|
1351
|
+
// skip transferSession, mark leader as `no_session`, and close the
|
|
1352
|
+
// scratch so it doesn't linger as a ghost.
|
|
1353
|
+
//
|
|
1354
|
+
// The new chat is still created (createGroupWithBots already ran
|
|
1355
|
+
// above) — that itself is a valuable product outcome since the
|
|
1356
|
+
// mentioned bots were invited. Peers continue through their normal
|
|
1357
|
+
// path; the final M1 template adapts to "all_fresh" when no bot
|
|
1358
|
+
// actually had a session to bring along.
|
|
1359
|
+
const reportLines = [];
|
|
1360
|
+
const leaderName = nameOf(creatorAppId);
|
|
1361
|
+
const successBotNames = [];
|
|
1362
|
+
const failedBotNames = [];
|
|
1363
|
+
// Use the persisted-marker predicate, not runtime ds.hasHistory:
|
|
1364
|
+
// restoreActiveSessions sets hasHistory:true UNCONDITIONALLY on
|
|
1365
|
+
// restart (session-manager.ts:618), so a scratch that survives a
|
|
1366
|
+
// restart comes back with hasHistory:true and would defeat a
|
|
1367
|
+
// naive `!!ds.worker || ds.hasHistory` check. cliId / lastCliInput
|
|
1368
|
+
// are only written after a real worker started the CLI, so they
|
|
1369
|
+
// survive restart correctly.
|
|
1370
|
+
const { isRelayableRealSession } = await import('./worker-pool.js');
|
|
1371
|
+
const leaderHasRealSession = isRelayableRealSession(ds);
|
|
1372
|
+
if (leaderHasRealSession) {
|
|
1373
|
+
const { transferSession } = await import('./worker-pool.js');
|
|
1374
|
+
// Target chat was just built by createGroupWithBots — by
|
|
1375
|
+
// construction a regular group.
|
|
1376
|
+
const leaderResult = await transferSession(ds.session.sessionId, newChatId, placeholderRootMessageId, 'group');
|
|
1377
|
+
if (!leaderResult.ok) {
|
|
1378
|
+
// Real session, real failure (worker busy / unsupported target
|
|
1379
|
+
// / tmux issue). Abort the entire --create flow — the new chat
|
|
1380
|
+
// exists but is empty of any migrated session; we don't post
|
|
1381
|
+
// an M1 because there's nothing to announce.
|
|
1382
|
+
reportLines.push(t('cmd.relay.report_leader_failed', { bot: leaderName, error: leaderResult.error }, loc));
|
|
1383
|
+
await sessionReply(rootId, t('cmd.relay.created', { name: groupName, link: inviteLink, report: reportLines.join('\n') }, loc));
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
reportLines.push(t('cmd.relay.report_leader_ok', { bot: leaderName }, loc));
|
|
1387
|
+
successBotNames.push(leaderName);
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
// Empty leader: no real session to migrate.
|
|
1391
|
+
reportLines.push(t('cmd.relay.report_leader_no_session', { bot: leaderName }, loc));
|
|
1392
|
+
failedBotNames.push(leaderName);
|
|
1393
|
+
// Close the daemon-command scratch so it doesn't linger as a
|
|
1394
|
+
// ghost active row at the source anchor (same hygiene that
|
|
1395
|
+
// transferSession's pre-flight applies to target-chat scratches).
|
|
1396
|
+
const { closeSession } = await import('./worker-pool.js');
|
|
1397
|
+
await closeSession(ds.session.sessionId).catch(err => {
|
|
1398
|
+
logger.warn(`[${logTag}] /relay --create: failed to close empty-leader scratch: ${err instanceof Error ? err.message : err}`);
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
// ── Step 2: coordinate peer daemons (parallel) ─────────────────────
|
|
1402
|
+
const { findOnlineDaemon } = await import('../utils/daemon-discovery.js');
|
|
1403
|
+
const peerAppIds = mentionedBotAppIds.filter(id => id !== creatorAppId);
|
|
1404
|
+
const peerOutcomes = await Promise.all(peerAppIds.map(async (peerAppId) => {
|
|
1405
|
+
const botName = nameOf(peerAppId);
|
|
1406
|
+
const daemon = findOnlineDaemon(peerAppId);
|
|
1407
|
+
if (!daemon)
|
|
1408
|
+
return { peerAppId, botName, status: 'offline' };
|
|
1409
|
+
try {
|
|
1410
|
+
const ctrl = new AbortController();
|
|
1411
|
+
const tt = setTimeout(() => ctrl.abort(), 5000);
|
|
1412
|
+
const res = await fetch(`http://127.0.0.1:${daemon.ipcPort}/api/sessions/migrate-to-chat`, {
|
|
1413
|
+
method: 'POST',
|
|
1414
|
+
headers: { 'content-type': 'application/json' },
|
|
1415
|
+
body: JSON.stringify({
|
|
1416
|
+
sourceAnchor,
|
|
1417
|
+
targetChatId: newChatId,
|
|
1418
|
+
targetRootMessageId: placeholderRootMessageId,
|
|
1419
|
+
requesterLarkAppId: creatorAppId,
|
|
1420
|
+
requestingUserOpenId: senderOpenId,
|
|
1421
|
+
// union_id is cross-app stable within a tenant — peer
|
|
1422
|
+
// compares against its own session.ownerUnionId rather
|
|
1423
|
+
// than translating open_ids per bot. Optional for
|
|
1424
|
+
// backward compat with daemons older than this commit.
|
|
1425
|
+
requestingUserUnionId: senderUnionId,
|
|
1426
|
+
}),
|
|
1427
|
+
signal: ctrl.signal,
|
|
1428
|
+
}).finally(() => clearTimeout(tt));
|
|
1429
|
+
const body = await res.json().catch(() => ({}));
|
|
1430
|
+
if (res.ok && body.ok)
|
|
1431
|
+
return { peerAppId, botName, status: 'ok' };
|
|
1432
|
+
if (body.error === 'no_session_at_anchor')
|
|
1433
|
+
return { peerAppId, botName, status: 'no_session' };
|
|
1434
|
+
if (body.error === 'not_session_owner')
|
|
1435
|
+
return { peerAppId, botName, status: 'not_owner' };
|
|
1436
|
+
if (body.error === 'worker_busy')
|
|
1437
|
+
return { peerAppId, botName, status: 'busy' };
|
|
1438
|
+
return { peerAppId, botName, status: 'failed', error: body.error ?? `http_${res.status}` };
|
|
1439
|
+
}
|
|
1440
|
+
catch (err) {
|
|
1441
|
+
const reason = err?.name === 'AbortError' ? 'busy' : 'failed';
|
|
1442
|
+
return { peerAppId, botName, status: reason, error: err?.message ?? String(err) };
|
|
1443
|
+
}
|
|
1444
|
+
}));
|
|
1445
|
+
// Bucket peer outcomes for the final M1 (success / failure) AND extend the
|
|
1446
|
+
// source-chat report with per-peer detail. Leader was already bucketed
|
|
1447
|
+
// above (real-success → successBotNames; real-fail or empty-leader →
|
|
1448
|
+
// failedBotNames), so we only iterate peers here.
|
|
1449
|
+
for (const r of peerOutcomes) {
|
|
1450
|
+
if (r.status === 'ok') {
|
|
1451
|
+
successBotNames.push(r.botName);
|
|
1452
|
+
reportLines.push(t('cmd.relay.report_peer_ok', { bot: r.botName }, loc));
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
failedBotNames.push(r.botName);
|
|
1456
|
+
switch (r.status) {
|
|
1457
|
+
case 'no_session':
|
|
1458
|
+
reportLines.push(t('cmd.relay.report_peer_no_session', { bot: r.botName }, loc));
|
|
1459
|
+
break;
|
|
1460
|
+
case 'not_owner':
|
|
1461
|
+
reportLines.push(t('cmd.relay.report_peer_not_owner', { bot: r.botName }, loc));
|
|
1462
|
+
break;
|
|
1463
|
+
case 'offline':
|
|
1464
|
+
reportLines.push(t('cmd.relay.report_peer_offline', { bot: r.botName }, loc));
|
|
1465
|
+
break;
|
|
1466
|
+
case 'busy':
|
|
1467
|
+
reportLines.push(t('cmd.relay.report_peer_busy', { bot: r.botName }, loc));
|
|
1468
|
+
break;
|
|
1469
|
+
case 'failed':
|
|
1470
|
+
reportLines.push(t('cmd.relay.report_peer_failed', { bot: r.botName, error: r.error ?? 'unknown' }, loc));
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
// ── Step 3: post the real M1 with status breakdown ─────────────────
|
|
1476
|
+
// Three templates:
|
|
1477
|
+
// - all_ok : every bot migrated cleanly
|
|
1478
|
+
// - partial : some migrated, some didn't (failed list explains)
|
|
1479
|
+
// - all_fresh : nobody had a session to migrate (group's still
|
|
1480
|
+
// useful — bots were invited; user just @s to start)
|
|
1481
|
+
// Pass the raw text — sendMessage wraps `'text'` msgType bodies into
|
|
1482
|
+
// { text: content } itself.
|
|
1483
|
+
let finalM1Text;
|
|
1484
|
+
if (successBotNames.length === 0) {
|
|
1485
|
+
finalM1Text = t('cmd.relay.m1_final_all_fresh', { sourceChat: sourceLabel }, loc);
|
|
1486
|
+
}
|
|
1487
|
+
else if (failedBotNames.length === 0) {
|
|
1488
|
+
finalM1Text = t('cmd.relay.m1_final_all_ok', {
|
|
1489
|
+
sourceChat: sourceLabel,
|
|
1490
|
+
successBots: successBotNames.join('、'),
|
|
1491
|
+
}, loc);
|
|
1492
|
+
}
|
|
1493
|
+
else {
|
|
1494
|
+
finalM1Text = t('cmd.relay.m1_final_partial', {
|
|
1495
|
+
sourceChat: sourceLabel,
|
|
1496
|
+
successBots: successBotNames.join('、'),
|
|
1497
|
+
failedBots: failedBotNames.join('、'),
|
|
1498
|
+
}, loc);
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
const finalM1Id = await sendMessage(creatorAppId, newChatId, finalM1Text, 'text');
|
|
1502
|
+
// Patch the leader's session.rootMessageId to the real M1 id, but
|
|
1503
|
+
// only if the leader was actually transferred — for the empty-
|
|
1504
|
+
// leader / all_fresh path, ds was either closed or never moved,
|
|
1505
|
+
// so we don't touch it (would write to a closed/stale record).
|
|
1506
|
+
if (leaderHasRealSession && successBotNames.includes(leaderName)) {
|
|
1507
|
+
ds.session.rootMessageId = finalM1Id;
|
|
1508
|
+
sessionStore.updateSession(ds.session);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
catch (err) {
|
|
1512
|
+
// Non-fatal: transfers already succeeded. The source-chat report
|
|
1513
|
+
// (sessionReply below) is the user's authoritative status.
|
|
1514
|
+
logger.warn(`[${logTag}] /relay --create: final M1 send failed: ${err?.message ?? err}`);
|
|
1515
|
+
}
|
|
1516
|
+
await sessionReply(rootId, t('cmd.relay.created', { name: groupName, link: inviteLink, report: reportLines.join('\n') }, loc));
|
|
1517
|
+
logger.info(`[${logTag}] /relay --create completed: chat=${newChatId} leader=${creatorAppId} peers=[${peerAppIds.join(',')}]`);
|
|
1518
|
+
break;
|
|
1519
|
+
}
|
|
1520
|
+
case '/card': {
|
|
1521
|
+
if (!ds) {
|
|
1522
|
+
await sessionReply(rootId, t('cmd.no_active_session', undefined, loc));
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
// Private mode (`privateCard`): send a one-shot snapshot only to the
|
|
1526
|
+
// explicit talk-grant audience via the ephemeral API, instead of the
|
|
1527
|
+
// group-visible live card. Ephemeral cards only work in plain `group`
|
|
1528
|
+
// chats and can't be patched — so no live updates, and we fail closed
|
|
1529
|
+
// (never fall back to a group-visible card) since not leaking is the
|
|
1530
|
+
// entire point of this mode.
|
|
1531
|
+
if (getBot(ds.larkAppId).config.privateCard) {
|
|
1532
|
+
// Strict gate: only a *confirmed* plain group is safe — getChatModeStrict
|
|
1533
|
+
// returns 'unknown' on API error instead of guessing 'group', so we fail
|
|
1534
|
+
// closed (no leak) when we can't verify the chat type.
|
|
1535
|
+
const mode = await getChatModeStrict(ds.larkAppId, ds.chatId);
|
|
1536
|
+
if (mode !== 'group') {
|
|
1537
|
+
await sessionReply(rootId, t('cmd.card.private_not_group', undefined, loc));
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
const audience = resolvePrivateCardAudience(ds);
|
|
1541
|
+
if (audience.length === 0) {
|
|
1542
|
+
await sessionReply(rootId, t('cmd.card.private_no_audience', undefined, loc));
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
const r = await postPrivateSnapshotCard(ds, audience);
|
|
1546
|
+
if (r.notReady) {
|
|
1547
|
+
await sessionReply(rootId, t('cmd.card.private_not_ready', undefined, loc));
|
|
1548
|
+
}
|
|
1549
|
+
else if (r.sent === 0) {
|
|
1550
|
+
// Total failure — surface a non-sensitive error (no terminal content,
|
|
1551
|
+
// no open_id list). Most likely cause: missing send permission / bot
|
|
1552
|
+
// not in chat / topic-thread chat.
|
|
1553
|
+
await sessionReply(rootId, t('cmd.card.private_failed', undefined, loc));
|
|
1554
|
+
}
|
|
1555
|
+
else if (r.sent < r.total) {
|
|
1556
|
+
// Partial — report counts only, never the audience identities.
|
|
1557
|
+
await sessionReply(rootId, t('cmd.card.private_partial', { sent: r.sent, total: r.total }, loc));
|
|
1558
|
+
}
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
// Manual summon. Force the live card on for the rest of this session —
|
|
1562
|
+
// even when the bot has `disableStreamingCard` set — then post a fresh
|
|
1563
|
+
// card. If the worker terminal isn't up yet, the force flag still sticks
|
|
1564
|
+
// so the card appears (and live-updates) as soon as the worker is ready.
|
|
1565
|
+
ds.streamingCardForced = true;
|
|
1566
|
+
const posted = await postFreshStreamingCard(ds, deps.sessionReply);
|
|
1567
|
+
if (!posted) {
|
|
1568
|
+
await sessionReply(rootId, t('cmd.card.not_ready', undefined, loc));
|
|
1569
|
+
}
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1095
1572
|
case '/help': {
|
|
1096
1573
|
const botCfg = ds ? getBot(ds.larkAppId).config : getAllBots()[0]?.config;
|
|
1097
1574
|
const cliName = getCliDisplayName(botCfg?.cliId ?? 'claude-code');
|
|
@@ -1102,7 +1579,9 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
1102
1579
|
t('help.cd', { cliName }, loc),
|
|
1103
1580
|
t('help.repo_list', undefined, loc),
|
|
1104
1581
|
t('help.repo_n', undefined, loc),
|
|
1582
|
+
t('help.repo_path', undefined, loc),
|
|
1105
1583
|
t('help.status', undefined, loc),
|
|
1584
|
+
t('help.card', undefined, loc),
|
|
1106
1585
|
'',
|
|
1107
1586
|
t('help.heading_passthrough', { cliName }, loc),
|
|
1108
1587
|
// 直接从集合渲染,保证文案与 PASSTHROUGH_COMMANDS 不漂移
|
|
@@ -1115,10 +1594,6 @@ export async function handleCommand(cmd, rootId, message, deps, larkAppId) {
|
|
|
1115
1594
|
t('help.schedule_toggle', undefined, loc),
|
|
1116
1595
|
t('help.schedule_run', undefined, loc),
|
|
1117
1596
|
'',
|
|
1118
|
-
'任务管理:',
|
|
1119
|
-
'/task new <name> — 登记当前任务',
|
|
1120
|
-
'/task list | /task status <id> | /task assign <id> @Bot | /task close <id>',
|
|
1121
|
-
'',
|
|
1122
1597
|
t('help.schedule_formats', undefined, loc),
|
|
1123
1598
|
'',
|
|
1124
1599
|
t('help.heading_adopt', undefined, loc),
|