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.
Files changed (190) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +5 -1
  3. package/README.md +10 -2
  4. package/dist/bridge/approval-coordinator.d.ts +131 -0
  5. package/dist/bridge/approval-coordinator.js +409 -0
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +138 -5
  9. package/dist/bridge/index.js +569 -65
  10. package/dist/bridge/queue-manager.d.ts +11 -1
  11. package/dist/bridge/queue-manager.js +20 -11
  12. package/dist/card/approval-render.d.ts +16 -0
  13. package/dist/card/approval-render.js +363 -0
  14. package/dist/card/bash-card-session.d.ts +2 -24
  15. package/dist/card/bash-card-session.js +6 -73
  16. package/dist/card/bash-renderer.js +2 -3
  17. package/dist/card/card-budget.js +29 -75
  18. package/dist/card/card-session.d.ts +48 -0
  19. package/dist/card/card-session.js +86 -0
  20. package/dist/card/card-shared.d.ts +13 -10
  21. package/dist/card/card-shared.js +27 -24
  22. package/dist/card/run-card-session.d.ts +2 -55
  23. package/dist/card/run-card-session.js +6 -104
  24. package/dist/card/run-renderer.js +140 -202
  25. package/dist/card/run-state.d.ts +38 -3
  26. package/dist/card/run-state.js +221 -11
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/dir.js +1 -1
  40. package/dist/config/index.d.ts +116 -9
  41. package/dist/config/index.js +122 -29
  42. package/dist/connector/index.d.ts +74 -2
  43. package/dist/connector/index.js +119 -1
  44. package/dist/index.js +139 -99
  45. package/dist/instance-lock.js +4 -12
  46. package/dist/order/alias-resolve.d.ts +13 -0
  47. package/dist/order/alias-resolve.js +22 -0
  48. package/dist/order/alias-store.d.ts +38 -0
  49. package/dist/order/alias-store.js +106 -0
  50. package/dist/persistence/atomic-write.d.ts +8 -2
  51. package/dist/persistence/atomic-write.js +23 -7
  52. package/dist/restart.js +2 -1
  53. package/dist/router/card-helpers.d.ts +63 -0
  54. package/dist/router/card-helpers.js +146 -0
  55. package/dist/router/config/claude.js +25 -1
  56. package/dist/router/config/codex.d.ts +1 -1
  57. package/dist/router/config/codex.js +33 -5
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +9 -4
  61. package/dist/router/config/index.d.ts +12 -0
  62. package/dist/router/config/index.js +25 -1
  63. package/dist/router/config/kimi.d.ts +4 -1
  64. package/dist/router/config/kimi.js +27 -0
  65. package/dist/router/config/opencode.js +18 -5
  66. package/dist/router/config/pi.js +4 -5
  67. package/dist/router/config/types.d.ts +12 -3
  68. package/dist/router/index.d.ts +77 -17
  69. package/dist/router/index.js +455 -549
  70. package/dist/router/utils.d.ts +4 -0
  71. package/dist/router/utils.js +9 -2
  72. package/dist/runner/agent-choices-common.d.ts +16 -0
  73. package/dist/runner/agent-choices-common.js +31 -0
  74. package/dist/runner/claude/runner.d.ts +47 -10
  75. package/dist/runner/claude/runner.js +117 -46
  76. package/dist/runner/claude/session.d.ts +166 -0
  77. package/dist/runner/claude/session.js +749 -0
  78. package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
  79. package/dist/runner/codex/app-server/protocol-types.js +56 -0
  80. package/dist/runner/codex/app-server/runner.d.ts +104 -0
  81. package/dist/runner/codex/app-server/runner.js +377 -0
  82. package/dist/runner/codex/app-server/translator.d.ts +89 -0
  83. package/dist/runner/codex/app-server/translator.js +716 -0
  84. package/dist/runner/codex/index.d.ts +1 -1
  85. package/dist/runner/codex/index.js +1 -1
  86. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  87. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  88. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  89. package/dist/runner/common/acp/protocol-types.js +75 -0
  90. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  91. package/dist/runner/common/connection-based-runner.js +261 -0
  92. package/dist/runner/common/jsonrpc/client.d.ts +89 -0
  93. package/dist/runner/common/jsonrpc/client.js +243 -0
  94. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  95. package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
  96. package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
  97. package/dist/runner/common/jsonrpc/transport.js +230 -0
  98. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  99. package/dist/runner/common/jsonrpc/types.js +9 -0
  100. package/dist/runner/common/process-stopper.d.ts +2 -1
  101. package/dist/runner/common/process-stopper.js +1 -0
  102. package/dist/runner/common/runner-utils.d.ts +6 -13
  103. package/dist/runner/common/runner-utils.js +6 -47
  104. package/dist/runner/common/spawning-runner.d.ts +24 -70
  105. package/dist/runner/common/spawning-runner.js +98 -152
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/index.d.ts +5 -5
  109. package/dist/runner/index.js +4 -4
  110. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  111. package/dist/runner/kimi/acp/runner.js +448 -0
  112. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  113. package/dist/runner/kimi/acp/translator.js +336 -0
  114. package/dist/runner/kimi/index.d.ts +1 -1
  115. package/dist/runner/kimi/index.js +1 -1
  116. package/dist/runner/opencode/acp/index.d.ts +1 -0
  117. package/dist/runner/opencode/acp/index.js +2 -0
  118. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  119. package/dist/runner/opencode/acp/runner.js +429 -0
  120. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  121. package/dist/runner/opencode/acp/translator.js +345 -0
  122. package/dist/runner/opencode/index.d.ts +1 -1
  123. package/dist/runner/opencode/index.js +1 -1
  124. package/dist/runner/pi/index.d.ts +1 -1
  125. package/dist/runner/pi/index.js +1 -1
  126. package/dist/runner/pi/rpc/client.d.ts +52 -0
  127. package/dist/runner/pi/rpc/client.js +114 -0
  128. package/dist/runner/pi/rpc/index.d.ts +4 -0
  129. package/dist/runner/pi/rpc/index.js +4 -0
  130. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  131. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  132. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  133. package/dist/runner/pi/rpc/runner.js +199 -0
  134. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  135. package/dist/runner/pi/rpc/translator.js +258 -0
  136. package/dist/runner/registry.d.ts +7 -8
  137. package/dist/runner/registry.js +3 -4
  138. package/dist/runner/resolve-agent-choices.js +18 -59
  139. package/dist/runner/sync-agent-choices.js +16 -67
  140. package/dist/runner/types.d.ts +153 -2
  141. package/dist/session/claude/session-index.js +2 -3
  142. package/dist/session/claude/session-reader.js +3 -6
  143. package/dist/session/claude/sessions.js +6 -13
  144. package/dist/session/codex/rollout-reader.d.ts +2 -2
  145. package/dist/session/codex/rollout-reader.js +57 -17
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +2 -2
  148. package/dist/session/common/content-blocks.d.ts +7 -0
  149. package/dist/session/common/content-blocks.js +13 -9
  150. package/dist/session/common/jsonl.d.ts +8 -25
  151. package/dist/session/common/jsonl.js +126 -261
  152. package/dist/session/common/pagination.d.ts +28 -0
  153. package/dist/session/common/pagination.js +33 -0
  154. package/dist/session/kimi/sessions.d.ts +34 -1
  155. package/dist/session/kimi/sessions.js +208 -98
  156. package/dist/session/opencode/sessions.d.ts +17 -3
  157. package/dist/session/opencode/sessions.js +29 -29
  158. package/dist/session/pi/sessions.d.ts +5 -1
  159. package/dist/session/pi/sessions.js +12 -16
  160. package/dist/session/session-store.d.ts +11 -1
  161. package/dist/session/session-store.js +75 -178
  162. package/dist/update/index.d.ts +2 -2
  163. package/dist/update/index.js +2 -2
  164. package/dist/update/install.d.ts +20 -2
  165. package/dist/update/install.js +51 -7
  166. package/package.json +1 -1
  167. package/dist/runner/codex/argv.d.ts +0 -33
  168. package/dist/runner/codex/argv.js +0 -40
  169. package/dist/runner/codex/jsonl.d.ts +0 -37
  170. package/dist/runner/codex/jsonl.js +0 -221
  171. package/dist/runner/codex/runner.d.ts +0 -63
  172. package/dist/runner/codex/runner.js +0 -124
  173. package/dist/runner/common/exec-translator.d.ts +0 -73
  174. package/dist/runner/common/exec-translator.js +0 -105
  175. package/dist/runner/kimi/jsonl.d.ts +0 -28
  176. package/dist/runner/kimi/jsonl.js +0 -143
  177. package/dist/runner/kimi/runner.d.ts +0 -49
  178. package/dist/runner/kimi/runner.js +0 -105
  179. package/dist/runner/opencode/argv.d.ts +0 -32
  180. package/dist/runner/opencode/argv.js +0 -35
  181. package/dist/runner/opencode/jsonl.d.ts +0 -68
  182. package/dist/runner/opencode/jsonl.js +0 -295
  183. package/dist/runner/opencode/runner.d.ts +0 -61
  184. package/dist/runner/opencode/runner.js +0 -93
  185. package/dist/runner/pi/jsonl.d.ts +0 -128
  186. package/dist/runner/pi/jsonl.js +0 -249
  187. package/dist/runner/pi/runner.d.ts +0 -36
  188. package/dist/runner/pi/runner.js +0 -76
  189. package/dist/session/common/catch-up.d.ts +0 -12
  190. package/dist/session/common/catch-up.js +0 -31
@@ -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, formatUsageStats } from './utils.js';
11
- import { markdownDiv, sessionEventPanel } from './card-helpers.js';
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
- pendingConfig = null; // /config 卡片编辑暂存区
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 rawPageSize = Number(value.pageSize);
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 rawOffset = Number(value.offset);
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: if auto-resume produced a card, send it as a reply
988
- if (useResult.card) {
989
- await this.bridge.sendResult(useResult, ctx);
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 auto-resume card was already sent)
1162
+ // Toast feedback (suppress text when a persistent message was already sent)
998
1163
  return {
999
1164
  toast: {
1000
1165
  type: 'success',
1001
- content: useResult.card ? '' : (useResult.text ?? `已切换到 workspace "${name}"`),
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
- let current = target;
1248
- for (let i = 0; i < parts.length - 1; i++) {
1249
- // §P1-4:卡片 config.set/config.input 的 key 来自卡片 action,
1250
- // 必须与 config 模块的 setNestedValue/deleteNestedValue/getConfigValue 同等
1251
- // 守卫 __proto__/prototype/constructor 段——否则伪造 key 经
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 current[parts[parts.length - 1]];
1423
+ delete container[lastPart];
1267
1424
  }
1268
1425
  else {
1269
- current[parts[parts.length - 1]] = value;
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
- { cmd: 'order', label: '/order /o save|list', desc: '保存或列出常用指令(/order 默认列出)' },
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 → spawn replacement bridge → exit.
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. Restart (reuse /restart's restartSpawner + pendingExit)
1706
- if (!this.restartSpawner) {
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
- // Build header with displayTitle (aiTitle or last user message) and recap
1829
- let header = `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`;
1830
- const sections = [];
1831
- if (displayTitle) {
1832
- const label = aiTitle ? 'AI 标题' : '最近输入';
1833
- sections.push(`🏷️ **${label}**\n${displayTitle}`);
1834
- }
1835
- if (recap) {
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
- // Add usage/stats at the end
1850
- if (usage) {
1851
- const usageStr = formatUsageStats(usage, { showResult: true, result: 'success' });
1852
- elements.push({
1853
- tag: 'div',
1854
- text: { tag: 'lark_md', content: usageStr },
1855
- });
1856
- }
1857
- // Remove trailing hr and add action buttons
1858
- if (elements.length > 0 && elements[elements.length - 1].tag === 'hr') {
1859
- elements.pop();
1860
- }
1861
- const actions = [];
1862
- // Stop button for active sessions (isActive = in-memory activeRun is
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
- let header = `📂 \`${cwd}\`\n已恢复会话: **${sessionId}**`;
1972
- const sections = [];
1973
- if (displayTitle) {
1974
- const label = aiTitle ? 'AI 标题' : '最近输入';
1975
- sections.push(`🏷️ **${label}**\n${displayTitle}`);
1976
- }
1977
- if (recap) {
1978
- const recapPreview = recap.length > 200 ? recap.slice(0, 197) + '...' : recap;
1979
- sections.push(`📝 **Recap**\n${recapPreview}`);
1980
- }
1981
- if (sections.length > 0) {
1982
- header += '\n\n' + sections.join('\n\n──\n\n');
1983
- }
1984
- const elements = [markdownDiv(header), { tag: 'hr' }];
1985
- if (content.length === 0) {
1986
- elements.push(markdownDiv('_该会话暂无新消息可显示(最后一条为用户输入)_'));
1987
- }
1988
- else {
1989
- content.forEach((ev, i) => {
1990
- elements.push(sessionEventPanel(ev, i, content.length, 2, newAgent));
1991
- });
1992
- }
1993
- if (usage) {
1994
- const usageStr = formatUsageStats(usage, { showResult: true, result: 'success' });
1995
- elements.push(markdownDiv(usageStr));
1996
- }
1997
- if (elements.length > 0 && elements[elements.length - 1].tag === 'hr') {
1998
- elements.pop();
1999
- }
2000
- // Action buttons
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: [{ type: 'callback', value: { cmd: 'stop', runId: activeRunRunId, cwd } }],
2138
+ behaviors: [
2139
+ {
2140
+ type: 'callback',
2141
+ value: { cmd: 'stop', runId: opts.activeRunRunId, cwd: opts.cwd },
2142
+ },
2143
+ ],
2008
2144
  });
2009
2145
  }
2010
- actions.push(newSessionButton());
2011
- elements.push({
2012
- tag: 'column_set',
2013
- columns: actions.map((btn) => ({
2014
- tag: 'column',
2015
- width: 'auto',
2016
- elements: [btn],
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 hasPrev = offset > 0;
2218
- const hasNext = offset + CommandRouter.LS_PAGE_SIZE < totalCount;
2219
- // Build columns dynamically — Feishu rejects empty elements[] in a column
2220
- // (ErrCode 200621: "no tag specified"). Only include columns that have content.
2221
- const pageColumns = [];
2222
- if (hasPrev) {
2223
- pageColumns.push({
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({ tag: 'column_set', columns: pageColumns });
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 = Math.min(Math.max(offset, 0), maxOffset);
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 hasPrev = safeOffset > 0;
2489
- const hasNext = safeOffset + WS_PAGE_SIZE < totalCount;
2490
- const pageColumns = [];
2491
- if (hasPrev) {
2492
- pageColumns.push({
2493
- tag: 'column',
2494
- width: 'auto',
2495
- vertical_align: 'center',
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({ tag: 'column_set', columns: pageColumns });
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
- // Build card header with displayTitle (aiTitle or last user message) and recap
2622
- let header = `📂 \`${cwd}\`\n会话: **${sessionIdArg}**`;
2623
- const sections = [];
2624
- if (displayTitle) {
2625
- const label = aiTitle ? 'AI 标题' : '最近输入';
2626
- sections.push(`🏷️ **${label}**\n${displayTitle}`);
2627
- }
2628
- if (recap) {
2629
- const recapPreview = recap.length > 200 ? recap.slice(0, 197) + '...' : recap;
2630
- sections.push(`📝 **Recap**\n${recapPreview}`);
2631
- }
2632
- if (sections.length > 0) {
2633
- header += '\n\n' + sections.join('\n\n──\n\n');
2634
- }
2635
- // Build card with session history
2636
- const elements = [markdownDiv(header), { tag: 'hr' }];
2637
- // Fold session history into collapsible panels: the last 2 events stay
2638
- // expanded so the user sees the most recent context without clicking;
2639
- // older events are collapsed to keep the card compact.
2640
- if (content.length === 0) {
2641
- elements.push(markdownDiv('_该会话暂无新消息可显示(最后一条为用户输入)_'));
2642
- }
2643
- else {
2644
- content.forEach((ev, i) => {
2645
- elements.push(sessionEventPanel(ev, i, content.length, 2, this.config.defaultAgent));
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 = Math.min(Math.max(limit ?? RESUME_PAGE_SIZE, 1), RESUME_PAGE_SIZE);
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 = Math.min(Math.max(pageOffset, 0), maxOffset);
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 hasPrev = pageOffset > 0;
2841
- const hasNext = pageOffset + pageSize < total;
2842
- // Build columns dynamically — Feishu rejects empty elements[] in a column
2843
- // (ErrCode 200621: "no tag specified"). Only include columns that have content.
2844
- const pageColumns = [];
2845
- if (hasPrev) {
2846
- pageColumns.push({
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({ tag: 'column_set', columns: pageColumns });
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
- const allAgents = listRegisteredAgents();
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
- if (!sub || sub === 'list') {
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 = Math.min(Math.max(offset, 0), maxOffset);
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 hasPrev = safeOffset > 0;
3307
- const hasNext = safeOffset + ORDER_PAGE_SIZE < totalCount;
3308
- const pageColumns = [];
3309
- if (hasPrev) {
3310
- pageColumns.push({
3311
- tag: 'column',
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({ tag: 'column_set', columns: pageColumns });
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