lark-remote 0.5.19 → 0.5.21
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/CHANGELOG.md +29 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +72 -46
- package/dist/bridge/index.d.ts +106 -153
- package/dist/bridge/index.js +400 -1000
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +141 -130
- package/dist/card/card-budget.js +123 -98
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +114 -159
- package/dist/card/run-state.d.ts +1 -15
- package/dist/card/run-state.js +358 -292
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +21 -17
- package/dist/card/usage-format.d.ts +22 -7
- package/dist/card/usage-format.js +119 -100
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +387 -281
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +254 -237
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +190 -152
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +401 -391
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +2 -5
- package/dist/runner/codex/app-server/translator.js +291 -238
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +50 -26
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +6 -1
- package/dist/runner/common/spawning-runner.js +9 -4
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +7 -11
- package/dist/runner/dsh/client.js +356 -289
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +44 -4
- package/dist/runner/dsh/runner.js +296 -199
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +82 -46
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +160 -122
- package/dist/session/codex/rollout-reader.js +181 -182
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +291 -235
- package/dist/session/kimi/sessions.d.ts +40 -8
- package/dist/session/kimi/sessions.js +373 -301
- package/dist/session/opencode/sessions.d.ts +142 -1
- package/dist/session/opencode/sessions.js +378 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +9 -1
- package/dist/session/session-store.js +99 -99
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
package/dist/router/index.js
CHANGED
|
@@ -57,10 +57,10 @@ function findMergedCommandTail(message) {
|
|
|
57
57
|
/** 人类可读字节数(B/KB/MB/GB);/ls 列表与单文件卡共用。 */
|
|
58
58
|
/**
|
|
59
59
|
* /order 列表页大小;指令超过此数量时显示分页导航栏。
|
|
60
|
-
*
|
|
60
|
+
* 实测:每行 2 个元素(div + column_set,行间 hr),20 行 + 分页栏
|
|
61
61
|
* = 61 个 body 元素,触发飞书 ErrCode 11310 "element exceeds the limit"
|
|
62
62
|
* (21+ 条指令时第 1 页必炸)。改为 15 行:3*15 - 1 + 2 = 46 个元素,留足余量。
|
|
63
|
-
*
|
|
63
|
+
* 别名并卡后每行变 3 个元素(文本 div + 操作 column_set + hr),
|
|
64
64
|
* 从 15 降为 8:3*8 - 1 + 2 = 25 个元素,仍在余量内,同时保证手机窄屏
|
|
65
65
|
* 每条指令的操作控件(执行/别名/删除)不被挤压。
|
|
66
66
|
*/
|
|
@@ -148,7 +148,7 @@ const APPROVAL_ACTION_SPECS = {
|
|
|
148
148
|
}),
|
|
149
149
|
successToast: '审批已提交',
|
|
150
150
|
failureToast: (msg) => {
|
|
151
|
-
//
|
|
151
|
+
// 审批已过期:给用户明确反馈,不静默不误导(否则点了允许无任何反馈)。
|
|
152
152
|
const content = /state=expired/.test(msg)
|
|
153
153
|
? '⏰ 审批已过期,无法响应'
|
|
154
154
|
: `审批响应失败:${msg}`;
|
|
@@ -296,7 +296,7 @@ const IMMEDIATE_ACTION_CMDS = new Set([
|
|
|
296
296
|
'config.save',
|
|
297
297
|
// 审批响应/权限切换必须即时触达在途 run(同 stop 类控制动作)。若走串行
|
|
298
298
|
// 队列会排在等待审批的 run 之后形成死锁:run 不结束审批不执行,run 结束
|
|
299
|
-
// coordinator
|
|
299
|
+
// coordinator 已删响应空转(approval.respond 落进排队卡、审批永不生效)。
|
|
300
300
|
...APPROVAL_ACTION_CMDS,
|
|
301
301
|
]);
|
|
302
302
|
/**
|
|
@@ -304,7 +304,7 @@ const IMMEDIATE_ACTION_CMDS = new Set([
|
|
|
304
304
|
* sessionId / cwd / defaultAgent,必须与入站装配器的 700ms 静默窗口定序:
|
|
305
305
|
* 控制层(index.ts)在执行它们之前先 flush 该 user/chat 的装配窗口,保证窗口内
|
|
306
306
|
* 先到的文本消息在本动作之前 commit(绑定动作前的旧状态)。否则窗口内消息会
|
|
307
|
-
*
|
|
307
|
+
* 漂移到动作之后的新状态(输入「重新构建」后立即点「新会话」,
|
|
308
308
|
* 消息落到新会话)。
|
|
309
309
|
*
|
|
310
310
|
* 文本命令(/new、/cd、/resume、/config)已由 index.ts 命令分支先 flush,
|
|
@@ -321,7 +321,7 @@ export const SESSION_MUTATING_ACTION_CMDS = new Set([
|
|
|
321
321
|
* 会产生压缩工作的卡片动作(单源)。这类条目占用 lane 时必须自报身份:
|
|
322
322
|
* 排队卡写明「正在执行: 🗜 Compact」而不是「前面还有 0 条」,并且期间禁用
|
|
323
323
|
* 「立即执行」——该按钮走 interruptCurrentRun,会连带中断正在跑的压缩
|
|
324
|
-
* (
|
|
324
|
+
* (Compact 占 lane 期间新消息的排队卡自相矛盾,且按钮
|
|
325
325
|
* 一点就会杀掉压缩)。同时它们也不允许编辑(预览不是用户文本)。
|
|
326
326
|
*/
|
|
327
327
|
export const COMPACT_ACTION_CMDS = new Set(['compact', 'resume.compact']);
|
|
@@ -362,6 +362,169 @@ function touchesAgentsSubtree(keys, kind) {
|
|
|
362
362
|
function touchesAgentConfig(keys, kind) {
|
|
363
363
|
return (touchesAgentsSubtree(keys, kind) || keys.some((k) => k === kind || k.startsWith(`${kind}.`)));
|
|
364
364
|
}
|
|
365
|
+
const CARD_ACTION_SPECS = new Map([
|
|
366
|
+
['ls.file', { method: 'cardLsFile', args: 'path' }],
|
|
367
|
+
['ls.refresh', { method: 'handleLsRefresh', args: 'voidCall' }],
|
|
368
|
+
['ls.browse', { method: 'handleLsBrowse', args: 'voidCall' }],
|
|
369
|
+
['ls.switch', { method: 'handleLsSwitch', args: 'voidCall' }],
|
|
370
|
+
['ls.page', { method: 'handleLsPage', args: 'returnCall' }],
|
|
371
|
+
['ls.filter', { method: 'handleLsFilter', args: 'returnCall' }],
|
|
372
|
+
['ws.page', { method: 'handleWsPage', args: 'returnCall' }],
|
|
373
|
+
['ws.filter', { method: 'handleWsFilter', args: 'returnCall' }],
|
|
374
|
+
['ws.use', { method: 'handleWsUse', args: 'returnCall' }],
|
|
375
|
+
['ws.sort', { method: 'handleWsSort', args: 'returnCall' }],
|
|
376
|
+
['ws.remove', { method: 'handleWsRemove', args: 'returnCall' }],
|
|
377
|
+
['resume.use', { method: 'handleResumeUse', args: 'voidCall' }],
|
|
378
|
+
['resume.page', { method: 'handleResumePage', args: 'returnCall' }],
|
|
379
|
+
['active.page', { method: 'handleActivePage', args: 'returnCall' }],
|
|
380
|
+
['new-session', { method: 'handleNewSession', args: 'ctxOnly' }],
|
|
381
|
+
['stop', { method: 'handleCardStop', args: 'voidCall' }],
|
|
382
|
+
['job.kill', { method: 'handleJobKill', args: 'voidCall' }],
|
|
383
|
+
['queue.cancel', { method: 'handleQueueCancel', args: 'voidCall' }],
|
|
384
|
+
['queue.immediate', { method: 'handleQueueImmediate', args: 'voidCall' }],
|
|
385
|
+
['queue.diagnose', { method: 'handleQueueDiagnose', args: 'voidCall' }],
|
|
386
|
+
['queue.edit', { method: 'handleQueueEdit', args: 'voidCall' }],
|
|
387
|
+
['queue.input', { method: 'handleQueueInput', args: 'returnCall' }],
|
|
388
|
+
['order.delete', { method: 'handleOrderDelete', args: 'returnCall' }],
|
|
389
|
+
['order.page', { method: 'handleOrderPage', args: 'returnCall' }],
|
|
390
|
+
['order.aliasEdit', { method: 'handleOrderAliasEdit', args: 'returnCall' }],
|
|
391
|
+
['order.aliasInput', { method: 'handleOrderAliasInput', args: 'returnCall' }],
|
|
392
|
+
['order.aliasRemove', { method: 'handleOrderAliasRemove', args: 'returnCall' }],
|
|
393
|
+
['order.textEdit', { method: 'handleOrderTextEdit', args: 'returnCall' }],
|
|
394
|
+
['order.textInput', { method: 'handleOrderTextInput', args: 'returnCall' }],
|
|
395
|
+
['config.set', { method: 'enqueueConfigAction', args: 'returnCall' }],
|
|
396
|
+
['config.input', { method: 'enqueueConfigAction', args: 'returnCall' }],
|
|
397
|
+
['config.save', { method: 'enqueueConfigAction', args: 'returnCall' }],
|
|
398
|
+
['compact', { method: 'handleCompact', args: 'voidCall' }],
|
|
399
|
+
['resume.compact', { method: 'handleResumeCompact', args: 'voidCall' }],
|
|
400
|
+
]);
|
|
401
|
+
const COMMAND_SPECS = new Map([
|
|
402
|
+
['help', { method: 'cmdHelp', args: 'none' }],
|
|
403
|
+
['h', { method: 'cmdHelp', args: 'none' }],
|
|
404
|
+
['status', { method: 'cmdStatus', args: 'ctxOnly' }],
|
|
405
|
+
['s', { method: 'cmdStatus', args: 'ctxOnly' }],
|
|
406
|
+
['ps', { method: 'cmdPs', args: 'ctxOnly' }],
|
|
407
|
+
['stop', { method: 'cmdStop', args: 'ctxOnly' }],
|
|
408
|
+
['t', { method: 'cmdStop', args: 'ctxOnly' }],
|
|
409
|
+
['exit', { method: 'cmdExit', args: 'none' }],
|
|
410
|
+
['quit', { method: 'cmdExit', args: 'none' }],
|
|
411
|
+
['e', { method: 'cmdExit', args: 'none' }],
|
|
412
|
+
['restart', { method: 'cmdRestart', args: 'none' }],
|
|
413
|
+
['new', { method: 'cmdNew', args: 'ctxOnly' }],
|
|
414
|
+
['cd', { method: 'cmdCd', args: 'argsCtx' }],
|
|
415
|
+
['ls', { method: 'cmdLs', args: 'argsCtx' }],
|
|
416
|
+
['download', { method: 'cmdDownload', args: 'argsCtx' }],
|
|
417
|
+
['d', { method: 'cmdDownload', args: 'argsCtx' }],
|
|
418
|
+
['ws', { method: 'cmdWs', args: 'argsCtx' }],
|
|
419
|
+
['resume', { method: 'cmdResume', args: 'argsCtx' }],
|
|
420
|
+
['r', { method: 'cmdResume', args: 'argsCtx' }],
|
|
421
|
+
['active', { method: 'cmdActive', args: 'argsCtx' }],
|
|
422
|
+
['config', { method: 'cmdConfig', args: 'argsCtx' }],
|
|
423
|
+
['c', { method: 'cmdConfig', args: 'argsCtx' }],
|
|
424
|
+
['order', { method: 'cmdOrder', args: 'argsCtx' }],
|
|
425
|
+
['o', { method: 'cmdOrder', args: 'argsCtx' }],
|
|
426
|
+
['update', { method: 'cmdUpdate', args: 'argsCtx' }],
|
|
427
|
+
['u', { method: 'cmdUpdate', args: 'argsCtx' }],
|
|
428
|
+
['clone', { method: 'cmdClone', args: 'argsCtx' }],
|
|
429
|
+
]);
|
|
430
|
+
/** 命令名 → 命令方法查表分发;无匹配时返回与旧 default 分支一致的文案。 */
|
|
431
|
+
function dispatchCommand(router, cmd, args, ctx) {
|
|
432
|
+
const spec = COMMAND_SPECS.get(cmd ?? '');
|
|
433
|
+
if (!spec) {
|
|
434
|
+
return { text: `未知命令 /${cmd},输入 /help 查看可用命令` };
|
|
435
|
+
}
|
|
436
|
+
const handler = router[spec.method];
|
|
437
|
+
if (spec.args === 'none') {
|
|
438
|
+
return handler.call(router);
|
|
439
|
+
}
|
|
440
|
+
if (spec.args === 'ctxOnly') {
|
|
441
|
+
return handler.call(router, ctx);
|
|
442
|
+
}
|
|
443
|
+
return handler.call(router, args, ctx);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* defaultAgent 切换时的 session 迁移(Step 0-3)。抽为模块级命名函数,
|
|
447
|
+
* 避免其内部分支与判定闭包计入外层 propagateConfigSave 的 CC。
|
|
448
|
+
*/
|
|
449
|
+
function applyDefaultAgentSwitch(sessionStore, userId, oldDefaultAgent, newAgent) {
|
|
450
|
+
// Step 0: 在清空 old 之前计算「用户活动」基线差(arrival 基线):
|
|
451
|
+
const oldSessionId = sessionStore.getSessionId(userId, oldDefaultAgent);
|
|
452
|
+
const oldArrivalSessionId = sessionStore.getArrivalSessionId(userId, oldDefaultAgent);
|
|
453
|
+
const userChangedOld = (oldSessionId ?? '') !== (oldArrivalSessionId ?? '');
|
|
454
|
+
// Step 1: 保存旧 agent 的 session 到 previousSessions(如果有)
|
|
455
|
+
if (oldSessionId) {
|
|
456
|
+
sessionStore.setPreviousSessionId(userId, oldDefaultAgent, oldSessionId);
|
|
457
|
+
}
|
|
458
|
+
// Step 2: 清除旧 agent 的 session(保存后清除,为新会话腾出空间)
|
|
459
|
+
sessionStore.clearSessionId(userId, oldDefaultAgent);
|
|
460
|
+
// Step 2.5: 显式选择判定
|
|
461
|
+
const newSessionId = sessionStore.getSessionId(userId, newAgent);
|
|
462
|
+
const newArrivalSessionId = sessionStore.getArrivalSessionId(userId, newAgent);
|
|
463
|
+
const userSelectedNew = !!newSessionId && (newSessionId ?? '') !== (newArrivalSessionId ?? '');
|
|
464
|
+
// Step 3: 恢复判定
|
|
465
|
+
const previousSessionId = sessionStore.getPreviousSessionId(userId, newAgent);
|
|
466
|
+
const canRestore = !!previousSessionId && !userChangedOld;
|
|
467
|
+
if (userSelectedNew) {
|
|
468
|
+
sessionStore.setArrivalSessionId(userId, newAgent, newSessionId);
|
|
469
|
+
const newAgentName = agentDisplayName(newAgent);
|
|
470
|
+
return {
|
|
471
|
+
notice: `已切换到 ${newAgentName},已使用所选 session,sessionId: ${newSessionId}`,
|
|
472
|
+
newAgent,
|
|
473
|
+
sessionId: newSessionId,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (canRestore) {
|
|
477
|
+
sessionStore.setSessionId(userId, newAgent, previousSessionId);
|
|
478
|
+
sessionStore.clearPreviousSessionId(userId, newAgent);
|
|
479
|
+
sessionStore.setArrivalSessionId(userId, newAgent, previousSessionId);
|
|
480
|
+
const newAgentName = agentDisplayName(newAgent);
|
|
481
|
+
return {
|
|
482
|
+
notice: `已切换到 ${newAgentName},将继续之前的 session,sessionId: ${previousSessionId}`,
|
|
483
|
+
newAgent,
|
|
484
|
+
sessionId: previousSessionId,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
sessionStore.clearSessionId(userId, newAgent, { clearSessionCwd: true });
|
|
488
|
+
sessionStore.setArrivalSessionId(userId, newAgent, '');
|
|
489
|
+
const newAgentName = agentDisplayName(newAgent);
|
|
490
|
+
return {
|
|
491
|
+
notice: `已切换到 ${newAgentName},session 已清空,下次消息将启动新对话`,
|
|
492
|
+
newAgent,
|
|
493
|
+
sessionId: '',
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* DSH 会话边界变更(preset/host)共用的 notice 计算。返回更新后的 notice;
|
|
498
|
+
* 条件不满足时原样返回 currentNotice,等价于原闭包「不修改」的语义。
|
|
499
|
+
*/
|
|
500
|
+
function parkDshSessionBoundaryNotice(sessionStore, userId, defaultAgent, changed, label, currentNotice) {
|
|
501
|
+
if (!changed || defaultAgent !== 'dsh' || currentNotice)
|
|
502
|
+
return currentNotice;
|
|
503
|
+
const oldSessionId = sessionStore.getSessionId(userId, 'dsh');
|
|
504
|
+
if (oldSessionId) {
|
|
505
|
+
sessionStore.setPreviousSessionId(userId, 'dsh', oldSessionId);
|
|
506
|
+
sessionStore.clearSessionId(userId, 'dsh');
|
|
507
|
+
return `DSH ${label} 已变更,旧会话已停放到恢复槽(可 /resume 校验后恢复),下次消息将新建会话`;
|
|
508
|
+
}
|
|
509
|
+
return `DSH ${label} 已变更,下次消息将新建会话`;
|
|
510
|
+
}
|
|
511
|
+
function computeHasAgentConfigChange(changedKeys) {
|
|
512
|
+
if (changedKeys.includes('defaultAgent'))
|
|
513
|
+
return true;
|
|
514
|
+
for (const kind of AGENT_KINDS) {
|
|
515
|
+
if (touchesAgentConfig(changedKeys, kind))
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const APPROVAL_AGENT_KINDS = ['codex', 'kimi', 'opencode'];
|
|
521
|
+
function computeHasApprovalConfigChange(changedKeys) {
|
|
522
|
+
for (const kind of APPROVAL_AGENT_KINDS) {
|
|
523
|
+
if (touchesAgentConfig(changedKeys, kind))
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
return changedKeys.includes('agents.dsh.permissionPreset');
|
|
527
|
+
}
|
|
365
528
|
export class CommandRouter {
|
|
366
529
|
sessionStore;
|
|
367
530
|
/** 飞书桥接实例(public:测试直接访问断言,替代 as unknown as)。 */
|
|
@@ -400,8 +563,8 @@ export class CommandRouter {
|
|
|
400
563
|
*
|
|
401
564
|
* 草稿(pendingConfig)是全局一份:/config 文本命令重开卡片时沿用同一份草稿
|
|
402
565
|
* (这是既有语义,不改)。但**视图**是每张卡一份 —— 只刷新被点击的那张,其余
|
|
403
|
-
*
|
|
404
|
-
*
|
|
566
|
+
* 卡片就停在旧视图上:用户在旧卡上看到的和点保存实际提交的是两套内容。
|
|
567
|
+
* 这里记住所有交互过的 config 卡,任何一次编辑/保存
|
|
405
568
|
* 后把草稿刷到每张卡,保证「看到的就是会保存的」。
|
|
406
569
|
*/
|
|
407
570
|
configCardIds = new Set();
|
|
@@ -412,7 +575,7 @@ export class CommandRouter {
|
|
|
412
575
|
* 条目的身份必须与「一次入队动作」1:1(queuedTasks 的定位、queueCardMessages
|
|
413
576
|
* 的卡片更新、taskIndex 的取消判据全部按这个 key 做 lookup)。复用卡片 id
|
|
414
577
|
* 会让同 key 的多条任务互相顶替:位置显示命中最早那条兄弟、第一条开始时删掉
|
|
415
|
-
*
|
|
578
|
+
* 索引导致其余兄弟被判「已撤销」静默丢弃(连点 Compact
|
|
416
579
|
* 三次 → 三张排队卡都显示「第 1 位」、两张永久停在「等待中」,只压缩一次)。
|
|
417
580
|
*
|
|
418
581
|
* Date.now() 不够(两次同步调用可落在同一毫秒);计数器与时间无关,恒唯一。
|
|
@@ -433,7 +596,7 @@ export class CommandRouter {
|
|
|
433
596
|
/** Session reader 注册中心(public:测试注入/读取,替代 as unknown as)。 */
|
|
434
597
|
sessionReaderRegistry;
|
|
435
598
|
/**
|
|
436
|
-
* 串行化 config.*
|
|
599
|
+
* 串行化 config.* 卡片回调。
|
|
437
600
|
* CardKit 2.0 input/button 回调经 `enqueueImmediate` 分发,**不进串行队列**,
|
|
438
601
|
* 多次快速点击下拉/文本框会并发读写 `pendingConfig` 并同时调
|
|
439
602
|
* `updateCardInPlace` → Feishu API 乱序到达,控件卡死在某个状态。
|
|
@@ -483,7 +646,7 @@ export class CommandRouter {
|
|
|
483
646
|
/**
|
|
484
647
|
* Route a message: if it starts with /, handle as command; if it starts with !, execute as bash; otherwise forward to Claude.
|
|
485
648
|
*
|
|
486
|
-
*
|
|
649
|
+
* 命令识别前置条件(`inbound-unified-input-design.md` §5.6):
|
|
487
650
|
* 裸前缀判定会被结构占位符命中 —— SDK 把富文本里的图片渲染成 ``,
|
|
488
651
|
* 首字符恰好是 `!`,整段用户消息被当 shell 命令执行(`executeBash "[image](img_v3_…"`)。
|
|
489
652
|
* 因此命令前缀只在「调用方声明纯文本 + 剥离占位符后非空且无占位符 + 首字符是 / 或 !」
|
|
@@ -560,147 +723,98 @@ export class CommandRouter {
|
|
|
560
723
|
if (APPROVAL_ACTION_CMDS.includes(value.cmd)) {
|
|
561
724
|
return this.handleApprovalAction(value, ctx);
|
|
562
725
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
await
|
|
569
|
-
return;
|
|
570
|
-
case 'ls.browse':
|
|
571
|
-
await this.handleLsBrowse(value, ctx);
|
|
572
|
-
return;
|
|
573
|
-
case 'ls.switch':
|
|
574
|
-
await this.handleLsSwitch(value, ctx);
|
|
575
|
-
return;
|
|
576
|
-
case 'ls.page':
|
|
577
|
-
return this.handleLsPage(value, ctx);
|
|
578
|
-
case 'ls.filter':
|
|
579
|
-
return this.handleLsFilter(value, ctx);
|
|
580
|
-
case 'ws.page':
|
|
581
|
-
return this.handleWsPage(value, ctx);
|
|
582
|
-
case 'ws.filter':
|
|
583
|
-
return this.handleWsFilter(value, ctx);
|
|
584
|
-
case 'ws.use':
|
|
585
|
-
return await this.handleWsUse(value, ctx);
|
|
586
|
-
case 'ws.sort':
|
|
587
|
-
return await this.handleWsSort(value, ctx);
|
|
588
|
-
case 'ws.remove':
|
|
589
|
-
return await this.handleWsRemove(value, ctx);
|
|
590
|
-
case 'resume.use': {
|
|
591
|
-
// resume.use carries an agent field from /resume [agent] list cards
|
|
592
|
-
// (both active and completed cards include the agent field).
|
|
593
|
-
const resolvedAgent = resolveAgentKind(value.agent, this.config.defaultAgent);
|
|
594
|
-
const entry = this.sessionStore.get(ctx.userId);
|
|
595
|
-
const cwd = entry?.cwd;
|
|
596
|
-
if (!cwd) {
|
|
597
|
-
await this.bridge.sendResult({ text: '请先 /cd 设置工作目录' }, ctx);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
// 校验前不得写入 sessionId。旧实现先无条件 setSessionId 再调
|
|
601
|
-
// cmdResume 校验,过期卡片(session 不在当前 cwd)校验失败时 store 已被
|
|
602
|
-
// 污染;value.sessionId 缺失时还会静默清空已有绑定。现在统一走 cmdResume
|
|
603
|
-
// 的已验证路径(:2352 校验通过才写入),sessionId 缺失直接报错。
|
|
604
|
-
if (!value.sessionId) {
|
|
605
|
-
await this.bridge.sendResult({ text: '缺少 sessionId,请重新从 /resume 列表选择会话' }, ctx);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
await this.bridge.sendResult(this.cmdResume([resolvedAgent, value.sessionId], ctx), ctx);
|
|
726
|
+
const spec = CARD_ACTION_SPECS.get(value.cmd);
|
|
727
|
+
if (spec) {
|
|
728
|
+
// Map 查表取出的是未绑定方法,必须显式 .call(this, ...) 保留类实例上下文。
|
|
729
|
+
const handler = this[spec.method];
|
|
730
|
+
if (spec.args === 'path') {
|
|
731
|
+
await handler.call(this, value.path, ctx);
|
|
609
732
|
return;
|
|
610
733
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
case 'active.page':
|
|
614
|
-
return this.handleActivePage(value, ctx);
|
|
615
|
-
case 'new-session':
|
|
616
|
-
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
617
|
-
clearSessionCwd: true,
|
|
618
|
-
});
|
|
619
|
-
{
|
|
620
|
-
const cwd = this.sessionStore.getCwd(ctx.userId) ?? '(未设置)';
|
|
621
|
-
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
622
|
-
await this.bridge.sendResult({ text: `已创建新 ${agentName} 会话,下一条消息将开始全新对话\n📁 ${cwd}` }, ctx);
|
|
623
|
-
}
|
|
624
|
-
return;
|
|
625
|
-
case 'stop':
|
|
626
|
-
await this.handleCardStop(value, ctx);
|
|
734
|
+
if (spec.args === 'ctxOnly') {
|
|
735
|
+
await handler.call(this, ctx);
|
|
627
736
|
return;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
// 撤销:从队列中删除这个消息
|
|
633
|
-
await this.handleQueueCancel(value, ctx);
|
|
634
|
-
return;
|
|
635
|
-
case 'queue.immediate':
|
|
636
|
-
// 立即执行:停止当前进程,删除队列中这个消息之前的全部消息,立即执行这一条
|
|
637
|
-
await this.handleQueueImmediate(value, ctx);
|
|
638
|
-
return;
|
|
639
|
-
case 'queue.diagnose':
|
|
640
|
-
// 诊断:切换到项目目录,启动 Claude -p 定位问题
|
|
641
|
-
await this.handleQueueDiagnose(value, ctx);
|
|
642
|
-
return;
|
|
643
|
-
case 'queue.edit':
|
|
644
|
-
// 编辑:显示输入框让用户修改消息内容
|
|
645
|
-
await this.handleQueueEdit(value, ctx);
|
|
646
|
-
return;
|
|
647
|
-
case 'queue.input':
|
|
648
|
-
// 输入:提交新消息内容。返回 toast(SDK 作为飞书回调响应给点击用户即时反馈)。
|
|
649
|
-
return await this.handleQueueInput(value, ctx);
|
|
650
|
-
case 'order.delete':
|
|
651
|
-
return await this.handleOrderDelete(value, ctx);
|
|
652
|
-
case 'order.page':
|
|
653
|
-
return await this.handleOrderPage(value, ctx);
|
|
654
|
-
case 'order.aliasEdit':
|
|
655
|
-
return await this.handleOrderAliasEdit(value, ctx);
|
|
656
|
-
case 'order.aliasInput':
|
|
657
|
-
return await this.handleOrderAliasInput(value, ctx);
|
|
658
|
-
case 'order.aliasRemove':
|
|
659
|
-
return await this.handleOrderAliasRemove(value, ctx);
|
|
660
|
-
case 'order.textEdit':
|
|
661
|
-
return await this.handleOrderTextEdit(value, ctx);
|
|
662
|
-
case 'order.textInput':
|
|
663
|
-
return await this.handleOrderTextInput(value, ctx);
|
|
664
|
-
case 'config.set':
|
|
665
|
-
case 'config.input':
|
|
666
|
-
case 'config.save':
|
|
667
|
-
// 2026-07-04: 串行化所有 config.* 动作。CardKit 2.0 回调经 enqueueImmediate
|
|
668
|
-
// 不进串行队列,多次快速点击下拉/文本框会并发读写 pendingConfig 并同时
|
|
669
|
-
// 调 updateCardInPlace → Feishu API 乱序到达导致控件卡死。
|
|
670
|
-
// 2026-07-18: 返回 enqueueConfigAction 的结果以支持 toast 响应
|
|
671
|
-
return this.enqueueConfigAction(value, ctx);
|
|
672
|
-
case 'compact':
|
|
673
|
-
await this.bridge.handleCompactAction(value, ctx);
|
|
674
|
-
return;
|
|
675
|
-
case 'resume.compact':
|
|
676
|
-
await this.bridge.handleResumeCompact(value, ctx);
|
|
677
|
-
return;
|
|
678
|
-
default: {
|
|
679
|
-
// Help card buttons: help.<cmd> → execute /<cmd>
|
|
680
|
-
if (value.cmd.startsWith('help.')) {
|
|
681
|
-
const subCmd = value.cmd.slice(5); // e.g. "status", "ps", "stop"
|
|
682
|
-
const result = await this.executeCommand(`/${subCmd}`, ctx);
|
|
683
|
-
if (result) {
|
|
684
|
-
await this.bridge.sendResult(result, ctx);
|
|
685
|
-
}
|
|
686
|
-
// /exit、/restart 等命令通过 pendingExit 表达"回复送达后退出"。
|
|
687
|
-
// handle() 在 sendResult 后消费它;help.* 按钮点击走 handleCardAction,
|
|
688
|
-
// 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
|
|
689
|
-
// 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
|
|
690
|
-
if (this.pendingExit) {
|
|
691
|
-
await this.flushInbound?.();
|
|
692
|
-
this.exitHandler();
|
|
693
|
-
}
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
// Design constraint: miss paths must reply via bridge.sendResult —
|
|
697
|
-
// don't silently swallow unknown card actions.
|
|
698
|
-
getLogger().warn(`[router] unknown card action: ${value.cmd}`);
|
|
699
|
-
await this.bridge.sendResult({ text: `⚠️ 未知的卡片操作: ${value.cmd}` }, ctx);
|
|
737
|
+
}
|
|
738
|
+
// voidCall 对应原 switch 的 `await this.xxx(...); return;`,丢弃返回值。
|
|
739
|
+
if (spec.args === 'voidCall') {
|
|
740
|
+
await handler.call(this, value, ctx);
|
|
700
741
|
return;
|
|
701
742
|
}
|
|
743
|
+
return handler.call(this, value, ctx);
|
|
744
|
+
}
|
|
745
|
+
if (value.cmd.startsWith('help.')) {
|
|
746
|
+
await this.handleHelpAction(value, ctx);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
await this.handleUnknownCardAction(value, ctx);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* resume.use: 从 /resume 列表卡切换到所选会话。
|
|
754
|
+
*/
|
|
755
|
+
async handleResumeUse(value, ctx) {
|
|
756
|
+
// resume.use carries an agent field from /resume [agent] list cards
|
|
757
|
+
// (both active and completed cards include the agent field).
|
|
758
|
+
const resolvedAgent = resolveAgentKind(value.agent, this.config.defaultAgent);
|
|
759
|
+
const entry = this.sessionStore.get(ctx.userId);
|
|
760
|
+
const cwd = entry?.cwd;
|
|
761
|
+
if (!cwd) {
|
|
762
|
+
await this.bridge.sendResult({ text: '请先 /cd 设置工作目录' }, ctx);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
// 校验前不得写入 sessionId。旧实现先无条件 setSessionId 再调
|
|
766
|
+
// cmdResume 校验,过期卡片(session 不在当前 cwd)校验失败时 store 已被
|
|
767
|
+
// 污染;value.sessionId 缺失时还会静默清空已有绑定。现在统一走 cmdResume
|
|
768
|
+
// 的已验证路径(:2352 校验通过才写入),sessionId 缺失直接报错。
|
|
769
|
+
if (!value.sessionId) {
|
|
770
|
+
await this.bridge.sendResult({ text: '缺少 sessionId,请重新从 /resume 列表选择会话' }, ctx);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
await this.bridge.sendResult(this.cmdResume([resolvedAgent, value.sessionId], ctx), ctx);
|
|
774
|
+
}
|
|
775
|
+
/** new-session: 清空当前会话绑定,下一条消息开始全新对话。 */
|
|
776
|
+
async handleNewSession(ctx) {
|
|
777
|
+
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
778
|
+
clearSessionCwd: true,
|
|
779
|
+
});
|
|
780
|
+
const cwd = this.sessionStore.getCwd(ctx.userId) ?? '(未设置)';
|
|
781
|
+
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
782
|
+
await this.bridge.sendResult({ text: `已创建新 ${agentName} 会话,下一条消息将开始全新对话\n📁 ${cwd}` }, ctx);
|
|
783
|
+
}
|
|
784
|
+
/** compact: 对当前会话发起压缩(bridge 收口)。 */
|
|
785
|
+
async handleCompact(value, ctx) {
|
|
786
|
+
await this.bridge.handleCompactAction(value, ctx);
|
|
787
|
+
}
|
|
788
|
+
/** resume.compact: 对 /resume 选中的历史会话发起压缩(bridge 收口)。 */
|
|
789
|
+
async handleResumeCompact(value, ctx) {
|
|
790
|
+
await this.bridge.handleResumeCompact(value, ctx);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Help card buttons: help.<cmd> → execute /<cmd>.
|
|
794
|
+
* /exit、/restart 等命令通过 pendingExit 表达"回复送达后退出"。
|
|
795
|
+
* handle() 在 sendResult 后消费它;help.* 按钮点击走 handleCardAction,
|
|
796
|
+
* 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
|
|
797
|
+
* 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
|
|
798
|
+
*/
|
|
799
|
+
async handleHelpAction(value, ctx) {
|
|
800
|
+
const subCmd = value.cmd.slice(5); // e.g. "status", "ps", "stop"
|
|
801
|
+
const result = await this.executeCommand(`/${subCmd}`, ctx);
|
|
802
|
+
if (result) {
|
|
803
|
+
await this.bridge.sendResult(result, ctx);
|
|
804
|
+
}
|
|
805
|
+
if (this.pendingExit) {
|
|
806
|
+
await this.flushInbound?.();
|
|
807
|
+
this.exitHandler();
|
|
702
808
|
}
|
|
703
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Design constraint: miss paths must reply via bridge.sendResult —
|
|
812
|
+
* don't silently swallow unknown card actions.
|
|
813
|
+
*/
|
|
814
|
+
async handleUnknownCardAction(value, ctx) {
|
|
815
|
+
getLogger().warn(`[router] unknown card action: ${value.cmd}`);
|
|
816
|
+
await this.bridge.sendResult({ text: `⚠️ 未知的卡片操作: ${value.cmd}` }, ctx);
|
|
817
|
+
}
|
|
704
818
|
/**
|
|
705
819
|
* Handle approval-related card actions.
|
|
706
820
|
* Routes to the appropriate bridge method based on value.cmd.
|
|
@@ -1330,7 +1444,7 @@ export class CommandRouter {
|
|
|
1330
1444
|
cardMessageId: ctx.messageId,
|
|
1331
1445
|
});
|
|
1332
1446
|
const stopped = interrupt.stopped;
|
|
1333
|
-
//
|
|
1447
|
+
// card stop button silently no-op'd when the
|
|
1334
1448
|
// run had already exited. Give the user a visible result.
|
|
1335
1449
|
if (!stopped) {
|
|
1336
1450
|
await this.bridge.sendResult({ text: '该任务已结束,无需终止' }, ctx);
|
|
@@ -1810,8 +1924,6 @@ export class CommandRouter {
|
|
|
1810
1924
|
this.configCardIds.add(messageId);
|
|
1811
1925
|
while (this.configCardIds.size > MAX_CONFIG_CARDS) {
|
|
1812
1926
|
const oldest = this.configCardIds.values().next().value;
|
|
1813
|
-
if (oldest === undefined)
|
|
1814
|
-
break;
|
|
1815
1927
|
this.configCardIds.delete(oldest);
|
|
1816
1928
|
}
|
|
1817
1929
|
}
|
|
@@ -1858,7 +1970,7 @@ export class CommandRouter {
|
|
|
1858
1970
|
}
|
|
1859
1971
|
}
|
|
1860
1972
|
/**
|
|
1861
|
-
* 串行化 config.*
|
|
1973
|
+
* 串行化 config.* 卡片回调(修复连击卡死 bug)。
|
|
1862
1974
|
*
|
|
1863
1975
|
* CardKit 2.0 input/button 回调经 `enqueueImmediate` 分发,不进 bridge 的
|
|
1864
1976
|
* 串行队列;多次快速点击下拉/文本框会并发读写 `pendingConfig` 并同时调
|
|
@@ -1899,7 +2011,7 @@ export class CommandRouter {
|
|
|
1899
2011
|
}
|
|
1900
2012
|
case 'config.input': {
|
|
1901
2013
|
// Handle input field: value.key is the field key.
|
|
1902
|
-
//
|
|
2014
|
+
// CardKit 2.0 input 提交(回车/完成键)触发 callback,回传 input_value。
|
|
1903
2015
|
// Fall back to formValue[key]: the SDK currently reports input values
|
|
1904
2016
|
// via action.formValue for form-wrapped inputs.
|
|
1905
2017
|
const key = value.key;
|
|
@@ -1934,8 +2046,8 @@ export class CommandRouter {
|
|
|
1934
2046
|
// defaultAgent 切换的 session 处理)抽到共享方法,与文本直写路径共用。
|
|
1935
2047
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, updates, ctx);
|
|
1936
2048
|
this.pendingConfig = null;
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
2049
|
+
// 原地更新卡片:保存后卡片刷新为已保存状态。
|
|
2050
|
+
// 卡片刷新失败不吞切换通知——保存已成功,仅记录日志,
|
|
1939
2051
|
// 后续切换消息照常发送;真正写盘/传播阶段的失败才走外层 catch 报「保存失败」。
|
|
1940
2052
|
try {
|
|
1941
2053
|
// host 可能已变更 → 重新预取 DSH 目录(内部按 host 缓存,未变不重复拉)
|
|
@@ -1945,7 +2057,7 @@ export class CommandRouter {
|
|
|
1945
2057
|
catch (refreshErr) {
|
|
1946
2058
|
getLogger().warn(`[router] config.save 卡片刷新失败(保存已成功): ${refreshErr.message}`);
|
|
1947
2059
|
}
|
|
1948
|
-
//
|
|
2060
|
+
// agent 切换时发送 Resume 卡片(替代纯文本通知),
|
|
1949
2061
|
// 让用户直观看到新 agent 的会话状态和历史。
|
|
1950
2062
|
// 发送失败(resolve false)时兜底回退 toast 即时反馈。
|
|
1951
2063
|
if (switchResult.notice) {
|
|
@@ -1996,11 +2108,10 @@ export class CommandRouter {
|
|
|
1996
2108
|
// idle.watchdogMinutes 改动需同步到 bridge 的 idleTimeoutMs
|
|
1997
2109
|
this.idleTimeoutMs = this.config.idle.watchdogMinutes * 60_000;
|
|
1998
2110
|
this.bridge.setIdleTimeout(this.idleTimeoutMs);
|
|
1999
|
-
//
|
|
2111
|
+
// 检测是否有任何 agent 配置变更或 defaultAgent 切换
|
|
2000
2112
|
const hasDefaultAgentChange = oldDefaultAgent !== this.config.defaultAgent;
|
|
2001
2113
|
const changedKeys = Object.keys(updates);
|
|
2002
|
-
const hasAgentConfigChange = changedKeys
|
|
2003
|
-
AGENT_KINDS.some((kind) => touchesAgentConfig(changedKeys, kind));
|
|
2114
|
+
const hasAgentConfigChange = computeHasAgentConfigChange(changedKeys);
|
|
2004
2115
|
// 当 defaultAgent 改变时,清除旧 agent 的 sessionId,
|
|
2005
2116
|
// 同时清除新 agent 的 sessionId(等效于 /new 命令),
|
|
2006
2117
|
// 并支持 session 恢复 - 切换回来时如果条件满足则恢复之前的 session。
|
|
@@ -2008,46 +2119,10 @@ export class CommandRouter {
|
|
|
2008
2119
|
let switchNewAgent;
|
|
2009
2120
|
let switchSessionId;
|
|
2010
2121
|
if (hasDefaultAgentChange) {
|
|
2011
|
-
const
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
const oldArrivalSessionId = this.sessionStore.getArrivalSessionId(ctx.userId, oldDefaultAgent);
|
|
2016
|
-
const userChangedOld = (oldSessionId ?? '') !== (oldArrivalSessionId ?? '');
|
|
2017
|
-
// Step 1: 保存旧 agent 的 session 到 previousSessions(如果有)
|
|
2018
|
-
if (oldSessionId) {
|
|
2019
|
-
this.sessionStore.setPreviousSessionId(ctx.userId, oldDefaultAgent, oldSessionId);
|
|
2020
|
-
}
|
|
2021
|
-
// Step 2: 清除旧 agent 的 session(保存后清除,为新会话腾出空间)
|
|
2022
|
-
this.sessionStore.clearSessionId(ctx.userId, oldDefaultAgent);
|
|
2023
|
-
// Step 2.5: 显式选择判定
|
|
2024
|
-
const newSessionId = this.sessionStore.getSessionId(ctx.userId, newAgent);
|
|
2025
|
-
const newArrivalSessionId = this.sessionStore.getArrivalSessionId(ctx.userId, newAgent);
|
|
2026
|
-
const userSelectedNew = !!newSessionId && (newSessionId ?? '') !== (newArrivalSessionId ?? '');
|
|
2027
|
-
// Step 3: 恢复判定
|
|
2028
|
-
const previousSessionId = this.sessionStore.getPreviousSessionId(ctx.userId, newAgent);
|
|
2029
|
-
const canRestore = !!previousSessionId && !userChangedOld;
|
|
2030
|
-
if (userSelectedNew) {
|
|
2031
|
-
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, newSessionId);
|
|
2032
|
-
const newAgentName = agentDisplayName(newAgent);
|
|
2033
|
-
switchNotice = `已切换到 ${newAgentName},已使用所选 session,sessionId: ${newSessionId}`;
|
|
2034
|
-
switchSessionId = newSessionId;
|
|
2035
|
-
}
|
|
2036
|
-
else if (canRestore) {
|
|
2037
|
-
this.sessionStore.setSessionId(ctx.userId, newAgent, previousSessionId);
|
|
2038
|
-
this.sessionStore.clearPreviousSessionId(ctx.userId, newAgent);
|
|
2039
|
-
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, previousSessionId);
|
|
2040
|
-
const newAgentName = agentDisplayName(newAgent);
|
|
2041
|
-
switchNotice = `已切换到 ${newAgentName},将继续之前的 session,sessionId: ${previousSessionId}`;
|
|
2042
|
-
switchSessionId = previousSessionId;
|
|
2043
|
-
}
|
|
2044
|
-
else {
|
|
2045
|
-
this.sessionStore.clearSessionId(ctx.userId, newAgent, { clearSessionCwd: true });
|
|
2046
|
-
this.sessionStore.setArrivalSessionId(ctx.userId, newAgent, '');
|
|
2047
|
-
const newAgentName = agentDisplayName(newAgent);
|
|
2048
|
-
switchNotice = `已切换到 ${newAgentName},session 已清空,下次消息将启动新对话`;
|
|
2049
|
-
switchSessionId = '';
|
|
2050
|
-
}
|
|
2122
|
+
const outcome = applyDefaultAgentSwitch(this.sessionStore, ctx.userId, oldDefaultAgent, this.config.defaultAgent);
|
|
2123
|
+
switchNotice = outcome.notice;
|
|
2124
|
+
switchNewAgent = outcome.newAgent;
|
|
2125
|
+
switchSessionId = outcome.sessionId;
|
|
2051
2126
|
}
|
|
2052
2127
|
// DSH 的两个「会话边界」变更共用一套处理:preset 在 session 创建时固定,
|
|
2053
2128
|
// 中途切换会被服务端拒绝(agent-preset-conflict);host 变更等于换服务,
|
|
@@ -2055,22 +2130,9 @@ export class CommandRouter {
|
|
|
2055
2130
|
// previousSessions 恢复槽(可 /resume 校验后恢复)并清空当前值,下次 run
|
|
2056
2131
|
// 走 session.create 新建。已发生 agent 切换时(switchNotice 已设置)跳过,
|
|
2057
2132
|
// 避免覆盖切换 notice——切换分支对 dsh 的 session 已做恢复/清空处理。
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
const oldSessionId = this.sessionStore.getSessionId(ctx.userId, 'dsh');
|
|
2062
|
-
if (oldSessionId) {
|
|
2063
|
-
this.sessionStore.setPreviousSessionId(ctx.userId, 'dsh', oldSessionId);
|
|
2064
|
-
this.sessionStore.clearSessionId(ctx.userId, 'dsh');
|
|
2065
|
-
switchNotice = `DSH ${label} 已变更,旧会话已停放到恢复槽(可 /resume 校验后恢复),下次消息将新建会话`;
|
|
2066
|
-
}
|
|
2067
|
-
else {
|
|
2068
|
-
switchNotice = `DSH ${label} 已变更,下次消息将新建会话`;
|
|
2069
|
-
}
|
|
2070
|
-
};
|
|
2071
|
-
parkDshSessionBoundary(changedKeys.includes('agents.dsh.agentPreset'), 'preset');
|
|
2072
|
-
parkDshSessionBoundary(changedKeys.includes('agents.dsh.host'), 'host');
|
|
2073
|
-
// 2026-07-13: 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
|
|
2133
|
+
switchNotice = parkDshSessionBoundaryNotice(this.sessionStore, ctx.userId, this.config.defaultAgent, changedKeys.includes('agents.dsh.agentPreset'), 'preset', switchNotice);
|
|
2134
|
+
switchNotice = parkDshSessionBoundaryNotice(this.sessionStore, ctx.userId, this.config.defaultAgent, changedKeys.includes('agents.dsh.host'), 'host', switchNotice);
|
|
2135
|
+
// 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
|
|
2074
2136
|
const currentAgent = this.config.defaultAgent;
|
|
2075
2137
|
if (touchesAgentsSubtree(changedKeys, currentAgent)) {
|
|
2076
2138
|
this.config = syncAgentChoices(this.config, currentAgent);
|
|
@@ -2084,12 +2146,11 @@ export class CommandRouter {
|
|
|
2084
2146
|
// duck method `updateApprovalMode` (codex thread/settings/update,
|
|
2085
2147
|
// kimi/opencode session/set_mode) so they take effect for subsequent
|
|
2086
2148
|
// turns without waiting for the runner to be recreated.
|
|
2087
|
-
const hasApprovalConfigChange =
|
|
2088
|
-
changedKeys.includes('agents.dsh.permissionPreset');
|
|
2149
|
+
const hasApprovalConfigChange = computeHasApprovalConfigChange(changedKeys);
|
|
2089
2150
|
if (hasApprovalConfigChange) {
|
|
2090
2151
|
this.bridge.syncActiveApprovalModes();
|
|
2091
2152
|
}
|
|
2092
|
-
//
|
|
2153
|
+
// 任何 agent 配置变更或 defaultAgent 切换,都清除 runner 缓存
|
|
2093
2154
|
if (hasDefaultAgentChange || hasAgentConfigChange) {
|
|
2094
2155
|
this.bridge.clearRunners();
|
|
2095
2156
|
}
|
|
@@ -2109,60 +2170,13 @@ export class CommandRouter {
|
|
|
2109
2170
|
'如果发得太快,也可能被飞书合并成一条消息',
|
|
2110
2171
|
}, ctx);
|
|
2111
2172
|
}
|
|
2112
|
-
|
|
2113
|
-
case 'help':
|
|
2114
|
-
case 'h':
|
|
2115
|
-
return this.cmdHelp();
|
|
2116
|
-
case 'status':
|
|
2117
|
-
case 's':
|
|
2118
|
-
return this.cmdStatus(ctx);
|
|
2119
|
-
case 'ps':
|
|
2120
|
-
return this.cmdPs(ctx);
|
|
2121
|
-
case 'stop':
|
|
2122
|
-
case 't':
|
|
2123
|
-
return await this.cmdStop(ctx);
|
|
2124
|
-
case 'exit':
|
|
2125
|
-
case 'quit':
|
|
2126
|
-
case 'e':
|
|
2127
|
-
return await this.cmdExit();
|
|
2128
|
-
case 'restart':
|
|
2129
|
-
return this.cmdRestart();
|
|
2130
|
-
case 'new':
|
|
2131
|
-
return this.cmdNew(ctx);
|
|
2132
|
-
case 'cd':
|
|
2133
|
-
return this.cmdCd(args, ctx);
|
|
2134
|
-
case 'ls':
|
|
2135
|
-
return this.cmdLs(args, ctx);
|
|
2136
|
-
case 'download':
|
|
2137
|
-
case 'd':
|
|
2138
|
-
return await this.cmdDownload(args, ctx);
|
|
2139
|
-
case 'ws':
|
|
2140
|
-
return this.cmdWs(args, ctx);
|
|
2141
|
-
case 'resume':
|
|
2142
|
-
case 'r':
|
|
2143
|
-
return this.cmdResume(args, ctx);
|
|
2144
|
-
case 'active':
|
|
2145
|
-
return this.cmdActive(args, ctx);
|
|
2146
|
-
case 'config':
|
|
2147
|
-
case 'c':
|
|
2148
|
-
return await this.cmdConfig(args, ctx);
|
|
2149
|
-
case 'order':
|
|
2150
|
-
case 'o':
|
|
2151
|
-
return this.cmdOrder(args, ctx);
|
|
2152
|
-
case 'update':
|
|
2153
|
-
case 'u':
|
|
2154
|
-
return await this.cmdUpdate(args, ctx);
|
|
2155
|
-
case 'clone':
|
|
2156
|
-
return await this.cmdClone(args, ctx);
|
|
2157
|
-
default:
|
|
2158
|
-
return { text: `未知命令 /${cmd},输入 /help 查看可用命令` };
|
|
2159
|
-
}
|
|
2173
|
+
return dispatchCommand(this, cmd, args, ctx);
|
|
2160
2174
|
}
|
|
2161
2175
|
// --- Command implementations ---
|
|
2162
2176
|
cmdHelp() {
|
|
2163
2177
|
// 按钮组(可直接点击触发;按钮 label 只保留子命令,不含参数)
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2178
|
+
// /ws 子命令 (save|use|remove) 移到右侧文本说明,按钮只显示 /ws
|
|
2179
|
+
// 按钮 label 按长度升序排列(短在上,长在下),视觉更整齐
|
|
2166
2180
|
const buttonCommands = [
|
|
2167
2181
|
{ cmd: 'help', label: '/help /h', desc: '显示此帮助' },
|
|
2168
2182
|
{ cmd: 'ps', label: '/ps', desc: '查看是否有 agent 进程在跑' },
|
|
@@ -2203,11 +2217,11 @@ export class CommandRouter {
|
|
|
2203
2217
|
];
|
|
2204
2218
|
const bodyElements = [];
|
|
2205
2219
|
// 按钮组 — CardKit 2.0: column_set + column + button with behaviors
|
|
2206
|
-
//
|
|
2220
|
+
// 对齐修复(方案 A):两列都用 width:'weighted' + 固定 weight。
|
|
2207
2221
|
// 根因:每行独立 column_set + width:'auto' → 列宽随按钮/文本内容变化 →
|
|
2208
2222
|
// 跨行不对齐(按钮列右边界参差)。改用 weighted 后,每行 column_set
|
|
2209
2223
|
// 总宽相同(同级 body.elements),weight 比例一致,列宽跨行恒等。
|
|
2210
|
-
//
|
|
2224
|
+
// 手机排版修复:weight 1:3 → 2:3。1:3 时按钮列只占 25%,
|
|
2211
2225
|
// 手机窄屏(~330px 可用)下约 82px,容不下 `/config /c`(10 字符 ≈ 100px),
|
|
2212
2226
|
// 按钮文字被截断成 "..."。改 2:3 后按钮列占 40% ≈ 132px,足够显示
|
|
2213
2227
|
// 最长的 /config /c,文本列仍占 60% 足以放下描述。
|
|
@@ -2460,8 +2474,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2460
2474
|
}
|
|
2461
2475
|
cmdNew(ctx) {
|
|
2462
2476
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
2463
|
-
// 设置工作目录",但用户的 cwd
|
|
2464
|
-
//
|
|
2477
|
+
// 设置工作目录",但用户的 cwd 还在,用户 /new → /resume 后会被误导
|
|
2478
|
+
// 以为需要重新 /cd。
|
|
2465
2479
|
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
2466
2480
|
this.sessionStore.clearSessionId(ctx.userId, this.config.defaultAgent, {
|
|
2467
2481
|
clearSessionCwd: true,
|
|
@@ -2546,38 +2560,66 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2546
2560
|
}
|
|
2547
2561
|
/** Build card for auto-resumed session with new session button */
|
|
2548
2562
|
buildAutoResumeCard(cwd, session, _ctx) {
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2563
|
+
const agentKind = this.config.defaultAgent;
|
|
2564
|
+
return this.buildResumeLikeCard({
|
|
2565
|
+
cwd,
|
|
2566
|
+
sessionId: session.sessionId,
|
|
2567
|
+
agentKind,
|
|
2568
|
+
// 仅 auto-resume 入口带 Compact 按钮(runner 支持压缩时);
|
|
2569
|
+
// config-switch 卡不带。
|
|
2570
|
+
compact: this.canCompactSession(agentKind, cwd)
|
|
2571
|
+
? { sessionId: session.sessionId, agentKind }
|
|
2572
|
+
: undefined,
|
|
2573
|
+
titleBase: '自动恢复会话',
|
|
2574
|
+
idleIcon: '🔁',
|
|
2575
|
+
resumedLine: `已恢复最近会话: **${session.sessionId}**`,
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
/**
|
|
2579
|
+
* Shared body of the two "resume-like" cards(cwd 切换后的 auto-resume 与
|
|
2580
|
+
* config.save 换 agent):两者都是同一序列 readSessionDisplayState →
|
|
2581
|
+
* buildResumeActionButtons → buildSessionHistoryCard,差异仅 4 处——
|
|
2582
|
+
* Compact 按钮(仅 auto-resume,canCompactSession 门控)、标题文案、
|
|
2583
|
+
* headerText 的「已恢复…会话」行与可选尾部行、空会话占位——全部参数化,
|
|
2584
|
+
* 共享序列只留一份(Clean Code DRY)。
|
|
2585
|
+
*/
|
|
2586
|
+
buildResumeLikeCard(opts) {
|
|
2587
|
+
const agentName = agentDisplayName(opts.agentKind);
|
|
2588
|
+
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(opts.sessionId, opts.cwd, {
|
|
2589
|
+
// 默认 agent 时 readSessionDisplayState 内部仍走 this.sessionReader
|
|
2590
|
+
//(agentKind === defaultAgent 不触发 registry 查找),与旧的不传等价。
|
|
2591
|
+
agentKind: opts.agentKind,
|
|
2592
|
+
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2593
|
+
});
|
|
2551
2594
|
const actions = this.buildResumeActionButtons({
|
|
2552
2595
|
isActive,
|
|
2553
2596
|
activeRunRunId,
|
|
2554
|
-
cwd,
|
|
2555
|
-
compact:
|
|
2556
|
-
? { sessionId: session.sessionId, agentKind: this.config.defaultAgent }
|
|
2557
|
-
: undefined,
|
|
2597
|
+
cwd: opts.cwd,
|
|
2598
|
+
compact: opts.compact,
|
|
2558
2599
|
newSession: true,
|
|
2559
2600
|
});
|
|
2560
2601
|
const card = buildSessionHistoryCard({
|
|
2561
|
-
sessionId:
|
|
2562
|
-
cwd,
|
|
2602
|
+
sessionId: opts.sessionId,
|
|
2603
|
+
cwd: opts.cwd,
|
|
2563
2604
|
displayTitle,
|
|
2564
2605
|
aiTitle,
|
|
2565
2606
|
recap,
|
|
2566
2607
|
events: content,
|
|
2567
2608
|
usage,
|
|
2568
2609
|
}, {
|
|
2569
|
-
agentKind:
|
|
2570
|
-
headerText: `📂 \`${cwd}\`\n
|
|
2571
|
-
title: `${isActive ?
|
|
2610
|
+
agentKind: opts.agentKind,
|
|
2611
|
+
headerText: `📂 \`${opts.cwd}\`\n${opts.resumedLine}${opts.headerTail ? `\n${opts.headerTail}` : ''}`,
|
|
2612
|
+
title: `${isActive ? `⏳ ${opts.titleBase}(完成中)` : `${opts.idleIcon} ${opts.titleBase}`} · ${agentName}`,
|
|
2572
2613
|
usageResult: 'success',
|
|
2614
|
+
...(opts.emptyPlaceholder !== undefined ? { emptyPlaceholder: opts.emptyPlaceholder } : {}),
|
|
2573
2615
|
actions,
|
|
2574
2616
|
});
|
|
2575
2617
|
return { card };
|
|
2576
2618
|
}
|
|
2577
2619
|
/**
|
|
2578
2620
|
* Build card for config.save agent switch.
|
|
2579
|
-
* When the new agent has a restored session, renders a Resume card (
|
|
2580
|
-
*
|
|
2621
|
+
* When the new agent has a restored session, renders a Resume card (shared
|
|
2622
|
+
* body via buildResumeLikeCard) with session history + stop/new buttons.
|
|
2581
2623
|
* When the session was cleared, renders a compact notice card instead.
|
|
2582
2624
|
*/
|
|
2583
2625
|
buildConfigSwitchCard(switchResult, ctx) {
|
|
@@ -2601,62 +2643,25 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2601
2643
|
},
|
|
2602
2644
|
};
|
|
2603
2645
|
}
|
|
2604
|
-
// Has session → Resume card (
|
|
2605
|
-
//
|
|
2606
|
-
//
|
|
2607
|
-
|
|
2608
|
-
? this.sessionReader
|
|
2609
|
-
: this.sessionReaderRegistry.get(newAgent);
|
|
2610
|
-
if (!readerForNewAgent) {
|
|
2611
|
-
// Fallback: compact notice card with the notice text
|
|
2612
|
-
const elements = [markdownDiv(switchResult.notice)];
|
|
2613
|
-
elements.push({
|
|
2614
|
-
tag: 'column_set',
|
|
2615
|
-
columns: [
|
|
2616
|
-
{
|
|
2617
|
-
tag: 'column',
|
|
2618
|
-
width: 'auto',
|
|
2619
|
-
elements: [newSessionButton()],
|
|
2620
|
-
},
|
|
2621
|
-
],
|
|
2622
|
-
});
|
|
2623
|
-
return {
|
|
2624
|
-
card: {
|
|
2625
|
-
schema: '2.0',
|
|
2626
|
-
config: { wide_screen_mode: true },
|
|
2627
|
-
header: { title: { tag: 'plain_text', content: `🔀 切换到 ${agentName}` } },
|
|
2628
|
-
body: { elements },
|
|
2629
|
-
},
|
|
2630
|
-
};
|
|
2631
|
-
}
|
|
2632
|
-
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionId, cwd, {
|
|
2633
|
-
agentKind: newAgent,
|
|
2634
|
-
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2635
|
-
});
|
|
2636
|
-
// Action buttons: stop (active) + new session
|
|
2637
|
-
const actions = this.buildResumeActionButtons({
|
|
2638
|
-
isActive,
|
|
2639
|
-
activeRunRunId,
|
|
2646
|
+
// Has session → Resume card (shared body with buildAutoResumeCard)
|
|
2647
|
+
// SessionReaderRegistry.get() throws when the agent is not registered —
|
|
2648
|
+
// a null-reader fallback branch here is unreachable either way.
|
|
2649
|
+
return this.buildResumeLikeCard({
|
|
2640
2650
|
cwd,
|
|
2641
|
-
newSession: true,
|
|
2642
|
-
});
|
|
2643
|
-
const card = buildSessionHistoryCard({
|
|
2644
2651
|
sessionId,
|
|
2645
|
-
cwd,
|
|
2646
|
-
displayTitle,
|
|
2647
|
-
aiTitle,
|
|
2648
|
-
recap,
|
|
2649
|
-
events: content,
|
|
2650
|
-
usage,
|
|
2651
|
-
}, {
|
|
2652
2652
|
agentKind: newAgent,
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2653
|
+
titleBase: '切换 Agent',
|
|
2654
|
+
idleIcon: '🔀',
|
|
2655
|
+
// 切换语义(已使用所选 / 将继续之前的 / 已清空)必须随卡可见:
|
|
2656
|
+
// 2026-08-13 换 Resume 卡时这条信息一度只在「无 reader」回退分支
|
|
2657
|
+
// 渲染,生产路径(registry 恒有 reader)上不可见——卡上只剩
|
|
2658
|
+
// sessionId,用户无法区分显式选择、停车恢复与清空。notice 含
|
|
2659
|
+
// sessionId,与「已恢复会话」行并存(agent-switch-entry-points 与
|
|
2660
|
+
// arrival-baseline 两族 anchor 分别钉住两行)。
|
|
2661
|
+
resumedLine: `已恢复会话: **${sessionId}**`,
|
|
2662
|
+
headerTail: switchResult.notice,
|
|
2656
2663
|
emptyPlaceholder: '_该会话暂无新消息可显示(最后一条为用户输入)_',
|
|
2657
|
-
actions,
|
|
2658
2664
|
});
|
|
2659
|
-
return { card };
|
|
2660
2665
|
}
|
|
2661
2666
|
/**
|
|
2662
2667
|
* Whether a session of the given agent kind can be compacted from a resume
|
|
@@ -2701,7 +2706,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2701
2706
|
* @param offset 分页起点(条目数)
|
|
2702
2707
|
* @param rootDir 浏览起点:卡片「返回」按钮回到这里。省略时默认 = 本次列出的
|
|
2703
2708
|
* 目录(`/ls <dir>` 的起点即该目录),因此「返回」不会把用户
|
|
2704
|
-
* 丢回
|
|
2709
|
+
* 丢回 工作目录。
|
|
2705
2710
|
* @param q 关键词筛选:只匹配**当前层**条目名(不递归),大小写不敏感。
|
|
2706
2711
|
* 省略/空 = 无筛选。
|
|
2707
2712
|
*/
|
|
@@ -2791,7 +2796,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2791
2796
|
}
|
|
2792
2797
|
cmdActive(_args, _ctx, offset = 0) {
|
|
2793
2798
|
// List all active runs from lark-remote memory (not from file system scan)
|
|
2794
|
-
//
|
|
2799
|
+
// (only shows runs started by THIS lark-remote process)
|
|
2795
2800
|
const activeRuns = this.bridge.getActiveRuns();
|
|
2796
2801
|
const activeBashRuns = this.bridge.getActiveBashRuns();
|
|
2797
2802
|
if (activeRuns.length === 0 && activeBashRuns.length === 0) {
|
|
@@ -2815,54 +2820,31 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2815
2820
|
const elements = [];
|
|
2816
2821
|
let remaining = pageSize;
|
|
2817
2822
|
let skipped = safeOffset;
|
|
2818
|
-
// Agent runs section
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
tag: '
|
|
2828
|
-
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
else {
|
|
2844
|
-
skipped -= activeRuns.length;
|
|
2845
|
-
}
|
|
2846
|
-
// Bash runs section
|
|
2847
|
-
if (activeBashRuns.length > 0 && skipped < activeBashRuns.length && remaining > 0) {
|
|
2848
|
-
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '## 💻 Bash 命令' } });
|
|
2849
|
-
const start = skipped;
|
|
2850
|
-
const end = Math.min(start + remaining, activeBashRuns.length);
|
|
2851
|
-
for (let i = start; i < end; i++) {
|
|
2852
|
-
const run = activeBashRuns[i];
|
|
2853
|
-
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2854
|
-
elements.push({
|
|
2855
|
-
tag: 'div',
|
|
2856
|
-
text: { tag: 'lark_md', content: `**\`${run.command}\`**` },
|
|
2857
|
-
});
|
|
2858
|
-
elements.push({
|
|
2859
|
-
tag: 'div',
|
|
2860
|
-
text: { tag: 'lark_md', content: `> 目录: ${run.cwd} \n> 状态: ${statusLabel}` },
|
|
2861
|
-
});
|
|
2862
|
-
elements.push(cardButton('⏹ 停止', { cmd: 'stop', runId: run.runId, cwd: run.cwd }, { type: 'danger', size: 'small' }));
|
|
2863
|
-
elements.push({ tag: 'hr' });
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2823
|
+
// Agent runs section(跨段 offset 分配:agent 段吃满 remaining 才轮到 bash 段)
|
|
2824
|
+
({ remaining, skipped } = this.appendActiveCardSection(elements, activeRuns, '## 🤖 Agent 任务', (run, statusLabel) => [
|
|
2825
|
+
{
|
|
2826
|
+
tag: 'div',
|
|
2827
|
+
text: { tag: 'lark_md', content: `**📂 ${displayName(run.cwd)}**` },
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
tag: 'div',
|
|
2831
|
+
text: {
|
|
2832
|
+
tag: 'lark_md',
|
|
2833
|
+
content: `> session: ${run.sessionId.slice(0, 12)}... \n> 状态: ${statusLabel}`,
|
|
2834
|
+
},
|
|
2835
|
+
},
|
|
2836
|
+
], remaining, skipped));
|
|
2837
|
+
// Bash runs section(末段:不再使用返回的记账值)
|
|
2838
|
+
this.appendActiveCardSection(elements, activeBashRuns, '## 💻 Bash 命令', (run, statusLabel) => [
|
|
2839
|
+
{
|
|
2840
|
+
tag: 'div',
|
|
2841
|
+
text: { tag: 'lark_md', content: `**\`${run.command}\`**` },
|
|
2842
|
+
},
|
|
2843
|
+
{
|
|
2844
|
+
tag: 'div',
|
|
2845
|
+
text: { tag: 'lark_md', content: `> 目录: ${run.cwd} \n> 状态: ${statusLabel}` },
|
|
2846
|
+
},
|
|
2847
|
+
], remaining, skipped);
|
|
2866
2848
|
// Pagination bar(复用 paginationBar:safeOffset 已对齐页边界,
|
|
2867
2849
|
// offset±pageSize 与原先 currentPage±1 的页码换算等价)
|
|
2868
2850
|
if (totalPages > 1) {
|
|
@@ -2887,6 +2869,34 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2887
2869
|
};
|
|
2888
2870
|
return { card };
|
|
2889
2871
|
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Render one section of the /active(/ps)card — the agent-runs and
|
|
2874
|
+
* bash-runs sections are the same loop (title line → per-entry rows →
|
|
2875
|
+
* stop button → trailing hr) and differ only in the title and the two
|
|
2876
|
+
* per-entry content rows, which the caller supplies via `renderRows`.
|
|
2877
|
+
*
|
|
2878
|
+
* Cross-section offset accounting (caller threads `remaining`/`skipped`
|
|
2879
|
+
* through the returned value): entries rendered decrement `remaining` and
|
|
2880
|
+
* consume the offset (`skipped` → 0, so the next section renders from its
|
|
2881
|
+
* own head); a section the offset/page has moved past deducts its full size
|
|
2882
|
+
* from `skipped` instead, so agent runs eat the offset before bash runs do.
|
|
2883
|
+
*/
|
|
2884
|
+
appendActiveCardSection(elements, runs, title, renderRows, remaining, skipped) {
|
|
2885
|
+
if (runs.length > 0 && skipped < runs.length && remaining > 0) {
|
|
2886
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: title } });
|
|
2887
|
+
const start = skipped;
|
|
2888
|
+
const end = Math.min(start + remaining, runs.length);
|
|
2889
|
+
for (let i = start; i < end; i++) {
|
|
2890
|
+
const run = runs[i];
|
|
2891
|
+
const statusLabel = run.terminal === 'running' ? '运行中' : '处理中';
|
|
2892
|
+
elements.push(...renderRows(run, statusLabel));
|
|
2893
|
+
elements.push(cardButton('⏹ 停止', { cmd: 'stop', runId: run.runId, cwd: run.cwd }, { type: 'danger', size: 'small' }));
|
|
2894
|
+
elements.push({ tag: 'hr' });
|
|
2895
|
+
}
|
|
2896
|
+
return { remaining: remaining - (end - start), skipped: 0 };
|
|
2897
|
+
}
|
|
2898
|
+
return { remaining, skipped: skipped - runs.length };
|
|
2899
|
+
}
|
|
2890
2900
|
async cmdConfig(args, ctx) {
|
|
2891
2901
|
// Ensure probe cache is populated before building the card.
|
|
2892
2902
|
await probeAllAgents();
|
|
@@ -2913,21 +2923,21 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2913
2923
|
// defaultAgent 不再漏判。
|
|
2914
2924
|
const switchResult = this.propagateConfigSave(oldDefaultAgent, { [mapAgentKey(key)]: value }, ctx);
|
|
2915
2925
|
this.pendingConfig = null; // 命令式写盘后清空暂存,避免不一致
|
|
2916
|
-
//
|
|
2926
|
+
// agent 切换时额外发送 Resume 卡片
|
|
2917
2927
|
if (switchResult.notice) {
|
|
2918
2928
|
const switchCard = this.buildConfigSwitchCard(switchResult, ctx);
|
|
2919
2929
|
// 文本路径返回 config card,Resume card 通过 bridge 异步发送
|
|
2920
2930
|
void this.bridge.sendResult(switchCard, ctx);
|
|
2921
2931
|
}
|
|
2922
2932
|
const card = this.buildConfigCard();
|
|
2923
|
-
if (discarded > 0
|
|
2933
|
+
if (discarded > 0) {
|
|
2934
|
+
// buildConfigCardFromTabs 恒返回 body.elements(至少含保存按钮),
|
|
2935
|
+
// 这里直接收窄插入提示条——同模块自己刚构建的值无需逐层探测。
|
|
2924
2936
|
const body = card.card.body;
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
});
|
|
2930
|
-
}
|
|
2937
|
+
body.elements.unshift({
|
|
2938
|
+
tag: 'div',
|
|
2939
|
+
text: { tag: 'lark_md', content: `⚠️ 已丢弃 ${discarded} 项未保存修改` },
|
|
2940
|
+
});
|
|
2931
2941
|
}
|
|
2932
2942
|
return card;
|
|
2933
2943
|
}
|