lark-remote 0.5.10 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -2
- package/README.en.md +1 -14
- package/README.md +1 -14
- package/dist/binder.js +7 -1
- package/dist/bridge/approval-coordinator.d.ts +1 -0
- package/dist/bridge/approval-coordinator.js +12 -24
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +89 -230
- package/dist/bridge/run-approval.d.ts +15 -0
- package/dist/bridge/run-approval.js +41 -0
- package/dist/bridge/run-usage.d.ts +46 -0
- package/dist/bridge/run-usage.js +96 -0
- package/dist/bridge/session-writeback.d.ts +18 -0
- package/dist/bridge/session-writeback.js +36 -0
- package/dist/card/approval-render.d.ts +14 -0
- package/dist/card/approval-render.js +155 -29
- package/dist/card/bash-card-session.js +10 -5
- package/dist/card/bash-renderer.js +11 -23
- package/dist/card/card-budget.js +24 -4
- package/dist/card/card-session.d.ts +2 -0
- package/dist/card/card-session.js +20 -22
- package/dist/card/card-shared.d.ts +4 -4
- package/dist/card/card-shared.js +3 -9
- package/dist/card/collapsible.js +3 -2
- package/dist/card/final-answer-overflow.d.ts +7 -0
- package/dist/card/final-answer-overflow.js +46 -0
- package/dist/card/run-card-session.js +2 -7
- package/dist/card/run-renderer.d.ts +14 -0
- package/dist/card/run-renderer.js +150 -10
- package/dist/card/run-state.d.ts +12 -1
- package/dist/card/run-state.js +73 -34
- package/dist/common/text-width.js +5 -0
- package/dist/common/tool-summary.js +4 -2
- package/dist/config/codex-config.js +5 -3
- package/dist/config/index.d.ts +4 -2
- package/dist/config/index.js +19 -5
- package/dist/inbound/turn-assembler.js +6 -0
- package/dist/index.js +15 -391
- package/dist/instance-lock.d.ts +9 -1
- package/dist/instance-lock.js +42 -7
- package/dist/message-handlers.d.ts +26 -0
- package/dist/message-handlers.js +275 -0
- package/dist/platform/identity.js +4 -3
- package/dist/platform/path.js +3 -1
- package/dist/platform/terminator-win32.d.ts +13 -1
- package/dist/platform/terminator-win32.js +4 -14
- package/dist/router/card-action-payload.d.ts +1 -1
- package/dist/router/card-action-payload.js +1 -1
- package/dist/router/commands/ls.d.ts +9 -0
- package/dist/router/commands/ls.js +243 -0
- package/dist/router/commands/order.d.ts +12 -0
- package/dist/router/commands/order.js +178 -0
- package/dist/router/commands/resume.d.ts +47 -0
- package/dist/router/commands/resume.js +246 -0
- package/dist/router/commands/ws.d.ts +15 -0
- package/dist/router/commands/ws.js +189 -0
- package/dist/router/config/claude.js +3 -3
- package/dist/router/config/codex.js +2 -6
- package/dist/router/config/common/turn-idle.d.ts +2 -0
- package/dist/router/config/common/turn-idle.js +9 -0
- package/dist/router/config/kimi.js +2 -6
- package/dist/router/config/opencode.js +2 -6
- package/dist/router/index.d.ts +14 -22
- package/dist/router/index.js +93 -1133
- package/dist/router/paging.d.ts +13 -0
- package/dist/router/paging.js +20 -0
- package/dist/router/path-validation.d.ts +27 -0
- package/dist/router/path-validation.js +45 -0
- package/dist/runner/claude/runner.d.ts +3 -4
- package/dist/runner/claude/runner.js +4 -5
- package/dist/runner/claude/session.js +1 -1
- package/dist/runner/codex/app-server/runner.js +12 -0
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +65 -9
- package/dist/runner/common/acp/base-translator.d.ts +15 -8
- package/dist/runner/common/acp/base-translator.js +38 -7
- package/dist/runner/common/jsonl-stream.js +12 -1
- package/dist/runner/common/jsonrpc/connection-manager.js +11 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
- package/dist/runner/common/jsonrpc/transport.js +7 -1
- package/dist/runner/common/spawning-runner.js +10 -4
- package/dist/runner/common/turn-started.d.ts +2 -0
- package/dist/runner/common/turn-started.js +10 -0
- package/dist/runner/dsh/approval.js +16 -3
- package/dist/runner/dsh/auth.js +2 -3
- package/dist/runner/dsh/client.d.ts +32 -7
- package/dist/runner/dsh/client.js +115 -149
- package/dist/runner/dsh/daemon.d.ts +0 -2
- package/dist/runner/dsh/daemon.js +1 -6
- package/dist/runner/dsh/errors.d.ts +5 -0
- package/dist/runner/dsh/errors.js +11 -0
- package/dist/runner/dsh/exit-cancel.d.ts +0 -2
- package/dist/runner/dsh/exit-cancel.js +63 -24
- package/dist/runner/dsh/home.d.ts +2 -0
- package/dist/runner/dsh/home.js +8 -0
- package/dist/runner/dsh/inflight.d.ts +0 -2
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.d.ts +26 -0
- package/dist/runner/dsh/mux-stream.js +157 -0
- package/dist/runner/dsh/runner.d.ts +7 -0
- package/dist/runner/dsh/runner.js +42 -5
- package/dist/runner/dsh/translator.d.ts +0 -10
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.js +42 -5
- package/dist/runner/kimi/acp/translator.d.ts +8 -0
- package/dist/runner/kimi/acp/translator.js +11 -12
- package/dist/runner/opencode/acp/translator.js +1 -12
- package/dist/runner/pi/rpc/runner.js +33 -5
- package/dist/runner/pi/rpc/translator.d.ts +2 -8
- package/dist/runner/pi/rpc/translator.js +2 -7
- package/dist/runner/registry.d.ts +0 -19
- package/dist/runner/registry.js +0 -32
- package/dist/runner/resolve-agent-choices.js +5 -1
- package/dist/runner/sync-agent-choices.js +4 -1
- package/dist/runner/types.d.ts +17 -1
- package/dist/session/claude/session-index.js +13 -4
- package/dist/session/codex/rollout-reader.js +51 -26
- package/dist/session/common/usage-accumulator.d.ts +10 -1
- package/dist/session/common/usage-accumulator.js +10 -2
- package/dist/session/dsh/sessions.js +29 -9
- package/dist/session/kimi/sessions.js +11 -7
- package/dist/session/opencode/sessions.js +15 -7
- package/dist/session/pi/sessions.js +3 -2
- package/dist/session/session-store.js +4 -3
- package/package.json +1 -1
package/dist/card/card-budget.js
CHANGED
|
@@ -224,7 +224,12 @@ function enforceByteBudget(truncated, opts = {}, bytesBefore) {
|
|
|
224
224
|
dropOldestPanels(panelCount - maxEventPanels);
|
|
225
225
|
reasons.push('event_count_limited');
|
|
226
226
|
}
|
|
227
|
-
|
|
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()) {
|
|
228
233
|
insertOmittedHint(elements, omitted, truncationHint);
|
|
229
234
|
return done(modifications > 0 || omitted > 0);
|
|
230
235
|
}
|
|
@@ -235,13 +240,12 @@ function enforceByteBudget(truncated, opts = {}, bytesBefore) {
|
|
|
235
240
|
if (omitted > 0) {
|
|
236
241
|
reasons.push('panels_dropped');
|
|
237
242
|
}
|
|
238
|
-
if (size() <= CARD_BUDGET_BYTES) {
|
|
243
|
+
if (size() <= CARD_BUDGET_BYTES && elementsWithinBudgetWithHint()) {
|
|
239
244
|
insertOmittedHint(elements, omitted, truncationHint);
|
|
240
245
|
return done(true);
|
|
241
246
|
}
|
|
242
247
|
// ========== 阶段4:截断剩余顶层 div 文本 ==========
|
|
243
248
|
// 骨架元素(header/usage 等)自身超限的情况(如超大 displayTitle)。
|
|
244
|
-
insertOmittedHint(elements, omitted, truncationHint);
|
|
245
249
|
let textTruncations = 0;
|
|
246
250
|
for (const el of elements) {
|
|
247
251
|
if (el.tag === 'div' && el.text?.content) {
|
|
@@ -255,9 +259,25 @@ function enforceByteBudget(truncated, opts = {}, bytesBefore) {
|
|
|
255
259
|
if (textTruncations > 0) {
|
|
256
260
|
reasons.push('text_truncated');
|
|
257
261
|
}
|
|
258
|
-
if (size() <= CARD_BUDGET_BYTES) {
|
|
262
|
+
if (size() <= CARD_BUDGET_BYTES && elementsWithinBudgetWithHint()) {
|
|
263
|
+
insertOmittedHint(elements, omitted, truncationHint);
|
|
259
264
|
return done(true);
|
|
260
265
|
}
|
|
266
|
+
// ========== 阶段4.5:元素数兜底(无面板可删的元素超限) ==========
|
|
267
|
+
// 阶段1-4 只会裁内容/删面板;元素超限且无面板时在此按序丢弃最旧的顶层
|
|
268
|
+
// 元素直到达标(与 table/panel 的「删最旧、留最新」口径一致,尾部操作
|
|
269
|
+
// 按钮保留)。留 ≥3 个元素兜底,仍不达标则交给阶段5 的最小卡。
|
|
270
|
+
if (countCardElements(truncated) > MAX_CARD_ELEMENTS) {
|
|
271
|
+
while (countCardElements(truncated) > MAX_CARD_ELEMENTS && elements.length > 3) {
|
|
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);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
261
281
|
// ========== 阶段5:最终兜底(原 extreme_fallback) ==========
|
|
262
282
|
const minimalCard = cardWithHeader({
|
|
263
283
|
template: 'orange',
|
|
@@ -117,5 +117,7 @@ export declare abstract class CardSession<S, RO = unknown> {
|
|
|
117
117
|
* connector.updateCard if the controller is unavailable or fails.
|
|
118
118
|
*/
|
|
119
119
|
protected updateCard(): Promise<void>;
|
|
120
|
+
private waitForControllerReady;
|
|
121
|
+
protected prepareTerminalUpdate(): Promise<void>;
|
|
120
122
|
protected release(): void;
|
|
121
123
|
}
|
|
@@ -138,17 +138,7 @@ export class CardSession {
|
|
|
138
138
|
// 二次调用也套 timeout 保护:若 controllerReady 永不 settle(首次超时失败),
|
|
139
139
|
// 不能永久挂起。复用同样的 startTimeoutMs 超时。streamOutcome 已存在,
|
|
140
140
|
// 不重复创建 stream。
|
|
141
|
-
|
|
142
|
-
const timeout = new Promise((_, reject) => {
|
|
143
|
-
timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
|
|
144
|
-
});
|
|
145
|
-
try {
|
|
146
|
-
await Promise.race([this.controllerReady, timeout]);
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
if (timer)
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
}
|
|
141
|
+
await this.waitForControllerReady();
|
|
152
142
|
return;
|
|
153
143
|
}
|
|
154
144
|
// NOTE: the two renderCard calls below are NOT redundant. The streamCard
|
|
@@ -177,17 +167,7 @@ export class CardSession {
|
|
|
177
167
|
this.rejectController(error);
|
|
178
168
|
return { ok: false, error };
|
|
179
169
|
});
|
|
180
|
-
|
|
181
|
-
const timeout = new Promise((_, reject) => {
|
|
182
|
-
timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
|
|
183
|
-
});
|
|
184
|
-
try {
|
|
185
|
-
await Promise.race([this.controllerReady, timeout]);
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
if (timer)
|
|
189
|
-
clearTimeout(timer);
|
|
190
|
-
}
|
|
170
|
+
await this.waitForControllerReady();
|
|
191
171
|
}
|
|
192
172
|
async settle() {
|
|
193
173
|
this.release();
|
|
@@ -247,6 +227,24 @@ export class CardSession {
|
|
|
247
227
|
}
|
|
248
228
|
}
|
|
249
229
|
}
|
|
230
|
+
async waitForControllerReady() {
|
|
231
|
+
let timer;
|
|
232
|
+
const timeout = new Promise((_, reject) => {
|
|
233
|
+
timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
await Promise.race([this.controllerReady, timeout]);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
if (timer)
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async prepareTerminalUpdate() {
|
|
244
|
+
this.cancelPendingFlush();
|
|
245
|
+
if (this.flushP)
|
|
246
|
+
await this.flushP;
|
|
247
|
+
}
|
|
250
248
|
release() {
|
|
251
249
|
if (this.released)
|
|
252
250
|
return;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { type AgentKind } from '../runner/
|
|
1
|
+
import { type AgentKind } from '../runner/types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Display name for an agent kind in the run card header.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Display names are static UI labels, so the constant is the single source.
|
|
6
|
+
* Keeping a second mutable registry here made render helpers depend on startup
|
|
7
|
+
* order and forced tests to reset global state.
|
|
8
8
|
*/
|
|
9
9
|
export declare function agentDisplayName(kind: string): string;
|
|
10
10
|
export declare function terminalToLabel(terminal: string): string;
|
package/dist/card/card-shared.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import { AgentRegistry } from '../runner/index.js';
|
|
2
1
|
import { AGENT_DISPLAY_NAMES } from '../runner/types.js';
|
|
3
2
|
/**
|
|
4
3
|
* Display name for an agent kind in the run card header.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Display names are static UI labels, so the constant is the single source.
|
|
6
|
+
* Keeping a second mutable registry here made render helpers depend on startup
|
|
7
|
+
* order and forced tests to reset global state.
|
|
9
8
|
*/
|
|
10
9
|
export function agentDisplayName(kind) {
|
|
11
|
-
const globalReg = AgentRegistry.getGlobalInstance();
|
|
12
|
-
if (globalReg) {
|
|
13
|
-
return globalReg.getDisplayName(kind);
|
|
14
|
-
}
|
|
15
|
-
// Fallback for render helpers that run without the global registry.
|
|
16
10
|
return AGENT_DISPLAY_NAMES[kind] ?? kind;
|
|
17
11
|
}
|
|
18
12
|
const TERMINAL_LABELS = {
|
package/dist/card/collapsible.js
CHANGED
|
@@ -76,8 +76,9 @@ export function markdownDiv(content, textSize) {
|
|
|
76
76
|
* to be interpreted as a literal. Escape it to preserve the raw backslash.
|
|
77
77
|
*
|
|
78
78
|
* Note: Square brackets [] and pipe | are NOT escaped -- they are valid markdown
|
|
79
|
-
* (links, tables) and escaping them breaks link rendering. Verified
|
|
80
|
-
* tests/escape-bracket-
|
|
79
|
+
* (links, tables) and escaping them breaks link rendering. Verified by the live
|
|
80
|
+
* regression test tests/feishu-live/escape-bracket-regression.test.ts: unescaped
|
|
81
|
+
* []| does not trigger 11311.
|
|
81
82
|
*/
|
|
82
83
|
function escapeMarkdown(content) {
|
|
83
84
|
if (!content)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split an authoritative final answer into plain-text messages without losing
|
|
3
|
+
* any content. Feishu cards have a hard size budget; a long answer must spill
|
|
4
|
+
* rather than being silently truncated inside the progress card.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FINAL_ANSWER_CHUNK_BYTES = 20000;
|
|
7
|
+
export declare function splitFinalAnswer(text: string, maxBytes?: number): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split an authoritative final answer into plain-text messages without losing
|
|
3
|
+
* any content. Feishu cards have a hard size budget; a long answer must spill
|
|
4
|
+
* rather than being silently truncated inside the progress card.
|
|
5
|
+
*/
|
|
6
|
+
export const FINAL_ANSWER_CHUNK_BYTES = 20_000;
|
|
7
|
+
export function splitFinalAnswer(text, maxBytes = FINAL_ANSWER_CHUNK_BYTES) {
|
|
8
|
+
if (!text || maxBytes <= 0)
|
|
9
|
+
return text ? [text] : [];
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let rest = text;
|
|
12
|
+
let remainingBytes = Buffer.byteLength(rest, 'utf8');
|
|
13
|
+
while (remainingBytes > maxBytes) {
|
|
14
|
+
let bytes = 0;
|
|
15
|
+
let end = 0;
|
|
16
|
+
let lastNewline = 0;
|
|
17
|
+
for (const char of rest) {
|
|
18
|
+
const charBytes = Buffer.byteLength(char, 'utf8');
|
|
19
|
+
if (bytes + charBytes > maxBytes)
|
|
20
|
+
break;
|
|
21
|
+
bytes += charBytes;
|
|
22
|
+
end += char.length;
|
|
23
|
+
if (char === '\n')
|
|
24
|
+
lastNewline = end;
|
|
25
|
+
}
|
|
26
|
+
if (end === 0) {
|
|
27
|
+
// A caller can choose a byte limit smaller than one codepoint. Keep one
|
|
28
|
+
// codepoint per chunk rather than looping forever with a zero-length cut.
|
|
29
|
+
const first = Array.from(rest)[0];
|
|
30
|
+
chunks.push(first);
|
|
31
|
+
rest = rest.slice(first.length);
|
|
32
|
+
remainingBytes -= Buffer.byteLength(first, 'utf8');
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Prefer a paragraph/line boundary when it does not waste most of a chunk.
|
|
36
|
+
const cut = lastNewline > 0 && lastNewline >= Math.floor(end * 0.6) ? lastNewline : end;
|
|
37
|
+
const chunk = rest.slice(0, cut);
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
rest = rest.slice(cut);
|
|
40
|
+
remainingBytes -= Buffer.byteLength(chunk, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
if (rest)
|
|
43
|
+
chunks.push(rest);
|
|
44
|
+
return chunks;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=final-answer-overflow.js.map
|
|
@@ -46,13 +46,8 @@ export class RunCardSession extends CardSession {
|
|
|
46
46
|
}
|
|
47
47
|
async finish(terminal, meta = {}) {
|
|
48
48
|
// 终态必须即时显示(spec):取消合批窗口,不等延迟 flush 调度。
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// 否则两条并发 controller.update 无 FIFO 保证,in-flight 的 pre-terminal
|
|
52
|
-
// patch 可能在终态 patch 之后 resolve → 用户看到"思考中"终帧。
|
|
53
|
-
// await 保证顺序:pre-terminal 先落地 → terminal 后落地 → terminal 胜出。
|
|
54
|
-
if (this.flushP)
|
|
55
|
-
await this.flushP;
|
|
49
|
+
// 终态 patch 必须排在已开始的 pre-terminal flush 之后。
|
|
50
|
+
await this.prepareTerminalUpdate();
|
|
56
51
|
this.state = finishRun(this.state, terminal, meta);
|
|
57
52
|
await this.updateCard();
|
|
58
53
|
this.release();
|
|
@@ -20,6 +20,12 @@ export interface RunCardRenderOptions {
|
|
|
20
20
|
*/
|
|
21
21
|
sessionFallbackAvailable?: boolean;
|
|
22
22
|
}
|
|
23
|
+
export interface RunCardRenderOutcome {
|
|
24
|
+
card: object;
|
|
25
|
+
finalAnswerStatus: 'none' | 'full' | 'overflow';
|
|
26
|
+
/** Full text to deliver outside the card when finalAnswerStatus is overflow. */
|
|
27
|
+
finalAnswerText?: string;
|
|
28
|
+
}
|
|
23
29
|
type BlockGroup = {
|
|
24
30
|
kind: 'thinking';
|
|
25
31
|
content: string;
|
|
@@ -73,4 +79,12 @@ export declare function estimateCardBytes(state: RunState, options?: RunCardRend
|
|
|
73
79
|
* CardKit 2.0 renderer - simplified for streaming (no tabs)
|
|
74
80
|
*/
|
|
75
81
|
export declare function renderRunCard(state: RunState, options?: RunCardRenderOptions): object;
|
|
82
|
+
/**
|
|
83
|
+
* Render a run card with metadata the bridge needs to deliver an answer that
|
|
84
|
+
* cannot fit in one Feishu card. `renderRunCard` remains the card-only API used
|
|
85
|
+
* by streaming sessions.
|
|
86
|
+
*/
|
|
87
|
+
export declare function renderRunCardDetailed(state: RunState, options?: RunCardRenderOptions): RunCardRenderOutcome;
|
|
88
|
+
/** Exact-match marker used in the card before the answer is sent separately. */
|
|
89
|
+
export declare const FINAL_ANSWER_OVERFLOW_MARKER = "_\u6700\u7EC8\u56DE\u7B54\u8F83\u957F\uFF0C\u5B8C\u6574\u5185\u5BB9\u5DF2\u968F\u540E\u53D1\u9001_";
|
|
76
90
|
export {};
|
|
@@ -9,7 +9,7 @@ import { displayWidth, truncateToDisplayWidth } from '../common/text-width.js';
|
|
|
9
9
|
import { isContentFilterFailure } from '../common/turn-failure.js';
|
|
10
10
|
import { formatClock, formatTimestamp } from './time.js';
|
|
11
11
|
import { formatUsageStats } from '../router/usage-format.js';
|
|
12
|
-
import { renderApprovalArea } from './approval-render.js';
|
|
12
|
+
import { renderApprovalArea, renderApprovalAreaCompact } from './approval-render.js';
|
|
13
13
|
const REASONING_BYTES = 4_500;
|
|
14
14
|
const TEXT_BYTES = 10_000;
|
|
15
15
|
const DEGRADED_THINKING_KEEP = 2;
|
|
@@ -448,6 +448,24 @@ function approvalArea(state) {
|
|
|
448
448
|
}
|
|
449
449
|
return elements;
|
|
450
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* 紧凑审批区(skeleton/truncatedSkeleton 兜底专用):完整审批区不受估算器
|
|
453
|
+
* 管辖、不参与 degraded/extreme 分层裁剪,pathological 输入(超长 reason、
|
|
454
|
+
* 题数×选项爆元素数)会让所有兜底层一起超限——超限卡被飞书整卡拒收(11310),
|
|
455
|
+
* 审批彻底不可见,run 挂到审批超时/看门狗。兜底层换用紧凑形态(固定小体积、
|
|
456
|
+
* 保留决策/跳过按钮),结构性保证审批入口在任何降级档都活着。
|
|
457
|
+
*/
|
|
458
|
+
function approvalAreaCompact(state) {
|
|
459
|
+
const elements = [];
|
|
460
|
+
for (const slot of state.approvals ?? []) {
|
|
461
|
+
elements.push(...renderApprovalAreaCompact(slot.view, {
|
|
462
|
+
expired: slot.expired,
|
|
463
|
+
runId: state.runId,
|
|
464
|
+
terminal: state.terminal,
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
return elements;
|
|
468
|
+
}
|
|
451
469
|
/** 底部操作行之前的内容区提示:被 MAX_BLOCKS 截掉的早期块计数(信息保真)。 */
|
|
452
470
|
function omittedBlocksHint(state) {
|
|
453
471
|
return state.omittedBlocks !== undefined && state.omittedBlocks > 0
|
|
@@ -654,11 +672,12 @@ function elapsedLabel(state) {
|
|
|
654
672
|
return `${hours}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
655
673
|
return `${Math.floor(hours / 24)}d${String(hours % 24).padStart(2, '0')}h`;
|
|
656
674
|
}
|
|
657
|
-
/** token 数量的紧凑写法:`950` / `12K` / `1.
|
|
675
|
+
/** token 数量的紧凑写法:`950` / `12K` / `1.0M`。 */
|
|
658
676
|
function formatTokens(tokens) {
|
|
659
677
|
if (tokens < 1000)
|
|
660
678
|
return String(tokens);
|
|
661
|
-
|
|
679
|
+
// 千位边界取 M 档:999_500 起四舍五入会得到 "1000K",直接按 M 档展示。
|
|
680
|
+
if (tokens < 999_500)
|
|
662
681
|
return `${Math.round(tokens / 1000)}K`;
|
|
663
682
|
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
664
683
|
}
|
|
@@ -848,6 +867,20 @@ function measurePanelBytes(title, expanded, border, content) {
|
|
|
848
867
|
* CardKit 2.0 renderer - simplified for streaming (no tabs)
|
|
849
868
|
*/
|
|
850
869
|
export function renderRunCard(state, options = {}) {
|
|
870
|
+
return renderRunCardDetailed(state, options).card;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Render a run card with metadata the bridge needs to deliver an answer that
|
|
874
|
+
* cannot fit in one Feishu card. `renderRunCard` remains the card-only API used
|
|
875
|
+
* by streaming sessions.
|
|
876
|
+
*/
|
|
877
|
+
export function renderRunCardDetailed(state, options = {}) {
|
|
878
|
+
if (state.finalAnswer?.content.trim()) {
|
|
879
|
+
return buildFinalAnswerPriorityCard(state, options);
|
|
880
|
+
}
|
|
881
|
+
return { card: renderLegacyRunCard(state, options), finalAnswerStatus: 'none' };
|
|
882
|
+
}
|
|
883
|
+
function renderLegacyRunCard(state, options) {
|
|
851
884
|
// 先估后建:用廉价估算预测是否超 28KB 预算,明显超预算直接跳过完整卡构建走
|
|
852
885
|
// degraded。groups/prepared 只算一次,测量与正式渲染共用同一份截断结果,
|
|
853
886
|
// 避免影子测量 + 正式渲染对每个超限块双倍截断。
|
|
@@ -885,26 +918,133 @@ export function renderRunCard(state, options = {}) {
|
|
|
885
918
|
return extremeCard;
|
|
886
919
|
}
|
|
887
920
|
const skeletonCard = assembleRunCard(state, options, buildSkeletonElements(state, options));
|
|
888
|
-
// skeleton 卡是静态结构,理论上必 ≤28KB
|
|
889
|
-
|
|
921
|
+
// skeleton 卡是静态结构,理论上必 ≤28KB;但审批区/summary 携带 agent 产出
|
|
922
|
+
// 文本,仍可能把任一维度顶爆(此前只查字节——题数×选项的元素数超限会漏网)。
|
|
923
|
+
// 保留兜底断言以防未来结构膨胀;超限则降级到 truncatedSkeleton。
|
|
924
|
+
if (withinRunCardBudget(skeletonCard)) {
|
|
890
925
|
return skeletonCard;
|
|
891
926
|
}
|
|
892
|
-
// 理论上不可达:skeleton
|
|
893
|
-
//
|
|
894
|
-
//
|
|
927
|
+
// 理论上不可达:skeleton 已是静态最小结构。若仍超限(超长 reason、题数×
|
|
928
|
+
// 选项爆元素数等审批区输入),硬截断只丢 summary 与完整审批区——审批入口
|
|
929
|
+
// (紧凑形态:标题 + 摘要 + 决策/跳过按钮)与 stop/新会话按钮是红线,任何
|
|
930
|
+
// 一级兜底都不能把它们截掉(丢了用户就再也停不下这个 run / 答不了审批)。
|
|
895
931
|
const truncatedSkeleton = {
|
|
896
932
|
...skeletonCard,
|
|
897
933
|
body: {
|
|
898
934
|
elements: [
|
|
899
935
|
statusRow(state),
|
|
900
936
|
markdownDiv('_⚠️ 输出过大_'),
|
|
901
|
-
...
|
|
937
|
+
...approvalAreaCompact(state),
|
|
902
938
|
...actionRow(state, options),
|
|
903
939
|
],
|
|
904
940
|
},
|
|
905
941
|
};
|
|
906
942
|
return truncatedSkeleton;
|
|
907
943
|
}
|
|
944
|
+
/** Exact-match marker used in the card before the answer is sent separately. */
|
|
945
|
+
export const FINAL_ANSWER_OVERFLOW_MARKER = '_最终回答较长,完整内容已随后发送_';
|
|
946
|
+
/**
|
|
947
|
+
* Build a card around the authoritative final answer.
|
|
948
|
+
*
|
|
949
|
+
* The answer is measured first with the fixed controls. Progress groups are
|
|
950
|
+
* added newest-first only while the whole card still fits. If the answer
|
|
951
|
+
* cannot fit even without progress/summary, the card carries an explicit
|
|
952
|
+
* marker and the bridge sends the complete answer separately.
|
|
953
|
+
*/
|
|
954
|
+
function buildFinalAnswerPriorityCard(state, options) {
|
|
955
|
+
const finalAnswer = state.finalAnswer;
|
|
956
|
+
const groups = groupBlocks(state.blocks);
|
|
957
|
+
const prepared = prepareGroupContent(groups);
|
|
958
|
+
const safeAnswer = truncateMarkdownTables(finalAnswer.content);
|
|
959
|
+
const requiresOverflow = safeAnswer !== finalAnswer.content;
|
|
960
|
+
const ts = formatTimestamp(finalAnswer.timestamp);
|
|
961
|
+
const answerElements = renderTextBlock(safeAnswer, ts, state.terminal !== 'running');
|
|
962
|
+
const fixedStart = [statusRow(state), ...omittedBlocksHint(state)];
|
|
963
|
+
const summary = buildSummaryContent(state, { includeNotices: false });
|
|
964
|
+
const controls = [...actionRow(state, options), ...approvalArea(state)];
|
|
965
|
+
const buildWithProgress = (start, includeSummary) => {
|
|
966
|
+
const renderedProgress = buildAnswerProgressContent(state, prepared, groups.slice(start));
|
|
967
|
+
const progress = start < groups.length
|
|
968
|
+
? [markdownDiv(`_💡 已省略 ${start} 个早期步骤_`), ...renderedProgress]
|
|
969
|
+
: renderedProgress;
|
|
970
|
+
return assembleRunCard(state, options, [
|
|
971
|
+
...fixedStart,
|
|
972
|
+
...progress,
|
|
973
|
+
...(requiresOverflow ? [markdownDiv(FINAL_ANSWER_OVERFLOW_MARKER)] : []),
|
|
974
|
+
...answerElements,
|
|
975
|
+
...(includeSummary ? summary : []),
|
|
976
|
+
...controls,
|
|
977
|
+
]);
|
|
978
|
+
};
|
|
979
|
+
let best = buildWithProgress(groups.length, true);
|
|
980
|
+
if (withinRunCardBudget(best)) {
|
|
981
|
+
for (let start = groups.length - 1; start >= 0; start--) {
|
|
982
|
+
const candidate = buildWithProgress(start, true);
|
|
983
|
+
if (!withinRunCardBudget(candidate))
|
|
984
|
+
break;
|
|
985
|
+
best = candidate;
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
card: best,
|
|
989
|
+
finalAnswerStatus: requiresOverflow ? 'overflow' : 'full',
|
|
990
|
+
...(requiresOverflow ? { finalAnswerText: finalAnswer.content } : {}),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
// The answer outranks the usage summary. Keep the answer and controls first,
|
|
994
|
+
// then retry without the summary before declaring a physical overflow.
|
|
995
|
+
const withoutSummary = buildWithProgress(groups.length, false);
|
|
996
|
+
if (withinRunCardBudget(withoutSummary)) {
|
|
997
|
+
return {
|
|
998
|
+
card: withoutSummary,
|
|
999
|
+
finalAnswerStatus: requiresOverflow ? 'overflow' : 'full',
|
|
1000
|
+
...(requiresOverflow ? { finalAnswerText: finalAnswer.content } : {}),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
const overflowCard = assembleRunCard(state, options, [
|
|
1004
|
+
...fixedStart,
|
|
1005
|
+
markdownDiv(FINAL_ANSWER_OVERFLOW_MARKER),
|
|
1006
|
+
...controls,
|
|
1007
|
+
]);
|
|
1008
|
+
return {
|
|
1009
|
+
card: overflowCard,
|
|
1010
|
+
finalAnswerStatus: 'overflow',
|
|
1011
|
+
finalAnswerText: finalAnswer.content,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
/** Render a contiguous progress suffix for the answer-priority card. */
|
|
1015
|
+
function buildAnswerProgressContent(state, prepared, visible) {
|
|
1016
|
+
const elements = [];
|
|
1017
|
+
const outputMax = toolOutputBudget(state);
|
|
1018
|
+
const contentItems = mergeNoticesIntoContent(visible, state.notices);
|
|
1019
|
+
for (const item of contentItems) {
|
|
1020
|
+
if (item.kind === 'notice') {
|
|
1021
|
+
elements.push(renderNoticeRow(item.notice));
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const group = item;
|
|
1025
|
+
if (group.kind === 'tool') {
|
|
1026
|
+
elements.push(renderTool(group.tool, outputMax));
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (group.kind === 'text') {
|
|
1030
|
+
const content = prepared.get(group) ?? '';
|
|
1031
|
+
const ts = formatTimestamp(group.timestamp);
|
|
1032
|
+
elements.push(...renderTextBlock(content, ts, state.terminal !== 'running'));
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
const spec = describePanelGroup(group, prepared);
|
|
1036
|
+
if (!spec)
|
|
1037
|
+
continue;
|
|
1038
|
+
elements.push(collapsibleMarkdownPanel({
|
|
1039
|
+
title: spec.title,
|
|
1040
|
+
expanded: spec.expanded,
|
|
1041
|
+
border: spec.border,
|
|
1042
|
+
content: spec.content,
|
|
1043
|
+
textSize: 'notation',
|
|
1044
|
+
}));
|
|
1045
|
+
}
|
|
1046
|
+
return elements;
|
|
1047
|
+
}
|
|
908
1048
|
/**
|
|
909
1049
|
* Run 卡片的双预算检查:UTF-8 字节 ≤ 28KB **且** 元素数 ≤ `MAX_CARD_ELEMENTS`。
|
|
910
1050
|
*
|
|
@@ -1015,7 +1155,7 @@ function buildSummaryContent(state, options = {}) {
|
|
|
1015
1155
|
}
|
|
1016
1156
|
else {
|
|
1017
1157
|
const result = state.resultSubtype ?? 'success';
|
|
1018
|
-
const hasContent = state.blocks.length > 0;
|
|
1158
|
+
const hasContent = state.blocks.length > 0 || !!state.finalAnswer?.content.trim();
|
|
1019
1159
|
// 压缩(operationKind='compaction')不会返回 agent 正文——引擎只压缩上下文
|
|
1020
1160
|
// 窗口,空内容属正常,不能沿用普通 run 的「(未返回内容)」异常信号
|
|
1021
1161
|
// (2026-09 用户反馈)。所有会压缩的 agent(codex/claude/kimi/opencode/pi)
|
package/dist/card/run-state.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type RunTerminal = 'running' | 'finalizing' | 'done' | 'error' | 'interru
|
|
|
13
13
|
* 禁止各自再抄一份字面量——三处漂移过一次(goal_continuing 只进白名单不进
|
|
14
14
|
* 按钮判据),新增终态时必漏一处的风险是结构性的。
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
declare const RUN_TERMINAL_STATES: readonly ["done", "error", "interrupted", "idle_timeout", "goal_continuing"];
|
|
17
17
|
export type RunFinalTerminal = (typeof RUN_TERMINAL_STATES)[number];
|
|
18
18
|
/** 该状态是否已收尾(非 running/finalizing)。 */
|
|
19
19
|
export declare function isRunTerminal(terminal: RunTerminal): terminal is RunFinalTerminal;
|
|
@@ -85,6 +85,12 @@ export interface RunNotice {
|
|
|
85
85
|
text: string;
|
|
86
86
|
timestamp?: string;
|
|
87
87
|
}
|
|
88
|
+
export interface FinalAnswerState {
|
|
89
|
+
content: string;
|
|
90
|
+
timestamp?: string;
|
|
91
|
+
complete: boolean;
|
|
92
|
+
source: 'protocol' | 'inferred';
|
|
93
|
+
}
|
|
88
94
|
export interface RunState {
|
|
89
95
|
runId: string;
|
|
90
96
|
terminal: RunTerminal;
|
|
@@ -94,6 +100,11 @@ export interface RunState {
|
|
|
94
100
|
/** 本 run 结束时间(epoch ms):终态后耗时定格,不再随渲染时刻增长。 */
|
|
95
101
|
finishedAt?: number;
|
|
96
102
|
blocks: RunBlock[];
|
|
103
|
+
/**
|
|
104
|
+
* The answer the user is waiting for. Kept outside `blocks` so progress
|
|
105
|
+
* traffic cannot evict or consume its display budget.
|
|
106
|
+
*/
|
|
107
|
+
finalAnswer?: FinalAnswerState;
|
|
97
108
|
sessionId?: string;
|
|
98
109
|
resultSubtype?: 'success' | 'error' | 'interrupted';
|
|
99
110
|
/** interrupted 终态的中断来源;undefined = 未归类(渲染默认文案)。 */
|