botmux 3.7.1 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/capabilities.d.ts +17 -0
- package/dist/adapters/backend/capabilities.d.ts.map +1 -0
- package/dist/adapters/backend/capabilities.js +22 -0
- package/dist/adapters/backend/capabilities.js.map +1 -0
- package/dist/adapters/backend/critical-control-key.d.ts +19 -0
- package/dist/adapters/backend/critical-control-key.d.ts.map +1 -0
- package/dist/adapters/backend/critical-control-key.js +34 -0
- package/dist/adapters/backend/critical-control-key.js.map +1 -0
- package/dist/adapters/backend/herdr-backend.d.ts +9 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +23 -4
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
- package/dist/adapters/backend/riff-backend.js +3 -2
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/session-backend-selector.d.ts +61 -6
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +173 -9
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +10 -3
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +25 -8
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +3 -2
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +45 -4
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js +11 -0
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-backend.js +3 -2
- package/dist/adapters/backend/zellij-backend.js.map +1 -1
- package/dist/adapters/backend/zmx-backend.d.ts +286 -0
- package/dist/adapters/backend/zmx-backend.d.ts.map +1 -0
- package/dist/adapters/backend/zmx-backend.js +2396 -0
- package/dist/adapters/backend/zmx-backend.js.map +1 -0
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +3 -0
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +5 -1
- package/dist/adapters/cli/codex-app.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +26 -1
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/fs-policy.d.ts +79 -1
- package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
- package/dist/adapters/cli/fs-policy.js +290 -13
- package/dist/adapters/cli/fs-policy.js.map +1 -1
- package/dist/adapters/cli/grok.d.ts.map +1 -1
- package/dist/adapters/cli/grok.js +4 -1
- package/dist/adapters/cli/grok.js.map +1 -1
- package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
- package/dist/adapters/cli/oh-my-pi.js +5 -3
- package/dist/adapters/cli/oh-my-pi.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +7 -4
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +52 -35
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/strict-input-handle.d.ts +13 -0
- package/dist/adapters/cli/strict-input-handle.d.ts.map +1 -0
- package/dist/adapters/cli/strict-input-handle.js +39 -0
- package/dist/adapters/cli/strict-input-handle.js.map +1 -0
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +6 -4
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +16 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +137 -8
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +298 -12
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/session-list-liveness.d.ts +1 -1
- package/dist/cli/session-list-liveness.js +1 -1
- package/dist/cli/zmx-managed-attach.d.ts +41 -0
- package/dist/cli/zmx-managed-attach.d.ts.map +1 -0
- package/dist/cli/zmx-managed-attach.js +72 -0
- package/dist/cli/zmx-managed-attach.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +780 -131
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +90 -2
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/codex-rpc-engine.d.ts.map +1 -1
- package/dist/codex-rpc-engine.js +22 -6
- package/dist/codex-rpc-engine.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +29 -1
- package/dist/config.js.map +1 -1
- package/dist/core/ask-broker.d.ts +22 -1
- package/dist/core/ask-broker.d.ts.map +1 -1
- package/dist/core/ask-broker.js +12 -5
- package/dist/core/ask-broker.js.map +1 -1
- package/dist/core/botmux-wrapper.d.ts +18 -0
- package/dist/core/botmux-wrapper.d.ts.map +1 -1
- package/dist/core/botmux-wrapper.js +25 -1
- package/dist/core/botmux-wrapper.js.map +1 -1
- package/dist/core/chat-rename.d.ts +27 -0
- package/dist/core/chat-rename.d.ts.map +1 -0
- package/dist/core/chat-rename.js +53 -0
- package/dist/core/chat-rename.js.map +1 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +160 -12
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts +25 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +102 -4
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +16 -2
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +611 -62
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +22 -1
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +61 -0
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/dashboard-url.d.ts +6 -0
- package/dist/core/dashboard-url.d.ts.map +1 -1
- package/dist/core/dashboard-url.js +11 -1
- package/dist/core/dashboard-url.js.map +1 -1
- package/dist/core/device-isolation-daemon.d.ts +28 -6
- package/dist/core/device-isolation-daemon.d.ts.map +1 -1
- package/dist/core/device-isolation-daemon.js +96 -13
- package/dist/core/device-isolation-daemon.js.map +1 -1
- package/dist/core/host-overload-alert.d.ts +75 -0
- package/dist/core/host-overload-alert.d.ts.map +1 -1
- package/dist/core/host-overload-alert.js +103 -0
- package/dist/core/host-overload-alert.js.map +1 -1
- package/dist/core/idle-worker-sweeper.js +1 -1
- package/dist/core/idle-worker-sweeper.js.map +1 -1
- package/dist/core/inflight-input-tracker.d.ts +3 -0
- package/dist/core/inflight-input-tracker.d.ts.map +1 -1
- package/dist/core/inflight-input-tracker.js +9 -0
- package/dist/core/inflight-input-tracker.js.map +1 -1
- package/dist/core/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +10 -0
- package/dist/core/passthrough-commands.js.map +1 -1
- package/dist/core/persistent-backend.d.ts +32 -4
- package/dist/core/persistent-backend.d.ts.map +1 -1
- package/dist/core/persistent-backend.js +75 -6
- package/dist/core/persistent-backend.js.map +1 -1
- package/dist/core/plugins/mcp/gateway.js +2 -2
- package/dist/core/plugins/mcp/gateway.js.map +1 -1
- package/dist/core/plugins/runtime.d.ts +3 -0
- package/dist/core/plugins/runtime.d.ts.map +1 -1
- package/dist/core/plugins/runtime.js.map +1 -1
- package/dist/core/plugins/service-manager.d.ts +4 -1
- package/dist/core/plugins/service-manager.d.ts.map +1 -1
- package/dist/core/plugins/service-manager.js +11 -6
- package/dist/core/plugins/service-manager.js.map +1 -1
- package/dist/core/role-library.d.ts +48 -1
- package/dist/core/role-library.d.ts.map +1 -1
- package/dist/core/role-library.js +111 -3
- package/dist/core/role-library.js.map +1 -1
- package/dist/core/role-resolver.d.ts +14 -2
- package/dist/core/role-resolver.d.ts.map +1 -1
- package/dist/core/role-resolver.js +47 -4
- package/dist/core/role-resolver.js.map +1 -1
- package/dist/core/session-activity.d.ts +14 -0
- package/dist/core/session-activity.d.ts.map +1 -1
- package/dist/core/session-activity.js +46 -1
- package/dist/core/session-activity.js.map +1 -1
- package/dist/core/session-board.d.ts +5 -2
- package/dist/core/session-board.d.ts.map +1 -1
- package/dist/core/session-board.js +5 -2
- package/dist/core/session-board.js.map +1 -1
- package/dist/core/session-liveness.d.ts +6 -5
- package/dist/core/session-liveness.d.ts.map +1 -1
- package/dist/core/session-liveness.js +34 -13
- package/dist/core/session-liveness.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +397 -103
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-marker.d.ts +11 -2
- package/dist/core/session-marker.d.ts.map +1 -1
- package/dist/core/session-marker.js +50 -11
- package/dist/core/session-marker.js.map +1 -1
- package/dist/core/session-message-preview.d.ts +20 -0
- package/dist/core/session-message-preview.d.ts.map +1 -0
- package/dist/core/session-message-preview.js +188 -0
- package/dist/core/session-message-preview.js.map +1 -0
- package/dist/core/session-title.d.ts +5 -1
- package/dist/core/session-title.d.ts.map +1 -1
- package/dist/core/session-title.js +20 -3
- package/dist/core/session-title.js.map +1 -1
- package/dist/core/startup-commands.d.ts +1 -1
- package/dist/core/startup-commands.js +1 -1
- package/dist/core/terminal-url.d.ts.map +1 -1
- package/dist/core/terminal-url.js +3 -2
- package/dist/core/terminal-url.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +75 -12
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +35 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +23 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-ipc.d.ts +11 -0
- package/dist/core/worker-ipc.d.ts.map +1 -0
- package/dist/core/worker-ipc.js +26 -0
- package/dist/core/worker-ipc.js.map +1 -0
- package/dist/core/worker-pool.d.ts +179 -29
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +2118 -331
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +132 -3
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +1327 -449
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/auth.d.ts +2 -0
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +9 -0
- package/dist/dashboard/auth.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +4 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +5 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/federation-api.d.ts.map +1 -1
- package/dist/dashboard/federation-api.js +4 -0
- package/dist/dashboard/federation-api.js.map +1 -1
- package/dist/dashboard/federation-spoke-api.d.ts.map +1 -1
- package/dist/dashboard/federation-spoke-api.js +32 -5
- package/dist/dashboard/federation-spoke-api.js.map +1 -1
- package/dist/dashboard/public-redact.d.ts +8 -2
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +34 -7
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/session-card-model.d.ts.map +1 -1
- package/dist/dashboard/session-card-model.js +10 -2
- package/dist/dashboard/session-card-model.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +35 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +88 -1
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +0 -1
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +15 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +212 -58
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +3 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts +2 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/clipboard.d.ts +2 -0
- package/dist/dashboard/web/clipboard.d.ts.map +1 -0
- package/dist/dashboard/web/clipboard.js +42 -0
- package/dist/dashboard/web/clipboard.js.map +1 -0
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +12 -8
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/dashboard-components.d.ts +17 -16
- package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-components.js.map +1 -1
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +7 -3
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +205 -12
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/icon-tooltip.d.ts.map +1 -1
- package/dist/dashboard/web/icon-tooltip.js +16 -1
- package/dist/dashboard/web/icon-tooltip.js.map +1 -1
- package/dist/dashboard/web/listener-filters.d.ts +23 -0
- package/dist/dashboard/web/listener-filters.d.ts.map +1 -0
- package/dist/dashboard/web/listener-filters.js +36 -0
- package/dist/dashboard/web/listener-filters.js.map +1 -0
- package/dist/dashboard/web/overview-page.d.ts.map +1 -1
- package/dist/dashboard/web/overview-page.js.map +1 -1
- package/dist/dashboard/web/plugin-page.d.ts.map +1 -1
- package/dist/dashboard/web/plugin-page.js.map +1 -1
- package/dist/dashboard/web/preview-markdown.d.ts +5 -0
- package/dist/dashboard/web/preview-markdown.d.ts.map +1 -0
- package/dist/dashboard/web/preview-markdown.js +52 -0
- package/dist/dashboard/web/preview-markdown.js.map +1 -0
- package/dist/dashboard/web/roles-page.d.ts.map +1 -1
- package/dist/dashboard/web/roles-page.js +535 -10
- package/dist/dashboard/web/roles-page.js.map +1 -1
- package/dist/dashboard/web/roles.d.ts +77 -0
- package/dist/dashboard/web/roles.d.ts.map +1 -1
- package/dist/dashboard/web/roles.js +98 -0
- package/dist/dashboard/web/roles.js.map +1 -1
- package/dist/dashboard/web/session-terminal.d.ts.map +1 -1
- package/dist/dashboard/web/session-terminal.js +6 -4
- package/dist/dashboard/web/session-terminal.js.map +1 -1
- package/dist/dashboard/web/sessions-kanban.d.ts +4 -1
- package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-kanban.js +1 -1
- package/dist/dashboard/web/sessions-kanban.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts +3 -2
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +163 -28
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +25 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +58 -7
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +28 -3
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +61 -1
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/skills-page.d.ts +6 -5
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +2 -0
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/web/v3-components.d.ts +3 -2
- package/dist/dashboard/web/v3-components.d.ts.map +1 -1
- package/dist/dashboard/web/v3-components.js.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts +12 -2
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.js +0 -9
- package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
- package/dist/dashboard/web/webhook-logs-page.d.ts +2 -1
- package/dist/dashboard/web/webhook-logs-page.d.ts.map +1 -1
- package/dist/dashboard/web/webhook-logs-page.js +4 -1
- package/dist/dashboard/web/webhook-logs-page.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-FAPIMQV3.js +10 -0
- package/dist/dashboard-web/chunks/{chunk-QFMH6ITB.js → chunk-5CJMHTT4.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-KQB42ML5.js → chunk-DLZK52JU.js} +14 -12
- package/dist/dashboard-web/chunks/chunk-FDZ7WOBW.js +16 -0
- package/dist/dashboard-web/chunks/{chunk-3QAUJXAS.js → chunk-GTAJV5YC.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-FNPGOZMX.js → chunk-IHNG6KE6.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-FLGXZJTD.js → chunk-VA6QWSGQ.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-XJ7BGPJH.js +1 -0
- package/dist/dashboard-web/chunks/chunk-ZJVTGU5X.js +1 -0
- package/dist/dashboard-web/chunks/connectors-page-LKWRT3GS.js +1 -0
- package/dist/dashboard-web/chunks/groups-page-V6654ICQ.js +8 -0
- package/dist/dashboard-web/chunks/insights-page-237RMFQ5.js +7 -0
- package/dist/dashboard-web/chunks/{monitor-room-7IVIS75W.js → monitor-room-OO5Y4FRQ.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-5SSJRQZF.js → monitoring-page-OUEITM5P.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-EOND3L5D.js → office-page-4KNAHQ4Q.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-NISBVBBJ.js → overview-page-QDP4YK2S.js} +1 -1
- package/dist/dashboard-web/chunks/{plugin-page-JXC5F5YV.js → plugin-page-VEGTCEAS.js} +1 -1
- package/dist/dashboard-web/chunks/roles-page-PGXNZZNP.js +1 -0
- package/dist/dashboard-web/chunks/{schedules-page-AVOOKLUH.js → schedules-page-BJNC5TID.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-M2ZYYCMO.js +6 -0
- package/dist/dashboard-web/chunks/settings-page-IJFZKUUW.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-5MW54PCC.js → skills-page-YRAIRZOQ.js} +3 -3
- package/dist/dashboard-web/chunks/{team-federation-page-2SNIMVXI.js → team-federation-page-DEU27GRU.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-W6SBXXQC.js → v3-page-6KHEW2YL.js} +5 -5
- package/dist/dashboard-web/chunks/{whiteboards-page-STC54KTP.js → whiteboards-page-VDECAALH.js} +1 -1
- package/dist/dashboard-web/style.css +12545 -11432
- package/dist/dashboard.js +215 -36
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +41 -2
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +47 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +71 -19
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +71 -19
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/callback-button-marker.d.ts +34 -0
- package/dist/im/lark/callback-button-marker.d.ts.map +1 -0
- package/dist/im/lark/callback-button-marker.js +99 -0
- package/dist/im/lark/callback-button-marker.js.map +1 -0
- package/dist/im/lark/card-builder.d.ts +46 -10
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +299 -78
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +3 -4
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +380 -148
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +24 -1
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +101 -8
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/doc-comment.d.ts.map +1 -1
- package/dist/im/lark/doc-comment.js +6 -1
- package/dist/im/lark/doc-comment.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +115 -3
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +534 -147
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts +16 -12
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +48 -151
- package/dist/im/lark/grant-command.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts +12 -4
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +31 -8
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/im/lark/invite-command.d.ts +20 -0
- package/dist/im/lark/invite-command.d.ts.map +1 -0
- package/dist/im/lark/invite-command.js +239 -0
- package/dist/im/lark/invite-command.js.map +1 -0
- package/dist/im/lark/md-card.d.ts +66 -4
- package/dist/im/lark/md-card.d.ts.map +1 -1
- package/dist/im/lark/md-card.js +201 -31
- package/dist/im/lark/md-card.js.map +1 -1
- package/dist/im/lark/mention-targets.d.ts +43 -0
- package/dist/im/lark/mention-targets.d.ts.map +1 -0
- package/dist/im/lark/mention-targets.js +190 -0
- package/dist/im/lark/mention-targets.js.map +1 -0
- package/dist/im/lark/message-parser.d.ts +31 -0
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +305 -32
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/reply-card-footer-signature.d.ts +7 -0
- package/dist/im/lark/reply-card-footer-signature.d.ts.map +1 -0
- package/dist/im/lark/reply-card-footer-signature.js +7 -0
- package/dist/im/lark/reply-card-footer-signature.js.map +1 -0
- package/dist/im/lark/sessions-card.d.ts.map +1 -1
- package/dist/im/lark/sessions-card.js +4 -1
- package/dist/im/lark/sessions-card.js.map +1 -1
- package/dist/im/lark/substitute-command.d.ts.map +1 -1
- package/dist/im/lark/substitute-command.js +8 -0
- package/dist/im/lark/substitute-command.js.map +1 -1
- package/dist/im/lark/topic-root-context.d.ts +35 -0
- package/dist/im/lark/topic-root-context.d.ts.map +1 -0
- package/dist/im/lark/topic-root-context.js +37 -0
- package/dist/im/lark/topic-root-context.js.map +1 -0
- package/dist/im/lark/v3-blocked-card.d.ts.map +1 -1
- package/dist/im/lark/v3-blocked-card.js +2 -1
- package/dist/im/lark/v3-blocked-card.js.map +1 -1
- package/dist/im/lark/v3-gate-card.d.ts.map +1 -1
- package/dist/im/lark/v3-gate-card.js +2 -1
- package/dist/im/lark/v3-gate-card.js.map +1 -1
- package/dist/im/lark/v3-loop-grant-card.d.ts.map +1 -1
- package/dist/im/lark/v3-loop-grant-card.js +2 -1
- package/dist/im/lark/v3-loop-grant-card.js.map +1 -1
- package/dist/im/lark/v3-progress-card.d.ts.map +1 -1
- package/dist/im/lark/v3-progress-card.js +2 -1
- package/dist/im/lark/v3-progress-card.js.map +1 -1
- package/dist/im/lark/v3-revisit-grant-card.d.ts.map +1 -1
- package/dist/im/lark/v3-revisit-grant-card.js +2 -1
- package/dist/im/lark/v3-revisit-grant-card.js.map +1 -1
- package/dist/index-core-only.d.ts +3 -0
- package/dist/index-core-only.d.ts.map +1 -0
- package/dist/index-core-only.js +107 -0
- package/dist/index-core-only.js.map +1 -0
- package/dist/services/async-trigger-state.d.ts +8 -0
- package/dist/services/async-trigger-state.d.ts.map +1 -1
- package/dist/services/async-trigger-state.js +3 -2
- package/dist/services/async-trigger-state.js.map +1 -1
- package/dist/services/async-trigger-store.d.ts +10 -2
- package/dist/services/async-trigger-store.d.ts.map +1 -1
- package/dist/services/async-trigger-store.js +3 -2
- package/dist/services/async-trigger-store.js.map +1 -1
- package/dist/services/backend-availability.d.ts +3 -1
- package/dist/services/backend-availability.d.ts.map +1 -1
- package/dist/services/backend-availability.js +14 -1
- package/dist/services/backend-availability.js.map +1 -1
- package/dist/services/backend-type-store.d.ts.map +1 -1
- package/dist/services/backend-type-store.js +1 -1
- package/dist/services/backend-type-store.js.map +1 -1
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +2 -1
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/bridge-fallback-gate.d.ts +14 -1
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
- package/dist/services/bridge-fallback-gate.js +65 -1
- package/dist/services/bridge-fallback-gate.js.map +1 -1
- package/dist/services/card-prefs-store.d.ts +7 -2
- package/dist/services/card-prefs-store.d.ts.map +1 -1
- package/dist/services/card-prefs-store.js +30 -4
- package/dist/services/card-prefs-store.js.map +1 -1
- package/dist/services/codex-app-runner-protocol.d.ts +9 -0
- package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
- package/dist/services/codex-app-runner-protocol.js +17 -0
- package/dist/services/codex-app-runner-protocol.js.map +1 -1
- package/dist/services/codex-app-token-usage.d.ts +93 -0
- package/dist/services/codex-app-token-usage.d.ts.map +1 -0
- package/dist/services/codex-app-token-usage.js +190 -0
- package/dist/services/codex-app-token-usage.js.map +1 -0
- package/dist/services/codex-transcript.d.ts +6 -1
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js +103 -32
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/services/federation-roster.d.ts +4 -0
- package/dist/services/federation-roster.d.ts.map +1 -1
- package/dist/services/federation-roster.js +3 -0
- package/dist/services/federation-roster.js.map +1 -1
- package/dist/services/federation-store.d.ts +6 -0
- package/dist/services/federation-store.d.ts.map +1 -1
- package/dist/services/federation-store.js.map +1 -1
- package/dist/services/grant-policy.d.ts +13 -0
- package/dist/services/grant-policy.d.ts.map +1 -0
- package/dist/services/grant-policy.js +40 -0
- package/dist/services/grant-policy.js.map +1 -0
- package/dist/services/grant-store.d.ts +24 -3
- package/dist/services/grant-store.d.ts.map +1 -1
- package/dist/services/grant-store.js +196 -12
- package/dist/services/grant-store.js.map +1 -1
- package/dist/services/groups-store.d.ts +36 -0
- package/dist/services/groups-store.d.ts.map +1 -1
- package/dist/services/groups-store.js +123 -1
- package/dist/services/groups-store.js.map +1 -1
- package/dist/services/jsonl-cursor.d.ts.map +1 -1
- package/dist/services/jsonl-cursor.js +8 -2
- package/dist/services/jsonl-cursor.js.map +1 -1
- package/dist/services/local-cli-opener.d.ts.map +1 -1
- package/dist/services/local-cli-opener.js +32 -0
- package/dist/services/local-cli-opener.js.map +1 -1
- package/dist/services/message-listener-run-preview-store.d.ts +43 -0
- package/dist/services/message-listener-run-preview-store.d.ts.map +1 -0
- package/dist/services/message-listener-run-preview-store.js +152 -0
- package/dist/services/message-listener-run-preview-store.js.map +1 -0
- package/dist/services/message-listener-store.d.ts +26 -0
- package/dist/services/message-listener-store.d.ts.map +1 -0
- package/dist/services/message-listener-store.js +138 -0
- package/dist/services/message-listener-store.js.map +1 -0
- package/dist/services/message-listener.d.ts +112 -0
- package/dist/services/message-listener.d.ts.map +1 -0
- package/dist/services/message-listener.js +391 -0
- package/dist/services/message-listener.js.map +1 -0
- package/dist/services/overload-alert-migration.d.ts +15 -0
- package/dist/services/overload-alert-migration.d.ts.map +1 -0
- package/dist/services/overload-alert-migration.js +115 -0
- package/dist/services/overload-alert-migration.js.map +1 -0
- package/dist/services/send-policy.d.ts +38 -0
- package/dist/services/send-policy.d.ts.map +1 -1
- package/dist/services/send-policy.js +34 -0
- package/dist/services/send-policy.js.map +1 -1
- package/dist/services/session-store.d.ts +12 -1
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +44 -2
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +87 -12
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/substitute-mode-normalize.d.ts +3 -0
- package/dist/services/substitute-mode-normalize.d.ts.map +1 -1
- package/dist/services/substitute-mode-normalize.js +8 -0
- package/dist/services/substitute-mode-normalize.js.map +1 -1
- package/dist/services/substitute-mode-store.d.ts.map +1 -1
- package/dist/services/substitute-mode-store.js +8 -1
- package/dist/services/substitute-mode-store.js.map +1 -1
- package/dist/services/transcript-resolver.d.ts +6 -1
- package/dist/services/transcript-resolver.d.ts.map +1 -1
- package/dist/services/transcript-resolver.js +145 -29
- package/dist/services/transcript-resolver.js.map +1 -1
- package/dist/services/trigger-types.d.ts +16 -0
- package/dist/services/trigger-types.d.ts.map +1 -1
- package/dist/services/trigger-types.js +6 -0
- package/dist/services/trigger-types.js.map +1 -1
- package/dist/setup/bot-config-editor.js +2 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-zmx.d.ts +31 -0
- package/dist/setup/ensure-zmx.d.ts.map +1 -0
- package/dist/setup/ensure-zmx.js +161 -0
- package/dist/setup/ensure-zmx.js.map +1 -0
- package/dist/setup/index.d.ts +1 -1
- package/dist/setup/index.js +1 -1
- package/dist/setup/open-platform-automation.d.ts +9 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +15 -2
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/setup/setup-args.d.ts +1 -1
- package/dist/setup/setup-args.d.ts.map +1 -1
- package/dist/setup/setup-args.js +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +32 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/injection-mode.d.ts +6 -2
- package/dist/skills/injection-mode.d.ts.map +1 -1
- package/dist/skills/injection-mode.js +10 -5
- package/dist/skills/injection-mode.js.map +1 -1
- package/dist/types.d.ts +79 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/child-env.d.ts +1 -1
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +4 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/effort-confirm-dialog.d.ts +40 -0
- package/dist/utils/effort-confirm-dialog.d.ts.map +1 -0
- package/dist/utils/effort-confirm-dialog.js +74 -0
- package/dist/utils/effort-confirm-dialog.js.map +1 -0
- package/dist/utils/pending-input-queue.d.ts +56 -0
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js +80 -0
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/utils/render-dimensions.d.ts +8 -0
- package/dist/utils/render-dimensions.d.ts.map +1 -1
- package/dist/utils/render-dimensions.js +10 -0
- package/dist/utils/render-dimensions.js.map +1 -1
- package/dist/utils/stuck-key-guard.d.ts +3 -2
- package/dist/utils/stuck-key-guard.d.ts.map +1 -1
- package/dist/utils/stuck-key-guard.js +3 -3
- package/dist/utils/stuck-key-guard.js.map +1 -1
- package/dist/utils/terminal-renderer.d.ts +6 -0
- package/dist/utils/terminal-renderer.d.ts.map +1 -1
- package/dist/utils/terminal-renderer.js +15 -0
- package/dist/utils/terminal-renderer.js.map +1 -1
- package/dist/utils/tui-input-delivery.d.ts +32 -0
- package/dist/utils/tui-input-delivery.d.ts.map +1 -0
- package/dist/utils/tui-input-delivery.js +53 -0
- package/dist/utils/tui-input-delivery.js.map +1 -0
- package/dist/utils/xml.d.ts +19 -0
- package/dist/utils/xml.d.ts.map +1 -0
- package/dist/utils/xml.js +26 -0
- package/dist/utils/xml.js.map +1 -0
- package/dist/worker.js +1852 -374
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +8 -8
- package/dist/workflows/events/payloads.d.ts +8 -8
- package/dist/workflows/events/schema.d.ts +160 -160
- package/dist/workflows/v3/session-relay-client.js +1 -1
- package/dist/workflows/v3/session-relay-client.js.map +1 -1
- package/dist/workflows/v3/worker-fence.d.ts.map +1 -1
- package/dist/workflows/v3/worker-fence.js +15 -10
- package/dist/workflows/v3/worker-fence.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-UKA7K4JV.js +0 -10
- package/dist/dashboard-web/chunks/chunk-DFLNXQNT.js +0 -1
- package/dist/dashboard-web/chunks/connectors-page-A77YAB5Y.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-RUZNVXQB.js +0 -8
- package/dist/dashboard-web/chunks/insights-page-RF7U7RTA.js +0 -22
- package/dist/dashboard-web/chunks/roles-page-BUMKD7Y4.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-6HLSEAB7.js +0 -2
- package/dist/dashboard-web/chunks/settings-page-MJDZHQ4X.js +0 -2
|
@@ -10,13 +10,13 @@ import { atomicWriteFileSync } from '../../utils/atomic-write.js';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { getBot, getAllBots, findOncallChat, getOwnerOpenId } from '../../bot-registry.js';
|
|
12
12
|
import { config, isVcMeetingAgentGloballyEnabled, vcMeetingAgentGlobalListenerBotAppId } from '../../config.js';
|
|
13
|
-
import { getChatInfo, getChatMode, getCachedChatMode, resolveSiblingBotBySenderOpenId, replyMessage, sendMessage, sendUserMessage, isHumanOpenId, updateMessage } from './client.js';
|
|
13
|
+
import { getChatInfo, getChatMode, getCachedChatMode, getUserProfile, listChatMessagesUntil, resolveSiblingBotBySenderOpenId, replyMessage, sendMessage, sendUserMessage, isHumanOpenId, updateMessage } from './client.js';
|
|
14
14
|
import { logger } from '../../utils/logger.js';
|
|
15
15
|
import { BoundedMap } from '../../utils/bounded-map.js';
|
|
16
16
|
import { serializeByAnchor } from '../../utils/anchor-serializer.js';
|
|
17
17
|
import { parseForceTopicInvocation } from '../../core/command-handler.js';
|
|
18
18
|
import { shouldAutoStartOnNewTopic } from '../../core/auto-start.js';
|
|
19
|
-
import { stripLeadingMentions, mentionOpenId, extractMentionIdentities } from './message-parser.js';
|
|
19
|
+
import { stripLeadingMentions, mentionOpenId, mentionAppId, extractMentionIdentities, messageMentionsBot } from './message-parser.js';
|
|
20
20
|
import { recordObservedBots, listObservedBots } from '../../services/observed-bots-store.js';
|
|
21
21
|
import { isTeamBot, recordTeamBot } from '../../services/team-bots-store.js';
|
|
22
22
|
import { isTeamGroupChat } from '../../services/team-groups-store.js';
|
|
@@ -28,12 +28,13 @@ import { BOTMUX_REQUIRED_SCOPES, DOC_FEATURE_SCOPES, DOC_WATCH_SCOPES, DOC_COMME
|
|
|
28
28
|
import { automateOpenPlatformSetup } from '../../setup/open-platform-automation.js';
|
|
29
29
|
import { larkHosts, normalizeBrand, sdkDomain } from './lark-hosts.js';
|
|
30
30
|
import { tryHandleGrantCommand } from './grant-command.js';
|
|
31
|
+
import { tryHandleInviteCommand } from './invite-command.js';
|
|
31
32
|
import { tryHandleReplyModeCommand } from './reply-mode-command.js';
|
|
32
33
|
import { tryHandleSubstituteCommand } from './substitute-command.js';
|
|
33
34
|
import { buildGrantCard } from './card-builder.js';
|
|
34
35
|
import { openPending, isThrottled, clearPending } from './grant-pending.js';
|
|
35
36
|
import { localeForBot, t } from '../../i18n/index.js';
|
|
36
|
-
import { chatQuotaKey, globalQuotaKey } from '../../services/grant-store.js';
|
|
37
|
+
import { chatQuotaKey, getGrantExpiresAt, globalQuotaKey, removeExpiredGrant, } from '../../services/grant-store.js';
|
|
37
38
|
import { ForwardFollowupBuffer } from './forward-followup-buffer.js';
|
|
38
39
|
import { listForwardFollowups, putForwardFollowup, removeForwardFollowup } from './forward-followup-store.js';
|
|
39
40
|
import { claimMessageOnce, _resetCacheForTest as _resetSeenMessagesForTest } from '../../services/seen-message-store.js';
|
|
@@ -42,7 +43,9 @@ import { resolveRegularGroupMode, resolveGroupMentionMode } from '../../services
|
|
|
42
43
|
import { buildSummaryCommandPrompt } from './summary-command.js';
|
|
43
44
|
import { DEFAULT_SUMMARY_PROMPT, summaryRangeFromBotConfig } from '../../services/summary-range-store.js';
|
|
44
45
|
import { isSubstituteEnabledForChat } from '../../services/substitute-chat-toggle-store.js';
|
|
46
|
+
import { evaluateMessageListener, resolveListenerSenderIdentity, buildListenerBotAppIdToOpenId, collectListenerBotAppIds } from '../../services/message-listener.js';
|
|
45
47
|
import { parseVcMeetingPushEvent, VC_BOT_MEETING_ACTIVITY_EVENT, VC_BOT_MEETING_ENDED_EVENT, VC_BOT_MEETING_INVITED_EVENT, VC_PARTICIPANT_MEETING_JOINED_EVENT, } from '../../vc-agent/push-source.js';
|
|
48
|
+
import { DEFAULT_GRANT_DURATION_MS, DEFAULT_GRANT_QUOTA } from '../../services/grant-policy.js';
|
|
46
49
|
// 大厅回执互教的防环闸:每进程对同一打卡者只回一次(见 hall swallow 分支)。
|
|
47
50
|
const hallEchoReplied = new Set();
|
|
48
51
|
function vcMeetingEventPayloadForLog(data) {
|
|
@@ -674,6 +677,24 @@ export async function getGroupStats(larkAppId, chatId) {
|
|
|
674
677
|
return { userCount: 999, botCount: 999 };
|
|
675
678
|
}
|
|
676
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Drop the cached group stats for one chat — called when a membership-change
|
|
682
|
+
* event that is VISIBLE to this app arrives (user add/delete broadcast, or our
|
|
683
|
+
* own bot being added/removed), so the 1v1 relax gate sees fresh counts on the
|
|
684
|
+
* very next message instead of coasting on stale numbers for up to
|
|
685
|
+
* CHAT_CACHE_TTL. TTL 继续保留,作为事件未订阅(老应用)时的兜底。
|
|
686
|
+
* 注意跨进程边界:「别的 bot 进群/离群」事件只推给当事 bot 自己的 app,
|
|
687
|
+
* 一 bot 一 daemon 部署下此函数够不到兄弟进程的缓存(见 handler 处注释)。
|
|
688
|
+
*/
|
|
689
|
+
export function invalidateChatStats(larkAppId, chatId) {
|
|
690
|
+
if (chatStatsCache.delete(`${larkAppId}:${chatId}`)) {
|
|
691
|
+
logger.debug(`[group-stats] invalidated cached stats for ${chatId} (${larkAppId}): membership changed`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/** Test-only: clear the group-stats cache between cases. */
|
|
695
|
+
export function __resetChatStatsForTest() {
|
|
696
|
+
chatStatsCache.clear();
|
|
697
|
+
}
|
|
677
698
|
// ─── Cross-bot open_id mapping ──────────────────────────────────────────
|
|
678
699
|
//
|
|
679
700
|
// Lark open_id is per-app scoped: Bot A sees a different open_id for Bot B
|
|
@@ -730,6 +751,10 @@ export function isKnownPeerBot(dataDir, larkAppId, senderOpenId) {
|
|
|
730
751
|
* union_id roster)与 isTeamBot(团队群里学来的 union_id)都是 union_id 锚定的
|
|
731
752
|
* bot 专属信任;平台拉的团队群则经 platform: 前缀镜像进 team-groups,走同一个
|
|
732
753
|
* isTeamGroupChat。两条路都算团队模式,都不需要 /grant。
|
|
754
|
+
*
|
|
755
|
+
* 调用方只剩 {@link evaluateBotTalk} 一个(bot 闸门的唯一入口);(1) 的两条腿与
|
|
756
|
+
* evaluateTalk 的 teamBot 腿重合,真正额外放行的是 (2)。保留本谓词是因为它是
|
|
757
|
+
* 「bot 团队信任」的定义所在,且有专属单测(team-peer-trust / platform-team-trust)。
|
|
733
758
|
*/
|
|
734
759
|
export function isTrustedTeamBotSender(dataDir, chatId, senderUnionId) {
|
|
735
760
|
return isTeamBot(dataDir, senderUnionId)
|
|
@@ -889,40 +914,17 @@ export function isBotMentioned(larkAppId, message, _senderOpenId) {
|
|
|
889
914
|
logger.debug(`[${larkAppId}] Bot open_id not yet known, skipping @mention check`);
|
|
890
915
|
return false;
|
|
891
916
|
}
|
|
892
|
-
// 1. Check message.mentions array (populated for user-sent text messages).
|
|
893
|
-
// mentionOpenId() tolerates both the WS event object shape and the REST
|
|
894
|
-
// string shape, so a Lark mention-shape change can't silently break
|
|
895
|
-
// @-detection (which would make the bot ignore every @ with no error).
|
|
896
|
-
const mentions = message.mentions ?? [];
|
|
897
917
|
// Early-warning: today's WS events carry mention.id as an object; the REST
|
|
898
918
|
// API carries a bare string. A string-form id on the event path means Lark
|
|
899
919
|
// has converged the event onto the REST shape — surface it (once) so a silent
|
|
900
|
-
// @-routing regression announces itself even though
|
|
920
|
+
// @-routing regression announces itself even though the shared gate absorbs it.
|
|
921
|
+
const mentions = message?.mentions ?? [];
|
|
901
922
|
if (!warnedStringMentionShape && mentions.some((m) => typeof m?.id === 'string')) {
|
|
902
923
|
warnedStringMentionShape = true;
|
|
903
924
|
logger.warn(`[${larkAppId}] mention.id arrived in string form on the event path — Lark may have converged the WS event onto the REST shape. mentionOpenId() absorbs it, but verify group @-routing. (logged once per process)`);
|
|
904
925
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}
|
|
908
|
-
// 2. Check post content for inline at tags (bot-sent post messages may not
|
|
909
|
-
// populate message.mentions — the @mention is embedded in the content structure)
|
|
910
|
-
try {
|
|
911
|
-
const content = JSON.parse(message.content ?? '{}');
|
|
912
|
-
const inner = content.zh_cn ?? content.en_us ?? content;
|
|
913
|
-
if (Array.isArray(inner?.content)) {
|
|
914
|
-
for (const paragraph of inner.content) {
|
|
915
|
-
if (!Array.isArray(paragraph))
|
|
916
|
-
continue;
|
|
917
|
-
for (const node of paragraph) {
|
|
918
|
-
if (node.tag === 'at' && node.user_id === botOpenId)
|
|
919
|
-
return true;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
catch { /* ignore parse errors */ }
|
|
925
|
-
return false;
|
|
926
|
+
// Single source of truth shared with the poll + dashboard-preview legs.
|
|
927
|
+
return messageMentionsBot(message, larkAppId, botOpenId);
|
|
926
928
|
}
|
|
927
929
|
/** Does this message @mention a *specific other member* (a person or bot that
|
|
928
930
|
* is NOT this bot)? Used by the 'ambient' mention policy to decide whether to
|
|
@@ -1011,7 +1013,15 @@ export function resolveSubstituteTrigger(larkAppId, message) {
|
|
|
1011
1013
|
}
|
|
1012
1014
|
return undefined;
|
|
1013
1015
|
}
|
|
1016
|
+
function isSubstituteExcludedChat(cfg, chatId) {
|
|
1017
|
+
return cfg?.excludedChats?.includes(chatId) ?? false;
|
|
1018
|
+
}
|
|
1014
1019
|
function isSubstituteAllowedChat(cfg, chatId) {
|
|
1020
|
+
// 黑名单先判且 deny-wins:命中即整段替身触发块短路(连带跳过其后的运行态
|
|
1021
|
+
// 开关 isSubstituteEnabledForChat),因此配置黑名单 = 硬关闭,/substitute on
|
|
1022
|
+
// 也翻不回来。对普通群与话题群一视同仁,与白名单同层。
|
|
1023
|
+
if (isSubstituteExcludedChat(cfg, chatId))
|
|
1024
|
+
return false;
|
|
1015
1025
|
if (!cfg?.chats?.length)
|
|
1016
1026
|
return true;
|
|
1017
1027
|
return cfg.chats.includes(chatId);
|
|
@@ -1027,29 +1037,29 @@ function mentionMatchesBot(m, larkAppId, botOpenId) {
|
|
|
1027
1037
|
const appId = mentionAppId(m);
|
|
1028
1038
|
return Boolean(larkAppId && appId === larkAppId);
|
|
1029
1039
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1040
|
+
/**
|
|
1041
|
+
* 全部 talk 放行源的**运行时**枚举(TalkReason 由它派生)。
|
|
1042
|
+
*
|
|
1043
|
+
* 之所以是数组而不是纯 type:`test/bot-talk-parity.test.ts` 按它逐条核对
|
|
1044
|
+
* 「这条腿对人和对 bot 分别是什么行为」,少一格就红。tsconfig 的 include 只有
|
|
1045
|
+
* `src/**`,测试不过 tsc——纯类型层的穷尽约束在这里执行不了,必须落到运行时。
|
|
1046
|
+
*
|
|
1047
|
+
* 新增一条 talk 源时:改 evaluateTalk(人/bot 同时生效)+ 在那张表补一格说明,
|
|
1048
|
+
* **不要**去 bot 路由闸里加 OR 腿(那正是 allowedChatGroups 漏掉的老路)。
|
|
1049
|
+
*/
|
|
1050
|
+
export const TALK_REASONS = [
|
|
1051
|
+
'allowedUser',
|
|
1052
|
+
'oncall',
|
|
1053
|
+
'peer',
|
|
1054
|
+
'teamBot',
|
|
1055
|
+
'teamMember',
|
|
1056
|
+
'allowedChatGroup',
|
|
1057
|
+
'open',
|
|
1058
|
+
'chatGrant',
|
|
1059
|
+
'globalGrant',
|
|
1060
|
+
'p2pOpen',
|
|
1061
|
+
'none',
|
|
1062
|
+
];
|
|
1053
1063
|
export function grantCommandRestriction(larkAppId, chatId, senderOpenId) {
|
|
1054
1064
|
const bot = getBot(larkAppId);
|
|
1055
1065
|
if (bot.config.restrictGrantCommands !== true)
|
|
@@ -1062,11 +1072,42 @@ export function grantCommandRestriction(larkAppId, chatId, senderOpenId) {
|
|
|
1062
1072
|
}
|
|
1063
1073
|
/** per-chat per-user 授权命中判断(仅用于 canTalk —— 不给管理命令权)。 */
|
|
1064
1074
|
function hasChatGrant(larkAppId, chatId, openId) {
|
|
1065
|
-
|
|
1075
|
+
if (!chatId || !openId || !getBot(larkAppId).config.chatGrants?.[chatId]?.includes(openId))
|
|
1076
|
+
return false;
|
|
1077
|
+
return grantNotExpired(larkAppId, 'chat', chatId, openId, chatQuotaKey(chatId, openId));
|
|
1066
1078
|
}
|
|
1067
1079
|
/** 全局对话授权命中判断(人/bot 通用,仅用于 canTalk / bot 路由闸 —— 不给管理命令权)。 */
|
|
1068
1080
|
function hasGlobalGrant(larkAppId, openId) {
|
|
1069
|
-
|
|
1081
|
+
if (!openId || !getBot(larkAppId).config.globalGrants?.includes(openId))
|
|
1082
|
+
return false;
|
|
1083
|
+
return grantNotExpired(larkAppId, 'global', undefined, openId, globalQuotaKey(openId));
|
|
1084
|
+
}
|
|
1085
|
+
const expiryCleanupInFlight = new Set();
|
|
1086
|
+
/**
|
|
1087
|
+
* 到期判断是同步安全边界:过期后本条消息立即拒绝;持久化清理由带 observedExpiresAt
|
|
1088
|
+
* 条件的原子 RMW 异步完成,避免误删刚续期的授权。
|
|
1089
|
+
*/
|
|
1090
|
+
function grantNotExpired(larkAppId, scope, chatId, openId, grantKey) {
|
|
1091
|
+
const expiresAt = getGrantExpiresAt(larkAppId, grantKey);
|
|
1092
|
+
if (expiresAt === undefined || Date.now() < expiresAt)
|
|
1093
|
+
return true;
|
|
1094
|
+
const cleanupKey = `${larkAppId}:${grantKey}:${expiresAt}`;
|
|
1095
|
+
if (!expiryCleanupInFlight.has(cleanupKey)) {
|
|
1096
|
+
expiryCleanupInFlight.add(cleanupKey);
|
|
1097
|
+
void removeExpiredGrant(larkAppId, scope, chatId, openId, expiresAt)
|
|
1098
|
+
.then(result => {
|
|
1099
|
+
if (!result.ok)
|
|
1100
|
+
logger.warn(`[grant:${larkAppId}] expiry cleanup failed key=${grantKey} reason=${result.reason}`);
|
|
1101
|
+
})
|
|
1102
|
+
.catch(err => logger.warn(`[grant:${larkAppId}] expiry cleanup failed key=${grantKey}: ${err}`))
|
|
1103
|
+
.finally(() => expiryCleanupInFlight.delete(cleanupKey));
|
|
1104
|
+
}
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
/** 整群 talk 授权命中判断(裸 `/grant` 写入的 allowedChatGroups)。chat 维度、sender 无关,
|
|
1108
|
+
* 与 oncall 同一个安全模型:只放行 canTalk / bot 路由闸,canOperate 绝不读它。 */
|
|
1109
|
+
function hasAllowedChatGroup(larkAppId, chatId) {
|
|
1110
|
+
return !!chatId && !!getBot(larkAppId).config.allowedChatGroups?.includes(chatId);
|
|
1070
1111
|
}
|
|
1071
1112
|
/**
|
|
1072
1113
|
* 是否配置了任何白名单(限制态)。判定用 **原始** config.allowedUsers, 不用
|
|
@@ -1110,7 +1151,20 @@ export function evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId, mem
|
|
|
1110
1151
|
if (chatId && findOncallChat(larkAppId, chatId)) {
|
|
1111
1152
|
const def = bot.config.messageQuota?.defaultLimit;
|
|
1112
1153
|
if (typeof def === 'number' && Number.isInteger(def) && def > 0 && senderOpenId) {
|
|
1113
|
-
|
|
1154
|
+
// 交集处理:同群若还持有显式 chatGrant,额度由授权决定而非 oncall default。
|
|
1155
|
+
// 三态:live 显式授权 → explicitGrantOverride(不兜 default);已过期 → expiredGrantCleanup
|
|
1156
|
+
//(透传给 consumeQuota,锁内以当前 expiry 为准原子清成员+quota+expiry 并回落 default);
|
|
1157
|
+
// 非成员 → 无覆盖,正常走 oncall default 懒初始化。
|
|
1158
|
+
const gk = chatQuotaKey(chatId, senderOpenId);
|
|
1159
|
+
const isMember = !!bot.config.chatGrants?.[chatId]?.includes(senderOpenId);
|
|
1160
|
+
const exp = isMember ? getGrantExpiresAt(larkAppId, gk) : undefined;
|
|
1161
|
+
if (isMember && exp !== undefined && Date.now() >= exp) {
|
|
1162
|
+
return {
|
|
1163
|
+
allowed: true, reason: 'oncall', quotaKey: gk,
|
|
1164
|
+
expiredGrantCleanup: { scope: 'chat', chatId, openId: senderOpenId },
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
return { allowed: true, reason: 'oncall', quotaKey: gk, explicitGrantOverride: isMember };
|
|
1114
1168
|
}
|
|
1115
1169
|
return { allowed: true, reason: 'oncall' };
|
|
1116
1170
|
}
|
|
@@ -1130,7 +1184,7 @@ export function evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId, mem
|
|
|
1130
1184
|
if (memberUnionId && isPlatformTeamMemberChat(config.session.dataDir, chatId, memberUnionId)) {
|
|
1131
1185
|
return { allowed: true, reason: 'teamMember' };
|
|
1132
1186
|
}
|
|
1133
|
-
if (
|
|
1187
|
+
if (hasAllowedChatGroup(larkAppId, chatId))
|
|
1134
1188
|
return { allowed: true, reason: 'allowedChatGroup' };
|
|
1135
1189
|
// p2pOpen:私聊维度的 talk-open,与 oncall(群维度)同一个安全模型——放行 canTalk,
|
|
1136
1190
|
// canOperate 一行不读它(管理仍限 allowedUsers)。谁能私聊由飞书应用「可用范围」控制。
|
|
@@ -1151,6 +1205,63 @@ export function evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId, mem
|
|
|
1151
1205
|
}
|
|
1152
1206
|
return { allowed: false, reason: 'none' };
|
|
1153
1207
|
}
|
|
1208
|
+
/**
|
|
1209
|
+
* BOT 发送方的 talk 判定 —— bot 路由闸(外部 bot @ 本 bot)的**唯一**入口。
|
|
1210
|
+
*
|
|
1211
|
+
* = 人侧 evaluateTalk 的完整模型 + 一条 bot 专属腿。历史上 bot 侧手抄了一份
|
|
1212
|
+
* talk 源子集(oncall / peer / teamBot / chatGrant / globalGrant 的 OR 链),
|
|
1213
|
+
* 于是每加一条新 talk 源就漏一次、复发一次同类 bug:oncall 漏过一次、开放模式
|
|
1214
|
+
* 漏过一次、allowedChatGroups 又漏一次(owner 整群 `/grant` 后真人能说话、外部 bot
|
|
1215
|
+
* 一 @ 仍弹授权卡)。收敛成单一谓词后,**新增 talk 源只需改 evaluateTalk**;
|
|
1216
|
+
* test/bot-talk-parity.test.ts 按 TALK_REASONS 逐条盯着这条约束。
|
|
1217
|
+
*
|
|
1218
|
+
* 与人侧仅有的两条差异,都是刻意的,改动前先读:
|
|
1219
|
+
*
|
|
1220
|
+
* 1. `isTrustedTeamBotSender`(+,bot 才有):其中真正额外放行的是**团队拉群**那条腿
|
|
1221
|
+
* ——本群是团队拉群(团队掌控成员)→ 说话的 botmux bot 即视为背书队友,首次接触
|
|
1222
|
+
* 也放行,与 oncall 群豁免同构。chat 维度、不看 union,覆盖 sender 事件没带
|
|
1223
|
+
* `union_id` 的情况(此时 gate 前那次 `recordTeamBot` 被 `senderUnionId &&` 短路,
|
|
1224
|
+
* evaluateTalk 的 teamBot 腿必然落空)。**这条不能下沉进 evaluateTalk**:那是人/bot
|
|
1225
|
+
* 共用的,加进去等于团队群里任何真人都自动过 canTalk,绕开 teamMember 腿的成员校验
|
|
1226
|
+
* (isPlatformTeamMemberChat 要求 union 在该团队成员名单里)。它另外两条 union 锚定
|
|
1227
|
+
* 的腿与 evaluateTalk 的 teamBot 腿重合,留着是为了让「bot 团队信任」只有一处定义。
|
|
1228
|
+
*
|
|
1229
|
+
* 2. `p2pOpen`(−):不传 chatType → 该腿 fail-closed 不生效。飞书里 bot 之间不存在
|
|
1230
|
+
* 私聊,开着只是白扩边界。
|
|
1231
|
+
*
|
|
1232
|
+
* @param senderUnionId 必须是飞书盖章的 bot 发送方 union(`sender_type ∈ app|bot`),
|
|
1233
|
+
* 与 evaluateTalk 的 bot-trust 腿同一契约。真人 union 走 memberUnionId,本函数恒不传。
|
|
1234
|
+
*/
|
|
1235
|
+
export function evaluateBotTalk(larkAppId, chatId, senderOpenId, senderUnionId) {
|
|
1236
|
+
const ev = evaluateTalk(larkAppId, chatId, senderOpenId, senderUnionId);
|
|
1237
|
+
if (ev.allowed)
|
|
1238
|
+
return ev;
|
|
1239
|
+
return isTrustedTeamBotSender(config.session.dataDir, chatId, senderUnionId)
|
|
1240
|
+
? { allowed: true, reason: 'teamBot' }
|
|
1241
|
+
: ev;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* `botmux ask` 答复者是否可作答(= 该 chat 的 canTalk 门)——ask-broker 注入的
|
|
1245
|
+
* canTalkChecker 的**唯一生产实现**,daemon bootstrap 一行转调它。
|
|
1246
|
+
*
|
|
1247
|
+
* 抽成独立导出函数(而非把分派内联进 daemon setter 闭包)的意义:让**分派谓词本身**
|
|
1248
|
+
* 能被回归测试直接咬住——测试对本函数喂真实 team store 状态断言三组行为,改坏这里的
|
|
1249
|
+
* 分派逻辑(如退回旧 evaluateTalk / 不认 actor.botSender)会立刻红。注意测试只覆盖到
|
|
1250
|
+
* 本函数;daemon setter 的一行转调与 submitCustomReply 的 actor 构造仍靠人工核对
|
|
1251
|
+
* (setter 只有一行,不经测试机械覆盖)。
|
|
1252
|
+
*
|
|
1253
|
+
* - bot 发送方(actor.botSender)→ evaluateBotTalk(含团队拉群那条 chat 维度腿,
|
|
1254
|
+
* 覆盖 sender 事件没带 union_id 的场景),与 dispatcher 外层闸 / quota 复查同源。
|
|
1255
|
+
* - 人 / 无 actor(卡片点击路径)→ evaluateTalk 全模型:actor.senderUnionId 走
|
|
1256
|
+
* teamBot 腿(人路径恒 undefined)、actor.memberUnionId 走 teamMember 腿。
|
|
1257
|
+
* actor 省略时两者都是 undefined → 退化为纯 evaluateTalk(openId, chatType),
|
|
1258
|
+
* 与本次改动前的卡片点击语义完全一致。
|
|
1259
|
+
*/
|
|
1260
|
+
export function evaluateAskAnswerTalk(larkAppId, chatId, senderOpenId, chatType, actor) {
|
|
1261
|
+
return actor?.botSender
|
|
1262
|
+
? evaluateBotTalk(larkAppId, chatId, senderOpenId, actor.senderUnionId).allowed
|
|
1263
|
+
: evaluateTalk(larkAppId, chatId, senderOpenId, actor?.senderUnionId, actor?.memberUnionId, chatType).allowed;
|
|
1264
|
+
}
|
|
1154
1265
|
export function canOperate(larkAppId, _chatId, senderOpenId, senderUnionId) {
|
|
1155
1266
|
const bot = getBot(larkAppId);
|
|
1156
1267
|
// 同部署 cross-ref / isKnownPeerBot 只证明「这是一个可路由的 bot 身份」,仅供
|
|
@@ -1181,19 +1292,27 @@ export function canOperate(larkAppId, _chatId, senderOpenId, senderUnionId) {
|
|
|
1181
1292
|
* 对话放行腿命中即可)。名单外或未配置 → 与 canOperate 完全等价(现状不变)。
|
|
1182
1293
|
*
|
|
1183
1294
|
* 只作用于 daemon.ts 两条路由的 DAEMON_COMMANDS 统一闸;在统一闸之前特判的命令
|
|
1184
|
-
* (/vc-auth /term)与 handler 内部自带 owner 闸的命令(/card /insight
|
|
1295
|
+
* (/vc-auth /term)与 handler 内部自带 owner 闸的命令(/card /insight)
|
|
1185
1296
|
* 不受影响——内部闸仍是最终权威(fail-closed)。
|
|
1186
1297
|
*
|
|
1187
1298
|
* chatType 省略时 p2pOpen 腿不生效(fail-closed),与 canTalk 语义一致——
|
|
1188
1299
|
* 私聊路径的调用点必须把 chatType 传进来。
|
|
1300
|
+
*
|
|
1301
|
+
* botSender:降权到 talk 判定这一段,必须与 dispatcher 外层闸 / quota 复查用**同一个**
|
|
1302
|
+
* 谓词。bot 发送方走 evaluateBotTalk(含团队拉群那条 chat 维度腿,覆盖 sender 事件没带
|
|
1303
|
+
* union_id 的情况);否则「团队拉群里外部 bot 能 talk 却执行不了降权到 canTalk 的命令
|
|
1304
|
+
* (如 /status)」——单一 talk 谓词在这道 daemon 命令闸继续分叉。不传(人的路径)→
|
|
1305
|
+
* 原样走 canTalk / evaluateTalk,语义不变。canOperate 那段人/bot 通用,不受影响。
|
|
1189
1306
|
*/
|
|
1190
|
-
export function canRunDaemonCommand(larkAppId, chatId, senderOpenId, senderUnionId, cmd, memberUnionId, chatType) {
|
|
1307
|
+
export function canRunDaemonCommand(larkAppId, chatId, senderOpenId, senderUnionId, cmd, memberUnionId, chatType, botSender) {
|
|
1191
1308
|
if (canOperate(larkAppId, chatId, senderOpenId, senderUnionId))
|
|
1192
1309
|
return true;
|
|
1193
1310
|
const list = getBot(larkAppId).config.canTalkDaemonCommands;
|
|
1194
1311
|
if (!list?.includes(cmd))
|
|
1195
1312
|
return false;
|
|
1196
|
-
return
|
|
1313
|
+
return botSender
|
|
1314
|
+
? evaluateBotTalk(larkAppId, chatId, senderOpenId, senderUnionId).allowed
|
|
1315
|
+
: canTalk(larkAppId, chatId, senderOpenId, senderUnionId, memberUnionId, chatType);
|
|
1197
1316
|
}
|
|
1198
1317
|
/**
|
|
1199
1318
|
* 入口 A:无权限者 @bot 时弹授权申请卡(正文 @owner,由 owner 处置)。
|
|
@@ -1214,10 +1333,23 @@ async function maybeSendGrantRequestCard(larkAppId, message, chatId, requesterOp
|
|
|
1214
1333
|
const observedName = mentionName
|
|
1215
1334
|
? undefined
|
|
1216
1335
|
: listObservedBots(config.session.dataDir, larkAppId, chatId).find(b => b.openId === requesterOpenId)?.name;
|
|
1217
|
-
const
|
|
1336
|
+
const profileName = mentionName || observedName
|
|
1337
|
+
? undefined
|
|
1338
|
+
: (await getUserProfile(larkAppId, requesterOpenId).catch(() => null))?.name;
|
|
1339
|
+
const shortRequester = `${requesterOpenId.slice(0, 10)}…${requesterOpenId.slice(-4)}`;
|
|
1340
|
+
const name = mentionName ?? observedName ?? profileName ?? shortRequester;
|
|
1218
1341
|
// 把原始消息事件挂在 pending 上:授权成功后可重放,用户无需再 @ 一遍。
|
|
1219
|
-
const
|
|
1220
|
-
const
|
|
1342
|
+
const quota = getBot(larkAppId).config.messageQuota?.defaultLimit ?? DEFAULT_GRANT_QUOTA;
|
|
1343
|
+
const nonce = openPending(larkAppId, chatId, requesterOpenId, quota, messageData, DEFAULT_GRANT_DURATION_MS);
|
|
1344
|
+
const card = buildGrantCard({
|
|
1345
|
+
ownerOpenId: owner,
|
|
1346
|
+
targets: [{ openId: requesterOpenId, name: String(name) }],
|
|
1347
|
+
chatId,
|
|
1348
|
+
nonce,
|
|
1349
|
+
mode: 'request',
|
|
1350
|
+
quota,
|
|
1351
|
+
durationMs: DEFAULT_GRANT_DURATION_MS,
|
|
1352
|
+
}, localeForBot(larkAppId));
|
|
1221
1353
|
await replyMessage(larkAppId, message.message_id, card, 'interactive')
|
|
1222
1354
|
.catch(err => {
|
|
1223
1355
|
// 发卡失败必须撤掉刚开的 pending,否则该发送方被节流压死、owner 永远看不到卡片,
|
|
@@ -1244,7 +1376,13 @@ export async function checkGroupMessageAccess(larkAppId, message, chatId, sender
|
|
|
1244
1376
|
// No @mention — only allow if sender is the sole human in the group
|
|
1245
1377
|
// AND this is the only bot in the chat. With multiple bots, require @mention
|
|
1246
1378
|
// to disambiguate.
|
|
1247
|
-
|
|
1379
|
+
//
|
|
1380
|
+
// 若消息 @ 了别的具体成员(mentionsAnotherMember),群必然不是 1人1bot——
|
|
1381
|
+
// 只有群成员能被 @,多出的那个 @ 本身就是人数变化的证据。上游可能还抱着
|
|
1382
|
+
// 陈旧缓存 {1,1}(刚拉了新 bot 的 TTL 窗口),这会直接跳过人数查询落到
|
|
1383
|
+
// 'ignore',挡住「用户 @ 新 bot、老 bot 跟着回复」。群聊 @ 策略 never/ambient
|
|
1384
|
+
// 在调用方(relax 条款)已先行结算,这里不受影响。
|
|
1385
|
+
if (isAllowed && !mentionsAnotherMember(larkAppId, message)) {
|
|
1248
1386
|
const { userCount, botCount } = await getGroupStats(larkAppId, chatId);
|
|
1249
1387
|
logger.debug(`Group user count: ${userCount}, bot count: ${botCount}`);
|
|
1250
1388
|
if (userCount <= 1 && botCount <= 1) {
|
|
@@ -1253,6 +1391,154 @@ export async function checkGroupMessageAccess(larkAppId, message, chatId, sender
|
|
|
1253
1391
|
}
|
|
1254
1392
|
return 'ignore';
|
|
1255
1393
|
}
|
|
1394
|
+
function listenerRoutingContext(input) {
|
|
1395
|
+
return {
|
|
1396
|
+
data: input.data,
|
|
1397
|
+
ctx: {
|
|
1398
|
+
chatId: input.chatId,
|
|
1399
|
+
messageId: input.messageId,
|
|
1400
|
+
chatType: input.chatType,
|
|
1401
|
+
larkAppId: input.larkAppId,
|
|
1402
|
+
scope: 'thread',
|
|
1403
|
+
anchor: input.messageId,
|
|
1404
|
+
messageListener: input.match,
|
|
1405
|
+
},
|
|
1406
|
+
ownsSession: false,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
const MESSAGE_LISTENER_POLL_INTERVAL_MS = Math.max(5_000, Number(process.env.BOTMUX_MESSAGE_LISTENER_POLL_INTERVAL_MS) || 30_000);
|
|
1410
|
+
const MESSAGE_LISTENER_BACKFILL_WINDOW_MS = Math.max(60_000, Number(process.env.BOTMUX_MESSAGE_LISTENER_BACKFILL_WINDOW_MS) || 30 * 60_000);
|
|
1411
|
+
const MESSAGE_LISTENER_BACKFILL_SCAN_LIMIT = Math.max(1, Number(process.env.BOTMUX_MESSAGE_LISTENER_BACKFILL_SCAN_LIMIT) || 100);
|
|
1412
|
+
const MESSAGE_LISTENER_BACKFILL_PAGE_SIZE = Math.min(50, Math.max(1, Number(process.env.BOTMUX_MESSAGE_LISTENER_BACKFILL_PAGE_SIZE) || 50));
|
|
1413
|
+
function enabledMessageListenerChatIds(bot) {
|
|
1414
|
+
return Object.entries(bot.config.messageListeners ?? {})
|
|
1415
|
+
.filter(([, listener]) => listener?.enabled === true && !!listener.prompt?.trim())
|
|
1416
|
+
.map(([chatId]) => chatId);
|
|
1417
|
+
}
|
|
1418
|
+
function messageCreateTimeMs(message) {
|
|
1419
|
+
const raw = message?.create_time ?? message?.createTime;
|
|
1420
|
+
const value = Number(raw);
|
|
1421
|
+
return Number.isFinite(value) ? value : undefined;
|
|
1422
|
+
}
|
|
1423
|
+
function historyMessageSender(message) {
|
|
1424
|
+
const sender = message?.sender ?? {};
|
|
1425
|
+
const senderId = sender.id ?? sender.open_id ?? sender.user_id ?? sender.app_id
|
|
1426
|
+
?? message?.sender_id?.open_id ?? message?.sender_id?.user_id ?? message?.sender_id?.app_id;
|
|
1427
|
+
const senderIdType = sender.id_type ?? sender.sender_id_type;
|
|
1428
|
+
const inferredType = sender.sender_type
|
|
1429
|
+
?? message?.sender_type
|
|
1430
|
+
?? (senderIdType === 'app_id' ? 'app' : undefined);
|
|
1431
|
+
return {
|
|
1432
|
+
senderOpenId: typeof senderId === 'string' ? senderId : undefined,
|
|
1433
|
+
senderTypeRaw: typeof inferredType === 'string' ? inferredType : undefined,
|
|
1434
|
+
senderIdType: typeof senderIdType === 'string' ? senderIdType : undefined,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function larkReceiveEventFromHistoryMessage(message, chatId) {
|
|
1438
|
+
const { senderOpenId, senderTypeRaw, senderIdType } = historyMessageSender(message);
|
|
1439
|
+
const isAppId = senderIdType === 'app_id' || senderTypeRaw === 'app' || senderTypeRaw === 'bot';
|
|
1440
|
+
return {
|
|
1441
|
+
message: {
|
|
1442
|
+
...message,
|
|
1443
|
+
message_type: message?.message_type ?? message?.msg_type,
|
|
1444
|
+
chat_id: message?.chat_id ?? chatId,
|
|
1445
|
+
chat_type: message?.chat_type ?? 'group',
|
|
1446
|
+
},
|
|
1447
|
+
sender: {
|
|
1448
|
+
sender_type: senderTypeRaw ?? (isAppId ? 'app' : 'user'),
|
|
1449
|
+
sender_id: isAppId
|
|
1450
|
+
? { app_id: senderOpenId }
|
|
1451
|
+
: { open_id: senderOpenId },
|
|
1452
|
+
},
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
async function dispatchHumanMessageViaHandlers(larkAppId, handlers, payload) {
|
|
1456
|
+
await serializeByAnchor(payload.ctx.anchor, () => {
|
|
1457
|
+
const ownsSession = handlers.isSessionOwner?.(payload.ctx.anchor, larkAppId) ?? payload.ownsSession;
|
|
1458
|
+
return ownsSession
|
|
1459
|
+
? handlers.handleThreadReply(payload.data, payload.ctx)
|
|
1460
|
+
: handlers.handleNewTopic(payload.data, payload.ctx);
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
async function dispatchPolledMessageListenerMatch(input) {
|
|
1464
|
+
if (!claimMessageOnce(input.larkAppId, input.messageId)) {
|
|
1465
|
+
logger.debug(`[message-listener:${input.larkAppId}] polled duplicate ignored msg=${input.messageId.substring(0, 12)}`);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
logger.info(`[message-listener:${input.larkAppId}] matched polled chat=${input.chatId.substring(0, 12)} ` +
|
|
1469
|
+
`msg=${input.messageId.substring(0, 12)} sender=${input.match.senderOpenId?.substring(0, 12) ?? '-'}`);
|
|
1470
|
+
await dispatchHumanMessageViaHandlers(input.larkAppId, input.handlers, listenerRoutingContext({
|
|
1471
|
+
data: input.data,
|
|
1472
|
+
match: input.match,
|
|
1473
|
+
chatId: input.chatId,
|
|
1474
|
+
messageId: input.messageId,
|
|
1475
|
+
chatType: 'group',
|
|
1476
|
+
larkAppId: input.larkAppId,
|
|
1477
|
+
}));
|
|
1478
|
+
}
|
|
1479
|
+
async function pollMessageListenersOnce(larkAppId, handlers, now = Date.now()) {
|
|
1480
|
+
const bot = getBot(larkAppId);
|
|
1481
|
+
const chatIds = enabledMessageListenerChatIds(bot);
|
|
1482
|
+
if (chatIds.length === 0)
|
|
1483
|
+
return;
|
|
1484
|
+
const cutoff = now - MESSAGE_LISTENER_BACKFILL_WINDOW_MS;
|
|
1485
|
+
await ensureBotOpenId(larkAppId).catch(() => { });
|
|
1486
|
+
for (const chatId of chatIds) {
|
|
1487
|
+
let messages;
|
|
1488
|
+
try {
|
|
1489
|
+
messages = await listChatMessagesUntil(larkAppId, chatId, {
|
|
1490
|
+
pageSize: MESSAGE_LISTENER_BACKFILL_PAGE_SIZE,
|
|
1491
|
+
stopAfter: (message, seenCount) => {
|
|
1492
|
+
const createdAt = messageCreateTimeMs(message);
|
|
1493
|
+
return seenCount >= MESSAGE_LISTENER_BACKFILL_SCAN_LIMIT ||
|
|
1494
|
+
(Number.isFinite(createdAt) && createdAt < cutoff);
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
catch (err) {
|
|
1499
|
+
logger.warn(`[message-listener:${larkAppId}] failed to poll chat=${chatId.substring(0, 12)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
// Build the app_id→open_id map once per chat (shared with the dashboard
|
|
1503
|
+
// preview leg) so bot senders (reported by app_id in history) match sender
|
|
1504
|
+
// filters keyed on open_id, and unprovable bots fail closed on exclusion.
|
|
1505
|
+
const candidateBotAppIds = collectListenerBotAppIds(messages, historyMessageSender);
|
|
1506
|
+
const appIdToOpenId = await buildListenerBotAppIdToOpenId(larkAppId, chatId, candidateBotAppIds);
|
|
1507
|
+
for (const message of messages) {
|
|
1508
|
+
const messageId = String(message?.message_id ?? '');
|
|
1509
|
+
if (!messageId)
|
|
1510
|
+
continue;
|
|
1511
|
+
const createdAt = messageCreateTimeMs(message);
|
|
1512
|
+
if (Number.isFinite(createdAt) && createdAt < cutoff)
|
|
1513
|
+
continue;
|
|
1514
|
+
const data = larkReceiveEventFromHistoryMessage(message, chatId);
|
|
1515
|
+
const rawSender = historyMessageSender(message);
|
|
1516
|
+
const resolved = resolveListenerSenderIdentity(rawSender, appIdToOpenId);
|
|
1517
|
+
const match = evaluateMessageListener({
|
|
1518
|
+
bot,
|
|
1519
|
+
chatId,
|
|
1520
|
+
message: data.message,
|
|
1521
|
+
senderOpenId: resolved.senderOpenId,
|
|
1522
|
+
senderTypeRaw: rawSender.senderTypeRaw,
|
|
1523
|
+
senderIdentityUnverified: resolved.identityUnverified,
|
|
1524
|
+
explicitlyMentionedThisBot: isBotMentioned(larkAppId, data.message, resolved.senderOpenId),
|
|
1525
|
+
});
|
|
1526
|
+
if (!match)
|
|
1527
|
+
continue;
|
|
1528
|
+
await dispatchPolledMessageListenerMatch({
|
|
1529
|
+
larkAppId,
|
|
1530
|
+
handlers,
|
|
1531
|
+
data,
|
|
1532
|
+
match,
|
|
1533
|
+
chatId,
|
|
1534
|
+
messageId,
|
|
1535
|
+
}).catch(err => logger.error(`Error handling polled message listener event: ${err}`));
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
export async function __pollMessageListenersOnceForTest(larkAppId, handlers, now = Date.now()) {
|
|
1540
|
+
await pollMessageListenersOnce(larkAppId, handlers, now);
|
|
1541
|
+
}
|
|
1256
1542
|
function usesForwardFollowupDelay(mentionMode) {
|
|
1257
1543
|
return mentionMode === 'never' || mentionMode === 'ambient';
|
|
1258
1544
|
}
|
|
@@ -1756,12 +2042,7 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
1756
2042
|
resolveSessionsReady();
|
|
1757
2043
|
sessionsReadyBarriers.set(larkAppId, { promise: sessionsReady, resolve: resolveSessionsReady });
|
|
1758
2044
|
const dispatchHumanMessage = async (payload) => {
|
|
1759
|
-
await
|
|
1760
|
-
const ownsSession = handlers.isSessionOwner?.(payload.ctx.anchor, larkAppId) ?? payload.ownsSession;
|
|
1761
|
-
return ownsSession
|
|
1762
|
-
? handlers.handleThreadReply(payload.data, payload.ctx)
|
|
1763
|
-
: handlers.handleNewTopic(payload.data, payload.ctx);
|
|
1764
|
-
});
|
|
2045
|
+
await dispatchHumanMessageViaHandlers(larkAppId, handlers, payload);
|
|
1765
2046
|
};
|
|
1766
2047
|
const dispatchPersistedForwardFollowup = async (seedMessageId, payload) => {
|
|
1767
2048
|
// Wait for activeSessions to be rehydrated before checking ownership, so a
|
|
@@ -1860,7 +2141,12 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
1860
2141
|
const senderType = sender?.sender_type;
|
|
1861
2142
|
const isBotSenderType = senderType === 'app' || senderType === 'bot';
|
|
1862
2143
|
if (isBotSenderType) {
|
|
1863
|
-
const senderOpenId = sender.sender_id?.open_id;
|
|
2144
|
+
const senderOpenId = sender.sender_id?.open_id ?? sender.sender_id?.app_id;
|
|
2145
|
+
// When Feishu gave us only an app_id (no open_id), this bot sender's
|
|
2146
|
+
// identity is not in the open_id domain the listener filters use, so
|
|
2147
|
+
// the matcher must fail closed on open_id-based exclusion. Realtime
|
|
2148
|
+
// events normally carry open_id; the app_id fallback is the rare case.
|
|
2149
|
+
const senderIdentityUnverified = !sender.sender_id?.open_id && !!sender.sender_id?.app_id;
|
|
1864
2150
|
const isSelfMessage = senderOpenId === getBot(larkAppId).botOpenId;
|
|
1865
2151
|
// Self messages: learn our OWN union_id from the echo first (the only
|
|
1866
2152
|
// reliable source — see bot-union-ids-store; reported to the platform
|
|
@@ -1919,6 +2205,30 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
1919
2205
|
logger.debug(`[${larkAppId}] hall bot message swallowed after learning (chat=${chatId.substring(0, 12)})`);
|
|
1920
2206
|
return;
|
|
1921
2207
|
}
|
|
2208
|
+
const botMessageListener = chatType === 'group'
|
|
2209
|
+
? evaluateMessageListener({
|
|
2210
|
+
bot: getBot(larkAppId),
|
|
2211
|
+
chatId,
|
|
2212
|
+
message,
|
|
2213
|
+
senderOpenId,
|
|
2214
|
+
senderTypeRaw: sender?.sender_type,
|
|
2215
|
+
senderIdentityUnverified,
|
|
2216
|
+
explicitlyMentionedThisBot: isBotMentioned(larkAppId, message, senderOpenId),
|
|
2217
|
+
})
|
|
2218
|
+
: undefined;
|
|
2219
|
+
if (botMessageListener) {
|
|
2220
|
+
logger.info(`[message-listener:${larkAppId}] matched bot-sender chat=${chatId.substring(0, 12)} ` +
|
|
2221
|
+
`msg=${messageId.substring(0, 12)} sender=${senderOpenId?.substring(0, 12) ?? '-'}`);
|
|
2222
|
+
await dispatchHumanMessage(listenerRoutingContext({
|
|
2223
|
+
data,
|
|
2224
|
+
match: botMessageListener,
|
|
2225
|
+
chatId,
|
|
2226
|
+
messageId,
|
|
2227
|
+
chatType,
|
|
2228
|
+
larkAppId,
|
|
2229
|
+
})).catch(err => logger.error(`Error handling bot message listener event: ${err}`));
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
1922
2232
|
// Foreign bot: only route on @mention of us.
|
|
1923
2233
|
if (!isBotMentioned(larkAppId, message, undefined))
|
|
1924
2234
|
return;
|
|
@@ -1938,13 +2248,13 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
1938
2248
|
const ownsThreadSession = ctx.scope === 'thread'
|
|
1939
2249
|
? (handlers.isSessionOwner?.(ctx.anchor, larkAppId) ?? false)
|
|
1940
2250
|
: false;
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2251
|
+
// 这个 bot 能不能在本群跟我们说话 —— 判定一次,fold 与下面的 gate 共用同一个
|
|
2252
|
+
// 结论(二者之间只有 fold / shared-seed 的路由计算,不改任何授权状态)。
|
|
2253
|
+
// 曾经这里是两条手抄的 OR 链,靠人肉保持同步;漏一处就是「能路由但不能 fold」
|
|
2254
|
+
// 或「fold 了却弹卡」的二次分裂。
|
|
2255
|
+
const botTalk = evaluateBotTalk(larkAppId, chatId, senderOpenId, senderUnionId);
|
|
1946
2256
|
let replyRootId = await maybeFoldMentionedRegularGroupThreadToChat({
|
|
1947
|
-
larkAppId, chatId, chatType, message, routing: ctx, forceTopicApplied: forcedTopic, mentionedThisBot:
|
|
2257
|
+
larkAppId, chatId, chatType, message, routing: ctx, forceTopicApplied: forcedTopic, mentionedThisBot: botTalk.allowed, ownsThreadSession,
|
|
1948
2258
|
});
|
|
1949
2259
|
if (!replyRootId) {
|
|
1950
2260
|
replyRootId = await maybeApplySharedTopicSeed({
|
|
@@ -1956,71 +2266,44 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
1956
2266
|
// threads. Previously native topic replies skipped this outer gate and
|
|
1957
2267
|
// were silently dropped by daemon's second evaluateTalk check, so the
|
|
1958
2268
|
// owner never saw a grant card. Owning an existing session is not an
|
|
1959
|
-
// authorization source; a revoked/unknown bot must still pass
|
|
1960
|
-
// the
|
|
2269
|
+
// authorization source; a revoked/unknown bot must still pass
|
|
2270
|
+
// evaluateBotTalk (the single talk predicate — see its doc for the two
|
|
2271
|
+
// deliberate human/bot deltas) or prove itself a sibling below.
|
|
1961
2272
|
//
|
|
1962
|
-
//
|
|
1963
|
-
//
|
|
1964
|
-
//
|
|
1965
|
-
//
|
|
1966
|
-
// observed-bots-store,只负责让发送方"发现并能 @ 到"对方,过不了这道接收闸。
|
|
2273
|
+
// 这里**只判 evaluateBotTalk**。以前是手抄的 OR 链,每加一条 talk 源就漏一次:
|
|
2274
|
+
// oncall 漏过、开放模式漏过、allowedChatGroups 又漏过(整群 /grant 后人通 bot 卡)。
|
|
2275
|
+
// 要加新的 talk 放行源,改 evaluateTalk(人/bot 同时生效),不要在这里加腿;
|
|
2276
|
+
// test/bot-talk-parity.test.ts 会盯着这条约束。
|
|
1967
2277
|
//
|
|
1968
|
-
//
|
|
1969
|
-
//
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
// Cold-start self-heal: the cross-ref (bot-openids-<appId>.json) is
|
|
1998
|
-
// learned lazily from observed mentions[], so the FIRST bot→bot
|
|
1999
|
-
// direct @ from a same-deployment sibling can arrive before the
|
|
2000
|
-
// receiver has learned that sibling's receiver-scoped open_id
|
|
2001
|
-
// (Lark open_id is per-app). That raced a real sibling into this
|
|
2002
|
-
// "unknown external bot" branch and mis-fired a /grant card
|
|
2003
|
-
// (regression from ec146a49). Before deciding unknown, confirm the
|
|
2004
|
-
// sender against the group's LIVE `/members/bots` roster: accept
|
|
2005
|
-
// only when it binds to exactly one locally-configured sibling of
|
|
2006
|
-
// the same unique name that independently confirms is_in_chat.
|
|
2007
|
-
// Fails closed to the grant card on any API error / ambiguity /
|
|
2008
|
-
// genuine external bot — never a name-only shortcut.
|
|
2009
|
-
const sibling = await resolveSiblingBotBySenderOpenId(larkAppId, chatId, senderOpenId)
|
|
2010
|
-
.catch((err) => ({ ok: false, reason: `resolve_threw: ${err?.message ?? String(err)}` }));
|
|
2011
|
-
if (sibling.ok) {
|
|
2012
|
-
// Persist the newly-proven mapping so subsequent @s from this
|
|
2013
|
-
// sibling hit isKnownPeerBot directly (no live API roundtrip).
|
|
2014
|
-
updateBotOpenIdCrossRef(config.session.dataDir, larkAppId, [
|
|
2015
|
-
{ name: sibling.botName, id: { open_id: sibling.senderOpenId } },
|
|
2016
|
-
]);
|
|
2017
|
-
logger.info(`Lazy sibling cross-ref backfill: ${sibling.botName} → ${senderOpenId?.substring(0, 12)} (was cold; skipping /grant)`);
|
|
2018
|
-
}
|
|
2019
|
-
else {
|
|
2020
|
-
logger.info(`Foreign bot @mention not a known sibling (${sibling.reason}); sending grant request card`);
|
|
2021
|
-
await maybeSendGrantRequestCard(larkAppId, message, chatId, senderOpenId, data);
|
|
2022
|
-
return;
|
|
2023
|
-
}
|
|
2278
|
+
// 唯一留在闸门里的 bot 专属逻辑是下面的 cross-ref 冷启动自愈:它不是一条
|
|
2279
|
+
// 授权来源,而是「同部署兄弟 bot 的身份还没学到」这个**识别**问题的补救。
|
|
2280
|
+
if (!botTalk.allowed) {
|
|
2281
|
+
// Cold-start self-heal: the cross-ref (bot-openids-<appId>.json) is
|
|
2282
|
+
// learned lazily from observed mentions[], so the FIRST bot→bot
|
|
2283
|
+
// direct @ from a same-deployment sibling can arrive before the
|
|
2284
|
+
// receiver has learned that sibling's receiver-scoped open_id
|
|
2285
|
+
// (Lark open_id is per-app). That raced a real sibling into this
|
|
2286
|
+
// "unknown external bot" branch and mis-fired a /grant card
|
|
2287
|
+
// (regression from ec146a49). Before deciding unknown, confirm the
|
|
2288
|
+
// sender against the group's LIVE `/members/bots` roster: accept
|
|
2289
|
+
// only when it binds to exactly one locally-configured sibling of
|
|
2290
|
+
// the same unique name that independently confirms is_in_chat.
|
|
2291
|
+
// Fails closed to the grant card on any API error / ambiguity /
|
|
2292
|
+
// genuine external bot — never a name-only shortcut.
|
|
2293
|
+
const sibling = await resolveSiblingBotBySenderOpenId(larkAppId, chatId, senderOpenId)
|
|
2294
|
+
.catch((err) => ({ ok: false, reason: `resolve_threw: ${err?.message ?? String(err)}` }));
|
|
2295
|
+
if (sibling.ok) {
|
|
2296
|
+
// Persist the newly-proven mapping so subsequent @s from this
|
|
2297
|
+
// sibling hit isKnownPeerBot directly (no live API roundtrip).
|
|
2298
|
+
updateBotOpenIdCrossRef(config.session.dataDir, larkAppId, [
|
|
2299
|
+
{ name: sibling.botName, id: { open_id: sibling.senderOpenId } },
|
|
2300
|
+
]);
|
|
2301
|
+
logger.info(`Lazy sibling cross-ref backfill: ${sibling.botName} → ${senderOpenId?.substring(0, 12)} (was cold; skipping /grant)`);
|
|
2302
|
+
}
|
|
2303
|
+
else {
|
|
2304
|
+
logger.info(`Foreign bot @mention not a known sibling (${sibling.reason}); sending grant request card`);
|
|
2305
|
+
await maybeSendGrantRequestCard(larkAppId, message, chatId, senderOpenId, data);
|
|
2306
|
+
return;
|
|
2024
2307
|
}
|
|
2025
2308
|
}
|
|
2026
2309
|
logger.info(`Bot-to-bot @mention detected (scope=${ctx.scope}): routing to handleThreadReply`);
|
|
@@ -2058,6 +2341,11 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2058
2341
|
if (await tryHandleGrantCommand(larkAppId, message, senderOpenId)) {
|
|
2059
2342
|
return;
|
|
2060
2343
|
}
|
|
2344
|
+
// /invite — 把群外 bot 拉进本群的元命令。与 /grant 同款拦截模型(仅 owner,
|
|
2345
|
+
// 需明确 @ 本 bot),防止进路由/spawn,也防多 bot 重复执行拉人。
|
|
2346
|
+
if (await tryHandleInviteCommand(larkAppId, message, senderOpenId)) {
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2061
2349
|
logger.debug('Received message:', message);
|
|
2062
2350
|
// Diagnostic: record the Lark quote-bubble UI quirk where root_id
|
|
2063
2351
|
// appears without thread_id. decideRouting now treats this as
|
|
@@ -2087,6 +2375,24 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2087
2375
|
replyRootId = message.root_id;
|
|
2088
2376
|
}
|
|
2089
2377
|
const explicitlyMentionedThisBot = isBotMentioned(larkAppId, message, senderOpenId);
|
|
2378
|
+
const messageListener = chatType === 'group'
|
|
2379
|
+
? evaluateMessageListener({
|
|
2380
|
+
bot: getBot(larkAppId),
|
|
2381
|
+
chatId,
|
|
2382
|
+
message,
|
|
2383
|
+
senderOpenId,
|
|
2384
|
+
senderTypeRaw: sender?.sender_type,
|
|
2385
|
+
explicitlyMentionedThisBot,
|
|
2386
|
+
})
|
|
2387
|
+
: undefined;
|
|
2388
|
+
if (messageListener) {
|
|
2389
|
+
routing.scope = 'thread';
|
|
2390
|
+
routing.anchor = messageId;
|
|
2391
|
+
routingSource = 'topic-chat';
|
|
2392
|
+
replyRootId = undefined;
|
|
2393
|
+
logger.info(`[message-listener:${larkAppId}] matched chat=${chatId.substring(0, 12)} ` +
|
|
2394
|
+
`msg=${messageId.substring(0, 12)} sender=${senderOpenId?.substring(0, 12) ?? '-'}`);
|
|
2395
|
+
}
|
|
2090
2396
|
// Cheap in-memory gate FIRST: skip the getChatMode roundtrip and the
|
|
2091
2397
|
// per-chat toggle disk read entirely for bots that never configured a
|
|
2092
2398
|
// substitute target (the overwhelming majority on the hot path).
|
|
@@ -2103,6 +2409,21 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2103
2409
|
substituteChatMode = chatMode;
|
|
2104
2410
|
}
|
|
2105
2411
|
}
|
|
2412
|
+
// 黑名单硬静默:命中黑名单的群里,一条「本该触发替身」的消息(@ 到了配置的
|
|
2413
|
+
// 替身对象、但没有直接 @ 本 bot)必须当作没读到——直接 return,不只是清 trigger。
|
|
2414
|
+
// 只清 trigger 不够:消息会继续 fall-through 到通用群消息门,若 bot 在该群有活跃
|
|
2415
|
+
// 会话 / 是 solo 群 / mentionMode 放开,仍会被喂进去并弹卡片(用户实测现象)。
|
|
2416
|
+
// 直接 @ 本 bot(explicitlyMentionedThisBot)不受影响:黑名单只静音替身代答,
|
|
2417
|
+
// 不静音「直接找 bot 问问题」。/substitute 命令已在上方 command 处理器拦截。
|
|
2418
|
+
if (substituteCfg?.enabled === true
|
|
2419
|
+
&& chatType === 'group'
|
|
2420
|
+
&& !explicitlyMentionedThisBot
|
|
2421
|
+
&& isSubstituteExcludedChat(substituteCfg, chatId)
|
|
2422
|
+
&& resolveSubstituteTrigger(larkAppId, message)) {
|
|
2423
|
+
logger.info(`[substitute:${larkAppId}] excluded chat — dropping @target message ` +
|
|
2424
|
+
`msg=${messageId.substring(0, 12)} chat=${chatId.substring(0, 12)}`);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2106
2427
|
let substituteTrigger = substituteChatMode
|
|
2107
2428
|
? resolveSubstituteTrigger(larkAppId, message)
|
|
2108
2429
|
: undefined;
|
|
@@ -2187,7 +2508,8 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2187
2508
|
// can't be cache-stale) or when chatType is p2p (DMs always thread).
|
|
2188
2509
|
// Runs BEFORE /t override so a `@bot /t …` in a now-flat 普通群 still
|
|
2189
2510
|
// gets the explicit topic seed it asked for.
|
|
2190
|
-
if (
|
|
2511
|
+
if (!messageListener &&
|
|
2512
|
+
routing.scope === 'thread' &&
|
|
2191
2513
|
routing.anchor === messageId &&
|
|
2192
2514
|
!message.thread_id &&
|
|
2193
2515
|
chatType === 'group') {
|
|
@@ -2360,9 +2682,16 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2360
2682
|
// !ownsSession (group) → require @mention + allowlist
|
|
2361
2683
|
// p2p → allowlist only
|
|
2362
2684
|
if (chatType === 'group') {
|
|
2685
|
+
const mentionMode = resolveGroupMentionMode(larkAppId);
|
|
2686
|
+
// 消息里 @ 了别的具体成员,就已经证明群不是 1人1bot(只有群成员能被 @)——
|
|
2687
|
+
// 此刻末条 solo 放行必然不成立,而 stats 只被末条消费,直接跳过这次(可能
|
|
2688
|
+
// 昂贵的)人数查询。这在「刚拉了新 bot、用户 @ 新 bot」窗口里尤为重要:
|
|
2689
|
+
// 拦截老 bot 的同时不再为它反复刷新陈旧缓存。
|
|
2690
|
+
const mentionsOther = mentionsAnotherMember(larkAppId, message);
|
|
2363
2691
|
let stats = null;
|
|
2364
|
-
if (ownsSession && !replyRootId)
|
|
2692
|
+
if (ownsSession && !replyRootId && !mentionsOther && mentionMode !== 'never') {
|
|
2365
2693
|
stats = await getGroupStats(larkAppId, chatId);
|
|
2694
|
+
}
|
|
2366
2695
|
// replyRootId means this turn has already been explicitly addressed
|
|
2367
2696
|
// to the bot by shared-topic logic (possibly from inside an existing
|
|
2368
2697
|
// Lark thread). Do not re-run the generic group @ gate, which would
|
|
@@ -2387,8 +2716,8 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2387
2716
|
// treat it as a hand-off and stay quiet.
|
|
2388
2717
|
// Both gated on isAllowed so restricted groups still only react to
|
|
2389
2718
|
// permitted senders. (The shared fold-back's replyRootId is already
|
|
2390
|
-
// handled by the first clause.
|
|
2391
|
-
|
|
2719
|
+
// handled by the first clause. `mentionMode` 已在块首解析,用于 stats
|
|
2720
|
+
// 惰性获取。)
|
|
2392
2721
|
// 话题群 owned-topic 免@续话不再无条件放行(#336 引入的默认行为回归:
|
|
2393
2722
|
// 多人群里旁人不 @ 也会触发 bot)。现在与普通群共用同一套「群聊 @ 策略」:
|
|
2394
2723
|
// 默认 'always' 在多人群里必须 @,想要话题内免@续话就把 mentionMode 配成
|
|
@@ -2397,12 +2726,17 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2397
2726
|
// 注:pairedForwardSeed 仅在 never/ambient 模式下产生,且 ambient redirect
|
|
2398
2727
|
// 已在配对前排除,故 isAllowed=true 时下方 never/ambient 条款必然放行;
|
|
2399
2728
|
// 不在此单独加 clause,以免 isAllowed=false 时绕过权限检查。
|
|
2729
|
+
// 末条 solo 放行另有 `!mentionsOther` 守卫:消息 @ 了别的具体成员时,
|
|
2730
|
+
// 群必然不是 1人1bot(只有群成员能被 @),且是指给别人的——拦住「刚拉新 bot、
|
|
2731
|
+
// 缓存仍是陈旧 {1,1} 时老 bot 跟着回复」的误放行。never 条款在前已短路,
|
|
2732
|
+
// 故该守卫不影响「群聊 @ 策略」配置的 never 语义。
|
|
2400
2733
|
const relax = (!!replyRootId && isAllowed)
|
|
2401
2734
|
|| (!!substituteTrigger && isAllowed)
|
|
2735
|
+
|| !!messageListener
|
|
2402
2736
|
|| (isAllowed && mentionMode === 'never')
|
|
2403
|
-
|| (isAllowed && mentionMode === 'ambient' && !
|
|
2404
|
-
|| (isAllowed && mentionMode === 'topic' && ownsSession && !!message.thread_id && !
|
|
2405
|
-
|| (ownsSession && isAllowed && !!stats && stats.userCount <= 1 && stats.botCount <= 1);
|
|
2737
|
+
|| (isAllowed && mentionMode === 'ambient' && !mentionsOther)
|
|
2738
|
+
|| (isAllowed && mentionMode === 'topic' && ownsSession && !!message.thread_id && !mentionsOther)
|
|
2739
|
+
|| (ownsSession && isAllowed && !!stats && !mentionsOther && stats.userCount <= 1 && stats.botCount <= 1);
|
|
2406
2740
|
if (!relax) {
|
|
2407
2741
|
const access = await checkGroupMessageAccess(larkAppId, message, chatId, senderOpenId, humanSenderUnionId);
|
|
2408
2742
|
if (access === 'not_allowed') {
|
|
@@ -2459,6 +2793,7 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2459
2793
|
? { name: 'summary-command', chatKind: summaryCommandMatch.chatKind }
|
|
2460
2794
|
: undefined,
|
|
2461
2795
|
substituteTrigger,
|
|
2796
|
+
messageListener,
|
|
2462
2797
|
forwardSeedData: pairedForwardSeed?.payload.data,
|
|
2463
2798
|
};
|
|
2464
2799
|
if (explicitlyMentionedThisBot) {
|
|
@@ -2552,6 +2887,15 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2552
2887
|
const chatIdForKey = data?.chat_id;
|
|
2553
2888
|
const operatorForKey = data?.operator_id?.open_id;
|
|
2554
2889
|
const eventKey = `im.chat.member.bot.added_v1:${larkAppId}:${eventIdForKey(data) ?? `${chatIdForKey ?? 'unknown'}:${operatorForKey ?? 'unknown'}`}`;
|
|
2890
|
+
// 飞书只把 bot.added 推给「进群的那个 bot 自己的 app」(官方文档语義,
|
|
2891
|
+
// codex 复审证实),且生产是 PM2「一 bot 一 daemon 进程」(daemon.ts
|
|
2892
|
+
// "Load the assigned bot (one daemon per bot)")——同群其他 bot 的缓存在
|
|
2893
|
+
// 别的进程,这里够不到,只能清自己的 key。覆盖增量其实很小:新添进群
|
|
2894
|
+
// 时本 bot 此前若恰有该群条目(曾被移出又拉回),避免带着上轮陈旧数放行。
|
|
2895
|
+
// 「拉了别的 bot 进群→存量 bot 陈旧」方向无事件信号,靠 relax 末条款的
|
|
2896
|
+
// mentionsAnotherMember 守卫 + TTL 兜底。
|
|
2897
|
+
if (chatIdForKey)
|
|
2898
|
+
invalidateChatStats(larkAppId, chatIdForKey);
|
|
2555
2899
|
scheduleAckSafeEvent(eventKey, async () => {
|
|
2556
2900
|
try {
|
|
2557
2901
|
const chatId = data?.chat_id;
|
|
@@ -2566,6 +2910,26 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2566
2910
|
}
|
|
2567
2911
|
}, 'bot-added event');
|
|
2568
2912
|
},
|
|
2913
|
+
// bot.deleted 同样只推给被移出的 bot 自己的 app——清自己的 key 即可(语义
|
|
2914
|
+
// 同 bot.added:进程边界上够不到兄弟 daemon;「别的 bot 离群→存量 bot
|
|
2915
|
+
// 陈旧」靠①守卫+TTL)。user.added/deleted_v1 推给群内已订阅的所有 bot
|
|
2916
|
+
// app,每个 bot 自己的 WS 都收到,各自清自己那条。纯本地 map 删除,同步
|
|
2917
|
+
// 处理、即时 ACK,不进 work 队列;去重也不必要——失效是幂等的。
|
|
2918
|
+
'im.chat.member.bot.deleted_v1': (data) => {
|
|
2919
|
+
const chatId = data?.chat_id;
|
|
2920
|
+
if (chatId)
|
|
2921
|
+
invalidateChatStats(larkAppId, chatId);
|
|
2922
|
+
},
|
|
2923
|
+
'im.chat.member.user.added_v1': (data) => {
|
|
2924
|
+
const chatId = data?.chat_id;
|
|
2925
|
+
if (chatId)
|
|
2926
|
+
invalidateChatStats(larkAppId, chatId);
|
|
2927
|
+
},
|
|
2928
|
+
'im.chat.member.user.deleted_v1': (data) => {
|
|
2929
|
+
const chatId = data?.chat_id;
|
|
2930
|
+
if (chatId)
|
|
2931
|
+
invalidateChatStats(larkAppId, chatId);
|
|
2932
|
+
},
|
|
2569
2933
|
// 文档评论入口(/watch-comment / /subscribe-lark-doc)。notice 事件主要覆盖 @Bot
|
|
2570
2934
|
// 通知;普通评论由 daemon 应用身份轮询补齐,不依赖逐文件 subscribe API。
|
|
2571
2935
|
'drive.notice.comment_add_v1': (data) => handleCommentEventAckSafe(data, larkAppId, handlers),
|
|
@@ -2677,6 +3041,29 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
2677
3041
|
});
|
|
2678
3042
|
wsClient.start({ eventDispatcher });
|
|
2679
3043
|
logger.info('Daemon WSClient started');
|
|
3044
|
+
let listenerPollInFlight = false;
|
|
3045
|
+
const listenerPollTimer = setInterval(() => {
|
|
3046
|
+
if (listenerPollInFlight)
|
|
3047
|
+
return;
|
|
3048
|
+
listenerPollInFlight = true;
|
|
3049
|
+
void pollMessageListenersOnce(larkAppId, handlers)
|
|
3050
|
+
.catch(err => logger.error(`[message-listener:${larkAppId}] poll failed: ${err instanceof Error ? err.message : String(err)}`))
|
|
3051
|
+
.finally(() => { listenerPollInFlight = false; });
|
|
3052
|
+
}, MESSAGE_LISTENER_POLL_INTERVAL_MS);
|
|
3053
|
+
listenerPollTimer.unref();
|
|
3054
|
+
const hasListenerBackfill = enabledMessageListenerChatIds(getBot(larkAppId)).length > 0;
|
|
3055
|
+
if (hasListenerBackfill) {
|
|
3056
|
+
setTimeout(() => {
|
|
3057
|
+
if (listenerPollInFlight)
|
|
3058
|
+
return;
|
|
3059
|
+
listenerPollInFlight = true;
|
|
3060
|
+
void pollMessageListenersOnce(larkAppId, handlers)
|
|
3061
|
+
.catch(err => logger.error(`[message-listener:${larkAppId}] initial poll failed: ${err instanceof Error ? err.message : String(err)}`))
|
|
3062
|
+
.finally(() => { listenerPollInFlight = false; });
|
|
3063
|
+
}, 2_000).unref();
|
|
3064
|
+
logger.info(`[message-listener:${larkAppId}] polling backfill enabled interval=${MESSAGE_LISTENER_POLL_INTERVAL_MS}ms ` +
|
|
3065
|
+
`window=${MESSAGE_LISTENER_BACKFILL_WINDOW_MS}ms chats=${enabledMessageListenerChatIds(getBot(larkAppId)).length}`);
|
|
3066
|
+
}
|
|
2680
3067
|
// ② SDK 重连耗尽后停在 terminalError(getConnectionStatus().state === 'failed')并
|
|
2681
3068
|
// 永久放弃。每分钟探测一次,发现已放弃就 start() 重新发起一轮全新握手 —— start()
|
|
2682
3069
|
// 会清掉 terminalError 并重新 pullConnectConfig + connect,无需手动重启 daemon。
|