botmux 2.63.1 → 2.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +3 -3
- package/dist/adapters/backend/herdr-backend.d.ts +8 -1
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +15 -2
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +17 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +25 -4
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +14 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.d.ts +12 -1
- package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-backend.js +25 -8
- package/dist/adapters/backend/zellij-backend.js.map +1 -1
- package/dist/bot-registry.d.ts +40 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +30 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/send-dispatch.d.ts +23 -0
- package/dist/cli/send-dispatch.d.ts.map +1 -0
- package/dist/cli/send-dispatch.js +23 -0
- package/dist/cli/send-dispatch.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +141 -58
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +8 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -6
- package/dist/config.js.map +1 -1
- package/dist/core/ask-broker.d.ts +33 -0
- package/dist/core/ask-broker.d.ts.map +1 -1
- package/dist/core/ask-broker.js +58 -0
- package/dist/core/ask-broker.js.map +1 -1
- package/dist/core/ask-hook/claude-code.d.ts.map +1 -1
- package/dist/core/ask-hook/claude-code.js +15 -9
- package/dist/core/ask-hook/claude-code.js.map +1 -1
- package/dist/core/ask-hook/codex.d.ts.map +1 -1
- package/dist/core/ask-hook/codex.js +2 -1
- package/dist/core/ask-hook/codex.js.map +1 -1
- package/dist/core/ask-hook/opencode.d.ts.map +1 -1
- package/dist/core/ask-hook/opencode.js +9 -6
- package/dist/core/ask-hook/opencode.js.map +1 -1
- package/dist/core/ask-hook/types.d.ts +3 -1
- package/dist/core/ask-hook/types.d.ts.map +1 -1
- package/dist/core/ask-types.d.ts +13 -6
- package/dist/core/ask-types.d.ts.map +1 -1
- package/dist/core/ask-types.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +255 -4
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/daemon-heartbeat.d.ts +15 -0
- package/dist/core/daemon-heartbeat.d.ts.map +1 -0
- package/dist/core/daemon-heartbeat.js +83 -0
- package/dist/core/daemon-heartbeat.js.map +1 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +80 -33
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dispatch.d.ts +1 -23
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +1 -17
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/idle-worker-sweeper.d.ts +13 -0
- package/dist/core/idle-worker-sweeper.d.ts.map +1 -0
- package/dist/core/idle-worker-sweeper.js +42 -0
- package/dist/core/idle-worker-sweeper.js.map +1 -0
- package/dist/core/maintenance-schedule.d.ts +34 -0
- package/dist/core/maintenance-schedule.d.ts.map +1 -0
- package/dist/core/maintenance-schedule.js +72 -0
- package/dist/core/maintenance-schedule.js.map +1 -0
- package/dist/core/maintenance.d.ts +43 -0
- package/dist/core/maintenance.d.ts.map +1 -0
- package/dist/core/maintenance.js +160 -0
- package/dist/core/maintenance.js.map +1 -0
- package/dist/core/reply-target.d.ts +23 -0
- package/dist/core/reply-target.d.ts.map +1 -0
- package/dist/core/reply-target.js +47 -0
- package/dist/core/reply-target.js.map +1 -0
- package/dist/core/restart-report.d.ts +49 -0
- package/dist/core/restart-report.d.ts.map +1 -0
- package/dist/core/restart-report.js +98 -0
- package/dist/core/restart-report.js.map +1 -0
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +20 -0
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/session-manager.d.ts +26 -10
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +113 -31
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-marker.d.ts +13 -0
- package/dist/core/session-marker.d.ts.map +1 -0
- package/dist/core/session-marker.js +55 -0
- package/dist/core/session-marker.js.map +1 -0
- package/dist/core/types.d.ts +20 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-budget.d.ts +19 -0
- package/dist/core/worker-budget.d.ts.map +1 -0
- package/dist/core/worker-budget.js +50 -0
- package/dist/core/worker-budget.js.map +1 -0
- package/dist/core/worker-pool.d.ts +5 -2
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +105 -12
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +252 -38
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/public-redact.d.ts +2 -1
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +3 -1
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/registry.d.ts +2 -0
- package/dist/dashboard/registry.d.ts.map +1 -1
- package/dist/dashboard/registry.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +123 -4
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/groups.d.ts.map +1 -1
- package/dist/dashboard/web/groups.js +8 -3
- package/dist/dashboard/web/groups.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +44 -0
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/overview.d.ts.map +1 -1
- package/dist/dashboard/web/overview.js +6 -4
- package/dist/dashboard/web/overview.js.map +1 -1
- package/dist/dashboard/web/roles.d.ts.map +1 -1
- package/dist/dashboard/web/roles.js +3 -2
- package/dist/dashboard/web/roles.js.map +1 -1
- package/dist/dashboard/web/sessions.js +2 -2
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings.d.ts.map +1 -1
- package/dist/dashboard/web/settings.js +84 -13
- package/dist/dashboard/web/settings.js.map +1 -1
- package/dist/dashboard/web/ui.d.ts +33 -0
- package/dist/dashboard/web/ui.d.ts.map +1 -1
- package/dist/dashboard/web/ui.js +84 -0
- package/dist/dashboard/web/ui.js.map +1 -1
- package/dist/dashboard-web/app.js +573 -503
- package/dist/dashboard-web/style.css +25 -0
- package/dist/dashboard.js +88 -8
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +46 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +115 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +72 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +72 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/ask-card.d.ts.map +1 -1
- package/dist/im/lark/ask-card.js +15 -1
- package/dist/im/lark/ask-card.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +17 -0
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +146 -0
- 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 +119 -4
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +14 -2
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +59 -5
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +7 -17
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +174 -50
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/forwarded-renderer.d.ts.map +1 -1
- package/dist/im/lark/forwarded-renderer.js +10 -3
- package/dist/im/lark/forwarded-renderer.js.map +1 -1
- package/dist/im/lark/merge-forward.d.ts.map +1 -1
- package/dist/im/lark/merge-forward.js +33 -8
- package/dist/im/lark/merge-forward.js.map +1 -1
- package/dist/im/lark/message-parser.d.ts +1 -0
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +1 -0
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/reply-mode-command.d.ts +2 -0
- package/dist/im/lark/reply-mode-command.d.ts.map +1 -0
- package/dist/im/lark/reply-mode-command.js +102 -0
- package/dist/im/lark/reply-mode-command.js.map +1 -0
- package/dist/services/bot-config-store.d.ts +144 -0
- package/dist/services/bot-config-store.d.ts.map +1 -0
- package/dist/services/bot-config-store.js +241 -0
- package/dist/services/bot-config-store.js.map +1 -0
- package/dist/services/card-prefs-store.d.ts +5 -0
- package/dist/services/card-prefs-store.d.ts.map +1 -1
- package/dist/services/card-prefs-store.js +47 -0
- package/dist/services/card-prefs-store.js.map +1 -1
- package/dist/services/chat-reply-mode-store.d.ts +28 -0
- package/dist/services/chat-reply-mode-store.d.ts.map +1 -0
- package/dist/services/chat-reply-mode-store.js +115 -0
- package/dist/services/chat-reply-mode-store.js.map +1 -0
- package/dist/services/groups-store.d.ts +2 -0
- package/dist/services/groups-store.d.ts.map +1 -1
- package/dist/services/groups-store.js +1 -0
- package/dist/services/groups-store.js.map +1 -1
- package/dist/services/hook-runner.d.ts +43 -0
- package/dist/services/hook-runner.d.ts.map +1 -0
- package/dist/services/hook-runner.js +394 -0
- package/dist/services/hook-runner.js.map +1 -0
- package/dist/services/observed-bots-store.d.ts.map +1 -1
- package/dist/services/observed-bots-store.js +5 -0
- package/dist/services/observed-bots-store.js.map +1 -1
- package/dist/services/restart-intent-store.d.ts +26 -0
- package/dist/services/restart-intent-store.d.ts.map +1 -0
- package/dist/services/restart-intent-store.js +84 -0
- package/dist/services/restart-intent-store.js.map +1 -0
- package/dist/services/session-lifecycle-hooks.d.ts +10 -0
- package/dist/services/session-lifecycle-hooks.d.ts.map +1 -0
- package/dist/services/session-lifecycle-hooks.js +66 -0
- package/dist/services/session-lifecycle-hooks.js.map +1 -0
- package/dist/services/session-store.d.ts +6 -0
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +25 -0
- package/dist/services/session-store.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +28 -3
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/install-info.d.ts +13 -0
- package/dist/utils/install-info.d.ts.map +1 -0
- package/dist/utils/install-info.js +56 -0
- package/dist/utils/install-info.js.map +1 -0
- package/dist/utils/listen-with-probe.d.ts +26 -0
- package/dist/utils/listen-with-probe.d.ts.map +1 -0
- package/dist/utils/listen-with-probe.js +64 -0
- package/dist/utils/listen-with-probe.js.map +1 -0
- package/dist/utils/web-terminal-listen.d.ts +30 -0
- package/dist/utils/web-terminal-listen.d.ts.map +1 -0
- package/dist/utils/web-terminal-listen.js +81 -0
- package/dist/utils/web-terminal-listen.js.map +1 -0
- package/dist/worker.js +71 -44
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +30 -30
- package/dist/workflows/events/payloads.d.ts +4 -4
- package/dist/workflows/events/schema.d.ts +156 -156
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -5,7 +5,10 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
|
-
import { config } from './config.js';
|
|
8
|
+
import { config, getDashboardExternalHost } from './config.js';
|
|
9
|
+
import { writeHeartbeat } from './core/daemon-heartbeat.js';
|
|
10
|
+
import { startMaintenance, stopMaintenance } from './core/maintenance.js';
|
|
11
|
+
import { sendRestartReportIfPending } from './core/restart-report.js';
|
|
9
12
|
import { statSync } from 'node:fs';
|
|
10
13
|
import { getChatMode, listChatMemberOpenIds, replyMessage, resolveAllowedUsersWithMap, sendMessage, sendUserMessage, updateMessage } from './im/lark/client.js';
|
|
11
14
|
import { chatHasAllowedUser, resolveGroupJoinPrompt } from './core/auto-start.js';
|
|
@@ -15,6 +18,8 @@ import * as chatFirstSeenStore from './services/chat-first-seen-store.js';
|
|
|
15
18
|
import { ensureDefaultOncallBound } from './services/oncall-store.js';
|
|
16
19
|
import * as scheduleStore from './services/schedule-store.js';
|
|
17
20
|
import * as messageQueue from './services/message-queue.js';
|
|
21
|
+
import { emitHookEvent, HOOK_EVENTS } from './services/hook-runner.js';
|
|
22
|
+
import { setSessionLifecycleShutdown } from './services/session-lifecycle-hooks.js';
|
|
18
23
|
import { parseEventMessage, resolveNonsupportMessage, stripLeadingMentions } from './im/lark/message-parser.js';
|
|
19
24
|
import { expandMergeForward } from './im/lark/merge-forward.js';
|
|
20
25
|
import { buildQuoteHint } from './im/lark/quote-hint.js';
|
|
@@ -27,9 +32,8 @@ import { buildTerminalUrl, setTerminalProxyPort } from './core/terminal-url.js';
|
|
|
27
32
|
import { startTerminalProxy } from './core/terminal-proxy.js';
|
|
28
33
|
import * as scheduler from './core/scheduler.js';
|
|
29
34
|
import { scanMultipleProjects } from './services/project-scanner.js';
|
|
30
|
-
import {
|
|
31
|
-
import { createPendingResponseQueue, markPendingResponseCardPatched,
|
|
32
|
-
import { readPendingResponsePatchMarker } from './services/pending-response-transaction-store.js';
|
|
35
|
+
import { buildQuotaExhaustedCard, buildRepoSelectCard, buildStreamingCard, getCliDisplayName } from './im/lark/card-builder.js';
|
|
36
|
+
import { createPendingResponseQueue, markPendingResponseCardPatched, syncPendingResponseState } from './core/pending-response.js';
|
|
33
37
|
import { t as tr, botLocale, localeForBot } from './i18n/index.js';
|
|
34
38
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
35
39
|
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, killWorker, scheduleCardPatch, setCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, writableTerminalLinkFor, } from './core/worker-pool.js';
|
|
@@ -41,6 +45,8 @@ import { isCallbackUrl, handleCallbackUrl } from './utils/user-token.js';
|
|
|
41
45
|
import { consumeQuota, removeChatGrant, removeGlobalGrant } from './services/grant-store.js';
|
|
42
46
|
import { abortCharge, commitCharge, beginCharge } from './services/quota-dedup.js';
|
|
43
47
|
import { getSessionWorkingDir, getProjectScanDirs, expandHome, downloadResources, formatAttachmentsHint, buildNewTopicPrompt, buildFollowUpContent, buildBridgeInputContent, buildReforkPrompt, getAvailableBots, restoreActiveSessions, executeScheduledTask, persistStreamCardState, rememberLastCliInput, ensureTerminalWorkerPort, } from './core/session-manager.js';
|
|
48
|
+
import { beginReplyTargetTurn, resolveSessionReplyTarget, syncReplyTargetState } from './core/reply-target.js';
|
|
49
|
+
import { sweepIdleWorkers } from './core/idle-worker-sweeper.js';
|
|
44
50
|
import { handleCardAction } from './im/lark/card-handler.js';
|
|
45
51
|
import { executeWorkflowCommand, parseWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
|
|
46
52
|
import { workflowRunDetailUrl } from './im/lark/workflow-cards.js';
|
|
@@ -68,12 +74,32 @@ import { resolveWait } from './workflows/wait.js';
|
|
|
68
74
|
import { replay } from './workflows/events/replay.js';
|
|
69
75
|
import { isValidRunId, readRunSnapshot } from './workflows/ops-projection.js';
|
|
70
76
|
import { AttemptResumeManager } from './workflows/attempt-resume.js';
|
|
71
|
-
import { setCardDispatcher as setAskCardDispatcher, registerAsk as registerAskBroker, } from './core/ask-broker.js';
|
|
77
|
+
import { setCardDispatcher as setAskCardDispatcher, registerAsk as registerAskBroker, findPendingAskByAnchor, submitCustomReply, } from './core/ask-broker.js';
|
|
72
78
|
import { parseAskBody, resolveAskApprovers } from './core/ask-api.js';
|
|
73
79
|
import { createLarkAskCardDispatcher } from './im/lark/ask-card.js';
|
|
74
80
|
// ─── State ───────────────────────────────────────────────────────────────────
|
|
75
81
|
const activeSessions = new Map();
|
|
76
82
|
const workflowEventWatchers = new Map();
|
|
83
|
+
function sessionHasReplyThreadAlias(s, rootId) {
|
|
84
|
+
return s.scope === 'chat' && !!s.replyThreadAliases?.[rootId];
|
|
85
|
+
}
|
|
86
|
+
function findChatReplyAlias(rootId, chatId, larkAppId) {
|
|
87
|
+
// A real thread-scope session at this root wins over any historical alias.
|
|
88
|
+
if (activeSessions.get(sessionKey(rootId, larkAppId))?.scope === 'thread')
|
|
89
|
+
return null;
|
|
90
|
+
for (const ds of activeSessions.values()) {
|
|
91
|
+
if (ds.larkAppId !== larkAppId || ds.scope !== 'chat' || ds.chatId !== chatId)
|
|
92
|
+
continue;
|
|
93
|
+
if (sessionHasReplyThreadAlias(ds.session, rootId))
|
|
94
|
+
return { chatId: ds.chatId, sessionId: ds.session.sessionId };
|
|
95
|
+
}
|
|
96
|
+
const diskSessions = sessionStore.listSessions();
|
|
97
|
+
if (diskSessions.some(s => s.status === 'active' && s.larkAppId === larkAppId && s.scope !== 'chat' && s.rootMessageId === rootId)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const hit = diskSessions.find(s => s.status === 'active' && s.larkAppId === larkAppId && s.chatId === chatId && sessionHasReplyThreadAlias(s, rootId));
|
|
101
|
+
return hit ? { chatId: hit.chatId, sessionId: hit.sessionId } : null;
|
|
102
|
+
}
|
|
77
103
|
const workflowRuns = new Map();
|
|
78
104
|
// v0.1.5 slice 1: run-level progress card index. daemon-internal only
|
|
79
105
|
// (codex contract boundary 2: daemon restart drops the cardMessageId
|
|
@@ -213,29 +239,24 @@ function readSessionFreshFromDisk(sessionId, larkAppId) {
|
|
|
213
239
|
}
|
|
214
240
|
return undefined;
|
|
215
241
|
}
|
|
216
|
-
async function postPendingResponseCard(ds, replyToMessageId, prompt, sender) {
|
|
217
|
-
|
|
218
|
-
|
|
242
|
+
async function postPendingResponseCard(ds, replyToMessageId, prompt, sender, turnId) {
|
|
243
|
+
// Card-off means no visible botmux cards at all. If a prior build left an
|
|
244
|
+
// open pending-response placeholder on this session, clear its state so a
|
|
245
|
+
// later `botmux send --mention...` cannot patch it to “final reply sent via
|
|
246
|
+
// new message”. Do not call any Lark send/update API here.
|
|
219
247
|
await pendingResponseQueue.run(ds.session.sessionId, async () => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (
|
|
248
|
+
const fresh = readSessionFreshFromDisk(ds.session.sessionId, ds.larkAppId);
|
|
249
|
+
syncPendingResponseState(ds, fresh);
|
|
250
|
+
if (fresh)
|
|
251
|
+
syncReplyTargetState(ds, fresh);
|
|
252
|
+
if (ds.pendingResponseCardId || ds.session.pendingResponseCardId) {
|
|
223
253
|
markPendingResponseCardPatched(ds);
|
|
224
|
-
|
|
225
|
-
syncPendingResponseState(ds.session, ds);
|
|
226
|
-
const card = buildPendingResponseCard(localeForBot(ds.larkAppId));
|
|
227
|
-
try {
|
|
228
|
-
const messageId = await replyMessage(ds.larkAppId, replyToMessageId, card, 'interactive', shouldReplyPendingInThread(ds.scope));
|
|
229
|
-
startPendingResponseTurn(ds, messageId);
|
|
230
|
-
startPendingResponseTurn(ds.session, messageId);
|
|
254
|
+
markPendingResponseCardPatched(ds.session);
|
|
231
255
|
sessionStore.updateSession(ds.session);
|
|
232
256
|
}
|
|
233
|
-
catch (err) {
|
|
234
|
-
logger.warn(`[${tag(ds)}] failed to post pending response card: ${err instanceof Error ? err.message : String(err)}`);
|
|
235
|
-
}
|
|
236
257
|
});
|
|
237
258
|
}
|
|
238
|
-
async function sessionReply(anchor, content, msgType = 'text', larkAppId) {
|
|
259
|
+
async function sessionReply(anchor, content, msgType = 'text', larkAppId, turnId) {
|
|
239
260
|
let ds;
|
|
240
261
|
if (larkAppId) {
|
|
241
262
|
ds = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
@@ -251,6 +272,11 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId) {
|
|
|
251
272
|
const appId = larkAppId ?? ds?.larkAppId ?? getAllBots()[0]?.config.larkAppId;
|
|
252
273
|
if (!appId)
|
|
253
274
|
throw new Error('No bot configured');
|
|
275
|
+
const hookContext = ds ? {
|
|
276
|
+
sessionId: ds.session.sessionId,
|
|
277
|
+
scope: ds.scope,
|
|
278
|
+
anchor: sessionAnchorId(ds),
|
|
279
|
+
} : undefined;
|
|
254
280
|
// Chat-scope: post a plain message to the chat. No reply_in_thread → keeps
|
|
255
281
|
// the conversation flat in 普通群. The card layer carries chatId in its button
|
|
256
282
|
// values, so handleCardAction routes back via sessionKey(chatId).
|
|
@@ -267,17 +293,25 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId) {
|
|
|
267
293
|
// to know we should sendMessage, not reply_in_thread to a non-message-id.
|
|
268
294
|
if (ds?.scope === 'chat' || anchor.startsWith('oc_')) {
|
|
269
295
|
const chatId = ds?.chatId ?? anchor;
|
|
270
|
-
if (ds?.scope === 'chat'
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
296
|
+
if (ds?.scope === 'chat') {
|
|
297
|
+
const fresh = readSessionFreshFromDisk(ds.session.sessionId, ds.larkAppId);
|
|
298
|
+
if (fresh)
|
|
299
|
+
syncReplyTargetState(ds, fresh);
|
|
300
|
+
const target = resolveSessionReplyTarget(ds, turnId);
|
|
301
|
+
if (target.mode === 'thread')
|
|
302
|
+
return replyMessage(appId, target.rootMessageId, content, msgType, true, undefined, hookContext);
|
|
303
|
+
if (ds.session.rootMessageId) {
|
|
304
|
+
const mode = await getChatMode(appId, chatId, { forceRefresh: true });
|
|
305
|
+
if (mode === 'topic') {
|
|
306
|
+
logger.warn(`[routing] Chat-scope session ${ds.session.sessionId.substring(0, 8)} is now topic-mode; replying in original thread ${ds.session.rootMessageId.substring(0, 12)}`);
|
|
307
|
+
return replyMessage(appId, ds.session.rootMessageId, content, msgType, true, undefined, hookContext);
|
|
308
|
+
}
|
|
275
309
|
}
|
|
276
310
|
}
|
|
277
|
-
return sendMessage(appId, chatId, content, msgType);
|
|
311
|
+
return sendMessage(appId, chatId, content, msgType, undefined, hookContext);
|
|
278
312
|
}
|
|
279
313
|
// Thread-scope (or unknown / legacy): reply in thread.
|
|
280
|
-
return replyMessage(appId, anchor, content, msgType, true);
|
|
314
|
+
return replyMessage(appId, anchor, content, msgType, true, undefined, hookContext);
|
|
281
315
|
}
|
|
282
316
|
async function revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev) {
|
|
283
317
|
const result = ev.reason === 'chatGrant'
|
|
@@ -1374,6 +1408,33 @@ ipcRoute('POST', '/api/asks', async (req, res) => {
|
|
|
1374
1408
|
});
|
|
1375
1409
|
return jsonRes(res, 200, result);
|
|
1376
1410
|
});
|
|
1411
|
+
// ─── hooks emit 转发端点 ────────────────────────────────────────────────────
|
|
1412
|
+
// CLI side(botmux send 等)调用 emitHookEvent 时,把事件转发到 daemon 这条
|
|
1413
|
+
// 接口;daemon 在自己的长寿命事件循环里负责 spawn hook、跑 timeout、超时杀
|
|
1414
|
+
// 整个进程组。短命 CLI 进程的 timer.unref 会让超时承诺失效、跑飞的 hook 留
|
|
1415
|
+
// 孤儿,让 daemon 接管根治这一缺口。daemon 进程自身不带 BOTMUX_SESSION_ID
|
|
1416
|
+
// 环境变量,所以这里调 emitHookEvent 不会再触发转发回退(无递归)。
|
|
1417
|
+
ipcRoute('POST', '/api/hooks/emit', async (req, res) => {
|
|
1418
|
+
let raw;
|
|
1419
|
+
try {
|
|
1420
|
+
raw = await readJsonBody(req);
|
|
1421
|
+
}
|
|
1422
|
+
catch {
|
|
1423
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_json' });
|
|
1424
|
+
}
|
|
1425
|
+
if (!raw || typeof raw !== 'object') {
|
|
1426
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_body' });
|
|
1427
|
+
}
|
|
1428
|
+
const { event, payload } = raw;
|
|
1429
|
+
if (typeof event !== 'string' || !HOOK_EVENTS.includes(event)) {
|
|
1430
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_event' });
|
|
1431
|
+
}
|
|
1432
|
+
if (!payload || typeof payload !== 'object') {
|
|
1433
|
+
return jsonRes(res, 400, { ok: false, error: 'bad_payload' });
|
|
1434
|
+
}
|
|
1435
|
+
emitHookEvent(event, payload);
|
|
1436
|
+
return jsonRes(res, 202, { ok: true });
|
|
1437
|
+
});
|
|
1377
1438
|
// ─── adopt-session 查询端点 ───────────────────────────────────────────────────
|
|
1378
1439
|
// CLI side(botmux hook)通过祖先 PID 匹配 adopt 会话,路由 askUserQuestion。
|
|
1379
1440
|
// GET /api/adopt-session/:pid — 返回该 pid 对应的 adopt 会话路由信息。
|
|
@@ -1493,7 +1554,7 @@ async function replyInvalidWorkingDirs(anchor, larkAppId, ds) {
|
|
|
1493
1554
|
return true;
|
|
1494
1555
|
}
|
|
1495
1556
|
async function handleNewTopic(data, ctx) {
|
|
1496
|
-
const { chatId, messageId, chatType, larkAppId } = ctx;
|
|
1557
|
+
const { chatId, messageId, chatType, larkAppId, replyRootId } = ctx;
|
|
1497
1558
|
// scope/anchor are mutable here: `/t` / `/topic` may flip a 普通群 chat-scope
|
|
1498
1559
|
// routing into thread-scope so the bot's first reply seeds a Lark thread.
|
|
1499
1560
|
let scope = ctx.scope;
|
|
@@ -1538,6 +1599,18 @@ async function handleNewTopic(data, ctx) {
|
|
|
1538
1599
|
const senderUnionId = data.sender?.sender_id?.union_id;
|
|
1539
1600
|
const botCfg = getBot(larkAppId).config;
|
|
1540
1601
|
logger.info(`New session: "${content.substring(0, 60)}" (scope=${scope}, anchor=${anchor.substring(0, 12)}, resources: ${resources.length}, active: ${getActiveCount()}, messageId: ${messageId}, chatId: ${chatId})`);
|
|
1602
|
+
emitHookEvent('topic.new', {
|
|
1603
|
+
larkAppId,
|
|
1604
|
+
chatId,
|
|
1605
|
+
chatType,
|
|
1606
|
+
scope,
|
|
1607
|
+
anchor,
|
|
1608
|
+
messageId,
|
|
1609
|
+
senderOpenId,
|
|
1610
|
+
senderType: parsed.senderType,
|
|
1611
|
+
msgType: parsed.msgType,
|
|
1612
|
+
content,
|
|
1613
|
+
});
|
|
1541
1614
|
if (parseWorkflowCommand(cmdContent)) {
|
|
1542
1615
|
if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, '/workflow')) {
|
|
1543
1616
|
return;
|
|
@@ -1720,6 +1793,8 @@ async function handleNewTopic(data, ctx) {
|
|
|
1720
1793
|
ds.session.workingDir = pinnedWorkingDir;
|
|
1721
1794
|
sessionStore.updateSession(ds.session);
|
|
1722
1795
|
}
|
|
1796
|
+
beginReplyTargetTurn(ds, replyRootId, messageId);
|
|
1797
|
+
sessionStore.updateSession(ds.session);
|
|
1723
1798
|
activeSessions.set(sessionKey(anchor, larkAppId), ds);
|
|
1724
1799
|
// Pinned (oncall binding or inherited from sibling bot): spawn CLI immediately.
|
|
1725
1800
|
if (pinnedWorkingDir) {
|
|
@@ -1728,7 +1803,7 @@ async function handleNewTopic(data, ctx) {
|
|
|
1728
1803
|
const selfBot = getBot(larkAppId);
|
|
1729
1804
|
const prompt = buildNewTopicPrompt(promptContent, session.sessionId, botCfg.cliId, botCfg.cliPathOverride, attachments, parsed.mentions, await getAvailableBots(larkAppId, chatId), undefined, { name: selfBot.botName, openId: selfBot.botOpenId }, localeForBot(larkAppId), newTopicSender, { larkAppId, chatId });
|
|
1730
1805
|
rememberLastCliInput(ds, promptContent, prompt);
|
|
1731
|
-
await postPendingResponseCard(ds, messageId, content, newTopicSender);
|
|
1806
|
+
await postPendingResponseCard(ds, messageId, content, newTopicSender, messageId);
|
|
1732
1807
|
forkWorker(ds, prompt);
|
|
1733
1808
|
const reason = oncallEntry
|
|
1734
1809
|
? `oncall-bound chat ${chatId}`
|
|
@@ -1760,7 +1835,7 @@ async function handleNewTopic(data, ctx) {
|
|
|
1760
1835
|
const selfBot = getBot(larkAppId);
|
|
1761
1836
|
const prompt = buildNewTopicPrompt(promptContent, session.sessionId, botCfg.cliId, botCfg.cliPathOverride, attachments, parsed.mentions, await getAvailableBots(larkAppId, chatId), undefined, { name: selfBot.botName, openId: selfBot.botOpenId }, localeForBot(larkAppId), newTopicSender, { larkAppId, chatId });
|
|
1762
1837
|
rememberLastCliInput(ds, promptContent, prompt);
|
|
1763
|
-
await postPendingResponseCard(ds, messageId, content, newTopicSender);
|
|
1838
|
+
await postPendingResponseCard(ds, messageId, content, newTopicSender, messageId);
|
|
1764
1839
|
forkWorker(ds, prompt);
|
|
1765
1840
|
logger.info(`Session ${session.sessionId} ready (no projects to select), total active: ${getActiveCount()}`);
|
|
1766
1841
|
}
|
|
@@ -1987,7 +2062,7 @@ function lookupForeignBotName(senderOpenId, larkAppId) {
|
|
|
1987
2062
|
return 'Bot';
|
|
1988
2063
|
}
|
|
1989
2064
|
async function handleThreadReply(data, ctx) {
|
|
1990
|
-
const { chatId: ctxChatId, chatType: ctxChatType, scope, anchor, larkAppId } = ctx;
|
|
2065
|
+
const { chatId: ctxChatId, chatType: ctxChatType, scope, anchor, larkAppId, replyRootId } = ctx;
|
|
1991
2066
|
await resolveNonsupportMessage(data, larkAppId);
|
|
1992
2067
|
const { parsed, resources } = parseEventMessage(data);
|
|
1993
2068
|
// Expand merge_forward: fetch sub-messages and collect their resources
|
|
@@ -2021,6 +2096,22 @@ async function handleThreadReply(data, ctx) {
|
|
|
2021
2096
|
? `${tr('daemon.foreign_bot_mention_prefix', { botName: foreignBotName }, localeForBot(larkAppId))}\n`
|
|
2022
2097
|
: '';
|
|
2023
2098
|
const promptContent = buildQuoteHint(parsed, scope, anchor) + botSenderPrefix + parsed.content;
|
|
2099
|
+
const existingHookSession = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
2100
|
+
emitHookEvent('thread.reply', {
|
|
2101
|
+
larkAppId,
|
|
2102
|
+
chatId: ctxChatId,
|
|
2103
|
+
chatType: ctxChatType,
|
|
2104
|
+
scope,
|
|
2105
|
+
anchor,
|
|
2106
|
+
messageId: parsed.messageId,
|
|
2107
|
+
rootId: parsed.rootId,
|
|
2108
|
+
parentId: parsed.parentId,
|
|
2109
|
+
senderOpenId: senderOpenIdForPrefix,
|
|
2110
|
+
senderType: parsed.senderType,
|
|
2111
|
+
msgType: parsed.msgType,
|
|
2112
|
+
sessionId: existingHookSession?.session.sessionId,
|
|
2113
|
+
content: parsed.content,
|
|
2114
|
+
});
|
|
2024
2115
|
if (isForeignBot) {
|
|
2025
2116
|
logger.info(`[${larkAppId}] foreign-bot @mention prefix attached: sender=${senderOpenIdForPrefix?.substring(0, 12)} ` +
|
|
2026
2117
|
`senderType=${parsed.senderType} via=${isBotSenderType ? 'sender_type' : 'cross-ref'}`);
|
|
@@ -2163,6 +2254,30 @@ async function handleThreadReply(data, ctx) {
|
|
|
2163
2254
|
return;
|
|
2164
2255
|
}
|
|
2165
2256
|
}
|
|
2257
|
+
// 自定义回复拦截:该话题有未结的 ask 且发送者有答复权限 → 把这条文字当答案,
|
|
2258
|
+
// 走 submitCustomReply settle 掉 ask(替代选项语义),不再当作新一轮指令喂给 CLI。
|
|
2259
|
+
// 此时发起 ask 的 CLI 正阻塞等结果,回什么都得先等 ask 结束,故无副作用。
|
|
2260
|
+
// 仅拦截纯文字(slash 命令 / 回调 URL / workflow 已在上方各自 return,可用来中止);
|
|
2261
|
+
// 外部 bot 的 open_id 不在 approvers 里,天然不会命中。非授权人 / 空文字则落到正常
|
|
2262
|
+
// 路由。卡片由 broker.onSettle 自动 PATCH 反映答案,无需额外回消息。
|
|
2263
|
+
if (threadSenderOpenId && threadChatId) {
|
|
2264
|
+
const askReplyText = cmdContent.trim();
|
|
2265
|
+
if (askReplyText) {
|
|
2266
|
+
const pendingAsk = findPendingAskByAnchor({ larkAppId, chatId: threadChatId, anchor });
|
|
2267
|
+
if (pendingAsk && pendingAsk.approvers.has(threadSenderOpenId)) {
|
|
2268
|
+
const outcome = submitCustomReply({
|
|
2269
|
+
askId: pendingAsk.askId,
|
|
2270
|
+
by: threadSenderOpenId,
|
|
2271
|
+
text: askReplyText,
|
|
2272
|
+
});
|
|
2273
|
+
if (outcome === 'accepted') {
|
|
2274
|
+
logger.info(`[${anchor.substring(0, 12)}] ask custom reply accepted from ${threadSenderOpenId.substring(0, 12)}`);
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
logger.info(`[${anchor.substring(0, 12)}] ask custom reply not accepted (${outcome}); falling through to normal routing`);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2166
2281
|
logger.info(`Reply in ${scope}-scope session ${anchor.substring(0, 12)}: ${content.substring(0, 100)} (resources: ${resources.length})`);
|
|
2167
2282
|
let ds = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
2168
2283
|
// If another bot already owns this anchor, ignore unmentioned replies here as a
|
|
@@ -2210,6 +2325,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
2210
2325
|
ds.session.quoteTargetId = parsed.messageId;
|
|
2211
2326
|
ds.session.quoteTargetSenderOpenId = callerOpenId;
|
|
2212
2327
|
ds.session.quoteTargetSenderIsBot = isForeignBot;
|
|
2328
|
+
beginReplyTargetTurn(ds, replyRootId, parsed.messageId);
|
|
2213
2329
|
if (callerOpenId && ds.session.lastCallerOpenId !== callerOpenId) {
|
|
2214
2330
|
ds.session.lastCallerOpenId = callerOpenId;
|
|
2215
2331
|
}
|
|
@@ -2350,6 +2466,8 @@ async function handleThreadReply(data, ctx) {
|
|
|
2350
2466
|
newDs.session.workingDir = pinnedWorkingDir;
|
|
2351
2467
|
sessionStore.updateSession(newDs.session);
|
|
2352
2468
|
}
|
|
2469
|
+
beginReplyTargetTurn(newDs, replyRootId, parsed.messageId);
|
|
2470
|
+
sessionStore.updateSession(newDs.session);
|
|
2353
2471
|
activeSessions.set(sessionKey(anchor, larkAppId), newDs);
|
|
2354
2472
|
// Pinned (oncall binding or inherited from peer bot in same thread):
|
|
2355
2473
|
// spawn CLI immediately, skip repo selection.
|
|
@@ -2359,7 +2477,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
2359
2477
|
const selfBot = getBot(larkAppId);
|
|
2360
2478
|
const prompt = buildNewTopicPrompt(promptContent, session.sessionId, botCfg.cliId, botCfg.cliPathOverride, attachments, parsed.mentions, await getAvailableBots(larkAppId, autoCreateChatId), undefined, { name: selfBot.botName, openId: selfBot.botOpenId }, localeForBot(larkAppId), autoCreateSender, { larkAppId, chatId: autoCreateChatId });
|
|
2361
2479
|
rememberLastCliInput(newDs, promptContent, prompt);
|
|
2362
|
-
await postPendingResponseCard(newDs, parsed.messageId, parsed.content, autoCreateSender);
|
|
2480
|
+
await postPendingResponseCard(newDs, parsed.messageId, parsed.content, autoCreateSender, parsed.messageId);
|
|
2363
2481
|
forkWorker(newDs, prompt);
|
|
2364
2482
|
const reason = oncallEntry
|
|
2365
2483
|
? `oncall-bound chat ${autoCreateChatId}`
|
|
@@ -2391,7 +2509,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
2391
2509
|
const selfBot = getBot(larkAppId);
|
|
2392
2510
|
const prompt = buildNewTopicPrompt(promptContent, session.sessionId, botCfg.cliId, botCfg.cliPathOverride, attachments, parsed.mentions, await getAvailableBots(larkAppId, autoCreateChatId), undefined, { name: selfBot.botName, openId: selfBot.botOpenId }, localeForBot(larkAppId), autoCreateSender, { larkAppId, chatId: autoCreateChatId });
|
|
2393
2511
|
rememberLastCliInput(newDs, promptContent, prompt);
|
|
2394
|
-
await postPendingResponseCard(newDs, parsed.messageId, parsed.content, autoCreateSender);
|
|
2512
|
+
await postPendingResponseCard(newDs, parsed.messageId, parsed.content, autoCreateSender, parsed.messageId);
|
|
2395
2513
|
forkWorker(newDs, prompt);
|
|
2396
2514
|
}
|
|
2397
2515
|
return;
|
|
@@ -2427,8 +2545,8 @@ async function handleThreadReply(data, ctx) {
|
|
|
2427
2545
|
});
|
|
2428
2546
|
beginNewTurn(ds, parsed.content);
|
|
2429
2547
|
rememberLastCliInput(ds, promptContent, msgContent);
|
|
2430
|
-
await postPendingResponseCard(ds, parsed.messageId, parsed.content, await getThreadSender());
|
|
2431
|
-
ds.worker.send({ type: 'message', content: msgContent });
|
|
2548
|
+
await postPendingResponseCard(ds, parsed.messageId, parsed.content, await getThreadSender(), parsed.messageId);
|
|
2549
|
+
ds.worker.send({ type: 'message', content: msgContent, turnId: parsed.messageId });
|
|
2432
2550
|
}
|
|
2433
2551
|
else {
|
|
2434
2552
|
// Worker not running — re-fork with resume. This is a NEW turn, so drop
|
|
@@ -2476,11 +2594,47 @@ async function handleThreadReply(data, ctx) {
|
|
|
2476
2594
|
sender: await getThreadSender(),
|
|
2477
2595
|
});
|
|
2478
2596
|
rememberLastCliInput(ds, promptContent, wrappedPrompt);
|
|
2479
|
-
await postPendingResponseCard(ds, parsed.messageId, parsed.content, await getThreadSender());
|
|
2597
|
+
await postPendingResponseCard(ds, parsed.messageId, parsed.content, await getThreadSender(), parsed.messageId);
|
|
2598
|
+
sessionStore.updateSession(ds.session);
|
|
2480
2599
|
forkWorker(ds, wrappedPrompt, ds.hasHistory);
|
|
2481
2600
|
}
|
|
2482
2601
|
}
|
|
2483
2602
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
2603
|
+
/** Owner to DM for the restart report: the bot's first resolved allowedUser
|
|
2604
|
+
* (open_id). Falls back to a raw `ou_…` entry in the config. */
|
|
2605
|
+
function resolvePrimaryOwnerOpenId(larkAppId) {
|
|
2606
|
+
try {
|
|
2607
|
+
const bot = getBot(larkAppId);
|
|
2608
|
+
const resolved = (bot.resolvedAllowedUsers ?? []).find(u => typeof u === 'string' && u.startsWith('ou_'));
|
|
2609
|
+
if (resolved)
|
|
2610
|
+
return resolved;
|
|
2611
|
+
return (bot.config.allowedUsers ?? []).find(u => typeof u === 'string' && u.startsWith('ou_'));
|
|
2612
|
+
}
|
|
2613
|
+
catch {
|
|
2614
|
+
return undefined;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
/** Build the current dashboard URL (active token, not a rotation) from the
|
|
2618
|
+
* dashboard process's persisted `.dashboard-port` / `.dashboard-token`. Falls
|
|
2619
|
+
* back to a token-less base URL if the dashboard hasn't published a token yet. */
|
|
2620
|
+
function dashboardUrlForReport() {
|
|
2621
|
+
try {
|
|
2622
|
+
const dir = join(homedir(), '.botmux');
|
|
2623
|
+
const portFile = join(dir, '.dashboard-port');
|
|
2624
|
+
const tokenFile = join(dir, '.dashboard-token');
|
|
2625
|
+
const port = existsSync(portFile) ? readFileSync(portFile, 'utf8').trim() : String(config.dashboard.port);
|
|
2626
|
+
const base = `http://${getDashboardExternalHost()}:${port}/`;
|
|
2627
|
+
if (existsSync(tokenFile)) {
|
|
2628
|
+
const tok = readFileSync(tokenFile, 'utf8').trim();
|
|
2629
|
+
if (tok)
|
|
2630
|
+
return `${base}?t=${tok}`;
|
|
2631
|
+
}
|
|
2632
|
+
return base;
|
|
2633
|
+
}
|
|
2634
|
+
catch {
|
|
2635
|
+
return undefined;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2484
2638
|
export async function startDaemon(botIndex) {
|
|
2485
2639
|
// 首次启动时后台尝试安装 CJK 字体(Debian/Ubuntu),避免截图中文显示豆腐块。
|
|
2486
2640
|
// 不阻塞:首张截图可能仍是豆腐块,装完重启 daemon 即可正常。
|
|
@@ -2560,6 +2714,11 @@ export async function startDaemon(botIndex) {
|
|
|
2560
2714
|
// newly-started daemon's hydrate failing on dashboard startup. Binds to
|
|
2561
2715
|
// 127.0.0.1 only since the dashboard sibling runs on the same host.
|
|
2562
2716
|
const ipcHandle = await startIpcServer({ port: ipcPort, host: '127.0.0.1' });
|
|
2717
|
+
// startIpcServer probes upward on EADDRINUSE (e.g. a second botmux instance on
|
|
2718
|
+
// this host already holds ipcBasePort+idx), so the bound port may differ from
|
|
2719
|
+
// the requested one. Republish the ACTUAL port into the descriptor before it
|
|
2720
|
+
// is written below — the dashboard reaches us via desc.ipcPort verbatim.
|
|
2721
|
+
desc.ipcPort = ipcHandle.port;
|
|
2563
2722
|
logger.info(`[dashboard-ipc] listening on 127.0.0.1:${ipcHandle.port} (bot ${idx})`);
|
|
2564
2723
|
// Single reverse-proxy port that fronts every session's web terminal under
|
|
2565
2724
|
// /s/{sessionId}, so dev-machine users forward one port (proxyBasePort+idx)
|
|
@@ -2649,8 +2808,17 @@ export async function startDaemon(botIndex) {
|
|
|
2649
2808
|
probeBotOpenId(cfg.larkAppId).then(() => {
|
|
2650
2809
|
writeBotInfoFile(config.session.dataDir);
|
|
2651
2810
|
const probedName = bot.botName;
|
|
2811
|
+
const probedAvatar = bot.botAvatarUrl;
|
|
2812
|
+
let descChanged = false;
|
|
2652
2813
|
if (probedName && probedName !== desc.botName) {
|
|
2653
2814
|
desc.botName = probedName;
|
|
2815
|
+
descChanged = true;
|
|
2816
|
+
}
|
|
2817
|
+
if (probedAvatar && probedAvatar !== desc.botAvatarUrl) {
|
|
2818
|
+
desc.botAvatarUrl = probedAvatar;
|
|
2819
|
+
descChanged = true;
|
|
2820
|
+
}
|
|
2821
|
+
if (descChanged) {
|
|
2654
2822
|
try {
|
|
2655
2823
|
writeDaemonDescriptor(desc);
|
|
2656
2824
|
}
|
|
@@ -2686,6 +2854,7 @@ export async function startDaemon(botIndex) {
|
|
|
2686
2854
|
handleThreadReply: (data, ctx) => handleThreadReply(data, ctx),
|
|
2687
2855
|
handleBotAdded: (chatId, operatorOpenId, appId) => handleBotAdded(chatId, operatorOpenId, appId),
|
|
2688
2856
|
isSessionOwner: (anchor, appId) => activeSessions.has(sessionKey(anchor, appId)),
|
|
2857
|
+
resolveReplyThreadAlias: (rootId, chatId, appId) => findChatReplyAlias(rootId, chatId, appId),
|
|
2689
2858
|
// Chat was converted 普通群 → 话题群 while we held a chat-scope session.
|
|
2690
2859
|
// Evict it from the routing map so subsequent inbound messages can land
|
|
2691
2860
|
// on a fresh thread-scope session (dispatcher already rerouted this turn
|
|
@@ -2703,6 +2872,13 @@ export async function startDaemon(botIndex) {
|
|
|
2703
2872
|
}
|
|
2704
2873
|
// Restore active sessions from previous run
|
|
2705
2874
|
await restoreActiveSessions(activeSessions);
|
|
2875
|
+
const idleWorkerSweepTimer = setInterval(() => {
|
|
2876
|
+
const suspended = sweepIdleWorkers(activeSessions);
|
|
2877
|
+
if (suspended.length > 0) {
|
|
2878
|
+
logger.info(`[idle-worker-sweeper] suspended ${suspended.length} idle worker(s)`);
|
|
2879
|
+
}
|
|
2880
|
+
}, 60_000);
|
|
2881
|
+
idleWorkerSweepTimer.unref?.();
|
|
2706
2882
|
await attachColdWorkflowRuns(cfg.larkAppId);
|
|
2707
2883
|
// Start scheduler in every daemon. Each daemon owns exactly one bot, so
|
|
2708
2884
|
// each filters to only execute tasks whose `larkAppId` matches its bot
|
|
@@ -2711,6 +2887,39 @@ export async function startDaemon(botIndex) {
|
|
|
2711
2887
|
scheduler.setExecuteCallback((task) => executeScheduledTask(task, activeSessions, refreshCliVersion));
|
|
2712
2888
|
scheduler.setOwnerFilter(cfg.larkAppId, idx === 0);
|
|
2713
2889
|
scheduler.startScheduler();
|
|
2890
|
+
// Cross-daemon busy heartbeat: each daemon reports how many of its sessions
|
|
2891
|
+
// are mid-CLI-turn so the primary daemon's maintenance gate sees activity
|
|
2892
|
+
// across all bots (one daemon per bot). See core/daemon-heartbeat.ts.
|
|
2893
|
+
const writeBusyHeartbeat = () => {
|
|
2894
|
+
try {
|
|
2895
|
+
let busy = 0;
|
|
2896
|
+
for (const [, ds] of activeSessions) {
|
|
2897
|
+
if (ds.worker && !ds.worker.killed && ds.lastScreenStatus === 'working')
|
|
2898
|
+
busy++;
|
|
2899
|
+
}
|
|
2900
|
+
writeHeartbeat(cfg.larkAppId, busy);
|
|
2901
|
+
}
|
|
2902
|
+
catch { /* best-effort */ }
|
|
2903
|
+
};
|
|
2904
|
+
writeBusyHeartbeat();
|
|
2905
|
+
const maintenanceHeartbeat = setInterval(writeBusyHeartbeat, 15_000);
|
|
2906
|
+
maintenanceHeartbeat.unref?.();
|
|
2907
|
+
// Auto-update / auto-restart and the restart-report DM run only on the
|
|
2908
|
+
// primary daemon (bot-0) — a restart is host-wide.
|
|
2909
|
+
if (idx === 0) {
|
|
2910
|
+
startMaintenance();
|
|
2911
|
+
// After an intentional restart, DM the owner a summary. Delayed a few
|
|
2912
|
+
// seconds so the dashboard process can publish its token first.
|
|
2913
|
+
setTimeout(() => {
|
|
2914
|
+
void sendRestartReportIfPending({
|
|
2915
|
+
primaryLarkAppId: cfg.larkAppId,
|
|
2916
|
+
ownerOpenId: resolvePrimaryOwnerOpenId(cfg.larkAppId),
|
|
2917
|
+
dashboardUrl: dashboardUrlForReport(),
|
|
2918
|
+
sendCard: (openId, card) => sendUserMessage(cfg.larkAppId, openId, card, 'interactive').then(() => undefined),
|
|
2919
|
+
log: (m) => logger.info(`[restart-report] ${m}`),
|
|
2920
|
+
});
|
|
2921
|
+
}, 5_000).unref?.();
|
|
2922
|
+
}
|
|
2714
2923
|
// Graceful shutdown. Sends SIGTERM (or `{type:'close'}` IPC via killWorker)
|
|
2715
2924
|
// to every worker, then waits up to SHUTDOWN_GRACE_MS for them to exit
|
|
2716
2925
|
// before sending SIGKILL to stragglers. Without the wait, daemon
|
|
@@ -2725,13 +2934,17 @@ export async function startDaemon(botIndex) {
|
|
|
2725
2934
|
if (shuttingDown)
|
|
2726
2935
|
return;
|
|
2727
2936
|
shuttingDown = true;
|
|
2937
|
+
setSessionLifecycleShutdown(true);
|
|
2728
2938
|
logger.info(`Daemon shutting down... (active: ${getActiveCount()})`);
|
|
2729
2939
|
scheduler.stopScheduler();
|
|
2940
|
+
stopMaintenance();
|
|
2941
|
+
clearInterval(maintenanceHeartbeat);
|
|
2730
2942
|
for (const watcher of workflowEventWatchers.values())
|
|
2731
2943
|
watcher.close();
|
|
2732
2944
|
workflowEventWatchers.clear();
|
|
2733
2945
|
workflowRuns.clear();
|
|
2734
2946
|
clearInterval(descriptorHeartbeat);
|
|
2947
|
+
clearInterval(idleWorkerSweepTimer);
|
|
2735
2948
|
if (memoryDiagnostics)
|
|
2736
2949
|
clearInterval(memoryDiagnostics);
|
|
2737
2950
|
removeDaemonDescriptor(cfg.larkAppId);
|
|
@@ -2805,6 +3018,7 @@ export async function startDaemon(botIndex) {
|
|
|
2805
3018
|
// the descriptor so the dashboard doesn't see a phantom daemon.
|
|
2806
3019
|
process.on('exit', () => {
|
|
2807
3020
|
clearInterval(descriptorHeartbeat);
|
|
3021
|
+
clearInterval(idleWorkerSweepTimer);
|
|
2808
3022
|
if (memoryDiagnostics)
|
|
2809
3023
|
clearInterval(memoryDiagnostics);
|
|
2810
3024
|
removeDaemonDescriptor(cfg.larkAppId);
|