botmux 2.51.0 → 2.52.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 +36 -1
- package/README.md +33 -1
- package/dist/bot-registry.d.ts +30 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +31 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +400 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +37 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dispatch.d.ts +163 -0
- package/dist/core/dispatch.d.ts.map +1 -0
- package/dist/core/dispatch.js +212 -0
- package/dist/core/dispatch.js.map +1 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +184 -11
- 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 +114 -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 +22 -0
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard-web/app.js +449 -426
- package/dist/dashboard.js +20 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +7 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +7 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +4 -2
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +15 -3
- 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 +6 -4
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +12 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +61 -36
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts +13 -0
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +49 -3
- package/dist/im/lark/grant-command.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts +7 -4
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +12 -6
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/services/grant-prefs-store.d.ts +23 -0
- package/dist/services/grant-prefs-store.d.ts.map +1 -0
- package/dist/services/grant-prefs-store.js +94 -0
- package/dist/services/grant-prefs-store.js.map +1 -0
- package/dist/services/grant-store.d.ts +34 -2
- package/dist/services/grant-store.d.ts.map +1 -1
- package/dist/services/grant-store.js +160 -9
- package/dist/services/grant-store.js.map +1 -1
- package/dist/services/quota-dedup.d.ts +33 -0
- package/dist/services/quota-dedup.d.ts.map +1 -0
- package/dist/services/quota-dedup.js +67 -0
- package/dist/services/quota-dedup.js.map +1 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +73 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/anchor-serializer.d.ts +11 -0
- package/dist/utils/anchor-serializer.d.ts.map +1 -0
- package/dist/utils/anchor-serializer.js +49 -0
- package/dist/utils/anchor-serializer.js.map +1 -0
- package/dist/utils/input-gate.d.ts +31 -0
- package/dist/utils/input-gate.d.ts.map +1 -0
- package/dist/utils/input-gate.js +27 -0
- package/dist/utils/input-gate.js.map +1 -0
- package/dist/utils/web-terminal-seed.d.ts +40 -0
- package/dist/utils/web-terminal-seed.d.ts.map +1 -0
- package/dist/utils/web-terminal-seed.js +46 -0
- package/dist/utils/web-terminal-seed.js.map +1 -0
- package/dist/worker.js +23 -6
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -27,7 +27,7 @@ import { buildTerminalUrl, setTerminalProxyPort } from './core/terminal-url.js';
|
|
|
27
27
|
import { startTerminalProxy } from './core/terminal-proxy.js';
|
|
28
28
|
import * as scheduler from './core/scheduler.js';
|
|
29
29
|
import { scanMultipleProjects } from './services/project-scanner.js';
|
|
30
|
-
import { buildRepoSelectCard, buildStreamingCard, getCliDisplayName } from './im/lark/card-builder.js';
|
|
30
|
+
import { buildQuotaExhaustedCard, buildRepoSelectCard, buildStreamingCard, getCliDisplayName } from './im/lark/card-builder.js';
|
|
31
31
|
import { t as tr, botLocale, localeForBot } from './i18n/index.js';
|
|
32
32
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
33
33
|
import { initWorkerPool, setActiveSessionsRegistry, forkWorker, killWorker, scheduleCardPatch, setCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, writableTerminalLinkFor, } from './core/worker-pool.js';
|
|
@@ -36,14 +36,16 @@ import { saveFrozenCards } from './services/frozen-card-store.js';
|
|
|
36
36
|
import { DAEMON_COMMANDS, SESSIONLESS_DAEMON_COMMANDS, PASSTHROUGH_COMMANDS, handleCommand, parseSlashCommandInvocation, parseForceTopicInvocation } from './core/command-handler.js';
|
|
37
37
|
import { findInheritablePeer } from './core/inherit-peer.js';
|
|
38
38
|
import { isCallbackUrl, handleCallbackUrl } from './utils/user-token.js';
|
|
39
|
+
import { consumeQuota, removeChatGrant, removeGlobalGrant } from './services/grant-store.js';
|
|
40
|
+
import { abortCharge, commitCharge, beginCharge } from './services/quota-dedup.js';
|
|
39
41
|
import { getSessionWorkingDir, getProjectScanDirs, expandHome, downloadResources, formatAttachmentsHint, buildNewTopicPrompt, buildFollowUpContent, buildBridgeInputContent, buildReforkPrompt, getAvailableBots, restoreActiveSessions, executeScheduledTask, persistStreamCardState, rememberLastCliInput, } from './core/session-manager.js';
|
|
40
42
|
import { handleCardAction } from './im/lark/card-handler.js';
|
|
41
|
-
import { executeWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
|
|
43
|
+
import { executeWorkflowCommand, parseWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
|
|
42
44
|
import { workflowRunDetailUrl } from './im/lark/workflow-cards.js';
|
|
43
45
|
import { buildWorkflowStartingCard, buildWorkflowProgressCard, buildAttemptDeeplinkEnricher, } from './im/lark/workflow-progress-card.js';
|
|
44
46
|
import { EventLog as WorkflowEventLog } from './workflows/events/append.js';
|
|
45
47
|
import { replay as replayWorkflow } from './workflows/events/replay.js';
|
|
46
|
-
import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, writeBotInfoFile, canOperate, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
|
|
48
|
+
import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, writeBotInfoFile, canOperate, evaluateTalk, grantCommandRestriction, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
|
|
47
49
|
import { learnFromMentions, resolveSender, flushIdentityCacheSync } from './im/lark/identity-cache.js';
|
|
48
50
|
import { renderSenderTag } from './core/session-manager.js';
|
|
49
51
|
import { markSessionActivity } from './core/session-activity.js';
|
|
@@ -222,6 +224,94 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId) {
|
|
|
222
224
|
// Thread-scope (or unknown / legacy): reply in thread.
|
|
223
225
|
return replyMessage(appId, anchor, content, msgType, true);
|
|
224
226
|
}
|
|
227
|
+
async function revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev) {
|
|
228
|
+
const result = ev.reason === 'chatGrant'
|
|
229
|
+
? await removeChatGrant(larkAppId, chatId, senderOpenId)
|
|
230
|
+
: ev.reason === 'globalGrant'
|
|
231
|
+
? await removeGlobalGrant(larkAppId, senderOpenId)
|
|
232
|
+
: { ok: true, removed: false };
|
|
233
|
+
if (!result.ok) {
|
|
234
|
+
logger.warn(`[quota:${larkAppId}] revoke after quota exhaustion failed: reason=${result.reason} user=${senderOpenId.substring(0, 12)} reasonType=${ev.reason}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function notifyQuotaExhausted(larkAppId, anchor, senderOpenId, limit) {
|
|
238
|
+
if (typeof limit !== 'number')
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
await sessionReply(anchor, buildQuotaExhaustedCard(senderOpenId, limit, localeForBot(larkAppId)), 'interactive', larkAppId);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
logger.warn(`[quota:${larkAppId}] quota exhausted notify failed: ${err}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
export async function enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor) {
|
|
248
|
+
const ev = evaluateTalk(larkAppId, chatId, senderOpenId);
|
|
249
|
+
if (!ev.allowed) {
|
|
250
|
+
logger.debug(`[quota:${larkAppId}] dropping message ${messageId.substring(0, 12)} from non-allowed sender ${senderOpenId?.substring(0, 12) ?? '?'}`);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (!ev.quotaKey)
|
|
254
|
+
return true;
|
|
255
|
+
if (!senderOpenId)
|
|
256
|
+
return false;
|
|
257
|
+
// 去重三态:'done' = 同条已成功扣费 → 放行(不重复扣);'pending' = 同条扣费 in-flight 未定论
|
|
258
|
+
// → fail-closed drop(绝不在定论前放行第二投);'fresh' = 首次见 → 继续扣费。
|
|
259
|
+
const charge = beginCharge(larkAppId, messageId);
|
|
260
|
+
if (charge === 'done')
|
|
261
|
+
return true;
|
|
262
|
+
if (charge === 'pending')
|
|
263
|
+
return false;
|
|
264
|
+
let quota;
|
|
265
|
+
try {
|
|
266
|
+
quota = await consumeQuota(larkAppId, ev.quotaKey);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
logger.warn(`[quota:${larkAppId}] consume failed; dropping message ${messageId.substring(0, 12)}: ${err}`);
|
|
270
|
+
abortCharge(larkAppId, messageId);
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
// 无额度记录(无限授权):放行;标 done 去重后续重投。
|
|
274
|
+
if (!quota.tracked) {
|
|
275
|
+
commitCharge(larkAppId, messageId);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
// 已超额:fail-closed drop。**绝不 commit 成 done**(否则同条重投会被 'done' 直接放行,
|
|
279
|
+
// 在 revoke 自愈失败/竞态时绕过硬上限)——abortCharge 让重投重新走扣费判定(仍会被拒,
|
|
280
|
+
// 或在授权已收回时被上面的 evaluateTalk 闸拦掉)。
|
|
281
|
+
if (!quota.allow) {
|
|
282
|
+
abortCharge(larkAppId, messageId);
|
|
283
|
+
await revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev);
|
|
284
|
+
await notifyQuotaExhausted(larkAppId, anchor, senderOpenId, quota.limit);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
// 扣费成功才定论为 done。
|
|
288
|
+
commitCharge(larkAppId, messageId);
|
|
289
|
+
if (quota.exhausted) {
|
|
290
|
+
await revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev);
|
|
291
|
+
await notifyQuotaExhausted(larkAppId, anchor, senderOpenId, quota.limit);
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
export function grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd) {
|
|
296
|
+
return grantCommandRestriction(larkAppId, chatId, senderOpenId).blocked
|
|
297
|
+
? tr('cmd.grant_restricted', { cmd }, localeForBot(larkAppId))
|
|
298
|
+
: undefined;
|
|
299
|
+
}
|
|
300
|
+
export function grantRestrictedSlashCommandText(larkAppId, chatId, senderOpenId, cmd) {
|
|
301
|
+
if (!/^\/[a-z][a-z0-9_-]*$/.test(cmd))
|
|
302
|
+
return undefined;
|
|
303
|
+
return grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd);
|
|
304
|
+
}
|
|
305
|
+
async function replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, cmd) {
|
|
306
|
+
const text = grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd);
|
|
307
|
+
if (!text)
|
|
308
|
+
return false;
|
|
309
|
+
await sessionReply(anchor, text, 'text', larkAppId);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function forceTopicCommandLabel(content) {
|
|
313
|
+
return /^\/topic(?:\s|$)/i.test(content.trimStart()) ? '/topic' : '/t';
|
|
314
|
+
}
|
|
225
315
|
// ─── PID file ────────────────────────────────────────────────────────────────
|
|
226
316
|
function getPidFile() {
|
|
227
317
|
const botIndex = process.env.BOTMUX_BOT_INDEX;
|
|
@@ -1384,8 +1474,12 @@ async function handleNewTopic(data, ctx) {
|
|
|
1384
1474
|
// (already thread-scope) it's just a prefix strip — no routing change.
|
|
1385
1475
|
// Empty prompt is allowed: the user can fill it in while the repo card is
|
|
1386
1476
|
// pending (pendingFollowUps in handleThreadReply picks up subsequent text).
|
|
1477
|
+
const senderOpenId = data.sender?.sender_id?.open_id;
|
|
1387
1478
|
const forceTopic = parseForceTopicInvocation(cmdContent);
|
|
1388
1479
|
if (forceTopic) {
|
|
1480
|
+
if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, forceTopicCommandLabel(cmdContent))) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1389
1483
|
if (scope === 'chat') {
|
|
1390
1484
|
scope = 'thread';
|
|
1391
1485
|
anchor = messageId;
|
|
@@ -1395,10 +1489,15 @@ async function handleNewTopic(data, ctx) {
|
|
|
1395
1489
|
cmdContent = forceTopic.prompt;
|
|
1396
1490
|
logger.info(`[/t] Force-topic invocation: prompt="${forceTopic.prompt.substring(0, 60)}" (scope=${scope}, anchor=${anchor.substring(0, 12)})`);
|
|
1397
1491
|
}
|
|
1398
|
-
|
|
1492
|
+
// senderOpenId 已在上方(force-topic grant 限制前)声明;这里只补 master 新增的 senderUnionId。
|
|
1399
1493
|
const senderUnionId = data.sender?.sender_id?.union_id;
|
|
1400
1494
|
const botCfg = getBot(larkAppId).config;
|
|
1401
1495
|
logger.info(`New session: "${content.substring(0, 60)}" (scope=${scope}, anchor=${anchor.substring(0, 12)}, resources: ${resources.length}, active: ${getActiveCount()}, messageId: ${messageId}, chatId: ${chatId})`);
|
|
1496
|
+
if (parseWorkflowCommand(cmdContent)) {
|
|
1497
|
+
if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, '/workflow')) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1402
1501
|
if (await handleWorkflowCommandIfAny(cmdContent, anchor, chatId, larkAppId, senderOpenId)) {
|
|
1403
1502
|
return;
|
|
1404
1503
|
}
|
|
@@ -1406,6 +1505,11 @@ async function handleNewTopic(data, ctx) {
|
|
|
1406
1505
|
const invocation = parseSlashCommandInvocation(cmdContent);
|
|
1407
1506
|
if (invocation) {
|
|
1408
1507
|
const { cmd, content: commandContent } = invocation;
|
|
1508
|
+
const restrictedText = grantRestrictedSlashCommandText(larkAppId, chatId, senderOpenId, cmd);
|
|
1509
|
+
if (restrictedText) {
|
|
1510
|
+
await sessionReply(anchor, restrictedText, 'text', larkAppId);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1409
1513
|
if (PASSTHROUGH_COMMANDS.has(cmd)) {
|
|
1410
1514
|
await sessionReply(anchor, tr('daemon.cmd_requires_session', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
1411
1515
|
return;
|
|
@@ -1479,6 +1583,9 @@ async function handleNewTopic(data, ctx) {
|
|
|
1479
1583
|
return;
|
|
1480
1584
|
}
|
|
1481
1585
|
}
|
|
1586
|
+
if (!await enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor)) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1482
1589
|
// Download attachments
|
|
1483
1590
|
const { attachments, needLogin } = await downloadResources(larkAppId, messageId, resources);
|
|
1484
1591
|
if (attachments.length > 0) {
|
|
@@ -1874,6 +1981,8 @@ async function handleThreadReply(data, ctx) {
|
|
|
1874
1981
|
const content = parsed.content.trim();
|
|
1875
1982
|
// Strip leading @<bot> mentions so "@bot /restart" is recognized as a command.
|
|
1876
1983
|
const cmdContent = stripLeadingMentions(content, parsed.mentions);
|
|
1984
|
+
const threadSenderOpenId = parsed.senderId || data?.sender?.sender_id?.open_id;
|
|
1985
|
+
const threadChatId = ctxChatId ?? data?.message?.chat_id;
|
|
1877
1986
|
// Intercept OAuth callback URLs (from /login flow)
|
|
1878
1987
|
if (isCallbackUrl(content)) {
|
|
1879
1988
|
const result = await handleCallbackUrl(content);
|
|
@@ -1885,13 +1994,31 @@ async function handleThreadReply(data, ctx) {
|
|
|
1885
1994
|
return;
|
|
1886
1995
|
}
|
|
1887
1996
|
}
|
|
1888
|
-
|
|
1997
|
+
const threadForceTopic = parseForceTopicInvocation(cmdContent);
|
|
1998
|
+
if (threadForceTopic) {
|
|
1999
|
+
if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, forceTopicCommandLabel(cmdContent))) {
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (parseWorkflowCommand(cmdContent)) {
|
|
2004
|
+
if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, '/workflow')) {
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
if (await handleWorkflowCommandIfAny(cmdContent, anchor, threadChatId, larkAppId, threadSenderOpenId)) {
|
|
1889
2009
|
return;
|
|
1890
2010
|
}
|
|
1891
2011
|
// Intercept daemon commands
|
|
1892
2012
|
const invocation = parseSlashCommandInvocation(cmdContent);
|
|
1893
2013
|
if (invocation) {
|
|
1894
2014
|
const { cmd, content: commandContent } = invocation;
|
|
2015
|
+
const existingDs = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
2016
|
+
const effectiveThreadChatId = existingDs?.chatId ?? threadChatId;
|
|
2017
|
+
const restrictedText = grantRestrictedSlashCommandText(larkAppId, effectiveThreadChatId, threadSenderOpenId, cmd);
|
|
2018
|
+
if (restrictedText) {
|
|
2019
|
+
await sessionReply(anchor, restrictedText, 'text', larkAppId);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
1895
2022
|
if (PASSTHROUGH_COMMANDS.has(cmd)) {
|
|
1896
2023
|
// 语义边界(刻意保留,非疏漏):passthrough(/model /clear /compact 等)按
|
|
1897
2024
|
// “发给 CLI 的对话输入”处理,因此不过下面 DAEMON_COMMANDS 的 oncall
|
|
@@ -1900,7 +2027,7 @@ async function handleThreadReply(data, ctx) {
|
|
|
1900
2027
|
// 已存在的 session 发这些命令(清上下文/换模型,需已有活跃 worker,无法凭空
|
|
1901
2028
|
// 拉起)。TODO(后续产品决策):是否把 CLI passthrough 也纳入 canOperate,
|
|
1902
2029
|
// 收紧到与 daemon 命令同档;这会同时改变真人 oncall 成员的现有行为,应单独评估。
|
|
1903
|
-
const ds =
|
|
2030
|
+
const ds = existingDs;
|
|
1904
2031
|
if (ds?.worker && !ds.worker.killed) {
|
|
1905
2032
|
// Mark a new turn so the CLI's response to /model, /clear, /compact, etc.
|
|
1906
2033
|
// shows up as a fresh streaming card instead of silently PATCH-ing the
|
|
@@ -1918,16 +2045,54 @@ async function handleThreadReply(data, ctx) {
|
|
|
1918
2045
|
if (DAEMON_COMMANDS.has(cmd)) {
|
|
1919
2046
|
// canOperate gate for thread-reply daemon commands — required in every chat
|
|
1920
2047
|
// (see spawn-path gate above). Denies chat-granted users management commands.
|
|
1921
|
-
|
|
1922
|
-
const threadChatId = existingDs?.chatId ?? ctxChatId ?? data?.message?.chat_id;
|
|
1923
|
-
const threadSenderOpenId = parsed.senderId || data?.sender?.sender_id?.open_id;
|
|
1924
|
-
if (!canOperate(larkAppId, threadChatId, threadSenderOpenId)) {
|
|
2048
|
+
if (!canOperate(larkAppId, effectiveThreadChatId, threadSenderOpenId)) {
|
|
1925
2049
|
sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
|
|
1926
2050
|
return;
|
|
1927
2051
|
}
|
|
2052
|
+
// First message of a fresh thread carrying a session-needing daemon command
|
|
2053
|
+
// — e.g. another bot dispatched `/repo <path>` into a brand-new thread.
|
|
2054
|
+
// Without a session, handleCommand gets ds=undefined and `/repo` (and other
|
|
2055
|
+
// session commands) fall through to the repo-select card. Create the session
|
|
2056
|
+
// first, mirroring handleNewTopic's first-message `/repo` pendingRepo setup.
|
|
2057
|
+
// Session-less commands (/group /g) don't need one.
|
|
2058
|
+
if (!existingDs && threadChatId && !SESSIONLESS_DAEMON_COMMANDS.has(cmd)) {
|
|
2059
|
+
const session = sessionStore.createSession(threadChatId, anchor, cmdContent.substring(0, 50), ctxChatType);
|
|
2060
|
+
const now = Date.now();
|
|
2061
|
+
session.larkAppId = larkAppId;
|
|
2062
|
+
session.ownerOpenId = threadSenderOpenId;
|
|
2063
|
+
session.creatorOpenId = threadSenderOpenId; // stable creator (= dispatch orchestrator for /repo prime) — see Session.creatorOpenId
|
|
2064
|
+
session.ownerUnionId = data?.sender?.sender_id?.union_id;
|
|
2065
|
+
session.lastCallerOpenId = threadSenderOpenId;
|
|
2066
|
+
session.lastMessageAt = new Date(now).toISOString();
|
|
2067
|
+
session.scope = scope;
|
|
2068
|
+
let cmdPending;
|
|
2069
|
+
if (cmd === '/repo') {
|
|
2070
|
+
const { pinnedWorkingDir } = await resolvePinnedWorkingDir({ scope, anchor, chatId: threadChatId, chatType: ctxChatType, larkAppId });
|
|
2071
|
+
if (pinnedWorkingDir)
|
|
2072
|
+
session.workingDir = pinnedWorkingDir;
|
|
2073
|
+
cmdPending = { pendingRepo: true, pendingPrompt: '', workingDir: pinnedWorkingDir };
|
|
2074
|
+
}
|
|
2075
|
+
sessionStore.updateSession(session);
|
|
2076
|
+
activeSessions.set(sessionKey(anchor, larkAppId), {
|
|
2077
|
+
session,
|
|
2078
|
+
worker: null,
|
|
2079
|
+
workerPort: null,
|
|
2080
|
+
workerToken: null,
|
|
2081
|
+
larkAppId,
|
|
2082
|
+
chatId: threadChatId,
|
|
2083
|
+
chatType: ctxChatType,
|
|
2084
|
+
scope,
|
|
2085
|
+
spawnedAt: Date.parse(session.createdAt) || now,
|
|
2086
|
+
cliVersion: cliVersionCache.get(getBot(larkAppId).config.cliId)?.version ?? 'unknown',
|
|
2087
|
+
lastMessageAt: now,
|
|
2088
|
+
hasHistory: false,
|
|
2089
|
+
ownerOpenId: threadSenderOpenId,
|
|
2090
|
+
...cmdPending,
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
1928
2093
|
// Pass mention-stripped content so /command argument parsing works.
|
|
1929
2094
|
// chatId lets session-less handlers (e.g. /group) reach the chat roster.
|
|
1930
|
-
handleCommand(cmd, anchor, { ...parsed, content: commandContent, chatId: threadChatId }, commandDeps, larkAppId);
|
|
2095
|
+
await handleCommand(cmd, anchor, { ...parsed, content: commandContent, chatId: threadChatId }, commandDeps, larkAppId);
|
|
1931
2096
|
return;
|
|
1932
2097
|
}
|
|
1933
2098
|
}
|
|
@@ -1951,6 +2116,10 @@ async function handleThreadReply(data, ctx) {
|
|
|
1951
2116
|
return;
|
|
1952
2117
|
}
|
|
1953
2118
|
}
|
|
2119
|
+
const quotaSenderOpenId = threadSenderOpenId;
|
|
2120
|
+
if (!await enforceMessageQuotaForCliInput(larkAppId, ctxChatId ?? data?.message?.chat_id, quotaSenderOpenId, parsed.messageId, anchor)) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
1954
2123
|
// Download attachments
|
|
1955
2124
|
const effectiveAppId = ds?.larkAppId ?? larkAppId;
|
|
1956
2125
|
const { attachments, needLogin } = await downloadResources(effectiveAppId, parsed.messageId, resources);
|
|
@@ -2039,6 +2208,10 @@ async function handleThreadReply(data, ctx) {
|
|
|
2039
2208
|
const ownerUnionId = isForeignBot ? undefined : senderUId;
|
|
2040
2209
|
session.larkAppId = larkAppId;
|
|
2041
2210
|
session.ownerOpenId = ownerOpenId;
|
|
2211
|
+
// creatorOpenId is the raw creating sender — set even for foreign-bot
|
|
2212
|
+
// sessions (unlike ownerOpenId, nulled above) so `botmux report` can find the
|
|
2213
|
+
// dispatch orchestrator on a no-`/repo` kickoff auto-create. See Session.creatorOpenId.
|
|
2214
|
+
session.creatorOpenId = senderOId;
|
|
2042
2215
|
session.ownerUnionId = ownerUnionId;
|
|
2043
2216
|
session.lastCallerOpenId = senderOId;
|
|
2044
2217
|
session.quoteTargetId = parsed.messageId;
|