lark-remote 0.1.6 → 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 +23 -1
- package/README.en.md +9 -1
- package/README.md +14 -2
- package/dist/bridge/approval-coordinator.d.ts +57 -34
- package/dist/bridge/approval-coordinator.js +191 -40
- package/dist/bridge/inbound-media.d.ts +62 -0
- package/dist/bridge/inbound-media.js +282 -0
- package/dist/bridge/index.d.ts +78 -7
- package/dist/bridge/index.js +202 -79
- package/dist/bridge/queue-manager.d.ts +2 -1
- package/dist/bridge/queue-manager.js +1 -0
- package/dist/card/approval-render.js +152 -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 +1 -12
- package/dist/card/card-shared.js +1 -26
- 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 +99 -232
- 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 +88 -23
- package/dist/config/index.js +105 -34
- package/dist/connector/index.d.ts +69 -2
- package/dist/connector/index.js +107 -1
- package/dist/index.js +123 -105
- 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.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/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 +11 -2
- package/dist/router/index.d.ts +56 -21
- package/dist/router/index.js +373 -548
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +4 -0
- 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 +31 -39
- package/dist/runner/codex/app-server/protocol-types.js +1 -12
- package/dist/runner/codex/app-server/runner.d.ts +42 -59
- package/dist/runner/codex/app-server/runner.js +160 -337
- package/dist/runner/codex/app-server/translator.d.ts +1 -21
- package/dist/runner/codex/app-server/translator.js +0 -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 +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/{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/index.d.ts +4 -4
- 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 +43 -13
- 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 +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/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/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, } 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,24 @@ 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');
|
|
105
125
|
}
|
|
106
126
|
export class CommandRouter {
|
|
107
127
|
/** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
|
|
108
128
|
static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
|
|
109
129
|
sessionStore;
|
|
130
|
+
/** 飞书桥接实例(public:测试直接访问断言,替代 as unknown as)。 */
|
|
110
131
|
bridge;
|
|
132
|
+
/** 当前配置(public:测试直接读取断言,替代 as unknown as)。 */
|
|
111
133
|
config;
|
|
134
|
+
/** 配置文件的绝对路径(public:/config 保存链路与测试共用)。 */
|
|
112
135
|
configPath;
|
|
113
136
|
workspaceStore;
|
|
114
137
|
orderStore;
|
|
138
|
+
aliasStore;
|
|
115
139
|
exitHandler;
|
|
116
140
|
pendingExit = false;
|
|
117
141
|
/**
|
|
@@ -126,7 +150,8 @@ export class CommandRouter {
|
|
|
126
150
|
updateCachePath;
|
|
127
151
|
/** Injected update functions (for testability; defaults to real implementation). */
|
|
128
152
|
updateFns;
|
|
129
|
-
|
|
153
|
+
/** /config 卡片编辑暂存区(public:测试直接读取断言,替代 as unknown as)。 */
|
|
154
|
+
pendingConfig = null;
|
|
130
155
|
/**
|
|
131
156
|
* Monotonic counter minting unique internal keys for order.exec enqueue
|
|
132
157
|
* actions. One order card can be clicked many times, so the Feishu card
|
|
@@ -147,6 +172,7 @@ export class CommandRouter {
|
|
|
147
172
|
* it uses `listRegisteredAgents()` from `router/config/index.ts` (the
|
|
148
173
|
* config-builder registry), which mirrors the agents with a config card.
|
|
149
174
|
*/
|
|
175
|
+
/** Session reader 注册中心(public:测试注入/读取,替代 as unknown as)。 */
|
|
150
176
|
sessionReaderRegistry;
|
|
151
177
|
/**
|
|
152
178
|
* 串行化 config.* 卡片回调(2026-07-04)。
|
|
@@ -164,6 +190,7 @@ export class CommandRouter {
|
|
|
164
190
|
this.configPath = opts.configPath;
|
|
165
191
|
this.workspaceStore = new WorkspaceStore(opts.workspacePath);
|
|
166
192
|
this.orderStore = new OrderStore(opts.ordersPath);
|
|
193
|
+
this.aliasStore = new AliasStore(opts.aliasesPath);
|
|
167
194
|
this.exitHandler = opts.exitHandler ?? (() => process.exit(0));
|
|
168
195
|
this.restartSpawner = opts.restartSpawner;
|
|
169
196
|
// idle watchdog 窗口从 config.idle.watchdogMinutes 读取
|
|
@@ -202,6 +229,8 @@ export class CommandRouter {
|
|
|
202
229
|
await this.bridge.sendResult(result, ctx);
|
|
203
230
|
}
|
|
204
231
|
if (this.pendingExit) {
|
|
232
|
+
// 干净退出前冲刷待合批的媒体保存提示,避免 500ms 窗口内的提示丢失。
|
|
233
|
+
await this.bridge.flushAllMediaNotifications();
|
|
205
234
|
this.exitHandler();
|
|
206
235
|
}
|
|
207
236
|
return result;
|
|
@@ -222,6 +251,13 @@ export class CommandRouter {
|
|
|
222
251
|
await this.bridge.forwardToClaude(trimmed, ctx, opts);
|
|
223
252
|
return null;
|
|
224
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* 一次别名展开:bridge 收到用户消息后、命令分发前调用。
|
|
256
|
+
* 仅 `$name` 开头的消息可能被展开;未知别名 / 非 `$` 消息原样返回。
|
|
257
|
+
*/
|
|
258
|
+
expandAliasMessage(message) {
|
|
259
|
+
return resolveAlias(message, this.aliasStore.list()) ?? message;
|
|
260
|
+
}
|
|
225
261
|
/**
|
|
226
262
|
* Handle a CardKit card button click (§6.2).
|
|
227
263
|
* Validates payload and dispatches to the corresponding command.
|
|
@@ -326,6 +362,9 @@ export class CommandRouter {
|
|
|
326
362
|
return this.enqueueConfigAction(value, ctx);
|
|
327
363
|
case 'approval.respond':
|
|
328
364
|
case 'approval.toggle':
|
|
365
|
+
case 'approval.answer':
|
|
366
|
+
case 'approval.answerSubmit':
|
|
367
|
+
case 'approval.answerCustom':
|
|
329
368
|
return this.handleApprovalAction(value, ctx);
|
|
330
369
|
case 'codex.compact':
|
|
331
370
|
await this.bridge.handleCodexCompact(value, ctx);
|
|
@@ -346,6 +385,7 @@ export class CommandRouter {
|
|
|
346
385
|
// 必须同样消费,否则点击 /restart 按钮 spawn 成功后旧进程不退出,
|
|
347
386
|
// 新进程撞单例锁退出 → 重启两头落空(2026-08-01 红绿 anchor 锁定)。
|
|
348
387
|
if (this.pendingExit) {
|
|
388
|
+
await this.bridge.flushAllMediaNotifications();
|
|
349
389
|
this.exitHandler();
|
|
350
390
|
}
|
|
351
391
|
return;
|
|
@@ -364,7 +404,7 @@ export class CommandRouter {
|
|
|
364
404
|
* Returns a toast response for immediate user feedback.
|
|
365
405
|
*/
|
|
366
406
|
async handleApprovalAction(value, _ctx) {
|
|
367
|
-
const { runId, requestId, decision,
|
|
407
|
+
const { runId, requestId, decision, nonce, permId } = value;
|
|
368
408
|
if (value.cmd === 'approval.respond') {
|
|
369
409
|
if (!runId || requestId === undefined || !decision || !nonce) {
|
|
370
410
|
return { toast: { type: 'error', content: '缺少审批响应参数' } };
|
|
@@ -374,7 +414,6 @@ export class CommandRouter {
|
|
|
374
414
|
runId,
|
|
375
415
|
requestId,
|
|
376
416
|
decision,
|
|
377
|
-
scope,
|
|
378
417
|
nonce,
|
|
379
418
|
});
|
|
380
419
|
return { toast: { type: 'success', content: '审批已提交' } };
|
|
@@ -389,7 +428,7 @@ export class CommandRouter {
|
|
|
389
428
|
}
|
|
390
429
|
}
|
|
391
430
|
if (value.cmd === 'approval.toggle') {
|
|
392
|
-
if (!runId || requestId === undefined || !permId
|
|
431
|
+
if (!runId || requestId === undefined || !permId) {
|
|
393
432
|
return { toast: { type: 'error', content: '缺少权限切换参数' } };
|
|
394
433
|
}
|
|
395
434
|
try {
|
|
@@ -397,7 +436,6 @@ export class CommandRouter {
|
|
|
397
436
|
runId,
|
|
398
437
|
requestId,
|
|
399
438
|
permId,
|
|
400
|
-
nonce,
|
|
401
439
|
selected: value.selected ?? true,
|
|
402
440
|
});
|
|
403
441
|
return { toast: { type: 'info', content: '已切换' } };
|
|
@@ -407,6 +445,70 @@ export class CommandRouter {
|
|
|
407
445
|
return { toast: { type: 'error', content: `权限切换失败:${msg}` } };
|
|
408
446
|
}
|
|
409
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
|
+
}
|
|
410
512
|
return { toast: { type: 'error', content: '未知的审批操作' } };
|
|
411
513
|
}
|
|
412
514
|
/**
|
|
@@ -888,17 +990,7 @@ export class CommandRouter {
|
|
|
888
990
|
const resolvedAgent = validAgents.includes(resumeAgent)
|
|
889
991
|
? resumeAgent
|
|
890
992
|
: 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;
|
|
993
|
+
const { pageSize, alignedOffset } = normalizePageArgs(value, RESUME_PAGE_SIZE);
|
|
902
994
|
const entry = this.sessionStore.get(ctx.userId);
|
|
903
995
|
const cwd = entry?.cwd;
|
|
904
996
|
if (!cwd) {
|
|
@@ -925,9 +1017,7 @@ export class CommandRouter {
|
|
|
925
1017
|
* Mirrors handleResumePage — updates the same card, never sends a new one.
|
|
926
1018
|
*/
|
|
927
1019
|
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;
|
|
1020
|
+
const { alignedOffset } = normalizePageArgs(value, ACTIVE_PAGE_SIZE);
|
|
931
1021
|
const result = this.cmdActive([], ctx, alignedOffset);
|
|
932
1022
|
if (!result.card) {
|
|
933
1023
|
return { toast: { type: 'error', content: '当前没有正在进行中的任务' } };
|
|
@@ -1054,7 +1144,7 @@ export class CommandRouter {
|
|
|
1054
1144
|
// immediate action the callback response is swallowed by enqueueImmediate
|
|
1055
1145
|
// (fire-and-forget), and even if delivered it is transient — the user gets
|
|
1056
1146
|
// no perceivable feedback on the switch. Same rationale as config.save
|
|
1057
|
-
// agent-switch notice: persistent message first, toast only as a
|
|
1147
|
+
// agent-switch notice (74e99f0): persistent message first, toast only as a
|
|
1058
1148
|
// fallback if sendResult fails.
|
|
1059
1149
|
let fallbackToast;
|
|
1060
1150
|
if (useResult.card || useResult.text) {
|
|
@@ -1186,6 +1276,7 @@ export class CommandRouter {
|
|
|
1186
1276
|
return next;
|
|
1187
1277
|
}
|
|
1188
1278
|
/** 实际的 config.* 分发逻辑(在串行队列内执行) */
|
|
1279
|
+
/** config.* 卡片动作分发(public:测试直接调用,替代 as unknown as)。 */
|
|
1189
1280
|
async dispatchConfigAction(value, ctx) {
|
|
1190
1281
|
switch (value.cmd) {
|
|
1191
1282
|
case 'config.toggle': {
|
|
@@ -1314,37 +1405,29 @@ export class CommandRouter {
|
|
|
1314
1405
|
}
|
|
1315
1406
|
}
|
|
1316
1407
|
/** 设置嵌套属性值(仅修改内存对象,不写盘) */
|
|
1408
|
+
/** 在 pendingConfig 上按 dot-separated key 设置嵌套值(config 卡片编辑用)。 */
|
|
1317
1409
|
setNestedValue(target, key, value) {
|
|
1318
1410
|
// Path mapping 共用 config 模块的 mapAgentKey(G11 Inconsistency 修复):
|
|
1319
1411
|
// pi.xxx/codex.xxx/opencode.xxx → agents.xxx,claude.xxx 保持顶层
|
|
1320
1412
|
const mappedKey = mapAgentKey(key);
|
|
1321
1413
|
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]);
|
|
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);
|
|
1337
1419
|
if (value === undefined) {
|
|
1338
1420
|
// value=undefined 表示"删除键"(如清空 reasoningEffort),
|
|
1339
1421
|
// 不能写成 undefined 值——diffConfig 会把 undefined 转成字面量 "undefined"
|
|
1340
1422
|
// 写入 config.yaml 并透传给 codex(ReasoningEffort::Custom("undefined"))。
|
|
1341
|
-
delete
|
|
1423
|
+
delete container[lastPart];
|
|
1342
1424
|
}
|
|
1343
1425
|
else {
|
|
1344
|
-
|
|
1426
|
+
container[lastPart] = value;
|
|
1345
1427
|
}
|
|
1346
1428
|
}
|
|
1347
1429
|
/** 对比原始 config 与 pendingConfig,返回变化的 key→string 映射 */
|
|
1430
|
+
/** config diff(public:测试直接调用,替代 as unknown as)。 */
|
|
1348
1431
|
diffConfig(original, pending) {
|
|
1349
1432
|
const updates = {};
|
|
1350
1433
|
this.collectDiff('', original, pending, updates);
|
|
@@ -1480,6 +1563,27 @@ export class CommandRouter {
|
|
|
1480
1563
|
atomicWrite(this.configPath, YAML.stringify(this.config));
|
|
1481
1564
|
this.bridge.setConfig(this.config);
|
|
1482
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
|
+
}
|
|
1483
1587
|
// 2026-07-13: 任何 agent 配置变更或 defaultAgent 切换,都清除 runner 缓存
|
|
1484
1588
|
if (hasDefaultAgentChange || hasAgentConfigChange) {
|
|
1485
1589
|
this.bridge.clearRunners();
|
|
@@ -1560,7 +1664,11 @@ export class CommandRouter {
|
|
|
1560
1664
|
{ cmd: 'cd', label: '/cd <path>', desc: '切换工作目录' },
|
|
1561
1665
|
{ cmd: 'ls', label: '/ls [dir]', desc: '列出当前目录,可指定子目录' },
|
|
1562
1666
|
{ cmd: 'resume', label: '/resume /r [agent] [list|id|N]', desc: '列出或切换 Agent session' },
|
|
1563
|
-
{
|
|
1667
|
+
{
|
|
1668
|
+
cmd: 'order',
|
|
1669
|
+
label: '/order /o save|list',
|
|
1670
|
+
desc: '收藏指令 /order save,快捷别名 /order alias',
|
|
1671
|
+
},
|
|
1564
1672
|
];
|
|
1565
1673
|
const bodyElements = [];
|
|
1566
1674
|
// 按钮组 — CardKit 2.0: column_set + column + button with behaviors
|
|
@@ -1641,6 +1749,7 @@ export class CommandRouter {
|
|
|
1641
1749
|
},
|
|
1642
1750
|
};
|
|
1643
1751
|
}
|
|
1752
|
+
/** /status 实现(public:测试直接调用,替代 as any)。 */
|
|
1644
1753
|
cmdStatus(ctx) {
|
|
1645
1754
|
const entry = this.sessionStore.get(ctx.userId);
|
|
1646
1755
|
const cwd = entry?.cwd ?? '(未设置)';
|
|
@@ -1886,84 +1995,30 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1886
1995
|
buildAutoResumeCard(cwd, session, _ctx) {
|
|
1887
1996
|
const agentName = agentDisplayName(this.config.defaultAgent);
|
|
1888
1997
|
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));
|
|
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,
|
|
1909
2006
|
});
|
|
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
|
-
})),
|
|
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,
|
|
1955
2021
|
});
|
|
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
2022
|
return { card };
|
|
1968
2023
|
}
|
|
1969
2024
|
/**
|
|
@@ -1977,20 +2032,11 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
1977
2032
|
const agentName = agentDisplayName(newAgent);
|
|
1978
2033
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
1979
2034
|
const sessionId = switchResult.sessionId;
|
|
1980
|
-
// 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)
|
|
1981
2038
|
if (!cwd || !sessionId) {
|
|
1982
2039
|
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
2040
|
return {
|
|
1995
2041
|
card: {
|
|
1996
2042
|
schema: '2.0',
|
|
@@ -2034,79 +2080,79 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2034
2080
|
agentKind: newAgent,
|
|
2035
2081
|
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2036
2082
|
});
|
|
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
|
-
|
|
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) {
|
|
2067
2132
|
const actions = [];
|
|
2068
|
-
if (isActive) {
|
|
2133
|
+
if (opts.isActive) {
|
|
2069
2134
|
actions.push({
|
|
2070
2135
|
tag: 'button',
|
|
2071
2136
|
text: { tag: 'plain_text', content: '⏹ 终止' },
|
|
2072
2137
|
type: 'danger',
|
|
2073
|
-
behaviors: [
|
|
2138
|
+
behaviors: [
|
|
2139
|
+
{
|
|
2140
|
+
type: 'callback',
|
|
2141
|
+
value: { cmd: 'stop', runId: opts.activeRunRunId, cwd: opts.cwd },
|
|
2142
|
+
},
|
|
2143
|
+
],
|
|
2074
2144
|
});
|
|
2075
2145
|
}
|
|
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');
|
|
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;
|
|
2108
2153
|
}
|
|
2109
2154
|
static LS_PAGE_SIZE = 30;
|
|
2155
|
+
/** /ls 实现(public:测试直接调用,替代 as unknown as)。 */
|
|
2110
2156
|
cmdLs(args, ctx, offset = 0) {
|
|
2111
2157
|
const cwd = this.sessionStore.getCwd(ctx.userId);
|
|
2112
2158
|
if (!cwd) {
|
|
@@ -2290,78 +2336,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2290
2336
|
}
|
|
2291
2337
|
// Pagination bar (only shown when there are more items than PAGE_SIZE)
|
|
2292
2338
|
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
|
-
],
|
|
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} 项)`,
|
|
2337
2346
|
});
|
|
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
2347
|
elements.push({ tag: 'hr' });
|
|
2364
|
-
elements.push(
|
|
2348
|
+
elements.push(bar);
|
|
2365
2349
|
}
|
|
2366
2350
|
return {
|
|
2367
2351
|
card: {
|
|
@@ -2451,7 +2435,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2451
2435
|
const totalCount = entries.length;
|
|
2452
2436
|
const totalPages = Math.max(1, Math.ceil(totalCount / WS_PAGE_SIZE));
|
|
2453
2437
|
const maxOffset = Math.max(0, (totalPages - 1) * WS_PAGE_SIZE);
|
|
2454
|
-
const safeOffset =
|
|
2438
|
+
const safeOffset = clampInt(offset, 0, maxOffset);
|
|
2455
2439
|
const currentPage = Math.floor(safeOffset / WS_PAGE_SIZE) + 1;
|
|
2456
2440
|
const pageEntries = entries.slice(safeOffset, safeOffset + WS_PAGE_SIZE);
|
|
2457
2441
|
const hasPagination = totalCount > WS_PAGE_SIZE;
|
|
@@ -2561,68 +2545,17 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2561
2545
|
// Pagination bar — separate row below the list (not crowded with sort
|
|
2562
2546
|
// toggle; mobile-friendly vertical layout).
|
|
2563
2547
|
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
|
-
],
|
|
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: '➡',
|
|
2602
2556
|
});
|
|
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
2557
|
bodyElements.push({ tag: 'hr' });
|
|
2625
|
-
bodyElements.push(
|
|
2558
|
+
bodyElements.push(bar);
|
|
2626
2559
|
}
|
|
2627
2560
|
}
|
|
2628
2561
|
const card = {
|
|
@@ -2642,6 +2575,10 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2642
2575
|
* misleading "未找到". Reuses the same card-builder helpers (markdownDiv) as
|
|
2643
2576
|
* the normal resume card; enforceCardBudget still applies at send time.
|
|
2644
2577
|
*/
|
|
2578
|
+
/**
|
|
2579
|
+
* /resume 核心实现(public:handleResumePage / handle / resume.use 共用,
|
|
2580
|
+
* 也是测试的直接 seam,避免测试用 @ts-expect-error 摸私有方法)。
|
|
2581
|
+
*/
|
|
2645
2582
|
cmdResume(args, ctx, offset = 0) {
|
|
2646
2583
|
const entry = this.sessionStore.get(ctx.userId);
|
|
2647
2584
|
const cwd = entry?.cwd;
|
|
@@ -2694,87 +2631,39 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2694
2631
|
agentKind,
|
|
2695
2632
|
maxEvents: AUTO_RESUME_MAX_EVENTS,
|
|
2696
2633
|
});
|
|
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
|
-
};
|
|
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
|
+
});
|
|
2771
2660
|
return { card };
|
|
2772
2661
|
}
|
|
2773
2662
|
// `/resume` or `/resume list` or `/resume <N>` — list sessions for the current cwd as a card
|
|
2774
2663
|
// Use agentKind to select the appropriate sessionReader
|
|
2775
2664
|
if (!cwd)
|
|
2776
2665
|
return { text: '请先 /cd 设置工作目录' };
|
|
2777
|
-
const pageSize =
|
|
2666
|
+
const pageSize = clampInt(limit ?? RESUME_PAGE_SIZE, 1, RESUME_PAGE_SIZE);
|
|
2778
2667
|
let reader;
|
|
2779
2668
|
// Use this.sessionReader when agentKind matches default, otherwise get from registry
|
|
2780
2669
|
if (agentKind === this.config.defaultAgent) {
|
|
@@ -2794,7 +2683,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2794
2683
|
// 这样越界 offset clamp 后 prev/next 仍按 pageSize 步长移动,不会错位。
|
|
2795
2684
|
const maxOffset = Math.max(0, Math.ceil(pageResult.total / pageSize) - 1) * pageSize;
|
|
2796
2685
|
if (pageOffset < 0 || pageOffset > maxOffset) {
|
|
2797
|
-
pageOffset =
|
|
2686
|
+
pageOffset = clampInt(pageOffset, 0, maxOffset);
|
|
2798
2687
|
pageResult = reader.listSessions(cwd, { limit: pageSize, offset: pageOffset });
|
|
2799
2688
|
}
|
|
2800
2689
|
}
|
|
@@ -2918,80 +2807,16 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
2918
2807
|
if (total > pageSize) {
|
|
2919
2808
|
const totalPages = Math.ceil(total / pageSize);
|
|
2920
2809
|
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
|
-
],
|
|
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} 个会话`,
|
|
2966
2817
|
});
|
|
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
2818
|
elements.push({ tag: 'hr' });
|
|
2994
|
-
elements.push(
|
|
2819
|
+
elements.push(bar);
|
|
2995
2820
|
}
|
|
2996
2821
|
const card = {
|
|
2997
2822
|
schema: '2.0',
|
|
@@ -3219,7 +3044,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3219
3044
|
// Direct inline construction (not via ConfigField) so that text (display label)
|
|
3220
3045
|
// and value (agentKind for config storage) can differ — uninstalled agents get
|
|
3221
3046
|
// a "⚠️ (未安装)" suffix in the label while the value stays clean.
|
|
3222
|
-
|
|
3047
|
+
// 固定展示顺序(Codex → Claude → OpenCode → Pi → Kimi),
|
|
3048
|
+
// 明确未安装的 agent 排到后面(getCachedAvailability === false)。
|
|
3049
|
+
const allAgents = sortAgentsForDisplay(listRegisteredAgents(), getCachedAvailability);
|
|
3223
3050
|
const agentOptions = allAgents.map((kind) => {
|
|
3224
3051
|
const available = getCachedAvailability(kind);
|
|
3225
3052
|
const label = available === false ? `${agentDisplayName(kind)} ⚠️ (未安装)` : agentDisplayName(kind);
|
|
@@ -3313,11 +3140,14 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3313
3140
|
}
|
|
3314
3141
|
return { card };
|
|
3315
3142
|
}
|
|
3143
|
+
/** /order 实现(public:测试直接调用,替代 as unknown as)。 */
|
|
3316
3144
|
cmdOrder(args, _ctx, offset = 0) {
|
|
3317
3145
|
const sub = args[0]?.toLowerCase();
|
|
3318
|
-
// /order — list orders (CardKit 2.0 with pagination)
|
|
3319
|
-
|
|
3146
|
+
// /order — list orders + aliases (CardKit 2.0 with pagination)
|
|
3147
|
+
// `/order alias`(无参数)也并入列表卡片。
|
|
3148
|
+
if (!sub || sub === 'list' || (sub === 'alias' && !args[1])) {
|
|
3320
3149
|
this.orderStore.reload();
|
|
3150
|
+
this.aliasStore.reload();
|
|
3321
3151
|
const allOrders = this.orderStore.get();
|
|
3322
3152
|
const elements = [];
|
|
3323
3153
|
if (allOrders.length === 0) {
|
|
@@ -3329,7 +3159,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3329
3159
|
const totalPages = Math.max(1, Math.ceil(totalCount / ORDER_PAGE_SIZE));
|
|
3330
3160
|
// Clamp offset to last page boundary to avoid empty pages
|
|
3331
3161
|
const maxOffset = Math.max(0, (totalPages - 1) * ORDER_PAGE_SIZE);
|
|
3332
|
-
const safeOffset =
|
|
3162
|
+
const safeOffset = clampInt(offset, 0, maxOffset);
|
|
3333
3163
|
const currentPage = Math.floor(safeOffset / ORDER_PAGE_SIZE) + 1;
|
|
3334
3164
|
const pageOrders = allOrders.slice(safeOffset, safeOffset + ORDER_PAGE_SIZE);
|
|
3335
3165
|
const hasPagination = totalCount > ORDER_PAGE_SIZE;
|
|
@@ -3384,74 +3214,31 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3384
3214
|
}
|
|
3385
3215
|
// Pagination bar (only shown when there are more items than ORDER_PAGE_SIZE)
|
|
3386
3216
|
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
|
-
],
|
|
3217
|
+
const bar = paginationBar({
|
|
3218
|
+
cmd: 'order.page',
|
|
3219
|
+
offset: safeOffset,
|
|
3220
|
+
pageSize: ORDER_PAGE_SIZE,
|
|
3221
|
+
total: totalCount,
|
|
3222
|
+
label: `**第 ${currentPage}/${totalPages} 页**(共 ${totalCount} 条)`,
|
|
3428
3223
|
});
|
|
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
3224
|
elements.push({ tag: 'hr' });
|
|
3454
|
-
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
|
+
});
|
|
3455
3242
|
}
|
|
3456
3243
|
}
|
|
3457
3244
|
return {
|
|
@@ -3463,6 +3250,47 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3463
3250
|
},
|
|
3464
3251
|
};
|
|
3465
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
|
+
}
|
|
3466
3294
|
// /order save <text>
|
|
3467
3295
|
if (sub === 'save') {
|
|
3468
3296
|
const text = args.slice(1).join(' ');
|
|
@@ -3477,10 +3305,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
|
|
|
3477
3305
|
return { text: `保存失败: ${err.message}` };
|
|
3478
3306
|
}
|
|
3479
3307
|
}
|
|
3480
|
-
return { text: '用法: /order [list|save <text>]' };
|
|
3308
|
+
return { text: '用法: /order [list|save <text>|alias <name> <text>|alias remove <name>]' };
|
|
3481
3309
|
}
|
|
3482
3310
|
}
|
|
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
3311
|
//# sourceMappingURL=index.js.map
|