lark-remote 0.1.5 → 0.1.7
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 +39 -0
- package/README.en.md +5 -1
- package/README.md +10 -2
- package/dist/bridge/approval-coordinator.d.ts +131 -0
- package/dist/bridge/approval-coordinator.js +409 -0
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +138 -5
- package/dist/bridge/index.js +569 -65
- package/dist/bridge/queue-manager.d.ts +11 -1
- package/dist/bridge/queue-manager.js +20 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +363 -0
- 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 +13 -10
- package/dist/card/card-shared.js +27 -24
- package/dist/card/run-card-session.d.ts +2 -55
- package/dist/card/run-card-session.js +6 -104
- package/dist/card/run-renderer.js +140 -202
- package/dist/card/run-state.d.ts +38 -3
- package/dist/card/run-state.js +221 -11
- 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/dir.js +1 -1
- package/dist/config/index.d.ts +116 -9
- package/dist/config/index.js +122 -29
- package/dist/connector/index.d.ts +74 -2
- package/dist/connector/index.js +119 -1
- package/dist/index.js +139 -99
- 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-helpers.d.ts +63 -0
- package/dist/router/card-helpers.js +146 -0
- package/dist/router/config/claude.js +25 -1
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +33 -5
- 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 +9 -4
- package/dist/router/config/index.d.ts +12 -0
- package/dist/router/config/index.js +25 -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 +12 -3
- package/dist/router/index.d.ts +77 -17
- package/dist/router/index.js +455 -549
- package/dist/router/utils.d.ts +4 -0
- package/dist/router/utils.js +9 -2
- package/dist/runner/agent-choices-common.d.ts +16 -0
- package/dist/runner/agent-choices-common.js +31 -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 +749 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
- package/dist/runner/codex/app-server/protocol-types.js +56 -0
- package/dist/runner/codex/app-server/runner.d.ts +104 -0
- package/dist/runner/codex/app-server/runner.js +377 -0
- package/dist/runner/codex/app-server/translator.d.ts +89 -0
- package/dist/runner/codex/app-server/translator.js +716 -0
- package/dist/runner/codex/index.d.ts +1 -1
- package/dist/runner/codex/index.js +1 -1
- 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 +238 -0
- package/dist/runner/common/acp/protocol-types.js +75 -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/common/jsonrpc/client.d.ts +89 -0
- package/dist/runner/common/jsonrpc/client.js +243 -0
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
- package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
- package/dist/runner/common/jsonrpc/transport.js +230 -0
- 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/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +5 -5
- package/dist/runner/index.js +4 -4
- package/dist/runner/kimi/acp/runner.d.ts +106 -0
- package/dist/runner/kimi/acp/runner.js +448 -0
- package/dist/runner/kimi/acp/translator.d.ts +124 -0
- package/dist/runner/kimi/acp/translator.js +336 -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 +80 -0
- package/dist/runner/pi/rpc/runner.js +199 -0
- package/dist/runner/pi/rpc/translator.d.ts +55 -0
- package/dist/runner/pi/rpc/translator.js +258 -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 +153 -2
- package/dist/session/claude/session-index.js +2 -3
- package/dist/session/claude/session-reader.js +3 -6
- package/dist/session/claude/sessions.js +6 -13
- package/dist/session/codex/rollout-reader.d.ts +2 -2
- package/dist/session/codex/rollout-reader.js +57 -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/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/package.json +1 -1
- package/dist/runner/codex/argv.d.ts +0 -33
- package/dist/runner/codex/argv.js +0 -40
- 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 -63
- package/dist/runner/codex/runner.js +0 -124
- 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, mapAgentKey, assertSafeKeyPart, } from '../config/index.js';
|
|
5
|
+
import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgentKey, assertSafeKeyPart, walkNestedContainer, } 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
|
/**
|
|
@@ -45,7 +62,7 @@ const RESUME_CONTENT_PREFETCH = 5;
|
|
|
45
62
|
* `New Session`、空串),都不是真实输入,不得渲染成行标题。
|
|
46
63
|
*/
|
|
47
64
|
const RESUME_SUMMARY_PLACEHOLDERS = new Set(['', '(no user message)', '(无摘要)', 'New Session']);
|
|
48
|
-
import { newSessionButton, agentDisplayName } from '../card/card-shared.js';
|
|
65
|
+
import { newSessionButton, resumeCompactButton, agentDisplayName } from '../card/card-shared.js';
|
|
49
66
|
/**
|
|
50
67
|
* Card-action commands that must run with enqueue({ immediate: true }).
|
|
51
68
|
* These bypass the serial queue and execute immediately.
|
|
@@ -96,17 +113,29 @@ export function isImmediateAction(cmd) {
|
|
|
96
113
|
cmd === 'config.toggle' ||
|
|
97
114
|
cmd === 'config.set' ||
|
|
98
115
|
cmd === 'config.input' ||
|
|
99
|
-
cmd === 'config.save'
|
|
116
|
+
cmd === 'config.save' ||
|
|
117
|
+
// 审批响应/权限切换必须即时触达在途 run(同 stop 类控制动作)。若走串行
|
|
118
|
+
// 队列会排在等待审批的 run 之后形成死锁:run 不结束审批不执行,run 结束
|
|
119
|
+
// coordinator 已删响应空转(线上复现:approval.respond 排队卡、审批永不生效)。
|
|
120
|
+
cmd === 'approval.respond' ||
|
|
121
|
+
cmd === 'approval.toggle' ||
|
|
122
|
+
cmd === 'approval.answer' ||
|
|
123
|
+
cmd === 'approval.answerSubmit' ||
|
|
124
|
+
cmd === 'approval.answerCustom');
|
|
100
125
|
}
|
|
101
126
|
export class CommandRouter {
|
|
102
127
|
/** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
|
|
103
128
|
static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
104
129
|
sessionStore;
|
|
130
|
+
/** 飞书桥接实例(public:测试直接访问断言,替代 as unknown as)。 */
|
|
105
131
|
bridge;
|
|
132
|
+
/** 当前配置(public:测试直接读取断言,替代 as unknown as)。 */
|
|
106
133
|
config;
|
|
134
|
+
/** 配置文件的绝对路径(public:/config 保存链路与测试共用)。 */
|
|
107
135
|
configPath;
|
|
108
136
|
workspaceStore;
|
|
109
137
|
orderStore;
|
|
138
|
+
aliasStore;
|
|
110
139
|
exitHandler;
|
|
111
140
|
pendingExit = false;
|
|
112
141
|
/**
|
|
@@ -121,7 +150,8 @@ export class CommandRouter {
|
|
|
121
150
|
updateCachePath;
|
|
122
151
|
/** Injected update functions (for testability; defaults to real implementation). */
|
|
123
152
|
updateFns;
|
|
124
|
-
|
|
153
|
+
/** /config 卡片编辑暂存区(public:测试直接读取断言,替代 as unknown as)。 */
|
|
154
|
+
pendingConfig = null;
|
|
125
155
|
/**
|
|
126
156
|
* Monotonic counter minting unique internal keys for order.exec enqueue
|
|
127
157
|
* actions. One order card can be clicked many times, so the Feishu card
|
|
@@ -142,6 +172,7 @@ export class CommandRouter {
|
|
|
142
172
|
* it uses `listRegisteredAgents()` from `router/config/index.ts` (the
|
|
143
173
|
* config-builder registry), which mirrors the agents with a config card.
|
|
144
174
|
*/
|
|
175
|
+
/** Session reader 注册中心(public:测试注入/读取,替代 as unknown as)。 */
|
|
145
176
|
sessionReaderRegistry;
|
|
146
177
|
/**
|
|
147
178
|
* 串行化 config.* 卡片回调(2026-07-04)。
|
|
@@ -159,6 +190,7 @@ export class CommandRouter {
|
|
|
159
190
|
this.configPath = opts.configPath;
|
|
160
191
|
this.workspaceStore = new WorkspaceStore(opts.workspacePath);
|
|
161
192
|
this.orderStore = new OrderStore(opts.ordersPath);
|
|
193
|
+
this.aliasStore = new AliasStore(opts.aliasesPath);
|
|
162
194
|
this.exitHandler = opts.exitHandler ?? (() => process.exit(0));
|
|
163
195
|
this.restartSpawner = opts.restartSpawner;
|
|
164
196
|
// idle watchdog 窗口从 config.idle.watchdogMinutes 读取
|
|
@@ -197,6 +229,8 @@ export class CommandRouter {
|
|
|
197
229
|
await this.bridge.sendResult(result, ctx);
|
|
198
230
|
}
|
|
199
231
|
if (this.pendingExit) {
|
|
232
|
+
// 干净退出前冲刷待合批的媒体保存提示,避免 500ms 窗口内的提示丢失。
|
|
233
|
+
await this.bridge.flushAllMediaNotifications();
|
|
200
234
|
this.exitHandler();
|
|
201
235
|
}
|
|
202
236
|
return result;
|
|
@@ -217,6 +251,13 @@ export class CommandRouter {
|
|
|
217
251
|
await this.bridge.forwardToClaude(trimmed, ctx, opts);
|
|
218
252
|
return null;
|
|
219
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* 一次别名展开:bridge 收到用户消息后、命令分发前调用。
|
|
256
|
+
* 仅 `$name` 开头的消息可能被展开;未知别名 / 非 `$` 消息原样返回。
|
|
257
|
+
*/
|
|
258
|
+
expandAliasMessage(message) {
|
|
259
|
+
return resolveAlias(message, this.aliasStore.list()) ?? message;
|
|
260
|
+
}
|
|
220
261
|
/**
|
|
221
262
|
* Handle a CardKit card button click (§6.2).
|
|
222
263
|
* Validates payload and dispatches to the corresponding command.
|
|
@@ -237,12 +278,12 @@ export class CommandRouter {
|
|
|
237
278
|
return;
|
|
238
279
|
case 'ls.page':
|
|
239
280
|
return this.handleLsPage(value, ctx);
|
|
281
|
+
case 'ws.page':
|
|
282
|
+
return this.handleWsPage(value, ctx);
|
|
240
283
|
case 'ws.use':
|
|
241
284
|
return await this.handleWsUse(value, ctx);
|
|
242
285
|
case 'ws.sort':
|
|
243
286
|
return await this.handleWsSort(value, ctx);
|
|
244
|
-
case 'ws.page':
|
|
245
|
-
return await this.handleWsPage(value, ctx);
|
|
246
287
|
case 'ws.remove':
|
|
247
288
|
return await this.handleWsRemove(value, ctx);
|
|
248
289
|
case 'resume.use': {
|
|
@@ -319,6 +360,18 @@ export class CommandRouter {
|
|
|
319
360
|
// 调 updateCardInPlace → Feishu API 乱序到达导致 toggle 卡死。
|
|
320
361
|
// 2026-07-18: 返回 enqueueConfigAction 的结果以支持 toast 响应
|
|
321
362
|
return this.enqueueConfigAction(value, ctx);
|
|
363
|
+
case 'approval.respond':
|
|
364
|
+
case 'approval.toggle':
|
|
365
|
+
case 'approval.answer':
|
|
366
|
+
case 'approval.answerSubmit':
|
|
367
|
+
case 'approval.answerCustom':
|
|
368
|
+
return this.handleApprovalAction(value, ctx);
|
|
369
|
+
case 'codex.compact':
|
|
370
|
+
await this.bridge.handleCodexCompact(value, ctx);
|
|
371
|
+
return;
|
|
372
|
+
case 'resume.compact':
|
|
373
|
+
await this.bridge.handleResumeCompact(value, ctx);
|
|
374
|
+
return;
|
|
322
375
|
default: {
|
|
323
376
|
// Help card buttons: help.<cmd> → execute /<cmd>
|
|
324
377
|
if (value.cmd.startsWith('help.')) {
|
|
@@ -332,6 +385,7 @@ export class CommandRouter {
|
|
|
332
385
|
// 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
|
|
333
386
|
// 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
|
|
334
387
|
if (this.pendingExit) {
|
|
388
|
+
await this.bridge.flushAllMediaNotifications();
|
|
335
389
|
this.exitHandler();
|
|
336
390
|
}
|
|
337
391
|
return;
|
|
@@ -344,6 +398,119 @@ export class CommandRouter {
|
|
|
344
398
|
}
|
|
345
399
|
}
|
|
346
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Handle approval-related card actions.
|
|
403
|
+
* Routes to the appropriate bridge method based on value.cmd.
|
|
404
|
+
* Returns a toast response for immediate user feedback.
|
|
405
|
+
*/
|
|
406
|
+
async handleApprovalAction(value, _ctx) {
|
|
407
|
+
const { runId, requestId, decision, nonce, permId } = value;
|
|
408
|
+
if (value.cmd === 'approval.respond') {
|
|
409
|
+
if (!runId || requestId === undefined || !decision || !nonce) {
|
|
410
|
+
return { toast: { type: 'error', content: '缺少审批响应参数' } };
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
await this.bridge.handleApprovalRespond({
|
|
414
|
+
runId,
|
|
415
|
+
requestId,
|
|
416
|
+
decision,
|
|
417
|
+
nonce,
|
|
418
|
+
});
|
|
419
|
+
return { toast: { type: 'success', content: '审批已提交' } };
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
423
|
+
// 审批已过期:给用户明确反馈,不静默不误导(2026-08-12 事故:点了允许无任何反馈)。
|
|
424
|
+
const content = /state=expired/.test(msg)
|
|
425
|
+
? '⏰ 审批已过期,无法响应'
|
|
426
|
+
: `审批响应失败:${msg}`;
|
|
427
|
+
return { toast: { type: 'error', content } };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (value.cmd === 'approval.toggle') {
|
|
431
|
+
if (!runId || requestId === undefined || !permId) {
|
|
432
|
+
return { toast: { type: 'error', content: '缺少权限切换参数' } };
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
await this.bridge.handleApprovalToggle({
|
|
436
|
+
runId,
|
|
437
|
+
requestId,
|
|
438
|
+
permId,
|
|
439
|
+
selected: value.selected ?? true,
|
|
440
|
+
});
|
|
441
|
+
return { toast: { type: 'info', content: '已切换' } };
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
445
|
+
return { toast: { type: 'error', content: `权限切换失败:${msg}` } };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (value.cmd === 'approval.answer') {
|
|
449
|
+
if (!runId ||
|
|
450
|
+
requestId === undefined ||
|
|
451
|
+
value.questionIndex === undefined ||
|
|
452
|
+
!value.option ||
|
|
453
|
+
!nonce) {
|
|
454
|
+
return { toast: { type: 'error', content: '缺少问题答案参数' } };
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
await this.bridge.handleApprovalAnswer({
|
|
458
|
+
runId,
|
|
459
|
+
requestId,
|
|
460
|
+
questionIndex: value.questionIndex,
|
|
461
|
+
option: value.option,
|
|
462
|
+
nonce,
|
|
463
|
+
});
|
|
464
|
+
return { toast: { type: 'success', content: '已选择' } };
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
468
|
+
return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (value.cmd === 'approval.answerSubmit') {
|
|
472
|
+
if (!runId || requestId === undefined || value.questionIndex === undefined || !nonce) {
|
|
473
|
+
return { toast: { type: 'error', content: '缺少提交参数' } };
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
await this.bridge.handleApprovalAnswerSubmit({
|
|
477
|
+
runId,
|
|
478
|
+
requestId,
|
|
479
|
+
questionIndex: value.questionIndex,
|
|
480
|
+
nonce,
|
|
481
|
+
});
|
|
482
|
+
return { toast: { type: 'success', content: '答案已提交' } };
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
486
|
+
return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (value.cmd === 'approval.answerCustom') {
|
|
490
|
+
if (!runId ||
|
|
491
|
+
requestId === undefined ||
|
|
492
|
+
value.questionIndex === undefined ||
|
|
493
|
+
!nonce ||
|
|
494
|
+
!value.inputValue) {
|
|
495
|
+
return { toast: { type: 'error', content: '缺少答案文本参数' } };
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
await this.bridge.handleApprovalAnswerCustom({
|
|
499
|
+
runId,
|
|
500
|
+
requestId,
|
|
501
|
+
questionIndex: value.questionIndex,
|
|
502
|
+
text: value.inputValue,
|
|
503
|
+
nonce,
|
|
504
|
+
});
|
|
505
|
+
return { toast: { type: 'success', content: '答案已提交' } };
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
509
|
+
return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return { toast: { type: 'error', content: '未知的审批操作' } };
|
|
513
|
+
}
|
|
347
514
|
/**
|
|
348
515
|
* Handle queue.cancel: remove this message from the queue.
|
|
349
516
|
*/
|
|
@@ -823,17 +990,7 @@ export class CommandRouter {
|
|
|
823
990
|
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
824
991
|
? resumeAgent
|
|
825
992
|
: this.config.defaultAgent;
|
|
826
|
-
const
|
|
827
|
-
const pageSize = Number.isFinite(rawPageSize)
|
|
828
|
-
? Math.min(Math.max(Math.trunc(rawPageSize), 1), RESUME_PAGE_SIZE)
|
|
829
|
-
: RESUME_PAGE_SIZE;
|
|
830
|
-
// Normalize the callback offset to a finite non-negative integer, then
|
|
831
|
-
// align it to a page boundary so prev/next always step by pageSize.
|
|
832
|
-
// Non-numeric payloads (e.g. 'abc') must not produce NaN slices that look
|
|
833
|
-
// like an empty directory.
|
|
834
|
-
const rawOffset = Number(value.offset);
|
|
835
|
-
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
|
|
836
|
-
const alignedOffset = Math.floor(offset / pageSize) * pageSize;
|
|
993
|
+
const { pageSize, alignedOffset } = normalizePageArgs(value, RESUME_PAGE_SIZE);
|
|
837
994
|
const entry = this.sessionStore.get(ctx.userId);
|
|
838
995
|
const cwd = entry?.cwd;
|
|
839
996
|
if (!cwd) {
|
|
@@ -860,9 +1017,7 @@ export class CommandRouter {
|
|
|
860
1017
|
* Mirrors handleResumePage — updates the same card, never sends a new one.
|
|
861
1018
|
*/
|
|
862
1019
|
async handleActivePage(value, ctx) {
|
|
863
|
-
const
|
|
864
|
-
const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.trunc(rawOffset)) : 0;
|
|
865
|
-
const alignedOffset = Math.floor(offset / ACTIVE_PAGE_SIZE) * ACTIVE_PAGE_SIZE;
|
|
1020
|
+
const { alignedOffset } = normalizePageArgs(value, ACTIVE_PAGE_SIZE);
|
|
866
1021
|
const result = this.cmdActive([], ctx, alignedOffset);
|
|
867
1022
|
if (!result.card) {
|
|
868
1023
|
return { toast: { type: 'error', content: '当前没有正在进行中的任务' } };
|
|
@@ -984,9 +1139,19 @@ export class CommandRouter {
|
|
|
984
1139
|
if (useResult.text && !useResult.text.includes('已切换')) {
|
|
985
1140
|
return { toast: { type: 'error', content: useResult.text } };
|
|
986
1141
|
}
|
|
987
|
-
// Success:
|
|
988
|
-
|
|
989
|
-
|
|
1142
|
+
// Success: send auto-resume card, or a persistent text confirmation when
|
|
1143
|
+
// there is no history session. Toast alone is insufficient: for this
|
|
1144
|
+
// immediate action the callback response is swallowed by enqueueImmediate
|
|
1145
|
+
// (fire-and-forget), and even if delivered it is transient — the user gets
|
|
1146
|
+
// no perceivable feedback on the switch. Same rationale as config.save
|
|
1147
|
+
// agent-switch notice (74e99f0): persistent message first, toast only as a
|
|
1148
|
+
// fallback if sendResult fails.
|
|
1149
|
+
let fallbackToast;
|
|
1150
|
+
if (useResult.card || useResult.text) {
|
|
1151
|
+
const sent = await this.bridge.sendResult(useResult, ctx);
|
|
1152
|
+
if (!sent && useResult.text) {
|
|
1153
|
+
fallbackToast = useResult.text;
|
|
1154
|
+
}
|
|
990
1155
|
}
|
|
991
1156
|
// Refresh the /ws list card in place so "recent" sort is immediately visible
|
|
992
1157
|
const currentOffset = Math.max(0, Math.trunc(Number(value.offset) || 0));
|
|
@@ -994,11 +1159,11 @@ export class CommandRouter {
|
|
|
994
1159
|
if (refreshed.card) {
|
|
995
1160
|
await this.bridge.updateCardInPlace(refreshed.card, ctx);
|
|
996
1161
|
}
|
|
997
|
-
// Toast feedback (suppress text when
|
|
1162
|
+
// Toast feedback (suppress text when a persistent message was already sent)
|
|
998
1163
|
return {
|
|
999
1164
|
toast: {
|
|
1000
1165
|
type: 'success',
|
|
1001
|
-
content:
|
|
1166
|
+
content: fallbackToast ?? '',
|
|
1002
1167
|
},
|
|
1003
1168
|
};
|
|
1004
1169
|
}
|
|
@@ -1111,6 +1276,7 @@ export class CommandRouter {
|
|
|
1111
1276
|
return next;
|
|
1112
1277
|
}
|
|
1113
1278
|
/** 实际的 config.* 分发逻辑(在串行队列内执行) */
|
|
1279
|
+
/** config.* 卡片动作分发(public:测试直接调用,替代 as unknown as)。 */
|
|
1114
1280
|
async dispatchConfigAction(value, ctx) {
|
|
1115
1281
|
switch (value.cmd) {
|
|
1116
1282
|
case 'config.toggle': {
|
|
@@ -1239,37 +1405,29 @@ export class CommandRouter {
|
|
|
1239
1405
|
}
|
|
1240
1406
|
}
|
|
1241
1407
|
/** 设置嵌套属性值(仅修改内存对象,不写盘) */
|
|
1408
|
+
/** 在 pendingConfig 上按 dot-separated key 设置嵌套值(config 卡片编辑用)。 */
|
|
1242
1409
|
setNestedValue(target, key, value) {
|
|
1243
1410
|
// Path mapping 共用 config 模块的 mapAgentKey(G11 Inconsistency 修复):
|
|
1244
1411
|
// pi.xxx/codex.xxx/opencode.xxx → agents.xxx,claude.xxx 保持顶层
|
|
1245
1412
|
const mappedKey = mapAgentKey(key);
|
|
1246
1413
|
const parts = mappedKey.split('.');
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
// `current['__proto__']` 污染 Object.prototype(文本直写路径已守卫,
|
|
1253
|
-
// 卡片路径是同一威胁模型的另一入口)。
|
|
1254
|
-
assertSafeKeyPart(parts[i]);
|
|
1255
|
-
const next = current[parts[i]];
|
|
1256
|
-
if (next == null || typeof next !== 'object') {
|
|
1257
|
-
current[parts[i]] = {};
|
|
1258
|
-
}
|
|
1259
|
-
current = current[parts[i]];
|
|
1260
|
-
}
|
|
1261
|
-
assertSafeKeyPart(parts[parts.length - 1]);
|
|
1414
|
+
// 复用 config 模块的 walkNestedContainer(含 __proto__/prototype/constructor
|
|
1415
|
+
// 守卫与中间段补 {}),router 只保留 value=undefined → delete 分支。
|
|
1416
|
+
const container = walkNestedContainer(target, parts, true);
|
|
1417
|
+
const lastPart = parts[parts.length - 1];
|
|
1418
|
+
assertSafeKeyPart(lastPart);
|
|
1262
1419
|
if (value === undefined) {
|
|
1263
1420
|
// value=undefined 表示"删除键"(如清空 reasoningEffort),
|
|
1264
1421
|
// 不能写成 undefined 值——diffConfig 会把 undefined 转成字面量 "undefined"
|
|
1265
1422
|
// 写入 config.yaml 并透传给 codex(ReasoningEffort::Custom("undefined"))。
|
|
1266
|
-
delete
|
|
1423
|
+
delete container[lastPart];
|
|
1267
1424
|
}
|
|
1268
1425
|
else {
|
|
1269
|
-
|
|
1426
|
+
container[lastPart] = value;
|
|
1270
1427
|
}
|
|
1271
1428
|
}
|
|
1272
1429
|
/** 对比原始 config 与 pendingConfig,返回变化的 key→string 映射 */
|
|
1430
|
+
/** config diff(public:测试直接调用,替代 as unknown as)。 */
|
|
1273
1431
|
diffConfig(original, pending) {
|
|
1274
1432
|
const updates = {};
|
|
1275
1433
|
this.collectDiff('', original, pending, updates);
|
|
@@ -1405,6 +1563,27 @@ export class CommandRouter {
|
|
|
1405
1563
|
atomicWrite(this.configPath, YAML.stringify(this.config));
|
|
1406
1564
|
this.bridge.setConfig(this.config);
|
|
1407
1565
|
}
|
|
1566
|
+
// P1-6 + live approval settings (§P5): active workspace-lifetime runners
|
|
1567
|
+
// are not evicted by clearRunners() (would orphan the live subprocess).
|
|
1568
|
+
// Push the new approval-mode settings to those runners via the unified
|
|
1569
|
+
// duck method `updateApprovalMode` (codex thread/settings/update,
|
|
1570
|
+
// kimi/opencode session/set_mode) so they take effect for subsequent
|
|
1571
|
+
// turns without waiting for the runner to be recreated.
|
|
1572
|
+
const hasApprovalConfigChange = Object.keys(updates).some((k) => k === 'agents.codex' ||
|
|
1573
|
+
k.startsWith('agents.codex.') ||
|
|
1574
|
+
k === 'codex' ||
|
|
1575
|
+
k.startsWith('codex.') ||
|
|
1576
|
+
k === 'agents.kimi' ||
|
|
1577
|
+
k.startsWith('agents.kimi.') ||
|
|
1578
|
+
k === 'kimi' ||
|
|
1579
|
+
k.startsWith('kimi.') ||
|
|
1580
|
+
k === 'agents.opencode' ||
|
|
1581
|
+
k.startsWith('agents.opencode.') ||
|
|
1582
|
+
k === 'opencode' ||
|
|
1583
|
+
k.startsWith('opencode.'));
|
|
1584
|
+
if (hasApprovalConfigChange) {
|
|
1585
|
+
this.bridge.syncActiveApprovalModes();
|
|
1586
|
+
}
|
|
1408
1587
|
// 2026-07-13: 任何 agent 配置变更或 defaultAgent 切换,都清除 runner 缓存
|
|
1409
1588
|
if (hasDefaultAgentChange || hasAgentConfigChange) {
|
|
1410
1589
|
this.bridge.clearRunners();
|
|
@@ -1485,7 +1664,11 @@ export class CommandRouter {
|
|
|
1485
1664
|
{ cmd: 'cd', label: '/cd <path>', desc: '切换工作目录' },
|
|
1486
1665
|
{ cmd: 'ls', label: '/ls [dir]', desc: '列出当前目录,可指定子目录' },
|
|
1487
1666
|
{ cmd: 'resume', label: '/resume /r [agent] [list|id|N]', desc: '列出或切换 Agent session' },
|
|
1488
|
-
{
|
|
1667
|
+
{
|
|
1668
|
+
cmd: 'order',
|
|
1669
|
+
label: '/order /o save|list',
|
|
1670
|
+
desc: '收藏指令 /order save,快捷别名 /order alias',
|
|
1671
|
+
},
|
|
1489
1672
|
];
|
|
1490
1673
|
const bodyElements = [];
|
|
1491
1674
|
// 按钮组 — CardKit 2.0: column_set + column + button with behaviors
|
|
@@ -1566,6 +1749,7 @@ export class CommandRouter {
|
|
|
1566
1749
|
},
|
|
1567
1750
|
};
|
|
1568
1751
|
}
|
|
1752
|
+
/** /status 实现(public:测试直接调用,替代 as any)。 */
|
|
1569
1753
|
cmdStatus(ctx) {
|
|
1570
1754
|
const entry = this.sessionStore.get(ctx.userId);
|
|
1571
1755
|
const cwd = entry?.cwd ?? '(未设置)';
|
|
@@ -1667,8 +1851,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1667
1851
|
* /update: check for newer version and upgrade if available.
|
|
1668
1852
|
* /update check: only check, don't upgrade.
|
|
1669
1853
|
*
|
|
1670
|
-
* Flow: check version → if newer, install →
|
|
1671
|
-
* Reuses /restart's restartSpawner + pendingExit mechanism.
|
|
1854
|
+
* Flow: check version → if newer, install → report success (no auto-restart).
|
|
1672
1855
|
*/
|
|
1673
1856
|
async cmdUpdate(args, ctx) {
|
|
1674
1857
|
const checkOnly = args[0] === 'check';
|
|
@@ -1702,21 +1885,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1702
1885
|
if (!installResult.success) {
|
|
1703
1886
|
return { text: `❌ 升级失败: ${installResult.error}` };
|
|
1704
1887
|
}
|
|
1705
|
-
// 4.
|
|
1706
|
-
|
|
1707
|
-
return { text: `✅ 已升级到 ${latest},请手动 /restart 重启 bridge` };
|
|
1708
|
-
}
|
|
1709
|
-
try {
|
|
1710
|
-
const pid = this.restartSpawner();
|
|
1711
|
-
this.pendingExit = true;
|
|
1712
|
-
return { text: `✅ 升级成功 (${current} → ${latest}),正在重启(新进程 pid ${pid})…` };
|
|
1713
|
-
}
|
|
1714
|
-
catch (err) {
|
|
1715
|
-
getLogger().error('[router] update restart spawn failed:', err);
|
|
1716
|
-
return {
|
|
1717
|
-
text: `✅ 已升级到 ${latest},但重启失败:${err.message},请手动 /restart`,
|
|
1718
|
-
};
|
|
1719
|
-
}
|
|
1888
|
+
// 4. Report success (no auto-restart — user decides when to /restart)
|
|
1889
|
+
return { text: `✅ 升级成功 (${current} → ${latest}),发送 /restart 重启后生效` };
|
|
1720
1890
|
}
|
|
1721
1891
|
cmdNew(ctx) {
|
|
1722
1892
|
// 只清 sessionId 保留 cwd — 否则 /new 之后 /resume 会提示"请先 /cd
|
|
@@ -1825,79 +1995,30 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1825
1995
|
buildAutoResumeCard(cwd, session, _ctx) {
|
|
1826
1996
|
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
1827
1997
|
const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(session.sessionId, cwd, { maxEvents: AUTO_RESUME_MAX_EVENTS });
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const recapPreview = recap.length > 200 ? recap.slice(0, 197) + '...' : recap;
|
|
1837
|
-
sections.push(`📝 **Recap**\n${recapPreview}`);
|
|
1838
|
-
}
|
|
1839
|
-
if (sections.length > 0) {
|
|
1840
|
-
header += '\n\n' + sections.join('\n\n──\n\n');
|
|
1841
|
-
}
|
|
1842
|
-
const elements = [markdownDiv(header), { tag: 'hr' }];
|
|
1843
|
-
// Fold session history into collapsible panels: the last 2 events stay
|
|
1844
|
-
// expanded so the user sees the most recent context without clicking;
|
|
1845
|
-
// older events are collapsed to keep the card compact.
|
|
1846
|
-
content.forEach((ev, i) => {
|
|
1847
|
-
elements.push(sessionEventPanel(ev, i, content.length, 2, this.config.defaultAgent));
|
|
1998
|
+
const actions = this.buildResumeActionButtons({
|
|
1999
|
+
isActive,
|
|
2000
|
+
activeRunRunId,
|
|
2001
|
+
cwd,
|
|
2002
|
+
compact: this.canCompactSession(this.config.defaultAgent, cwd)
|
|
2003
|
+
? { sessionId: session.sessionId, agentKind: this.config.defaultAgent }
|
|
2004
|
+
: undefined,
|
|
2005
|
+
newSession: true,
|
|
1848
2006
|
});
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
// non-terminal, which guarantees activeRunRunId is set). Bind the REAL
|
|
1864
|
-
// activeRun.runId (UUID) so bridge.interruptCurrentRun can match and
|
|
1865
|
-
// stop a live run.
|
|
1866
|
-
if (isActive) {
|
|
1867
|
-
actions.push({
|
|
1868
|
-
tag: 'button',
|
|
1869
|
-
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
1870
|
-
type: 'danger',
|
|
1871
|
-
behaviors: [
|
|
1872
|
-
{
|
|
1873
|
-
type: 'callback',
|
|
1874
|
-
value: { cmd: 'stop', runId: activeRunRunId, cwd },
|
|
1875
|
-
},
|
|
1876
|
-
],
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
// New session button always shown (use shared function for consistency)
|
|
1880
|
-
actions.push(newSessionButton());
|
|
1881
|
-
// Use column_set+column for 2.0 (action tag not supported in 2.0)
|
|
1882
|
-
elements.push({
|
|
1883
|
-
tag: 'column_set',
|
|
1884
|
-
columns: actions.map((btn) => ({
|
|
1885
|
-
tag: 'column',
|
|
1886
|
-
width: 'auto',
|
|
1887
|
-
elements: [btn],
|
|
1888
|
-
})),
|
|
2007
|
+
const card = buildSessionHistoryCard({
|
|
2008
|
+
sessionId: session.sessionId,
|
|
2009
|
+
cwd,
|
|
2010
|
+
displayTitle,
|
|
2011
|
+
aiTitle,
|
|
2012
|
+
recap,
|
|
2013
|
+
events: content,
|
|
2014
|
+
usage,
|
|
2015
|
+
}, {
|
|
2016
|
+
agentKind: this.config.defaultAgent,
|
|
2017
|
+
headerText: `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`,
|
|
2018
|
+
title: `${isActive ? '⏳ 自动恢复会话(完成中)' : '🔁 自动恢复会话'} · ${agentName}`,
|
|
2019
|
+
usageResult: 'success',
|
|
2020
|
+
actions,
|
|
1889
2021
|
});
|
|
1890
|
-
const card = {
|
|
1891
|
-
schema: '2.0',
|
|
1892
|
-
config: { wide_screen_mode: true },
|
|
1893
|
-
header: {
|
|
1894
|
-
title: {
|
|
1895
|
-
tag: 'plain_text',
|
|
1896
|
-
content: `${isActive ? '⏳ 自动恢复会话(完成中)' : '🔁 自动恢复会话'} · ${agentName}`,
|
|
1897
|
-
},
|
|
1898
|
-
},
|
|
1899
|
-
body: { elements },
|
|
1900
|
-
};
|
|
1901
2022
|
return { card };
|
|
1902
2023
|
}
|
|
1903
2024
|
/**
|
|
@@ -1911,20 +2032,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1911
2032
|
const agentName = agentDisplayName(newAgent);
|
|
1912
2033
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
1913
2034
|
const sessionId = switchResult.sessionId;
|
|
1914
|
-
// No cwd or no session → compact notice card
|
|
2035
|
+
// No cwd or no session → compact notice card (no new-session button: the
|
|
2036
|
+
// next message already starts a fresh session, a button would mislead
|
|
2037
|
+
// users into thinking a click is required)
|
|
1915
2038
|
if (!cwd || !sessionId) {
|
|
1916
2039
|
const elements = [markdownDiv(switchResult.notice)];
|
|
1917
|
-
// New session button
|
|
1918
|
-
elements.push({
|
|
1919
|
-
tag: 'column_set',
|
|
1920
|
-
columns: [
|
|
1921
|
-
{
|
|
1922
|
-
tag: 'column',
|
|
1923
|
-
width: 'auto',
|
|
1924
|
-
elements: [newSessionButton()],
|
|
1925
|
-
},
|
|
1926
|
-
],
|
|
1927
|
-
});
|
|
1928
2040
|
return {
|
|
1929
2041
|
card: {
|
|
1930
2042
|
schema: '2.0',
|
|
@@ -1968,69 +2080,79 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1968
2080
|
agentKind: newAgent,
|
|
1969
2081
|
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
1970
2082
|
});
|
|
1971
|
-
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2083
|
+
// Action buttons: stop (active) + new session
|
|
2084
|
+
const actions = this.buildResumeActionButtons({
|
|
2085
|
+
isActive,
|
|
2086
|
+
activeRunRunId,
|
|
2087
|
+
cwd,
|
|
2088
|
+
newSession: true,
|
|
2089
|
+
});
|
|
2090
|
+
const card = buildSessionHistoryCard({
|
|
2091
|
+
sessionId,
|
|
2092
|
+
cwd,
|
|
2093
|
+
displayTitle,
|
|
2094
|
+
aiTitle,
|
|
2095
|
+
recap,
|
|
2096
|
+
events: content,
|
|
2097
|
+
usage,
|
|
2098
|
+
}, {
|
|
2099
|
+
agentKind: newAgent,
|
|
2100
|
+
headerText: `📂 \`${cwd}\`\n已恢复会话: **${sessionId}**`,
|
|
2101
|
+
title: `${isActive ? '⏳ 切换 Agent(完成中)' : '🔀 切换 Agent'} · ${agentName}`,
|
|
2102
|
+
usageResult: 'success',
|
|
2103
|
+
emptyPlaceholder: '_该会话暂无新消息可显示(最后一条为用户输入)_',
|
|
2104
|
+
actions,
|
|
2105
|
+
});
|
|
2106
|
+
return { card };
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Whether a session of the given agent kind can be compacted from a resume
|
|
2110
|
+
* card. Design doc §6.2-2: duck-typing — check if the runner has `runCompact`
|
|
2111
|
+
* (codex app-server, kimi acp), instead of hardcoding agentKind === 'codex'.
|
|
2112
|
+
*/
|
|
2113
|
+
canCompactSession(agentKind, cwd) {
|
|
2114
|
+
if (!cwd)
|
|
2115
|
+
return false;
|
|
2116
|
+
return this.bridge.hasRunCompact(cwd, agentKind);
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Assemble the action buttons for a resume / session-history card.
|
|
2120
|
+
* Shared by buildAutoResumeCard, buildConfigSwitchCard, and cmdResume so the
|
|
2121
|
+
* button set (stop / compact / new-session) follows one contract instead of
|
|
2122
|
+
* drifting per call site.
|
|
2123
|
+
*
|
|
2124
|
+
* - stop: only when the session has an in-memory active run (isActive), bound
|
|
2125
|
+
* to the real activeRun.runId so bridge.interruptCurrentRun can match.
|
|
2126
|
+
* - compact: only when a compact-capable runner applies and the session is
|
|
2127
|
+
* not running (a live run would occupy the runner, so compaction would
|
|
2128
|
+
* fail). The caller passes `compact` only when canCompactSession holds.
|
|
2129
|
+
* - new-session: when requested (always shown on resume cards).
|
|
2130
|
+
*/
|
|
2131
|
+
buildResumeActionButtons(opts) {
|
|
2001
2132
|
const actions = [];
|
|
2002
|
-
if (isActive) {
|
|
2133
|
+
if (opts.isActive) {
|
|
2003
2134
|
actions.push({
|
|
2004
2135
|
tag: 'button',
|
|
2005
2136
|
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
2006
2137
|
type: 'danger',
|
|
2007
|
-
behaviors: [
|
|
2138
|
+
behaviors: [
|
|
2139
|
+
{
|
|
2140
|
+
type: 'callback',
|
|
2141
|
+
value: { cmd: 'stop', runId: opts.activeRunRunId, cwd: opts.cwd },
|
|
2142
|
+
},
|
|
2143
|
+
],
|
|
2008
2144
|
});
|
|
2009
2145
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
})),
|
|
2018
|
-
});
|
|
2019
|
-
return {
|
|
2020
|
-
card: {
|
|
2021
|
-
schema: '2.0',
|
|
2022
|
-
config: { wide_screen_mode: true },
|
|
2023
|
-
header: {
|
|
2024
|
-
title: {
|
|
2025
|
-
tag: 'plain_text',
|
|
2026
|
-
content: `${isActive ? '⏳ 切换 Agent(完成中)' : '🔀 切换 Agent'} · ${agentName}`,
|
|
2027
|
-
},
|
|
2028
|
-
},
|
|
2029
|
-
body: { elements },
|
|
2030
|
-
},
|
|
2031
|
-
};
|
|
2146
|
+
if (opts.compact && !opts.isActive) {
|
|
2147
|
+
actions.push(resumeCompactButton(opts.compact.sessionId, opts.compact.agentKind));
|
|
2148
|
+
}
|
|
2149
|
+
if (opts.newSession) {
|
|
2150
|
+
actions.push(newSessionButton());
|
|
2151
|
+
}
|
|
2152
|
+
return actions;
|
|
2032
2153
|
}
|
|
2033
2154
|
static LS_PAGE_SIZE = 30;
|
|
2155
|
+
/** /ls 实现(public:测试直接调用,替代 as unknown as)。 */
|
|
2034
2156
|
cmdLs(args, ctx, offset = 0) {
|
|
2035
2157
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
2036
2158
|
if (!cwd) {
|
|
@@ -2214,78 +2336,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2214
2336
|
}
|
|
2215
2337
|
// Pagination bar (only shown when there are more items than PAGE_SIZE)
|
|
2216
2338
|
if (hasPagination) {
|
|
2217
|
-
const
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
tag: 'column',
|
|
2225
|
-
width: 'auto',
|
|
2226
|
-
vertical_align: 'center',
|
|
2227
|
-
elements: [
|
|
2228
|
-
{
|
|
2229
|
-
tag: 'button',
|
|
2230
|
-
text: { tag: 'plain_text', content: '⬅ 上一页' },
|
|
2231
|
-
type: 'default',
|
|
2232
|
-
size: 'small',
|
|
2233
|
-
behaviors: [
|
|
2234
|
-
{
|
|
2235
|
-
type: 'callback',
|
|
2236
|
-
value: {
|
|
2237
|
-
cmd: 'ls.page',
|
|
2238
|
-
path: targetDir,
|
|
2239
|
-
offset: offset - CommandRouter.LS_PAGE_SIZE,
|
|
2240
|
-
},
|
|
2241
|
-
},
|
|
2242
|
-
],
|
|
2243
|
-
},
|
|
2244
|
-
],
|
|
2245
|
-
});
|
|
2246
|
-
}
|
|
2247
|
-
pageColumns.push({
|
|
2248
|
-
tag: 'column',
|
|
2249
|
-
width: 'weighted',
|
|
2250
|
-
weight: 1,
|
|
2251
|
-
vertical_align: 'center',
|
|
2252
|
-
elements: [
|
|
2253
|
-
{
|
|
2254
|
-
tag: 'div',
|
|
2255
|
-
text: {
|
|
2256
|
-
tag: 'lark_md',
|
|
2257
|
-
content: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 项)`,
|
|
2258
|
-
},
|
|
2259
|
-
},
|
|
2260
|
-
],
|
|
2339
|
+
const bar = paginationBar({
|
|
2340
|
+
cmd: 'ls.page',
|
|
2341
|
+
offset,
|
|
2342
|
+
pageSize: CommandRouter.LS_PAGE_SIZE,
|
|
2343
|
+
total: totalCount,
|
|
2344
|
+
extra: { path: targetDir },
|
|
2345
|
+
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 项)`,
|
|
2261
2346
|
});
|
|
2262
|
-
if (hasNext) {
|
|
2263
|
-
pageColumns.push({
|
|
2264
|
-
tag: 'column',
|
|
2265
|
-
width: 'auto',
|
|
2266
|
-
vertical_align: 'center',
|
|
2267
|
-
elements: [
|
|
2268
|
-
{
|
|
2269
|
-
tag: 'button',
|
|
2270
|
-
text: { tag: 'plain_text', content: '下一页 ➡' },
|
|
2271
|
-
type: 'default',
|
|
2272
|
-
size: 'small',
|
|
2273
|
-
behaviors: [
|
|
2274
|
-
{
|
|
2275
|
-
type: 'callback',
|
|
2276
|
-
value: {
|
|
2277
|
-
cmd: 'ls.page',
|
|
2278
|
-
path: targetDir,
|
|
2279
|
-
offset: offset + CommandRouter.LS_PAGE_SIZE,
|
|
2280
|
-
},
|
|
2281
|
-
},
|
|
2282
|
-
],
|
|
2283
|
-
},
|
|
2284
|
-
],
|
|
2285
|
-
});
|
|
2286
|
-
}
|
|
2287
2347
|
elements.push({ tag: 'hr' });
|
|
2288
|
-
elements.push(
|
|
2348
|
+
elements.push(bar);
|
|
2289
2349
|
}
|
|
2290
2350
|
return {
|
|
2291
2351
|
card: {
|
|
@@ -2375,7 +2435,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2375
2435
|
const totalCount = entries.length;
|
|
2376
2436
|
const totalPages = Math.max(1, Math.ceil(totalCount / WS_PAGE_SIZE));
|
|
2377
2437
|
const maxOffset = Math.max(0, (totalPages - 1) * WS_PAGE_SIZE);
|
|
2378
|
-
const safeOffset =
|
|
2438
|
+
const safeOffset = clampInt(offset, 0, maxOffset);
|
|
2379
2439
|
const currentPage = Math.floor(safeOffset / WS_PAGE_SIZE) + 1;
|
|
2380
2440
|
const pageEntries = entries.slice(safeOffset, safeOffset + WS_PAGE_SIZE);
|
|
2381
2441
|
const hasPagination = totalCount > WS_PAGE_SIZE;
|
|
@@ -2485,73 +2545,22 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2485
2545
|
// Pagination bar — separate row below the list (not crowded with sort
|
|
2486
2546
|
// toggle; mobile-friendly vertical layout).
|
|
2487
2547
|
if (hasPagination) {
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
elements: [
|
|
2497
|
-
{
|
|
2498
|
-
tag: 'button',
|
|
2499
|
-
text: { tag: 'plain_text', content: '⬅' },
|
|
2500
|
-
type: 'default',
|
|
2501
|
-
size: 'small',
|
|
2502
|
-
behaviors: [
|
|
2503
|
-
{
|
|
2504
|
-
type: 'callback',
|
|
2505
|
-
value: { cmd: 'ws.page', offset: safeOffset - WS_PAGE_SIZE },
|
|
2506
|
-
},
|
|
2507
|
-
],
|
|
2508
|
-
},
|
|
2509
|
-
],
|
|
2510
|
-
});
|
|
2511
|
-
}
|
|
2512
|
-
pageColumns.push({
|
|
2513
|
-
tag: 'column',
|
|
2514
|
-
width: 'weighted',
|
|
2515
|
-
weight: 1,
|
|
2516
|
-
vertical_align: 'center',
|
|
2517
|
-
elements: [
|
|
2518
|
-
{
|
|
2519
|
-
tag: 'div',
|
|
2520
|
-
text: {
|
|
2521
|
-
tag: 'lark_md',
|
|
2522
|
-
content: `**${currentPage}/${totalPages}**(${totalCount})`,
|
|
2523
|
-
},
|
|
2524
|
-
},
|
|
2525
|
-
],
|
|
2548
|
+
const bar = paginationBar({
|
|
2549
|
+
cmd: 'ws.page',
|
|
2550
|
+
offset: safeOffset,
|
|
2551
|
+
pageSize: WS_PAGE_SIZE,
|
|
2552
|
+
total: totalCount,
|
|
2553
|
+
label: `**${currentPage}/${totalPages}**(${totalCount})`,
|
|
2554
|
+
prevText: '⬅',
|
|
2555
|
+
nextText: '➡',
|
|
2526
2556
|
});
|
|
2527
|
-
if (hasNext) {
|
|
2528
|
-
pageColumns.push({
|
|
2529
|
-
tag: 'column',
|
|
2530
|
-
width: 'auto',
|
|
2531
|
-
vertical_align: 'center',
|
|
2532
|
-
elements: [
|
|
2533
|
-
{
|
|
2534
|
-
tag: 'button',
|
|
2535
|
-
text: { tag: 'plain_text', content: '➡' },
|
|
2536
|
-
type: 'default',
|
|
2537
|
-
size: 'small',
|
|
2538
|
-
behaviors: [
|
|
2539
|
-
{
|
|
2540
|
-
type: 'callback',
|
|
2541
|
-
value: { cmd: 'ws.page', offset: safeOffset + WS_PAGE_SIZE },
|
|
2542
|
-
},
|
|
2543
|
-
],
|
|
2544
|
-
},
|
|
2545
|
-
],
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
2557
|
bodyElements.push({ tag: 'hr' });
|
|
2549
|
-
bodyElements.push(
|
|
2558
|
+
bodyElements.push(bar);
|
|
2550
2559
|
}
|
|
2551
2560
|
}
|
|
2552
2561
|
const card = {
|
|
2553
2562
|
schema: '2.0',
|
|
2554
|
-
config: { wide_screen_mode: true },
|
|
2563
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
2555
2564
|
header: { title: { tag: 'plain_text', content: 'Workspaces' } },
|
|
2556
2565
|
body: { elements: bodyElements },
|
|
2557
2566
|
};
|
|
@@ -2566,6 +2575,10 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2566
2575
|
* misleading "未找到". Reuses the same card-builder helpers (markdownDiv) as
|
|
2567
2576
|
* the normal resume card; enforceCardBudget still applies at send time.
|
|
2568
2577
|
*/
|
|
2578
|
+
/**
|
|
2579
|
+
* /resume 核心实现(public:handleResumePage / handle / resume.use 共用,
|
|
2580
|
+
* 也是测试的直接 seam,避免测试用 @ts-expect-error 摸私有方法)。
|
|
2581
|
+
*/
|
|
2569
2582
|
cmdResume(args, ctx, offset = 0) {
|
|
2570
2583
|
const entry = this.sessionStore.get(ctx.userId);
|
|
2571
2584
|
const cwd = entry?.cwd;
|
|
@@ -2618,82 +2631,39 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2618
2631
|
agentKind,
|
|
2619
2632
|
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2620
2633
|
});
|
|
2621
|
-
//
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
}
|
|
2648
|
-
// Add usage stats at the end
|
|
2649
|
-
if (usage) {
|
|
2650
|
-
const usageStr = formatUsageStats(usage, { showResult: true, result: 'success' });
|
|
2651
|
-
elements.push(markdownDiv(usageStr));
|
|
2652
|
-
}
|
|
2653
|
-
if (elements.length > 0 && elements[elements.length - 1].tag === 'hr') {
|
|
2654
|
-
elements.pop();
|
|
2655
|
-
}
|
|
2656
|
-
// Add action buttons - use column_set+column for 2.0
|
|
2657
|
-
const buttons = [];
|
|
2658
|
-
// Stop button for active sessions.
|
|
2659
|
-
// isActive is only true when an in-memory activeRun exists, so
|
|
2660
|
-
// activeRunRunId is always set here. See buildAutoResumeCard for details.
|
|
2661
|
-
if (isActive) {
|
|
2662
|
-
buttons.push({
|
|
2663
|
-
tag: 'button',
|
|
2664
|
-
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
2665
|
-
type: 'danger',
|
|
2666
|
-
behaviors: [{ type: 'callback', value: { cmd: 'stop', runId: activeRunRunId, cwd } }],
|
|
2667
|
-
});
|
|
2668
|
-
}
|
|
2669
|
-
if (buttons.length > 0) {
|
|
2670
|
-
elements.push({
|
|
2671
|
-
tag: 'column_set',
|
|
2672
|
-
columns: buttons.map((btn) => ({
|
|
2673
|
-
tag: 'column',
|
|
2674
|
-
width: 'auto',
|
|
2675
|
-
elements: [btn],
|
|
2676
|
-
})),
|
|
2677
|
-
});
|
|
2678
|
-
}
|
|
2679
|
-
const card = {
|
|
2680
|
-
schema: '2.0',
|
|
2681
|
-
config: { wide_screen_mode: true },
|
|
2682
|
-
header: {
|
|
2683
|
-
title: {
|
|
2684
|
-
tag: 'plain_text',
|
|
2685
|
-
content: `${isActive ? '⏳ 恢复会话(完成中)' : '🔁 恢复会话'} · ${agentDisplayName(agentKind)}`,
|
|
2686
|
-
},
|
|
2687
|
-
},
|
|
2688
|
-
body: { elements },
|
|
2689
|
-
};
|
|
2634
|
+
// Action buttons: stop (active) + compact (capable runner, not running) + new session
|
|
2635
|
+
const actions = this.buildResumeActionButtons({
|
|
2636
|
+
isActive,
|
|
2637
|
+
activeRunRunId,
|
|
2638
|
+
cwd,
|
|
2639
|
+
compact: this.canCompactSession(agentKind, cwd)
|
|
2640
|
+
? { sessionId: sessionIdArg, agentKind }
|
|
2641
|
+
: undefined,
|
|
2642
|
+
newSession: true,
|
|
2643
|
+
});
|
|
2644
|
+
const card = buildSessionHistoryCard({
|
|
2645
|
+
sessionId: sessionIdArg,
|
|
2646
|
+
cwd,
|
|
2647
|
+
displayTitle,
|
|
2648
|
+
aiTitle,
|
|
2649
|
+
recap,
|
|
2650
|
+
events: content,
|
|
2651
|
+
usage,
|
|
2652
|
+
}, {
|
|
2653
|
+
agentKind,
|
|
2654
|
+
headerText: `📂 \`${cwd}\`\n会话: **${sessionIdArg}**`,
|
|
2655
|
+
title: `${isActive ? '⏳ 恢复会话(完成中)' : '🔁 恢复会话'} · ${agentDisplayName(agentKind)}`,
|
|
2656
|
+
usageResult: 'success',
|
|
2657
|
+
emptyPlaceholder: '_该会话暂无新消息可显示(最后一条为用户输入)_',
|
|
2658
|
+
actions,
|
|
2659
|
+
});
|
|
2690
2660
|
return { card };
|
|
2691
2661
|
}
|
|
2692
2662
|
// `/resume` or `/resume list` or `/resume <N>` — list sessions for the current cwd as a card
|
|
2693
2663
|
// Use agentKind to select the appropriate sessionReader
|
|
2694
2664
|
if (!cwd)
|
|
2695
2665
|
return { text: '请先 /cd 设置工作目录' };
|
|
2696
|
-
const pageSize =
|
|
2666
|
+
const pageSize = clampInt(limit ?? RESUME_PAGE_SIZE, 1, RESUME_PAGE_SIZE);
|
|
2697
2667
|
let reader;
|
|
2698
2668
|
// Use this.sessionReader when agentKind matches default, otherwise get from registry
|
|
2699
2669
|
if (agentKind === this.config.defaultAgent) {
|
|
@@ -2713,7 +2683,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2713
2683
|
// 这样越界 offset clamp 后 prev/next 仍按 pageSize 步长移动,不会错位。
|
|
2714
2684
|
const maxOffset = Math.max(0, Math.ceil(pageResult.total / pageSize) - 1) * pageSize;
|
|
2715
2685
|
if (pageOffset < 0 || pageOffset > maxOffset) {
|
|
2716
|
-
pageOffset =
|
|
2686
|
+
pageOffset = clampInt(pageOffset, 0, maxOffset);
|
|
2717
2687
|
pageResult = reader.listSessions(cwd, { limit: pageSize, offset: pageOffset });
|
|
2718
2688
|
}
|
|
2719
2689
|
}
|
|
@@ -2837,80 +2807,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2837
2807
|
if (total > pageSize) {
|
|
2838
2808
|
const totalPages = Math.ceil(total / pageSize);
|
|
2839
2809
|
const currentPage = Math.floor(pageOffset / pageSize) + 1;
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
tag: 'column',
|
|
2848
|
-
width: 'auto',
|
|
2849
|
-
vertical_align: 'center',
|
|
2850
|
-
elements: [
|
|
2851
|
-
{
|
|
2852
|
-
tag: 'button',
|
|
2853
|
-
text: { tag: 'plain_text', content: '⬅ 上一页' },
|
|
2854
|
-
type: 'default',
|
|
2855
|
-
size: 'small',
|
|
2856
|
-
behaviors: [
|
|
2857
|
-
{
|
|
2858
|
-
type: 'callback',
|
|
2859
|
-
value: {
|
|
2860
|
-
cmd: 'resume.page',
|
|
2861
|
-
agent: agentKind,
|
|
2862
|
-
offset: pageOffset - pageSize,
|
|
2863
|
-
pageSize,
|
|
2864
|
-
},
|
|
2865
|
-
},
|
|
2866
|
-
],
|
|
2867
|
-
},
|
|
2868
|
-
],
|
|
2869
|
-
});
|
|
2870
|
-
}
|
|
2871
|
-
pageColumns.push({
|
|
2872
|
-
tag: 'column',
|
|
2873
|
-
width: 'weighted',
|
|
2874
|
-
weight: 1,
|
|
2875
|
-
vertical_align: 'center',
|
|
2876
|
-
elements: [
|
|
2877
|
-
{
|
|
2878
|
-
tag: 'div',
|
|
2879
|
-
text: {
|
|
2880
|
-
tag: 'lark_md',
|
|
2881
|
-
content: `第 ${currentPage}/${totalPages} 页 · 共 ${total} 个会话`,
|
|
2882
|
-
},
|
|
2883
|
-
},
|
|
2884
|
-
],
|
|
2810
|
+
const bar = paginationBar({
|
|
2811
|
+
cmd: 'resume.page',
|
|
2812
|
+
offset: pageOffset,
|
|
2813
|
+
pageSize,
|
|
2814
|
+
total,
|
|
2815
|
+
extra: { agent: agentKind, pageSize },
|
|
2816
|
+
label: `第 ${currentPage}/${totalPages} 页 · 共 ${total} 个会话`,
|
|
2885
2817
|
});
|
|
2886
|
-
if (hasNext) {
|
|
2887
|
-
pageColumns.push({
|
|
2888
|
-
tag: 'column',
|
|
2889
|
-
width: 'auto',
|
|
2890
|
-
vertical_align: 'center',
|
|
2891
|
-
elements: [
|
|
2892
|
-
{
|
|
2893
|
-
tag: 'button',
|
|
2894
|
-
text: { tag: 'plain_text', content: '下一页 ➡' },
|
|
2895
|
-
type: 'default',
|
|
2896
|
-
size: 'small',
|
|
2897
|
-
behaviors: [
|
|
2898
|
-
{
|
|
2899
|
-
type: 'callback',
|
|
2900
|
-
value: {
|
|
2901
|
-
cmd: 'resume.page',
|
|
2902
|
-
agent: agentKind,
|
|
2903
|
-
offset: pageOffset + pageSize,
|
|
2904
|
-
pageSize,
|
|
2905
|
-
},
|
|
2906
|
-
},
|
|
2907
|
-
],
|
|
2908
|
-
},
|
|
2909
|
-
],
|
|
2910
|
-
});
|
|
2911
|
-
}
|
|
2912
2818
|
elements.push({ tag: 'hr' });
|
|
2913
|
-
elements.push(
|
|
2819
|
+
elements.push(bar);
|
|
2914
2820
|
}
|
|
2915
2821
|
const card = {
|
|
2916
2822
|
schema: '2.0',
|
|
@@ -3138,7 +3044,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3138
3044
|
// Direct inline construction (not via ConfigField) so that text (display label)
|
|
3139
3045
|
// and value (agentKind for config storage) can differ — uninstalled agents get
|
|
3140
3046
|
// a "⚠️ (未安装)" suffix in the label while the value stays clean.
|
|
3141
|
-
|
|
3047
|
+
// 固定展示顺序(Codex → Claude → OpenCode → Pi → Kimi),
|
|
3048
|
+
// 明确未安装的 agent 排到后面(getCachedAvailability === false)。
|
|
3049
|
+
const allAgents = sortAgentsForDisplay(listRegisteredAgents(), getCachedAvailability);
|
|
3142
3050
|
const agentOptions = allAgents.map((kind) => {
|
|
3143
3051
|
const available = getCachedAvailability(kind);
|
|
3144
3052
|
const label = available === false ? `${agentDisplayName(kind)} ⚠️ (未安装)` : agentDisplayName(kind);
|
|
@@ -3232,11 +3140,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3232
3140
|
}
|
|
3233
3141
|
return { card };
|
|
3234
3142
|
}
|
|
3143
|
+
/** /order 实现(public:测试直接调用,替代 as unknown as)。 */
|
|
3235
3144
|
cmdOrder(args, _ctx, offset = 0) {
|
|
3236
3145
|
const sub = args[0]?.toLowerCase();
|
|
3237
|
-
// /order — list orders (CardKit 2.0 with pagination)
|
|
3238
|
-
|
|
3146
|
+
// /order — list orders + aliases (CardKit 2.0 with pagination)
|
|
3147
|
+
// `/order alias`(无参数)也并入列表卡片。
|
|
3148
|
+
if (!sub || sub === 'list' || (sub === 'alias' && !args[1])) {
|
|
3239
3149
|
this.orderStore.reload();
|
|
3150
|
+
this.aliasStore.reload();
|
|
3240
3151
|
const allOrders = this.orderStore.get();
|
|
3241
3152
|
const elements = [];
|
|
3242
3153
|
if (allOrders.length === 0) {
|
|
@@ -3248,7 +3159,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3248
3159
|
const totalPages = Math.max(1, Math.ceil(totalCount / ORDER_PAGE_SIZE));
|
|
3249
3160
|
// Clamp offset to last page boundary to avoid empty pages
|
|
3250
3161
|
const maxOffset = Math.max(0, (totalPages - 1) * ORDER_PAGE_SIZE);
|
|
3251
|
-
const safeOffset =
|
|
3162
|
+
const safeOffset = clampInt(offset, 0, maxOffset);
|
|
3252
3163
|
const currentPage = Math.floor(safeOffset / ORDER_PAGE_SIZE) + 1;
|
|
3253
3164
|
const pageOrders = allOrders.slice(safeOffset, safeOffset + ORDER_PAGE_SIZE);
|
|
3254
3165
|
const hasPagination = totalCount > ORDER_PAGE_SIZE;
|
|
@@ -3303,74 +3214,31 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3303
3214
|
}
|
|
3304
3215
|
// Pagination bar (only shown when there are more items than ORDER_PAGE_SIZE)
|
|
3305
3216
|
if (hasPagination) {
|
|
3306
|
-
const
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
width: 'auto',
|
|
3313
|
-
vertical_align: 'center',
|
|
3314
|
-
elements: [
|
|
3315
|
-
{
|
|
3316
|
-
tag: 'button',
|
|
3317
|
-
text: { tag: 'plain_text', content: '⬅ 上一页' },
|
|
3318
|
-
type: 'default',
|
|
3319
|
-
size: 'small',
|
|
3320
|
-
behaviors: [
|
|
3321
|
-
{
|
|
3322
|
-
type: 'callback',
|
|
3323
|
-
value: {
|
|
3324
|
-
cmd: 'order.page',
|
|
3325
|
-
offset: safeOffset - ORDER_PAGE_SIZE,
|
|
3326
|
-
},
|
|
3327
|
-
},
|
|
3328
|
-
],
|
|
3329
|
-
},
|
|
3330
|
-
],
|
|
3331
|
-
});
|
|
3332
|
-
}
|
|
3333
|
-
pageColumns.push({
|
|
3334
|
-
tag: 'column',
|
|
3335
|
-
width: 'weighted',
|
|
3336
|
-
weight: 1,
|
|
3337
|
-
vertical_align: 'center',
|
|
3338
|
-
elements: [
|
|
3339
|
-
{
|
|
3340
|
-
tag: 'div',
|
|
3341
|
-
text: {
|
|
3342
|
-
tag: 'lark_md',
|
|
3343
|
-
content: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 条)`,
|
|
3344
|
-
},
|
|
3345
|
-
},
|
|
3346
|
-
],
|
|
3217
|
+
const bar = paginationBar({
|
|
3218
|
+
cmd: 'order.page',
|
|
3219
|
+
offset: safeOffset,
|
|
3220
|
+
pageSize: ORDER_PAGE_SIZE,
|
|
3221
|
+
total: totalCount,
|
|
3222
|
+
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 条)`,
|
|
3347
3223
|
});
|
|
3348
|
-
if (hasNext) {
|
|
3349
|
-
pageColumns.push({
|
|
3350
|
-
tag: 'column',
|
|
3351
|
-
width: 'auto',
|
|
3352
|
-
vertical_align: 'center',
|
|
3353
|
-
elements: [
|
|
3354
|
-
{
|
|
3355
|
-
tag: 'button',
|
|
3356
|
-
text: { tag: 'plain_text', content: '下一页 ➡' },
|
|
3357
|
-
type: 'default',
|
|
3358
|
-
size: 'small',
|
|
3359
|
-
behaviors: [
|
|
3360
|
-
{
|
|
3361
|
-
type: 'callback',
|
|
3362
|
-
value: {
|
|
3363
|
-
cmd: 'order.page',
|
|
3364
|
-
offset: safeOffset + ORDER_PAGE_SIZE,
|
|
3365
|
-
},
|
|
3366
|
-
},
|
|
3367
|
-
],
|
|
3368
|
-
},
|
|
3369
|
-
],
|
|
3370
|
-
});
|
|
3371
|
-
}
|
|
3372
3224
|
elements.push({ tag: 'hr' });
|
|
3373
|
-
elements.push(
|
|
3225
|
+
elements.push(bar);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
// 别名区:并入 /order 卡片(`/order alias` 列出全部)。
|
|
3229
|
+
const aliases = this.aliasStore.list();
|
|
3230
|
+
if (aliases.length > 0) {
|
|
3231
|
+
elements.push({ tag: 'hr' });
|
|
3232
|
+
elements.push({
|
|
3233
|
+
tag: 'div',
|
|
3234
|
+
text: { tag: 'lark_md', content: '**⚡ 别名**(输入 $name 触发)' },
|
|
3235
|
+
});
|
|
3236
|
+
for (const alias of aliases) {
|
|
3237
|
+
const text = alias.text.length > 100 ? alias.text.slice(0, 97) + '...' : alias.text;
|
|
3238
|
+
elements.push({
|
|
3239
|
+
tag: 'div',
|
|
3240
|
+
text: { tag: 'lark_md', content: `$${alias.name} → ${text}` },
|
|
3241
|
+
});
|
|
3374
3242
|
}
|
|
3375
3243
|
}
|
|
3376
3244
|
return {
|
|
@@ -3382,6 +3250,47 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3382
3250
|
},
|
|
3383
3251
|
};
|
|
3384
3252
|
}
|
|
3253
|
+
// /order alias <name> <text> — 注册;/order alias <name> — 查看;/order alias remove <name> — 删除
|
|
3254
|
+
if (sub === 'alias') {
|
|
3255
|
+
const name = args[1];
|
|
3256
|
+
if (name === 'remove') {
|
|
3257
|
+
const target = args[2];
|
|
3258
|
+
if (!target) {
|
|
3259
|
+
return { text: '用法: /order alias remove <name>' };
|
|
3260
|
+
}
|
|
3261
|
+
// remove 是保留子命令,不能作为别名名:后面跟多词文本说明用户想注册
|
|
3262
|
+
// 名为 remove 的别名,明确报错而不是静默删除已存在的 target(P2 review)。
|
|
3263
|
+
if (args.length > 3) {
|
|
3264
|
+
return { text: '`remove` 是保留子命令,不能作为别名名,请换一个名称' };
|
|
3265
|
+
}
|
|
3266
|
+
const entry = this.aliasStore.get(target);
|
|
3267
|
+
if (!entry) {
|
|
3268
|
+
return { text: `别名 $${target} 不存在` };
|
|
3269
|
+
}
|
|
3270
|
+
this.aliasStore.remove(target);
|
|
3271
|
+
// 回显原内容:单词场景无法区分「注册 remove」与「删除」,误删后
|
|
3272
|
+
// 用户可直接按回显内容重新注册,消除静默破坏(P3 review)。
|
|
3273
|
+
const preview = entry.text.length > 50 ? entry.text.slice(0, 50) + '...' : entry.text;
|
|
3274
|
+
return { text: `✅ 已删除别名 $${target}(原内容:${preview})` };
|
|
3275
|
+
}
|
|
3276
|
+
const text = args.slice(2).join(' ');
|
|
3277
|
+
if (!text) {
|
|
3278
|
+
const entry = name ? this.aliasStore.get(name) : undefined;
|
|
3279
|
+
if (!entry) {
|
|
3280
|
+
return { text: name ? `别名 $${name} 不存在` : '用法: /order alias <name> <text>' };
|
|
3281
|
+
}
|
|
3282
|
+
return { text: `$${entry.name} → ${entry.text}` };
|
|
3283
|
+
}
|
|
3284
|
+
try {
|
|
3285
|
+
const existed = this.aliasStore.has(name);
|
|
3286
|
+
this.aliasStore.set(name, text);
|
|
3287
|
+
const preview = text.length > 50 ? text.slice(0, 50) + '...' : text;
|
|
3288
|
+
return { text: `${existed ? '✅ 已更新别名' : '✅ 已保存别名'} $${name} → ${preview}` };
|
|
3289
|
+
}
|
|
3290
|
+
catch (err) {
|
|
3291
|
+
return { text: `保存失败: ${err.message}` };
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3385
3294
|
// /order save <text>
|
|
3386
3295
|
if (sub === 'save') {
|
|
3387
3296
|
const text = args.slice(1).join(' ');
|
|
@@ -3396,10 +3305,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3396
3305
|
return { text: `保存失败: ${err.message}` };
|
|
3397
3306
|
}
|
|
3398
3307
|
}
|
|
3399
|
-
return { text: '用法: /order [list|save <text>]' };
|
|
3308
|
+
return { text: '用法: /order [list|save <text>|alias <name> <text>|alias remove <name>]' };
|
|
3400
3309
|
}
|
|
3401
3310
|
}
|
|
3402
|
-
// --- Re-exported from ./utils.js (moved for decomposition) ---
|
|
3403
|
-
// formatUsageStats is shared by run-renderer and bridge via ../router/index.js.
|
|
3404
|
-
export { formatUsageStats } from './utils.js';
|
|
3405
3311
|
//# sourceMappingURL=index.js.map
|