lark-remote 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +57 -34
  5. package/dist/bridge/approval-coordinator.js +191 -40
  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 +78 -7
  9. package/dist/bridge/index.js +202 -79
  10. package/dist/bridge/queue-manager.d.ts +2 -1
  11. package/dist/bridge/queue-manager.js +1 -0
  12. package/dist/card/approval-render.js +152 -36
  13. package/dist/card/bash-card-session.d.ts +2 -24
  14. package/dist/card/bash-card-session.js +6 -73
  15. package/dist/card/bash-renderer.js +2 -3
  16. package/dist/card/card-budget.js +29 -75
  17. package/dist/card/card-session.d.ts +48 -0
  18. package/dist/card/card-session.js +86 -0
  19. package/dist/card/card-shared.d.ts +1 -12
  20. package/dist/card/card-shared.js +1 -26
  21. package/dist/card/run-card-session.d.ts +2 -55
  22. package/dist/card/run-card-session.js +6 -104
  23. package/dist/card/run-renderer.js +99 -232
  24. package/dist/card/run-state.d.ts +13 -0
  25. package/dist/card/run-state.js +25 -16
  26. package/dist/card/text-truncate.d.ts +13 -12
  27. package/dist/card/text-truncate.js +13 -63
  28. package/dist/common/fs.d.ts +8 -0
  29. package/dist/common/fs.js +17 -0
  30. package/dist/common/guards.d.ts +0 -20
  31. package/dist/common/guards.js +0 -43
  32. package/dist/common/sleep.d.ts +2 -0
  33. package/dist/common/sleep.js +5 -0
  34. package/dist/common/truncate.d.ts +39 -0
  35. package/dist/common/truncate.js +92 -0
  36. package/dist/config/codex-config.d.ts +1 -1
  37. package/dist/config/codex-config.js +1 -1
  38. package/dist/config/index.d.ts +88 -23
  39. package/dist/config/index.js +105 -34
  40. package/dist/connector/index.d.ts +69 -2
  41. package/dist/connector/index.js +107 -1
  42. package/dist/index.js +123 -105
  43. package/dist/instance-lock.js +4 -12
  44. package/dist/order/alias-resolve.d.ts +13 -0
  45. package/dist/order/alias-resolve.js +22 -0
  46. package/dist/order/alias-store.d.ts +38 -0
  47. package/dist/order/alias-store.js +106 -0
  48. package/dist/persistence/atomic-write.d.ts +8 -2
  49. package/dist/persistence/atomic-write.js +23 -7
  50. package/dist/restart.js +2 -1
  51. package/dist/router/card-helpers.d.ts +63 -0
  52. package/dist/router/card-helpers.js +146 -0
  53. package/dist/router/config/claude.js +25 -1
  54. package/dist/router/config/codex.js +29 -50
  55. package/dist/router/config/common/model-patch.d.ts +22 -0
  56. package/dist/router/config/common/model-patch.js +26 -0
  57. package/dist/router/config/common/render.js +5 -4
  58. package/dist/router/config/index.d.ts +12 -0
  59. package/dist/router/config/index.js +25 -1
  60. package/dist/router/config/kimi.d.ts +4 -1
  61. package/dist/router/config/kimi.js +27 -0
  62. package/dist/router/config/opencode.js +18 -5
  63. package/dist/router/config/pi.js +4 -5
  64. package/dist/router/config/types.d.ts +11 -2
  65. package/dist/router/index.d.ts +56 -21
  66. package/dist/router/index.js +373 -548
  67. package/dist/router/utils.d.ts +2 -0
  68. package/dist/router/utils.js +4 -0
  69. package/dist/runner/agent-choices-common.d.ts +16 -0
  70. package/dist/runner/agent-choices-common.js +31 -0
  71. package/dist/runner/claude/runner.d.ts +47 -10
  72. package/dist/runner/claude/runner.js +117 -46
  73. package/dist/runner/claude/session.d.ts +166 -0
  74. package/dist/runner/claude/session.js +749 -0
  75. package/dist/runner/codex/app-server/protocol-types.d.ts +31 -39
  76. package/dist/runner/codex/app-server/protocol-types.js +1 -12
  77. package/dist/runner/codex/app-server/runner.d.ts +42 -59
  78. package/dist/runner/codex/app-server/runner.js +160 -337
  79. package/dist/runner/codex/app-server/translator.d.ts +1 -21
  80. package/dist/runner/codex/app-server/translator.js +0 -18
  81. package/dist/runner/codex/index.d.ts +0 -2
  82. package/dist/runner/codex/index.js +0 -2
  83. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  84. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  85. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  86. package/dist/runner/common/acp/protocol-types.js +75 -0
  87. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  88. package/dist/runner/common/connection-based-runner.js +261 -0
  89. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  90. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  91. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  92. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  93. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  95. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  96. package/dist/runner/common/jsonrpc/types.js +9 -0
  97. package/dist/runner/common/process-stopper.d.ts +2 -1
  98. package/dist/runner/common/process-stopper.js +1 -0
  99. package/dist/runner/common/runner-utils.d.ts +6 -13
  100. package/dist/runner/common/runner-utils.js +6 -47
  101. package/dist/runner/common/spawning-runner.d.ts +24 -70
  102. package/dist/runner/common/spawning-runner.js +98 -152
  103. package/dist/runner/index.d.ts +4 -4
  104. package/dist/runner/index.js +4 -4
  105. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  106. package/dist/runner/kimi/acp/runner.js +448 -0
  107. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  108. package/dist/runner/kimi/acp/translator.js +336 -0
  109. package/dist/runner/kimi/index.d.ts +1 -1
  110. package/dist/runner/kimi/index.js +1 -1
  111. package/dist/runner/opencode/acp/index.d.ts +1 -0
  112. package/dist/runner/opencode/acp/index.js +2 -0
  113. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  114. package/dist/runner/opencode/acp/runner.js +429 -0
  115. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  116. package/dist/runner/opencode/acp/translator.js +345 -0
  117. package/dist/runner/opencode/index.d.ts +1 -1
  118. package/dist/runner/opencode/index.js +1 -1
  119. package/dist/runner/pi/index.d.ts +1 -1
  120. package/dist/runner/pi/index.js +1 -1
  121. package/dist/runner/pi/rpc/client.d.ts +52 -0
  122. package/dist/runner/pi/rpc/client.js +114 -0
  123. package/dist/runner/pi/rpc/index.d.ts +4 -0
  124. package/dist/runner/pi/rpc/index.js +4 -0
  125. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  126. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  127. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  128. package/dist/runner/pi/rpc/runner.js +199 -0
  129. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  130. package/dist/runner/pi/rpc/translator.js +258 -0
  131. package/dist/runner/registry.d.ts +7 -8
  132. package/dist/runner/registry.js +3 -4
  133. package/dist/runner/resolve-agent-choices.js +18 -59
  134. package/dist/runner/sync-agent-choices.js +16 -67
  135. package/dist/runner/types.d.ts +43 -13
  136. package/dist/session/claude/session-index.js +2 -3
  137. package/dist/session/claude/session-reader.js +3 -6
  138. package/dist/session/claude/sessions.js +6 -13
  139. package/dist/session/codex/rollout-reader.d.ts +2 -2
  140. package/dist/session/codex/rollout-reader.js +14 -17
  141. package/dist/session/codex/sessions.d.ts +1 -1
  142. package/dist/session/codex/sessions.js +2 -2
  143. package/dist/session/common/content-blocks.d.ts +7 -0
  144. package/dist/session/common/content-blocks.js +13 -9
  145. package/dist/session/common/jsonl.d.ts +8 -25
  146. package/dist/session/common/jsonl.js +126 -261
  147. package/dist/session/common/pagination.d.ts +28 -0
  148. package/dist/session/common/pagination.js +33 -0
  149. package/dist/session/kimi/sessions.d.ts +34 -1
  150. package/dist/session/kimi/sessions.js +208 -98
  151. package/dist/session/opencode/sessions.d.ts +17 -3
  152. package/dist/session/opencode/sessions.js +29 -29
  153. package/dist/session/pi/sessions.d.ts +5 -1
  154. package/dist/session/pi/sessions.js +12 -16
  155. package/dist/session/session-store.d.ts +11 -1
  156. package/dist/session/session-store.js +75 -178
  157. package/dist/update/index.d.ts +2 -2
  158. package/dist/update/index.js +2 -2
  159. package/dist/update/install.d.ts +20 -2
  160. package/dist/update/install.js +51 -7
  161. package/package.json +1 -1
  162. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  163. package/dist/runner/codex/argv.d.ts +0 -33
  164. package/dist/runner/codex/argv.js +0 -40
  165. package/dist/runner/codex/factory.d.ts +0 -29
  166. package/dist/runner/codex/factory.js +0 -39
  167. package/dist/runner/codex/jsonl.d.ts +0 -37
  168. package/dist/runner/codex/jsonl.js +0 -221
  169. package/dist/runner/codex/runner.d.ts +0 -66
  170. package/dist/runner/codex/runner.js +0 -129
  171. package/dist/runner/common/exec-translator.d.ts +0 -73
  172. package/dist/runner/common/exec-translator.js +0 -105
  173. package/dist/runner/kimi/jsonl.d.ts +0 -28
  174. package/dist/runner/kimi/jsonl.js +0 -143
  175. package/dist/runner/kimi/runner.d.ts +0 -49
  176. package/dist/runner/kimi/runner.js +0 -105
  177. package/dist/runner/opencode/argv.d.ts +0 -32
  178. package/dist/runner/opencode/argv.js +0 -35
  179. package/dist/runner/opencode/jsonl.d.ts +0 -68
  180. package/dist/runner/opencode/jsonl.js +0 -295
  181. package/dist/runner/opencode/runner.d.ts +0 -61
  182. package/dist/runner/opencode/runner.js +0 -93
  183. package/dist/runner/pi/jsonl.d.ts +0 -128
  184. package/dist/runner/pi/jsonl.js +0 -249
  185. package/dist/runner/pi/runner.d.ts +0 -36
  186. package/dist/runner/pi/runner.js +0 -76
  187. package/dist/session/common/catch-up.d.ts +0 -12
  188. package/dist/session/common/catch-up.js +0 -31
@@ -8,6 +8,24 @@
8
8
  function mkNonce() {
9
9
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
10
10
  }
11
+ /** reason div(3 处共用);反引号中和防 lark_md 解析错误(review P2-2)。 */
12
+ function reasonDiv(approval) {
13
+ if (!approval.reason)
14
+ return null;
15
+ const neutralized = approval.reason.replace(/`/g, '·');
16
+ return {
17
+ tag: 'div',
18
+ text: { tag: 'lark_md', content: `💡 ${neutralized}` },
19
+ };
20
+ }
21
+ /**
22
+ * Append a possibly-null element to the array (convenience for optional divs).
23
+ * If the element is null, nothing is appended.
24
+ */
25
+ function pushIf(arr, el) {
26
+ if (el !== null)
27
+ arr.push(el);
28
+ }
11
29
  /**
12
30
  * Render the approval area for a run card.
13
31
  * Returns an array of CardKit 2.0 elements to append to the card body.
@@ -30,6 +48,9 @@ export function renderApprovalArea(approval, opts) {
30
48
  else if (approval.kind === 'permissions') {
31
49
  elements.push(...renderPermissionsApproval(approval, opts?.expired, opts?.runId));
32
50
  }
51
+ else if (approval.kind === 'question') {
52
+ elements.push(...renderQuestionApproval(approval, opts?.expired, opts?.runId));
53
+ }
33
54
  return elements;
34
55
  }
35
56
  /**
@@ -51,11 +72,18 @@ function renderDecisionButtons(approval, expired, runId) {
51
72
  accept: { label: '✅ 允许', type: 'primary' },
52
73
  acceptForSession: { label: '✅ 允许本次会话', type: 'primary' },
53
74
  acceptWithExecpolicyAmendment: { label: '🔒 允许并记住命令', type: 'primary' },
75
+ acceptAll: { label: '✅ 允许所有', type: 'primary' },
54
76
  decline: { label: '❌ 拒绝', type: 'danger' },
55
77
  };
56
78
  // Fixed presentation order; only offer decisions the protocol listed
57
- // (accept/decline are always available).
58
- const offered = ['accept', 'acceptForSession', 'acceptWithExecpolicyAmendment', 'decline'].filter((d) => d === 'accept' || d === 'decline' || approval.availableDecisions.includes(d));
79
+ // (accept/decline are always available; acceptAll is Claude 专属「允许所有」)。
80
+ const offered = [
81
+ 'accept',
82
+ 'acceptForSession',
83
+ 'acceptWithExecpolicyAmendment',
84
+ 'acceptAll',
85
+ 'decline',
86
+ ].filter((d) => d === 'accept' || d === 'decline' || approval.availableDecisions.includes(d));
59
87
  // 决策按钮逐个作为 body 直接元素返回(CardKit body 纵向堆叠 = 上下排列)。
60
88
  // 不能用 column_set width:auto 横排:手机窄屏下多个按钮挤在一行,按钮文字
61
89
  // 被截断成省略号,用户无法辨认审批动作。
@@ -83,7 +111,12 @@ function renderDecisionButtons(approval, expired, runId) {
83
111
  function renderCommandApproval(approval, expired, runId) {
84
112
  const elements = [];
85
113
  // Command display
86
- const cmdContent = approval.command ? `\`${approval.command.slice(0, 500)}\`` : '(no command)';
114
+ // review P2-2:命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
115
+ // 反引号极易触发 11311 解析错误导致整卡失败(同 renderFileDiff 的中和策略)。
116
+ const neutralizedCommand = (approval.command ?? '').replace(/`/g, '·');
117
+ const cmdContent = neutralizedCommand
118
+ ? `\`${neutralizedCommand.slice(0, 500)}\``
119
+ : '(no command)';
87
120
  elements.push({
88
121
  tag: 'div',
89
122
  text: { tag: 'lark_md', content: `**⚡ 命令审批**\n${cmdContent}` },
@@ -94,23 +127,11 @@ function renderCommandApproval(approval, expired, runId) {
94
127
  text: { tag: 'lark_md', content: `📂 \`${approval.commandCwd}\`` },
95
128
  });
96
129
  }
97
- if (approval.reason) {
98
- elements.push({
99
- tag: 'div',
100
- text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
101
- });
102
- }
130
+ pushIf(elements, reasonDiv(approval));
103
131
  // Action buttons — derived from the real protocol decision list (see
104
132
  // renderDecisionButtons): accept/decline always, acceptForSession and
105
133
  // acceptWithExecpolicyAmendment only when the server offers them.
106
134
  elements.push(...renderDecisionButtons(approval, expired, runId));
107
- // Pending count
108
- if (approval.pendingTotal > 1) {
109
- elements.push({
110
- tag: 'div',
111
- text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
112
- });
113
- }
114
135
  return elements;
115
136
  }
116
137
  // =============================================================================
@@ -122,12 +143,7 @@ function renderFileApproval(approval, expired, runId) {
122
143
  tag: 'div',
123
144
  text: { tag: 'lark_md', content: '**📄 文件变更审批**' },
124
145
  });
125
- if (approval.reason) {
126
- elements.push({
127
- tag: 'div',
128
- text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
129
- });
130
- }
146
+ pushIf(elements, reasonDiv(approval));
131
147
  // File changes list
132
148
  if (approval.fileChanges && approval.fileChanges.length > 0) {
133
149
  // 审批区 diff 总预算:估算函数不计审批区,多文件大 diff 可能顶爆 28KB 卡
@@ -164,12 +180,6 @@ function renderFileApproval(approval, expired, runId) {
164
180
  // Action buttons
165
181
  // Derived from the real protocol decision list (see renderDecisionButtons).
166
182
  elements.push(...renderDecisionButtons(approval, expired, runId));
167
- if (approval.pendingTotal > 1) {
168
- elements.push({
169
- tag: 'div',
170
- text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
171
- });
172
- }
173
183
  return elements;
174
184
  }
175
185
  /**
@@ -195,12 +205,7 @@ function renderPermissionsApproval(approval, expired, runId) {
195
205
  tag: 'div',
196
206
  text: { tag: 'lark_md', content: '**🔒 权限审批**' },
197
207
  });
198
- if (approval.reason) {
199
- elements.push({
200
- tag: 'div',
201
- text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
202
- });
203
- }
208
+ pushIf(elements, reasonDiv(approval));
204
209
  // Permission items with toggle buttons
205
210
  if (approval.permissions?.items) {
206
211
  for (const item of approval.permissions.items) {
@@ -236,11 +241,122 @@ function renderPermissionsApproval(approval, expired, runId) {
236
241
  // Action buttons
237
242
  // Derived from the real protocol decision list (see renderDecisionButtons).
238
243
  elements.push(...renderDecisionButtons(approval, expired, runId));
239
- if (approval.pendingTotal > 1) {
244
+ return elements;
245
+ }
246
+ // =============================================================================
247
+ // AskUserQuestion Approval
248
+ // =============================================================================
249
+ /**
250
+ * 渲染 Claude AskUserQuestion 选项卡(审批区复用)。
251
+ *
252
+ * - 单选问题:点击选项即选中(协调器在全部问题答完后自动提交)。
253
+ * - 多选问题:点击切换勾选,勾选后出现「提交答案」按钮。
254
+ * - 已选状态经 approval_view_updated 回流(view.questions[].selected)。
255
+ */
256
+ function renderQuestionApproval(approval, expired, runId) {
257
+ const elements = [];
258
+ elements.push({
259
+ tag: 'div',
260
+ text: { tag: 'lark_md', content: '**❓ 需要你回答**' },
261
+ });
262
+ if (expired) {
263
+ elements.push({
264
+ tag: 'div',
265
+ text: { tag: 'lark_md', content: '⏰ **审批已过期**' },
266
+ });
267
+ return elements;
268
+ }
269
+ const questions = approval.questions ?? [];
270
+ for (let i = 0; i < questions.length; i++) {
271
+ const q = questions[i];
272
+ const selected = q.selected ?? [];
273
+ const header = q.header || `问题 ${i + 1}`;
240
274
  elements.push({
241
275
  tag: 'div',
242
- text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
276
+ text: {
277
+ tag: 'lark_md',
278
+ content: `**${header}**${q.multiSelect ? '(可多选)' : ''}\n${q.question}`,
279
+ },
243
280
  });
281
+ for (const option of q.options) {
282
+ const isSelected = selected.includes(option.label);
283
+ elements.push({
284
+ tag: 'div',
285
+ text: {
286
+ tag: 'lark_md',
287
+ content: `${isSelected ? '✅' : '⬜'} ${option.label}${option.description ? `\n_${option.description}_` : ''}`,
288
+ },
289
+ });
290
+ elements.push({
291
+ tag: 'button',
292
+ text: { tag: 'plain_text', content: isSelected ? '取消选择' : '选择' },
293
+ type: isSelected ? 'default' : 'primary',
294
+ size: 'small',
295
+ behaviors: [
296
+ {
297
+ type: 'callback',
298
+ value: {
299
+ cmd: 'approval.answer',
300
+ requestId: approval.requestId,
301
+ questionIndex: i,
302
+ option: option.label,
303
+ runId,
304
+ nonce: mkNonce(),
305
+ },
306
+ },
307
+ ],
308
+ });
309
+ }
310
+ // 自定义答案(Other)的选中态展示:选项按钮无法表示自由文本,单独显示
311
+ // 已选文本,避免「点了没反应」的困惑(review P3)。
312
+ const customSelected = q.selected?.find((s) => !q.options.some((o) => o.label === s));
313
+ if (customSelected) {
314
+ elements.push({
315
+ tag: 'div',
316
+ text: { tag: 'lark_md', content: `✍️ 自定义答案:${customSelected}` },
317
+ });
318
+ }
319
+ // review P3-4:单选问题提供自定义答案(Other)输入——输入文本后点击
320
+ // 输入框右侧 ✓ 提交图标,input_value 经 connector raw 事件回传。
321
+ if (!q.multiSelect) {
322
+ elements.push({
323
+ tag: 'input',
324
+ name: `answer-custom-${approval.requestId}-${i}`,
325
+ placeholder: { tag: 'plain_text', content: '自定义答案(Other)…' },
326
+ behaviors: [
327
+ {
328
+ type: 'callback',
329
+ value: {
330
+ cmd: 'approval.answerCustom',
331
+ requestId: approval.requestId,
332
+ questionIndex: i,
333
+ runId,
334
+ nonce: mkNonce(),
335
+ },
336
+ },
337
+ ],
338
+ });
339
+ }
340
+ // 多选问题:有勾选才给提交按钮(单选在协调器内即时提交)。
341
+ if (q.multiSelect && selected.length > 0) {
342
+ elements.push({
343
+ tag: 'button',
344
+ text: { tag: 'plain_text', content: `✅ 提交答案(已选 ${selected.length} 项)` },
345
+ type: 'primary',
346
+ behaviors: [
347
+ {
348
+ type: 'callback',
349
+ value: {
350
+ cmd: 'approval.answerSubmit',
351
+ requestId: approval.requestId,
352
+ questionIndex: i,
353
+ runId,
354
+ nonce: mkNonce(),
355
+ },
356
+ },
357
+ ],
358
+ });
359
+ }
244
360
  }
245
361
  return elements;
246
362
  }
@@ -1,4 +1,4 @@
1
- import { CardSession, type CardChannel, type StreamSettleResult } from './card-session.js';
1
+ import { CardSession, type CardChannel } from './card-session.js';
2
2
  import { type BashState, type BashRenderOptions } from './bash-renderer.js';
3
3
  /**
4
4
  * Store-time 输出上限(字符,P1-3 层①)。§P1-3 建议「store-time 截断,
@@ -23,18 +23,6 @@ export declare function capBashOutput(value: string): string;
23
23
  * independent cards.
24
24
  */
25
25
  export declare class BashCardSession extends CardSession<BashState, BashRenderOptions> {
26
- /**
27
- * P1-3 层③:push 合批窗口(ms),与 RunCardSession 同语义。连续 update 各触发
28
- * 一次全卡 render + patch 是 PATCH 风暴主源;窗口内多个事件复用同一次延迟 flush。
29
- * 窗口大小可通过 constructor 覆盖(测试用小窗口/0 禁用)。
30
- */
31
- private readonly coalesceMs;
32
- private flushTimer;
33
- private flushInFlight;
34
- /** in-flight flush 期间到达的 update 标记:flush 完成后需重新调度(防 stale 帧)。 */
35
- private pendingReschedule;
36
- /** 当前 in-flight flush 的 promise:finish 在终态 patch 前 await 它保证顺序。 */
37
- private flushP;
38
26
  constructor(opts: {
39
27
  connector: CardChannel;
40
28
  chatId: string;
@@ -51,6 +39,7 @@ export declare class BashCardSession extends CardSession<BashState, BashRenderOp
51
39
  protected get errorPrefix(): string;
52
40
  protected renderCard(state: BashState, options: BashRenderOptions): object;
53
41
  protected hasOwnBudgetProtection(): boolean;
42
+ protected isFlushableState(): boolean;
54
43
  /** Patch the card with updated bash state (e.g. new stdout/stderr). */
55
44
  update(patch: Partial<BashState>): Promise<void>;
56
45
  /** Transition to a terminal state and patch the card. */
@@ -59,17 +48,6 @@ export declare class BashCardSession extends CardSession<BashState, BashRenderOp
59
48
  stderr?: string;
60
49
  output?: string;
61
50
  }): Promise<void>;
62
- /**
63
- * 调度一次延迟 flush:已有 pending 调度则复用(合批核心);flushInFlight 期间
64
- * 不再起 timer(防重入叠加),但置 pendingReschedule 让 in-flight 完成后补渲染。
65
- */
66
- private scheduleFlush;
67
- /** 执行一次 render + updateCard(合批窗口到期或 finish 即时 flush 时调用)。 */
68
- private flush;
69
- /** 取消待 flush 的合批调度(finish/settle 前清理,避免延迟 update 与终态竞争)。 */
70
- private cancelPendingFlush;
71
- /** settle 前兜底取消合批调度,避免 release 后 dangling timer 更新已释放的 stream。 */
72
- settle(): Promise<StreamSettleResult>;
73
51
  /**
74
52
  * 合并 patch 并对 output/stderr 做 store-time 截断(P1-3 层①)。
75
53
  * 截断发生在入口(store 时),渲染层无需再为大 state 付代价。
@@ -1,5 +1,6 @@
1
1
  import { CardSession } from './card-session.js';
2
2
  import { renderBashCard } from './bash-renderer.js';
3
+ import { keepTail } from '../common/truncate.js';
3
4
  /**
4
5
  * Store-time 输出上限(字符,P1-3 层①)。§P1-3 建议「store-time 截断,
5
6
  * 保留尾部,上限如 24~64KB」——取区间下界 24_000 字符(字符上限 ×4 字节最坏
@@ -8,10 +9,6 @@ import { renderBashCard } from './bash-renderer.js';
8
9
  * state 与 bridge 局部变量。
9
10
  */
10
11
  export const BASH_OUTPUT_STORE_CAP = 24_000;
11
- /** 字符级 tail 保留(与 run-state.keepLatest 的 slice(-maxChars) 语义一致)。 */
12
- function keepTail(value, maxChars) {
13
- return value.length <= maxChars ? value : value.slice(-maxChars);
14
- }
15
12
  /**
16
13
  * Store-time 截断入口(P1-3 层①/层④共用)。bridge 本地 output/stderr 与 session
17
14
  * state 都走它,保证任意路径驻留的字符串 ≤ BASH_OUTPUT_STORE_CAP 字符。
@@ -29,18 +26,6 @@ export function capBashOutput(value) {
29
26
  * independent cards.
30
27
  */
31
28
  export class BashCardSession extends CardSession {
32
- /**
33
- * P1-3 层③:push 合批窗口(ms),与 RunCardSession 同语义。连续 update 各触发
34
- * 一次全卡 render + patch 是 PATCH 风暴主源;窗口内多个事件复用同一次延迟 flush。
35
- * 窗口大小可通过 constructor 覆盖(测试用小窗口/0 禁用)。
36
- */
37
- coalesceMs;
38
- flushTimer;
39
- flushInFlight = false;
40
- /** in-flight flush 期间到达的 update 标记:flush 完成后需重新调度(防 stale 帧)。 */
41
- pendingReschedule = false;
42
- /** 当前 in-flight flush 的 promise:finish 在终态 patch 前 await 它保证顺序。 */
43
- flushP;
44
29
  constructor(opts) {
45
30
  super({
46
31
  runId: opts.runId,
@@ -56,8 +41,8 @@ export class BashCardSession extends CardSession {
56
41
  renderOptions: opts.renderOptions,
57
42
  startTimeoutMs: opts.startTimeoutMs,
58
43
  settleTimeoutMs: opts.settleTimeoutMs,
44
+ coalesceMs: opts.coalesceMs,
59
45
  });
60
- this.coalesceMs = opts.coalesceMs ?? 100;
61
46
  }
62
47
  get logPrefix() {
63
48
  return '[bash-card]';
@@ -76,6 +61,10 @@ export class BashCardSession extends CardSession {
76
61
  // 卡应跳过它,靠自身 renderer 保护。
77
62
  return true;
78
63
  }
64
+ isFlushableState() {
65
+ // finish() 已把 state 转终态后,in-flight/延迟 flush 不再用旧 state patch。
66
+ return this.state.terminal === 'running';
67
+ }
79
68
  /** Patch the card with updated bash state (e.g. new stdout/stderr). */
80
69
  async update(patch) {
81
70
  this.state = this.applyCap(patch);
@@ -102,62 +91,6 @@ export class BashCardSession extends CardSession {
102
91
  await this.updateCard();
103
92
  this.release();
104
93
  }
105
- /**
106
- * 调度一次延迟 flush:已有 pending 调度则复用(合批核心);flushInFlight 期间
107
- * 不再起 timer(防重入叠加),但置 pendingReschedule 让 in-flight 完成后补渲染。
108
- */
109
- scheduleFlush() {
110
- if (this.flushTimer)
111
- return;
112
- if (this.flushInFlight) {
113
- this.pendingReschedule = true;
114
- return;
115
- }
116
- this.flushTimer = setTimeout(() => {
117
- this.flushTimer = undefined;
118
- void this.flush();
119
- }, this.coalesceMs);
120
- }
121
- /** 执行一次 render + updateCard(合批窗口到期或 finish 即时 flush 时调用)。 */
122
- async flush() {
123
- if (this.flushInFlight)
124
- return;
125
- // 终态守卫:finish 已把 state 转终态后,in-flight/延迟 flush 不再用旧 state patch
126
- if (this.state.terminal !== 'running')
127
- return;
128
- this.flushInFlight = true;
129
- const flushPromise = (async () => {
130
- try {
131
- await this.updateCard();
132
- }
133
- finally {
134
- this.flushInFlight = false;
135
- this.flushP = undefined;
136
- // in-flight 期间若有 update 到达(pendingReschedule),重新调度一次渲染
137
- if (this.pendingReschedule) {
138
- this.pendingReschedule = false;
139
- this.scheduleFlush();
140
- }
141
- }
142
- })();
143
- this.flushP = flushPromise;
144
- return flushPromise;
145
- }
146
- /** 取消待 flush 的合批调度(finish/settle 前清理,避免延迟 update 与终态竞争)。 */
147
- cancelPendingFlush() {
148
- this.pendingReschedule = false;
149
- if (this.flushTimer) {
150
- clearTimeout(this.flushTimer);
151
- this.flushTimer = undefined;
152
- return true;
153
- }
154
- return false;
155
- }
156
- /** settle 前兜底取消合批调度,避免 release 后 dangling timer 更新已释放的 stream。 */
157
- async settle() {
158
- this.cancelPendingFlush();
159
- return super.settle();
160
- }
161
94
  /**
162
95
  * 合并 patch 并对 output/stderr 做 store-time 截断(P1-3 层①)。
163
96
  * 截断发生在入口(store 时),渲染层无需再为大 state 付代价。
@@ -1,11 +1,10 @@
1
1
  import { terminalToColor, terminalToLabel, stopButton } from './card-shared.js';
2
2
  import { markdownDiv } from './collapsible.js';
3
- import { truncateUtf8 } from './text-truncate.js';
3
+ import { truncateUtf8, CARD_BUDGET_BYTES } from './text-truncate.js';
4
4
  /**
5
5
  * 飞书卡片大小限制(安全阈值,与 run-renderer 一致)。
6
- * 本地定义,不从 card-budget.ts 导入(避免流式/静态耦合)。
6
+ * 从共享的 text-truncate.ts 导入,避免三处各自定义后数值漂移。
7
7
  */
8
- const CARD_BUDGET_BYTES = 28_000;
9
8
  /**
10
9
  * 单个输出字段的默认最大字节数(保守值,用于正常路径)。
11
10
  * 飞书卡片限制约 28KB,留余量给卡片其他部分(header、status row、footer)
@@ -1,6 +1,4 @@
1
- import { truncateUtf8, truncateMarkdownTables, countMarkdownTables, FEISHU_MAX_TABLES, } from './text-truncate.js';
2
- // 飞书卡片大小限制(安全阈值,取 run-renderer 相同值)
3
- const CARD_BUDGET_BYTES = 28_000;
1
+ import { truncateUtf8, truncateMarkdownTables, countMarkdownTables, FEISHU_MAX_TABLES, CARD_BUDGET_BYTES, } from './text-truncate.js';
4
2
  /**
5
3
  * 静态卡片体积保护
6
4
  *
@@ -17,7 +15,6 @@ export function enforceCardBudget(card, opts = {}) {
17
15
  const totalTables = countCardTables(JSON.parse(cardStr));
18
16
  if (totalTables > FEISHU_MAX_TABLES) {
19
17
  // 深拷贝后对每个 lark_md 文本做 truncateMarkdownTables
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
18
  const fixed = JSON.parse(cardStr);
22
19
  truncateCardMarkdownTables(fixed);
23
20
  const fixedStr = JSON.stringify(fixed);
@@ -38,7 +35,6 @@ export function enforceCardBudget(card, opts = {}) {
38
35
  return { card, wasTruncated: false, bytesBefore, bytesAfter: bytesBefore };
39
36
  }
40
37
  // 字节超限但 table 不超限
41
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
38
  return enforceByteBudget(JSON.parse(cardStr), opts, bytesBefore);
43
39
  }
44
40
  /**
@@ -59,12 +55,9 @@ export function enforceCardBudget(card, opts = {}) {
59
55
  * 阶段4 截断剩余顶层 div 文本
60
56
  * 阶段5 最终兜底 minimal card(骨架本身已超限的病理场景)
61
57
  */
62
- function enforceByteBudget(
63
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
- truncated, opts = {}, bytesBefore) {
58
+ function enforceByteBudget(truncated, opts = {}, bytesBefore) {
65
59
  const { maxEventPanels = 5, maxPanelContentBytes = 2000, truncationHint = '📜 还有 {count} 个事件未显示', } = opts;
66
60
  const reasons = [];
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
61
  const elements = truncated.body?.elements ?? [];
69
62
  const size = () => Buffer.byteLength(JSON.stringify(truncated), 'utf8');
70
63
  const done = (wasTruncated) => ({
@@ -173,7 +166,6 @@ truncated, opts = {}, bytesBefore) {
173
166
  /**
174
167
  * 在卡片头部插入「N 个事件未显示」提示(仅在有面板被移除时)。
175
168
  */
176
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
169
  function insertOmittedHint(elements, omitted, truncationHint) {
178
170
  if (omitted <= 0)
179
171
  return;
@@ -185,7 +177,6 @@ function insertOmittedHint(elements, omitted, truncationHint) {
185
177
  /**
186
178
  * 获取所有 collapsible_panel 的索引(结构化判定,不看标题文本)。
187
179
  */
188
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
180
  function getPanelIndices(elements) {
190
181
  const indices = [];
191
182
  for (let i = 0; i < elements.length; i++) {
@@ -196,84 +187,47 @@ function getPanelIndices(elements) {
196
187
  return indices;
197
188
  }
198
189
  /**
199
- * Count all markdown tables across every lark_md text field in a card JSON.
200
- * Recursively walks elements, collapsible_panel.elements, column.elements, etc.
190
+ * 遍历卡片元素树(含 collapsible_panel.elements 与 column_set.columns[].elements)。
191
+ *
192
+ * count 与 truncate 两条路径共用同一遍历,避免两套递归漂移(G5 Duplication)。
201
193
  */
202
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
203
- function countCardTables(card) {
204
- let total = 0;
205
- const elements = card?.body?.elements ?? [];
194
+ function walkCardElements(elements, visit) {
206
195
  for (const el of elements) {
207
- total += countElementTables(el);
196
+ visit(el);
197
+ if (Array.isArray(el.elements))
198
+ walkCardElements(el.elements, visit);
199
+ if (Array.isArray(el.columns)) {
200
+ for (const col of el.columns) {
201
+ if (Array.isArray(col.elements))
202
+ walkCardElements(col.elements, visit);
203
+ }
204
+ }
208
205
  }
209
- return total;
210
206
  }
211
207
  /**
212
- * Recursively count markdown tables in a single element and its children.
208
+ * Count all markdown tables across every lark_md text field in a card JSON.
213
209
  */
214
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
- function countElementTables(el) {
216
- let count = 0;
217
- // lark_md div text
218
- if (el.text?.tag === 'lark_md' && typeof el.text.content === 'string') {
219
- count += countMarkdownTables(el.text.content);
220
- }
221
- // collapsible_panel inner elements
222
- if (Array.isArray(el.elements)) {
223
- for (const child of el.elements) {
224
- count += countElementTables(child);
225
- }
226
- }
227
- // column_set → columns → elements
228
- if (Array.isArray(el.columns)) {
229
- for (const col of el.columns) {
230
- if (Array.isArray(col.elements)) {
231
- for (const child of col.elements) {
232
- count += countElementTables(child);
233
- }
234
- }
210
+ function countCardTables(card) {
211
+ let total = 0;
212
+ walkCardElements(card?.body?.elements ?? [], (el) => {
213
+ if (el.text?.tag === 'lark_md' && typeof el.text.content === 'string') {
214
+ total += countMarkdownTables(el.text.content);
235
215
  }
236
- }
237
- return count;
216
+ });
217
+ return total;
238
218
  }
239
219
  /**
240
220
  * In-place truncate markdown tables in every lark_md text field of a card,
241
221
  * keeping at most FEISHU_MAX_TABLES per field.
242
222
  */
243
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
223
  function truncateCardMarkdownTables(card) {
245
- const elements = card?.body?.elements ?? [];
246
- for (const el of elements) {
247
- truncateElementTables(el);
248
- }
249
- }
250
- /**
251
- * Recursively truncate markdown tables in a single element and its children.
252
- */
253
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
254
- function truncateElementTables(el) {
255
- // lark_md div text
256
- if (el.text?.tag === 'lark_md' && typeof el.text.content === 'string') {
257
- const truncated = truncateMarkdownTables(el.text.content);
258
- if (truncated !== el.text.content) {
259
- el.text.content = truncated;
260
- }
261
- }
262
- // collapsible_panel inner elements
263
- if (Array.isArray(el.elements)) {
264
- for (const child of el.elements) {
265
- truncateElementTables(child);
266
- }
267
- }
268
- // column_set → columns → elements
269
- if (Array.isArray(el.columns)) {
270
- for (const col of el.columns) {
271
- if (Array.isArray(col.elements)) {
272
- for (const child of col.elements) {
273
- truncateElementTables(child);
274
- }
224
+ walkCardElements(card?.body?.elements ?? [], (el) => {
225
+ if (el.text?.tag === 'lark_md' && typeof el.text.content === 'string') {
226
+ const truncated = truncateMarkdownTables(el.text.content);
227
+ if (truncated !== el.text.content) {
228
+ el.text.content = truncated;
275
229
  }
276
230
  }
277
- }
231
+ });
278
232
  }
279
233
  //# sourceMappingURL=card-budget.js.map
@@ -52,10 +52,58 @@ export declare abstract class CardSession<S, RO = unknown> {
52
52
  renderOptions?: RO;
53
53
  startTimeoutMs?: number;
54
54
  settleTimeoutMs?: number;
55
+ /** update/push 合批窗口,默认 100ms(0 = 禁用合批,每事件立即 flush)。 */
56
+ coalesceMs?: number;
55
57
  });
58
+ /**
59
+ * 调度一次延迟 flush。已有 pending 调度则复用(合批核心);否则起一个
60
+ * coalesceMs 定时器。flushInFlight 期间不再起 timer(避免重入叠加),但置
61
+ * pendingReschedule 以便 in-flight flush 完成后重新调度——否则 in-flight 期间
62
+ * 到达的 push/update 会停留为 stale 中间帧(P2-1)。
63
+ */
64
+ protected scheduleFlush(): void;
65
+ /**
66
+ * 执行一次 render + updateCard。合批窗口到期或 finish 即时 flush 时调用。
67
+ * flushInFlight 防止 timer 回调与 finish 的即时 flush 重入。
68
+ *
69
+ * 终态守卫:若 finish() 已把 state 转为终态,跳过本次 patch——避免 in-flight flush
70
+ * 用 pre-terminal state 覆盖 finish 已发的终态卡。finish() 自身会 await 任何
71
+ * in-flight flush 保证顺序,此守卫是双保险。
72
+ */
73
+ protected flush(): Promise<void>;
74
+ /**
75
+ * Whether the current state still permits a flush (i.e. is not yet terminal).
76
+ * Subclasses define their terminal set. Run treats 'finalizing' as non-terminal.
77
+ */
78
+ protected abstract isFlushableState(): boolean;
79
+ /**
80
+ * 取消待 flush 的合批调度(finish/settle 前清理,避免延迟 update 与终态
81
+ * 渲染竞争)。返回是否有被取消的 pending 调度。同时清掉 pendingReschedule,
82
+ * 防止 in-flight flush 的 finally 在 finish await 之后又起 dangling timer
83
+ * (finish 自身会渲染含累积事件的终态卡,无需 follow-up)。
84
+ */
85
+ protected cancelPendingFlush(): boolean;
56
86
  get currentState(): S;
57
87
  /** The messageId of the streaming card, available after `start()` resolves. */
58
88
  get cardMessageId(): string | undefined;
89
+ /**
90
+ * P1-3 合批窗口(ms)。连续事件各触发一次全卡 render + patch 是流式 CPU 主要
91
+ * 来源;窗口内多个事件复用同一次延迟 flush。窗口大小可通过 constructor 覆盖
92
+ * (测试用小窗口/0 禁用)。run/bash 子类共用的 flush 骨架(scheduleFlush/flush/
93
+ * cancelPendingFlush/settle)下沉到基类。
94
+ */
95
+ protected readonly coalesceMs: number;
96
+ private flushTimer;
97
+ private flushInFlight; /** P2-1:in-flight flush 期间到达的 push/update 标记。scheduleFlush 在
98
+ * flushInFlight 时早退不调度新 timer,但事件已 reduce 进 state;若无此标志,
99
+ * in-flight flush 完成后该事件无 follow-up render,停留为 stale 中间帧。
100
+ * finally 检查此标志后重新 scheduleFlush,保证 in-flight 期间累积的事件最终被渲染。 */
101
+ private pendingReschedule;
102
+ /** 当前 in-flight flush 的 promise(若 flush 正在 await updateCard)。
103
+ * finish() 在终态 patch 前 await 它,保证 in-flight 的 pre-terminal patch 先落地、
104
+ * terminal patch 后落地——否则两条 controller.update 并发无 FIFO,
105
+ * pre-terminal 可能在 terminal 之后 resolve 留下"思考中"终帧(P2)。 */
106
+ protected flushP: Promise<void> | undefined;
59
107
  start(): Promise<void>;
60
108
  settle(): Promise<StreamSettleResult>;
61
109
  /**