lark-remote 0.1.11 → 0.1.13

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 (71) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/bridge/inbound-media.d.ts +0 -3
  3. package/dist/bridge/inbound-media.js +0 -1
  4. package/dist/bridge/index.d.ts +7 -0
  5. package/dist/bridge/index.js +79 -70
  6. package/dist/card/card-shared.d.ts +1 -4
  7. package/dist/card/card-shared.js +2 -4
  8. package/dist/common/truncate.d.ts +0 -6
  9. package/dist/common/truncate.js +1 -1
  10. package/dist/common/ttl-cache.d.ts +24 -0
  11. package/dist/common/ttl-cache.js +53 -0
  12. package/dist/config/codex-config.d.ts +22 -0
  13. package/dist/config/codex-config.js +120 -25
  14. package/dist/config/index.d.ts +0 -3
  15. package/dist/config/index.js +0 -5
  16. package/dist/config/kimi-config.d.ts +1 -1
  17. package/dist/config/kimi-config.js +1 -1
  18. package/dist/config/opencode-config.d.ts +1 -1
  19. package/dist/config/opencode-config.js +3 -8
  20. package/dist/index.js +6 -0
  21. package/dist/router/index.d.ts +7 -0
  22. package/dist/router/index.js +229 -284
  23. package/dist/runner/claude/session.d.ts +0 -3
  24. package/dist/runner/claude/session.js +0 -5
  25. package/dist/runner/codex/app-server/runner.d.ts +1 -16
  26. package/dist/runner/codex/app-server/runner.js +2 -2
  27. package/dist/runner/common/acp/protocol-helpers.d.ts +31 -0
  28. package/dist/runner/common/acp/protocol-helpers.js +48 -0
  29. package/dist/runner/common/connection-based-runner.d.ts +0 -2
  30. package/dist/runner/common/connection-based-runner.js +1 -1
  31. package/dist/runner/dsh/index.d.ts +0 -2
  32. package/dist/runner/dsh/index.js +0 -2
  33. package/dist/runner/dsh/runner.d.ts +0 -2
  34. package/dist/runner/dsh/runner.js +1 -1
  35. package/dist/runner/index.d.ts +1 -1
  36. package/dist/runner/kimi/acp/runner.d.ts +18 -11
  37. package/dist/runner/kimi/acp/runner.js +95 -87
  38. package/dist/runner/kimi/acp/translator.d.ts +0 -1
  39. package/dist/runner/kimi/acp/translator.js +5 -13
  40. package/dist/runner/opencode/acp/error-monitor.d.ts +61 -0
  41. package/dist/runner/opencode/acp/error-monitor.js +150 -0
  42. package/dist/runner/opencode/acp/runner.d.ts +17 -0
  43. package/dist/runner/opencode/acp/runner.js +28 -42
  44. package/dist/runner/opencode/acp/translator.d.ts +0 -1
  45. package/dist/runner/opencode/acp/translator.js +3 -13
  46. package/dist/runner/pi/rpc/index.d.ts +0 -2
  47. package/dist/runner/pi/rpc/index.js +0 -2
  48. package/dist/runner/resolve-agent-choices.js +6 -2
  49. package/dist/runner/sync-agent-choices.js +1 -1
  50. package/dist/runner/types.d.ts +23 -0
  51. package/dist/session/claude/session-index.js +3 -1
  52. package/dist/session/claude/sessions.js +3 -7
  53. package/dist/session/codex/rollout-reader.d.ts +12 -2
  54. package/dist/session/codex/rollout-reader.js +259 -173
  55. package/dist/session/codex/sessions.d.ts +2 -1
  56. package/dist/session/codex/sessions.js +4 -1
  57. package/dist/session/common/pagination.d.ts +0 -2
  58. package/dist/session/common/pagination.js +1 -1
  59. package/dist/session/common/recency.d.ts +7 -0
  60. package/dist/session/common/recency.js +10 -0
  61. package/dist/session/common/usage-accumulator.d.ts +14 -0
  62. package/dist/session/common/usage-accumulator.js +15 -0
  63. package/dist/session/dsh/sessions.js +9 -6
  64. package/dist/session/kimi/sessions.d.ts +18 -4
  65. package/dist/session/kimi/sessions.js +49 -30
  66. package/dist/session/opencode/sessions.d.ts +2 -14
  67. package/dist/session/opencode/sessions.js +9 -7
  68. package/dist/session/pi/sessions.js +10 -22
  69. package/dist/update/index.d.ts +2 -2
  70. package/dist/update/index.js +1 -1
  71. package/package.json +1 -2
@@ -24,6 +24,29 @@ const AUTO_RESUME_MAX_EVENTS = 5;
24
24
  /** /resume 列表页大小;`/resume [N]` 的 N clamp 到 [1, RESUME_PAGE_SIZE]。 */
25
25
  const RESUME_PAGE_SIZE = 5;
26
26
  /** /active 卡片每页显示的最大条目数(agent run + bash run 合计)。 */
27
+ /**
28
+ * 分页算术(原先 5 处内联且 clamp 语义漂移:/ls 不 clamp、/active clamp 到
29
+ * totalCount-1 会把过期 offset 泄漏成残页)。统一 clamp 到最后一页边界。
30
+ */
31
+ function pageInfo(totalCount, offset, pageSize) {
32
+ const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
33
+ const maxOffset = Math.max(0, (totalPages - 1) * pageSize);
34
+ const safeOffset = clampInt(offset, 0, maxOffset);
35
+ const currentPage = Math.floor(safeOffset / pageSize) + 1;
36
+ return { totalPages, safeOffset, currentPage };
37
+ }
38
+ /** 在 pageInfo 之上切出当前页条目。 */
39
+ function pageSlice(items, offset, pageSize) {
40
+ const { totalPages, safeOffset, currentPage } = pageInfo(items.length, offset, pageSize);
41
+ return {
42
+ totalCount: items.length,
43
+ totalPages,
44
+ safeOffset,
45
+ currentPage,
46
+ pageItems: items.slice(safeOffset, safeOffset + pageSize),
47
+ hasPagination: items.length > pageSize,
48
+ };
49
+ }
27
50
  const ACTIVE_PAGE_SIZE = 20;
28
51
  /**
29
52
  * /order 列表页大小;指令超过此数量时显示分页导航栏。
@@ -132,6 +155,109 @@ export function isImmediateAction(cmd) {
132
155
  cmd === 'approval.answerNote' ||
133
156
  cmd === 'approval.planFeedback');
134
157
  }
158
+ const QUESTION_INDEX_PRESENT = (v) => v.questionIndex !== undefined;
159
+ const APPROVAL_ACTION_SPECS = {
160
+ 'approval.respond': {
161
+ missing: (v) => !v.runId || v.requestId === undefined || !v.decision || !v.nonce,
162
+ missingToast: '缺少审批响应参数',
163
+ run: (bridge, v) => bridge.handleApprovalRespond({
164
+ runId: v.runId,
165
+ requestId: v.requestId,
166
+ decision: v.decision,
167
+ nonce: v.nonce,
168
+ }),
169
+ successToast: '审批已提交',
170
+ failureToast: (msg) => {
171
+ // 审批已过期:给用户明确反馈,不静默不误导(2026-08-12 事故:点了允许无任何反馈)。
172
+ const content = /state=expired/.test(msg)
173
+ ? '⏰ 审批已过期,无法响应'
174
+ : `审批响应失败:${msg}`;
175
+ return { toast: { type: 'error', content } };
176
+ },
177
+ },
178
+ 'approval.toggle': {
179
+ missing: (v) => !v.runId || v.requestId === undefined || !v.permId,
180
+ missingToast: '缺少权限切换参数',
181
+ run: (bridge, v) => bridge.handleApprovalToggle({
182
+ runId: v.runId,
183
+ requestId: v.requestId,
184
+ permId: v.permId,
185
+ selected: v.selected ?? true,
186
+ }),
187
+ successToast: '已切换',
188
+ toastType: 'info',
189
+ failureToast: (msg) => ({ toast: { type: 'error', content: `权限切换失败:${msg}` } }),
190
+ },
191
+ 'approval.answer': {
192
+ missing: (v) => !v.runId || v.requestId === undefined || !QUESTION_INDEX_PRESENT(v) || !v.option || !v.nonce,
193
+ missingToast: '缺少问题答案参数',
194
+ run: (bridge, v) => bridge.handleApprovalAnswer({
195
+ runId: v.runId,
196
+ requestId: v.requestId,
197
+ questionIndex: v.questionIndex,
198
+ option: v.option,
199
+ nonce: v.nonce,
200
+ }),
201
+ successToast: '已选择',
202
+ },
203
+ 'approval.answerSubmit': {
204
+ missing: (v) => !v.runId || v.requestId === undefined || !QUESTION_INDEX_PRESENT(v) || !v.nonce,
205
+ missingToast: '缺少提交参数',
206
+ run: (bridge, v) => bridge.handleApprovalAnswerSubmit({
207
+ runId: v.runId,
208
+ requestId: v.requestId,
209
+ questionIndex: v.questionIndex,
210
+ nonce: v.nonce,
211
+ }),
212
+ successToast: '答案已提交',
213
+ },
214
+ 'approval.answerCustom': {
215
+ missing: (v) => !v.runId ||
216
+ v.requestId === undefined ||
217
+ !QUESTION_INDEX_PRESENT(v) ||
218
+ !v.nonce ||
219
+ !v.inputValue,
220
+ missingToast: '缺少答案文本参数',
221
+ run: (bridge, v) => bridge.handleApprovalAnswerCustom({
222
+ runId: v.runId,
223
+ requestId: v.requestId,
224
+ questionIndex: v.questionIndex,
225
+ text: v.inputValue,
226
+ nonce: v.nonce,
227
+ }),
228
+ successToast: '答案已提交',
229
+ },
230
+ 'approval.answerNote': {
231
+ missing: (v) => !v.runId ||
232
+ v.requestId === undefined ||
233
+ !QUESTION_INDEX_PRESENT(v) ||
234
+ !v.nonce ||
235
+ !v.inputValue,
236
+ missingToast: '缺少补充说明参数',
237
+ run: (bridge, v) => bridge.handleApprovalAnswerNote({
238
+ runId: v.runId,
239
+ requestId: v.requestId,
240
+ questionIndex: v.questionIndex,
241
+ text: v.inputValue,
242
+ nonce: v.nonce,
243
+ }),
244
+ successToast: '补充说明已保存',
245
+ },
246
+ 'approval.planFeedback': {
247
+ missing: (v) => !v.runId || v.requestId === undefined || !v.nonce || !v.inputValue,
248
+ missingToast: '缺少修改意见参数',
249
+ run: (bridge, v) => bridge.handleApprovalPlanFeedback({
250
+ runId: v.runId,
251
+ requestId: v.requestId,
252
+ text: v.inputValue,
253
+ nonce: v.nonce,
254
+ }),
255
+ successToast: '修改意见已保存',
256
+ failureToast: (msg) => ({ toast: { type: 'error', content: `修改意见保存失败:${msg}` } }),
257
+ },
258
+ };
259
+ /** 卡片 cardAction payload 缺字段的统一报错文案(原先 12 处手写且已漂移出两种前缀)。 */
260
+ const CARD_PAYLOAD_MISSING = '⚠️ 卡片 payload 缺少必要信息';
135
261
  export class CommandRouter {
136
262
  /** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
137
263
  static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
@@ -427,6 +553,14 @@ export class CommandRouter {
427
553
  * Routes to the appropriate bridge method based on value.cmd.
428
554
  * Returns a toast response for immediate user feedback.
429
555
  */
556
+ /** queue.* 卡片动作通用守卫:workspace/messageId 缺失时回复错误并返回 null。 */
557
+ async queuePayloadOrReply(value, ctx) {
558
+ if (!value.workspace || !value.messageId) {
559
+ await this.bridge.sendResult({ text: CARD_PAYLOAD_MISSING }, ctx);
560
+ return null;
561
+ }
562
+ return { workspace: value.workspace, messageId: value.messageId };
563
+ }
430
564
  /** 答案类操作的统一失败反馈:重复投递(同一 nonce 第二次点击)是已生效的
431
565
  * 中性事件,报 error 会误导(首次点击实际已成功)。 */
432
566
  answerFailureToast(msg) {
@@ -436,164 +570,30 @@ export class CommandRouter {
436
570
  return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
437
571
  }
438
572
  async handleApprovalAction(value, _ctx) {
439
- const { runId, requestId, decision, nonce, permId } = value;
440
- if (value.cmd === 'approval.respond') {
441
- if (!runId || requestId === undefined || !decision || !nonce) {
442
- return { toast: { type: 'error', content: '缺少审批响应参数' } };
443
- }
444
- try {
445
- await this.bridge.handleApprovalRespond({
446
- runId,
447
- requestId,
448
- decision,
449
- nonce,
450
- });
451
- return { toast: { type: 'success', content: '审批已提交' } };
452
- }
453
- catch (err) {
454
- const msg = err instanceof Error ? err.message : String(err);
455
- // 审批已过期:给用户明确反馈,不静默不误导(2026-08-12 事故:点了允许无任何反馈)。
456
- const content = /state=expired/.test(msg)
457
- ? '⏰ 审批已过期,无法响应'
458
- : `审批响应失败:${msg}`;
459
- return { toast: { type: 'error', content } };
460
- }
573
+ const spec = APPROVAL_ACTION_SPECS[value.cmd];
574
+ if (!spec) {
575
+ return { toast: { type: 'error', content: '未知的审批操作' } };
461
576
  }
462
- if (value.cmd === 'approval.toggle') {
463
- if (!runId || requestId === undefined || !permId) {
464
- return { toast: { type: 'error', content: '缺少权限切换参数' } };
465
- }
466
- try {
467
- await this.bridge.handleApprovalToggle({
468
- runId,
469
- requestId,
470
- permId,
471
- selected: value.selected ?? true,
472
- });
473
- return { toast: { type: 'info', content: '已切换' } };
474
- }
475
- catch (err) {
476
- const msg = err instanceof Error ? err.message : String(err);
477
- return { toast: { type: 'error', content: `权限切换失败:${msg}` } };
478
- }
479
- }
480
- if (value.cmd === 'approval.answer') {
481
- if (!runId ||
482
- requestId === undefined ||
483
- value.questionIndex === undefined ||
484
- !value.option ||
485
- !nonce) {
486
- return { toast: { type: 'error', content: '缺少问题答案参数' } };
487
- }
488
- try {
489
- await this.bridge.handleApprovalAnswer({
490
- runId,
491
- requestId,
492
- questionIndex: value.questionIndex,
493
- option: value.option,
494
- nonce,
495
- });
496
- return { toast: { type: 'success', content: '已选择' } };
497
- }
498
- catch (err) {
499
- const msg = err instanceof Error ? err.message : String(err);
500
- return this.answerFailureToast(msg);
501
- }
502
- }
503
- if (value.cmd === 'approval.answerSubmit') {
504
- if (!runId || requestId === undefined || value.questionIndex === undefined || !nonce) {
505
- return { toast: { type: 'error', content: '缺少提交参数' } };
506
- }
507
- try {
508
- await this.bridge.handleApprovalAnswerSubmit({
509
- runId,
510
- requestId,
511
- questionIndex: value.questionIndex,
512
- nonce,
513
- });
514
- return { toast: { type: 'success', content: '答案已提交' } };
515
- }
516
- catch (err) {
517
- const msg = err instanceof Error ? err.message : String(err);
518
- return this.answerFailureToast(msg);
519
- }
577
+ if (spec.missing(value)) {
578
+ return { toast: { type: 'error', content: spec.missingToast } };
520
579
  }
521
- if (value.cmd === 'approval.answerCustom') {
522
- if (!runId ||
523
- requestId === undefined ||
524
- value.questionIndex === undefined ||
525
- !nonce ||
526
- !value.inputValue) {
527
- return { toast: { type: 'error', content: '缺少答案文本参数' } };
528
- }
529
- try {
530
- await this.bridge.handleApprovalAnswerCustom({
531
- runId,
532
- requestId,
533
- questionIndex: value.questionIndex,
534
- text: value.inputValue,
535
- nonce,
536
- });
537
- return { toast: { type: 'success', content: '答案已提交' } };
538
- }
539
- catch (err) {
540
- const msg = err instanceof Error ? err.message : String(err);
541
- return this.answerFailureToast(msg);
542
- }
543
- }
544
- if (value.cmd === 'approval.answerNote') {
545
- if (!runId ||
546
- requestId === undefined ||
547
- value.questionIndex === undefined ||
548
- !nonce ||
549
- !value.inputValue) {
550
- return { toast: { type: 'error', content: '缺少补充说明参数' } };
551
- }
552
- try {
553
- await this.bridge.handleApprovalAnswerNote({
554
- runId,
555
- requestId,
556
- questionIndex: value.questionIndex,
557
- text: value.inputValue,
558
- nonce,
559
- });
560
- return { toast: { type: 'success', content: '补充说明已保存' } };
561
- }
562
- catch (err) {
563
- const msg = err instanceof Error ? err.message : String(err);
564
- return this.answerFailureToast(msg);
565
- }
580
+ try {
581
+ await spec.run(this.bridge, value);
582
+ return { toast: { type: spec.toastType ?? 'success', content: spec.successToast } };
566
583
  }
567
- if (value.cmd === 'approval.planFeedback') {
568
- if (!runId || requestId === undefined || !nonce || !value.inputValue) {
569
- return { toast: { type: 'error', content: '缺少修改意见参数' } };
570
- }
571
- try {
572
- await this.bridge.handleApprovalPlanFeedback({
573
- runId,
574
- requestId,
575
- text: value.inputValue,
576
- nonce,
577
- });
578
- return { toast: { type: 'success', content: '修改意见已保存' } };
579
- }
580
- catch (err) {
581
- const msg = err instanceof Error ? err.message : String(err);
582
- return { toast: { type: 'error', content: `修改意见保存失败:${msg}` } };
583
- }
584
+ catch (err) {
585
+ const msg = err instanceof Error ? err.message : String(err);
586
+ return spec.failureToast ? spec.failureToast(msg) : this.answerFailureToast(msg);
584
587
  }
585
- return { toast: { type: 'error', content: '未知的审批操作' } };
586
588
  }
587
589
  /**
588
590
  * Handle queue.cancel: remove this message from the queue.
589
591
  */
590
592
  async handleQueueCancel(value, ctx) {
591
- const workspace = value.workspace;
592
- const messageId = value.messageId;
593
- if (!workspace || !messageId) {
594
- await this.bridge.sendResult({ text: '⚠️ 卡片 payload 缺少必要信息' }, ctx);
593
+ const payload = await this.queuePayloadOrReply(value, ctx);
594
+ if (!payload)
595
595
  return;
596
- }
596
+ const { workspace, messageId } = payload;
597
597
  const removed = this.bridge.removeFromQueue(workspace, messageId);
598
598
  if (removed) {
599
599
  // Update the queue card in-place to "cancelled" state
@@ -610,12 +610,10 @@ export class CommandRouter {
610
610
  * Instead of asking user to resend, we keep the target task in queue and let it execute.
611
611
  */
612
612
  async handleQueueImmediate(value, ctx) {
613
- const workspace = value.workspace;
614
- const messageId = value.messageId;
615
- if (!workspace || !messageId) {
616
- await this.bridge.sendResult({ text: '卡片 payload 缺少必要信息' }, ctx);
613
+ const payload = await this.queuePayloadOrReply(value, ctx);
614
+ if (!payload)
617
615
  return;
618
- }
616
+ const { workspace, messageId } = payload;
619
617
  // 1. Check target exists BEFORE any await. The queue chain can advance
620
618
  // while later awaits (interruptCurrentRun / markQueueCardExecuting's card
621
619
  // send) are in flight, so the target must be read synchronously at entry.
@@ -751,13 +749,11 @@ export class CommandRouter {
751
749
  * Handle queue.diagnose: show diagnostic info for why message is queuing.
752
750
  */
753
751
  async handleQueueDiagnose(value, ctx) {
754
- const workspace = value.workspace;
755
- const messageId = value.messageId;
756
- const targetUserId = value.userId ?? ctx.userId;
757
- if (!workspace || !messageId) {
758
- await this.bridge.sendResult({ text: '卡片 payload 缺少必要信息' }, ctx);
752
+ const payload = await this.queuePayloadOrReply(value, ctx);
753
+ if (!payload)
759
754
  return;
760
- }
755
+ const { workspace, messageId } = payload;
756
+ const targetUserId = value.userId ?? ctx.userId;
761
757
  const task = this.bridge.getQueuedTask(workspace, messageId);
762
758
  const queueInfo = this.bridge.getQueueInfo(workspace);
763
759
  const activeRuns = this.bridge.getAllActiveRuns();
@@ -825,12 +821,10 @@ export class CommandRouter {
825
821
  * Handle queue.edit: show input field for editing the queued message.
826
822
  */
827
823
  async handleQueueEdit(value, ctx) {
828
- const workspace = value.workspace;
829
- const messageId = value.messageId;
830
- if (!workspace || !messageId) {
831
- await this.bridge.sendResult({ text: '⚠️ 卡片 payload 缺少必要信息' }, ctx);
824
+ const payload = await this.queuePayloadOrReply(value, ctx);
825
+ if (!payload)
832
826
  return;
833
- }
827
+ const { workspace, messageId } = payload;
834
828
  // Get current message preview
835
829
  const task = this.bridge.getQueuedTask(workspace, messageId);
836
830
  if (!task) {
@@ -896,13 +890,11 @@ export class CommandRouter {
896
890
  * when the callback response has no `card` field).
897
891
  */
898
892
  async handleQueueInput(value, ctx) {
899
- const workspace = value.workspace;
900
- const messageId = value.messageId;
901
893
  const newMessage = value.inputValue ?? value.formValue?.['newMessage'];
902
- if (!workspace || !messageId) {
903
- await this.bridge.sendResult({ text: '⚠️ 卡片 payload 缺少必要信息' }, ctx);
894
+ const payload = await this.queuePayloadOrReply(value, ctx);
895
+ if (!payload)
904
896
  return;
905
- }
897
+ const { workspace, messageId } = payload;
906
898
  if (!newMessage) {
907
899
  await this.bridge.sendResult({ text: '⚠️ 缺少新消息内容' }, ctx);
908
900
  return;
@@ -1173,7 +1165,7 @@ export class CommandRouter {
1173
1165
  async handleOrderDelete(value, ctx) {
1174
1166
  const orderId = value.orderId;
1175
1167
  if (!orderId) {
1176
- return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
1168
+ return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1177
1169
  }
1178
1170
  if (!this.orderStore.has(orderId)) {
1179
1171
  return { toast: { type: 'error', content: '指令不存在或已被删除' } };
@@ -1201,17 +1193,22 @@ export class CommandRouter {
1201
1193
  * Handle order.aliasEdit: 展示别名编辑卡(input + ✓ 提交图标)。
1202
1194
  * 有别名时预填;提交 `order.aliasInput` 处理(含校验与唯一性)。
1203
1195
  */
1204
- async handleOrderAliasEdit(value, ctx) {
1196
+ /**
1197
+ * order 编辑卡公共骨架:payload 校验 → 查指令 → 构造 input 编辑卡 → 原地更新。
1198
+ * handleOrderAliasEdit / handleOrderTextEdit 原先各持一份 ~90% 相同的 4 段结构。
1199
+ */
1200
+ async showOrderEditCard(value, ctx, opts) {
1205
1201
  const orderId = value.orderId;
1206
1202
  const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
1207
1203
  if (!orderId) {
1208
- return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
1204
+ return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1209
1205
  }
1210
1206
  this.orderStore.reload();
1211
1207
  const order = this.orderStore.get().find((o) => o.id === orderId);
1212
1208
  if (!order) {
1213
1209
  return { toast: { type: 'error', content: '指令不存在或已被删除' } };
1214
1210
  }
1211
+ // 列表卡显示的是截断版(≤100 字符 + ...),编辑卡预览保持一致
1215
1212
  const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;
1216
1213
  // CardKit 2.0 input 自带 ✓ 提交图标(input_value 经 raw 回传,红线)。
1217
1214
  // 不用 form 容器(触发 300123 无 submit button / 200621 嵌套 column)。
@@ -1220,7 +1217,7 @@ export class CommandRouter {
1220
1217
  config: { wide_screen_mode: true },
1221
1218
  header: {
1222
1219
  template: 'blue',
1223
- title: { tag: 'plain_text', content: '✏️ 给指令起别名' },
1220
+ title: { tag: 'plain_text', content: opts.title },
1224
1221
  },
1225
1222
  body: {
1226
1223
  elements: [
@@ -1231,10 +1228,7 @@ export class CommandRouter {
1231
1228
  { tag: 'hr' },
1232
1229
  {
1233
1230
  tag: 'div',
1234
- text: {
1235
- tag: 'lark_md',
1236
- content: '💡 输入别名名后点击右侧 ✓ 提交;留空提交 = 删除该别名',
1237
- },
1231
+ text: { tag: 'lark_md', content: opts.hint },
1238
1232
  },
1239
1233
  {
1240
1234
  tag: 'column_set',
@@ -1246,13 +1240,14 @@ export class CommandRouter {
1246
1240
  elements: [
1247
1241
  {
1248
1242
  tag: 'input',
1249
- name: 'aliasName',
1250
- placeholder: { tag: 'plain_text', content: '输入 $别名名(如 all)...' },
1251
- default_value: order.alias ?? '',
1243
+ name: opts.inputName,
1244
+ placeholder: { tag: 'plain_text', content: opts.placeholder },
1245
+ default_value: opts.defaultValue,
1246
+ ...(opts.maxLength !== undefined ? { max_length: opts.maxLength } : {}),
1252
1247
  behaviors: [
1253
1248
  {
1254
1249
  type: 'callback',
1255
- value: { cmd: 'order.aliasInput', orderId, offset },
1250
+ value: { cmd: opts.inputCmd, orderId, offset },
1256
1251
  },
1257
1252
  ],
1258
1253
  },
@@ -1265,6 +1260,18 @@ export class CommandRouter {
1265
1260
  };
1266
1261
  await this.bridge.updateCardInPlace(editCard, ctx);
1267
1262
  }
1263
+ async handleOrderAliasEdit(value, ctx) {
1264
+ this.orderStore.reload();
1265
+ const order = this.orderStore.get().find((o) => o.id === value.orderId);
1266
+ return this.showOrderEditCard(value, ctx, {
1267
+ title: '✏️ 给指令起别名',
1268
+ hint: '💡 输入别名名后点击右侧 ✓ 提交;留空提交 = 删除该别名',
1269
+ inputName: 'aliasName',
1270
+ placeholder: '输入 $别名名(如 all)...',
1271
+ defaultValue: order?.alias ?? '',
1272
+ inputCmd: 'order.aliasInput',
1273
+ });
1274
+ }
1268
1275
  /**
1269
1276
  * Handle order.aliasInput: 提交别名(绑/改/解绑),随后重渲染 /order 列表卡。
1270
1277
  * 留空 = 解绑;非空按命名规则 + 全局唯一校验,失败仅 toast 不重渲染。
@@ -1274,7 +1281,7 @@ export class CommandRouter {
1274
1281
  const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
1275
1282
  const name = value.inputValue ?? value.formValue?.['aliasName'];
1276
1283
  if (!orderId) {
1277
- return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
1284
+ return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1278
1285
  }
1279
1286
  this.orderStore.reload();
1280
1287
  const order = this.orderStore.get().find((o) => o.id === orderId);
@@ -1308,7 +1315,7 @@ export class CommandRouter {
1308
1315
  const orderId = value.orderId;
1309
1316
  const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
1310
1317
  if (!orderId) {
1311
- return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
1318
+ return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1312
1319
  }
1313
1320
  this.orderStore.reload();
1314
1321
  const order = this.orderStore.get().find((o) => o.id === orderId);
@@ -1335,73 +1342,20 @@ export class CommandRouter {
1335
1342
  * updateCardInPlace)。
1336
1343
  */
1337
1344
  async handleOrderTextEdit(value, ctx) {
1338
- const orderId = value.orderId;
1339
- const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
1340
- if (!orderId) {
1341
- return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
1342
- }
1343
1345
  this.orderStore.reload();
1344
- const order = this.orderStore.get().find((o) => o.id === orderId);
1345
- if (!order) {
1346
- return { toast: { type: 'error', content: '指令不存在或已被删除' } };
1347
- }
1348
- // 列表卡显示的是截断版(≤100 字符 + ...),编辑卡预览也保持一致;
1349
- // input 组件 default_value 预填完整 text(不受截断影响),用户提交后生效完整文本。
1350
- const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;
1351
- // CardKit 2.0 input 自带 ✓ 提交图标(input_value 经 raw 回传,红线)。
1352
- // 不用 form 容器(触发 300123 无 submit button / 200621 嵌套 column)。
1353
- const editCard = {
1354
- schema: '2.0',
1355
- config: { wide_screen_mode: true },
1356
- header: {
1357
- template: 'blue',
1358
- title: { tag: 'plain_text', content: '✏️ 编辑指令' },
1359
- },
1360
- body: {
1361
- elements: [
1362
- {
1363
- tag: 'div',
1364
- text: { tag: 'lark_md', content: `**当前指令:**\n\`${displayText}\`` },
1365
- },
1366
- { tag: 'hr' },
1367
- {
1368
- tag: 'div',
1369
- text: {
1370
- tag: 'lark_md',
1371
- content: '💡 修改后点击右侧 ✓ 提交;空白提交 = 报错;最多 200 字符',
1372
- },
1373
- },
1374
- {
1375
- tag: 'column_set',
1376
- columns: [
1377
- {
1378
- tag: 'column',
1379
- width: 'weighted',
1380
- weight: 3,
1381
- elements: [
1382
- {
1383
- tag: 'input',
1384
- name: 'text',
1385
- placeholder: { tag: 'plain_text', content: '输入新的指令文本...' },
1386
- default_value: order.text,
1387
- // max_length 与 MAX_TEXT_LENGTH=200 对齐:超长在输入侧直接截断,
1388
- // 避免提交后才报错(后端 updateText 仍会二次校验,双保险)。
1389
- max_length: 200,
1390
- behaviors: [
1391
- {
1392
- type: 'callback',
1393
- value: { cmd: 'order.textInput', orderId, offset },
1394
- },
1395
- ],
1396
- },
1397
- ],
1398
- },
1399
- ],
1400
- },
1401
- ],
1402
- },
1403
- };
1404
- await this.bridge.updateCardInPlace(editCard, ctx);
1346
+ const order = this.orderStore.get().find((o) => o.id === value.orderId);
1347
+ // input 组件 default_value 预填完整 text(不受截断影响),提交后生效完整文本
1348
+ return this.showOrderEditCard(value, ctx, {
1349
+ title: '✏️ 编辑指令',
1350
+ hint: '💡 修改后点击右侧 ✓ 提交;空白提交 = 报错;最多 200 字符',
1351
+ inputName: 'text',
1352
+ placeholder: '输入新的指令文本...',
1353
+ defaultValue: order?.text ?? '',
1354
+ inputCmd: 'order.textInput',
1355
+ // 与 MAX_TEXT_LENGTH=200 对齐:超长在输入侧直接截断,避免提交后才报错
1356
+ // (后端 updateText 仍会二次校验,双保险)
1357
+ maxLength: 200,
1358
+ });
1405
1359
  }
1406
1360
  /**
1407
1361
  * Handle order.textInput: 提交指令文本编辑。校验 orderId → OrderStore.updateText
@@ -1415,7 +1369,7 @@ export class CommandRouter {
1415
1369
  const offset = Math.max(0, Math.trunc(Number(value.offset) || 0));
1416
1370
  const raw = value.inputValue ?? value.formValue?.['text'];
1417
1371
  if (!orderId) {
1418
- return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
1372
+ return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1419
1373
  }
1420
1374
  this.orderStore.reload();
1421
1375
  const order = this.orderStore.get().find((o) => o.id === orderId);
@@ -1494,7 +1448,7 @@ export class CommandRouter {
1494
1448
  async handleWsRemove(value, ctx) {
1495
1449
  const name = value.name;
1496
1450
  if (!name) {
1497
- return { toast: { type: 'error', content: '卡片 payload 缺少必要信息' } };
1451
+ return { toast: { type: 'error', content: CARD_PAYLOAD_MISSING } };
1498
1452
  }
1499
1453
  // Execute the removal (consumes the write-side effect; the returned text
1500
1454
  // is discarded — feedback flows through the toast + refreshed card).
@@ -2557,12 +2511,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2557
2511
  size: fs.statSync(path.join(targetDir, f.name)).size,
2558
2512
  })),
2559
2513
  ];
2560
- // Pagination calculations
2561
- const totalCount = allItems.length;
2562
- const totalPages = Math.max(1, Math.ceil(totalCount / CommandRouter.LS_PAGE_SIZE));
2563
- const currentPage = Math.floor(offset / CommandRouter.LS_PAGE_SIZE) + 1;
2564
- const pageItems = allItems.slice(offset, offset + CommandRouter.LS_PAGE_SIZE);
2565
- const hasPagination = totalCount > CommandRouter.LS_PAGE_SIZE;
2514
+ // Pagination calculations(clamp 到最后一页边界,原先不 clamp 会显示空页)
2515
+ const { totalCount, totalPages, safeOffset, currentPage, pageItems, hasPagination } = pageSlice(allItems, offset, CommandRouter.LS_PAGE_SIZE);
2566
2516
  // Check if we need to show parent directory button
2567
2517
  const parentDir = path.dirname(targetDir);
2568
2518
  const hasParent = parentDir !== targetDir;
@@ -2593,7 +2543,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2593
2543
  text: { tag: 'plain_text', content: '刷新' },
2594
2544
  type: 'default',
2595
2545
  size: 'small',
2596
- behaviors: [{ type: 'callback', value: { cmd: 'ls.refresh', path: targetDir, offset } }],
2546
+ behaviors: [
2547
+ { type: 'callback', value: { cmd: 'ls.refresh', path: targetDir, offset: safeOffset } },
2548
+ ],
2597
2549
  });
2598
2550
  // Show "切换" button when viewing a subdirectory (to switch cwd to this directory)
2599
2551
  if (isSubdir) {
@@ -2701,7 +2653,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2701
2653
  if (hasPagination) {
2702
2654
  const bar = paginationBar({
2703
2655
  cmd: 'ls.page',
2704
- offset,
2656
+ offset: safeOffset,
2705
2657
  pageSize: CommandRouter.LS_PAGE_SIZE,
2706
2658
  total: totalCount,
2707
2659
  extra: { path: targetDir },
@@ -2795,13 +2747,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2795
2747
  }
2796
2748
  // Pagination calculations (mirror cmdOrder): clamp stale/out-of-range
2797
2749
  // offsets so prev/next always step by WS_PAGE_SIZE.
2798
- const totalCount = entries.length;
2799
- const totalPages = Math.max(1, Math.ceil(totalCount / WS_PAGE_SIZE));
2800
- const maxOffset = Math.max(0, (totalPages - 1) * WS_PAGE_SIZE);
2801
- const safeOffset = clampInt(offset, 0, maxOffset);
2802
- const currentPage = Math.floor(safeOffset / WS_PAGE_SIZE) + 1;
2803
- const pageEntries = entries.slice(safeOffset, safeOffset + WS_PAGE_SIZE);
2804
- const hasPagination = totalCount > WS_PAGE_SIZE;
2750
+ const { totalCount, totalPages, safeOffset, currentPage, pageItems: pageEntries, hasPagination, } = pageSlice(entries, offset, WS_PAGE_SIZE);
2805
2751
  // Build body elements: current cwd + workspace list with dividers
2806
2752
  const bodyElements = [];
2807
2753
  // Current working directory section
@@ -3108,12 +3054,19 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3108
3054
  const sessionMeta = new Map();
3109
3055
  const prefetchCount = Math.min(RESUME_CONTENT_PREFETCH, allSessions.length);
3110
3056
  for (const s of allSessions.slice(0, prefetchCount)) {
3111
- // Use the same reader as the listSessions call above
3112
- const content = reader.readSessionContent(s.sessionId, cwd);
3057
+ // 优先走轻量摘要(只取标题/用量,不构造 events)——codex 实现了
3058
+ // readSessionSummary,避免对前 N 条各做一次全量 JSONL 解析来构造
3059
+ // 立即被丢弃的 events 数组。其他 agent 无此方法时回退全量读取再取字段。
3060
+ const meta = reader.readSessionSummary
3061
+ ? reader.readSessionSummary(s.sessionId, cwd)
3062
+ : (() => {
3063
+ const c = reader.readSessionContent(s.sessionId, cwd);
3064
+ return { aiTitle: c.aiTitle, recap: c.recap, displayTitle: c.displayTitle };
3065
+ })();
3113
3066
  sessionMeta.set(s.sessionId, {
3114
- aiTitle: content.aiTitle,
3115
- recap: content.recap,
3116
- displayTitle: content.displayTitle,
3067
+ aiTitle: meta.aiTitle,
3068
+ recap: meta.recap,
3069
+ displayTitle: meta.displayTitle,
3117
3070
  });
3118
3071
  }
3119
3072
  const now = Date.now();
@@ -3207,8 +3160,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3207
3160
  }
3208
3161
  // Pagination bar (only shown when there are more sessions than pageSize)
3209
3162
  if (total > pageSize) {
3210
- const totalPages = Math.ceil(total / pageSize);
3211
- const currentPage = Math.floor(pageOffset / pageSize) + 1;
3163
+ const { totalPages, currentPage } = pageInfo(total, pageOffset, pageSize);
3212
3164
  const bar = paginationBar({
3213
3165
  cmd: 'resume.page',
3214
3166
  offset: pageOffset,
@@ -3252,9 +3204,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3252
3204
  buildActiveCardFromMemory(activeRuns, activeBashRuns, offset = 0) {
3253
3205
  const totalCount = activeRuns.length + activeBashRuns.length;
3254
3206
  const pageSize = ACTIVE_PAGE_SIZE;
3255
- const safeOffset = Math.max(0, Math.min(offset, Math.max(0, totalCount - 1)));
3256
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
3257
- const currentPage = Math.floor(safeOffset / pageSize) + 1;
3207
+ // 统一 clamp 到最后一页边界(原先 clamp 到 totalCount-1,过期 offset 会
3208
+ // 泄漏成「第 1 页只显示末尾几条」的残页)
3209
+ const { totalPages, safeOffset, currentPage } = pageInfo(totalCount, offset, pageSize);
3258
3210
  // Slice: distribute offset across agent runs first, then bash runs
3259
3211
  const elements = [];
3260
3212
  // Page info
@@ -3569,14 +3521,7 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3569
3521
  }
3570
3522
  else {
3571
3523
  // Pagination calculations
3572
- const totalCount = allOrders.length;
3573
- const totalPages = Math.max(1, Math.ceil(totalCount / ORDER_PAGE_SIZE));
3574
- // Clamp offset to last page boundary to avoid empty pages
3575
- const maxOffset = Math.max(0, (totalPages - 1) * ORDER_PAGE_SIZE);
3576
- const safeOffset = clampInt(offset, 0, maxOffset);
3577
- const currentPage = Math.floor(safeOffset / ORDER_PAGE_SIZE) + 1;
3578
- const pageOrders = allOrders.slice(safeOffset, safeOffset + ORDER_PAGE_SIZE);
3579
- const hasPagination = totalCount > ORDER_PAGE_SIZE;
3524
+ const { totalCount, totalPages, safeOffset, currentPage, pageItems: pageOrders, hasPagination, } = pageSlice(allOrders, offset, ORDER_PAGE_SIZE);
3580
3525
  for (let i = 0; i < pageOrders.length; i++) {
3581
3526
  const order = pageOrders[i];
3582
3527
  const displayText = order.text.length > 100 ? order.text.slice(0, 97) + '...' : order.text;