botmux 2.77.0 → 2.79.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/bot-registry.d.ts +7 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +3 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -8
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +90 -2
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +4 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/session-marker.d.ts +19 -0
- package/dist/core/session-marker.d.ts.map +1 -1
- package/dist/core/session-marker.js +26 -0
- package/dist/core/session-marker.js.map +1 -1
- package/dist/core/types.d.ts +11 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +41 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +142 -1
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +152 -1
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +25 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +8 -0
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard-web/app.js +283 -270
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +21 -3
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +21 -3
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +91 -11
- 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 +156 -106
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +19 -11
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/doc-comment.d.ts +97 -0
- package/dist/im/lark/doc-comment.d.ts.map +1 -0
- package/dist/im/lark/doc-comment.js +376 -0
- package/dist/im/lark/doc-comment.js.map +1 -0
- package/dist/im/lark/event-dispatcher.d.ts +18 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +155 -1
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/services/card-prefs-store.d.ts +2 -0
- package/dist/services/card-prefs-store.d.ts.map +1 -1
- package/dist/services/card-prefs-store.js +16 -0
- package/dist/services/card-prefs-store.js.map +1 -1
- package/dist/services/doc-subs-store.d.ts +43 -0
- package/dist/services/doc-subs-store.d.ts.map +1 -0
- package/dist/services/doc-subs-store.js +83 -0
- package/dist/services/doc-subs-store.js.map +1 -0
- package/dist/setup/verify-permissions.d.ts +4 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -1
- package/dist/setup/verify-permissions.js +21 -0
- package/dist/setup/verify-permissions.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/user-token.d.ts +11 -1
- package/dist/utils/user-token.d.ts.map +1 -1
- package/dist/utils/user-token.js +20 -2
- package/dist/utils/user-token.js.map +1 -1
- package/dist/workflows/definition.d.ts +8 -8
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -40,7 +40,7 @@ import { createPendingResponseQueue, markPendingResponseCardPatched, shouldTreat
|
|
|
40
40
|
import { readPendingResponsePatchMarker } from './services/pending-response-transaction-store.js';
|
|
41
41
|
import { t as tr, botLocale, localeForBot } from './i18n/index.js';
|
|
42
42
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
43
|
-
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, killWorker, scheduleCardPatch, setCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, writableTerminalLinkFor, } from './core/worker-pool.js';
|
|
43
|
+
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, killWorker, reapOrphanWorkers, scheduleCardPatch, setCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, writableTerminalLinkFor, } from './core/worker-pool.js';
|
|
44
44
|
import { ipcRoute, jsonRes, readJsonBody, setBotName, setLarkAppId, startIpcServer, setWorkflowRunner } from './core/dashboard-ipc-server.js';
|
|
45
45
|
import { saveFrozenCards } from './services/frozen-card-store.js';
|
|
46
46
|
import { DAEMON_COMMANDS, SESSIONLESS_DAEMON_COMMANDS, resolvePassthroughCommands, resolveAdapterDefaultPassthroughCommands, handleCommand, handleCardCommand, handleTermLinkCommand, parseSlashCommandInvocation, parseForceTopicInvocation } from './core/command-handler.js';
|
|
@@ -59,6 +59,8 @@ import { buildWorkflowStartingCard, buildWorkflowProgressCard, buildAttemptDeepl
|
|
|
59
59
|
import { EventLog as WorkflowEventLog } from './workflows/events/append.js';
|
|
60
60
|
import { replay as replayWorkflow } from './workflows/events/replay.js';
|
|
61
61
|
import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, writeBotInfoFile, canOperate, evaluateTalk, grantCommandRestriction, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
|
|
62
|
+
import { listAllDocSubscriptions, removeDocSubscription } from './services/doc-subs-store.js';
|
|
63
|
+
import { subscribeDocFile, unsubscribeDocFile } from './im/lark/doc-comment.js';
|
|
62
64
|
import { learnFromMentions, resolveSender, flushIdentityCacheSync } from './im/lark/identity-cache.js';
|
|
63
65
|
import { normalizeBrand } from './im/lark/lark-hosts.js';
|
|
64
66
|
import { renderBufferedSenderBlock } from './core/session-manager.js';
|
|
@@ -1149,6 +1151,16 @@ function getActiveCount() {
|
|
|
1149
1151
|
* sees no visible response.
|
|
1150
1152
|
*/
|
|
1151
1153
|
function beginNewTurn(ds, title) {
|
|
1154
|
+
// 每开新轮先清掉上一轮可能残留的「回评论落点」并**落盘**——botmux send 子进程只
|
|
1155
|
+
// 从磁盘读会话态判断"本轮是否文档评论轮",所以磁盘必须权威:飞书轮清掉,文档轮
|
|
1156
|
+
// 由随后的 handleDocComment 重新设值+落盘。只在确有残留时写盘,避免普通轮多余写。
|
|
1157
|
+
if (ds.session.currentDocCommentTarget) {
|
|
1158
|
+
ds.session.currentDocCommentTarget = undefined;
|
|
1159
|
+
try {
|
|
1160
|
+
sessionStore.updateSession(ds.session);
|
|
1161
|
+
}
|
|
1162
|
+
catch { /* best-effort */ }
|
|
1163
|
+
}
|
|
1152
1164
|
// `/card` summon is one-shot: it forces a live card only for the turn it ran
|
|
1153
1165
|
// in. A new turn returns to the config default (noCardChats / disableStreamingCard).
|
|
1154
1166
|
// Use `/card on` to persistently restore cards for the chat.
|
|
@@ -2819,6 +2831,9 @@ async function handleThreadReply(data, ctx) {
|
|
|
2819
2831
|
// any restored streaming-card reference; worker_ready will POST a fresh
|
|
2820
2832
|
// card instead of PATCHing the previous turn's card in place.
|
|
2821
2833
|
logger.info(`[${tag(ds)}] Worker not running, re-forking...`);
|
|
2834
|
+
// 这是飞书消息轮(非文档评论轮):清掉可能残留的回评论落点,避免本轮 botmux
|
|
2835
|
+
// send 误投到上一次文档评论(本路径不走 beginNewTurn,故显式清盘)。
|
|
2836
|
+
ds.session.currentDocCommentTarget = undefined;
|
|
2822
2837
|
if (ds.usageLimitRetryTimer) {
|
|
2823
2838
|
clearTimeout(ds.usageLimitRetryTimer);
|
|
2824
2839
|
ds.usageLimitRetryTimer = undefined;
|
|
@@ -2865,6 +2880,132 @@ async function handleThreadReply(data, ctx) {
|
|
|
2865
2880
|
forkWorker(ds, wrappedPrompt, ds.hasHistory);
|
|
2866
2881
|
}
|
|
2867
2882
|
}
|
|
2883
|
+
/**
|
|
2884
|
+
* 文档评论入口(/subscribe-lark-doc):一条命中订阅的文档评论喂进其绑定会话。
|
|
2885
|
+
*
|
|
2886
|
+
* 与 handleThreadReply 同构但精简:会话由订阅锚点直接定位(无需路由决策),
|
|
2887
|
+
* 输入是评论纯文本(带「来自文档评论」前缀),并把本轮回评论的落点记进
|
|
2888
|
+
* ds.docCommentTurns —— deliverFinalOutput 据此把正文发表为文档评论。状态卡 /
|
|
2889
|
+
* 占位卡仍走会话起点(飞书),天然实现「卡片留飞书、正文进评论」的分流。
|
|
2890
|
+
*
|
|
2891
|
+
* MVP 边界:仅投递给 activeSessions 里仍在的会话(含 idle 挂起、worker=null —
|
|
2892
|
+
* 走 resume 重 fork);已 /close 的会话其订阅在关闭时已退订,这里查不到 ds 即跳过。
|
|
2893
|
+
*/
|
|
2894
|
+
async function handleDocComment(ctx) {
|
|
2895
|
+
const { larkAppId, sub, commentId, text } = ctx;
|
|
2896
|
+
const turnId = ctx.replyId || commentId;
|
|
2897
|
+
const loc = localeForBot(larkAppId);
|
|
2898
|
+
const ds = activeSessions.get(sessionKey(sub.sessionAnchor, larkAppId));
|
|
2899
|
+
if (!ds) {
|
|
2900
|
+
logger.info(`[doc-comment] no active session at anchor=${sub.sessionAnchor.slice(0, 12)} (app=${larkAppId}); dropping comment ${commentId.slice(0, 12)}`);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
const sender = ctx.authorOpenId ? await resolveSender(larkAppId, ctx.authorOpenId, 'user') : undefined;
|
|
2904
|
+
const authorName = sender?.name || ctx.authorOpenId?.slice(0, 8) || '?';
|
|
2905
|
+
const promptContent = `${tr('daemon.doc_comment_prefix', { author: authorName }, loc)}\n${text}`;
|
|
2906
|
+
// 记录本轮回评论的落点。两条路都要覆盖:
|
|
2907
|
+
// • ds.docCommentTurns(内存,按 turnId)→ deliverFinalOutput「兜底」分流用
|
|
2908
|
+
// • session.currentDocCommentTarget(磁盘)→ `botmux send`「主回复」分流用
|
|
2909
|
+
// (botmux send 跑在独立子进程,只能从磁盘读会话态)
|
|
2910
|
+
(ds.docCommentTurns ??= new Map()).set(turnId, {
|
|
2911
|
+
fileToken: sub.fileToken,
|
|
2912
|
+
fileType: sub.fileType,
|
|
2913
|
+
commentId,
|
|
2914
|
+
replyToOpenId: ctx.authorOpenId,
|
|
2915
|
+
replyToName: sender?.name,
|
|
2916
|
+
});
|
|
2917
|
+
const docTarget = { fileToken: sub.fileToken, fileType: sub.fileType, commentId, replyToName: sender?.name, replyToOpenId: ctx.authorOpenId, turnId };
|
|
2918
|
+
const dsBotCfg = getBot(ds.larkAppId).config;
|
|
2919
|
+
const selfBot = getBot(ds.larkAppId);
|
|
2920
|
+
if (ds.worker && !ds.worker.killed) {
|
|
2921
|
+
const isBridge = !!ds.adoptedFrom;
|
|
2922
|
+
const msgContent = isBridge
|
|
2923
|
+
? buildBridgeInputContent(promptContent, {
|
|
2924
|
+
selfMention: { name: selfBot.botName, openId: selfBot.botOpenId },
|
|
2925
|
+
})
|
|
2926
|
+
: buildFollowUpContent(promptContent, ds.session.sessionId, {
|
|
2927
|
+
isAdoptMode: false,
|
|
2928
|
+
cliId: dsBotCfg.cliId,
|
|
2929
|
+
cliPathOverride: dsBotCfg.cliPathOverride,
|
|
2930
|
+
sender,
|
|
2931
|
+
larkAppId,
|
|
2932
|
+
chatId: ds.session.chatId,
|
|
2933
|
+
});
|
|
2934
|
+
beginNewTurn(ds, text);
|
|
2935
|
+
ds.session.currentDocCommentTarget = docTarget; // beginNewTurn 刚清空,这里设本轮落点
|
|
2936
|
+
rememberLastCliInput(ds, promptContent, msgContent);
|
|
2937
|
+
sessionStore.updateSession(ds.session); // 先落盘,botmux send 子进程才读得到落点
|
|
2938
|
+
await postPendingResponseCard(ds, commentId, text, sender, turnId);
|
|
2939
|
+
ds.worker.send({ type: 'message', content: msgContent, turnId });
|
|
2940
|
+
logger.info(`[${tag(ds)}] doc-comment turn injected (turn ${turnId.slice(0, 8)})`);
|
|
2941
|
+
}
|
|
2942
|
+
else {
|
|
2943
|
+
// Worker 挂起 / 已退出 —— resume 重 fork(与 handleThreadReply 同路)。
|
|
2944
|
+
logger.info(`[${tag(ds)}] Worker not running for doc-comment, re-forking...`);
|
|
2945
|
+
if (ds.usageLimitRetryTimer) {
|
|
2946
|
+
clearTimeout(ds.usageLimitRetryTimer);
|
|
2947
|
+
ds.usageLimitRetryTimer = undefined;
|
|
2948
|
+
}
|
|
2949
|
+
ds.usageLimit = undefined;
|
|
2950
|
+
ds.currentTurnTitle = text.substring(0, 50);
|
|
2951
|
+
parkStreamCard(ds);
|
|
2952
|
+
ds.streamCardId = undefined;
|
|
2953
|
+
ds.streamCardNonce = undefined;
|
|
2954
|
+
ds.streamCardPending = true;
|
|
2955
|
+
ds.currentImageKey = undefined;
|
|
2956
|
+
persistStreamCardState(ds);
|
|
2957
|
+
const wrappedPrompt = buildReforkPrompt(ds, promptContent, {
|
|
2958
|
+
cliId: dsBotCfg.cliId,
|
|
2959
|
+
cliPathOverride: dsBotCfg.cliPathOverride,
|
|
2960
|
+
selfMention: { name: selfBot.botName, openId: selfBot.botOpenId },
|
|
2961
|
+
sender,
|
|
2962
|
+
});
|
|
2963
|
+
ds.session.currentDocCommentTarget = docTarget;
|
|
2964
|
+
rememberLastCliInput(ds, promptContent, wrappedPrompt);
|
|
2965
|
+
await postPendingResponseCard(ds, commentId, text, sender, turnId);
|
|
2966
|
+
sessionStore.updateSession(ds.session);
|
|
2967
|
+
forkWorker(ds, wrappedPrompt, ds.hasHistory);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* daemon 启动恢复文档订阅:飞书订阅可能在停机期间失效,给仍活跃的会话重新
|
|
2972
|
+
* 订阅;其会话没被恢复(已 /close 或丢失)的订阅则退订 + 清注册表。best-effort。
|
|
2973
|
+
*/
|
|
2974
|
+
async function restoreDocSubscriptions(_sessions) {
|
|
2975
|
+
for (const bot of getAllBots()) {
|
|
2976
|
+
const appId = bot.config.larkAppId;
|
|
2977
|
+
let subs;
|
|
2978
|
+
try {
|
|
2979
|
+
subs = listAllDocSubscriptions(config.session.dataDir, appId);
|
|
2980
|
+
}
|
|
2981
|
+
catch {
|
|
2982
|
+
continue;
|
|
2983
|
+
}
|
|
2984
|
+
for (const sub of subs) {
|
|
2985
|
+
const file = { fileToken: sub.fileToken, fileType: sub.fileType };
|
|
2986
|
+
// 判定保留/退订以**持久化的会话状态**为准(不看内存 activeSessions,避免恢复
|
|
2987
|
+
// 时序 / keying 差异误删活跃会话的订阅)。只有「明确已关闭」才退订清表:
|
|
2988
|
+
// • 有 sessionId 且其会话 status==='closed' → 真的关了 → 退订 + 删表
|
|
2989
|
+
// • 会话不存在(被清理)→ 同上
|
|
2990
|
+
// • 会话 active / 缺 sessionId(老订阅,无从判定)→ 保留 + 重订阅(保守,不误删)
|
|
2991
|
+
const stored = sub.sessionId ? sessionStore.getSession(sub.sessionId) : undefined;
|
|
2992
|
+
const definitelyClosed = sub.sessionId && (!stored || stored.status === 'closed');
|
|
2993
|
+
if (definitelyClosed) {
|
|
2994
|
+
await unsubscribeDocFile(appId, file);
|
|
2995
|
+
removeDocSubscription(config.session.dataDir, appId, sub.fileToken);
|
|
2996
|
+
logger.info(`[doc-comment] restore: dropped subscription ${sub.fileToken.slice(0, 12)} (session ${sub.sessionId?.slice(0, 8)} closed)`);
|
|
2997
|
+
continue;
|
|
2998
|
+
}
|
|
2999
|
+
try {
|
|
3000
|
+
await subscribeDocFile(appId, file);
|
|
3001
|
+
logger.info(`[doc-comment] restore: re-subscribed ${sub.fileToken.slice(0, 12)} (session ${sub.sessionId?.slice(0, 8) ?? '?'})`);
|
|
3002
|
+
}
|
|
3003
|
+
catch (err) {
|
|
3004
|
+
logger.warn(`[doc-comment] restore: re-subscribe ${sub.fileToken.slice(0, 12)} failed: ${err?.message ?? err}`);
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
2868
3009
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
2869
3010
|
/** Owner to DM for the restart report: the bot's first resolved allowedUser
|
|
2870
3011
|
* (open_id). Falls back to a raw `ou_…` entry in the config. */
|
|
@@ -3130,6 +3271,7 @@ export async function startDaemon(botIndex) {
|
|
|
3130
3271
|
handleNewTopic: (data, ctx) => handleNewTopic(data, ctx),
|
|
3131
3272
|
handleThreadReply: (data, ctx) => handleThreadReply(data, ctx),
|
|
3132
3273
|
handleBotAdded: (chatId, operatorOpenId, appId) => handleBotAdded(chatId, operatorOpenId, appId),
|
|
3274
|
+
handleDocComment: (ctx) => handleDocComment(ctx),
|
|
3133
3275
|
isSessionOwner: (anchor, appId) => activeSessions.has(sessionKey(anchor, appId)),
|
|
3134
3276
|
resolveReplyThreadAlias: (rootId, chatId, appId) => findChatReplyAlias(rootId, chatId, appId),
|
|
3135
3277
|
// Chat was converted 普通群 → 话题群 while we held a chat-scope session.
|
|
@@ -3147,8 +3289,17 @@ export async function startDaemon(botIndex) {
|
|
|
3147
3289
|
},
|
|
3148
3290
|
}, normalizeBrand(cfg.brand));
|
|
3149
3291
|
}
|
|
3292
|
+
// Reap workers orphaned by a previous daemon that was hard-killed (SIGKILL /
|
|
3293
|
+
// OOM / crash) and so skipped graceful shutdown — they're re-parented to init
|
|
3294
|
+
// (ppid==1) and untracked by any session.pid, so killStalePids can't reach
|
|
3295
|
+
// them. Without this they leak ~0.5 GB each and accumulate across restarts.
|
|
3296
|
+
// See reapOrphanWorkers() in worker-pool.ts.
|
|
3297
|
+
reapOrphanWorkers();
|
|
3150
3298
|
// Restore active sessions from previous run
|
|
3151
3299
|
await restoreActiveSessions(activeSessions);
|
|
3300
|
+
// 文档订阅恢复:重启后订阅可能已失效,给仍活跃的会话重订阅;会话没恢复
|
|
3301
|
+
// (已关/丢失)的订阅则退订 + 清表,避免「命中订阅但无会话」的孤儿。
|
|
3302
|
+
await restoreDocSubscriptions(activeSessions);
|
|
3152
3303
|
// Sweep orphan sandbox overlays left by a previous run's crash/kill: any
|
|
3153
3304
|
// <dataDir>/sandboxes/<sid> whose session is no longer active gets its
|
|
3154
3305
|
// overlays unmounted and its dirs removed (plus the /var/tmp home scratch).
|