lark-remote 0.5.10 → 0.5.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 (126) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/binder.js +7 -1
  5. package/dist/bridge/approval-coordinator.d.ts +1 -0
  6. package/dist/bridge/approval-coordinator.js +12 -24
  7. package/dist/bridge/index.d.ts +7 -0
  8. package/dist/bridge/index.js +89 -230
  9. package/dist/bridge/run-approval.d.ts +15 -0
  10. package/dist/bridge/run-approval.js +41 -0
  11. package/dist/bridge/run-usage.d.ts +46 -0
  12. package/dist/bridge/run-usage.js +96 -0
  13. package/dist/bridge/session-writeback.d.ts +18 -0
  14. package/dist/bridge/session-writeback.js +36 -0
  15. package/dist/card/approval-render.d.ts +14 -0
  16. package/dist/card/approval-render.js +155 -29
  17. package/dist/card/bash-card-session.js +10 -5
  18. package/dist/card/bash-renderer.js +11 -23
  19. package/dist/card/card-budget.js +24 -4
  20. package/dist/card/card-session.d.ts +2 -0
  21. package/dist/card/card-session.js +20 -22
  22. package/dist/card/card-shared.d.ts +4 -4
  23. package/dist/card/card-shared.js +3 -9
  24. package/dist/card/collapsible.js +3 -2
  25. package/dist/card/final-answer-overflow.d.ts +7 -0
  26. package/dist/card/final-answer-overflow.js +46 -0
  27. package/dist/card/run-card-session.js +2 -7
  28. package/dist/card/run-renderer.d.ts +14 -0
  29. package/dist/card/run-renderer.js +150 -10
  30. package/dist/card/run-state.d.ts +12 -1
  31. package/dist/card/run-state.js +73 -34
  32. package/dist/common/text-width.js +5 -0
  33. package/dist/common/tool-summary.js +4 -2
  34. package/dist/config/codex-config.js +5 -3
  35. package/dist/config/index.d.ts +4 -2
  36. package/dist/config/index.js +19 -5
  37. package/dist/inbound/turn-assembler.js +6 -0
  38. package/dist/index.js +15 -391
  39. package/dist/instance-lock.d.ts +9 -1
  40. package/dist/instance-lock.js +42 -7
  41. package/dist/message-handlers.d.ts +26 -0
  42. package/dist/message-handlers.js +275 -0
  43. package/dist/platform/identity.js +4 -3
  44. package/dist/platform/path.js +3 -1
  45. package/dist/platform/terminator-win32.d.ts +13 -1
  46. package/dist/platform/terminator-win32.js +4 -14
  47. package/dist/router/card-action-payload.d.ts +1 -1
  48. package/dist/router/card-action-payload.js +1 -1
  49. package/dist/router/commands/ls.d.ts +9 -0
  50. package/dist/router/commands/ls.js +243 -0
  51. package/dist/router/commands/order.d.ts +12 -0
  52. package/dist/router/commands/order.js +178 -0
  53. package/dist/router/commands/resume.d.ts +47 -0
  54. package/dist/router/commands/resume.js +246 -0
  55. package/dist/router/commands/ws.d.ts +15 -0
  56. package/dist/router/commands/ws.js +189 -0
  57. package/dist/router/config/claude.js +3 -3
  58. package/dist/router/config/codex.js +2 -6
  59. package/dist/router/config/common/turn-idle.d.ts +2 -0
  60. package/dist/router/config/common/turn-idle.js +9 -0
  61. package/dist/router/config/kimi.js +2 -6
  62. package/dist/router/config/opencode.js +2 -6
  63. package/dist/router/index.d.ts +14 -22
  64. package/dist/router/index.js +93 -1133
  65. package/dist/router/paging.d.ts +13 -0
  66. package/dist/router/paging.js +20 -0
  67. package/dist/router/path-validation.d.ts +27 -0
  68. package/dist/router/path-validation.js +45 -0
  69. package/dist/runner/claude/runner.d.ts +3 -4
  70. package/dist/runner/claude/runner.js +4 -5
  71. package/dist/runner/claude/session.js +1 -1
  72. package/dist/runner/codex/app-server/runner.js +12 -0
  73. package/dist/runner/codex/app-server/translator.d.ts +2 -0
  74. package/dist/runner/codex/app-server/translator.js +65 -9
  75. package/dist/runner/common/acp/base-translator.d.ts +15 -8
  76. package/dist/runner/common/acp/base-translator.js +38 -7
  77. package/dist/runner/common/jsonl-stream.js +12 -1
  78. package/dist/runner/common/jsonrpc/connection-manager.js +11 -1
  79. package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
  80. package/dist/runner/common/jsonrpc/transport.js +7 -1
  81. package/dist/runner/common/spawning-runner.js +10 -4
  82. package/dist/runner/common/turn-started.d.ts +2 -0
  83. package/dist/runner/common/turn-started.js +10 -0
  84. package/dist/runner/dsh/approval.js +16 -3
  85. package/dist/runner/dsh/auth.js +2 -3
  86. package/dist/runner/dsh/client.d.ts +32 -7
  87. package/dist/runner/dsh/client.js +115 -149
  88. package/dist/runner/dsh/daemon.d.ts +0 -2
  89. package/dist/runner/dsh/daemon.js +1 -6
  90. package/dist/runner/dsh/errors.d.ts +5 -0
  91. package/dist/runner/dsh/errors.js +11 -0
  92. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  93. package/dist/runner/dsh/exit-cancel.js +63 -24
  94. package/dist/runner/dsh/home.d.ts +2 -0
  95. package/dist/runner/dsh/home.js +8 -0
  96. package/dist/runner/dsh/inflight.d.ts +0 -2
  97. package/dist/runner/dsh/inflight.js +1 -1
  98. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  99. package/dist/runner/dsh/mux-stream.js +157 -0
  100. package/dist/runner/dsh/runner.d.ts +7 -0
  101. package/dist/runner/dsh/runner.js +42 -5
  102. package/dist/runner/dsh/translator.d.ts +0 -10
  103. package/dist/runner/dsh/translator.js +1 -1
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.js +42 -5
  106. package/dist/runner/kimi/acp/translator.d.ts +8 -0
  107. package/dist/runner/kimi/acp/translator.js +11 -12
  108. package/dist/runner/opencode/acp/translator.js +1 -12
  109. package/dist/runner/pi/rpc/runner.js +33 -5
  110. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  111. package/dist/runner/pi/rpc/translator.js +2 -7
  112. package/dist/runner/registry.d.ts +0 -19
  113. package/dist/runner/registry.js +0 -32
  114. package/dist/runner/resolve-agent-choices.js +5 -1
  115. package/dist/runner/sync-agent-choices.js +4 -1
  116. package/dist/runner/types.d.ts +17 -1
  117. package/dist/session/claude/session-index.js +13 -4
  118. package/dist/session/codex/rollout-reader.js +51 -26
  119. package/dist/session/common/usage-accumulator.d.ts +10 -1
  120. package/dist/session/common/usage-accumulator.js +10 -2
  121. package/dist/session/dsh/sessions.js +29 -9
  122. package/dist/session/kimi/sessions.js +11 -7
  123. package/dist/session/opencode/sessions.js +15 -7
  124. package/dist/session/pi/sessions.js +3 -2
  125. package/dist/session/session-store.js +4 -3
  126. package/package.json +1 -1
@@ -0,0 +1,96 @@
1
+ import { normalizeResultUsage } from '../runner/common/result-usage.js';
2
+ export class RunUsageCollector {
3
+ contextLength;
4
+ finalContextLength;
5
+ finalContextLimit;
6
+ finalCompactCount;
7
+ finalCacheReadTokens;
8
+ finalCacheCreationTokens;
9
+ finalTotalTokens;
10
+ liveInput;
11
+ liveOutput;
12
+ liveContextLimit;
13
+ finalInputTokens;
14
+ finalOutputTokens;
15
+ finalCumulativeTotalTokens;
16
+ finalCumulativeInputTokens;
17
+ finalCumulativeOutputTokens;
18
+ finalCumulativeCacheReadTokens;
19
+ finalCumulativeCacheCreationTokens;
20
+ finalReasoningTokens;
21
+ resultNotSuccess = false;
22
+ get liveInputTokens() {
23
+ return this.liveInput;
24
+ }
25
+ get liveOutputTokens() {
26
+ return this.liveOutput;
27
+ }
28
+ observeCompaction(postTokens) {
29
+ this.contextLength = postTokens;
30
+ }
31
+ observeResult(event) {
32
+ this.resultNotSuccess = event.subtype !== 'success';
33
+ if (!event.usage)
34
+ return;
35
+ const usage = normalizeResultUsage(event.usage);
36
+ if (!this.contextLength)
37
+ this.contextLength = usage.contextLength;
38
+ this.liveInput = usage.inputTokens;
39
+ this.liveOutput = usage.outputTokens;
40
+ this.liveContextLimit = usage.contextLimit;
41
+ if (usage.cacheReadTokens !== undefined)
42
+ this.finalCacheReadTokens = usage.cacheReadTokens;
43
+ if (usage.cacheCreationTokens !== undefined) {
44
+ this.finalCacheCreationTokens = usage.cacheCreationTokens;
45
+ }
46
+ if (usage.totalTokens !== undefined)
47
+ this.finalTotalTokens = usage.totalTokens;
48
+ if (usage.reasoningTokens !== undefined)
49
+ this.finalReasoningTokens = usage.reasoningTokens;
50
+ }
51
+ finalize(finalUsage, authority) {
52
+ this.finalContextLength = finalUsage?.contextLength ?? this.contextLength;
53
+ this.finalContextLimit = finalUsage?.contextLimit ?? this.liveContextLimit;
54
+ this.finalCompactCount = finalUsage?.compactCount;
55
+ this.finalCumulativeTotalTokens = finalUsage?.cumulativeTotalTokens;
56
+ this.finalCumulativeInputTokens = finalUsage?.cumulativeInputTokens;
57
+ this.finalCumulativeOutputTokens = finalUsage?.cumulativeOutputTokens;
58
+ this.finalCumulativeCacheReadTokens = finalUsage?.cumulativeCacheReadTokens;
59
+ this.finalCumulativeCacheCreationTokens = finalUsage?.cumulativeCacheCreationTokens;
60
+ const useLiveFlowScope = authority !== 'jsonl' && this.liveInput !== undefined && this.liveOutput !== undefined;
61
+ if (useLiveFlowScope) {
62
+ this.finalInputTokens = this.liveInput;
63
+ this.finalOutputTokens = this.liveOutput;
64
+ return;
65
+ }
66
+ this.finalInputTokens = finalUsage?.inputTokens ?? this.liveInput;
67
+ this.finalOutputTokens = finalUsage?.outputTokens ?? this.liveOutput;
68
+ this.finalCacheReadTokens = finalUsage?.cacheReadTokens ?? this.finalCacheReadTokens;
69
+ this.finalCacheCreationTokens =
70
+ finalUsage?.cacheCreationTokens ?? this.finalCacheCreationTokens;
71
+ this.finalTotalTokens = finalUsage?.totalTokens ?? this.finalTotalTokens;
72
+ }
73
+ meta() {
74
+ return {
75
+ contextLength: this.finalContextLength,
76
+ contextLimit: this.finalContextLimit,
77
+ compactCount: this.finalCompactCount,
78
+ cacheReadTokens: this.finalCacheReadTokens,
79
+ cacheCreationTokens: this.finalCacheCreationTokens,
80
+ totalTokens: this.finalTotalTokens,
81
+ inputTokens: this.finalInputTokens,
82
+ outputTokens: this.finalOutputTokens,
83
+ cumulativeTotalTokens: this.finalCumulativeTotalTokens,
84
+ cumulativeInputTokens: this.finalCumulativeInputTokens,
85
+ cumulativeOutputTokens: this.finalCumulativeOutputTokens,
86
+ cumulativeCacheReadTokens: this.finalCumulativeCacheReadTokens,
87
+ cumulativeCacheCreationTokens: this.finalCumulativeCacheCreationTokens,
88
+ reasoningTokens: this.finalReasoningTokens,
89
+ };
90
+ }
91
+ logSummary(runId) {
92
+ const meta = this.meta();
93
+ return `[lark-remote] final usage runId=${runId} contextLength=${meta.contextLength} contextLimit=${meta.contextLimit ?? 'undefined'} compactCount=${meta.compactCount ?? 'undefined'} cacheRead=${meta.cacheReadTokens ?? 'undefined'} cacheCreate=${meta.cacheCreationTokens ?? 'undefined'}`;
94
+ }
95
+ }
96
+ //# sourceMappingURL=run-usage.js.map
@@ -0,0 +1,18 @@
1
+ import type { AgentKind, TurnStartedEvent } from '../runner/index.js';
2
+ import type { SessionStore } from '../session/index.js';
3
+ interface InitEvent {
4
+ session_id: string;
5
+ cwd?: string;
6
+ }
7
+ interface WritebackContext {
8
+ sessionStore: SessionStore;
9
+ userId: string;
10
+ agentKind: AgentKind;
11
+ cwd: string;
12
+ sessionEpochAtStart: number;
13
+ runId: string;
14
+ }
15
+ export declare function writeBackInitSession(ctx: WritebackContext, event: InitEvent): void;
16
+ export declare function writeBackTurnStarted(ctx: WritebackContext, event: TurnStartedEvent): boolean;
17
+ export declare function writeBackResultSession(ctx: WritebackContext, sessionId: string | undefined): void;
18
+ export {};
@@ -0,0 +1,36 @@
1
+ import { getLogger } from '../logger/index.js';
2
+ function pointerMoved(ctx) {
3
+ return ctx.sessionStore.getSessionEpoch(ctx.userId, ctx.agentKind) !== ctx.sessionEpochAtStart;
4
+ }
5
+ export function writeBackInitSession(ctx, event) {
6
+ if (pointerMoved(ctx)) {
7
+ getLogger().info(`[lark-remote] system.init write-back skipped: session pointer moved since run start runId=${ctx.runId} sessionId=${event.session_id}`);
8
+ return;
9
+ }
10
+ const eventCwd = typeof event.cwd === 'string' ? event.cwd : '';
11
+ const hasCwd = !!ctx.sessionStore.getCwd(ctx.userId);
12
+ const realCwd = eventCwd.length > 0 ? eventCwd : ctx.cwd;
13
+ if (hasCwd && eventCwd && eventCwd !== ctx.cwd) {
14
+ getLogger().info(`[lark-remote] system.init: cwd=${ctx.cwd}, session cwd=${eventCwd}`);
15
+ ctx.sessionStore.setSessionIdAndSessionCwd(ctx.userId, ctx.agentKind, event.session_id, realCwd);
16
+ return;
17
+ }
18
+ const bootstrapSessionCwd = !hasCwd && eventCwd.length > 0 ? eventCwd : undefined;
19
+ ctx.sessionStore.setSessionIdAndCwd(ctx.userId, ctx.agentKind, event.session_id, realCwd, bootstrapSessionCwd);
20
+ }
21
+ export function writeBackTurnStarted(ctx, event) {
22
+ if (pointerMoved(ctx)) {
23
+ getLogger().info(`[lark-remote] turn_started write-back skipped: session pointer moved since run start runId=${ctx.runId} sessionId=${event.threadId}`);
24
+ return false;
25
+ }
26
+ ctx.sessionStore.setSessionIdAndCwd(ctx.userId, ctx.agentKind, event.threadId, ctx.cwd);
27
+ getLogger().info(`[lark-remote] turn_started session write-back runId=${ctx.runId} sessionId=${event.threadId}`);
28
+ return true;
29
+ }
30
+ export function writeBackResultSession(ctx, sessionId) {
31
+ if (!sessionId || pointerMoved(ctx))
32
+ return;
33
+ ctx.sessionStore.setSessionIdAndCwd(ctx.userId, ctx.agentKind, sessionId, ctx.cwd);
34
+ getLogger().info(`[lark-remote] result session write-back runId=${ctx.runId} sessionId=${sessionId}`);
35
+ }
36
+ //# sourceMappingURL=session-writeback.js.map
@@ -14,3 +14,17 @@ export declare function renderApprovalArea(approval: ApprovalView | undefined, o
14
14
  runId?: string;
15
15
  terminal?: string;
16
16
  }): object[];
17
+ /**
18
+ * 紧凑审批区(run 卡 skeleton/truncatedSkeleton 兜底档专用):固定小体积的
19
+ * 「标题 + 摘要 reason + 决策按钮」,不渲染正文/选项/题面。
20
+ *
21
+ * 为什么需要:完整审批区是唯一不受估算器管辖、也不参与 degraded/extreme 分层
22
+ * 裁剪的区域; pathological 输入(超长 reason、题数×选项爆元素数)会让所有兜底
23
+ * 层一起超限,超限卡被飞书整卡拒收(11310)→ 审批不可见。兜底层换用本入口后
24
+ * 结构性保证 ≤ 几个元素 + ≤ 1KB 文本,审批入口(红线)在任何降级档都活着。
25
+ */
26
+ export declare function renderApprovalAreaCompact(approval: ApprovalView | undefined, opts?: {
27
+ expired?: boolean;
28
+ runId?: string;
29
+ terminal?: string;
30
+ }): object[];
@@ -8,17 +8,50 @@ import { collapsibleMarkdownPanel } from './collapsible.js';
8
8
  import { truncateMarkdownTables } from './text-truncate.js';
9
9
  import { truncateUtf8 } from '../common/truncate.js';
10
10
  import { cardButton } from './card-shared.js';
11
+ import { countCardElements } from './card-budget.js';
11
12
  /** Unique nonce for card button callbacks (SDK dedup + router validation). */
12
13
  function mkNonce() {
13
14
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
14
15
  }
15
16
  /** 计划面板正文字节预算(折叠展示 + 截断保护 28KB 卡片预算)。 */
16
17
  const PLAN_PANEL_BYTES = 6000;
17
- /** reason div(3 处共用);反引号中和防 lark_md 解析错误。 */
18
+ /**
19
+ * 审批区自由文本字段(reason/note/题面/描述/回执等 agent 产出的散文)的统一
20
+ * 字节预算。这些字段此前无上限:实测 reason 30,000 字符 → 整卡 31,450 字节被
21
+ * 飞书 11310 拒收,审批彻底不可见,run 只能挂到审批超时/看门狗。1KB 足以传达
22
+ * 语义并支撑决策。
23
+ */
24
+ const APPROVAL_FIELD_BYTES = 1000;
25
+ /** 短标签类字段(题头/工具名/路径)的字节预算。 */
26
+ const APPROVAL_LABEL_BYTES = 300;
27
+ /**
28
+ * 提问批次的渲染上限:题数 × 每题元素(选项行 + 输入框 + 按钮)撑爆的是元素
29
+ * 数预算(实测 8 题 × 5 选项 = 144 元素 > 90 上限 → 11310 整卡报废)。超出的
30
+ * 题折叠为计数提示;被折叠的题无法在卡上作答(整批「跳过」与 Web UI/终端
31
+ * 兜底),仍远好于整卡被拒、一题都答不了。
32
+ */
33
+ const MAX_RENDERED_QUESTIONS = 5;
34
+ /** 单题选项渲染上限(超过折叠为计数提示;选项 label 是回调 payload,不可截断)。 */
35
+ const MAX_RENDERED_OPTIONS = 8;
36
+ /** 文件变更 / 权限条目的渲染上限(每条 1–2 个元素,无上限时百条级直接爆元素预算)。 */
37
+ const MAX_RENDERED_CHANGE_ITEMS = 20;
38
+ /**
39
+ * 审批区自身的元素数预算:卡总预算(MAX_CARD_ELEMENTS=90)减去状态行 / goal /
40
+ * summary / 操作行等骨架(约 20 个,含极端情况的余量)后的安全份额。按已渲染
41
+ * 元素数**自适应**截断——固定题数 × 选项数的组合上限挡不住「少题多选项」与
42
+ * 「多题少选项」的两种组合(一行选项 = column_set + button + div 共 3 个
43
+ * 元素,5 题 × 5 选项就 ~92 个)。
44
+ */
45
+ const APPROVAL_AREA_ELEMENT_BUDGET = 50;
46
+ /** 审批区自由文本统一入口:反引号中和(防 11311)+ 字节预算截断(防 11310)。 */
47
+ function capApprovalText(text, maxBytes = APPROVAL_FIELD_BYTES) {
48
+ return truncateUtf8(text.replace(/`/g, '·'), maxBytes);
49
+ }
50
+ /** reason div(3 处共用);反引号中和防 lark_md 解析错误 + 字节预算截断。 */
18
51
  function reasonDiv(approval) {
19
52
  if (!approval.reason)
20
53
  return null;
21
- const neutralized = approval.reason.replace(/`/g, '·');
54
+ const neutralized = capApprovalText(approval.reason);
22
55
  return {
23
56
  tag: 'div',
24
57
  text: { tag: 'lark_md', content: `💡 ${neutralized}` },
@@ -41,12 +74,11 @@ export function renderApprovalArea(approval, opts) {
41
74
  return [];
42
75
  // 终态(done/error/interrupted/idle_timeout)下 run 已结束:coordinator 已
43
76
  // 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"。
44
- if (opts?.terminal && opts.terminal !== 'running' && opts.terminal !== 'finalizing') {
77
+ if (isTerminalHidden(opts))
45
78
  return [];
46
- }
47
79
  const elements = [];
48
80
  if (approval.note) {
49
- const neutralized = approval.note.replace(/`/g, '·');
81
+ const neutralized = capApprovalText(approval.note);
50
82
  elements.push({
51
83
  tag: 'div',
52
84
  text: { tag: 'lark_md', content: `ℹ️ ${neutralized}` },
@@ -69,7 +101,7 @@ export function renderApprovalArea(approval, opts) {
69
101
  }
70
102
  // 响应发送失败:明确告知原因 + 可重试,而不是静默停在可点状态。
71
103
  if (approval.deliveryError) {
72
- const neutralized = approval.deliveryError.replace(/`/g, '·');
104
+ const neutralized = capApprovalText(approval.deliveryError);
73
105
  elements.push({
74
106
  tag: 'div',
75
107
  text: {
@@ -80,6 +112,54 @@ export function renderApprovalArea(approval, opts) {
80
112
  }
81
113
  return elements;
82
114
  }
115
+ /**
116
+ * 紧凑审批区(run 卡 skeleton/truncatedSkeleton 兜底档专用):固定小体积的
117
+ * 「标题 + 摘要 reason + 决策按钮」,不渲染正文/选项/题面。
118
+ *
119
+ * 为什么需要:完整审批区是唯一不受估算器管辖、也不参与 degraded/extreme 分层
120
+ * 裁剪的区域; pathological 输入(超长 reason、题数×选项爆元素数)会让所有兜底
121
+ * 层一起超限,超限卡被飞书整卡拒收(11310)→ 审批不可见。兜底层换用本入口后
122
+ * 结构性保证 ≤ 几个元素 + ≤ 1KB 文本,审批入口(红线)在任何降级档都活着。
123
+ */
124
+ export function renderApprovalAreaCompact(approval, opts) {
125
+ if (!approval)
126
+ return [];
127
+ if (isTerminalHidden(opts))
128
+ return [];
129
+ const elements = [
130
+ {
131
+ tag: 'div',
132
+ text: {
133
+ tag: 'lark_md',
134
+ content: '✋ **等待审批**(内容过长已折叠,完整内容请查终端 / Web UI)',
135
+ },
136
+ },
137
+ ];
138
+ pushIf(elements, reasonDiv(approval));
139
+ if (approval.kind === 'question') {
140
+ if (opts?.expired) {
141
+ elements.push({ tag: 'div', text: { tag: 'lark_md', content: '⏰ **提问已超时**' } });
142
+ }
143
+ else {
144
+ // 提问批次的紧凑形态只保留整批跳过(逐题作答 UI 被折叠)。
145
+ elements.push(cardButton('⏭️ 跳过回答', {
146
+ cmd: 'approval.respond',
147
+ decision: 'decline',
148
+ requestId: approval.requestId,
149
+ runId: opts?.runId,
150
+ nonce: mkNonce(),
151
+ }, { type: 'default' }));
152
+ }
153
+ }
154
+ else {
155
+ elements.push(...renderDecisionButtons(approval, opts?.expired, opts?.runId));
156
+ }
157
+ return elements;
158
+ }
159
+ /** 终态隐藏判定(renderApprovalArea 与 compact 共用)。 */
160
+ function isTerminalHidden(opts) {
161
+ return !!opts?.terminal && opts.terminal !== 'running' && opts.terminal !== 'finalizing';
162
+ }
83
163
  /**
84
164
  * Render approval action buttons from the real protocol decision list.
85
165
  * accept and decline are always offered (decline is a universal safety
@@ -150,7 +230,7 @@ function renderToolApproval(approval, expired, runId) {
150
230
  content: '**🔧 工具请求**',
151
231
  },
152
232
  });
153
- const toolLabel = approval.toolName ?? '(unknown tool)';
233
+ const toolLabel = capApprovalText(approval.toolName ?? '(unknown tool)', APPROVAL_LABEL_BYTES);
154
234
  elements.push({
155
235
  tag: 'div',
156
236
  text: { tag: 'lark_md', content: `🔧 \`${toolLabel}\`` },
@@ -158,10 +238,10 @@ function renderToolApproval(approval, expired, runId) {
158
238
  pushIf(elements, reasonDiv(approval));
159
239
  // 原始 input(非空时展示,截断保护卡片预算;ExitPlanMode 的 `{}` 不展示)。
160
240
  if (approval.toolInput && approval.toolInput !== '{}') {
161
- const neutralized = approval.toolInput.replace(/`/g, '·');
241
+ const neutralized = capApprovalText(approval.toolInput, 500);
162
242
  elements.push({
163
243
  tag: 'div',
164
- text: { tag: 'lark_md', content: `\`${neutralized.slice(0, 500)}\`` },
244
+ text: { tag: 'lark_md', content: `\`${neutralized}\`` },
165
245
  });
166
246
  }
167
247
  elements.push(...renderDecisionButtons(approval, expired, runId));
@@ -184,7 +264,7 @@ function renderPlanApproval(approval, expired, runId) {
184
264
  });
185
265
  pushIf(elements, reasonDiv(approval));
186
266
  if (approval.planFilePath) {
187
- const neutralizedPath = approval.planFilePath.replace(/`/g, '·');
267
+ const neutralizedPath = capApprovalText(approval.planFilePath, APPROVAL_LABEL_BYTES);
188
268
  elements.push({
189
269
  tag: 'div',
190
270
  text: { tag: 'lark_md', content: `📄 ${neutralizedPath}` },
@@ -215,7 +295,7 @@ function renderPlanApproval(approval, expired, runId) {
215
295
  if (!expired) {
216
296
  elements.push(renderPlanFeedbackInput(approval.requestId, runId));
217
297
  if (approval.planFeedback) {
218
- const neutralized = approval.planFeedback.replace(/`/g, '·');
298
+ const neutralized = capApprovalText(approval.planFeedback);
219
299
  elements.push({
220
300
  tag: 'div',
221
301
  text: { tag: 'lark_md', content: `📝 ${neutralized}` },
@@ -259,18 +339,17 @@ function renderCommandApproval(approval, expired, runId) {
259
339
  // Command display
260
340
  // 命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
261
341
  // 反引号极易触发 11311 解析错误导致整卡失败(同 renderFileDiff 的中和策略)。
262
- const neutralizedCommand = (approval.command ?? '').replace(/`/g, '·');
263
- const cmdContent = neutralizedCommand
264
- ? `\`${neutralizedCommand.slice(0, 500)}\``
265
- : '(no command)';
342
+ const neutralizedCommand = capApprovalText(approval.command ?? '', 500);
343
+ const cmdContent = neutralizedCommand ? `\`${neutralizedCommand}\`` : '(no command)';
266
344
  elements.push({
267
345
  tag: 'div',
268
346
  text: { tag: 'lark_md', content: `**⚡ 命令审批**\n${cmdContent}` },
269
347
  });
270
348
  if (approval.commandCwd) {
349
+ const neutralizedCwd = capApprovalText(approval.commandCwd, APPROVAL_LABEL_BYTES);
271
350
  elements.push({
272
351
  tag: 'div',
273
- text: { tag: 'lark_md', content: `📂 \`${approval.commandCwd}\`` },
352
+ text: { tag: 'lark_md', content: `📂 \`${neutralizedCwd}\`` },
274
353
  });
275
354
  }
276
355
  pushIf(elements, reasonDiv(approval));
@@ -297,11 +376,16 @@ function renderFileApproval(approval, expired, runId) {
297
376
  const TOTAL_DIFF_CHARS = 2400;
298
377
  let renderedDiffChars = 0;
299
378
  let omittedDiffs = 0;
300
- for (const change of approval.fileChanges) {
379
+ // 条目数上限(元素预算):每条 1 path div + 至多 1 diff div,无上限时
380
+ // 百条级变更直接撑爆元素数(>90 → 11310 整卡报废)。
381
+ const changes = approval.fileChanges.slice(0, MAX_RENDERED_CHANGE_ITEMS);
382
+ const omittedChanges = approval.fileChanges.length - changes.length;
383
+ for (const change of changes) {
301
384
  const icon = change.kind === 'add' ? '🆕' : change.kind === 'delete' ? '🗑️' : '✏️';
385
+ const path = capApprovalText(change.path, APPROVAL_LABEL_BYTES);
302
386
  elements.push({
303
387
  tag: 'div',
304
- text: { tag: 'lark_md', content: `${icon} \`${change.path}\` (${change.kind})` },
388
+ text: { tag: 'lark_md', content: `${icon} \`${path}\` (${change.kind})` },
305
389
  });
306
390
  if (change.diff) {
307
391
  if (renderedDiffChars < TOTAL_DIFF_CHARS) {
@@ -316,6 +400,12 @@ function renderFileApproval(approval, expired, runId) {
316
400
  }
317
401
  }
318
402
  }
403
+ if (omittedChanges > 0) {
404
+ elements.push({
405
+ tag: 'div',
406
+ text: { tag: 'lark_md', content: `_…另有 ${omittedChanges} 个文件变更未展示(卡片预算)_` },
407
+ });
408
+ }
319
409
  if (omittedDiffs > 0) {
320
410
  elements.push({
321
411
  tag: 'div',
@@ -354,11 +444,16 @@ function renderPermissionsApproval(approval, expired, runId) {
354
444
  pushIf(elements, reasonDiv(approval));
355
445
  // Permission items with toggle buttons
356
446
  if (approval.permissions?.items) {
357
- for (const item of approval.permissions.items) {
447
+ // 条目数上限(元素预算):每条 label div + toggle button 共 2 个元素。
448
+ const items = approval.permissions.items.slice(0, MAX_RENDERED_CHANGE_ITEMS);
449
+ const omittedItems = approval.permissions.items.length - items.length;
450
+ for (const item of items) {
358
451
  const icon = item.selected ? '✅' : '⬜';
452
+ // label 仅展示(payload 用 item.id),可安全截断。
453
+ const label = capApprovalText(item.label, APPROVAL_LABEL_BYTES);
359
454
  elements.push({
360
455
  tag: 'div',
361
- text: { tag: 'lark_md', content: `${icon} ${item.label}` },
456
+ text: { tag: 'lark_md', content: `${icon} ${label}` },
362
457
  });
363
458
  elements.push(cardButton(item.selected ? '取消授予' : '授予', {
364
459
  cmd: 'approval.toggle',
@@ -369,6 +464,12 @@ function renderPermissionsApproval(approval, expired, runId) {
369
464
  nonce: mkNonce(),
370
465
  }, { type: item.selected ? 'default' : 'primary', size: 'small' }));
371
466
  }
467
+ if (omittedItems > 0) {
468
+ elements.push({
469
+ tag: 'div',
470
+ text: { tag: 'lark_md', content: `_…另有 ${omittedItems} 项权限未展示(卡片预算)_` },
471
+ });
472
+ }
372
473
  }
373
474
  // Action buttons
374
475
  // Derived from the real protocol decision list (see renderDecisionButtons).
@@ -396,7 +497,7 @@ function renderQuestionApproval(approval, expired, runId) {
396
497
  if (approval.intro) {
397
498
  elements.push({
398
499
  tag: 'div',
399
- text: { tag: 'lark_md', content: `📝 ${approval.intro}` },
500
+ text: { tag: 'lark_md', content: `📝 ${capApprovalText(approval.intro)}` },
400
501
  });
401
502
  }
402
503
  if (expired) {
@@ -410,22 +511,36 @@ function renderQuestionApproval(approval, expired, runId) {
410
511
  if (approval.expiredHint) {
411
512
  elements.push({
412
513
  tag: 'div',
413
- text: { tag: 'lark_md', content: approval.expiredHint.replace(/`/g, '·') },
514
+ text: { tag: 'lark_md', content: capApprovalText(approval.expiredHint) },
414
515
  });
415
516
  }
416
517
  return elements;
417
518
  }
418
519
  const questions = approval.questions ?? [];
520
+ let renderedQuestions = 0;
419
521
  for (let i = 0; i < questions.length; i++) {
522
+ // 题数粗上限 + 审批区元素预算(自适应):任一触发即折叠剩余题。
523
+ if (i >= MAX_RENDERED_QUESTIONS ||
524
+ countCardElements({ elements }) > APPROVAL_AREA_ELEMENT_BUDGET) {
525
+ elements.push({
526
+ tag: 'div',
527
+ text: {
528
+ tag: 'lark_md',
529
+ content: `_…另有 ${questions.length - renderedQuestions} 题未展示(卡片元素预算);可在终端 / Web UI 回答,或点下方「跳过回答」跳过整批_`,
530
+ },
531
+ });
532
+ break;
533
+ }
534
+ renderedQuestions++;
420
535
  const q = questions[i];
421
536
  const selected = q.selected ?? [];
422
- const header = q.header || `问题 ${i + 1}`;
537
+ const header = capApprovalText(q.header || `问题 ${i + 1}`, APPROVAL_LABEL_BYTES);
423
538
  const freeText = (q.options?.length ?? 0) === 0;
424
539
  elements.push({
425
540
  tag: 'div',
426
541
  text: {
427
542
  tag: 'lark_md',
428
- content: `**${header}**${q.multiSelect ? '(可多选)' : ''}\n${q.question}`,
543
+ content: `**${header}**${q.multiSelect ? '(可多选)' : ''}\n${capApprovalText(q.question)}`,
429
544
  },
430
545
  });
431
546
  if (q.detail) {
@@ -451,24 +566,35 @@ function renderQuestionApproval(approval, expired, runId) {
451
566
  if (answered) {
452
567
  elements.push({
453
568
  tag: 'div',
454
- text: { tag: 'lark_md', content: `✍️ 已答:${answered}` },
569
+ text: { tag: 'lark_md', content: `✍️ 已答:${capApprovalText(answered)}` },
455
570
  });
456
571
  }
457
572
  continue;
458
573
  }
459
- for (const option of q.options ?? []) {
574
+ // 选项渲染上限(label 是回调 payload 不可截断,只能按条数折叠)。
575
+ const options = q.options ?? [];
576
+ for (const option of options.slice(0, MAX_RENDERED_OPTIONS)) {
460
577
  const isSelected = selected.includes(option.label);
461
578
  // 单选/多选图标区分:单选 ⚪/🔵(radio 隐喻),多选 ⬜/☑️(checkbox 隐喻)。
462
579
  const icon = q.multiSelect ? (isSelected ? '☑️' : '⬜') : isSelected ? '🔵' : '⚪';
463
580
  elements.push(renderOptionRow(approval.requestId, i, option, isSelected, icon, runId));
464
581
  }
582
+ if (options.length > MAX_RENDERED_OPTIONS) {
583
+ elements.push({
584
+ tag: 'div',
585
+ text: {
586
+ tag: 'lark_md',
587
+ content: `_…另有 ${options.length - MAX_RENDERED_OPTIONS} 个选项未展示(卡片元素预算)_`,
588
+ },
589
+ });
590
+ }
465
591
  // 自定义答案(Other)的选中态展示:选项按钮无法表示自由文本,单独显示
466
592
  // 已选文本,避免「点了没反应」的困惑。
467
593
  const customSelected = q.selected?.find((s) => !q.options.some((o) => o.label === s));
468
594
  if (customSelected) {
469
595
  elements.push({
470
596
  tag: 'div',
471
- text: { tag: 'lark_md', content: `✍️ 自定义答案:${customSelected}` },
597
+ text: { tag: 'lark_md', content: `✍️ 自定义答案:${capApprovalText(customSelected)}` },
472
598
  });
473
599
  }
474
600
  // 单选问题提供自定义答案(Other)输入——输入文本后提交
@@ -486,7 +612,7 @@ function renderQuestionApproval(approval, expired, runId) {
486
612
  if (q.note) {
487
613
  elements.push({
488
614
  tag: 'div',
489
- text: { tag: 'lark_md', content: `📝 ${q.note}` },
615
+ text: { tag: 'lark_md', content: `📝 ${capApprovalText(q.note)}` },
490
616
  });
491
617
  }
492
618
  }
@@ -550,7 +676,7 @@ function renderOptionRow(requestId, questionIndex, option, isSelected, icon, run
550
676
  tag: 'div',
551
677
  text: {
552
678
  tag: 'lark_md',
553
- content: `${icon} ${option.label}${option.description ? `\n_${option.description}_` : ''}`,
679
+ content: `${icon} ${option.label}${option.description ? `\n_${capApprovalText(option.description)}_` : ''}`,
554
680
  },
555
681
  },
556
682
  ],
@@ -68,11 +68,16 @@ export class BashCardSession extends CardSession {
68
68
  }
69
69
  /** Transition to a terminal state and patch the card. */
70
70
  async finish(terminal, meta = {}) {
71
- this.cancelPendingFlush();
72
- // 若有 in-flight flush 正在 await updateCard,必须等它完成后再发终态 patch,
73
- // 保证顺序(pre-terminal 先落地 → terminal 后落地 → terminal 胜出)。
74
- if (this.flushP)
75
- await this.flushP;
71
+ // 终态守卫(与 run-state 的 finishRun 首终态优先同口径):/stop 路径
72
+ // (bridge.interruptCurrentRun)与 executeBashInternal 的自然收尾路径会对
73
+ // 同一张卡并发 finish——SIGKILL 后 exitCode 为 -1,自然路径的
74
+ // finish('error', { exitCode: -1 }) 会把 interrupt 路径刚写的 'interrupted'
75
+ // 覆盖成「执行失败」,后落者胜出且取决于时序。首终态胜出后忽略后续 finish。
76
+ if (this.state.terminal !== 'running') {
77
+ return;
78
+ }
79
+ // 终态 patch 必须排在已开始的 pre-terminal flush 之后。
80
+ await this.prepareTerminalUpdate();
76
81
  this.state = {
77
82
  ...this.state,
78
83
  terminal,
@@ -1,7 +1,7 @@
1
1
  import { terminalToColor, terminalToLabel, stopButton } from './card-shared.js';
2
2
  import { markdownDiv } from './collapsible.js';
3
3
  import { CARD_BUDGET_BYTES } from './text-truncate.js';
4
- import { truncateUtf8 } from '../common/truncate.js';
4
+ import { truncateUtf8, truncate } from '../common/truncate.js';
5
5
  /**
6
6
  * 飞书卡片大小限制(安全阈值,与 run-renderer 一致)。
7
7
  * 从共享的 text-truncate.ts 导入,避免三处各自定义后数值漂移。
@@ -20,36 +20,24 @@ const OUTPUT_MAX_BYTES = 12_000;
20
20
  */
21
21
  const DEGRADED_OUTPUT_BYTES = 4_000;
22
22
  /**
23
- * Truncate long commands for display.
23
+ * Truncate long commands for display. 按码点截断(裸 slice 切在 emoji 中间会
24
+ * 留下孤立代理项进卡片),项目已有 truncate() 实现同语义。
24
25
  */
25
26
  function truncateCommand(cmd, maxLen = 40) {
26
- if (cmd.length <= maxLen)
27
- return cmd;
28
- return cmd.slice(0, maxLen - 3) + '...';
27
+ return truncate(cmd, maxLen);
29
28
  }
30
29
  /**
31
- * Build the status row (status tag + command) element.
30
+ * Build the status row element.
31
+ *
32
+ * 单 div + ` · ` 分隔(窄屏硬闸,与 run-renderer 的 statusRow 同款):旧版把
33
+ * 状态标签(2 字短文本)放 `column_set` 的 auto 列、命令放 grow 列——手机
34
+ * 窄屏下 auto 列会被挤成竖排(run 卡踩过同型坑,bash 卡漏改)。
35
+ * 单行文本的换行点只落在 " · " 处,各分段自身不折行。
32
36
  */
33
37
  function buildStatusRow(state) {
34
38
  const { terminal, exitCode, command } = state;
35
39
  const statusLabel = bashStatusTagLabel(terminal, exitCode);
36
- return {
37
- tag: 'column_set',
38
- columns: [
39
- {
40
- tag: 'column',
41
- width: 'auto',
42
- elements: [{ tag: 'div', text: { content: statusLabel, tag: 'lark_md' } }],
43
- },
44
- {
45
- tag: 'column',
46
- width: 'grow',
47
- elements: [
48
- { tag: 'div', text: { content: `⏱ ${truncateCommand(command, 40)}`, tag: 'lark_md' } },
49
- ],
50
- },
51
- ],
52
- };
40
+ return markdownDiv(`${statusLabel} · ⏱ ${truncateCommand(command, 40)}`, 'notation');
53
41
  }
54
42
  /**
55
43
  * Build the exit code footer elements (terminal states only).