lark-remote 0.5.9 → 0.5.12
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 +49 -0
- package/dist/binder.js +7 -1
- package/dist/bridge/index.d.ts +9 -0
- package/dist/bridge/index.js +28 -3
- package/dist/bridge/queue-manager.d.ts +31 -0
- package/dist/bridge/queue-manager.js +78 -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 +8 -0
- package/dist/card/bash-renderer.js +11 -23
- package/dist/card/card-budget.js +24 -4
- package/dist/card/run-renderer.js +31 -9
- 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.js +13 -1
- package/dist/index.js +42 -24
- package/dist/instance-lock.d.ts +9 -1
- package/dist/instance-lock.js +42 -7
- 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/index.js +179 -122
- package/dist/runner/codex/app-server/runner.js +12 -0
- package/dist/runner/common/acp/base-translator.d.ts +12 -0
- package/dist/runner/common/acp/base-translator.js +22 -0
- package/dist/runner/common/jsonl-stream.js +12 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +6 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +34 -9
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -1
- package/dist/runner/common/jsonrpc/transport.js +7 -1
- package/dist/runner/dsh/approval.js +16 -3
- package/dist/runner/dsh/client.d.ts +31 -1
- package/dist/runner/dsh/client.js +126 -6
- package/dist/runner/dsh/exit-cancel.js +62 -23
- package/dist/runner/dsh/runner.d.ts +7 -0
- package/dist/runner/dsh/runner.js +42 -5
- 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 +10 -0
- package/dist/runner/pi/rpc/runner.js +33 -5
- package/dist/runner/resolve-agent-choices.js +5 -1
- package/dist/runner/sync-agent-choices.js +4 -1
- package/dist/session/claude/session-index.js +9 -2
- 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 +9 -5
- package/dist/session/opencode/sessions.js +13 -6
- package/dist/session/session-store.js +4 -3
- package/package.json +1 -1
|
@@ -8,17 +8,50 @@ import { collapsibleMarkdownPanel } from './collapsible.js';
|
|
|
8
8
|
import { truncateMarkdownTables } from './text-truncate.js';
|
|
9
9
|
import { truncateUtf8 } from '../common/truncate.js';
|
|
10
10
|
import { cardButton } from './card-shared.js';
|
|
11
|
+
import { countCardElements } from './card-budget.js';
|
|
11
12
|
/** Unique nonce for card button callbacks (SDK dedup + router validation). */
|
|
12
13
|
function mkNonce() {
|
|
13
14
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
14
15
|
}
|
|
15
16
|
/** 计划面板正文字节预算(折叠展示 + 截断保护 28KB 卡片预算)。 */
|
|
16
17
|
const PLAN_PANEL_BYTES = 6000;
|
|
17
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* 审批区自由文本字段(reason/note/题面/描述/回执等 agent 产出的散文)的统一
|
|
20
|
+
* 字节预算。这些字段此前无上限:实测 reason 30,000 字符 → 整卡 31,450 字节被
|
|
21
|
+
* 飞书 11310 拒收,审批彻底不可见,run 只能挂到审批超时/看门狗。1KB 足以传达
|
|
22
|
+
* 语义并支撑决策。
|
|
23
|
+
*/
|
|
24
|
+
const APPROVAL_FIELD_BYTES = 1000;
|
|
25
|
+
/** 短标签类字段(题头/工具名/路径)的字节预算。 */
|
|
26
|
+
const APPROVAL_LABEL_BYTES = 300;
|
|
27
|
+
/**
|
|
28
|
+
* 提问批次的渲染上限:题数 × 每题元素(选项行 + 输入框 + 按钮)撑爆的是元素
|
|
29
|
+
* 数预算(实测 8 题 × 5 选项 = 144 元素 > 90 上限 → 11310 整卡报废)。超出的
|
|
30
|
+
* 题折叠为计数提示;被折叠的题无法在卡上作答(整批「跳过」与 Web UI/终端
|
|
31
|
+
* 兜底),仍远好于整卡被拒、一题都答不了。
|
|
32
|
+
*/
|
|
33
|
+
const MAX_RENDERED_QUESTIONS = 5;
|
|
34
|
+
/** 单题选项渲染上限(超过折叠为计数提示;选项 label 是回调 payload,不可截断)。 */
|
|
35
|
+
const MAX_RENDERED_OPTIONS = 8;
|
|
36
|
+
/** 文件变更 / 权限条目的渲染上限(每条 1–2 个元素,无上限时百条级直接爆元素预算)。 */
|
|
37
|
+
const MAX_RENDERED_CHANGE_ITEMS = 20;
|
|
38
|
+
/**
|
|
39
|
+
* 审批区自身的元素数预算:卡总预算(MAX_CARD_ELEMENTS=90)减去状态行 / goal /
|
|
40
|
+
* summary / 操作行等骨架(约 20 个,含极端情况的余量)后的安全份额。按已渲染
|
|
41
|
+
* 元素数**自适应**截断——固定题数 × 选项数的组合上限挡不住「少题多选项」与
|
|
42
|
+
* 「多题少选项」的两种组合(一行选项 = column_set + button + div 共 3 个
|
|
43
|
+
* 元素,5 题 × 5 选项就 ~92 个)。
|
|
44
|
+
*/
|
|
45
|
+
const APPROVAL_AREA_ELEMENT_BUDGET = 50;
|
|
46
|
+
/** 审批区自由文本统一入口:反引号中和(防 11311)+ 字节预算截断(防 11310)。 */
|
|
47
|
+
function capApprovalText(text, maxBytes = APPROVAL_FIELD_BYTES) {
|
|
48
|
+
return truncateUtf8(text.replace(/`/g, '·'), maxBytes);
|
|
49
|
+
}
|
|
50
|
+
/** reason div(3 处共用);反引号中和防 lark_md 解析错误 + 字节预算截断。 */
|
|
18
51
|
function reasonDiv(approval) {
|
|
19
52
|
if (!approval.reason)
|
|
20
53
|
return null;
|
|
21
|
-
const neutralized = approval.reason
|
|
54
|
+
const neutralized = capApprovalText(approval.reason);
|
|
22
55
|
return {
|
|
23
56
|
tag: 'div',
|
|
24
57
|
text: { tag: 'lark_md', content: `💡 ${neutralized}` },
|
|
@@ -41,12 +74,11 @@ export function renderApprovalArea(approval, opts) {
|
|
|
41
74
|
return [];
|
|
42
75
|
// 终态(done/error/interrupted/idle_timeout)下 run 已结束:coordinator 已
|
|
43
76
|
// 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"。
|
|
44
|
-
if (opts
|
|
77
|
+
if (isTerminalHidden(opts))
|
|
45
78
|
return [];
|
|
46
|
-
}
|
|
47
79
|
const elements = [];
|
|
48
80
|
if (approval.note) {
|
|
49
|
-
const neutralized = approval.note
|
|
81
|
+
const neutralized = capApprovalText(approval.note);
|
|
50
82
|
elements.push({
|
|
51
83
|
tag: 'div',
|
|
52
84
|
text: { tag: 'lark_md', content: `ℹ️ ${neutralized}` },
|
|
@@ -69,7 +101,7 @@ export function renderApprovalArea(approval, opts) {
|
|
|
69
101
|
}
|
|
70
102
|
// 响应发送失败:明确告知原因 + 可重试,而不是静默停在可点状态。
|
|
71
103
|
if (approval.deliveryError) {
|
|
72
|
-
const neutralized = approval.deliveryError
|
|
104
|
+
const neutralized = capApprovalText(approval.deliveryError);
|
|
73
105
|
elements.push({
|
|
74
106
|
tag: 'div',
|
|
75
107
|
text: {
|
|
@@ -80,6 +112,54 @@ export function renderApprovalArea(approval, opts) {
|
|
|
80
112
|
}
|
|
81
113
|
return elements;
|
|
82
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* 紧凑审批区(run 卡 skeleton/truncatedSkeleton 兜底档专用):固定小体积的
|
|
117
|
+
* 「标题 + 摘要 reason + 决策按钮」,不渲染正文/选项/题面。
|
|
118
|
+
*
|
|
119
|
+
* 为什么需要:完整审批区是唯一不受估算器管辖、也不参与 degraded/extreme 分层
|
|
120
|
+
* 裁剪的区域; pathological 输入(超长 reason、题数×选项爆元素数)会让所有兜底
|
|
121
|
+
* 层一起超限,超限卡被飞书整卡拒收(11310)→ 审批不可见。兜底层换用本入口后
|
|
122
|
+
* 结构性保证 ≤ 几个元素 + ≤ 1KB 文本,审批入口(红线)在任何降级档都活着。
|
|
123
|
+
*/
|
|
124
|
+
export function renderApprovalAreaCompact(approval, opts) {
|
|
125
|
+
if (!approval)
|
|
126
|
+
return [];
|
|
127
|
+
if (isTerminalHidden(opts))
|
|
128
|
+
return [];
|
|
129
|
+
const elements = [
|
|
130
|
+
{
|
|
131
|
+
tag: 'div',
|
|
132
|
+
text: {
|
|
133
|
+
tag: 'lark_md',
|
|
134
|
+
content: '✋ **等待审批**(内容过长已折叠,完整内容请查终端 / Web UI)',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
pushIf(elements, reasonDiv(approval));
|
|
139
|
+
if (approval.kind === 'question') {
|
|
140
|
+
if (opts?.expired) {
|
|
141
|
+
elements.push({ tag: 'div', text: { tag: 'lark_md', content: '⏰ **提问已超时**' } });
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// 提问批次的紧凑形态只保留整批跳过(逐题作答 UI 被折叠)。
|
|
145
|
+
elements.push(cardButton('⏭️ 跳过回答', {
|
|
146
|
+
cmd: 'approval.respond',
|
|
147
|
+
decision: 'decline',
|
|
148
|
+
requestId: approval.requestId,
|
|
149
|
+
runId: opts?.runId,
|
|
150
|
+
nonce: mkNonce(),
|
|
151
|
+
}, { type: 'default' }));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
elements.push(...renderDecisionButtons(approval, opts?.expired, opts?.runId));
|
|
156
|
+
}
|
|
157
|
+
return elements;
|
|
158
|
+
}
|
|
159
|
+
/** 终态隐藏判定(renderApprovalArea 与 compact 共用)。 */
|
|
160
|
+
function isTerminalHidden(opts) {
|
|
161
|
+
return !!opts?.terminal && opts.terminal !== 'running' && opts.terminal !== 'finalizing';
|
|
162
|
+
}
|
|
83
163
|
/**
|
|
84
164
|
* Render approval action buttons from the real protocol decision list.
|
|
85
165
|
* accept and decline are always offered (decline is a universal safety
|
|
@@ -150,7 +230,7 @@ function renderToolApproval(approval, expired, runId) {
|
|
|
150
230
|
content: '**🔧 工具请求**',
|
|
151
231
|
},
|
|
152
232
|
});
|
|
153
|
-
const toolLabel = approval.toolName ?? '(unknown tool)';
|
|
233
|
+
const toolLabel = capApprovalText(approval.toolName ?? '(unknown tool)', APPROVAL_LABEL_BYTES);
|
|
154
234
|
elements.push({
|
|
155
235
|
tag: 'div',
|
|
156
236
|
text: { tag: 'lark_md', content: `🔧 \`${toolLabel}\`` },
|
|
@@ -158,10 +238,10 @@ function renderToolApproval(approval, expired, runId) {
|
|
|
158
238
|
pushIf(elements, reasonDiv(approval));
|
|
159
239
|
// 原始 input(非空时展示,截断保护卡片预算;ExitPlanMode 的 `{}` 不展示)。
|
|
160
240
|
if (approval.toolInput && approval.toolInput !== '{}') {
|
|
161
|
-
const neutralized = approval.toolInput
|
|
241
|
+
const neutralized = capApprovalText(approval.toolInput, 500);
|
|
162
242
|
elements.push({
|
|
163
243
|
tag: 'div',
|
|
164
|
-
text: { tag: 'lark_md', content: `\`${neutralized
|
|
244
|
+
text: { tag: 'lark_md', content: `\`${neutralized}\`` },
|
|
165
245
|
});
|
|
166
246
|
}
|
|
167
247
|
elements.push(...renderDecisionButtons(approval, expired, runId));
|
|
@@ -184,7 +264,7 @@ function renderPlanApproval(approval, expired, runId) {
|
|
|
184
264
|
});
|
|
185
265
|
pushIf(elements, reasonDiv(approval));
|
|
186
266
|
if (approval.planFilePath) {
|
|
187
|
-
const neutralizedPath = approval.planFilePath
|
|
267
|
+
const neutralizedPath = capApprovalText(approval.planFilePath, APPROVAL_LABEL_BYTES);
|
|
188
268
|
elements.push({
|
|
189
269
|
tag: 'div',
|
|
190
270
|
text: { tag: 'lark_md', content: `📄 ${neutralizedPath}` },
|
|
@@ -215,7 +295,7 @@ function renderPlanApproval(approval, expired, runId) {
|
|
|
215
295
|
if (!expired) {
|
|
216
296
|
elements.push(renderPlanFeedbackInput(approval.requestId, runId));
|
|
217
297
|
if (approval.planFeedback) {
|
|
218
|
-
const neutralized = approval.planFeedback
|
|
298
|
+
const neutralized = capApprovalText(approval.planFeedback);
|
|
219
299
|
elements.push({
|
|
220
300
|
tag: 'div',
|
|
221
301
|
text: { tag: 'lark_md', content: `📝 ${neutralized}` },
|
|
@@ -259,18 +339,17 @@ function renderCommandApproval(approval, expired, runId) {
|
|
|
259
339
|
// Command display
|
|
260
340
|
// 命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
|
|
261
341
|
// 反引号极易触发 11311 解析错误导致整卡失败(同 renderFileDiff 的中和策略)。
|
|
262
|
-
const neutralizedCommand = (approval.command ?? ''
|
|
263
|
-
const cmdContent = neutralizedCommand
|
|
264
|
-
? `\`${neutralizedCommand.slice(0, 500)}\``
|
|
265
|
-
: '(no command)';
|
|
342
|
+
const neutralizedCommand = capApprovalText(approval.command ?? '', 500);
|
|
343
|
+
const cmdContent = neutralizedCommand ? `\`${neutralizedCommand}\`` : '(no command)';
|
|
266
344
|
elements.push({
|
|
267
345
|
tag: 'div',
|
|
268
346
|
text: { tag: 'lark_md', content: `**⚡ 命令审批**\n${cmdContent}` },
|
|
269
347
|
});
|
|
270
348
|
if (approval.commandCwd) {
|
|
349
|
+
const neutralizedCwd = capApprovalText(approval.commandCwd, APPROVAL_LABEL_BYTES);
|
|
271
350
|
elements.push({
|
|
272
351
|
tag: 'div',
|
|
273
|
-
text: { tag: 'lark_md', content: `📂 \`${
|
|
352
|
+
text: { tag: 'lark_md', content: `📂 \`${neutralizedCwd}\`` },
|
|
274
353
|
});
|
|
275
354
|
}
|
|
276
355
|
pushIf(elements, reasonDiv(approval));
|
|
@@ -297,11 +376,16 @@ function renderFileApproval(approval, expired, runId) {
|
|
|
297
376
|
const TOTAL_DIFF_CHARS = 2400;
|
|
298
377
|
let renderedDiffChars = 0;
|
|
299
378
|
let omittedDiffs = 0;
|
|
300
|
-
|
|
379
|
+
// 条目数上限(元素预算):每条 1 path div + 至多 1 diff div,无上限时
|
|
380
|
+
// 百条级变更直接撑爆元素数(>90 → 11310 整卡报废)。
|
|
381
|
+
const changes = approval.fileChanges.slice(0, MAX_RENDERED_CHANGE_ITEMS);
|
|
382
|
+
const omittedChanges = approval.fileChanges.length - changes.length;
|
|
383
|
+
for (const change of changes) {
|
|
301
384
|
const icon = change.kind === 'add' ? '🆕' : change.kind === 'delete' ? '🗑️' : '✏️';
|
|
385
|
+
const path = capApprovalText(change.path, APPROVAL_LABEL_BYTES);
|
|
302
386
|
elements.push({
|
|
303
387
|
tag: 'div',
|
|
304
|
-
text: { tag: 'lark_md', content: `${icon} \`${
|
|
388
|
+
text: { tag: 'lark_md', content: `${icon} \`${path}\` (${change.kind})` },
|
|
305
389
|
});
|
|
306
390
|
if (change.diff) {
|
|
307
391
|
if (renderedDiffChars < TOTAL_DIFF_CHARS) {
|
|
@@ -316,6 +400,12 @@ function renderFileApproval(approval, expired, runId) {
|
|
|
316
400
|
}
|
|
317
401
|
}
|
|
318
402
|
}
|
|
403
|
+
if (omittedChanges > 0) {
|
|
404
|
+
elements.push({
|
|
405
|
+
tag: 'div',
|
|
406
|
+
text: { tag: 'lark_md', content: `_…另有 ${omittedChanges} 个文件变更未展示(卡片预算)_` },
|
|
407
|
+
});
|
|
408
|
+
}
|
|
319
409
|
if (omittedDiffs > 0) {
|
|
320
410
|
elements.push({
|
|
321
411
|
tag: 'div',
|
|
@@ -354,11 +444,16 @@ function renderPermissionsApproval(approval, expired, runId) {
|
|
|
354
444
|
pushIf(elements, reasonDiv(approval));
|
|
355
445
|
// Permission items with toggle buttons
|
|
356
446
|
if (approval.permissions?.items) {
|
|
357
|
-
|
|
447
|
+
// 条目数上限(元素预算):每条 label div + toggle button 共 2 个元素。
|
|
448
|
+
const items = approval.permissions.items.slice(0, MAX_RENDERED_CHANGE_ITEMS);
|
|
449
|
+
const omittedItems = approval.permissions.items.length - items.length;
|
|
450
|
+
for (const item of items) {
|
|
358
451
|
const icon = item.selected ? '✅' : '⬜';
|
|
452
|
+
// label 仅展示(payload 用 item.id),可安全截断。
|
|
453
|
+
const label = capApprovalText(item.label, APPROVAL_LABEL_BYTES);
|
|
359
454
|
elements.push({
|
|
360
455
|
tag: 'div',
|
|
361
|
-
text: { tag: 'lark_md', content: `${icon} ${
|
|
456
|
+
text: { tag: 'lark_md', content: `${icon} ${label}` },
|
|
362
457
|
});
|
|
363
458
|
elements.push(cardButton(item.selected ? '取消授予' : '授予', {
|
|
364
459
|
cmd: 'approval.toggle',
|
|
@@ -369,6 +464,12 @@ function renderPermissionsApproval(approval, expired, runId) {
|
|
|
369
464
|
nonce: mkNonce(),
|
|
370
465
|
}, { type: item.selected ? 'default' : 'primary', size: 'small' }));
|
|
371
466
|
}
|
|
467
|
+
if (omittedItems > 0) {
|
|
468
|
+
elements.push({
|
|
469
|
+
tag: 'div',
|
|
470
|
+
text: { tag: 'lark_md', content: `_…另有 ${omittedItems} 项权限未展示(卡片预算)_` },
|
|
471
|
+
});
|
|
472
|
+
}
|
|
372
473
|
}
|
|
373
474
|
// Action buttons
|
|
374
475
|
// Derived from the real protocol decision list (see renderDecisionButtons).
|
|
@@ -396,7 +497,7 @@ function renderQuestionApproval(approval, expired, runId) {
|
|
|
396
497
|
if (approval.intro) {
|
|
397
498
|
elements.push({
|
|
398
499
|
tag: 'div',
|
|
399
|
-
text: { tag: 'lark_md', content: `📝 ${approval.intro}` },
|
|
500
|
+
text: { tag: 'lark_md', content: `📝 ${capApprovalText(approval.intro)}` },
|
|
400
501
|
});
|
|
401
502
|
}
|
|
402
503
|
if (expired) {
|
|
@@ -410,22 +511,36 @@ function renderQuestionApproval(approval, expired, runId) {
|
|
|
410
511
|
if (approval.expiredHint) {
|
|
411
512
|
elements.push({
|
|
412
513
|
tag: 'div',
|
|
413
|
-
text: { tag: 'lark_md', content: approval.expiredHint
|
|
514
|
+
text: { tag: 'lark_md', content: capApprovalText(approval.expiredHint) },
|
|
414
515
|
});
|
|
415
516
|
}
|
|
416
517
|
return elements;
|
|
417
518
|
}
|
|
418
519
|
const questions = approval.questions ?? [];
|
|
520
|
+
let renderedQuestions = 0;
|
|
419
521
|
for (let i = 0; i < questions.length; i++) {
|
|
522
|
+
// 题数粗上限 + 审批区元素预算(自适应):任一触发即折叠剩余题。
|
|
523
|
+
if (i >= MAX_RENDERED_QUESTIONS ||
|
|
524
|
+
countCardElements({ elements }) > APPROVAL_AREA_ELEMENT_BUDGET) {
|
|
525
|
+
elements.push({
|
|
526
|
+
tag: 'div',
|
|
527
|
+
text: {
|
|
528
|
+
tag: 'lark_md',
|
|
529
|
+
content: `_…另有 ${questions.length - renderedQuestions} 题未展示(卡片元素预算);可在终端 / Web UI 回答,或点下方「跳过回答」跳过整批_`,
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
renderedQuestions++;
|
|
420
535
|
const q = questions[i];
|
|
421
536
|
const selected = q.selected ?? [];
|
|
422
|
-
const header = q.header || `问题 ${i + 1}
|
|
537
|
+
const header = capApprovalText(q.header || `问题 ${i + 1}`, APPROVAL_LABEL_BYTES);
|
|
423
538
|
const freeText = (q.options?.length ?? 0) === 0;
|
|
424
539
|
elements.push({
|
|
425
540
|
tag: 'div',
|
|
426
541
|
text: {
|
|
427
542
|
tag: 'lark_md',
|
|
428
|
-
content: `**${header}**${q.multiSelect ? '(可多选)' : ''}\n${q.question}`,
|
|
543
|
+
content: `**${header}**${q.multiSelect ? '(可多选)' : ''}\n${capApprovalText(q.question)}`,
|
|
429
544
|
},
|
|
430
545
|
});
|
|
431
546
|
if (q.detail) {
|
|
@@ -451,24 +566,35 @@ function renderQuestionApproval(approval, expired, runId) {
|
|
|
451
566
|
if (answered) {
|
|
452
567
|
elements.push({
|
|
453
568
|
tag: 'div',
|
|
454
|
-
text: { tag: 'lark_md', content: `✍️ 已答:${answered}` },
|
|
569
|
+
text: { tag: 'lark_md', content: `✍️ 已答:${capApprovalText(answered)}` },
|
|
455
570
|
});
|
|
456
571
|
}
|
|
457
572
|
continue;
|
|
458
573
|
}
|
|
459
|
-
|
|
574
|
+
// 选项渲染上限(label 是回调 payload 不可截断,只能按条数折叠)。
|
|
575
|
+
const options = q.options ?? [];
|
|
576
|
+
for (const option of options.slice(0, MAX_RENDERED_OPTIONS)) {
|
|
460
577
|
const isSelected = selected.includes(option.label);
|
|
461
578
|
// 单选/多选图标区分:单选 ⚪/🔵(radio 隐喻),多选 ⬜/☑️(checkbox 隐喻)。
|
|
462
579
|
const icon = q.multiSelect ? (isSelected ? '☑️' : '⬜') : isSelected ? '🔵' : '⚪';
|
|
463
580
|
elements.push(renderOptionRow(approval.requestId, i, option, isSelected, icon, runId));
|
|
464
581
|
}
|
|
582
|
+
if (options.length > MAX_RENDERED_OPTIONS) {
|
|
583
|
+
elements.push({
|
|
584
|
+
tag: 'div',
|
|
585
|
+
text: {
|
|
586
|
+
tag: 'lark_md',
|
|
587
|
+
content: `_…另有 ${options.length - MAX_RENDERED_OPTIONS} 个选项未展示(卡片元素预算)_`,
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
}
|
|
465
591
|
// 自定义答案(Other)的选中态展示:选项按钮无法表示自由文本,单独显示
|
|
466
592
|
// 已选文本,避免「点了没反应」的困惑。
|
|
467
593
|
const customSelected = q.selected?.find((s) => !q.options.some((o) => o.label === s));
|
|
468
594
|
if (customSelected) {
|
|
469
595
|
elements.push({
|
|
470
596
|
tag: 'div',
|
|
471
|
-
text: { tag: 'lark_md', content: `✍️ 自定义答案:${customSelected}` },
|
|
597
|
+
text: { tag: 'lark_md', content: `✍️ 自定义答案:${capApprovalText(customSelected)}` },
|
|
472
598
|
});
|
|
473
599
|
}
|
|
474
600
|
// 单选问题提供自定义答案(Other)输入——输入文本后提交
|
|
@@ -486,7 +612,7 @@ function renderQuestionApproval(approval, expired, runId) {
|
|
|
486
612
|
if (q.note) {
|
|
487
613
|
elements.push({
|
|
488
614
|
tag: 'div',
|
|
489
|
-
text: { tag: 'lark_md', content: `📝 ${q.note}` },
|
|
615
|
+
text: { tag: 'lark_md', content: `📝 ${capApprovalText(q.note)}` },
|
|
490
616
|
});
|
|
491
617
|
}
|
|
492
618
|
}
|
|
@@ -550,7 +676,7 @@ function renderOptionRow(requestId, questionIndex, option, isSelected, icon, run
|
|
|
550
676
|
tag: 'div',
|
|
551
677
|
text: {
|
|
552
678
|
tag: 'lark_md',
|
|
553
|
-
content: `${icon} ${option.label}${option.description ? `\n_${option.description}_` : ''}`,
|
|
679
|
+
content: `${icon} ${option.label}${option.description ? `\n_${capApprovalText(option.description)}_` : ''}`,
|
|
554
680
|
},
|
|
555
681
|
},
|
|
556
682
|
],
|
|
@@ -68,6 +68,14 @@ export class BashCardSession extends CardSession {
|
|
|
68
68
|
}
|
|
69
69
|
/** Transition to a terminal state and patch the card. */
|
|
70
70
|
async finish(terminal, meta = {}) {
|
|
71
|
+
// 终态守卫(与 run-state 的 finishRun 首终态优先同口径):/stop 路径
|
|
72
|
+
// (bridge.interruptCurrentRun)与 executeBashInternal 的自然收尾路径会对
|
|
73
|
+
// 同一张卡并发 finish——SIGKILL 后 exitCode 为 -1,自然路径的
|
|
74
|
+
// finish('error', { exitCode: -1 }) 会把 interrupt 路径刚写的 'interrupted'
|
|
75
|
+
// 覆盖成「执行失败」,后落者胜出且取决于时序。首终态胜出后忽略后续 finish。
|
|
76
|
+
if (this.state.terminal !== 'running') {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
71
79
|
this.cancelPendingFlush();
|
|
72
80
|
// 若有 in-flight flush 正在 await updateCard,必须等它完成后再发终态 patch,
|
|
73
81
|
// 保证顺序(pre-terminal 先落地 → terminal 后落地 → terminal 胜出)。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { terminalToColor, terminalToLabel, stopButton } from './card-shared.js';
|
|
2
2
|
import { markdownDiv } from './collapsible.js';
|
|
3
3
|
import { CARD_BUDGET_BYTES } from './text-truncate.js';
|
|
4
|
-
import { truncateUtf8 } from '../common/truncate.js';
|
|
4
|
+
import { truncateUtf8, truncate } from '../common/truncate.js';
|
|
5
5
|
/**
|
|
6
6
|
* 飞书卡片大小限制(安全阈值,与 run-renderer 一致)。
|
|
7
7
|
* 从共享的 text-truncate.ts 导入,避免三处各自定义后数值漂移。
|
|
@@ -20,36 +20,24 @@ const OUTPUT_MAX_BYTES = 12_000;
|
|
|
20
20
|
*/
|
|
21
21
|
const DEGRADED_OUTPUT_BYTES = 4_000;
|
|
22
22
|
/**
|
|
23
|
-
* Truncate long commands for display.
|
|
23
|
+
* Truncate long commands for display. 按码点截断(裸 slice 切在 emoji 中间会
|
|
24
|
+
* 留下孤立代理项进卡片),项目已有 truncate() 实现同语义。
|
|
24
25
|
*/
|
|
25
26
|
function truncateCommand(cmd, maxLen = 40) {
|
|
26
|
-
|
|
27
|
-
return cmd;
|
|
28
|
-
return cmd.slice(0, maxLen - 3) + '...';
|
|
27
|
+
return truncate(cmd, maxLen);
|
|
29
28
|
}
|
|
30
29
|
/**
|
|
31
|
-
* Build the status row
|
|
30
|
+
* Build the status row element.
|
|
31
|
+
*
|
|
32
|
+
* 单 div + ` · ` 分隔(窄屏硬闸,与 run-renderer 的 statusRow 同款):旧版把
|
|
33
|
+
* 状态标签(2 字短文本)放 `column_set` 的 auto 列、命令放 grow 列——手机
|
|
34
|
+
* 窄屏下 auto 列会被挤成竖排(run 卡踩过同型坑,bash 卡漏改)。
|
|
35
|
+
* 单行文本的换行点只落在 " · " 处,各分段自身不折行。
|
|
32
36
|
*/
|
|
33
37
|
function buildStatusRow(state) {
|
|
34
38
|
const { terminal, exitCode, command } = state;
|
|
35
39
|
const statusLabel = bashStatusTagLabel(terminal, exitCode);
|
|
36
|
-
return {
|
|
37
|
-
tag: 'column_set',
|
|
38
|
-
columns: [
|
|
39
|
-
{
|
|
40
|
-
tag: 'column',
|
|
41
|
-
width: 'auto',
|
|
42
|
-
elements: [{ tag: 'div', text: { content: statusLabel, tag: 'lark_md' } }],
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
tag: 'column',
|
|
46
|
-
width: 'grow',
|
|
47
|
-
elements: [
|
|
48
|
-
{ tag: 'div', text: { content: `⏱ ${truncateCommand(command, 40)}`, tag: 'lark_md' } },
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
};
|
|
40
|
+
return markdownDiv(`${statusLabel} · ⏱ ${truncateCommand(command, 40)}`, 'notation');
|
|
53
41
|
}
|
|
54
42
|
/**
|
|
55
43
|
* Build the exit code footer elements (terminal states only).
|
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',
|
|
@@ -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
|
}
|
|
@@ -885,20 +904,23 @@ export function renderRunCard(state, options = {}) {
|
|
|
885
904
|
return extremeCard;
|
|
886
905
|
}
|
|
887
906
|
const skeletonCard = assembleRunCard(state, options, buildSkeletonElements(state, options));
|
|
888
|
-
// skeleton 卡是静态结构,理论上必 ≤28KB
|
|
889
|
-
|
|
907
|
+
// skeleton 卡是静态结构,理论上必 ≤28KB;但审批区/summary 携带 agent 产出
|
|
908
|
+
// 文本,仍可能把任一维度顶爆(此前只查字节——题数×选项的元素数超限会漏网)。
|
|
909
|
+
// 保留兜底断言以防未来结构膨胀;超限则降级到 truncatedSkeleton。
|
|
910
|
+
if (withinRunCardBudget(skeletonCard)) {
|
|
890
911
|
return skeletonCard;
|
|
891
912
|
}
|
|
892
|
-
// 理论上不可达:skeleton
|
|
893
|
-
//
|
|
894
|
-
//
|
|
913
|
+
// 理论上不可达:skeleton 已是静态最小结构。若仍超限(超长 reason、题数×
|
|
914
|
+
// 选项爆元素数等审批区输入),硬截断只丢 summary 与完整审批区——审批入口
|
|
915
|
+
// (紧凑形态:标题 + 摘要 + 决策/跳过按钮)与 stop/新会话按钮是红线,任何
|
|
916
|
+
// 一级兜底都不能把它们截掉(丢了用户就再也停不下这个 run / 答不了审批)。
|
|
895
917
|
const truncatedSkeleton = {
|
|
896
918
|
...skeletonCard,
|
|
897
919
|
body: {
|
|
898
920
|
elements: [
|
|
899
921
|
statusRow(state),
|
|
900
922
|
markdownDiv('_⚠️ 输出过大_'),
|
|
901
|
-
...
|
|
923
|
+
...approvalAreaCompact(state),
|
|
902
924
|
...actionRow(state, options),
|
|
903
925
|
],
|
|
904
926
|
},
|
|
@@ -54,6 +54,11 @@ function charWidth(ch) {
|
|
|
54
54
|
return 2;
|
|
55
55
|
if (code >= 0x2600 && code <= 0x27bf)
|
|
56
56
|
return 2;
|
|
57
|
+
// Misc Technical 块(U+2300–U+23FF):⏱⏹⏳⏸⏺ 等计时器/播放器符号在
|
|
58
|
+
// 状态行里实际使用,手机端按 emoji 宽度渲染——按 1 计会系统性低估行宽,
|
|
59
|
+
// 窄屏余量比设计值更小。
|
|
60
|
+
if (code >= 0x2300 && code <= 0x23ff)
|
|
61
|
+
return 2;
|
|
57
62
|
return 1;
|
|
58
63
|
}
|
|
59
64
|
/**
|
|
@@ -61,7 +61,9 @@ export function summarizeToolInput(name, input) {
|
|
|
61
61
|
if (typeof v !== 'string')
|
|
62
62
|
return '';
|
|
63
63
|
const oneLine = v.replace(/\s+/g, ' ').trim();
|
|
64
|
-
|
|
64
|
+
// 码点安全截断:裸 slice 切在代理对中间会留下孤立代理项进卡片
|
|
65
|
+
// (同族修复见 tool-render.ts 的 toolHeaderText)。
|
|
66
|
+
return truncate(oneLine, max);
|
|
65
67
|
};
|
|
66
68
|
/** First non-empty of several keys(DSH 同名工具跨版本改过参数名)。 */
|
|
67
69
|
const pickAny = (keys, max = HEADER_SUMMARY_MAX) => {
|
|
@@ -82,7 +84,7 @@ export function summarizeToolInput(name, input) {
|
|
|
82
84
|
for (const item of v) {
|
|
83
85
|
if (typeof item === 'string' && item.trim()) {
|
|
84
86
|
const oneLine = item.replace(/\s+/g, ' ').trim();
|
|
85
|
-
return oneLine
|
|
87
|
+
return truncate(oneLine, max);
|
|
86
88
|
}
|
|
87
89
|
if (item && typeof item === 'object') {
|
|
88
90
|
const path = item.path;
|
|
@@ -518,9 +518,11 @@ export function loadCodexConfig(opts = {}) {
|
|
|
518
518
|
};
|
|
519
519
|
}
|
|
520
520
|
catch {
|
|
521
|
-
// config.toml
|
|
522
|
-
//
|
|
523
|
-
//
|
|
521
|
+
// config.toml 读取/解析失败:readModelCatalogJsonPath 对此返回
|
|
522
|
+
// TOML_PARSE_FAILED_SENTINEL(isCodexCatalogMode → false),进入本 catch
|
|
523
|
+
// 时 catalogMode 恒为 false——下面的 `if (catalogMode)` 分支实际不可达,
|
|
524
|
+
// 仅作防御保留。真正「catalog 声明退化」的场景('' 哨兵)在 parse 之前
|
|
525
|
+
// 就已按 FALLBACK 兜底处理,不会走到这里。
|
|
524
526
|
if (catalogMode) {
|
|
525
527
|
return {
|
|
526
528
|
currentModel: '',
|
package/dist/config/index.js
CHANGED
|
@@ -353,7 +353,19 @@ export function loadConfig(configPath) {
|
|
|
353
353
|
process.exit(1);
|
|
354
354
|
}
|
|
355
355
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
356
|
-
|
|
356
|
+
// YAML 语法错误(tab 缩进等)必须与校验失败同路径(console.error + exit):
|
|
357
|
+
// 直接上抛会冒到 main().catch——此时 logger 尚未 initLogger,getLogger()
|
|
358
|
+
// 写的是默认目录的自定义 --config-dir 对不上,且 logger 只写文件,终端
|
|
359
|
+
// 零输出,用户只看到进程无声退出。
|
|
360
|
+
let parsed;
|
|
361
|
+
try {
|
|
362
|
+
parsed = YAML.parse(raw);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.error(`[lark-remote] config YAML parse failed at ${configPath}:`);
|
|
366
|
+
console.error(` ${err.message}`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
357
369
|
const result = AppConfigSchema.safeParse(parsed);
|
|
358
370
|
if (!result.success) {
|
|
359
371
|
const errors = result.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n');
|