lark-remote 0.1.6 → 0.1.8
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 +51 -1
- package/README.en.md +9 -1
- package/README.md +14 -2
- package/dist/bridge/approval-coordinator.d.ts +71 -34
- package/dist/bridge/approval-coordinator.js +225 -41
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +97 -13
- package/dist/bridge/index.js +227 -87
- package/dist/bridge/queue-manager.d.ts +2 -1
- package/dist/bridge/queue-manager.js +1 -0
- package/dist/card/approval-render.js +294 -36
- package/dist/card/bash-card-session.d.ts +2 -24
- package/dist/card/bash-card-session.js +6 -73
- package/dist/card/bash-renderer.js +2 -3
- package/dist/card/card-budget.js +29 -75
- package/dist/card/card-session.d.ts +48 -0
- package/dist/card/card-session.js +86 -0
- package/dist/card/card-shared.d.ts +7 -16
- package/dist/card/card-shared.js +7 -30
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.d.ts +7 -0
- package/dist/card/run-renderer.js +116 -238
- package/dist/card/run-state.d.ts +13 -0
- package/dist/card/run-state.js +25 -16
- package/dist/card/text-truncate.d.ts +13 -12
- package/dist/card/text-truncate.js +13 -63
- package/dist/common/fs.d.ts +8 -0
- package/dist/common/fs.js +17 -0
- package/dist/common/guards.d.ts +0 -20
- package/dist/common/guards.js +0 -43
- package/dist/common/sleep.d.ts +2 -0
- package/dist/common/sleep.js +5 -0
- package/dist/common/truncate.d.ts +39 -0
- package/dist/common/truncate.js +92 -0
- package/dist/config/codex-config.d.ts +1 -1
- package/dist/config/codex-config.js +1 -1
- package/dist/config/index.d.ts +112 -24
- package/dist/config/index.js +139 -37
- package/dist/connector/index.d.ts +69 -2
- package/dist/connector/index.js +107 -1
- package/dist/index.js +150 -118
- package/dist/instance-lock.js +4 -12
- package/dist/order/alias-resolve.d.ts +13 -0
- package/dist/order/alias-resolve.js +22 -0
- package/dist/order/alias-store.d.ts +38 -0
- package/dist/order/alias-store.js +106 -0
- package/dist/persistence/atomic-write.d.ts +8 -2
- package/dist/persistence/atomic-write.js +23 -7
- package/dist/restart.js +2 -1
- package/dist/router/card-action-payload.d.ts +30 -0
- package/dist/router/card-action-payload.js +39 -0
- package/dist/router/card-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +30 -1
- package/dist/router/config/codex.js +29 -50
- package/dist/router/config/common/model-patch.d.ts +22 -0
- package/dist/router/config/common/model-patch.js +26 -0
- package/dist/router/config/common/render.js +5 -4
- package/dist/router/config/dsh.d.ts +51 -0
- package/dist/router/config/dsh.js +179 -0
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +28 -1
- package/dist/router/config/kimi.d.ts +4 -1
- package/dist/router/config/kimi.js +27 -0
- package/dist/router/config/opencode.js +18 -5
- package/dist/router/config/pi.js +4 -5
- package/dist/router/config/types.d.ts +17 -2
- package/dist/router/index.d.ts +76 -22
- package/dist/router/index.js +479 -548
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +16 -2
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +34 -0
- package/dist/runner/claude/runner.d.ts +47 -10
- package/dist/runner/claude/runner.js +117 -46
- package/dist/runner/claude/session.d.ts +166 -0
- package/dist/runner/claude/session.js +768 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
- package/dist/runner/codex/app-server/protocol-types.js +2 -13
- package/dist/runner/codex/app-server/runner.d.ts +52 -59
- package/dist/runner/codex/app-server/runner.js +216 -338
- package/dist/runner/codex/app-server/translator.d.ts +8 -21
- package/dist/runner/codex/app-server/translator.js +45 -18
- package/dist/runner/codex/index.d.ts +0 -2
- package/dist/runner/codex/index.js +0 -2
- package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
- package/dist/runner/common/acp/protocol-helpers.js +21 -0
- package/dist/runner/common/acp/protocol-types.d.ts +271 -0
- package/dist/runner/common/acp/protocol-types.js +77 -0
- package/dist/runner/common/connection-based-runner.d.ts +111 -0
- package/dist/runner/common/connection-based-runner.js +261 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
- package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
- package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
- package/dist/runner/common/jsonrpc/types.d.ts +32 -0
- package/dist/runner/common/jsonrpc/types.js +9 -0
- package/dist/runner/common/process-stopper.d.ts +2 -1
- package/dist/runner/common/process-stopper.js +1 -0
- package/dist/runner/common/runner-utils.d.ts +6 -13
- package/dist/runner/common/runner-utils.js +6 -47
- package/dist/runner/common/spawning-runner.d.ts +24 -70
- package/dist/runner/common/spawning-runner.js +98 -152
- package/dist/runner/dsh/client.d.ts +95 -0
- package/dist/runner/dsh/client.js +418 -0
- package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
- package/dist/runner/dsh/fake-dsh-server.js +156 -0
- package/dist/runner/dsh/index.d.ts +3 -0
- package/dist/runner/dsh/index.js +4 -0
- package/dist/runner/dsh/runner.d.ts +57 -0
- package/dist/runner/dsh/runner.js +191 -0
- package/dist/runner/dsh/translator.d.ts +28 -0
- package/dist/runner/dsh/translator.js +98 -0
- package/dist/runner/dsh/types.d.ts +155 -0
- package/dist/runner/dsh/types.js +14 -0
- package/dist/runner/index.d.ts +5 -4
- package/dist/runner/index.js +5 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +516 -0
- package/dist/runner/kimi/acp/translator.d.ts +138 -0
- package/dist/runner/kimi/acp/translator.js +441 -0
- package/dist/runner/kimi/index.d.ts +1 -1
- package/dist/runner/kimi/index.js +1 -1
- package/dist/runner/opencode/acp/index.d.ts +1 -0
- package/dist/runner/opencode/acp/index.js +2 -0
- package/dist/runner/opencode/acp/runner.d.ts +121 -0
- package/dist/runner/opencode/acp/runner.js +429 -0
- package/dist/runner/opencode/acp/translator.d.ts +119 -0
- package/dist/runner/opencode/acp/translator.js +345 -0
- package/dist/runner/opencode/index.d.ts +1 -1
- package/dist/runner/opencode/index.js +1 -1
- package/dist/runner/pi/index.d.ts +1 -1
- package/dist/runner/pi/index.js +1 -1
- package/dist/runner/pi/rpc/client.d.ts +52 -0
- package/dist/runner/pi/rpc/client.js +114 -0
- package/dist/runner/pi/rpc/index.d.ts +4 -0
- package/dist/runner/pi/rpc/index.js +4 -0
- package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
- package/dist/runner/pi/rpc/protocol-types.js +21 -0
- package/dist/runner/pi/rpc/runner.d.ts +74 -0
- package/dist/runner/pi/rpc/runner.js +190 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -0
- package/dist/runner/probe.js +6 -1
- package/dist/runner/question-common.d.ts +26 -0
- package/dist/runner/question-common.js +42 -0
- package/dist/runner/registry.d.ts +7 -8
- package/dist/runner/registry.js +3 -4
- package/dist/runner/resolve-agent-choices.js +18 -59
- package/dist/runner/sync-agent-choices.js +16 -67
- package/dist/runner/types.d.ts +97 -14
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +43 -28
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +14 -17
- package/dist/session/codex/sessions.d.ts +1 -1
- package/dist/session/codex/sessions.js +2 -2
- package/dist/session/common/content-blocks.d.ts +7 -0
- package/dist/session/common/content-blocks.js +13 -9
- package/dist/session/common/jsonl.d.ts +8 -25
- package/dist/session/common/jsonl.js +126 -261
- package/dist/session/common/pagination.d.ts +28 -0
- package/dist/session/common/pagination.js +33 -0
- package/dist/session/dsh/index.d.ts +1 -0
- package/dist/session/dsh/index.js +2 -0
- package/dist/session/dsh/sessions.d.ts +39 -0
- package/dist/session/dsh/sessions.js +237 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/kimi/sessions.d.ts +34 -1
- package/dist/session/kimi/sessions.js +208 -98
- package/dist/session/opencode/sessions.d.ts +17 -3
- package/dist/session/opencode/sessions.js +29 -29
- package/dist/session/pi/sessions.d.ts +5 -1
- package/dist/session/pi/sessions.js +12 -16
- package/dist/session/session-store.d.ts +11 -1
- package/dist/session/session-store.js +75 -178
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/install.d.ts +20 -2
- package/dist/update/install.js +51 -7
- package/dist/workspace/index.d.ts +4 -0
- package/dist/workspace/index.js +8 -1
- package/package.json +2 -1
- package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- package/dist/runner/codex/factory.d.ts +0 -29
- package/dist/runner/codex/factory.js +0 -39
- package/dist/runner/codex/jsonl.d.ts +0 -37
- package/dist/runner/codex/jsonl.js +0 -221
- package/dist/runner/codex/runner.d.ts +0 -66
- package/dist/runner/codex/runner.js +0 -129
- package/dist/runner/common/exec-translator.d.ts +0 -73
- package/dist/runner/common/exec-translator.js +0 -105
- package/dist/runner/kimi/jsonl.d.ts +0 -28
- package/dist/runner/kimi/jsonl.js +0 -143
- package/dist/runner/kimi/runner.d.ts +0 -49
- package/dist/runner/kimi/runner.js +0 -105
- package/dist/runner/opencode/argv.d.ts +0 -32
- package/dist/runner/opencode/argv.js +0 -35
- package/dist/runner/opencode/jsonl.d.ts +0 -68
- package/dist/runner/opencode/jsonl.js +0 -295
- package/dist/runner/opencode/runner.d.ts +0 -61
- package/dist/runner/opencode/runner.js +0 -93
- package/dist/runner/pi/jsonl.d.ts +0 -128
- package/dist/runner/pi/jsonl.js +0 -249
- package/dist/runner/pi/runner.d.ts +0 -36
- package/dist/runner/pi/runner.js +0 -76
- package/dist/session/common/catch-up.d.ts +0 -12
- package/dist/session/common/catch-up.js +0 -31
package/dist/router/index.js
CHANGED
|
@@ -2,18 +2,20 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
|
-
import { getConfigDir, getConfigValue, setConfigValue, setConfigValues,
|
|
5
|
+
import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgentKey, assertSafeKeyPart, walkNestedContainer, getAgentConfig, } from '../config/index.js';
|
|
6
6
|
import { syncAgentChoices } from '../runner/index.js';
|
|
7
7
|
import { WorkspaceStore } from '../workspace/index.js';
|
|
8
8
|
import { OrderStore } from '../order/index.js';
|
|
9
|
+
import { AliasStore } from '../order/alias-store.js';
|
|
10
|
+
import { resolveAlias } from '../order/alias-resolve.js';
|
|
9
11
|
import { getLogger } from '../logger/index.js';
|
|
10
|
-
import { activeRunUsage,
|
|
11
|
-
import { markdownDiv,
|
|
12
|
+
import { activeRunUsage, clampInt } from './utils.js';
|
|
13
|
+
import { markdownDiv, buildSessionHistoryCard, paginationBar } from './card-helpers.js';
|
|
12
14
|
import { MAX_FILE_UPLOAD_SIZE } from '../connector/file-limits.js';
|
|
13
15
|
import { atomicWrite } from '../persistence/atomic-write.js';
|
|
14
16
|
import { checkLatestVersion as defaultCheckLatestVersion, isNewer as defaultIsNewer, runInstallLatest as defaultRunInstallLatest, } from '../update/index.js';
|
|
15
17
|
/** Config card builder - delegates to per-agent builders */
|
|
16
|
-
import { getConfigBuilder, listRegisteredAgents } from './config/index.js';
|
|
18
|
+
import { getConfigBuilder, listRegisteredAgents, sortAgentsForDisplay } from './config/index.js';
|
|
17
19
|
import { probeAllAgents, getCachedAvailability } from '../runner/probe.js';
|
|
18
20
|
import { buildConfigCardFromTabs } from './config/common/render.js';
|
|
19
21
|
/** Max events to read for auto-resume and /resume cards.
|
|
@@ -37,6 +39,21 @@ const ORDER_PAGE_SIZE = 15;
|
|
|
37
39
|
* 留足余量。
|
|
38
40
|
*/
|
|
39
41
|
const WS_PAGE_SIZE = 5;
|
|
42
|
+
/**
|
|
43
|
+
* 归一化卡片分页参数:clamp pageSize 到 [1,maxPageSize],offset 归整为
|
|
44
|
+
* 非负整数并对齐到页边界。非数字 payload(如 'abc')不得产生 NaN slice。
|
|
45
|
+
*
|
|
46
|
+
* resume.page / active.page 共用,避免两处重复实现分页归一化。
|
|
47
|
+
*/
|
|
48
|
+
function normalizePageArgs(value, maxPageSize) {
|
|
49
|
+
const rawPageSize = Number(value.pageSize);
|
|
50
|
+
const pageSize = Number.isFinite(rawPageSize)
|
|
51
|
+
? clampInt(Math.trunc(rawPageSize), 1, maxPageSize)
|
|
52
|
+
: maxPageSize;
|
|
53
|
+
const rawOffset = Number(value.offset);
|
|
54
|
+
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
|
|
55
|
+
return { pageSize, alignedOffset: Math.floor(offset / pageSize) * pageSize };
|
|
56
|
+
}
|
|
40
57
|
/** /resume 列表页内全量预取(readSessionContent)的行数上限;其余行用轻量 summary 兜底。 */
|
|
41
58
|
const RESUME_CONTENT_PREFETCH = 5;
|
|
42
59
|
/**
|
|
@@ -101,17 +118,25 @@ export function isImmediateAction(cmd) {
|
|
|
101
118
|
// 队列会排在等待审批的 run 之后形成死锁:run 不结束审批不执行,run 结束
|
|
102
119
|
// coordinator 已删响应空转(线上复现:approval.respond 排队卡、审批永不生效)。
|
|
103
120
|
cmd === 'approval.respond' ||
|
|
104
|
-
cmd === 'approval.toggle'
|
|
121
|
+
cmd === 'approval.toggle' ||
|
|
122
|
+
cmd === 'approval.answer' ||
|
|
123
|
+
cmd === 'approval.answerSubmit' ||
|
|
124
|
+
cmd === 'approval.answerCustom' ||
|
|
125
|
+
cmd === 'approval.answerNote');
|
|
105
126
|
}
|
|
106
127
|
export class CommandRouter {
|
|
107
128
|
/** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
|
|
108
|
-
static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
129
|
+
static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
|
|
109
130
|
sessionStore;
|
|
131
|
+
/** 飞书桥接实例(public:测试直接访问断言,替代 as unknown as)。 */
|
|
110
132
|
bridge;
|
|
133
|
+
/** 当前配置(public:测试直接读取断言,替代 as unknown as)。 */
|
|
111
134
|
config;
|
|
135
|
+
/** 配置文件的绝对路径(public:/config 保存链路与测试共用)。 */
|
|
112
136
|
configPath;
|
|
113
137
|
workspaceStore;
|
|
114
138
|
orderStore;
|
|
139
|
+
aliasStore;
|
|
115
140
|
exitHandler;
|
|
116
141
|
pendingExit = false;
|
|
117
142
|
/**
|
|
@@ -126,7 +151,8 @@ export class CommandRouter {
|
|
|
126
151
|
updateCachePath;
|
|
127
152
|
/** Injected update functions (for testability; defaults to real implementation). */
|
|
128
153
|
updateFns;
|
|
129
|
-
|
|
154
|
+
/** /config 卡片编辑暂存区(public:测试直接读取断言,替代 as unknown as)。 */
|
|
155
|
+
pendingConfig = null;
|
|
130
156
|
/**
|
|
131
157
|
* Monotonic counter minting unique internal keys for order.exec enqueue
|
|
132
158
|
* actions. One order card can be clicked many times, so the Feishu card
|
|
@@ -147,6 +173,7 @@ export class CommandRouter {
|
|
|
147
173
|
* it uses `listRegisteredAgents()` from `router/config/index.ts` (the
|
|
148
174
|
* config-builder registry), which mirrors the agents with a config card.
|
|
149
175
|
*/
|
|
176
|
+
/** Session reader 注册中心(public:测试注入/读取,替代 as unknown as)。 */
|
|
150
177
|
sessionReaderRegistry;
|
|
151
178
|
/**
|
|
152
179
|
* 串行化 config.* 卡片回调(2026-07-04)。
|
|
@@ -164,6 +191,7 @@ export class CommandRouter {
|
|
|
164
191
|
this.configPath = opts.configPath;
|
|
165
192
|
this.workspaceStore = new WorkspaceStore(opts.workspacePath);
|
|
166
193
|
this.orderStore = new OrderStore(opts.ordersPath);
|
|
194
|
+
this.aliasStore = new AliasStore(opts.aliasesPath);
|
|
167
195
|
this.exitHandler = opts.exitHandler ?? (() => process.exit(0));
|
|
168
196
|
this.restartSpawner = opts.restartSpawner;
|
|
169
197
|
// idle watchdog 窗口从 config.idle.watchdogMinutes 读取
|
|
@@ -202,6 +230,8 @@ export class CommandRouter {
|
|
|
202
230
|
await this.bridge.sendResult(result, ctx);
|
|
203
231
|
}
|
|
204
232
|
if (this.pendingExit) {
|
|
233
|
+
// 干净退出前冲刷待合批的媒体保存提示,避免 500ms 窗口内的提示丢失。
|
|
234
|
+
await this.bridge.flushAllMediaNotifications();
|
|
205
235
|
this.exitHandler();
|
|
206
236
|
}
|
|
207
237
|
return result;
|
|
@@ -222,6 +252,13 @@ export class CommandRouter {
|
|
|
222
252
|
await this.bridge.forwardToClaude(trimmed, ctx, opts);
|
|
223
253
|
return null;
|
|
224
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* 一次别名展开:bridge 收到用户消息后、命令分发前调用。
|
|
257
|
+
* 仅 `$name` 开头的消息可能被展开;未知别名 / 非 `$` 消息原样返回。
|
|
258
|
+
*/
|
|
259
|
+
expandAliasMessage(message) {
|
|
260
|
+
return resolveAlias(message, this.aliasStore.list()) ?? message;
|
|
261
|
+
}
|
|
225
262
|
/**
|
|
226
263
|
* Handle a CardKit card button click (§6.2).
|
|
227
264
|
* Validates payload and dispatches to the corresponding command.
|
|
@@ -326,6 +363,10 @@ export class CommandRouter {
|
|
|
326
363
|
return this.enqueueConfigAction(value, ctx);
|
|
327
364
|
case 'approval.respond':
|
|
328
365
|
case 'approval.toggle':
|
|
366
|
+
case 'approval.answer':
|
|
367
|
+
case 'approval.answerSubmit':
|
|
368
|
+
case 'approval.answerCustom':
|
|
369
|
+
case 'approval.answerNote':
|
|
329
370
|
return this.handleApprovalAction(value, ctx);
|
|
330
371
|
case 'codex.compact':
|
|
331
372
|
await this.bridge.handleCodexCompact(value, ctx);
|
|
@@ -346,6 +387,7 @@ export class CommandRouter {
|
|
|
346
387
|
// 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
|
|
347
388
|
// 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
|
|
348
389
|
if (this.pendingExit) {
|
|
390
|
+
await this.bridge.flushAllMediaNotifications();
|
|
349
391
|
this.exitHandler();
|
|
350
392
|
}
|
|
351
393
|
return;
|
|
@@ -363,8 +405,16 @@ export class CommandRouter {
|
|
|
363
405
|
* Routes to the appropriate bridge method based on value.cmd.
|
|
364
406
|
* Returns a toast response for immediate user feedback.
|
|
365
407
|
*/
|
|
408
|
+
/** 答案类操作的统一失败反馈:重复投递(同一 nonce 第二次点击)是已生效的
|
|
409
|
+
* 中性事件,报 error 会误导(首次点击实际已成功)。 */
|
|
410
|
+
answerFailureToast(msg) {
|
|
411
|
+
if (/already submitted \(duplicate nonce\)/.test(msg)) {
|
|
412
|
+
return { toast: { type: 'info', content: '该选项已处理,请勿重复点击' } };
|
|
413
|
+
}
|
|
414
|
+
return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
|
|
415
|
+
}
|
|
366
416
|
async handleApprovalAction(value, _ctx) {
|
|
367
|
-
const { runId, requestId, decision,
|
|
417
|
+
const { runId, requestId, decision, nonce, permId } = value;
|
|
368
418
|
if (value.cmd === 'approval.respond') {
|
|
369
419
|
if (!runId || requestId === undefined || !decision || !nonce) {
|
|
370
420
|
return { toast: { type: 'error', content: '缺少审批响应参数' } };
|
|
@@ -374,7 +424,6 @@ export class CommandRouter {
|
|
|
374
424
|
runId,
|
|
375
425
|
requestId,
|
|
376
426
|
decision,
|
|
377
|
-
scope,
|
|
378
427
|
nonce,
|
|
379
428
|
});
|
|
380
429
|
return { toast: { type: 'success', content: '审批已提交' } };
|
|
@@ -389,7 +438,7 @@ export class CommandRouter {
|
|
|
389
438
|
}
|
|
390
439
|
}
|
|
391
440
|
if (value.cmd === 'approval.toggle') {
|
|
392
|
-
if (!runId || requestId === undefined || !permId
|
|
441
|
+
if (!runId || requestId === undefined || !permId) {
|
|
393
442
|
return { toast: { type: 'error', content: '缺少权限切换参数' } };
|
|
394
443
|
}
|
|
395
444
|
try {
|
|
@@ -397,7 +446,6 @@ export class CommandRouter {
|
|
|
397
446
|
runId,
|
|
398
447
|
requestId,
|
|
399
448
|
permId,
|
|
400
|
-
nonce,
|
|
401
449
|
selected: value.selected ?? true,
|
|
402
450
|
});
|
|
403
451
|
return { toast: { type: 'info', content: '已切换' } };
|
|
@@ -407,6 +455,93 @@ export class CommandRouter {
|
|
|
407
455
|
return { toast: { type: 'error', content: `权限切换失败:${msg}` } };
|
|
408
456
|
}
|
|
409
457
|
}
|
|
458
|
+
if (value.cmd === 'approval.answer') {
|
|
459
|
+
if (!runId ||
|
|
460
|
+
requestId === undefined ||
|
|
461
|
+
value.questionIndex === undefined ||
|
|
462
|
+
!value.option ||
|
|
463
|
+
!nonce) {
|
|
464
|
+
return { toast: { type: 'error', content: '缺少问题答案参数' } };
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
await this.bridge.handleApprovalAnswer({
|
|
468
|
+
runId,
|
|
469
|
+
requestId,
|
|
470
|
+
questionIndex: value.questionIndex,
|
|
471
|
+
option: value.option,
|
|
472
|
+
nonce,
|
|
473
|
+
});
|
|
474
|
+
return { toast: { type: 'success', content: '已选择' } };
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
478
|
+
return this.answerFailureToast(msg);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (value.cmd === 'approval.answerSubmit') {
|
|
482
|
+
if (!runId || requestId === undefined || value.questionIndex === undefined || !nonce) {
|
|
483
|
+
return { toast: { type: 'error', content: '缺少提交参数' } };
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
await this.bridge.handleApprovalAnswerSubmit({
|
|
487
|
+
runId,
|
|
488
|
+
requestId,
|
|
489
|
+
questionIndex: value.questionIndex,
|
|
490
|
+
nonce,
|
|
491
|
+
});
|
|
492
|
+
return { toast: { type: 'success', content: '答案已提交' } };
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
496
|
+
return this.answerFailureToast(msg);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (value.cmd === 'approval.answerCustom') {
|
|
500
|
+
if (!runId ||
|
|
501
|
+
requestId === undefined ||
|
|
502
|
+
value.questionIndex === undefined ||
|
|
503
|
+
!nonce ||
|
|
504
|
+
!value.inputValue) {
|
|
505
|
+
return { toast: { type: 'error', content: '缺少答案文本参数' } };
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
await this.bridge.handleApprovalAnswerCustom({
|
|
509
|
+
runId,
|
|
510
|
+
requestId,
|
|
511
|
+
questionIndex: value.questionIndex,
|
|
512
|
+
text: value.inputValue,
|
|
513
|
+
nonce,
|
|
514
|
+
});
|
|
515
|
+
return { toast: { type: 'success', content: '答案已提交' } };
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
519
|
+
return this.answerFailureToast(msg);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (value.cmd === 'approval.answerNote') {
|
|
523
|
+
if (!runId ||
|
|
524
|
+
requestId === undefined ||
|
|
525
|
+
value.questionIndex === undefined ||
|
|
526
|
+
!nonce ||
|
|
527
|
+
!value.inputValue) {
|
|
528
|
+
return { toast: { type: 'error', content: '缺少补充说明参数' } };
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
await this.bridge.handleApprovalAnswerNote({
|
|
532
|
+
runId,
|
|
533
|
+
requestId,
|
|
534
|
+
questionIndex: value.questionIndex,
|
|
535
|
+
text: value.inputValue,
|
|
536
|
+
nonce,
|
|
537
|
+
});
|
|
538
|
+
return { toast: { type: 'success', content: '补充说明已保存' } };
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
542
|
+
return this.answerFailureToast(msg);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
410
545
|
return { toast: { type: 'error', content: '未知的审批操作' } };
|
|
411
546
|
}
|
|
412
547
|
/**
|
|
@@ -888,17 +1023,7 @@ export class CommandRouter {
|
|
|
888
1023
|
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
889
1024
|
? resumeAgent
|
|
890
1025
|
: this.config.defaultAgent;
|
|
891
|
-
const
|
|
892
|
-
const pageSize = Number.isFinite(rawPageSize)
|
|
893
|
-
? Math.min(Math.max(Math.trunc(rawPageSize), 1), RESUME_PAGE_SIZE)
|
|
894
|
-
: RESUME_PAGE_SIZE;
|
|
895
|
-
// Normalize the callback offset to a finite non-negative integer, then
|
|
896
|
-
// align it to a page boundary so prev/next always step by pageSize.
|
|
897
|
-
// Non-numeric payloads (e.g. 'abc') must not produce NaN slices that look
|
|
898
|
-
// like an empty directory.
|
|
899
|
-
const rawOffset = Number(value.offset);
|
|
900
|
-
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
|
|
901
|
-
const alignedOffset = Math.floor(offset / pageSize) * pageSize;
|
|
1026
|
+
const { pageSize, alignedOffset } = normalizePageArgs(value, RESUME_PAGE_SIZE);
|
|
902
1027
|
const entry = this.sessionStore.get(ctx.userId);
|
|
903
1028
|
const cwd = entry?.cwd;
|
|
904
1029
|
if (!cwd) {
|
|
@@ -925,9 +1050,7 @@ export class CommandRouter {
|
|
|
925
1050
|
* Mirrors handleResumePage — updates the same card, never sends a new one.
|
|
926
1051
|
*/
|
|
927
1052
|
async handleActivePage(value, ctx) {
|
|
928
|
-
const
|
|
929
|
-
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
|
|
930
|
-
const alignedOffset = Math.floor(offset / ACTIVE_PAGE_SIZE) * ACTIVE_PAGE_SIZE;
|
|
1053
|
+
const { alignedOffset } = normalizePageArgs(value, ACTIVE_PAGE_SIZE);
|
|
931
1054
|
const result = this.cmdActive([], ctx, alignedOffset);
|
|
932
1055
|
if (!result.card) {
|
|
933
1056
|
return { toast: { type: 'error', content: '当前没有正在进行中的任务' } };
|
|
@@ -1186,6 +1309,7 @@ export class CommandRouter {
|
|
|
1186
1309
|
return next;
|
|
1187
1310
|
}
|
|
1188
1311
|
/** 实际的 config.* 分发逻辑(在串行队列内执行) */
|
|
1312
|
+
/** config.* 卡片动作分发(public:测试直接调用,替代 as unknown as)。 */
|
|
1189
1313
|
async dispatchConfigAction(value, ctx) {
|
|
1190
1314
|
switch (value.cmd) {
|
|
1191
1315
|
case 'config.toggle': {
|
|
@@ -1286,6 +1410,8 @@ export class CommandRouter {
|
|
|
1286
1410
|
// 2026-08-03: 卡片刷新失败不吞切换通知——保存已成功,仅记录日志,
|
|
1287
1411
|
// 后续切换消息照常发送;真正写盘/传播阶段的失败才走外层 catch 报「保存失败」。
|
|
1288
1412
|
try {
|
|
1413
|
+
// host 可能已变更 → 重新预取 DSH 目录(内部按 host 缓存,未变不重复拉)
|
|
1414
|
+
await this.prefetchConfigCardCatalogs();
|
|
1289
1415
|
await this.bridge.updateCardInPlace(this.buildConfigCard().card, ctx);
|
|
1290
1416
|
}
|
|
1291
1417
|
catch (refreshErr) {
|
|
@@ -1314,37 +1440,29 @@ export class CommandRouter {
|
|
|
1314
1440
|
}
|
|
1315
1441
|
}
|
|
1316
1442
|
/** 设置嵌套属性值(仅修改内存对象,不写盘) */
|
|
1443
|
+
/** 在 pendingConfig 上按 dot-separated key 设置嵌套值(config 卡片编辑用)。 */
|
|
1317
1444
|
setNestedValue(target, key, value) {
|
|
1318
1445
|
// Path mapping 共用 config 模块的 mapAgentKey(G11 Inconsistency 修复):
|
|
1319
1446
|
// pi.xxx/codex.xxx/opencode.xxx → agents.xxx,claude.xxx 保持顶层
|
|
1320
1447
|
const mappedKey = mapAgentKey(key);
|
|
1321
1448
|
const parts = mappedKey.split('.');
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
// `current['__proto__']` 污染 Object.prototype(文本直写路径已守卫,
|
|
1328
|
-
// 卡片路径是同一威胁模型的另一入口)。
|
|
1329
|
-
assertSafeKeyPart(parts[i]);
|
|
1330
|
-
const next = current[parts[i]];
|
|
1331
|
-
if (next == null || typeof next !== 'object') {
|
|
1332
|
-
current[parts[i]] = {};
|
|
1333
|
-
}
|
|
1334
|
-
current = current[parts[i]];
|
|
1335
|
-
}
|
|
1336
|
-
assertSafeKeyPart(parts[parts.length - 1]);
|
|
1449
|
+
// 复用 config 模块的 walkNestedContainer(含 __proto__/prototype/constructor
|
|
1450
|
+
// 守卫与中间段补 {}),router 只保留 value=undefined → delete 分支。
|
|
1451
|
+
const container = walkNestedContainer(target, parts, true);
|
|
1452
|
+
const lastPart = parts[parts.length - 1];
|
|
1453
|
+
assertSafeKeyPart(lastPart);
|
|
1337
1454
|
if (value === undefined) {
|
|
1338
1455
|
// value=undefined 表示"删除键"(如清空 reasoningEffort),
|
|
1339
1456
|
// 不能写成 undefined 值——diffConfig 会把 undefined 转成字面量 "undefined"
|
|
1340
1457
|
// 写入 config.yaml 并透传给 codex(ReasoningEffort::Custom("undefined"))。
|
|
1341
|
-
delete
|
|
1458
|
+
delete container[lastPart];
|
|
1342
1459
|
}
|
|
1343
1460
|
else {
|
|
1344
|
-
|
|
1461
|
+
container[lastPart] = value;
|
|
1345
1462
|
}
|
|
1346
1463
|
}
|
|
1347
1464
|
/** 对比原始 config 与 pendingConfig,返回变化的 key→string 映射 */
|
|
1465
|
+
/** config diff(public:测试直接调用,替代 as unknown as)。 */
|
|
1348
1466
|
diffConfig(original, pending) {
|
|
1349
1467
|
const updates = {};
|
|
1350
1468
|
this.collectDiff('', original, pending, updates);
|
|
@@ -1470,6 +1588,24 @@ export class CommandRouter {
|
|
|
1470
1588
|
switchSessionId = '';
|
|
1471
1589
|
}
|
|
1472
1590
|
}
|
|
1591
|
+
// DSH preset 变更 = 下次 run 新建 session(§4.3 D1):
|
|
1592
|
+
// preset 在 session 创建时固定,中途切换会被服务端拒绝(agent-preset-conflict)。
|
|
1593
|
+
// 旧 sessionId 存入 previousSessions 停车位(可手动 /resume,但 /resume 会做
|
|
1594
|
+
// preset 一致性校验),当前 sessionId 清空——下次 run 走 session.create 新建。
|
|
1595
|
+
const hasDshPresetChange = Object.keys(updates).some((k) => k === 'agents.dsh.agentPreset');
|
|
1596
|
+
// 已发生 agent 切换时(switchNotice 已设置)preset 分支跳过,避免覆盖切换 notice;
|
|
1597
|
+
// 切换分支对 dsh 的 session 已做恢复/清空处理。
|
|
1598
|
+
if (hasDshPresetChange && this.config.defaultAgent === 'dsh' && !switchNotice) {
|
|
1599
|
+
const oldSessionId = this.sessionStore.getSessionId(ctx.userId, 'dsh');
|
|
1600
|
+
if (oldSessionId) {
|
|
1601
|
+
this.sessionStore.setPreviousSessionId(ctx.userId, 'dsh', oldSessionId);
|
|
1602
|
+
this.sessionStore.clearSessionId(ctx.userId, 'dsh');
|
|
1603
|
+
switchNotice = `DSH preset 已变更,旧会话已停放到恢复槽(可 /resume 校验后恢复),下次消息将新建会话`;
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
switchNotice = `DSH preset 已变更,下次消息将新建会话`;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1473
1609
|
// 2026-07-13: 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
|
|
1474
1610
|
const currentAgent = this.config.defaultAgent;
|
|
1475
1611
|
const currentAgentConfigKey = `agents.${currentAgent}`;
|
|
@@ -1480,6 +1616,27 @@ export class CommandRouter {
|
|
|
1480
1616
|
atomicWrite(this.configPath, YAML.stringify(this.config));
|
|
1481
1617
|
this.bridge.setConfig(this.config);
|
|
1482
1618
|
}
|
|
1619
|
+
// P1-6 + live approval settings (§P5): active workspace-lifetime runners
|
|
1620
|
+
// are not evicted by clearRunners() (would orphan the live subprocess).
|
|
1621
|
+
// Push the new approval-mode settings to those runners via the unified
|
|
1622
|
+
// duck method `updateApprovalMode` (codex thread/settings/update,
|
|
1623
|
+
// kimi/opencode session/set_mode) so they take effect for subsequent
|
|
1624
|
+
// turns without waiting for the runner to be recreated.
|
|
1625
|
+
const hasApprovalConfigChange = Object.keys(updates).some((k) => k === 'agents.codex' ||
|
|
1626
|
+
k.startsWith('agents.codex.') ||
|
|
1627
|
+
k === 'codex' ||
|
|
1628
|
+
k.startsWith('codex.') ||
|
|
1629
|
+
k === 'agents.kimi' ||
|
|
1630
|
+
k.startsWith('agents.kimi.') ||
|
|
1631
|
+
k === 'kimi' ||
|
|
1632
|
+
k.startsWith('kimi.') ||
|
|
1633
|
+
k === 'agents.opencode' ||
|
|
1634
|
+
k.startsWith('agents.opencode.') ||
|
|
1635
|
+
k === 'opencode' ||
|
|
1636
|
+
k.startsWith('opencode.'));
|
|
1637
|
+
if (hasApprovalConfigChange) {
|
|
1638
|
+
this.bridge.syncActiveApprovalModes();
|
|
1639
|
+
}
|
|
1483
1640
|
// 2026-07-13: 任何 agent 配置变更或 defaultAgent 切换,都清除 runner 缓存
|
|
1484
1641
|
if (hasDefaultAgentChange || hasAgentConfigChange) {
|
|
1485
1642
|
this.bridge.clearRunners();
|
|
@@ -1560,7 +1717,11 @@ export class CommandRouter {
|
|
|
1560
1717
|
{ cmd: 'cd', label: '/cd <path>', desc: '切换工作目录' },
|
|
1561
1718
|
{ cmd: 'ls', label: '/ls [dir]', desc: '列出当前目录,可指定子目录' },
|
|
1562
1719
|
{ cmd: 'resume', label: '/resume /r [agent] [list|id|N]', desc: '列出或切换 Agent session' },
|
|
1563
|
-
{
|
|
1720
|
+
{
|
|
1721
|
+
cmd: 'order',
|
|
1722
|
+
label: '/order /o save|list',
|
|
1723
|
+
desc: '收藏指令 /order save,快捷别名 /order alias',
|
|
1724
|
+
},
|
|
1564
1725
|
];
|
|
1565
1726
|
const bodyElements = [];
|
|
1566
1727
|
// 按钮组 — CardKit 2.0: column_set + column + button with behaviors
|
|
@@ -1641,6 +1802,7 @@ export class CommandRouter {
|
|
|
1641
1802
|
},
|
|
1642
1803
|
};
|
|
1643
1804
|
}
|
|
1805
|
+
/** /status 实现(public:测试直接调用,替代 as any)。 */
|
|
1644
1806
|
cmdStatus(ctx) {
|
|
1645
1807
|
const entry = this.sessionStore.get(ctx.userId);
|
|
1646
1808
|
const cwd = entry?.cwd ?? '(未设置)';
|
|
@@ -1886,84 +2048,30 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1886
2048
|
buildAutoResumeCard(cwd, session, _ctx) {
|
|
1887
2049
|
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
1888
2050
|
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(session.sessionId, cwd, { maxEvents: AUTO_RESUME_MAX_EVENTS });
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
const recapPreview = recap.length > 200 ? recap.slice(0, 197) + '...' : recap;
|
|
1898
|
-
sections.push(`📝 **Recap**\n${recapPreview}`);
|
|
1899
|
-
}
|
|
1900
|
-
if (sections.length > 0) {
|
|
1901
|
-
header += '\n\n' + sections.join('\n\n──\n\n');
|
|
1902
|
-
}
|
|
1903
|
-
const elements = [markdownDiv(header), { tag: 'hr' }];
|
|
1904
|
-
// Fold session history into collapsible panels: the last 2 events stay
|
|
1905
|
-
// expanded so the user sees the most recent context without clicking;
|
|
1906
|
-
// older events are collapsed to keep the card compact.
|
|
1907
|
-
content.forEach((ev, i) => {
|
|
1908
|
-
elements.push(sessionEventPanel(ev, i, content.length, 2, this.config.defaultAgent));
|
|
2051
|
+
const actions = this.buildResumeActionButtons({
|
|
2052
|
+
isActive,
|
|
2053
|
+
activeRunRunId,
|
|
2054
|
+
cwd,
|
|
2055
|
+
compact: this.canCompactSession(this.config.defaultAgent, cwd)
|
|
2056
|
+
? { sessionId: session.sessionId, agentKind: this.config.defaultAgent }
|
|
2057
|
+
: undefined,
|
|
2058
|
+
newSession: true,
|
|
1909
2059
|
});
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
// non-terminal, which guarantees activeRunRunId is set). Bind the REAL
|
|
1925
|
-
// activeRun.runId (UUID) so bridge.interruptCurrentRun can match and
|
|
1926
|
-
// stop a live run.
|
|
1927
|
-
if (isActive) {
|
|
1928
|
-
actions.push({
|
|
1929
|
-
tag: 'button',
|
|
1930
|
-
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
1931
|
-
type: 'danger',
|
|
1932
|
-
behaviors: [
|
|
1933
|
-
{
|
|
1934
|
-
type: 'callback',
|
|
1935
|
-
value: { cmd: 'stop', runId: activeRunRunId, cwd },
|
|
1936
|
-
},
|
|
1937
|
-
],
|
|
1938
|
-
});
|
|
1939
|
-
}
|
|
1940
|
-
// Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且会话未在跑。
|
|
1941
|
-
// 在途 run 会占住 app-server runner,压缩必然失败,此时不渲染。
|
|
1942
|
-
if (!isActive && this.canCompactSession(this.config.defaultAgent)) {
|
|
1943
|
-
actions.push(resumeCompactButton(session.sessionId, this.config.defaultAgent));
|
|
1944
|
-
}
|
|
1945
|
-
// New session button always shown (use shared function for consistency)
|
|
1946
|
-
actions.push(newSessionButton());
|
|
1947
|
-
// Use column_set+column for 2.0 (action tag not supported in 2.0)
|
|
1948
|
-
elements.push({
|
|
1949
|
-
tag: 'column_set',
|
|
1950
|
-
columns: actions.map((btn) => ({
|
|
1951
|
-
tag: 'column',
|
|
1952
|
-
width: 'auto',
|
|
1953
|
-
elements: [btn],
|
|
1954
|
-
})),
|
|
2060
|
+
const card = buildSessionHistoryCard({
|
|
2061
|
+
sessionId: session.sessionId,
|
|
2062
|
+
cwd,
|
|
2063
|
+
displayTitle,
|
|
2064
|
+
aiTitle,
|
|
2065
|
+
recap,
|
|
2066
|
+
events: content,
|
|
2067
|
+
usage,
|
|
2068
|
+
}, {
|
|
2069
|
+
agentKind: this.config.defaultAgent,
|
|
2070
|
+
headerText: `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`,
|
|
2071
|
+
title: `${isActive ? '⏳ 自动恢复会话(完成中)' : '🔁 自动恢复会话'} · ${agentName}`,
|
|
2072
|
+
usageResult: 'success',
|
|
2073
|
+
actions,
|
|
1955
2074
|
});
|
|
1956
|
-
const card = {
|
|
1957
|
-
schema: '2.0',
|
|
1958
|
-
config: { wide_screen_mode: true },
|
|
1959
|
-
header: {
|
|
1960
|
-
title: {
|
|
1961
|
-
tag: 'plain_text',
|
|
1962
|
-
content: `${isActive ? '⏳ 自动恢复会话(完成中)' : '🔁 自动恢复会话'} · ${agentName}`,
|
|
1963
|
-
},
|
|
1964
|
-
},
|
|
1965
|
-
body: { elements },
|
|
1966
|
-
};
|
|
1967
2075
|
return { card };
|
|
1968
2076
|
}
|
|
1969
2077
|
/**
|
|
@@ -1977,20 +2085,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1977
2085
|
const agentName = agentDisplayName(newAgent);
|
|
1978
2086
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
1979
2087
|
const sessionId = switchResult.sessionId;
|
|
1980
|
-
// No cwd or no session → compact notice card
|
|
2088
|
+
// No cwd or no session → compact notice card (no new-session button: the
|
|
2089
|
+
// next message already starts a fresh session, a button would mislead
|
|
2090
|
+
// users into thinking a click is required)
|
|
1981
2091
|
if (!cwd || !sessionId) {
|
|
1982
2092
|
const elements = [markdownDiv(switchResult.notice)];
|
|
1983
|
-
// New session button
|
|
1984
|
-
elements.push({
|
|
1985
|
-
tag: 'column_set',
|
|
1986
|
-
columns: [
|
|
1987
|
-
{
|
|
1988
|
-
tag: 'column',
|
|
1989
|
-
width: 'auto',
|
|
1990
|
-
elements: [newSessionButton()],
|
|
1991
|
-
},
|
|
1992
|
-
],
|
|
1993
|
-
});
|
|
1994
2093
|
return {
|
|
1995
2094
|
card: {
|
|
1996
2095
|
schema: '2.0',
|
|
@@ -2034,79 +2133,79 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2034
2133
|
agentKind: newAgent,
|
|
2035
2134
|
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2036
2135
|
});
|
|
2037
|
-
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2136
|
+
// Action buttons: stop (active) + new session
|
|
2137
|
+
const actions = this.buildResumeActionButtons({
|
|
2138
|
+
isActive,
|
|
2139
|
+
activeRunRunId,
|
|
2140
|
+
cwd,
|
|
2141
|
+
newSession: true,
|
|
2142
|
+
});
|
|
2143
|
+
const card = buildSessionHistoryCard({
|
|
2144
|
+
sessionId,
|
|
2145
|
+
cwd,
|
|
2146
|
+
displayTitle,
|
|
2147
|
+
aiTitle,
|
|
2148
|
+
recap,
|
|
2149
|
+
events: content,
|
|
2150
|
+
usage,
|
|
2151
|
+
}, {
|
|
2152
|
+
agentKind: newAgent,
|
|
2153
|
+
headerText: `📂 \`${cwd}\`\n已恢复会话: **${sessionId}**`,
|
|
2154
|
+
title: `${isActive ? '⏳ 切换 Agent(完成中)' : '🔀 切换 Agent'} · ${agentName}`,
|
|
2155
|
+
usageResult: 'success',
|
|
2156
|
+
emptyPlaceholder: '_该会话暂无新消息可显示(最后一条为用户输入)_',
|
|
2157
|
+
actions,
|
|
2158
|
+
});
|
|
2159
|
+
return { card };
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Whether a session of the given agent kind can be compacted from a resume
|
|
2163
|
+
* card. Design doc §6.2-2: duck-typing — check if the runner has `runCompact`
|
|
2164
|
+
* (codex app-server, kimi acp), instead of hardcoding agentKind === 'codex'.
|
|
2165
|
+
*/
|
|
2166
|
+
canCompactSession(agentKind, cwd) {
|
|
2167
|
+
if (!cwd)
|
|
2168
|
+
return false;
|
|
2169
|
+
return this.bridge.hasRunCompact(cwd, agentKind);
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Assemble the action buttons for a resume / session-history card.
|
|
2173
|
+
* Shared by buildAutoResumeCard, buildConfigSwitchCard, and cmdResume so the
|
|
2174
|
+
* button set (stop / compact / new-session) follows one contract instead of
|
|
2175
|
+
* drifting per call site.
|
|
2176
|
+
*
|
|
2177
|
+
* - stop: only when the session has an in-memory active run (isActive), bound
|
|
2178
|
+
* to the real activeRun.runId so bridge.interruptCurrentRun can match.
|
|
2179
|
+
* - compact: only when a compact-capable runner applies and the session is
|
|
2180
|
+
* not running (a live run would occupy the runner, so compaction would
|
|
2181
|
+
* fail). The caller passes `compact` only when canCompactSession holds.
|
|
2182
|
+
* - new-session: when requested (always shown on resume cards).
|
|
2183
|
+
*/
|
|
2184
|
+
buildResumeActionButtons(opts) {
|
|
2067
2185
|
const actions = [];
|
|
2068
|
-
if (isActive) {
|
|
2186
|
+
if (opts.isActive) {
|
|
2069
2187
|
actions.push({
|
|
2070
2188
|
tag: 'button',
|
|
2071
2189
|
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
2072
2190
|
type: 'danger',
|
|
2073
|
-
behaviors: [
|
|
2191
|
+
behaviors: [
|
|
2192
|
+
{
|
|
2193
|
+
type: 'callback',
|
|
2194
|
+
value: { cmd: 'stop', runId: opts.activeRunRunId, cwd: opts.cwd },
|
|
2195
|
+
},
|
|
2196
|
+
],
|
|
2074
2197
|
});
|
|
2075
2198
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
})),
|
|
2084
|
-
});
|
|
2085
|
-
return {
|
|
2086
|
-
card: {
|
|
2087
|
-
schema: '2.0',
|
|
2088
|
-
config: { wide_screen_mode: true },
|
|
2089
|
-
header: {
|
|
2090
|
-
title: {
|
|
2091
|
-
tag: 'plain_text',
|
|
2092
|
-
content: `${isActive ? '⏳ 切换 Agent(完成中)' : '🔀 切换 Agent'} · ${agentName}`,
|
|
2093
|
-
},
|
|
2094
|
-
},
|
|
2095
|
-
body: { elements },
|
|
2096
|
-
},
|
|
2097
|
-
};
|
|
2098
|
-
}
|
|
2099
|
-
/**
|
|
2100
|
-
* Whether a session of the given agent kind can be compacted from a resume
|
|
2101
|
-
* card. Compact (thread/compact/start) is only implemented by the codex
|
|
2102
|
-
* app-server runner (`runCompact`), so the button is gated on
|
|
2103
|
-
* `serviceMode === 'app-server'` — exec mode spawns a fresh CLI per message
|
|
2104
|
-
* and has no persistent session to compact.
|
|
2105
|
-
*/
|
|
2106
|
-
canCompactSession(agentKind) {
|
|
2107
|
-
return (agentKind === 'codex' && getAgentConfig(this.config, 'codex')?.serviceMode === 'app-server');
|
|
2199
|
+
if (opts.compact && !opts.isActive) {
|
|
2200
|
+
actions.push(resumeCompactButton(opts.compact.sessionId, opts.compact.agentKind));
|
|
2201
|
+
}
|
|
2202
|
+
if (opts.newSession) {
|
|
2203
|
+
actions.push(newSessionButton());
|
|
2204
|
+
}
|
|
2205
|
+
return actions;
|
|
2108
2206
|
}
|
|
2109
2207
|
static LS_PAGE_SIZE = 30;
|
|
2208
|
+
/** /ls 实现(public:测试直接调用,替代 as unknown as)。 */
|
|
2110
2209
|
cmdLs(args, ctx, offset = 0) {
|
|
2111
2210
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
2112
2211
|
if (!cwd) {
|
|
@@ -2290,78 +2389,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2290
2389
|
}
|
|
2291
2390
|
// Pagination bar (only shown when there are more items than PAGE_SIZE)
|
|
2292
2391
|
if (hasPagination) {
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
tag: 'column',
|
|
2301
|
-
width: 'auto',
|
|
2302
|
-
vertical_align: 'center',
|
|
2303
|
-
elements: [
|
|
2304
|
-
{
|
|
2305
|
-
tag: 'button',
|
|
2306
|
-
text: { tag: 'plain_text', content: '⬅ 上一页' },
|
|
2307
|
-
type: 'default',
|
|
2308
|
-
size: 'small',
|
|
2309
|
-
behaviors: [
|
|
2310
|
-
{
|
|
2311
|
-
type: 'callback',
|
|
2312
|
-
value: {
|
|
2313
|
-
cmd: 'ls.page',
|
|
2314
|
-
path: targetDir,
|
|
2315
|
-
offset: offset - CommandRouter.LS_PAGE_SIZE,
|
|
2316
|
-
},
|
|
2317
|
-
},
|
|
2318
|
-
],
|
|
2319
|
-
},
|
|
2320
|
-
],
|
|
2321
|
-
});
|
|
2322
|
-
}
|
|
2323
|
-
pageColumns.push({
|
|
2324
|
-
tag: 'column',
|
|
2325
|
-
width: 'weighted',
|
|
2326
|
-
weight: 1,
|
|
2327
|
-
vertical_align: 'center',
|
|
2328
|
-
elements: [
|
|
2329
|
-
{
|
|
2330
|
-
tag: 'div',
|
|
2331
|
-
text: {
|
|
2332
|
-
tag: 'lark_md',
|
|
2333
|
-
content: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 项)`,
|
|
2334
|
-
},
|
|
2335
|
-
},
|
|
2336
|
-
],
|
|
2392
|
+
const bar = paginationBar({
|
|
2393
|
+
cmd: 'ls.page',
|
|
2394
|
+
offset,
|
|
2395
|
+
pageSize: CommandRouter.LS_PAGE_SIZE,
|
|
2396
|
+
total: totalCount,
|
|
2397
|
+
extra: { path: targetDir },
|
|
2398
|
+
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 项)`,
|
|
2337
2399
|
});
|
|
2338
|
-
if (hasNext) {
|
|
2339
|
-
pageColumns.push({
|
|
2340
|
-
tag: 'column',
|
|
2341
|
-
width: 'auto',
|
|
2342
|
-
vertical_align: 'center',
|
|
2343
|
-
elements: [
|
|
2344
|
-
{
|
|
2345
|
-
tag: 'button',
|
|
2346
|
-
text: { tag: 'plain_text', content: '下一页 ➡' },
|
|
2347
|
-
type: 'default',
|
|
2348
|
-
size: 'small',
|
|
2349
|
-
behaviors: [
|
|
2350
|
-
{
|
|
2351
|
-
type: 'callback',
|
|
2352
|
-
value: {
|
|
2353
|
-
cmd: 'ls.page',
|
|
2354
|
-
path: targetDir,
|
|
2355
|
-
offset: offset + CommandRouter.LS_PAGE_SIZE,
|
|
2356
|
-
},
|
|
2357
|
-
},
|
|
2358
|
-
],
|
|
2359
|
-
},
|
|
2360
|
-
],
|
|
2361
|
-
});
|
|
2362
|
-
}
|
|
2363
2400
|
elements.push({ tag: 'hr' });
|
|
2364
|
-
elements.push(
|
|
2401
|
+
elements.push(bar);
|
|
2365
2402
|
}
|
|
2366
2403
|
return {
|
|
2367
2404
|
card: {
|
|
@@ -2451,7 +2488,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2451
2488
|
const totalCount = entries.length;
|
|
2452
2489
|
const totalPages = Math.max(1, Math.ceil(totalCount / WS_PAGE_SIZE));
|
|
2453
2490
|
const maxOffset = Math.max(0, (totalPages - 1) * WS_PAGE_SIZE);
|
|
2454
|
-
const safeOffset =
|
|
2491
|
+
const safeOffset = clampInt(offset, 0, maxOffset);
|
|
2455
2492
|
const currentPage = Math.floor(safeOffset / WS_PAGE_SIZE) + 1;
|
|
2456
2493
|
const pageEntries = entries.slice(safeOffset, safeOffset + WS_PAGE_SIZE);
|
|
2457
2494
|
const hasPagination = totalCount > WS_PAGE_SIZE;
|
|
@@ -2561,68 +2598,17 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2561
2598
|
// Pagination bar — separate row below the list (not crowded with sort
|
|
2562
2599
|
// toggle; mobile-friendly vertical layout).
|
|
2563
2600
|
if (hasPagination) {
|
|
2564
|
-
const
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
elements: [
|
|
2573
|
-
{
|
|
2574
|
-
tag: 'button',
|
|
2575
|
-
text: { tag: 'plain_text', content: '⬅' },
|
|
2576
|
-
type: 'default',
|
|
2577
|
-
size: 'small',
|
|
2578
|
-
behaviors: [
|
|
2579
|
-
{
|
|
2580
|
-
type: 'callback',
|
|
2581
|
-
value: { cmd: 'ws.page', offset: safeOffset - WS_PAGE_SIZE },
|
|
2582
|
-
},
|
|
2583
|
-
],
|
|
2584
|
-
},
|
|
2585
|
-
],
|
|
2586
|
-
});
|
|
2587
|
-
}
|
|
2588
|
-
pageColumns.push({
|
|
2589
|
-
tag: 'column',
|
|
2590
|
-
width: 'weighted',
|
|
2591
|
-
weight: 1,
|
|
2592
|
-
vertical_align: 'center',
|
|
2593
|
-
elements: [
|
|
2594
|
-
{
|
|
2595
|
-
tag: 'div',
|
|
2596
|
-
text: {
|
|
2597
|
-
tag: 'lark_md',
|
|
2598
|
-
content: `**${currentPage}/${totalPages}**(${totalCount})`,
|
|
2599
|
-
},
|
|
2600
|
-
},
|
|
2601
|
-
],
|
|
2601
|
+
const bar = paginationBar({
|
|
2602
|
+
cmd: 'ws.page',
|
|
2603
|
+
offset: safeOffset,
|
|
2604
|
+
pageSize: WS_PAGE_SIZE,
|
|
2605
|
+
total: totalCount,
|
|
2606
|
+
label: `**${currentPage}/${totalPages}**(${totalCount})`,
|
|
2607
|
+
prevText: '⬅',
|
|
2608
|
+
nextText: '➡',
|
|
2602
2609
|
});
|
|
2603
|
-
if (hasNext) {
|
|
2604
|
-
pageColumns.push({
|
|
2605
|
-
tag: 'column',
|
|
2606
|
-
width: 'auto',
|
|
2607
|
-
vertical_align: 'center',
|
|
2608
|
-
elements: [
|
|
2609
|
-
{
|
|
2610
|
-
tag: 'button',
|
|
2611
|
-
text: { tag: 'plain_text', content: '➡' },
|
|
2612
|
-
type: 'default',
|
|
2613
|
-
size: 'small',
|
|
2614
|
-
behaviors: [
|
|
2615
|
-
{
|
|
2616
|
-
type: 'callback',
|
|
2617
|
-
value: { cmd: 'ws.page', offset: safeOffset + WS_PAGE_SIZE },
|
|
2618
|
-
},
|
|
2619
|
-
],
|
|
2620
|
-
},
|
|
2621
|
-
],
|
|
2622
|
-
});
|
|
2623
|
-
}
|
|
2624
2610
|
bodyElements.push({ tag: 'hr' });
|
|
2625
|
-
bodyElements.push(
|
|
2611
|
+
bodyElements.push(bar);
|
|
2626
2612
|
}
|
|
2627
2613
|
}
|
|
2628
2614
|
const card = {
|
|
@@ -2642,6 +2628,36 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2642
2628
|
* misleading "未找到". Reuses the same card-builder helpers (markdownDiv) as
|
|
2643
2629
|
* the normal resume card; enforceCardBudget still applies at send time.
|
|
2644
2630
|
*/
|
|
2631
|
+
/**
|
|
2632
|
+
* /resume 核心实现(public:handleResumePage / handle / resume.use 共用,
|
|
2633
|
+
* 也是测试的直接 seam,避免测试用 @ts-expect-error 摸私有方法)。
|
|
2634
|
+
*/
|
|
2635
|
+
/**
|
|
2636
|
+
* DSH /resume preset 一致性校验:返回 mismatch 详情,或 undefined(一致/不可判定)。
|
|
2637
|
+
*
|
|
2638
|
+
* session 的 preset 在创建时固定(§2.1)。通过 listSessions 拿到目标 session 的
|
|
2639
|
+
* agentPreset 与当前配置比对:不一致时不静默复用(返回提示),一致或查询失败
|
|
2640
|
+
* (无法判定)时不阻断,避免 DSH 离线导致 /resume 完全不可用。
|
|
2641
|
+
*/
|
|
2642
|
+
dshPresetMismatch(reader, sessionId, cwd) {
|
|
2643
|
+
const currentPreset = this.config.agents?.dsh?.agentPreset;
|
|
2644
|
+
if (!currentPreset)
|
|
2645
|
+
return undefined; // 配置跟随服务端默认,无法判定 → 放行
|
|
2646
|
+
let sessionPreset;
|
|
2647
|
+
try {
|
|
2648
|
+
const { sessions } = reader.listSessions(cwd, { limit: 100 });
|
|
2649
|
+
sessionPreset = sessions.find((s) => s.sessionId === sessionId)?.agentPreset;
|
|
2650
|
+
}
|
|
2651
|
+
catch (err) {
|
|
2652
|
+
getLogger().warn(`[router] dshPresetMismatch listSessions failed for ${sessionId}: ${err.message}`);
|
|
2653
|
+
return undefined;
|
|
2654
|
+
}
|
|
2655
|
+
if (!sessionPreset)
|
|
2656
|
+
return undefined; // 服务端未返回 preset → 无法判定,放行
|
|
2657
|
+
if (sessionPreset === currentPreset)
|
|
2658
|
+
return undefined;
|
|
2659
|
+
return { sessionPreset, currentPreset };
|
|
2660
|
+
}
|
|
2645
2661
|
cmdResume(args, ctx, offset = 0) {
|
|
2646
2662
|
const entry = this.sessionStore.get(ctx.userId);
|
|
2647
2663
|
const cwd = entry?.cwd;
|
|
@@ -2688,93 +2704,58 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2688
2704
|
text: `未找到 session ${sessionIdArg}(当前目录: ${cwd})。请确认该 session 属于此目录,或先 /cd 到正确目录。`,
|
|
2689
2705
|
};
|
|
2690
2706
|
}
|
|
2707
|
+
// DSH preset 一致性校验(§6 风险 1):session 的 preset 在创建时固定,
|
|
2708
|
+
// 若与当前配置不符,直接复用会让后续 run 报 agent-preset-conflict。
|
|
2709
|
+
// 不静默复用——明确提示用户,由用户决定是否继续(配置里手动改/清 preset)。
|
|
2710
|
+
if (agentKind === 'dsh' && this.config.defaultAgent === 'dsh') {
|
|
2711
|
+
const presetMismatch = this.dshPresetMismatch(reader, sessionIdArg, cwd);
|
|
2712
|
+
if (presetMismatch) {
|
|
2713
|
+
return {
|
|
2714
|
+
text: `⚠️ 该 DSH session 的 preset 是「${presetMismatch.sessionPreset}」,与当前配置「${presetMismatch.currentPreset}」不一致。\n` +
|
|
2715
|
+
`preset 在 session 创建时固定,无法中途切换(DSH 返回 agent-preset-conflict)。\n` +
|
|
2716
|
+
`继续恢复将在下次 run 失败。请在 /config 中把预设模式改回「${presetMismatch.sessionPreset}」后重试,或保持当前配置并发新消息新建会话。`,
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2691
2720
|
// L3: 校验通过后才写入
|
|
2692
2721
|
this.sessionStore.setSessionId(ctx.userId, agentKind, sessionIdArg, cwd);
|
|
2693
2722
|
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionIdArg, cwd, {
|
|
2694
2723
|
agentKind,
|
|
2695
2724
|
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2696
2725
|
});
|
|
2697
|
-
//
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
}
|
|
2724
|
-
// Add usage stats at the end
|
|
2725
|
-
if (usage) {
|
|
2726
|
-
const usageStr = formatUsageStats(usage, { showResult: true, result: 'success' });
|
|
2727
|
-
elements.push(markdownDiv(usageStr));
|
|
2728
|
-
}
|
|
2729
|
-
if (elements.length > 0 && elements[elements.length - 1].tag === 'hr') {
|
|
2730
|
-
elements.pop();
|
|
2731
|
-
}
|
|
2732
|
-
// Add action buttons - use column_set+column for 2.0
|
|
2733
|
-
const buttons = [];
|
|
2734
|
-
// Stop button for active sessions.
|
|
2735
|
-
// isActive is only true when an in-memory activeRun exists, so
|
|
2736
|
-
// activeRunRunId is always set here. See buildAutoResumeCard for details.
|
|
2737
|
-
if (isActive) {
|
|
2738
|
-
buttons.push({
|
|
2739
|
-
tag: 'button',
|
|
2740
|
-
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
2741
|
-
type: 'danger',
|
|
2742
|
-
behaviors: [{ type: 'callback', value: { cmd: 'stop', runId: activeRunRunId, cwd } }],
|
|
2743
|
-
});
|
|
2744
|
-
}
|
|
2745
|
-
// Compact 按钮:仅 codex app-server 模式(runner 有 runCompact)且会话未在跑。
|
|
2746
|
-
// agentKind 来自 /resume [agent] <id> / resume.use 卡片值,非 defaultAgent。
|
|
2747
|
-
if (!isActive && this.canCompactSession(agentKind)) {
|
|
2748
|
-
buttons.push(resumeCompactButton(sessionIdArg, agentKind));
|
|
2749
|
-
}
|
|
2750
|
-
if (buttons.length > 0) {
|
|
2751
|
-
elements.push({
|
|
2752
|
-
tag: 'column_set',
|
|
2753
|
-
columns: buttons.map((btn) => ({
|
|
2754
|
-
tag: 'column',
|
|
2755
|
-
width: 'auto',
|
|
2756
|
-
elements: [btn],
|
|
2757
|
-
})),
|
|
2758
|
-
});
|
|
2759
|
-
}
|
|
2760
|
-
const card = {
|
|
2761
|
-
schema: '2.0',
|
|
2762
|
-
config: { wide_screen_mode: true },
|
|
2763
|
-
header: {
|
|
2764
|
-
title: {
|
|
2765
|
-
tag: 'plain_text',
|
|
2766
|
-
content: `${isActive ? '⏳ 恢复会话(完成中)' : '🔁 恢复会话'} · ${agentDisplayName(agentKind)}`,
|
|
2767
|
-
},
|
|
2768
|
-
},
|
|
2769
|
-
body: { elements },
|
|
2770
|
-
};
|
|
2726
|
+
// Action buttons: stop (active) + compact (capable runner, not running) + new session
|
|
2727
|
+
const actions = this.buildResumeActionButtons({
|
|
2728
|
+
isActive,
|
|
2729
|
+
activeRunRunId,
|
|
2730
|
+
cwd,
|
|
2731
|
+
compact: this.canCompactSession(agentKind, cwd)
|
|
2732
|
+
? { sessionId: sessionIdArg, agentKind }
|
|
2733
|
+
: undefined,
|
|
2734
|
+
newSession: true,
|
|
2735
|
+
});
|
|
2736
|
+
const card = buildSessionHistoryCard({
|
|
2737
|
+
sessionId: sessionIdArg,
|
|
2738
|
+
cwd,
|
|
2739
|
+
displayTitle,
|
|
2740
|
+
aiTitle,
|
|
2741
|
+
recap,
|
|
2742
|
+
events: content,
|
|
2743
|
+
usage,
|
|
2744
|
+
}, {
|
|
2745
|
+
agentKind,
|
|
2746
|
+
headerText: `📂 \`${cwd}\`\n会话: **${sessionIdArg}**`,
|
|
2747
|
+
title: `${isActive ? '⏳ 恢复会话(完成中)' : '🔁 恢复会话'} · ${agentDisplayName(agentKind)}`,
|
|
2748
|
+
usageResult: 'success',
|
|
2749
|
+
emptyPlaceholder: '_该会话暂无新消息可显示(最后一条为用户输入)_',
|
|
2750
|
+
actions,
|
|
2751
|
+
});
|
|
2771
2752
|
return { card };
|
|
2772
2753
|
}
|
|
2773
2754
|
// `/resume` or `/resume list` or `/resume <N>` — list sessions for the current cwd as a card
|
|
2774
2755
|
// Use agentKind to select the appropriate sessionReader
|
|
2775
2756
|
if (!cwd)
|
|
2776
2757
|
return { text: '请先 /cd 设置工作目录' };
|
|
2777
|
-
const pageSize =
|
|
2758
|
+
const pageSize = clampInt(limit ?? RESUME_PAGE_SIZE, 1, RESUME_PAGE_SIZE);
|
|
2778
2759
|
let reader;
|
|
2779
2760
|
// Use this.sessionReader when agentKind matches default, otherwise get from registry
|
|
2780
2761
|
if (agentKind === this.config.defaultAgent) {
|
|
@@ -2794,7 +2775,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2794
2775
|
// 这样越界 offset clamp 后 prev/next 仍按 pageSize 步长移动,不会错位。
|
|
2795
2776
|
const maxOffset = Math.max(0, Math.ceil(pageResult.total / pageSize) - 1) * pageSize;
|
|
2796
2777
|
if (pageOffset < 0 || pageOffset > maxOffset) {
|
|
2797
|
-
pageOffset =
|
|
2778
|
+
pageOffset = clampInt(pageOffset, 0, maxOffset);
|
|
2798
2779
|
pageResult = reader.listSessions(cwd, { limit: pageSize, offset: pageOffset });
|
|
2799
2780
|
}
|
|
2800
2781
|
}
|
|
@@ -2918,80 +2899,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2918
2899
|
if (total > pageSize) {
|
|
2919
2900
|
const totalPages = Math.ceil(total / pageSize);
|
|
2920
2901
|
const currentPage = Math.floor(pageOffset / pageSize) + 1;
|
|
2921
|
-
const
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
tag: 'column',
|
|
2929
|
-
width: 'auto',
|
|
2930
|
-
vertical_align: 'center',
|
|
2931
|
-
elements: [
|
|
2932
|
-
{
|
|
2933
|
-
tag: 'button',
|
|
2934
|
-
text: { tag: 'plain_text', content: '⬅ 上一页' },
|
|
2935
|
-
type: 'default',
|
|
2936
|
-
size: 'small',
|
|
2937
|
-
behaviors: [
|
|
2938
|
-
{
|
|
2939
|
-
type: 'callback',
|
|
2940
|
-
value: {
|
|
2941
|
-
cmd: 'resume.page',
|
|
2942
|
-
agent: agentKind,
|
|
2943
|
-
offset: pageOffset - pageSize,
|
|
2944
|
-
pageSize,
|
|
2945
|
-
},
|
|
2946
|
-
},
|
|
2947
|
-
],
|
|
2948
|
-
},
|
|
2949
|
-
],
|
|
2950
|
-
});
|
|
2951
|
-
}
|
|
2952
|
-
pageColumns.push({
|
|
2953
|
-
tag: 'column',
|
|
2954
|
-
width: 'weighted',
|
|
2955
|
-
weight: 1,
|
|
2956
|
-
vertical_align: 'center',
|
|
2957
|
-
elements: [
|
|
2958
|
-
{
|
|
2959
|
-
tag: 'div',
|
|
2960
|
-
text: {
|
|
2961
|
-
tag: 'lark_md',
|
|
2962
|
-
content: `第 ${currentPage}/${totalPages} 页 · 共 ${total} 个会话`,
|
|
2963
|
-
},
|
|
2964
|
-
},
|
|
2965
|
-
],
|
|
2902
|
+
const bar = paginationBar({
|
|
2903
|
+
cmd: 'resume.page',
|
|
2904
|
+
offset: pageOffset,
|
|
2905
|
+
pageSize,
|
|
2906
|
+
total,
|
|
2907
|
+
extra: { agent: agentKind, pageSize },
|
|
2908
|
+
label: `第 ${currentPage}/${totalPages} 页 · 共 ${total} 个会话`,
|
|
2966
2909
|
});
|
|
2967
|
-
if (hasNext) {
|
|
2968
|
-
pageColumns.push({
|
|
2969
|
-
tag: 'column',
|
|
2970
|
-
width: 'auto',
|
|
2971
|
-
vertical_align: 'center',
|
|
2972
|
-
elements: [
|
|
2973
|
-
{
|
|
2974
|
-
tag: 'button',
|
|
2975
|
-
text: { tag: 'plain_text', content: '下一页 ➡' },
|
|
2976
|
-
type: 'default',
|
|
2977
|
-
size: 'small',
|
|
2978
|
-
behaviors: [
|
|
2979
|
-
{
|
|
2980
|
-
type: 'callback',
|
|
2981
|
-
value: {
|
|
2982
|
-
cmd: 'resume.page',
|
|
2983
|
-
agent: agentKind,
|
|
2984
|
-
offset: pageOffset + pageSize,
|
|
2985
|
-
pageSize,
|
|
2986
|
-
},
|
|
2987
|
-
},
|
|
2988
|
-
],
|
|
2989
|
-
},
|
|
2990
|
-
],
|
|
2991
|
-
});
|
|
2992
|
-
}
|
|
2993
2910
|
elements.push({ tag: 'hr' });
|
|
2994
|
-
elements.push(
|
|
2911
|
+
elements.push(bar);
|
|
2995
2912
|
}
|
|
2996
2913
|
const card = {
|
|
2997
2914
|
schema: '2.0',
|
|
@@ -3159,6 +3076,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3159
3076
|
async cmdConfig(args, ctx) {
|
|
3160
3077
|
// Ensure probe cache is populated before building the card.
|
|
3161
3078
|
await probeAllAgents();
|
|
3079
|
+
// 动态清单预取(DSH 模型/预设目录):卡渲染前拉取,失败回退兜底清单
|
|
3080
|
+
await this.prefetchConfigCardCatalogs();
|
|
3162
3081
|
// Check if this is a direct set command: /config <key> <value>
|
|
3163
3082
|
// Command-style set: immediate write to disk, clear pendingConfig
|
|
3164
3083
|
if (args.length >= 2) {
|
|
@@ -3205,6 +3124,18 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3205
3124
|
this.ensurePendingConfig();
|
|
3206
3125
|
return this.buildConfigCard();
|
|
3207
3126
|
}
|
|
3127
|
+
/**
|
|
3128
|
+
* DSH /config 卡片构建前异步预取模型/预设目录(§4.2 动态清单)。
|
|
3129
|
+
* 只在 defaultAgent 为 dsh 时生效;其他 agent 无 prefetch 钩子直接跳过。
|
|
3130
|
+
* 预取失败不阻断——DshConfigBuilder 内部已回退固定兜底清单。
|
|
3131
|
+
*/
|
|
3132
|
+
async prefetchConfigCardCatalogs() {
|
|
3133
|
+
if (this.config.defaultAgent !== 'dsh')
|
|
3134
|
+
return;
|
|
3135
|
+
const builder = getConfigBuilder('dsh');
|
|
3136
|
+
const dshConf = getAgentConfig(this.config, 'dsh');
|
|
3137
|
+
await builder.prefetch?.(dshConf?.host);
|
|
3138
|
+
}
|
|
3208
3139
|
/** Build an interactive config card with CardKit 2.0 tabs + batch save */
|
|
3209
3140
|
buildConfigCard() {
|
|
3210
3141
|
// 使用 pendingConfig(若存在)或当前 config
|
|
@@ -3219,7 +3150,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3219
3150
|
// Direct inline construction (not via ConfigField) so that text (display label)
|
|
3220
3151
|
// and value (agentKind for config storage) can differ — uninstalled agents get
|
|
3221
3152
|
// a "⚠️ (未安装)" suffix in the label while the value stays clean.
|
|
3222
|
-
|
|
3153
|
+
// 固定展示顺序(Codex → Claude → OpenCode → Pi → Kimi),
|
|
3154
|
+
// 明确未安装的 agent 排到后面(getCachedAvailability === false)。
|
|
3155
|
+
const allAgents = sortAgentsForDisplay(listRegisteredAgents(), getCachedAvailability);
|
|
3223
3156
|
const agentOptions = allAgents.map((kind) => {
|
|
3224
3157
|
const available = getCachedAvailability(kind);
|
|
3225
3158
|
const label = available === false ? `${agentDisplayName(kind)} ⚠️ (未安装)` : agentDisplayName(kind);
|
|
@@ -3313,11 +3246,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3313
3246
|
}
|
|
3314
3247
|
return { card };
|
|
3315
3248
|
}
|
|
3249
|
+
/** /order 实现(public:测试直接调用,替代 as unknown as)。 */
|
|
3316
3250
|
cmdOrder(args, _ctx, offset = 0) {
|
|
3317
3251
|
const sub = args[0]?.toLowerCase();
|
|
3318
|
-
// /order — list orders (CardKit 2.0 with pagination)
|
|
3319
|
-
|
|
3252
|
+
// /order — list orders + aliases (CardKit 2.0 with pagination)
|
|
3253
|
+
// `/order alias`(无参数)也并入列表卡片。
|
|
3254
|
+
if (!sub || sub === 'list' || (sub === 'alias' && !args[1])) {
|
|
3320
3255
|
this.orderStore.reload();
|
|
3256
|
+
this.aliasStore.reload();
|
|
3321
3257
|
const allOrders = this.orderStore.get();
|
|
3322
3258
|
const elements = [];
|
|
3323
3259
|
if (allOrders.length === 0) {
|
|
@@ -3329,7 +3265,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3329
3265
|
const totalPages = Math.max(1, Math.ceil(totalCount / ORDER_PAGE_SIZE));
|
|
3330
3266
|
// Clamp offset to last page boundary to avoid empty pages
|
|
3331
3267
|
const maxOffset = Math.max(0, (totalPages - 1) * ORDER_PAGE_SIZE);
|
|
3332
|
-
const safeOffset =
|
|
3268
|
+
const safeOffset = clampInt(offset, 0, maxOffset);
|
|
3333
3269
|
const currentPage = Math.floor(safeOffset / ORDER_PAGE_SIZE) + 1;
|
|
3334
3270
|
const pageOrders = allOrders.slice(safeOffset, safeOffset + ORDER_PAGE_SIZE);
|
|
3335
3271
|
const hasPagination = totalCount > ORDER_PAGE_SIZE;
|
|
@@ -3384,74 +3320,31 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3384
3320
|
}
|
|
3385
3321
|
// Pagination bar (only shown when there are more items than ORDER_PAGE_SIZE)
|
|
3386
3322
|
if (hasPagination) {
|
|
3387
|
-
const
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
width: 'auto',
|
|
3394
|
-
vertical_align: 'center',
|
|
3395
|
-
elements: [
|
|
3396
|
-
{
|
|
3397
|
-
tag: 'button',
|
|
3398
|
-
text: { tag: 'plain_text', content: '⬅ 上一页' },
|
|
3399
|
-
type: 'default',
|
|
3400
|
-
size: 'small',
|
|
3401
|
-
behaviors: [
|
|
3402
|
-
{
|
|
3403
|
-
type: 'callback',
|
|
3404
|
-
value: {
|
|
3405
|
-
cmd: 'order.page',
|
|
3406
|
-
offset: safeOffset - ORDER_PAGE_SIZE,
|
|
3407
|
-
},
|
|
3408
|
-
},
|
|
3409
|
-
],
|
|
3410
|
-
},
|
|
3411
|
-
],
|
|
3412
|
-
});
|
|
3413
|
-
}
|
|
3414
|
-
pageColumns.push({
|
|
3415
|
-
tag: 'column',
|
|
3416
|
-
width: 'weighted',
|
|
3417
|
-
weight: 1,
|
|
3418
|
-
vertical_align: 'center',
|
|
3419
|
-
elements: [
|
|
3420
|
-
{
|
|
3421
|
-
tag: 'div',
|
|
3422
|
-
text: {
|
|
3423
|
-
tag: 'lark_md',
|
|
3424
|
-
content: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 条)`,
|
|
3425
|
-
},
|
|
3426
|
-
},
|
|
3427
|
-
],
|
|
3323
|
+
const bar = paginationBar({
|
|
3324
|
+
cmd: 'order.page',
|
|
3325
|
+
offset: safeOffset,
|
|
3326
|
+
pageSize: ORDER_PAGE_SIZE,
|
|
3327
|
+
total: totalCount,
|
|
3328
|
+
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 条)`,
|
|
3428
3329
|
});
|
|
3429
|
-
if (hasNext) {
|
|
3430
|
-
pageColumns.push({
|
|
3431
|
-
tag: 'column',
|
|
3432
|
-
width: 'auto',
|
|
3433
|
-
vertical_align: 'center',
|
|
3434
|
-
elements: [
|
|
3435
|
-
{
|
|
3436
|
-
tag: 'button',
|
|
3437
|
-
text: { tag: 'plain_text', content: '下一页 ➡' },
|
|
3438
|
-
type: 'default',
|
|
3439
|
-
size: 'small',
|
|
3440
|
-
behaviors: [
|
|
3441
|
-
{
|
|
3442
|
-
type: 'callback',
|
|
3443
|
-
value: {
|
|
3444
|
-
cmd: 'order.page',
|
|
3445
|
-
offset: safeOffset + ORDER_PAGE_SIZE,
|
|
3446
|
-
},
|
|
3447
|
-
},
|
|
3448
|
-
],
|
|
3449
|
-
},
|
|
3450
|
-
],
|
|
3451
|
-
});
|
|
3452
|
-
}
|
|
3453
3330
|
elements.push({ tag: 'hr' });
|
|
3454
|
-
elements.push(
|
|
3331
|
+
elements.push(bar);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
// 别名区:并入 /order 卡片(`/order alias` 列出全部)。
|
|
3335
|
+
const aliases = this.aliasStore.list();
|
|
3336
|
+
if (aliases.length > 0) {
|
|
3337
|
+
elements.push({ tag: 'hr' });
|
|
3338
|
+
elements.push({
|
|
3339
|
+
tag: 'div',
|
|
3340
|
+
text: { tag: 'lark_md', content: '**⚡ 别名**(输入 $name 触发)' },
|
|
3341
|
+
});
|
|
3342
|
+
for (const alias of aliases) {
|
|
3343
|
+
const text = alias.text.length > 100 ? alias.text.slice(0, 97) + '...' : alias.text;
|
|
3344
|
+
elements.push({
|
|
3345
|
+
tag: 'div',
|
|
3346
|
+
text: { tag: 'lark_md', content: `$${alias.name} → ${text}` },
|
|
3347
|
+
});
|
|
3455
3348
|
}
|
|
3456
3349
|
}
|
|
3457
3350
|
return {
|
|
@@ -3463,6 +3356,47 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3463
3356
|
},
|
|
3464
3357
|
};
|
|
3465
3358
|
}
|
|
3359
|
+
// /order alias <name> <text> — 注册;/order alias <name> — 查看;/order alias remove <name> — 删除
|
|
3360
|
+
if (sub === 'alias') {
|
|
3361
|
+
const name = args[1];
|
|
3362
|
+
if (name === 'remove') {
|
|
3363
|
+
const target = args[2];
|
|
3364
|
+
if (!target) {
|
|
3365
|
+
return { text: '用法: /order alias remove <name>' };
|
|
3366
|
+
}
|
|
3367
|
+
// remove 是保留子命令,不能作为别名名:后面跟多词文本说明用户想注册
|
|
3368
|
+
// 名为 remove 的别名,明确报错而不是静默删除已存在的 target(P2 review)。
|
|
3369
|
+
if (args.length > 3) {
|
|
3370
|
+
return { text: '`remove` 是保留子命令,不能作为别名名,请换一个名称' };
|
|
3371
|
+
}
|
|
3372
|
+
const entry = this.aliasStore.get(target);
|
|
3373
|
+
if (!entry) {
|
|
3374
|
+
return { text: `别名 $${target} 不存在` };
|
|
3375
|
+
}
|
|
3376
|
+
this.aliasStore.remove(target);
|
|
3377
|
+
// 回显原内容:单词场景无法区分「注册 remove」与「删除」,误删后
|
|
3378
|
+
// 用户可直接按回显内容重新注册,消除静默破坏(P3 review)。
|
|
3379
|
+
const preview = entry.text.length > 50 ? entry.text.slice(0, 50) + '...' : entry.text;
|
|
3380
|
+
return { text: `✅ 已删除别名 $${target}(原内容:${preview})` };
|
|
3381
|
+
}
|
|
3382
|
+
const text = args.slice(2).join(' ');
|
|
3383
|
+
if (!text) {
|
|
3384
|
+
const entry = name ? this.aliasStore.get(name) : undefined;
|
|
3385
|
+
if (!entry) {
|
|
3386
|
+
return { text: name ? `别名 $${name} 不存在` : '用法: /order alias <name> <text>' };
|
|
3387
|
+
}
|
|
3388
|
+
return { text: `$${entry.name} → ${entry.text}` };
|
|
3389
|
+
}
|
|
3390
|
+
try {
|
|
3391
|
+
const existed = this.aliasStore.has(name);
|
|
3392
|
+
this.aliasStore.set(name, text);
|
|
3393
|
+
const preview = text.length > 50 ? text.slice(0, 50) + '...' : text;
|
|
3394
|
+
return { text: `${existed ? '✅ 已更新别名' : '✅ 已保存别名'} $${name} → ${preview}` };
|
|
3395
|
+
}
|
|
3396
|
+
catch (err) {
|
|
3397
|
+
return { text: `保存失败: ${err.message}` };
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3466
3400
|
// /order save <text>
|
|
3467
3401
|
if (sub === 'save') {
|
|
3468
3402
|
const text = args.slice(1).join(' ');
|
|
@@ -3477,10 +3411,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3477
3411
|
return { text: `保存失败: ${err.message}` };
|
|
3478
3412
|
}
|
|
3479
3413
|
}
|
|
3480
|
-
return { text: '用法: /order [list|save <text>]' };
|
|
3414
|
+
return { text: '用法: /order [list|save <text>|alias <name> <text>|alias remove <name>]' };
|
|
3481
3415
|
}
|
|
3482
3416
|
}
|
|
3483
|
-
// --- Re-exported from ./utils.js (moved for decomposition) ---
|
|
3484
|
-
// formatUsageStats is shared by run-renderer and bridge via ../router/index.js.
|
|
3485
|
-
export { formatUsageStats } from './utils.js';
|
|
3486
3417
|
//# sourceMappingURL=index.js.map
|