lark-remote 0.5.20 → 0.5.21
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.
- package/CHANGELOG.md +10 -0
- package/dist/bridge/inbound-media.js +71 -45
- package/dist/bridge/index.d.ts +9 -0
- package/dist/bridge/index.js +230 -203
- package/dist/card/approval-render.js +32 -33
- package/dist/card/card-budget.js +123 -98
- package/dist/card/run-renderer.js +82 -68
- package/dist/card/run-state.d.ts +0 -14
- package/dist/card/run-state.js +335 -207
- package/dist/card/tool-render.js +20 -16
- package/dist/card/usage-format.d.ts +12 -9
- package/dist/card/usage-format.js +111 -97
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/index.js +226 -190
- package/dist/message-handlers.js +253 -236
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +189 -133
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/index.js +192 -201
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +287 -221
- package/dist/runner/common/acp/base-translator.js +19 -17
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/spawning-runner.d.ts +5 -0
- package/dist/runner/common/spawning-runner.js +8 -3
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/client.d.ts +7 -9
- package/dist/runner/dsh/client.js +356 -287
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +4 -3
- package/dist/runner/dsh/runner.js +88 -64
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/kimi/acp/runner.js +81 -44
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/session/claude/sessions.js +113 -101
- package/dist/session/codex/rollout-reader.js +179 -180
- package/dist/session/dsh/sessions.js +281 -231
- package/dist/session/kimi/sessions.d.ts +0 -7
- package/dist/session/kimi/sessions.js +129 -109
- package/dist/session/opencode/sessions.d.ts +0 -11
- package/dist/session/opencode/sessions.js +161 -137
- package/dist/session/session-store.d.ts +8 -0
- package/dist/session/session-store.js +98 -98
- package/package.json +1 -1
|
@@ -66,6 +66,13 @@ function pushIf(arr, el) {
|
|
|
66
66
|
if (el !== null)
|
|
67
67
|
arr.push(el);
|
|
68
68
|
}
|
|
69
|
+
const APPROVAL_AREA_RENDERERS = new Map([
|
|
70
|
+
['command', renderCommandApproval],
|
|
71
|
+
['tool', renderToolApproval],
|
|
72
|
+
['file', renderFileApproval],
|
|
73
|
+
['permissions', renderPermissionsApproval],
|
|
74
|
+
['question', renderQuestionApproval],
|
|
75
|
+
]);
|
|
69
76
|
/**
|
|
70
77
|
* Render the approval area for a run card.
|
|
71
78
|
* Returns an array of CardKit 2.0 elements to append to the card body.
|
|
@@ -83,21 +90,10 @@ export function renderApprovalArea(approval, opts) {
|
|
|
83
90
|
text: { tag: 'lark_md', content: `ℹ️ ${neutralized}` },
|
|
84
91
|
});
|
|
85
92
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
elements.push(...renderToolApproval(approval, opts?.expired, opts?.runId));
|
|
91
|
-
}
|
|
92
|
-
else if (approval.kind === 'file') {
|
|
93
|
-
elements.push(...renderFileApproval(approval, opts?.expired, opts?.runId));
|
|
94
|
-
}
|
|
95
|
-
else if (approval.kind === 'permissions') {
|
|
96
|
-
elements.push(...renderPermissionsApproval(approval, opts?.expired, opts?.runId));
|
|
97
|
-
}
|
|
98
|
-
else if (approval.kind === 'question') {
|
|
99
|
-
elements.push(...renderQuestionApproval(approval, opts?.expired, opts?.runId));
|
|
100
|
-
}
|
|
93
|
+
// kind → 渲染器查表(Map 保持插入顺序,未知 kind 与原先无 else 分支一致:不追加)。
|
|
94
|
+
const render = APPROVAL_AREA_RENDERERS.get(approval.kind);
|
|
95
|
+
if (render)
|
|
96
|
+
elements.push(...render(approval, opts?.expired, opts?.runId));
|
|
101
97
|
// 响应发送失败:明确告知原因 + 可重试,而不是静默停在可点状态。
|
|
102
98
|
if (approval.deliveryError) {
|
|
103
99
|
const neutralized = capApprovalText(approval.deliveryError);
|
|
@@ -429,9 +425,26 @@ function renderFileDiff(diff) {
|
|
|
429
425
|
}
|
|
430
426
|
/** Per-diff render cap (chars), see TOTAL_DIFF_CHARS for the approval-area cap. */
|
|
431
427
|
const MAX_DIFF_CHARS = 1200;
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
428
|
+
/** 单个权限条目:label div + toggle 按钮(模块级命名函数,供循环逐个调用)。 */
|
|
429
|
+
function renderPermissionItem(item, requestId, runId) {
|
|
430
|
+
const icon = item.selected ? '✅' : '⬜';
|
|
431
|
+
// label 仅展示(payload 用 item.id),可安全截断。
|
|
432
|
+
const label = capApprovalText(item.label, APPROVAL_LABEL_BYTES);
|
|
433
|
+
return [
|
|
434
|
+
{
|
|
435
|
+
tag: 'div',
|
|
436
|
+
text: { tag: 'lark_md', content: `${icon} ${label}` },
|
|
437
|
+
},
|
|
438
|
+
cardButton(item.selected ? '取消授予' : '授予', {
|
|
439
|
+
cmd: 'approval.toggle',
|
|
440
|
+
requestId,
|
|
441
|
+
permId: item.id,
|
|
442
|
+
selected: !item.selected,
|
|
443
|
+
runId,
|
|
444
|
+
nonce: mkNonce(),
|
|
445
|
+
}, { type: item.selected ? 'default' : 'primary', size: 'small' }),
|
|
446
|
+
];
|
|
447
|
+
}
|
|
435
448
|
function renderPermissionsApproval(approval, expired, runId) {
|
|
436
449
|
const elements = [];
|
|
437
450
|
elements.push({
|
|
@@ -445,21 +458,7 @@ function renderPermissionsApproval(approval, expired, runId) {
|
|
|
445
458
|
const items = approval.permissions.items.slice(0, MAX_RENDERED_CHANGE_ITEMS);
|
|
446
459
|
const omittedItems = approval.permissions.items.length - items.length;
|
|
447
460
|
for (const item of items) {
|
|
448
|
-
|
|
449
|
-
// label 仅展示(payload 用 item.id),可安全截断。
|
|
450
|
-
const label = capApprovalText(item.label, APPROVAL_LABEL_BYTES);
|
|
451
|
-
elements.push({
|
|
452
|
-
tag: 'div',
|
|
453
|
-
text: { tag: 'lark_md', content: `${icon} ${label}` },
|
|
454
|
-
});
|
|
455
|
-
elements.push(cardButton(item.selected ? '取消授予' : '授予', {
|
|
456
|
-
cmd: 'approval.toggle',
|
|
457
|
-
requestId: approval.requestId,
|
|
458
|
-
permId: item.id,
|
|
459
|
-
selected: !item.selected,
|
|
460
|
-
runId,
|
|
461
|
-
nonce: mkNonce(),
|
|
462
|
-
}, { type: item.selected ? 'default' : 'primary', size: 'small' }));
|
|
461
|
+
elements.push(...renderPermissionItem(item, approval.requestId, runId));
|
|
463
462
|
}
|
|
464
463
|
if (omittedItems > 0) {
|
|
465
464
|
elements.push({
|
package/dist/card/card-budget.js
CHANGED
|
@@ -160,125 +160,130 @@ function enforceElementBudget(card, opts = {}, bytesBefore) {
|
|
|
160
160
|
// 它最终会落到最小卡(节点数固定很小)。
|
|
161
161
|
return enforceByteBudget(card, opts, bytesBefore);
|
|
162
162
|
}
|
|
163
|
-
|
|
164
|
-
* Byte-budget enforcement, extracted so table-limit fix can delegate to it
|
|
165
|
-
* when table truncation alone doesn't bring the card under the byte limit.
|
|
166
|
-
*
|
|
167
|
-
* 裁剪判定是**结构化**的:所有 collapsible_panel 都是待裁剪对象,不看面板
|
|
168
|
-
* 标题文本。历史教训:旧实现用
|
|
169
|
-
* emoji 标题模式(🤖👤💭🔧🟢🔴)识别「会话事件面板」,但真实
|
|
170
|
-
* sessionEventPanel 产出的标题是裸英文 type 名(emoji 在事件正文而非标题),
|
|
171
|
-
* 导致裁剪阶段对真实 resume 卡零命中,任何 >28KB 的卡片无裁剪直通极端降级,
|
|
172
|
-
* 整卡被替换成一句话。
|
|
173
|
-
*
|
|
174
|
-
* 渐进式降级(每级仍超限才进入下一级):
|
|
175
|
-
* 阶段1 逐面板截断内容(maxPanelContentBytes)
|
|
176
|
-
* 阶段2 限制面板数量(删除最老,maxEventPanels)
|
|
177
|
-
* 阶段3 丢弃全部面板,保留骨架(header/usage/按钮)+ 省略提示
|
|
178
|
-
* 阶段4 截断剩余顶层 div 文本
|
|
179
|
-
* 阶段5 最终兜底 minimal card(骨架本身已超限的病理场景)
|
|
180
|
-
*/
|
|
181
|
-
function enforceByteBudget(truncated, opts = {}, bytesBefore) {
|
|
163
|
+
function makeByteBudgetContext(truncated, opts, bytesBefore) {
|
|
182
164
|
const { maxEventPanels = 5, maxPanelContentBytes = 2000, truncationHint = '📜 还有 {count} 个事件未显示', } = opts;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
165
|
+
return {
|
|
166
|
+
truncated,
|
|
167
|
+
elements: truncated.body?.elements ?? [],
|
|
168
|
+
reasons: [],
|
|
169
|
+
omitted: 0,
|
|
170
|
+
modifications: 0,
|
|
171
|
+
textTruncations: 0,
|
|
172
|
+
maxEventPanels,
|
|
173
|
+
maxPanelContentBytes,
|
|
174
|
+
truncationHint,
|
|
175
|
+
bytesBefore,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/** 原 size 闭包:当前卡片序列化字节数。 */
|
|
179
|
+
function byteBudgetSize(ctx) {
|
|
180
|
+
return Buffer.byteLength(JSON.stringify(ctx.truncated), 'utf8');
|
|
181
|
+
}
|
|
182
|
+
/** 原 done 闭包:构造结果对象(reason 由各阶段累积)。 */
|
|
183
|
+
function byteBudgetDone(ctx, wasTruncated) {
|
|
184
|
+
return {
|
|
185
|
+
card: ctx.truncated,
|
|
188
186
|
wasTruncated,
|
|
189
|
-
reason: reasons.join('+') || undefined,
|
|
190
|
-
bytesBefore: bytesBefore ??
|
|
191
|
-
bytesAfter:
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
187
|
+
reason: ctx.reasons.join('+') || undefined,
|
|
188
|
+
bytesBefore: ctx.bytesBefore ?? byteBudgetSize(ctx),
|
|
189
|
+
bytesAfter: byteBudgetSize(ctx),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** 原 elementsWithinBudgetWithHint 闭包(含 hint 预留位)。 */
|
|
193
|
+
function byteBudgetElementsWithin(ctx) {
|
|
194
|
+
return countCardElements(ctx.truncated) + (ctx.omitted > 0 ? 1 : 0) <= MAX_CARD_ELEMENTS;
|
|
195
|
+
}
|
|
196
|
+
/** 原 `size() <= ... && elementsWithinBudgetWithHint()` 达标判定。 */
|
|
197
|
+
function byteBudgetFits(ctx) {
|
|
198
|
+
return byteBudgetSize(ctx) <= CARD_BUDGET_BYTES && byteBudgetElementsWithin(ctx);
|
|
199
|
+
}
|
|
200
|
+
function comparePanelIndexDesc(a, b) {
|
|
201
|
+
return b - a;
|
|
202
|
+
}
|
|
203
|
+
/** 原 dropOldestPanels 闭包:按文档顺序删最旧的 count 个面板。 */
|
|
204
|
+
function dropOldestPanels(ctx, count) {
|
|
205
|
+
const indices = getPanelIndices(ctx.elements).slice(0, count).sort(comparePanelIndexDesc);
|
|
206
|
+
for (const idx of indices) {
|
|
207
|
+
ctx.elements.splice(idx, 1);
|
|
208
|
+
ctx.omitted++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** 达标则插入 hint 并返回结果;未达标返回 undefined,交给下一阶段继续降级。 */
|
|
212
|
+
function byteBudgetTryFinish(ctx, wasTruncated) {
|
|
213
|
+
if (!byteBudgetFits(ctx))
|
|
214
|
+
return undefined;
|
|
215
|
+
insertOmittedHint(ctx.elements, ctx.omitted, ctx.truncationHint);
|
|
216
|
+
return byteBudgetDone(ctx, wasTruncated);
|
|
217
|
+
}
|
|
218
|
+
// ========== 阶段1:裁剪所有面板的内容 ==========
|
|
219
|
+
function truncatePanelContents(ctx) {
|
|
220
|
+
for (const el of ctx.elements) {
|
|
196
221
|
if (el.tag !== 'collapsible_panel')
|
|
197
222
|
continue;
|
|
198
223
|
for (const inner of el.elements ?? []) {
|
|
199
224
|
if (inner.tag === 'div' && inner.text?.content) {
|
|
200
225
|
const content = inner.text.content;
|
|
201
|
-
if (Buffer.byteLength(content, 'utf8') > maxPanelContentBytes) {
|
|
202
|
-
inner.text.content = truncateUtf8(content, maxPanelContentBytes);
|
|
203
|
-
modifications++;
|
|
226
|
+
if (Buffer.byteLength(content, 'utf8') > ctx.maxPanelContentBytes) {
|
|
227
|
+
inner.text.content = truncateUtf8(content, ctx.maxPanelContentBytes);
|
|
228
|
+
ctx.modifications++;
|
|
204
229
|
}
|
|
205
230
|
}
|
|
206
231
|
}
|
|
207
232
|
}
|
|
208
|
-
if (modifications > 0) {
|
|
209
|
-
reasons.push('panel_content_truncated');
|
|
210
|
-
}
|
|
211
|
-
// ========== 阶段2:限制面板数量(删除最老) ==========
|
|
212
|
-
let omitted = 0;
|
|
213
|
-
const dropOldestPanels = (count) => {
|
|
214
|
-
const indices = getPanelIndices(elements)
|
|
215
|
-
.slice(0, count)
|
|
216
|
-
.sort((a, b) => b - a);
|
|
217
|
-
for (const idx of indices) {
|
|
218
|
-
elements.splice(idx, 1);
|
|
219
|
-
omitted++;
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
const panelCount = getPanelIndices(elements).length;
|
|
223
|
-
if (panelCount > maxEventPanels) {
|
|
224
|
-
dropOldestPanels(panelCount - maxEventPanels);
|
|
225
|
-
reasons.push('event_count_limited');
|
|
226
|
-
}
|
|
227
|
-
// 元素数预算必须与字节预算同查(含 hint 预留位):字节路径的返回条件此前
|
|
228
|
-
// 只看字节——元素超限但卡内无 collapsible_panel 时(如 95 个小 div),
|
|
229
|
-
// 阶段1-4 全是空操作,超限卡原样直通(实测 wasTruncated:false、95 元素
|
|
230
|
-
// > 90 → 飞书 11310 整卡报废,正是文件头注释记载的 /ws、/order 故障形态)。
|
|
231
|
-
const elementsWithinBudgetWithHint = () => countCardElements(truncated) + (omitted > 0 ? 1 : 0) <= MAX_CARD_ELEMENTS;
|
|
232
|
-
if (size() <= CARD_BUDGET_BYTES && elementsWithinBudgetWithHint()) {
|
|
233
|
-
insertOmittedHint(elements, omitted, truncationHint);
|
|
234
|
-
return done(modifications > 0 || omitted > 0);
|
|
233
|
+
if (ctx.modifications > 0) {
|
|
234
|
+
ctx.reasons.push('panel_content_truncated');
|
|
235
235
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
236
|
+
}
|
|
237
|
+
// ========== 阶段2:限制面板数量(删除最老) ==========
|
|
238
|
+
function limitPanelCount(ctx) {
|
|
239
|
+
const panelCount = getPanelIndices(ctx.elements).length;
|
|
240
|
+
if (panelCount > ctx.maxEventPanels) {
|
|
241
|
+
dropOldestPanels(ctx, panelCount - ctx.maxEventPanels);
|
|
242
|
+
ctx.reasons.push('event_count_limited');
|
|
242
243
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
}
|
|
245
|
+
// ========== 阶段3:丢弃全部面板,保留卡片骨架 ==========
|
|
246
|
+
// header(cwd/sessionId/标题)、usage、操作按钮等信息量高、体积小的元素
|
|
247
|
+
// 必须保留——整卡替换为一句话是信息损失最大的降级方式。
|
|
248
|
+
function dropAllPanels(ctx) {
|
|
249
|
+
dropOldestPanels(ctx, getPanelIndices(ctx.elements).length);
|
|
250
|
+
if (ctx.omitted > 0) {
|
|
251
|
+
ctx.reasons.push('panels_dropped');
|
|
246
252
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
}
|
|
254
|
+
// ========== 阶段4:截断剩余顶层 div 文本 ==========
|
|
255
|
+
// 骨架元素(header/usage 等)自身超限的情况(如超大 displayTitle)。
|
|
256
|
+
function truncateTopLevelDivs(ctx) {
|
|
257
|
+
for (const el of ctx.elements) {
|
|
251
258
|
if (el.tag === 'div' && el.text?.content) {
|
|
252
259
|
const content = el.text.content;
|
|
253
|
-
if (Buffer.byteLength(content, 'utf8') > maxPanelContentBytes) {
|
|
254
|
-
el.text.content = truncateUtf8(content, maxPanelContentBytes);
|
|
255
|
-
textTruncations++;
|
|
260
|
+
if (Buffer.byteLength(content, 'utf8') > ctx.maxPanelContentBytes) {
|
|
261
|
+
el.text.content = truncateUtf8(content, ctx.maxPanelContentBytes);
|
|
262
|
+
ctx.textTruncations++;
|
|
256
263
|
}
|
|
257
264
|
}
|
|
258
265
|
}
|
|
259
|
-
if (textTruncations > 0) {
|
|
260
|
-
reasons.push('text_truncated');
|
|
266
|
+
if (ctx.textTruncations > 0) {
|
|
267
|
+
ctx.reasons.push('text_truncated');
|
|
261
268
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
elements.shift();
|
|
273
|
-
omitted++;
|
|
274
|
-
}
|
|
275
|
-
reasons.push('element_budget_elements_dropped');
|
|
276
|
-
if (size() <= CARD_BUDGET_BYTES && elementsWithinBudgetWithHint()) {
|
|
277
|
-
insertOmittedHint(elements, omitted, truncationHint);
|
|
278
|
-
return done(true);
|
|
269
|
+
}
|
|
270
|
+
// ========== 阶段4.5:元素数兜底(无面板可删的元素超限) ==========
|
|
271
|
+
// 阶段1-4 只会裁内容/删面板;元素超限且无面板时在此按序丢弃最旧的顶层
|
|
272
|
+
// 元素直到达标(与 table/panel 的「删最旧、留最新」口径一致,尾部操作
|
|
273
|
+
// 按钮保留)。留 ≥3 个元素兜底,仍不达标则交给阶段5 的最小卡。
|
|
274
|
+
function dropExcessElements(ctx) {
|
|
275
|
+
if (countCardElements(ctx.truncated) > MAX_CARD_ELEMENTS) {
|
|
276
|
+
while (countCardElements(ctx.truncated) > MAX_CARD_ELEMENTS && ctx.elements.length > 3) {
|
|
277
|
+
ctx.elements.shift();
|
|
278
|
+
ctx.omitted++;
|
|
279
279
|
}
|
|
280
|
+
ctx.reasons.push('element_budget_elements_dropped');
|
|
281
|
+
return byteBudgetTryFinish(ctx, true);
|
|
280
282
|
}
|
|
281
|
-
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
// ========== 阶段5:最终兜底(原 extreme_fallback) ==========
|
|
286
|
+
function byteBudgetMinimalCard(ctx) {
|
|
282
287
|
const minimalCard = cardWithHeader({
|
|
283
288
|
template: 'orange',
|
|
284
289
|
title: '⚠️ 内容已截断',
|
|
@@ -296,10 +301,30 @@ function enforceByteBudget(truncated, opts = {}, bytesBefore) {
|
|
|
296
301
|
card: minimalCard,
|
|
297
302
|
wasTruncated: true,
|
|
298
303
|
reason: 'extreme_fallback',
|
|
299
|
-
bytesBefore: bytesBefore ??
|
|
304
|
+
bytesBefore: ctx.bytesBefore ?? byteBudgetSize(ctx),
|
|
300
305
|
bytesAfter: Buffer.byteLength(JSON.stringify(minimalCard), 'utf8'),
|
|
301
306
|
};
|
|
302
307
|
}
|
|
308
|
+
function enforceByteBudget(truncated, opts = {}, bytesBefore) {
|
|
309
|
+
const ctx = makeByteBudgetContext(truncated, opts, bytesBefore);
|
|
310
|
+
truncatePanelContents(ctx);
|
|
311
|
+
limitPanelCount(ctx);
|
|
312
|
+
let result = byteBudgetTryFinish(ctx, ctx.modifications > 0 || ctx.omitted > 0);
|
|
313
|
+
if (result)
|
|
314
|
+
return result;
|
|
315
|
+
dropAllPanels(ctx);
|
|
316
|
+
result = byteBudgetTryFinish(ctx, true);
|
|
317
|
+
if (result)
|
|
318
|
+
return result;
|
|
319
|
+
truncateTopLevelDivs(ctx);
|
|
320
|
+
result = byteBudgetTryFinish(ctx, true);
|
|
321
|
+
if (result)
|
|
322
|
+
return result;
|
|
323
|
+
result = dropExcessElements(ctx);
|
|
324
|
+
if (result)
|
|
325
|
+
return result;
|
|
326
|
+
return byteBudgetMinimalCard(ctx);
|
|
327
|
+
}
|
|
303
328
|
/**
|
|
304
329
|
* 在卡片头部插入「N 个事件未显示」提示(仅在有面板被移除时)。
|
|
305
330
|
*/
|
|
@@ -1048,6 +1048,86 @@ function buildChronologicalContent(state, prepared, groups) {
|
|
|
1048
1048
|
}
|
|
1049
1049
|
return elements;
|
|
1050
1050
|
}
|
|
1051
|
+
/** running 时不显示统计(仍在生成);原分支直接 return,不追加元素。 */
|
|
1052
|
+
function summaryRunning(_state, _elements) { }
|
|
1053
|
+
function summaryFinalizing(_state, elements) {
|
|
1054
|
+
elements.push(markdownDiv('⏳ **等待进程退出…**'));
|
|
1055
|
+
}
|
|
1056
|
+
function summaryError(state, elements) {
|
|
1057
|
+
// 服务方内容审核截断(多为误报)不是「程序出错」:照抄英文内部报文会让
|
|
1058
|
+
// 用户以为代码或配置坏了。文案说清三件事——谁拦的、改动还在、下一步。
|
|
1059
|
+
if (isContentFilterFailure(state.errorMsg)) {
|
|
1060
|
+
elements.push(markdownDiv('⚠️ **服务方内容审核拦截**\n\n' +
|
|
1061
|
+
'模型输出被上游安全策略截断(`content_filter`),多数是误报,不是代码或配置问题。\n' +
|
|
1062
|
+
'本回合已中止;已写入磁盘的改动都还在。重试仍被拦截时,可换模型或稍后再试。'));
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function summaryInterrupted(state, elements) {
|
|
1069
|
+
// 中断来源三元:审批超时 / 用户取消审批 / 用户主动终止。
|
|
1070
|
+
// 显式原因优先:approvalExpired 是粘性标记(cancel 语义下 agent 可能继续跑),
|
|
1071
|
+
// 用户随后主动 /stop 时若仍按过期渲染,就把「我自己停的」误报成自动取消。
|
|
1072
|
+
const expiredApproval = state.interruptedReason === 'approval_timeout' ||
|
|
1073
|
+
(state.interruptedReason === undefined &&
|
|
1074
|
+
(state.approvalExpired === true || (state.approvals ?? []).some((a) => a.expired)));
|
|
1075
|
+
elements.push(markdownDiv(expiredApproval
|
|
1076
|
+
? '⏰ **审批超时未响应,已自动取消**'
|
|
1077
|
+
: state.interruptedReason === 'approval_cancelled'
|
|
1078
|
+
? '⏹ **已取消审批,任务终止**'
|
|
1079
|
+
: '⏹ **已被用户终止**'));
|
|
1080
|
+
}
|
|
1081
|
+
function summaryIdleTimeout(state, elements) {
|
|
1082
|
+
elements.push(markdownDiv(`⏱ **已超时**\n\n${state.idleTimeoutMinutes ?? 0} 分钟无响应,已自动终止`));
|
|
1083
|
+
}
|
|
1084
|
+
/** 成功/目标继续/其它终态:usage 统计 + 后台任务(原 else 兜底分支)。 */
|
|
1085
|
+
function summaryResult(state, elements) {
|
|
1086
|
+
const result = state.resultSubtype ?? 'success';
|
|
1087
|
+
const hasContent = state.blocks.length > 0 || !!state.finalAnswer?.content.trim();
|
|
1088
|
+
// 压缩(operationKind='compaction')不会返回 agent 正文——引擎只压缩上下文
|
|
1089
|
+
// 窗口,空内容属正常,不能沿用普通 run 的「(未返回内容)」异常信号。
|
|
1090
|
+
// 所有会压缩的 agent(codex/claude/kimi/opencode/pi)
|
|
1091
|
+
// 都经 streamCompact 走这条共享渲染路径,一处覆盖全部。
|
|
1092
|
+
const empty = hasContent
|
|
1093
|
+
? ''
|
|
1094
|
+
: state.operationKind === 'compaction'
|
|
1095
|
+
? '\n\n🗜 Compact 完成'
|
|
1096
|
+
: '\n\n(未返回内容)';
|
|
1097
|
+
// goal 本 run 读不到(known=false)时不能写「已完成」:这是 GoalStateEvent
|
|
1098
|
+
// 契约里明令禁止的推断(读不到 ≠ 没有目标)。run 本身成功,只是目标维度未知。
|
|
1099
|
+
const goalUnknown = state.terminal === 'done' && state.goalKnown === false;
|
|
1100
|
+
// RunState 的用量字段就是 formatUsageStats 的入参形态(同为
|
|
1101
|
+
// `UsageSnapshot`)——原先这里逐字段抄了 17 行,加一个字段要同时改
|
|
1102
|
+
// RunState、FinishMeta 和这张清单(§9.21)。
|
|
1103
|
+
const usageStatsStr = formatUsageStats(state, {
|
|
1104
|
+
showResult: true,
|
|
1105
|
+
result,
|
|
1106
|
+
// 本轮结束但目标继续:不能报"已完成",也不能说"出错"。
|
|
1107
|
+
...(state.terminal === 'goal_continuing'
|
|
1108
|
+
? { completedLabel: '⏳ 本轮结束 · 目标仍在继续' }
|
|
1109
|
+
: goalUnknown
|
|
1110
|
+
? { completedLabel: '✅ 本轮完成 · 目标状态未确认' }
|
|
1111
|
+
: {}),
|
|
1112
|
+
});
|
|
1113
|
+
elements.push(markdownDiv(usageStatsStr + empty));
|
|
1114
|
+
// 后台任务:跨 turn 生命周期,必须显式告知并可逐个停止。
|
|
1115
|
+
elements.push(...buildBackgroundJobsSection(state));
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* terminal → summary handler。用 Map 而非对象字面量(constructor/toString 等
|
|
1119
|
+
* 键会命中 Object.prototype);未登记的 terminal 走 summaryResult,与原 else
|
|
1120
|
+
* 兜底分支一致。
|
|
1121
|
+
*/
|
|
1122
|
+
const SUMMARY_HANDLERS = new Map([
|
|
1123
|
+
['running', summaryRunning],
|
|
1124
|
+
['finalizing', summaryFinalizing],
|
|
1125
|
+
['error', summaryError],
|
|
1126
|
+
['interrupted', summaryInterrupted],
|
|
1127
|
+
['idle_timeout', summaryIdleTimeout],
|
|
1128
|
+
['done', summaryResult],
|
|
1129
|
+
['goal_continuing', summaryResult],
|
|
1130
|
+
]);
|
|
1051
1131
|
/** Build summary tab content */
|
|
1052
1132
|
function buildSummaryContent(state, options = {}) {
|
|
1053
1133
|
const elements = [];
|
|
@@ -1059,74 +1139,8 @@ function buildSummaryContent(state, options = {}) {
|
|
|
1059
1139
|
elements.push(renderNoticeRow(notice));
|
|
1060
1140
|
}
|
|
1061
1141
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
return elements;
|
|
1065
|
-
}
|
|
1066
|
-
if (state.terminal === 'finalizing') {
|
|
1067
|
-
elements.push(markdownDiv('⏳ **等待进程退出…**'));
|
|
1068
|
-
return elements;
|
|
1069
|
-
}
|
|
1070
|
-
if (state.terminal === 'error') {
|
|
1071
|
-
// 服务方内容审核截断(多为误报)不是「程序出错」:照抄英文内部报文会让
|
|
1072
|
-
// 用户以为代码或配置坏了。文案说清三件事——谁拦的、改动还在、下一步。
|
|
1073
|
-
if (isContentFilterFailure(state.errorMsg)) {
|
|
1074
|
-
elements.push(markdownDiv('⚠️ **服务方内容审核拦截**\n\n' +
|
|
1075
|
-
'模型输出被上游安全策略截断(`content_filter`),多数是误报,不是代码或配置问题。\n' +
|
|
1076
|
-
'本回合已中止;已写入磁盘的改动都还在。重试仍被拦截时,可换模型或稍后再试。'));
|
|
1077
|
-
}
|
|
1078
|
-
else {
|
|
1079
|
-
elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
else if (state.terminal === 'interrupted') {
|
|
1083
|
-
// 中断来源三元:审批超时 / 用户取消审批 / 用户主动终止。
|
|
1084
|
-
// 显式原因优先:approvalExpired 是粘性标记(cancel 语义下 agent 可能继续跑),
|
|
1085
|
-
// 用户随后主动 /stop 时若仍按过期渲染,就把「我自己停的」误报成自动取消。
|
|
1086
|
-
const expiredApproval = state.interruptedReason === 'approval_timeout' ||
|
|
1087
|
-
(state.interruptedReason === undefined &&
|
|
1088
|
-
(state.approvalExpired === true || (state.approvals ?? []).some((a) => a.expired)));
|
|
1089
|
-
elements.push(markdownDiv(expiredApproval
|
|
1090
|
-
? '⏰ **审批超时未响应,已自动取消**'
|
|
1091
|
-
: state.interruptedReason === 'approval_cancelled'
|
|
1092
|
-
? '⏹ **已取消审批,任务终止**'
|
|
1093
|
-
: '⏹ **已被用户终止**'));
|
|
1094
|
-
}
|
|
1095
|
-
else if (state.terminal === 'idle_timeout') {
|
|
1096
|
-
elements.push(markdownDiv(`⏱ **已超时**\n\n${state.idleTimeoutMinutes ?? 0} 分钟无响应,已自动终止`));
|
|
1097
|
-
}
|
|
1098
|
-
else {
|
|
1099
|
-
const result = state.resultSubtype ?? 'success';
|
|
1100
|
-
const hasContent = state.blocks.length > 0 || !!state.finalAnswer?.content.trim();
|
|
1101
|
-
// 压缩(operationKind='compaction')不会返回 agent 正文——引擎只压缩上下文
|
|
1102
|
-
// 窗口,空内容属正常,不能沿用普通 run 的「(未返回内容)」异常信号。
|
|
1103
|
-
// 所有会压缩的 agent(codex/claude/kimi/opencode/pi)
|
|
1104
|
-
// 都经 streamCompact 走这条共享渲染路径,一处覆盖全部。
|
|
1105
|
-
const empty = hasContent
|
|
1106
|
-
? ''
|
|
1107
|
-
: state.operationKind === 'compaction'
|
|
1108
|
-
? '\n\n🗜 Compact 完成'
|
|
1109
|
-
: '\n\n(未返回内容)';
|
|
1110
|
-
// goal 本 run 读不到(known=false)时不能写「已完成」:这是 GoalStateEvent
|
|
1111
|
-
// 契约里明令禁止的推断(读不到 ≠ 没有目标)。run 本身成功,只是目标维度未知。
|
|
1112
|
-
const goalUnknown = state.terminal === 'done' && state.goalKnown === false;
|
|
1113
|
-
// RunState 的用量字段就是 formatUsageStats 的入参形态(同为
|
|
1114
|
-
// `UsageSnapshot`)——原先这里逐字段抄了 17 行,加一个字段要同时改
|
|
1115
|
-
// RunState、FinishMeta 和这张清单(§9.21)。
|
|
1116
|
-
const usageStatsStr = formatUsageStats(state, {
|
|
1117
|
-
showResult: true,
|
|
1118
|
-
result,
|
|
1119
|
-
// 本轮结束但目标继续:不能报"已完成",也不能说"出错"。
|
|
1120
|
-
...(state.terminal === 'goal_continuing'
|
|
1121
|
-
? { completedLabel: '⏳ 本轮结束 · 目标仍在继续' }
|
|
1122
|
-
: goalUnknown
|
|
1123
|
-
? { completedLabel: '✅ 本轮完成 · 目标状态未确认' }
|
|
1124
|
-
: {}),
|
|
1125
|
-
});
|
|
1126
|
-
elements.push(markdownDiv(usageStatsStr + empty));
|
|
1127
|
-
// 后台任务:跨 turn 生命周期,必须显式告知并可逐个停止。
|
|
1128
|
-
elements.push(...buildBackgroundJobsSection(state));
|
|
1129
|
-
}
|
|
1142
|
+
const handler = SUMMARY_HANDLERS.get(state.terminal) ?? summaryResult;
|
|
1143
|
+
handler(state, elements);
|
|
1130
1144
|
return elements;
|
|
1131
1145
|
}
|
|
1132
1146
|
/** 终态卡最多列几个后台任务(多出来的只报数)。 */
|
package/dist/card/run-state.d.ts
CHANGED
|
@@ -213,20 +213,6 @@ export declare function removeBackgroundJob(state: RunState, jobId: string): Run
|
|
|
213
213
|
* 按钮,等于逼用户拿「停止」当查询用。
|
|
214
214
|
*/
|
|
215
215
|
export declare function isLiveBackgroundJob(job: BackgroundJobRoster): boolean;
|
|
216
|
-
/**
|
|
217
|
-
* 收尾后把宿主推来的整份任务名单同步进终态卡状态。
|
|
218
|
-
*
|
|
219
|
-
* 与 reducer 的分工:reducer 处理的是"收尾那一刻的名单",只在 run 还在消费
|
|
220
|
-
* 事件时有效;终态之后 `reduceRunState` 直接早退(终态幂等),名单只能由
|
|
221
|
-
* 这里更新。
|
|
222
|
-
*
|
|
223
|
-
* 三条规则:
|
|
224
|
-
* - 名单里消失 = 宿主已结算(`job/list` 是整份替换,摘掉即结束)→ `ended`;
|
|
225
|
-
* - 名单里还在、宿主却给了终态词 → `ended`(有些宿主会把已结束的行留在名单里);
|
|
226
|
-
* - `ended` 是终态,不回退成 running —— 同一个 job id 不会重新开始跑。
|
|
227
|
-
*
|
|
228
|
-
* 无变化时返回**同一个 state 引用**:bridge 靠它跳过无谓的卡片 patch。
|
|
229
|
-
*/
|
|
230
216
|
export declare function syncBackgroundJobs(state: RunState, roster: ReadonlyArray<BackgroundJobRoster>): RunState;
|
|
231
217
|
/**
|
|
232
218
|
* 订阅断了但任务仍未结算:把在跑的任务标成「状态未知」。
|