lark-remote 0.5.13 → 0.5.14
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 +15 -0
- package/dist/agent-kinds.d.ts +27 -0
- package/dist/agent-kinds.js +38 -0
- package/dist/bridge/approval-actions.d.ts +95 -0
- package/dist/bridge/approval-actions.js +104 -0
- package/dist/bridge/final-usage.d.ts +8 -17
- package/dist/bridge/final-usage.js +1 -18
- package/dist/bridge/index.d.ts +46 -39
- package/dist/bridge/index.js +201 -229
- package/dist/bridge/queue-manager.d.ts +25 -1
- package/dist/bridge/queue-manager.js +179 -160
- package/dist/bridge/run-approval.d.ts +2 -1
- package/dist/bridge/run-usage.d.ts +16 -30
- package/dist/bridge/run-usage.js +41 -50
- package/dist/bridge/session-writeback.d.ts +3 -2
- package/dist/card/approval-render.d.ts +3 -2
- package/dist/card/approval-render.js +3 -2
- package/dist/{router → card}/card-helpers.d.ts +7 -7
- package/dist/{router → card}/card-helpers.js +8 -8
- package/dist/card/card-session.d.ts +4 -2
- package/dist/card/card-session.js +8 -6
- package/dist/card/card-shared.d.ts +1 -1
- package/dist/card/card-shared.js +1 -1
- package/dist/card/final-answer-overflow.d.ts +0 -6
- package/dist/card/final-answer-overflow.js +1 -1
- package/dist/card/run-card-session.d.ts +2 -2
- package/dist/card/run-card-session.js +2 -2
- package/dist/card/run-renderer.js +7 -22
- package/dist/card/run-state.d.ts +28 -50
- package/dist/card/run-state.js +23 -0
- package/dist/card/usage-format.d.ts +30 -0
- package/dist/{router → card}/usage-format.js +3 -10
- package/dist/clone.js +6 -1
- package/dist/config/index.d.ts +16 -20
- package/dist/config/index.js +0 -16
- package/dist/config/pi-config.js +6 -4
- package/dist/connector/index.d.ts +2 -4
- package/dist/connector/index.js +7 -11
- package/dist/{runner/dsh → dsh}/auth.js +1 -1
- package/dist/dsh/protocol.d.ts +9 -0
- package/dist/dsh/protocol.js +61 -0
- package/dist/index.js +10 -5
- package/dist/model-catalog.d.ts +14 -0
- package/dist/model-catalog.js +25 -0
- package/dist/platform/path.d.ts +9 -0
- package/dist/platform/path.js +18 -0
- package/dist/router/active-run-usage.d.ts +4 -0
- package/dist/router/active-run-usage.js +12 -0
- package/dist/router/commands/ls.js +1 -1
- package/dist/router/commands/order.js +1 -1
- package/dist/router/commands/resume.d.ts +5 -4
- package/dist/router/commands/resume.js +4 -4
- package/dist/router/commands/ws.js +1 -1
- package/dist/router/config/claude.js +2 -1
- package/dist/router/config/common/render.d.ts +5 -1
- package/dist/router/config/common/render.js +6 -13
- package/dist/router/config/dsh.d.ts +1 -1
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/types.d.ts +11 -3
- package/dist/router/index.d.ts +41 -11
- package/dist/router/index.js +165 -226
- package/dist/runner/agent-choices-common.d.ts +1 -1
- package/dist/runner/claude/runner.d.ts +20 -2
- package/dist/runner/claude/runner.js +2 -15
- package/dist/runner/codex/app-server/runner.d.ts +3 -1
- package/dist/runner/codex/app-server/translator.d.ts +11 -0
- package/dist/runner/codex/app-server/translator.js +56 -149
- package/dist/runner/common/acp/base-acp-runner.d.ts +3 -1
- package/dist/runner/common/connection-based-runner.d.ts +3 -1
- package/dist/runner/common/jsonrpc/transport.js +17 -7
- package/dist/runner/dsh/client.d.ts +1 -4
- package/dist/runner/dsh/client.js +2 -55
- package/dist/runner/dsh/daemon.js +9 -2
- package/dist/runner/dsh/exit-cancel.js +1 -1
- package/dist/runner/dsh/mux-stream.js +21 -2
- package/dist/runner/dsh/runner.d.ts +21 -1
- package/dist/runner/dsh/runner.js +76 -73
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +22 -1
- package/dist/runner/kimi/acp/runner.js +83 -60
- package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
- package/dist/runner/opencode/acp/error-monitor.js +31 -2
- package/dist/runner/opencode/acp/runner.d.ts +3 -1
- package/dist/runner/pi/rpc/runner.d.ts +2 -1
- package/dist/runner/probe.d.ts +1 -1
- package/dist/runner/probe.js +10 -9
- package/dist/runner/registry.d.ts +2 -1
- package/dist/runner/types.d.ts +20 -154
- package/dist/runner/types.js +4 -24
- package/dist/session/claude/session-index.d.ts +11 -4
- package/dist/session/claude/session-index.js +30 -8
- package/dist/session/claude/session-reader.d.ts +1 -1
- package/dist/session/claude/sessions.d.ts +2 -2
- package/dist/session/claude/sessions.js +41 -26
- package/dist/session/codex/rollout-reader.d.ts +1 -1
- package/dist/session/codex/rollout-reader.js +14 -13
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +4 -2
- package/dist/session/common/jsonl.js +7 -1
- package/dist/session/common/pagination.d.ts +2 -0
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +6 -0
- package/dist/session/common/recency.js +7 -5
- package/dist/session/common/two-pass.d.ts +1 -1
- package/dist/session/common/usage-accumulator.d.ts +1 -1
- package/dist/session/dsh/sessions.d.ts +1 -1
- package/dist/session/dsh/sessions.js +37 -12
- package/dist/session/kimi/sessions.d.ts +1 -1
- package/dist/session/kimi/sessions.js +35 -8
- package/dist/session/opencode/sessions.d.ts +1 -1
- package/dist/session/opencode/sessions.js +51 -47
- package/dist/session/pi/sessions.d.ts +1 -1
- package/dist/session/pi/sessions.js +20 -7
- package/dist/session/registry.d.ts +2 -1
- package/dist/session/session-store.d.ts +1 -2
- package/dist/session/session-store.js +14 -7
- package/dist/session-contract.d.ts +178 -0
- package/dist/session-contract.js +9 -0
- package/package.json +1 -1
- package/dist/router/usage-format.d.ts +0 -60
- /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/home.js +0 -0
- /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
- /package/dist/{runner/dsh → dsh}/types.js +0 -0
package/dist/router/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
|
-
import { getConfigDir,
|
|
4
|
+
import { getConfigDir, setConfigValue, setConfigValues, mapAgentKey, getAgentConfig, } from '../config/index.js';
|
|
5
5
|
import { syncAgentChoices } from '../runner/index.js';
|
|
6
|
-
import { resolveDshCredentialsFile } from '../
|
|
6
|
+
import { resolveDshCredentialsFile } from '../dsh/auth.js';
|
|
7
7
|
import { WorkspaceStore } from '../workspace/index.js';
|
|
8
8
|
import { OrderStore, ORDER_NOT_FOUND_TEXT } from '../order/index.js';
|
|
9
9
|
import { resolveAlias } from '../order/alias-resolve.js';
|
|
10
10
|
import { diffConfig, setNestedValue } from './config-diff.js';
|
|
11
|
-
import { AGENT_KINDS } from '../
|
|
11
|
+
import { AGENT_KINDS, resolveAgentKind } from '../agent-kinds.js';
|
|
12
12
|
import { getLogger } from '../logger/index.js';
|
|
13
13
|
import { stripPlaceholders } from '../inbound/placeholder.js';
|
|
14
14
|
import { clampInt } from '../common/clamp.js';
|
|
15
|
-
import { activeRunUsage } from './usage
|
|
16
|
-
import { markdownDiv, buildSessionHistoryCard, paginationBar, parseJumpOffset, SEARCH_INPUT_NAME, PAGE_JUMP_INVALID_HINT, } from '
|
|
15
|
+
import { activeRunUsage } from './active-run-usage.js';
|
|
16
|
+
import { markdownDiv, buildSessionHistoryCard, paginationBar, parseJumpOffset, SEARCH_INPUT_NAME, PAGE_JUMP_INVALID_HINT, } from '../card/card-helpers.js';
|
|
17
17
|
import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
|
|
18
18
|
import { cardButton, newSessionButton, resumeCompactButton, agentDisplayName, } from '../card/card-shared.js';
|
|
19
19
|
import { displayName } from '../platform/path.js';
|
|
@@ -290,7 +290,6 @@ const IMMEDIATE_ACTION_CMDS = new Set([
|
|
|
290
290
|
'order.aliasRemove',
|
|
291
291
|
'order.textEdit',
|
|
292
292
|
'order.textInput',
|
|
293
|
-
'config.toggle',
|
|
294
293
|
'config.set',
|
|
295
294
|
'config.input',
|
|
296
295
|
'config.save',
|
|
@@ -349,13 +348,20 @@ const CARD_PAYLOAD_MISSING = '⚠️ 卡片 payload 缺少必要信息';
|
|
|
349
348
|
* 卡片只有最近一两张,5 张足够覆盖;无上限会让长跑进程的 Set 随卡片数增长。
|
|
350
349
|
*/
|
|
351
350
|
const MAX_CONFIG_CARDS = 5;
|
|
351
|
+
/** 本次改动是否落进 `agents.<kind>` 子树。 */
|
|
352
|
+
function touchesAgentsSubtree(keys, kind) {
|
|
353
|
+
return keys.some((k) => k === `agents.${kind}` || k.startsWith(`agents.${kind}.`));
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* 本次改动是否触及某个 agent 的配置——两种键形态都要认:现行 `agents.<kind>[.*]`
|
|
357
|
+
* 与历史的顶层 `<kind>[.*]`(claude 至今是顶层键)。判定曾抄在三处(agent 变更检测 /
|
|
358
|
+
* agentChoices 同步 / approval 热推),每加一个 agent 都得记得改三遍,与
|
|
359
|
+
* AGENT_KINDS 名单漂移是同一种事故,这里收成一处。
|
|
360
|
+
*/
|
|
361
|
+
function touchesAgentConfig(keys, kind) {
|
|
362
|
+
return (touchesAgentsSubtree(keys, kind) || keys.some((k) => k === kind || k.startsWith(`${kind}.`)));
|
|
363
|
+
}
|
|
352
364
|
export class CommandRouter {
|
|
353
|
-
/**
|
|
354
|
-
* Valid agent kinds — single source of truth for resume.use / resume.page /
|
|
355
|
-
* cmdResume. 名单本体在 `runner/types.ts` 的 AGENT_KINDS(与 AgentKind 类型
|
|
356
|
-
* 同源),这里只做别名,避免人手抄第二份。
|
|
357
|
-
*/
|
|
358
|
-
static VALID_AGENTS = AGENT_KINDS;
|
|
359
365
|
sessionStore;
|
|
360
366
|
/** 飞书桥接实例(public:测试直接访问断言,替代 as unknown as)。 */
|
|
361
367
|
bridge;
|
|
@@ -428,8 +434,8 @@ export class CommandRouter {
|
|
|
428
434
|
/**
|
|
429
435
|
* 串行化 config.* 卡片回调(2026-07-04)。
|
|
430
436
|
* CardKit 2.0 input/button 回调经 `enqueueImmediate` 分发,**不进串行队列**,
|
|
431
|
-
*
|
|
432
|
-
* `updateCardInPlace` → Feishu API
|
|
437
|
+
* 多次快速点击下拉/文本框会并发读写 `pendingConfig` 并同时调
|
|
438
|
+
* `updateCardInPlace` → Feishu API 乱序到达,控件卡死在某个状态。
|
|
433
439
|
* 此 Promise chain 把所有 config.* 动作串成一个执行链,保证后到的动作
|
|
434
440
|
* 等前一个动作的卡片 patch 完成后再读 `pendingConfig`。
|
|
435
441
|
*/
|
|
@@ -583,11 +589,7 @@ export class CommandRouter {
|
|
|
583
589
|
case 'resume.use': {
|
|
584
590
|
// resume.use carries an agent field from /resume [agent] list cards
|
|
585
591
|
// (both active and completed cards include the agent field).
|
|
586
|
-
const
|
|
587
|
-
const validAgents = CommandRouter.VALID_AGENTS;
|
|
588
|
-
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
589
|
-
? resumeAgent
|
|
590
|
-
: this.config.defaultAgent;
|
|
592
|
+
const resolvedAgent = resolveAgentKind(value.agent, this.config.defaultAgent);
|
|
591
593
|
const entry = this.sessionStore.get(ctx.userId);
|
|
592
594
|
const cwd = entry?.cwd;
|
|
593
595
|
if (!cwd) {
|
|
@@ -658,13 +660,12 @@ export class CommandRouter {
|
|
|
658
660
|
return await this.handleOrderTextEdit(value, ctx);
|
|
659
661
|
case 'order.textInput':
|
|
660
662
|
return await this.handleOrderTextInput(value, ctx);
|
|
661
|
-
case 'config.toggle':
|
|
662
663
|
case 'config.set':
|
|
663
664
|
case 'config.input':
|
|
664
665
|
case 'config.save':
|
|
665
666
|
// 2026-07-04: 串行化所有 config.* 动作。CardKit 2.0 回调经 enqueueImmediate
|
|
666
|
-
//
|
|
667
|
-
// 调 updateCardInPlace → Feishu API
|
|
667
|
+
// 不进串行队列,多次快速点击下拉/文本框会并发读写 pendingConfig 并同时
|
|
668
|
+
// 调 updateCardInPlace → Feishu API 乱序到达导致控件卡死。
|
|
668
669
|
// 2026-07-18: 返回 enqueueConfigAction 的结果以支持 toast 响应
|
|
669
670
|
return this.enqueueConfigAction(value, ctx);
|
|
670
671
|
case 'compact':
|
|
@@ -759,6 +760,20 @@ export class CommandRouter {
|
|
|
759
760
|
* Handle queue.immediate: stop current run, clear queue before this message, execute this message immediately.
|
|
760
761
|
* Instead of asking user to resend, we keep the target task in queue and let it execute.
|
|
761
762
|
*/
|
|
763
|
+
/**
|
|
764
|
+
* 把「执行编辑后的内容」注册成原队列槽位的一次性 replacement(复用入队时
|
|
765
|
+
* 捕获的 binding + 恢复 lane cwd,否则编辑后重跑的 run 会丢 cwd 与绑定)。
|
|
766
|
+
*
|
|
767
|
+
* 重新入队不行:`enqueue` 只能追加到 promise 链尾部,会把编辑任务甩到
|
|
768
|
+
* 后来者后面,违背「立即执行 = 下一个就跑」。三个调用点都必须在任何 await
|
|
769
|
+
* 之前注册——begin 路径消费 replacement 的时机不可预测(见
|
|
770
|
+
* handleQueueImmediate 步骤 1.5 的竞态说明)。
|
|
771
|
+
*/
|
|
772
|
+
registerEditedReplacement(cwd, messageId, content, binding, ctx) {
|
|
773
|
+
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
774
|
+
await this.handle(content, ctx, { cwdOverride: cwd, binding });
|
|
775
|
+
});
|
|
776
|
+
}
|
|
762
777
|
async handleQueueImmediate(value, ctx) {
|
|
763
778
|
const payload = await this.queuePayloadOrReply(value, ctx);
|
|
764
779
|
if (!payload)
|
|
@@ -808,15 +823,7 @@ export class CommandRouter {
|
|
|
808
823
|
// the queue position: enqueue can only append to the promise chain tail,
|
|
809
824
|
// which would put the edited task behind tasks queued after it.
|
|
810
825
|
if (targetTask.editedMessage) {
|
|
811
|
-
|
|
812
|
-
///Step3: 替换闭包复用原任务的 binding(不重新快照)+ 恢复丢失的
|
|
813
|
-
// cwdOverride(lane cwd 同源)。否则编辑后重新执行的 run 会丢 cwd 与绑定。
|
|
814
|
-
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
815
|
-
await this.handle(editedContent, ctx, {
|
|
816
|
-
cwdOverride: cwd,
|
|
817
|
-
binding: targetTask.binding,
|
|
818
|
-
});
|
|
819
|
-
});
|
|
826
|
+
this.registerEditedReplacement(cwd, messageId, targetTask.editedMessage, targetTask.binding, ctx);
|
|
820
827
|
}
|
|
821
828
|
// 2. Stop current running process
|
|
822
829
|
// (Note: queue executing count is reset inside Bridge.interruptCurrentRun
|
|
@@ -833,29 +840,7 @@ export class CommandRouter {
|
|
|
833
840
|
const stopUnconfirmed = interrupt.stop?.status === 'unconfirmed';
|
|
834
841
|
getLogger().info(`[router] queue.immediate: stopped=${stopped} unconfirmed=${stopUnconfirmed}`);
|
|
835
842
|
// 3. Remove all tasks BEFORE this one (by position in the current queue).
|
|
836
|
-
|
|
837
|
-
// advances concurrently): then it is already out of the snapshot and every
|
|
838
|
-
// remaining queued task is BEHIND it — clearing must stop, not run off the
|
|
839
|
-
// end and delete tasks queued after the target.
|
|
840
|
-
const tasks = this.bridge.getQueuedTasks(cwd);
|
|
841
|
-
const targetIdx = tasks.findIndex((t) => t.messageId === messageId);
|
|
842
|
-
let removedCount = 0;
|
|
843
|
-
// Phase 1: remove synchronously. The queue chain advances on any await
|
|
844
|
-
// (card PATCH round trips included), so every task before the target must
|
|
845
|
-
// be removed before yielding to the event loop — otherwise a task the user
|
|
846
|
-
// asked to clear can begin executing while its cancellation card is still
|
|
847
|
-
// in flight.
|
|
848
|
-
for (let i = 0; i < targetIdx; i++) {
|
|
849
|
-
const task = tasks[i];
|
|
850
|
-
// 撤销卡片由 removeFromQueue 统一触发(fire-and-forget),保持本循环
|
|
851
|
-
// 同步推进的既有不变式。
|
|
852
|
-
if (this.bridge.removeFromQueue(cwd, task.messageId)) {
|
|
853
|
-
removedCount++;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
if (targetIdx < 0) {
|
|
857
|
-
getLogger().debug(`[router] queue.immediate: target no longer queued (began/cancelled), not clearing tasks behind it`);
|
|
858
|
-
}
|
|
843
|
+
const removedCount = this.clearTasksAheadOfTarget(cwd, messageId);
|
|
859
844
|
// 4. DO NOT remove the target task - keep it in queue to execute immediately
|
|
860
845
|
// The queue will naturally execute it after the current task (which we just stopped)
|
|
861
846
|
// 4.4. Refresh the replacement with the latest edited content (idempotent:
|
|
@@ -864,66 +849,92 @@ export class CommandRouter {
|
|
|
864
849
|
// the task has begun, because getQueuedTask then returns undefined.
|
|
865
850
|
const latestTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
866
851
|
if (latestTask?.editedMessage) {
|
|
867
|
-
|
|
868
|
-
///Step3: 复用原任务 binding + 恢复 cwdOverride(同上)。
|
|
869
|
-
this.bridge.setTaskReplacement(cwd, messageId, async () => {
|
|
870
|
-
await this.handle(editedContent, ctx, {
|
|
871
|
-
cwdOverride: cwd,
|
|
872
|
-
binding: latestTask.binding,
|
|
873
|
-
});
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
// 4.5/6. The target must only be marked executing (and the toast may only
|
|
877
|
-
// promise immediate execution) when it is really the next task after the
|
|
878
|
-
// stop. While interruptCurrentRun was awaiting, a task ahead of the target
|
|
879
|
-
// may have begun and now occupy the cwd — the target is still queued
|
|
880
|
-
// but is NOT next, so its card must stay queued and the toast must say so.
|
|
881
|
-
const targetStillQueued = this.bridge.getQueuedTask(cwd, messageId) !== undefined;
|
|
882
|
-
if (!targetStillQueued) {
|
|
883
|
-
// The target began or was cancelled while earlier awaits were in flight —
|
|
884
|
-
// a success toast must never promise execution of a task that will not
|
|
885
|
-
// run. The two missing-reason states get distinct feedback: a target
|
|
886
|
-
// that began while the interrupt was in flight is already executing, so
|
|
887
|
-
// the toast must acknowledge that instead of telling the user nothing
|
|
888
|
-
// was scheduled; only a target that was actually cancelled (never began)
|
|
889
|
-
// gets the "未安排执行" wording.
|
|
890
|
-
if (this.bridge.hasTaskBegan(messageId)) {
|
|
891
|
-
await this.bridge.sendResult({ text: `ℹ️ 目标消息已开始执行,无需重复操作。已清除 ${removedCount} 条排队消息。` }, ctx);
|
|
892
|
-
}
|
|
893
|
-
else {
|
|
894
|
-
await this.bridge.sendResult({
|
|
895
|
-
text: `⚠️ 目标消息已不在队列中(可能已被撤销),未安排执行。已清除 ${removedCount} 条排队消息。`,
|
|
896
|
-
}, ctx);
|
|
897
|
-
}
|
|
898
|
-
return;
|
|
852
|
+
this.registerEditedReplacement(cwd, messageId, latestTask.editedMessage, latestTask.binding, ctx);
|
|
899
853
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
854
|
+
// 4.5/6. 按「目标此刻真的是下一个吗」给唯一一份用户可见反馈。
|
|
855
|
+
await this.reportImmediateOutcome({
|
|
856
|
+
cwd,
|
|
857
|
+
messageId,
|
|
858
|
+
ctx,
|
|
859
|
+
removedCount,
|
|
860
|
+
stopped,
|
|
861
|
+
stopUnconfirmed,
|
|
862
|
+
agentKind: interrupt.agentKind,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
finally {
|
|
866
|
+
releaseRunStart();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* 清掉排在目标之前的所有排队任务,返回实际清除条数。
|
|
871
|
+
*
|
|
872
|
+
* 两条红线:
|
|
873
|
+
* - 目标可能在 interruptCurrentRun 等待期间已经接跑(链并发前进),那时它已不在
|
|
874
|
+
* 快照里,剩下的全部排在它**后面**——清除必须在目标位置之前停下,不得越界删掉
|
|
875
|
+
* 用户没要求清除的任务。
|
|
876
|
+
* - 移除循环必须整体同步:队列链在任何 await 处都会推进(卡片 PATCH 也算),
|
|
877
|
+
* 一旦让出事件循环,用户要求清除的任务可能趁撤销卡还在路上就跑起来了。
|
|
878
|
+
* 撤销卡片由 `removeFromQueue` 统一触发(fire-and-forget)。
|
|
879
|
+
*/
|
|
880
|
+
clearTasksAheadOfTarget(cwd, messageId) {
|
|
881
|
+
const tasks = this.bridge.getQueuedTasks(cwd);
|
|
882
|
+
const targetIdx = tasks.findIndex((t) => t.messageId === messageId);
|
|
883
|
+
let removedCount = 0;
|
|
884
|
+
for (let i = 0; i < targetIdx; i++) {
|
|
885
|
+
if (this.bridge.removeFromQueue(cwd, tasks[i].messageId)) {
|
|
886
|
+
removedCount++;
|
|
907
887
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
888
|
+
}
|
|
889
|
+
if (targetIdx < 0) {
|
|
890
|
+
getLogger().debug(`[router] queue.immediate: target no longer queued (began/cancelled), not clearing tasks behind it`);
|
|
891
|
+
}
|
|
892
|
+
return removedCount;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* `queue.immediate` 的终态反馈(唯一出口)。
|
|
896
|
+
*
|
|
897
|
+
* 承诺必须与真实调度一致:目标只有确实是停止后的下一个,才可以把它的卡翻成
|
|
898
|
+
* 「执行中」并说「将立即执行」;在途接跑/被撤销/被抢先/远程停止未确认四种状态
|
|
899
|
+
* 各给各的文案(把没发生的事说成发生了,比沉默更糟——用户会据文案决定要不要重发)。
|
|
900
|
+
*/
|
|
901
|
+
async reportImmediateOutcome(opts) {
|
|
902
|
+
const { cwd, messageId, ctx, removedCount, stopped, stopUnconfirmed, agentKind } = opts;
|
|
903
|
+
const targetStillQueued = this.bridge.getQueuedTask(cwd, messageId) !== undefined;
|
|
904
|
+
if (!targetStillQueued) {
|
|
905
|
+
// 目标在途接跑:已经在执行,说「无需重复操作」;真的被撤销了才说「未安排执行」。
|
|
906
|
+
if (this.bridge.hasTaskBegan(messageId)) {
|
|
907
|
+
await this.bridge.sendResult({ text: `ℹ️ 目标消息已开始执行,无需重复操作。已清除 ${removedCount} 条排队消息。` }, ctx);
|
|
917
908
|
}
|
|
918
909
|
else {
|
|
919
910
|
await this.bridge.sendResult({
|
|
920
|
-
text: `⚠️
|
|
911
|
+
text: `⚠️ 目标消息已不在队列中(可能已被撤销),未安排执行。已清除 ${removedCount} 条排队消息。`,
|
|
921
912
|
}, ctx);
|
|
922
913
|
}
|
|
914
|
+
return;
|
|
923
915
|
}
|
|
924
|
-
|
|
925
|
-
|
|
916
|
+
if (stopUnconfirmed) {
|
|
917
|
+
const name = agentKind ? agentDisplayName(agentKind) : 'agent';
|
|
918
|
+
await this.bridge.sendResult({
|
|
919
|
+
text: `⚠️ 未能确认 ${name} 的 turn 已停止,它可能仍在后台运行。\n` +
|
|
920
|
+
`已清除 ${removedCount} 条排队消息;您的消息已提交,但可能要等该 turn 结束后才开始。`,
|
|
921
|
+
}, ctx);
|
|
922
|
+
return;
|
|
926
923
|
}
|
|
924
|
+
if (!this.bridge.isBusyFor(cwd)) {
|
|
925
|
+
// Normal path: nothing new began during the stop, the target is next.
|
|
926
|
+
// Mark its own card as executing now so buttons grey out immediately.
|
|
927
|
+
// Done BEFORE any removal so getQueuedTask still returns the task with
|
|
928
|
+
// its (possibly edited) messagePreview for the card. The queue callback's
|
|
929
|
+
// later updateQueueCardToExecuting call becomes a no-op (idempotent).
|
|
930
|
+
await this.bridge.markQueueCardExecuting(cwd, messageId);
|
|
931
|
+
const stopPrefix = stopped ? '⚡ 已停止当前任务,' : '';
|
|
932
|
+
await this.bridge.sendResult({ text: `${stopPrefix}清除了 ${removedCount} 条排队消息。您的消息将立即执行。` }, ctx);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
await this.bridge.sendResult({
|
|
936
|
+
text: `⚠️ 排在目标之前的任务已开始执行,目标消息保持排队。已清除 ${removedCount} 条排队消息。`,
|
|
937
|
+
}, ctx);
|
|
927
938
|
}
|
|
928
939
|
/**
|
|
929
940
|
* Handle queue.diagnose: show diagnostic info for why message is queuing.
|
|
@@ -1095,8 +1106,6 @@ export class CommandRouter {
|
|
|
1095
1106
|
// replacement if the task starts during the subsequent await. Both
|
|
1096
1107
|
// setTaskReplacement and the begin-path consumption are synchronous, so
|
|
1097
1108
|
// there is no interleaving window.
|
|
1098
|
-
//
|
|
1099
|
-
///Step3:复用原任务 binding(不重新快照)+ 恢复 cwdOverride。
|
|
1100
1109
|
const inputTask = this.bridge.getQueuedTask(cwd, messageId);
|
|
1101
1110
|
if (!inputTask) {
|
|
1102
1111
|
// Task already left the queue (began or was cancelled) before we could
|
|
@@ -1106,9 +1115,7 @@ export class CommandRouter {
|
|
|
1106
1115
|
// toast/card field).
|
|
1107
1116
|
return { toast: { type: 'info', content: '任务已不在队列中(可能已开始执行或被撤销)' } };
|
|
1108
1117
|
}
|
|
1109
|
-
this.
|
|
1110
|
-
await this.handle(newMessage, ctx, { cwdOverride: cwd, binding: inputTask.binding });
|
|
1111
|
-
});
|
|
1118
|
+
this.registerEditedReplacement(cwd, messageId, newMessage, inputTask.binding, ctx);
|
|
1112
1119
|
// Update the message preview (and editedMessage so handleQueueImmediate
|
|
1113
1120
|
// re-enqueues the edited content instead of the stale original closure),
|
|
1114
1121
|
// and build the updated queue card. Returning the card in the callback
|
|
@@ -1247,11 +1254,7 @@ export class CommandRouter {
|
|
|
1247
1254
|
* Mirrors handleLsPage — updates the same card, never sends a new one.
|
|
1248
1255
|
*/
|
|
1249
1256
|
async handleResumePage(value, ctx) {
|
|
1250
|
-
const
|
|
1251
|
-
const validAgents = CommandRouter.VALID_AGENTS;
|
|
1252
|
-
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
1253
|
-
? resumeAgent
|
|
1254
|
-
: this.config.defaultAgent;
|
|
1257
|
+
const resolvedAgent = resolveAgentKind(value.agent, this.config.defaultAgent);
|
|
1255
1258
|
const { pageSize, alignedOffset, error } = normalizePageArgs(value, RESUME_PAGE_SIZE);
|
|
1256
1259
|
if (error) {
|
|
1257
1260
|
return { toast: { type: 'error', content: error } };
|
|
@@ -1381,8 +1384,6 @@ export class CommandRouter {
|
|
|
1381
1384
|
* Returns null when the order no longer exists; index.ts surfaces the error.
|
|
1382
1385
|
*/
|
|
1383
1386
|
resolveOrderExecForQueue(orderId) {
|
|
1384
|
-
if (!this.orderStore.has(orderId))
|
|
1385
|
-
return null;
|
|
1386
1387
|
const order = this.orderStore.get().find((o) => o.id === orderId);
|
|
1387
1388
|
if (!order)
|
|
1388
1389
|
return null;
|
|
@@ -1812,6 +1813,26 @@ export class CommandRouter {
|
|
|
1812
1813
|
this.configCardIds.delete(oldest);
|
|
1813
1814
|
}
|
|
1814
1815
|
}
|
|
1816
|
+
/**
|
|
1817
|
+
* 把一次字段变更交给当前 agent 的 config builder(处理 provider→model 这类
|
|
1818
|
+
* 依赖关系),逐条写进草稿,再把草稿刷到所有配置卡。
|
|
1819
|
+
* `config.set`(下拉)与 `config.input`(输入框提交)共用同一套应用逻辑。
|
|
1820
|
+
*/
|
|
1821
|
+
async applyConfigFieldChange(key, newValue, ctx) {
|
|
1822
|
+
try {
|
|
1823
|
+
this.ensurePendingConfig();
|
|
1824
|
+
const defaultAgent = this.pendingConfig.defaultAgent ?? 'claude';
|
|
1825
|
+
const configBuilder = getConfigBuilder(defaultAgent);
|
|
1826
|
+
const patches = configBuilder.handleFieldChange(key, newValue, this.pendingConfig);
|
|
1827
|
+
for (const patch of patches) {
|
|
1828
|
+
this.setNestedValue(this.pendingConfig, patch.key, patch.value);
|
|
1829
|
+
}
|
|
1830
|
+
await this.refreshConfigCards(ctx);
|
|
1831
|
+
}
|
|
1832
|
+
catch (err) {
|
|
1833
|
+
await this.bridge.sendResult({ text: `设置失败: ${err.message}` }, ctx);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1815
1836
|
/**
|
|
1816
1837
|
* 把当前草稿刷到所有已知 config 卡片上(含本次点击的那张)。
|
|
1817
1838
|
*
|
|
@@ -1835,12 +1856,12 @@ export class CommandRouter {
|
|
|
1835
1856
|
}
|
|
1836
1857
|
}
|
|
1837
1858
|
/**
|
|
1838
|
-
* 串行化 config.* 卡片回调(2026-07-04
|
|
1859
|
+
* 串行化 config.* 卡片回调(2026-07-04 修复连击卡死 bug)。
|
|
1839
1860
|
*
|
|
1840
1861
|
* CardKit 2.0 input/button 回调经 `enqueueImmediate` 分发,不进 bridge 的
|
|
1841
|
-
*
|
|
1862
|
+
* 串行队列;多次快速点击下拉/文本框会并发读写 `pendingConfig` 并同时调
|
|
1842
1863
|
* `updateCardInPlace` → 两个 patch 请求乱序到达飞书,后到达的 patch(点击 1
|
|
1843
|
-
* 的旧卡片)覆盖先到达的 patch(点击 2
|
|
1864
|
+
* 的旧卡片)覆盖先到达的 patch(点击 2 的新卡片),用户看到控件卡死。
|
|
1844
1865
|
*
|
|
1845
1866
|
* 此方法把所有 config.* 动作串成一个 Promise chain:每个动作等前一个动作
|
|
1846
1867
|
* 的 `updateCardInPlace` 完全 settle 后再读 `pendingConfig`。
|
|
@@ -1863,28 +1884,6 @@ export class CommandRouter {
|
|
|
1863
1884
|
/** config.* 卡片动作分发(public:测试直接调用,替代 as unknown as)。 */
|
|
1864
1885
|
async dispatchConfigAction(value, ctx) {
|
|
1865
1886
|
switch (value.cmd) {
|
|
1866
|
-
case 'config.toggle': {
|
|
1867
|
-
// 通用 boolean 翻转:目前无卡片字段使用(output.show* 已移除),
|
|
1868
|
-
// 保留供未来 boolean 配置项复用;configActionQueue 串行化覆盖连击。
|
|
1869
|
-
const key = value.key;
|
|
1870
|
-
if (!key) {
|
|
1871
|
-
await this.bridge.sendResult({ text: '缺少配置项 key' }, ctx);
|
|
1872
|
-
return;
|
|
1873
|
-
}
|
|
1874
|
-
this.ensurePendingConfig();
|
|
1875
|
-
const current = getConfigValue(this.pendingConfig, key);
|
|
1876
|
-
// 布尔字段未设置时视为 false,而非报错(如布尔配置项尚未写入 config)
|
|
1877
|
-
const boolVal = typeof current === 'boolean' ? current : false;
|
|
1878
|
-
try {
|
|
1879
|
-
// 只改暂存区,不写盘;原地更新卡片而非发送新卡(2026-07-04)
|
|
1880
|
-
this.setNestedValue(this.pendingConfig, key, !boolVal);
|
|
1881
|
-
await this.refreshConfigCards(ctx);
|
|
1882
|
-
}
|
|
1883
|
-
catch (err) {
|
|
1884
|
-
await this.bridge.sendResult({ text: `设置失败: ${err.message}` }, ctx);
|
|
1885
|
-
}
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
1887
|
case 'config.set': {
|
|
1889
1888
|
// Set config value from select dropdown (option from CardKit 2.0)
|
|
1890
1889
|
const key = value.key;
|
|
@@ -1893,20 +1892,7 @@ export class CommandRouter {
|
|
|
1893
1892
|
await this.bridge.sendResult({ text: '缺少参数' }, ctx);
|
|
1894
1893
|
return;
|
|
1895
1894
|
}
|
|
1896
|
-
|
|
1897
|
-
this.ensurePendingConfig();
|
|
1898
|
-
// 使用 agent config builder 处理字段变更(处理依赖关系如 provider→model)
|
|
1899
|
-
const defaultAgent = this.pendingConfig.defaultAgent ?? 'claude';
|
|
1900
|
-
const configBuilder = getConfigBuilder(defaultAgent);
|
|
1901
|
-
const patches = configBuilder.handleFieldChange(key, newValue, this.pendingConfig);
|
|
1902
|
-
for (const patch of patches) {
|
|
1903
|
-
this.setNestedValue(this.pendingConfig, patch.key, patch.value);
|
|
1904
|
-
}
|
|
1905
|
-
await this.refreshConfigCards(ctx);
|
|
1906
|
-
}
|
|
1907
|
-
catch (err) {
|
|
1908
|
-
await this.bridge.sendResult({ text: `设置失败: ${err.message}` }, ctx);
|
|
1909
|
-
}
|
|
1895
|
+
await this.applyConfigFieldChange(key, newValue, ctx);
|
|
1910
1896
|
return;
|
|
1911
1897
|
}
|
|
1912
1898
|
case 'config.input': {
|
|
@@ -1924,20 +1910,7 @@ export class CommandRouter {
|
|
|
1924
1910
|
await this.bridge.sendResult({ text: '缺少输入值' }, ctx);
|
|
1925
1911
|
return;
|
|
1926
1912
|
}
|
|
1927
|
-
|
|
1928
|
-
this.ensurePendingConfig();
|
|
1929
|
-
// 使用 agent config builder 处理字段变更
|
|
1930
|
-
const defaultAgent = this.pendingConfig.defaultAgent ?? 'claude';
|
|
1931
|
-
const configBuilder = getConfigBuilder(defaultAgent);
|
|
1932
|
-
const patches = configBuilder.handleFieldChange(key, newValue, this.pendingConfig);
|
|
1933
|
-
for (const patch of patches) {
|
|
1934
|
-
this.setNestedValue(this.pendingConfig, patch.key, patch.value);
|
|
1935
|
-
}
|
|
1936
|
-
await this.refreshConfigCards(ctx);
|
|
1937
|
-
}
|
|
1938
|
-
catch (err) {
|
|
1939
|
-
await this.bridge.sendResult({ text: `设置失败: ${err.message}` }, ctx);
|
|
1940
|
-
}
|
|
1913
|
+
await this.applyConfigFieldChange(key, newValue, ctx);
|
|
1941
1914
|
return;
|
|
1942
1915
|
}
|
|
1943
1916
|
case 'config.save': {
|
|
@@ -2023,18 +1996,9 @@ export class CommandRouter {
|
|
|
2023
1996
|
this.bridge.setIdleTimeout(this.idleTimeoutMs);
|
|
2024
1997
|
// 2026-07-13: 检测是否有任何 agent 配置变更或 defaultAgent 切换
|
|
2025
1998
|
const hasDefaultAgentChange = oldDefaultAgent !== this.config.defaultAgent;
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
k.startsWith('codex.') ||
|
|
2030
|
-
k.startsWith('opencode.') ||
|
|
2031
|
-
k.startsWith('kimi.') ||
|
|
2032
|
-
k === 'defaultAgent' ||
|
|
2033
|
-
k === 'claude' ||
|
|
2034
|
-
k === 'pi' ||
|
|
2035
|
-
k === 'codex' ||
|
|
2036
|
-
k === 'opencode' ||
|
|
2037
|
-
k === 'kimi');
|
|
1999
|
+
const changedKeys = Object.keys(updates);
|
|
2000
|
+
const hasAgentConfigChange = changedKeys.includes('defaultAgent') ||
|
|
2001
|
+
AGENT_KINDS.some((kind) => touchesAgentConfig(changedKeys, kind));
|
|
2038
2002
|
// 当 defaultAgent 改变时,清除旧 agent 的 sessionId,
|
|
2039
2003
|
// 同时清除新 agent 的 sessionId(等效于 /new 命令),
|
|
2040
2004
|
// 并支持 session 恢复 - 切换回来时如果条件满足则恢复之前的 session。
|
|
@@ -2083,43 +2047,30 @@ export class CommandRouter {
|
|
|
2083
2047
|
switchSessionId = '';
|
|
2084
2048
|
}
|
|
2085
2049
|
}
|
|
2086
|
-
// DSH preset
|
|
2087
|
-
//
|
|
2088
|
-
// 旧 sessionId
|
|
2089
|
-
//
|
|
2090
|
-
|
|
2091
|
-
//
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
if (oldSessionId) {
|
|
2096
|
-
this.sessionStore.setPreviousSessionId(ctx.userId, 'dsh', oldSessionId);
|
|
2097
|
-
this.sessionStore.clearSessionId(ctx.userId, 'dsh');
|
|
2098
|
-
switchNotice = `DSH preset 已变更,旧会话已停放到恢复槽(可 /resume 校验后恢复),下次消息将新建会话`;
|
|
2099
|
-
}
|
|
2100
|
-
else {
|
|
2101
|
-
switchNotice = `DSH preset 已变更,下次消息将新建会话`;
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
// DSH host 变更 = 换服务:旧 host 的 sessionId 发往新 host 是错的。
|
|
2105
|
-
// 视为 session 边界,仿 preset 分支停车 + 清空当前 sessionId,下次 run 新建会话。
|
|
2106
|
-
const hasDshHostChange = Object.keys(updates).some((k) => k === 'agents.dsh.host');
|
|
2107
|
-
if (hasDshHostChange && this.config.defaultAgent === 'dsh' && !switchNotice) {
|
|
2050
|
+
// DSH 的两个「会话边界」变更共用一套处理:preset 在 session 创建时固定,
|
|
2051
|
+
// 中途切换会被服务端拒绝(agent-preset-conflict);host 变更等于换服务,
|
|
2052
|
+
// 旧 host 的 sessionId 发往新 host 是错的。两者都把旧 sessionId 停到
|
|
2053
|
+
// previousSessions 恢复槽(可 /resume 校验后恢复)并清空当前值,下次 run
|
|
2054
|
+
// 走 session.create 新建。已发生 agent 切换时(switchNotice 已设置)跳过,
|
|
2055
|
+
// 避免覆盖切换 notice——切换分支对 dsh 的 session 已做恢复/清空处理。
|
|
2056
|
+
const parkDshSessionBoundary = (changed, label) => {
|
|
2057
|
+
if (!changed || this.config.defaultAgent !== 'dsh' || switchNotice)
|
|
2058
|
+
return;
|
|
2108
2059
|
const oldSessionId = this.sessionStore.getSessionId(ctx.userId, 'dsh');
|
|
2109
2060
|
if (oldSessionId) {
|
|
2110
2061
|
this.sessionStore.setPreviousSessionId(ctx.userId, 'dsh', oldSessionId);
|
|
2111
2062
|
this.sessionStore.clearSessionId(ctx.userId, 'dsh');
|
|
2112
|
-
switchNotice = `DSH
|
|
2063
|
+
switchNotice = `DSH ${label} 已变更,旧会话已停放到恢复槽(可 /resume 校验后恢复),下次消息将新建会话`;
|
|
2113
2064
|
}
|
|
2114
2065
|
else {
|
|
2115
|
-
switchNotice = `DSH
|
|
2066
|
+
switchNotice = `DSH ${label} 已变更,下次消息将新建会话`;
|
|
2116
2067
|
}
|
|
2117
|
-
}
|
|
2068
|
+
};
|
|
2069
|
+
parkDshSessionBoundary(changedKeys.includes('agents.dsh.agentPreset'), 'preset');
|
|
2070
|
+
parkDshSessionBoundary(changedKeys.includes('agents.dsh.host'), 'host');
|
|
2118
2071
|
// 2026-07-13: 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
|
|
2119
2072
|
const currentAgent = this.config.defaultAgent;
|
|
2120
|
-
|
|
2121
|
-
const hasCurrentAgentUpdate = Object.keys(updates).some((k) => k === currentAgentConfigKey || k.startsWith(`${currentAgentConfigKey}.`));
|
|
2122
|
-
if (hasCurrentAgentUpdate) {
|
|
2073
|
+
if (touchesAgentsSubtree(changedKeys, currentAgent)) {
|
|
2123
2074
|
this.config = syncAgentChoices(this.config, currentAgent);
|
|
2124
2075
|
// agentChoices 同步写盘必须走原子写(tmp+rename),禁裸同步写盘
|
|
2125
2076
|
atomicWrite(this.configPath, YAML.stringify(this.config));
|
|
@@ -2131,19 +2082,8 @@ export class CommandRouter {
|
|
|
2131
2082
|
// duck method `updateApprovalMode` (codex thread/settings/update,
|
|
2132
2083
|
// kimi/opencode session/set_mode) so they take effect for subsequent
|
|
2133
2084
|
// turns without waiting for the runner to be recreated.
|
|
2134
|
-
const hasApprovalConfigChange =
|
|
2135
|
-
|
|
2136
|
-
k === 'codex' ||
|
|
2137
|
-
k.startsWith('codex.') ||
|
|
2138
|
-
k === 'agents.kimi' ||
|
|
2139
|
-
k.startsWith('agents.kimi.') ||
|
|
2140
|
-
k === 'kimi' ||
|
|
2141
|
-
k.startsWith('kimi.') ||
|
|
2142
|
-
k === 'agents.opencode' ||
|
|
2143
|
-
k.startsWith('agents.opencode.') ||
|
|
2144
|
-
k === 'opencode' ||
|
|
2145
|
-
k.startsWith('opencode.') ||
|
|
2146
|
-
k === 'agents.dsh.permissionPreset');
|
|
2085
|
+
const hasApprovalConfigChange = ['codex', 'kimi', 'opencode'].some((kind) => touchesAgentConfig(changedKeys, kind)) ||
|
|
2086
|
+
changedKeys.includes('agents.dsh.permissionPreset');
|
|
2147
2087
|
if (hasApprovalConfigChange) {
|
|
2148
2088
|
this.bridge.syncActiveApprovalModes();
|
|
2149
2089
|
}
|
|
@@ -2356,14 +2296,14 @@ export class CommandRouter {
|
|
|
2356
2296
|
cmdStatus(ctx) {
|
|
2357
2297
|
const entry = this.sessionStore.get(ctx.userId);
|
|
2358
2298
|
const cwd = entry?.cwd ?? '(未设置)';
|
|
2359
|
-
const sessionId = entry?.sessions
|
|
2299
|
+
const sessionId = entry?.sessions.get(this.config.defaultAgent) ?? '(无)';
|
|
2360
2300
|
// 查 bridge.activeRuns 而不是默认 runner.isRunning —— 因为 per-workspace
|
|
2361
2301
|
// runner 启动的 run 不会反映到默认 runner 状态。
|
|
2362
2302
|
const running = cwd !== '(未设置)' && this.bridge.isBusyFor(cwd);
|
|
2363
2303
|
const logger = getLogger();
|
|
2364
2304
|
const pid = process.pid;
|
|
2365
2305
|
const configDir = getConfigDir();
|
|
2366
|
-
const logFile =
|
|
2306
|
+
const logFile = logger.getCurrentLogFile();
|
|
2367
2307
|
// 构建 agent 状态信息
|
|
2368
2308
|
const agentLines = [];
|
|
2369
2309
|
if (cwd !== '(未设置)') {
|
|
@@ -2841,7 +2781,6 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2841
2781
|
cmdResume(args, ctx, offset = 0) {
|
|
2842
2782
|
return buildResumeCommand(args, ctx, offset, {
|
|
2843
2783
|
defaultAgent: this.config.defaultAgent,
|
|
2844
|
-
validAgents: CommandRouter.VALID_AGENTS,
|
|
2845
2784
|
getSessionEntry: (userId) => this.sessionStore.get(userId),
|
|
2846
2785
|
setSessionId: (userId, agent, sessionId, cwd) => this.sessionStore.setSessionId(userId, agent, sessionId, cwd),
|
|
2847
2786
|
defaultReader: () => this.sessionReader,
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Claude Runner 实现
|
|
3
|
+
*
|
|
4
|
+
* ClaudeRunner 从「一次一跑」(claude -p <msg> --permission-mode
|
|
5
|
+
* bypassPermissions)升级为长驻交互会话(--input-format stream-json 双向
|
|
6
|
+
* 通道),同时复用本项目已有的 ApprovalCoordinator + run 卡审批区。
|
|
7
|
+
*
|
|
8
|
+
* 架构:ClaudeRunner 是 workspace-lifetime 的薄包装,进程与协议细节在
|
|
9
|
+
* ClaudeSession(src/runner/claude/session.ts,extends SpawningRunner 复用
|
|
10
|
+
* pid 文件/killOrphan/Terminator/心跳/退出分发器机制)。每次 run() 委托
|
|
11
|
+
* session:确保进程(可 --resume)→ 写 user 消息 → 消费事件直到本 turn result。
|
|
12
|
+
*/
|
|
13
|
+
import type { AgentSessionReader } from '../../session-contract.js';
|
|
14
|
+
import { type AgentEvent, type SpawnOptions, type AgentRunner, type AgentStatusInfo } from '../types.js';
|
|
2
15
|
/**
|
|
3
16
|
* Claude Agent Runner(长驻交互模式)。
|
|
4
17
|
*
|
|
@@ -23,7 +36,12 @@ export declare class ClaudeRunner implements AgentRunner {
|
|
|
23
36
|
pidDir?: string;
|
|
24
37
|
cwd: string;
|
|
25
38
|
spawnHeartbeatMs?: number;
|
|
26
|
-
|
|
39
|
+
/**
|
|
40
|
+
* 由组合根(`src/index.ts`)注入,与 sessionReaderRegistry 里的**同一个**实例。
|
|
41
|
+
* claude 不在此处自己 new:那会让 runner 反向依赖 session 层(runner ↔ session
|
|
42
|
+
* 循环的一半);其余 agent 从一开始就是注入的。
|
|
43
|
+
*/
|
|
44
|
+
sessionReader: AgentSessionReader;
|
|
27
45
|
/** Claude 权限模式(官方 --permission-mode 枚举;'default'=省略参数)。 */
|
|
28
46
|
permissionMode?: string;
|
|
29
47
|
/** 会话级空闲回收 TTL(ms);0=禁用。 */
|