lark-remote 0.1.0

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 (201) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +161 -0
  5. package/dist/binder.d.ts +57 -0
  6. package/dist/binder.js +105 -0
  7. package/dist/bridge/index.d.ts +338 -0
  8. package/dist/bridge/index.js +1281 -0
  9. package/dist/bridge/queue-manager.d.ts +277 -0
  10. package/dist/bridge/queue-manager.js +729 -0
  11. package/dist/card/bash-card-session.d.ts +78 -0
  12. package/dist/card/bash-card-session.js +174 -0
  13. package/dist/card/bash-renderer.d.ts +24 -0
  14. package/dist/card/bash-renderer.js +187 -0
  15. package/dist/card/card-budget.d.ts +31 -0
  16. package/dist/card/card-budget.js +280 -0
  17. package/dist/card/card-session.d.ts +83 -0
  18. package/dist/card/card-session.js +185 -0
  19. package/dist/card/card-shared.d.ts +28 -0
  20. package/dist/card/card-shared.js +99 -0
  21. package/dist/card/collapsible.d.ts +26 -0
  22. package/dist/card/collapsible.js +87 -0
  23. package/dist/card/run-card-session.d.ts +78 -0
  24. package/dist/card/run-card-session.js +165 -0
  25. package/dist/card/run-renderer.d.ts +66 -0
  26. package/dist/card/run-renderer.js +841 -0
  27. package/dist/card/run-state.d.ts +103 -0
  28. package/dist/card/run-state.js +313 -0
  29. package/dist/card/text-truncate.d.ts +56 -0
  30. package/dist/card/text-truncate.js +184 -0
  31. package/dist/card/time.d.ts +1 -0
  32. package/dist/card/time.js +14 -0
  33. package/dist/card/tool-render.d.ts +25 -0
  34. package/dist/card/tool-render.js +176 -0
  35. package/dist/cli.d.ts +28 -0
  36. package/dist/cli.js +89 -0
  37. package/dist/common/guards.d.ts +37 -0
  38. package/dist/common/guards.js +65 -0
  39. package/dist/config/codex-config.d.ts +113 -0
  40. package/dist/config/codex-config.js +428 -0
  41. package/dist/config/dir.d.ts +17 -0
  42. package/dist/config/dir.js +81 -0
  43. package/dist/config/index.d.ts +219 -0
  44. package/dist/config/index.js +388 -0
  45. package/dist/config/kimi-config.d.ts +33 -0
  46. package/dist/config/kimi-config.js +111 -0
  47. package/dist/config/opencode-config.d.ts +20 -0
  48. package/dist/config/opencode-config.js +124 -0
  49. package/dist/config/pi-config.d.ts +49 -0
  50. package/dist/config/pi-config.js +151 -0
  51. package/dist/config/wizard.d.ts +12 -0
  52. package/dist/config/wizard.js +84 -0
  53. package/dist/connector/file-limits.d.ts +7 -0
  54. package/dist/connector/file-limits.js +8 -0
  55. package/dist/connector/index.d.ts +86 -0
  56. package/dist/connector/index.js +340 -0
  57. package/dist/error-classification.d.ts +15 -0
  58. package/dist/error-classification.js +24 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +580 -0
  61. package/dist/instance-lock.d.ts +30 -0
  62. package/dist/instance-lock.js +184 -0
  63. package/dist/logger/index.d.ts +57 -0
  64. package/dist/logger/index.js +178 -0
  65. package/dist/order/index.d.ts +27 -0
  66. package/dist/order/index.js +90 -0
  67. package/dist/persistence/atomic-write.d.ts +10 -0
  68. package/dist/persistence/atomic-write.js +65 -0
  69. package/dist/persistence/load-json-file.d.ts +9 -0
  70. package/dist/persistence/load-json-file.js +23 -0
  71. package/dist/restart.d.ts +24 -0
  72. package/dist/restart.js +73 -0
  73. package/dist/router/card-helpers.d.ts +28 -0
  74. package/dist/router/card-helpers.js +65 -0
  75. package/dist/router/config/claude.d.ts +14 -0
  76. package/dist/router/config/claude.js +72 -0
  77. package/dist/router/config/codex.d.ts +22 -0
  78. package/dist/router/config/codex.js +100 -0
  79. package/dist/router/config/common/render.d.ts +16 -0
  80. package/dist/router/config/common/render.js +158 -0
  81. package/dist/router/config/index.d.ts +18 -0
  82. package/dist/router/config/index.js +38 -0
  83. package/dist/router/config/kimi.d.ts +21 -0
  84. package/dist/router/config/kimi.js +63 -0
  85. package/dist/router/config/opencode.d.ts +12 -0
  86. package/dist/router/config/opencode.js +44 -0
  87. package/dist/router/config/pi.d.ts +12 -0
  88. package/dist/router/config/pi.js +50 -0
  89. package/dist/router/config/types.d.ts +43 -0
  90. package/dist/router/config/types.js +8 -0
  91. package/dist/router/index.d.ts +318 -0
  92. package/dist/router/index.js +2715 -0
  93. package/dist/router/order-exec-dispatch.d.ts +30 -0
  94. package/dist/router/order-exec-dispatch.js +48 -0
  95. package/dist/router/utils.d.ts +44 -0
  96. package/dist/router/utils.js +116 -0
  97. package/dist/runner/bash/index.d.ts +4 -0
  98. package/dist/runner/bash/index.js +5 -0
  99. package/dist/runner/bash/runner.d.ts +55 -0
  100. package/dist/runner/bash/runner.js +213 -0
  101. package/dist/runner/claude/index.d.ts +4 -0
  102. package/dist/runner/claude/index.js +5 -0
  103. package/dist/runner/claude/runner.d.ts +29 -0
  104. package/dist/runner/claude/runner.js +83 -0
  105. package/dist/runner/codex/argv.d.ts +33 -0
  106. package/dist/runner/codex/argv.js +40 -0
  107. package/dist/runner/codex/index.d.ts +1 -0
  108. package/dist/runner/codex/index.js +2 -0
  109. package/dist/runner/codex/jsonl.d.ts +37 -0
  110. package/dist/runner/codex/jsonl.js +221 -0
  111. package/dist/runner/codex/runner.d.ts +65 -0
  112. package/dist/runner/codex/runner.js +124 -0
  113. package/dist/runner/common/exec-translator.d.ts +73 -0
  114. package/dist/runner/common/exec-translator.js +105 -0
  115. package/dist/runner/common/jsonl-stream.d.ts +39 -0
  116. package/dist/runner/common/jsonl-stream.js +203 -0
  117. package/dist/runner/common/process-stopper.d.ts +28 -0
  118. package/dist/runner/common/process-stopper.js +79 -0
  119. package/dist/runner/common/runner-utils.d.ts +21 -0
  120. package/dist/runner/common/runner-utils.js +65 -0
  121. package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
  122. package/dist/runner/common/spawn-heartbeat.js +51 -0
  123. package/dist/runner/common/spawning-runner.d.ts +321 -0
  124. package/dist/runner/common/spawning-runner.js +634 -0
  125. package/dist/runner/common/usage.d.ts +31 -0
  126. package/dist/runner/common/usage.js +29 -0
  127. package/dist/runner/index.d.ts +19 -0
  128. package/dist/runner/index.js +24 -0
  129. package/dist/runner/kimi/index.d.ts +1 -0
  130. package/dist/runner/kimi/index.js +2 -0
  131. package/dist/runner/kimi/jsonl.d.ts +28 -0
  132. package/dist/runner/kimi/jsonl.js +143 -0
  133. package/dist/runner/kimi/runner.d.ts +50 -0
  134. package/dist/runner/kimi/runner.js +105 -0
  135. package/dist/runner/opencode/argv.d.ts +32 -0
  136. package/dist/runner/opencode/argv.js +35 -0
  137. package/dist/runner/opencode/index.d.ts +1 -0
  138. package/dist/runner/opencode/index.js +2 -0
  139. package/dist/runner/opencode/jsonl.d.ts +68 -0
  140. package/dist/runner/opencode/jsonl.js +295 -0
  141. package/dist/runner/opencode/runner.d.ts +63 -0
  142. package/dist/runner/opencode/runner.js +93 -0
  143. package/dist/runner/pi/index.d.ts +1 -0
  144. package/dist/runner/pi/index.js +2 -0
  145. package/dist/runner/pi/jsonl.d.ts +128 -0
  146. package/dist/runner/pi/jsonl.js +249 -0
  147. package/dist/runner/pi/runner.d.ts +37 -0
  148. package/dist/runner/pi/runner.js +76 -0
  149. package/dist/runner/registry.d.ts +54 -0
  150. package/dist/runner/registry.js +75 -0
  151. package/dist/runner/resolve-agent-choices.d.ts +7 -0
  152. package/dist/runner/resolve-agent-choices.js +76 -0
  153. package/dist/runner/sync-agent-choices.d.ts +6 -0
  154. package/dist/runner/sync-agent-choices.js +73 -0
  155. package/dist/runner/types.d.ts +250 -0
  156. package/dist/runner/types.js +8 -0
  157. package/dist/session/claude/index.d.ts +1 -0
  158. package/dist/session/claude/index.js +2 -0
  159. package/dist/session/claude/session-reader.d.ts +34 -0
  160. package/dist/session/claude/session-reader.js +46 -0
  161. package/dist/session/claude/sessions.d.ts +53 -0
  162. package/dist/session/claude/sessions.js +528 -0
  163. package/dist/session/codex/index.d.ts +1 -0
  164. package/dist/session/codex/index.js +2 -0
  165. package/dist/session/codex/rollout-reader.d.ts +88 -0
  166. package/dist/session/codex/rollout-reader.js +473 -0
  167. package/dist/session/codex/sessions.d.ts +21 -0
  168. package/dist/session/codex/sessions.js +40 -0
  169. package/dist/session/common/catch-up.d.ts +12 -0
  170. package/dist/session/common/catch-up.js +31 -0
  171. package/dist/session/common/constants.d.ts +11 -0
  172. package/dist/session/common/constants.js +12 -0
  173. package/dist/session/common/content-blocks.d.ts +37 -0
  174. package/dist/session/common/content-blocks.js +68 -0
  175. package/dist/session/common/jsonl.d.ts +77 -0
  176. package/dist/session/common/jsonl.js +351 -0
  177. package/dist/session/common/usage-accumulator.d.ts +58 -0
  178. package/dist/session/common/usage-accumulator.js +72 -0
  179. package/dist/session/index.d.ts +7 -0
  180. package/dist/session/index.js +11 -0
  181. package/dist/session/kimi/index.d.ts +1 -0
  182. package/dist/session/kimi/index.js +2 -0
  183. package/dist/session/kimi/sessions.d.ts +81 -0
  184. package/dist/session/kimi/sessions.js +527 -0
  185. package/dist/session/opencode/index.d.ts +1 -0
  186. package/dist/session/opencode/index.js +2 -0
  187. package/dist/session/opencode/sessions.d.ts +81 -0
  188. package/dist/session/opencode/sessions.js +388 -0
  189. package/dist/session/pi/index.d.ts +1 -0
  190. package/dist/session/pi/index.js +2 -0
  191. package/dist/session/pi/sessions.d.ts +27 -0
  192. package/dist/session/pi/sessions.js +399 -0
  193. package/dist/session/registry.d.ts +12 -0
  194. package/dist/session/registry.js +20 -0
  195. package/dist/session/session-store.d.ts +86 -0
  196. package/dist/session/session-store.js +450 -0
  197. package/dist/startup-contact.d.ts +18 -0
  198. package/dist/startup-contact.js +51 -0
  199. package/dist/workspace/index.d.ts +19 -0
  200. package/dist/workspace/index.js +53 -0
  201. package/package.json +93 -0
@@ -0,0 +1,280 @@
1
+ import { truncateUtf8, truncateMarkdownTables, countMarkdownTables, FEISHU_MAX_TABLES, } from './text-truncate.js';
2
+ // 飞书卡片大小限制(安全阈值,取 run-renderer 相同值)
3
+ const CARD_BUDGET_BYTES = 28_000;
4
+ /**
5
+ * 静态卡片体积保护
6
+ *
7
+ * @param card - 原始卡片 JSON
8
+ * @param opts - 裁剪选项
9
+ * @returns 裁剪后的卡片 + 是否发生了裁剪 + 原因 + 截断前后字节数
10
+ */
11
+ export function enforceCardBudget(card, opts = {}) {
12
+ // ========== 阶段0:table 数量保护(独立于字节预算) ==========
13
+ // 飞书 11310 限制:单卡 markdown table >5 就报错。此维度与字节数正交——
14
+ // 小体积可以含很多 table,大体积可以没有 table。必须在字节检查之前独立判断。
15
+ const cardStr = JSON.stringify(card);
16
+ const bytesBefore = Buffer.byteLength(cardStr, 'utf8');
17
+ const totalTables = countCardTables(JSON.parse(cardStr));
18
+ if (totalTables > FEISHU_MAX_TABLES) {
19
+ // 深拷贝后对每个 lark_md 文本做 truncateMarkdownTables
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ const fixed = JSON.parse(cardStr);
22
+ truncateCardMarkdownTables(fixed);
23
+ const fixedStr = JSON.stringify(fixed);
24
+ // table 截断后可能仍超字节预算,继续走后续阶段
25
+ if (Buffer.byteLength(fixedStr, 'utf8') <= CARD_BUDGET_BYTES) {
26
+ return {
27
+ card: fixed,
28
+ wasTruncated: true,
29
+ reason: 'table_count_limited',
30
+ bytesBefore,
31
+ bytesAfter: Buffer.byteLength(fixedStr, 'utf8'),
32
+ };
33
+ }
34
+ // table 修好了但字节仍超,用 fixed 作为后续处理的起点
35
+ return enforceByteBudget(fixed, opts, bytesBefore);
36
+ }
37
+ if (bytesBefore <= CARD_BUDGET_BYTES) {
38
+ return { card, wasTruncated: false, bytesBefore, bytesAfter: bytesBefore };
39
+ }
40
+ // 字节超限但 table 不超限
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ return enforceByteBudget(JSON.parse(cardStr), opts, bytesBefore);
43
+ }
44
+ /**
45
+ * Byte-budget enforcement, extracted so table-limit fix can delegate to it
46
+ * when table truncation alone doesn't bring the card under the byte limit.
47
+ *
48
+ * 裁剪判定是**结构化**的:所有 collapsible_panel 都是待裁剪对象,不看面板
49
+ * 标题文本。历史教训(2026-08-08,详见
50
+ * docs/zh/architecture/resume-card-budget-failure-analysis.md):旧实现用
51
+ * emoji 标题模式(🤖👤💭🔧🟢🔴)识别「会话事件面板」,但真实
52
+ * sessionEventPanel 产出的标题是裸英文 type 名(emoji 在事件正文而非标题),
53
+ * 导致裁剪阶段对真实 resume 卡零命中,任何 >28KB 的卡片无裁剪直通极端降级,
54
+ * 整卡被替换成一句话。
55
+ *
56
+ * 渐进式降级(每级仍超限才进入下一级):
57
+ * 阶段1 逐面板截断内容(maxPanelContentBytes)
58
+ * 阶段2 限制面板数量(删除最老,maxEventPanels)
59
+ * 阶段3 丢弃全部面板,保留骨架(header/usage/按钮)+ 省略提示
60
+ * 阶段4 截断剩余顶层 div 文本
61
+ * 阶段5 最终兜底 minimal card(骨架本身已超限的病理场景)
62
+ */
63
+ function enforceByteBudget(
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ truncated, opts = {}, bytesBefore) {
66
+ const { maxEventPanels = 5, maxPanelContentBytes = 2000, truncationHint = '📜 还有 {count} 个事件未显示', } = opts;
67
+ const reasons = [];
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ const elements = truncated.body?.elements ?? [];
70
+ const size = () => Buffer.byteLength(JSON.stringify(truncated), 'utf8');
71
+ const done = (wasTruncated) => ({
72
+ card: truncated,
73
+ wasTruncated,
74
+ reason: reasons.join('+') || undefined,
75
+ bytesBefore: bytesBefore ?? size(),
76
+ bytesAfter: size(),
77
+ });
78
+ // ========== 阶段1:裁剪所有面板的内容 ==========
79
+ let modifications = 0;
80
+ for (const el of elements) {
81
+ if (el.tag !== 'collapsible_panel')
82
+ continue;
83
+ for (const inner of el.elements ?? []) {
84
+ if (inner.tag === 'div' && inner.text?.content) {
85
+ const content = inner.text.content;
86
+ if (Buffer.byteLength(content, 'utf8') > maxPanelContentBytes) {
87
+ inner.text.content = truncateUtf8(content, maxPanelContentBytes);
88
+ modifications++;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ if (modifications > 0) {
94
+ reasons.push('panel_content_truncated');
95
+ }
96
+ // ========== 阶段2:限制面板数量(删除最老) ==========
97
+ let omitted = 0;
98
+ const dropOldestPanels = (count) => {
99
+ const indices = getPanelIndices(elements)
100
+ .slice(0, count)
101
+ .sort((a, b) => b - a);
102
+ for (const idx of indices) {
103
+ elements.splice(idx, 1);
104
+ omitted++;
105
+ }
106
+ };
107
+ const panelCount = getPanelIndices(elements).length;
108
+ if (panelCount > maxEventPanels) {
109
+ dropOldestPanels(panelCount - maxEventPanels);
110
+ reasons.push('event_count_limited');
111
+ }
112
+ if (size() <= CARD_BUDGET_BYTES) {
113
+ insertOmittedHint(elements, omitted, truncationHint);
114
+ return done(modifications > 0 || omitted > 0);
115
+ }
116
+ // ========== 阶段3:丢弃全部面板,保留卡片骨架 ==========
117
+ // header(cwd/sessionId/标题)、usage、操作按钮等信息量高、体积小的元素
118
+ // 必须保留——整卡替换为一句话是信息损失最大的降级方式。
119
+ dropOldestPanels(getPanelIndices(elements).length);
120
+ if (omitted > 0) {
121
+ reasons.push('panels_dropped');
122
+ }
123
+ if (size() <= CARD_BUDGET_BYTES) {
124
+ insertOmittedHint(elements, omitted, truncationHint);
125
+ return done(true);
126
+ }
127
+ // ========== 阶段4:截断剩余顶层 div 文本 ==========
128
+ // 骨架元素(header/usage 等)自身超限的情况(如超大 displayTitle)。
129
+ insertOmittedHint(elements, omitted, truncationHint);
130
+ let textTruncations = 0;
131
+ for (const el of elements) {
132
+ if (el.tag === 'div' && el.text?.content) {
133
+ const content = el.text.content;
134
+ if (Buffer.byteLength(content, 'utf8') > maxPanelContentBytes) {
135
+ el.text.content = truncateUtf8(content, maxPanelContentBytes);
136
+ textTruncations++;
137
+ }
138
+ }
139
+ }
140
+ if (textTruncations > 0) {
141
+ reasons.push('text_truncated');
142
+ }
143
+ if (size() <= CARD_BUDGET_BYTES) {
144
+ return done(true);
145
+ }
146
+ // ========== 阶段5:最终兜底(原 extreme_fallback) ==========
147
+ const minimalCard = {
148
+ schema: '2.0',
149
+ config: { wide_screen_mode: true },
150
+ header: {
151
+ template: 'orange',
152
+ title: { tag: 'plain_text', content: '⚠️ 内容已截断' },
153
+ },
154
+ body: {
155
+ elements: [
156
+ {
157
+ tag: 'div',
158
+ text: {
159
+ tag: 'lark_md',
160
+ content: '会话内容过大,已自动截断。\n\n请使用 `/active` 查看完整列表。',
161
+ },
162
+ },
163
+ ],
164
+ },
165
+ };
166
+ return {
167
+ card: minimalCard,
168
+ wasTruncated: true,
169
+ reason: 'extreme_fallback',
170
+ bytesBefore: bytesBefore ?? size(),
171
+ bytesAfter: Buffer.byteLength(JSON.stringify(minimalCard), 'utf8'),
172
+ };
173
+ }
174
+ /**
175
+ * 在卡片头部插入「N 个事件未显示」提示(仅在有面板被移除时)。
176
+ */
177
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
178
+ function insertOmittedHint(elements, omitted, truncationHint) {
179
+ if (omitted <= 0)
180
+ return;
181
+ elements.unshift({
182
+ tag: 'div',
183
+ text: { tag: 'lark_md', content: truncationHint.replace('{count}', String(omitted)) },
184
+ });
185
+ }
186
+ /**
187
+ * 获取所有 collapsible_panel 的索引(结构化判定,不看标题文本)。
188
+ */
189
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
190
+ function getPanelIndices(elements) {
191
+ const indices = [];
192
+ for (let i = 0; i < elements.length; i++) {
193
+ if (elements[i].tag === 'collapsible_panel') {
194
+ indices.push(i);
195
+ }
196
+ }
197
+ return indices;
198
+ }
199
+ /**
200
+ * Count all markdown tables across every lark_md text field in a card JSON.
201
+ * Recursively walks elements, collapsible_panel.elements, column.elements, etc.
202
+ */
203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
204
+ function countCardTables(card) {
205
+ let total = 0;
206
+ const elements = card?.body?.elements ?? [];
207
+ for (const el of elements) {
208
+ total += countElementTables(el);
209
+ }
210
+ return total;
211
+ }
212
+ /**
213
+ * Recursively count markdown tables in a single element and its children.
214
+ */
215
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
216
+ function countElementTables(el) {
217
+ let count = 0;
218
+ // lark_md div text
219
+ if (el.text?.tag === 'lark_md' && typeof el.text.content === 'string') {
220
+ count += countMarkdownTables(el.text.content);
221
+ }
222
+ // collapsible_panel inner elements
223
+ if (Array.isArray(el.elements)) {
224
+ for (const child of el.elements) {
225
+ count += countElementTables(child);
226
+ }
227
+ }
228
+ // column_set → columns → elements
229
+ if (Array.isArray(el.columns)) {
230
+ for (const col of el.columns) {
231
+ if (Array.isArray(col.elements)) {
232
+ for (const child of col.elements) {
233
+ count += countElementTables(child);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ return count;
239
+ }
240
+ /**
241
+ * In-place truncate markdown tables in every lark_md text field of a card,
242
+ * keeping at most FEISHU_MAX_TABLES per field.
243
+ */
244
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
245
+ function truncateCardMarkdownTables(card) {
246
+ const elements = card?.body?.elements ?? [];
247
+ for (const el of elements) {
248
+ truncateElementTables(el);
249
+ }
250
+ }
251
+ /**
252
+ * Recursively truncate markdown tables in a single element and its children.
253
+ */
254
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
255
+ function truncateElementTables(el) {
256
+ // lark_md div text
257
+ if (el.text?.tag === 'lark_md' && typeof el.text.content === 'string') {
258
+ const truncated = truncateMarkdownTables(el.text.content);
259
+ if (truncated !== el.text.content) {
260
+ el.text.content = truncated;
261
+ }
262
+ }
263
+ // collapsible_panel inner elements
264
+ if (Array.isArray(el.elements)) {
265
+ for (const child of el.elements) {
266
+ truncateElementTables(child);
267
+ }
268
+ }
269
+ // column_set → columns → elements
270
+ if (Array.isArray(el.columns)) {
271
+ for (const col of el.columns) {
272
+ if (Array.isArray(col.elements)) {
273
+ for (const child of col.elements) {
274
+ truncateElementTables(child);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+ //# sourceMappingURL=card-budget.js.map
@@ -0,0 +1,83 @@
1
+ import type { CardStreamController, CardStreamProducer } from '@larksuite/channel';
2
+ /**
3
+ * Unified channel interface for streaming card sessions.
4
+ */
5
+ export interface CardChannel {
6
+ streamCard(chatId: string, initial: object, producer: CardStreamProducer, opts?: {
7
+ replyTo?: string;
8
+ }): Promise<string>;
9
+ updateCard(messageId: string, card: object): Promise<void>;
10
+ }
11
+ export type StreamSettleResult = 'streamed' | 'updated' | 'unsent';
12
+ /**
13
+ * Base class for single-card streaming sessions (RunCardSession, BashCardSession).
14
+ * Provides the shared lifecycle: start → (push/update) → finish → settle.
15
+ *
16
+ * Subclasses must implement `renderCard()` to produce the card JSON for their
17
+ * specific state type, and `logPrefix` / `errorPrefix` for differentiated
18
+ * log and error messages.
19
+ */
20
+ export declare abstract class CardSession<S, RO = unknown> {
21
+ protected readonly connector: CardChannel;
22
+ protected readonly chatId: string;
23
+ protected readonly replyTo?: string;
24
+ protected readonly renderOptions: RO;
25
+ protected readonly startTimeoutMs: number;
26
+ protected readonly settleTimeoutMs: number;
27
+ protected state: S;
28
+ protected controller?: CardStreamController;
29
+ protected messageId?: string;
30
+ protected streamOutcome?: Promise<{
31
+ ok: true;
32
+ } | {
33
+ ok: false;
34
+ error: unknown;
35
+ }>;
36
+ private releaseProducer;
37
+ private readonly producerReleased;
38
+ private resolveController;
39
+ private rejectController;
40
+ private readonly controllerReady;
41
+ private released;
42
+ /** Log prefix for differentiated messages (e.g. '[card]' vs '[bash-card]'). */
43
+ protected abstract get logPrefix(): string;
44
+ /** Prefix for timeout error messages (e.g. 'card' vs 'bash card'). */
45
+ protected abstract get errorPrefix(): string;
46
+ /** Render the current state into a card JSON object. */
47
+ protected abstract renderCard(state: S, options: RO): object;
48
+ constructor(initialState: S, opts: {
49
+ connector: CardChannel;
50
+ chatId: string;
51
+ replyTo?: string;
52
+ renderOptions?: RO;
53
+ startTimeoutMs?: number;
54
+ settleTimeoutMs?: number;
55
+ });
56
+ get currentState(): S;
57
+ /** The messageId of the streaming card, available after `start()` resolves. */
58
+ get cardMessageId(): string | undefined;
59
+ start(): Promise<void>;
60
+ settle(): Promise<StreamSettleResult>;
61
+ /**
62
+ * Whether renderCard() already enforces a size budget (so updateCard can
63
+ * skip enforceCardBudget). Subclasses whose renderer has its own
64
+ * stringify-level budget protection (e.g. RunCardSession via degraded/
65
+ * extreme fallback) should override this to return true.
66
+ */
67
+ protected hasOwnBudgetProtection(): boolean;
68
+ /**
69
+ * Render the current card and apply stringify-level budget enforcement unless
70
+ * the subclass renderer has its own budget protection. Centralizes the budget
71
+ * decision so start()/updateCard()/settle() stay symmetric — previously only
72
+ * updateCard() guarded, while start() producer and settle() fallback sent bare
73
+ * renderCard output (risking >28KB for BashCardSession, which has no own
74
+ * stringify-level safety net).
75
+ */
76
+ protected safeRenderCard(): object;
77
+ /**
78
+ * Push an update to the card. Tries controller.update first, falls back to
79
+ * connector.updateCard if the controller is unavailable or fails.
80
+ */
81
+ protected updateCard(): Promise<void>;
82
+ protected release(): void;
83
+ }
@@ -0,0 +1,185 @@
1
+ import { getLogger } from '../logger/index.js';
2
+ import { enforceCardBudget } from './card-budget.js';
3
+ /**
4
+ * Base class for single-card streaming sessions (RunCardSession, BashCardSession).
5
+ * Provides the shared lifecycle: start → (push/update) → finish → settle.
6
+ *
7
+ * Subclasses must implement `renderCard()` to produce the card JSON for their
8
+ * specific state type, and `logPrefix` / `errorPrefix` for differentiated
9
+ * log and error messages.
10
+ */
11
+ export class CardSession {
12
+ connector;
13
+ chatId;
14
+ replyTo;
15
+ renderOptions;
16
+ startTimeoutMs;
17
+ settleTimeoutMs;
18
+ state;
19
+ controller;
20
+ messageId;
21
+ streamOutcome;
22
+ releaseProducer;
23
+ producerReleased;
24
+ resolveController;
25
+ rejectController;
26
+ controllerReady;
27
+ released = false;
28
+ constructor(initialState, opts) {
29
+ this.state = initialState;
30
+ this.connector = opts.connector;
31
+ this.chatId = opts.chatId;
32
+ this.replyTo = opts.replyTo;
33
+ this.renderOptions = opts.renderOptions ?? {};
34
+ this.startTimeoutMs = opts.startTimeoutMs ?? 5_000;
35
+ this.settleTimeoutMs = opts.settleTimeoutMs ?? 5_000;
36
+ this.producerReleased = new Promise((resolve) => {
37
+ this.releaseProducer = resolve;
38
+ });
39
+ this.controllerReady = new Promise((resolve, reject) => {
40
+ this.resolveController = resolve;
41
+ this.rejectController = reject;
42
+ });
43
+ }
44
+ get currentState() {
45
+ return this.state;
46
+ }
47
+ /** The messageId of the streaming card, available after `start()` resolves. */
48
+ get cardMessageId() {
49
+ return this.messageId;
50
+ }
51
+ async start() {
52
+ if (this.streamOutcome) {
53
+ // 二次调用也套 timeout 保护:若 controllerReady 永不 settle(首次超时失败),
54
+ // 不能永久挂起。复用同样的 startTimeoutMs 超时。streamOutcome 已存在,
55
+ // 不重复创建 stream。
56
+ let timer;
57
+ const timeout = new Promise((_, reject) => {
58
+ timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
59
+ });
60
+ try {
61
+ await Promise.race([this.controllerReady, timeout]);
62
+ }
63
+ finally {
64
+ if (timer)
65
+ clearTimeout(timer);
66
+ }
67
+ return;
68
+ }
69
+ // NOTE: the two renderCard calls below are NOT redundant. The streamCard
70
+ // `initial` payload is evaluated EAGERLY (at start time, before any event
71
+ // has arrived), while the producer's `controller.update` runs LAZILY after
72
+ // the controller is ready — by which time events pushed before the
73
+ // producer ran (see test_anchor_events_before_controller_ready_...) have
74
+ // already updated `this.state`. Reusing the initial card here would drop
75
+ // those early events. The P3-7 "reuse first render" optimization was
76
+ // rejected for this reason (premise "two renders with identical input"
77
+ // does not hold).
78
+ const stream = this.connector.streamCard(this.chatId, this.safeRenderCard(), async (controller) => {
79
+ this.controller = controller;
80
+ this.messageId = controller.messageId;
81
+ try {
82
+ await controller.update(this.safeRenderCard());
83
+ }
84
+ catch (error) {
85
+ getLogger().warn(`${this.logPrefix} initial controller update failed:`, error);
86
+ }
87
+ this.resolveController();
88
+ await this.producerReleased;
89
+ }, { replyTo: this.replyTo });
90
+ this.streamOutcome = stream.then(() => ({ ok: true }), (error) => {
91
+ if (!this.controller)
92
+ this.rejectController(error);
93
+ return { ok: false, error };
94
+ });
95
+ let timer;
96
+ const timeout = new Promise((_, reject) => {
97
+ timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
98
+ });
99
+ try {
100
+ await Promise.race([this.controllerReady, timeout]);
101
+ }
102
+ finally {
103
+ if (timer)
104
+ clearTimeout(timer);
105
+ }
106
+ }
107
+ async settle() {
108
+ this.release();
109
+ if (!this.streamOutcome)
110
+ return 'unsent';
111
+ let timer;
112
+ const timeout = new Promise((resolve) => {
113
+ timer = setTimeout(() => resolve({ ok: false, error: new Error(`${this.errorPrefix} stream settle timeout`) }), this.settleTimeoutMs);
114
+ });
115
+ const outcome = await Promise.race([this.streamOutcome, timeout]);
116
+ if (timer)
117
+ clearTimeout(timer);
118
+ if (outcome.ok)
119
+ return 'streamed';
120
+ getLogger().warn(`${this.logPrefix} stream did not complete cleanly:`, outcome.error);
121
+ if (!this.messageId)
122
+ return 'unsent';
123
+ try {
124
+ await this.connector.updateCard(this.messageId, this.safeRenderCard());
125
+ return 'updated';
126
+ }
127
+ catch (error) {
128
+ getLogger().error(`${this.logPrefix} failed to finalize original card:`, error);
129
+ return 'unsent';
130
+ }
131
+ }
132
+ /**
133
+ * Whether renderCard() already enforces a size budget (so updateCard can
134
+ * skip enforceCardBudget). Subclasses whose renderer has its own
135
+ * stringify-level budget protection (e.g. RunCardSession via degraded/
136
+ * extreme fallback) should override this to return true.
137
+ */
138
+ hasOwnBudgetProtection() {
139
+ return false;
140
+ }
141
+ /**
142
+ * Render the current card and apply stringify-level budget enforcement unless
143
+ * the subclass renderer has its own budget protection. Centralizes the budget
144
+ * decision so start()/updateCard()/settle() stay symmetric — previously only
145
+ * updateCard() guarded, while start() producer and settle() fallback sent bare
146
+ * renderCard output (risking >28KB for BashCardSession, which has no own
147
+ * stringify-level safety net).
148
+ */
149
+ safeRenderCard() {
150
+ const card = this.renderCard(this.state, this.renderOptions);
151
+ return this.hasOwnBudgetProtection() ? card : enforceCardBudget(card).card;
152
+ }
153
+ /**
154
+ * Push an update to the card. Tries controller.update first, falls back to
155
+ * connector.updateCard if the controller is unavailable or fails.
156
+ */
157
+ async updateCard() {
158
+ const safeCard = this.safeRenderCard();
159
+ if (this.controller) {
160
+ try {
161
+ await this.controller.update(safeCard);
162
+ return;
163
+ }
164
+ catch (error) {
165
+ getLogger().warn(`${this.logPrefix} controller update failed; finalization will retry:`, error);
166
+ }
167
+ }
168
+ // Fallback: try updateCard when controller is unavailable or update failed
169
+ if (this.messageId) {
170
+ try {
171
+ await this.connector.updateCard(this.messageId, safeCard);
172
+ }
173
+ catch (error) {
174
+ getLogger().warn(`${this.logPrefix} updateCard fallback failed:`, error);
175
+ }
176
+ }
177
+ }
178
+ release() {
179
+ if (this.released)
180
+ return;
181
+ this.released = true;
182
+ this.releaseProducer();
183
+ }
184
+ }
185
+ //# sourceMappingURL=card-session.js.map
@@ -0,0 +1,28 @@
1
+ interface TruncateOptions {
2
+ suffix?: string;
3
+ normalizeWhitespace?: boolean;
4
+ }
5
+ /**
6
+ * Display name for an agent kind in the run card header.
7
+ *
8
+ * Reads from the global `AgentRegistry` (set up in `index.ts` at startup)
9
+ * when available. Falls back to a hardcoded map for callers that don't
10
+ * wire up the registry (notably unit tests of pure render helpers).
11
+ */
12
+ export declare function agentDisplayName(kind: string): string;
13
+ export declare function truncate(str: string, max: number, options?: TruncateOptions): string;
14
+ interface TerminalLabelOptions {
15
+ prefix?: string;
16
+ }
17
+ export declare function terminalToLabel(terminal: string, options?: TerminalLabelOptions): string;
18
+ export declare function terminalToColor(terminal: string): string;
19
+ /**
20
+ * Create a CardKit 2.0 stop button with behaviors callback.
21
+ */
22
+ export declare function stopButton(runId: string): object;
23
+ /**
24
+ * Create a CardKit 2.0 "new session" button with behaviors callback.
25
+ * Standardized: ✨ 新会话 + primary type
26
+ */
27
+ export declare function newSessionButton(): object;
28
+ export {};
@@ -0,0 +1,99 @@
1
+ import { AgentRegistry } from '../runner/index.js';
2
+ /**
3
+ * Display name for an agent kind in the run card header.
4
+ *
5
+ * Reads from the global `AgentRegistry` (set up in `index.ts` at startup)
6
+ * when available. Falls back to a hardcoded map for callers that don't
7
+ * wire up the registry (notably unit tests of pure render helpers).
8
+ */
9
+ export function agentDisplayName(kind) {
10
+ const globalReg = AgentRegistry.getGlobalInstance();
11
+ if (globalReg) {
12
+ return globalReg.getDisplayName(kind);
13
+ }
14
+ // Fallback: hardcoded default mapping for unit tests that exercise
15
+ // render helpers without booting the full AgentRegistry.
16
+ if (kind === 'claude')
17
+ return 'Claude';
18
+ if (kind === 'codex')
19
+ return 'Codex';
20
+ if (kind === 'opencode')
21
+ return 'Opencode';
22
+ if (kind === 'pi')
23
+ return 'Pi';
24
+ if (kind === 'kimi')
25
+ return 'Kimi';
26
+ return kind;
27
+ }
28
+ export function truncate(str, max, options) {
29
+ const suffix = options?.suffix ?? '…';
30
+ const normalize = options?.normalizeWhitespace ?? false;
31
+ let s = str;
32
+ if (normalize)
33
+ s = s.replace(/\s+/g, ' ').trim();
34
+ if (s.length <= max)
35
+ return s;
36
+ const suffixLen = suffix.length;
37
+ const budget = max - suffixLen;
38
+ if (budget <= 0)
39
+ return suffix;
40
+ let end = 0;
41
+ for (const ch of s) {
42
+ if (end + ch.length > budget)
43
+ break;
44
+ end += ch.length;
45
+ }
46
+ return s.slice(0, end) + suffix;
47
+ }
48
+ const TERMINAL_LABELS = {
49
+ done: '已完成',
50
+ error: '出错',
51
+ interrupted: '已终止',
52
+ idle_timeout: '已超时',
53
+ running: '运行中',
54
+ finalizing: '完成中',
55
+ };
56
+ export function terminalToLabel(terminal, options) {
57
+ const label = TERMINAL_LABELS[terminal] ?? '运行中';
58
+ const prefix = options?.prefix ?? '';
59
+ return prefix + label;
60
+ }
61
+ export function terminalToColor(terminal) {
62
+ if (terminal === 'done')
63
+ return 'green';
64
+ if (terminal === 'error')
65
+ return 'red';
66
+ if (terminal === 'interrupted')
67
+ return 'grey';
68
+ if (terminal === 'idle_timeout')
69
+ return 'orange';
70
+ if (terminal === 'finalizing')
71
+ return 'orange';
72
+ if (terminal === 'running')
73
+ return 'blue';
74
+ return 'red';
75
+ }
76
+ /**
77
+ * Create a CardKit 2.0 stop button with behaviors callback.
78
+ */
79
+ export function stopButton(runId) {
80
+ return {
81
+ tag: 'button',
82
+ text: { content: '⏹ 停止' },
83
+ type: 'danger',
84
+ behaviors: [{ type: 'callback', value: { cmd: 'stop', runId } }],
85
+ };
86
+ }
87
+ /**
88
+ * Create a CardKit 2.0 "new session" button with behaviors callback.
89
+ * Standardized: ✨ 新会话 + primary type
90
+ */
91
+ export function newSessionButton() {
92
+ return {
93
+ tag: 'button',
94
+ text: { content: '✨ 新会话' },
95
+ type: 'primary',
96
+ behaviors: [{ type: 'callback', value: { cmd: 'new-session' } }],
97
+ };
98
+ }
99
+ //# sourceMappingURL=card-shared.js.map