lark-remote 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -7
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/bridge/approval-coordinator.d.ts +13 -0
- package/dist/bridge/approval-coordinator.js +34 -1
- package/dist/bridge/index.d.ts +48 -1
- package/dist/bridge/index.js +86 -14
- package/dist/card/approval-render.js +23 -1
- package/dist/card/card-budget.d.ts +37 -0
- package/dist/card/card-budget.js +128 -5
- package/dist/card/run-renderer.d.ts +24 -0
- package/dist/card/run-renderer.js +256 -60
- package/dist/card/run-state.d.ts +26 -0
- package/dist/card/run-state.js +45 -0
- package/dist/card/time.d.ts +14 -0
- package/dist/card/time.js +30 -0
- package/dist/card/tool-render.d.ts +15 -2
- package/dist/card/tool-render.js +40 -66
- package/dist/common/text-width.d.ts +24 -0
- package/dist/common/text-width.js +93 -0
- package/dist/common/tool-summary.d.ts +11 -0
- package/dist/common/tool-summary.js +153 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/index.js +63 -8
- package/dist/platform/command.js +1 -1
- package/dist/platform/shell.d.ts +2 -2
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/router/card-helpers.d.ts +18 -16
- package/dist/router/card-helpers.js +65 -4
- package/dist/router/index.d.ts +9 -0
- package/dist/router/index.js +57 -7
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +3 -3
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +3 -3
- package/dist/runner/common/spawning-runner.d.ts +1 -1
- package/dist/runner/common/spawning-runner.js +2 -2
- package/dist/runner/dsh/auth.js +2 -1
- package/dist/runner/dsh/client.d.ts +124 -6
- package/dist/runner/dsh/client.js +398 -12
- package/dist/runner/dsh/exit-cancel.d.ts +32 -0
- package/dist/runner/dsh/exit-cancel.js +72 -0
- package/dist/runner/dsh/inflight.d.ts +62 -0
- package/dist/runner/dsh/inflight.js +140 -0
- package/dist/runner/dsh/runner.d.ts +117 -7
- package/dist/runner/dsh/runner.js +607 -21
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +103 -1
- package/dist/runner/dsh/types.d.ts +91 -1
- package/dist/runner/types.d.ts +86 -2
- package/dist/session/claude/sessions.js +2 -2
- package/dist/session/dsh/sessions.js +100 -15
- package/dist/update/install.js +1 -1
- package/package.json +1 -1
package/dist/card/card-budget.js
CHANGED
|
@@ -1,4 +1,77 @@
|
|
|
1
1
|
import { truncateUtf8, dropOldestMarkdownTables, countMarkdownTables, FEISHU_MAX_TABLES, CARD_BUDGET_BYTES, } from './text-truncate.js';
|
|
2
|
+
/**
|
|
3
|
+
* 静态卡片体积保护
|
|
4
|
+
*
|
|
5
|
+
* 用于:auto-resume, /active, completion notification 等静态卡片
|
|
6
|
+
* 不适用于:run-renderer 的流式卡片(已有独立保护)
|
|
7
|
+
*
|
|
8
|
+
* Docs: https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukz/feishu-cards/card-components/containers/collapsible-panel
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* 飞书单卡「元素」总量上限(实测)。
|
|
12
|
+
*
|
|
13
|
+
* 2026-10-19 用真实 API 在测试会话探测(`tests/feishu-live/feishu-card-element-total-limit.test.ts`
|
|
14
|
+
* 与 `feishu-workspace-card-limit` / `feishu-order-active-card-limit`):
|
|
15
|
+
*
|
|
16
|
+
* | 形状 | 结果 | elements 计数 |
|
|
17
|
+
* | --- | --- | --- |
|
|
18
|
+
* | 顶层平铺 99 个 div | ✅ | 99 |
|
|
19
|
+
* | 顶层平铺 100 个 div | ❌ 11310 | 100 |
|
|
20
|
+
* | 单列 column_set 内嵌 98 个 div | ✅ | 99 |
|
|
21
|
+
* | 单列 column_set 内嵌 99 个 div | ❌ | 100 |
|
|
22
|
+
* | `/order` 旧结构 20 行 | ❌ | 103 |
|
|
23
|
+
* | `/ws` 旧结构 20 行 | ❌ | 101 |
|
|
24
|
+
* | `/ws` 分页 15 行(整卡 227 个 JSON 对象) | ✅ | 80 |
|
|
25
|
+
*
|
|
26
|
+
* 判据是**递归统计 `elements` 数组成员**(不含 `columns`)**≤ 99**:所有实测的
|
|
27
|
+
* 成功/失败点都被它分开,而"整卡 JSON 对象数"分不开(`/ws` 分页卡 227 个对象
|
|
28
|
+
* 能发出去,平铺 100 个 div 只有 205 个对象却失败)。
|
|
29
|
+
*
|
|
30
|
+
* 另一个独立维度:单个 `column_set` 的列数(实测 66 列通过、67 列失败)。本仓库
|
|
31
|
+
* 所有卡片的列数都在 3 以内,用不到这个上限,仅在此记录以免再次误判。
|
|
32
|
+
*
|
|
33
|
+
* **旧结论勘误**:仓库曾把 `230025` 记为「elements 过多」。本次探测中它一次都
|
|
34
|
+
* 没有出现;元素超限统一是 `230099 / ErrCode: 11310 element exceeds the limit`。
|
|
35
|
+
*
|
|
36
|
+
* `MAX_CARD_ELEMENTS` 取 90(相对实测 99 留 10% 余量),用于渲染前的保守裁剪。
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_CARD_ELEMENTS = 90;
|
|
39
|
+
/**
|
|
40
|
+
* 卡片递归的 `elements` 数组成员数(`columns` 不计数)——与飞书实测边界对齐。
|
|
41
|
+
*
|
|
42
|
+
* 注意与"JSON 对象总数"不同:`column_set` 的 `column` 对象不计入,button 的
|
|
43
|
+
* `behaviors`/`value` 也不计入。用它做预算才既不误伤能发出去的卡片,也不放过
|
|
44
|
+
* 真正超限的卡片。
|
|
45
|
+
*/
|
|
46
|
+
export function countCardElements(card) {
|
|
47
|
+
let total = 0;
|
|
48
|
+
const walk = (value) => {
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
for (const item of value)
|
|
51
|
+
walk(item);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!value || typeof value !== 'object')
|
|
55
|
+
return;
|
|
56
|
+
const rec = value;
|
|
57
|
+
if (Array.isArray(rec.elements)) {
|
|
58
|
+
total += rec.elements.length;
|
|
59
|
+
walk(rec.elements);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(rec.columns)) {
|
|
62
|
+
walk(rec.columns);
|
|
63
|
+
}
|
|
64
|
+
// 继续下潜其余字段(`body`、`header` 等):卡片根对象本身没有 elements,
|
|
65
|
+
// 不遍历字段就永远数到 0(曾因此让整条元素预算静默失效)。
|
|
66
|
+
for (const [key, child] of Object.entries(value)) {
|
|
67
|
+
if (key === 'elements' || key === 'columns')
|
|
68
|
+
continue;
|
|
69
|
+
walk(child);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
walk(card);
|
|
73
|
+
return total;
|
|
74
|
+
}
|
|
2
75
|
/**
|
|
3
76
|
* 静态卡片体积保护
|
|
4
77
|
*
|
|
@@ -13,11 +86,12 @@ export function enforceCardBudget(card, opts = {}) {
|
|
|
13
86
|
const cardStr = JSON.stringify(card);
|
|
14
87
|
const bytesBefore = Buffer.byteLength(cardStr, 'utf8');
|
|
15
88
|
const parsed = JSON.parse(cardStr);
|
|
89
|
+
const elementsBefore = countCardElements(parsed);
|
|
16
90
|
const droppedTables = capCardMarkdownTables(parsed, FEISHU_MAX_TABLES);
|
|
17
91
|
if (droppedTables > 0) {
|
|
18
92
|
const fixedStr = JSON.stringify(parsed);
|
|
19
|
-
// table
|
|
20
|
-
if (Buffer.byteLength(fixedStr, 'utf8')
|
|
93
|
+
// table 截断后可能仍超预算(字节或元素总数),继续走后续阶段
|
|
94
|
+
if (fitsBudget(Buffer.byteLength(fixedStr, 'utf8'), countCardElements(parsed))) {
|
|
21
95
|
return {
|
|
22
96
|
card: parsed,
|
|
23
97
|
wasTruncated: true,
|
|
@@ -26,15 +100,64 @@ export function enforceCardBudget(card, opts = {}) {
|
|
|
26
100
|
bytesAfter: Buffer.byteLength(fixedStr, 'utf8'),
|
|
27
101
|
};
|
|
28
102
|
}
|
|
29
|
-
|
|
30
|
-
return enforceByteBudget(parsed, opts, bytesBefore);
|
|
103
|
+
return enforceElementBudget(parsed, opts, bytesBefore);
|
|
31
104
|
}
|
|
32
|
-
if (bytesBefore
|
|
105
|
+
if (fitsBudget(bytesBefore, elementsBefore)) {
|
|
33
106
|
return { card, wasTruncated: false, bytesBefore, bytesAfter: bytesBefore };
|
|
34
107
|
}
|
|
108
|
+
// 元素总数超限:table 与字节可能都正常,必须先按元素数裁剪。
|
|
109
|
+
if (bytesBefore <= CARD_BUDGET_BYTES && elementsBefore > MAX_CARD_ELEMENTS) {
|
|
110
|
+
return enforceElementBudget(parsed, opts, bytesBefore);
|
|
111
|
+
}
|
|
35
112
|
// 字节超限但 table 不超限
|
|
36
113
|
return enforceByteBudget(JSON.parse(cardStr), opts, bytesBefore);
|
|
37
114
|
}
|
|
115
|
+
/** 同时满足字节与元素总数两个预算。 */
|
|
116
|
+
function fitsBudget(bytes, elements) {
|
|
117
|
+
return bytes <= CARD_BUDGET_BYTES && elements <= MAX_CARD_ELEMENTS;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 元素总数超限时的裁剪:先丢最老的面板,再交给字节路径继续降级。
|
|
121
|
+
*
|
|
122
|
+
* 与字节路径分开是因为成因正交:字节超限来自内容长度(截断内容即可),元素
|
|
123
|
+
* 超限来自元素个数(只能删元素)。两条路径共用 `getPanelIndices`。
|
|
124
|
+
*/
|
|
125
|
+
function enforceElementBudget(card, opts = {}, bytesBefore) {
|
|
126
|
+
const reasons = [];
|
|
127
|
+
const elements = card.body?.elements ?? [];
|
|
128
|
+
const size = () => Buffer.byteLength(JSON.stringify(card), 'utf8');
|
|
129
|
+
const done = (wasTruncated) => ({
|
|
130
|
+
card,
|
|
131
|
+
wasTruncated,
|
|
132
|
+
reason: reasons.join('+') || undefined,
|
|
133
|
+
bytesBefore: bytesBefore ?? size(),
|
|
134
|
+
bytesAfter: size(),
|
|
135
|
+
});
|
|
136
|
+
const dropPanels = (count) => {
|
|
137
|
+
const indices = getPanelIndices(elements)
|
|
138
|
+
.slice(0, count)
|
|
139
|
+
.sort((a, b) => b - a);
|
|
140
|
+
for (const idx of indices)
|
|
141
|
+
elements.splice(idx, 1);
|
|
142
|
+
return indices.length;
|
|
143
|
+
};
|
|
144
|
+
// 阶段1:面板按数量砍(面板是节点大户:一个面板外壳 + 内容)
|
|
145
|
+
const panelCount = getPanelIndices(elements).length;
|
|
146
|
+
if (panelCount > 0) {
|
|
147
|
+
let dropped = 0;
|
|
148
|
+
while (countCardElements(card) > MAX_CARD_ELEMENTS && getPanelIndices(elements).length > 0) {
|
|
149
|
+
dropped += dropPanels(1);
|
|
150
|
+
}
|
|
151
|
+
if (dropped > 0)
|
|
152
|
+
reasons.push('element_budget_panels_dropped');
|
|
153
|
+
}
|
|
154
|
+
if (countCardElements(card) <= MAX_CARD_ELEMENTS && size() <= CARD_BUDGET_BYTES) {
|
|
155
|
+
return done(true);
|
|
156
|
+
}
|
|
157
|
+
// 阶段2:仍超限(既不是面板、或字节同时超)→ 交给字节路径继续降级,
|
|
158
|
+
// 它最终会落到最小卡(节点数固定很小)。
|
|
159
|
+
return enforceByteBudget(card, opts, bytesBefore);
|
|
160
|
+
}
|
|
38
161
|
/**
|
|
39
162
|
* Byte-budget enforcement, extracted so table-limit fix can delegate to it
|
|
40
163
|
* when table truncation alone doesn't bring the card under the byte limit.
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { RunState, ToolEntry } from './run-state.js';
|
|
2
|
+
/**
|
|
3
|
+
* 带「详情」按钮的工具行数上限(从最新往回数)。
|
|
4
|
+
*
|
|
5
|
+
* 按钮行是节点大户:`column_set` + 2 个 `column` + 标题 div/text + button/text/
|
|
6
|
+
* behaviors/value ≈ 10 个 JSON 节点;纯标题行只有 2 个。24 行全带按钮 ≈ 240 节点,
|
|
7
|
+
* 直接撞飞书的元素总数上限(实测 203 通过 / 204 失败,见 card-budget.ts)。
|
|
8
|
+
* 最近 8 行保留按钮(用户最可能展开的就是刚跑完的),更早的行退化成一行标题。
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_TOOL_ROWS_WITH_BUTTON = 8;
|
|
2
11
|
export interface RunCardRenderOptions {
|
|
3
12
|
/**
|
|
4
13
|
* Agent kind for the run card header title (e.g. "Claude · 思考中").
|
|
@@ -39,6 +48,21 @@ type BlockGroup = {
|
|
|
39
48
|
};
|
|
40
49
|
/** 每个块的截断后内容(thinking/plan/text),测量与渲染共用(建议项)。 */
|
|
41
50
|
type GroupContentPrepared = Map<BlockGroup, string>;
|
|
51
|
+
/**
|
|
52
|
+
* 状态行内容:**一行、一个 div、窄屏不换行**。
|
|
53
|
+
*
|
|
54
|
+
* 设计约束(2026-10-19 手机端实测):旧版把「状态标签」和「耗时/指标」放进
|
|
55
|
+
* `column_set` 的 auto + grow 两列,窄屏上 auto 列被挤到只剩两个字宽,
|
|
56
|
+
* "运行中" 被拆成 "运行 / 中"。列式布局的可用宽度取决于对端渲染,服务端无法
|
|
57
|
+
* 保证;所以这里改成单 div,段间用 " · " 分隔——换行只会发生在分隔符处,
|
|
58
|
+
* 而且整行控制在 ~26 个半角宽度内,320px 宽的手机也能放下。
|
|
59
|
+
*
|
|
60
|
+
* 状态标签(运行中/已完成…)不再重复:header 已经写了(`🔧 dsh · 调用工具`
|
|
61
|
+
* / `✅ dsh · 已完成`),终态 summary 还有一次。
|
|
62
|
+
*
|
|
63
|
+
* 导出供窄屏守卫测试断言分段宽度(与 `estimateCardBytes` 同一惯例)。
|
|
64
|
+
*/
|
|
65
|
+
export declare function statusLine(state: RunState): string;
|
|
42
66
|
/** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
|
|
43
67
|
export declare function estimateCardBytes(state: RunState, options?: RunCardRenderOptions, shared?: {
|
|
44
68
|
groups?: BlockGroup[];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { terminalToColor, terminalToLabel, newSessionButton, stopButton, compactButton, agentDisplayName, } from './card-shared.js';
|
|
1
|
+
import { terminalToColor, terminalToLabel, newSessionButton, stopButton, compactButton, cardButton, agentDisplayName, } from './card-shared.js';
|
|
2
2
|
import { collapsibleMarkdownPanel, markdownDiv } from './collapsible.js';
|
|
3
3
|
import { toolBodyMd, toolHeaderText } from './tool-render.js';
|
|
4
4
|
import { truncateUtf8, truncateMarkdownTables, CARD_BUDGET_BYTES } from './text-truncate.js';
|
|
5
|
-
import {
|
|
5
|
+
import { countCardElements, MAX_CARD_ELEMENTS } from './card-budget.js';
|
|
6
|
+
import { truncate } from '../common/truncate.js';
|
|
7
|
+
import { formatClock, formatTimestamp } from './time.js';
|
|
6
8
|
import { formatUsageStats } from '../router/usage-format.js';
|
|
7
9
|
import { renderApprovalArea } from './approval-render.js';
|
|
8
10
|
const REASONING_BYTES = 4_500;
|
|
@@ -10,6 +12,15 @@ const TEXT_BYTES = 10_000;
|
|
|
10
12
|
const DEGRADED_THINKING_KEEP = 2;
|
|
11
13
|
const DEGRADED_TEXT_BYTES = 5_000;
|
|
12
14
|
const DEGRADED_THINKING_BYTES = 1_000;
|
|
15
|
+
/**
|
|
16
|
+
* 带「详情」按钮的工具行数上限(从最新往回数)。
|
|
17
|
+
*
|
|
18
|
+
* 按钮行是节点大户:`column_set` + 2 个 `column` + 标题 div/text + button/text/
|
|
19
|
+
* behaviors/value ≈ 10 个 JSON 节点;纯标题行只有 2 个。24 行全带按钮 ≈ 240 节点,
|
|
20
|
+
* 直接撞飞书的元素总数上限(实测 203 通过 / 204 失败,见 card-budget.ts)。
|
|
21
|
+
* 最近 8 行保留按钮(用户最可能展开的就是刚跑完的),更早的行退化成一行标题。
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_TOOL_ROWS_WITH_BUTTON = 8;
|
|
13
24
|
const DEGRADED_TOOL_KEEP = 3; // Degraded: keep last 3 tool panels
|
|
14
25
|
const EXTREME_TOOL_KEEP = 1; // Extreme: keep last 1 tool panel
|
|
15
26
|
// --- Budget estimate (先估后建) ---
|
|
@@ -228,25 +239,97 @@ function groupBlocks(blocks) {
|
|
|
228
239
|
return groups;
|
|
229
240
|
}
|
|
230
241
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
242
|
+
* 工具块 = **原生折叠面板**(与 thinking/plan 同一交互)。
|
|
243
|
+
*
|
|
244
|
+
* 早先试过"一行 + 服务端「详情」按钮":折叠不省字节所以正文不入 payload,展开
|
|
245
|
+
* 只能回服务端重出卡。代价是手机上要多点一次、按钮还占掉标题宽度(长 URL 被挤
|
|
246
|
+
* 成两行,2026-10-19 反馈)。现在改成折叠面板 + **按工具数量缩放的正文截断**:
|
|
247
|
+
* 正文始终随卡下发(面板折叠时不显示),点标题即展开,无往返、无按钮。
|
|
248
|
+
*
|
|
249
|
+
* 展开态:运行中/失败的默认展开(用户在等结果、错误要立刻可见),其余折叠。
|
|
234
250
|
*/
|
|
235
|
-
function renderTool(tool,
|
|
236
|
-
const
|
|
237
|
-
const body = toolBodyMd(tool);
|
|
251
|
+
function renderTool(tool, outputMax) {
|
|
252
|
+
const body = toolBodyMd(tool, { outputMax });
|
|
238
253
|
return collapsibleMarkdownPanel({
|
|
239
254
|
title: toolTitle(tool),
|
|
240
|
-
expanded:
|
|
241
|
-
border,
|
|
255
|
+
expanded: tool.status === 'running' || tool.status === 'error',
|
|
256
|
+
border: tool.status === 'error' ? 'red' : 'grey',
|
|
242
257
|
content: body || '_无输出_',
|
|
243
258
|
textSize: 'notation',
|
|
244
259
|
});
|
|
245
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* 每个工具的正文输出预算:工具越多,单个工具留的字符越少。
|
|
263
|
+
*
|
|
264
|
+
* 24 个工具(`MAX_BLOCKS`,也是上限)× 1200 字符的正文会把卡片顶到 28KB 以上
|
|
265
|
+
* (实测 13 个 1200 字符的面板就开始走 skeleton 降级)。这里按总量反推:正文
|
|
266
|
+
* 合计约 8KB,单个工具 160–1200 字符——够看清前几行输出,又不会让整卡超预算。
|
|
267
|
+
*/
|
|
268
|
+
function toolOutputBudget(state) {
|
|
269
|
+
let count = 0;
|
|
270
|
+
for (const block of state.blocks)
|
|
271
|
+
if (block.kind === 'tool')
|
|
272
|
+
count += 1;
|
|
273
|
+
return Math.max(160, Math.min(1200, Math.floor(8000 / Math.max(1, count))));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 工具面板标题:`✅ **bash** — npm test (21:23)`。
|
|
277
|
+
*
|
|
278
|
+
* 标题连同时间戳一起收进 `HEADER_DISPLAY_MAX`(见 tool-render)——手机上折叠
|
|
279
|
+
* 面板的标题只有一行可用,超了就会断在 URL/命令中间。时间戳用 `HH:mm` 而不是
|
|
280
|
+
* 完整日期:同一张卡里的时间戳只需要区分先后。
|
|
281
|
+
*/
|
|
246
282
|
function toolTitle(tool) {
|
|
247
|
-
const
|
|
248
|
-
const title = toolHeaderText(tool);
|
|
249
|
-
return
|
|
283
|
+
const clock = formatClock(tool.completedAt ?? tool.startedAt);
|
|
284
|
+
const title = toolHeaderText(tool, clock ? ` (${clock})` : '');
|
|
285
|
+
return title;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 内容组的元素成本(口径 = 递归 `elements` 数组成员,与飞书实测判据一致)。
|
|
289
|
+
*
|
|
290
|
+
* 工具是折叠面板:面板 1 + 内部 div 1 = 2 个元素,与展开态无关(折叠跟展开的
|
|
291
|
+
* payload 一样,这正是"折叠不省字节"的含义——所以靠截断控制大小,而不是靠
|
|
292
|
+
* 不发送正文)。
|
|
293
|
+
*/
|
|
294
|
+
const TOOL_PANEL_ELEMENTS = 2;
|
|
295
|
+
const PANEL_ELEMENTS = 2;
|
|
296
|
+
const TEXT_BLOCK_ELEMENTS = 1;
|
|
297
|
+
/**
|
|
298
|
+
* 内容区(blocks)可用的元素预算 = 总预算 − 固定开销预留。
|
|
299
|
+
*
|
|
300
|
+
* 固定开销:状态行 1 + summary 1 + 操作行 4 + 省略提示/通知若干,实测约 10;
|
|
301
|
+
* 预留 20 留出通知与提示的波动空间。
|
|
302
|
+
*/
|
|
303
|
+
const CONTENT_ELEMENT_BUDGET = MAX_CARD_ELEMENTS - 20;
|
|
304
|
+
/** 单个内容组的元素成本(保守:宁多不少)。 */
|
|
305
|
+
function groupElementCost(group) {
|
|
306
|
+
if (group.kind === 'tool')
|
|
307
|
+
return TOOL_PANEL_ELEMENTS;
|
|
308
|
+
if (group.kind === 'text')
|
|
309
|
+
return TEXT_BLOCK_ELEMENTS;
|
|
310
|
+
return PANEL_ELEMENTS;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* 元素预算内从最新往回选择可见的内容组。
|
|
314
|
+
*
|
|
315
|
+
* 为什么在正常路径里先裁剪而不是整卡降级:降级档(degraded)只保留最后 3 个
|
|
316
|
+
* 工具 + 2 个思考块,对一次跑了几十步的 run 是灾难性的信息损失;元素超限的
|
|
317
|
+
* 成因只是"元素多",砍掉最旧的几步就能同时保住元素预算和最新内容的完整渲染。
|
|
318
|
+
* 被省略的数量由调用方渲染成 `_已省略 N 个早期步骤_`(与 omittedBlocks 同款提示)。
|
|
319
|
+
*/
|
|
320
|
+
function selectVisibleGroups(groups) {
|
|
321
|
+
if (groups.length === 0)
|
|
322
|
+
return { visible: groups, omitted: 0 };
|
|
323
|
+
let used = 0;
|
|
324
|
+
let start = groups.length;
|
|
325
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
326
|
+
const cost = groupElementCost(groups[i]);
|
|
327
|
+
if (start < groups.length && used + cost > CONTENT_ELEMENT_BUDGET)
|
|
328
|
+
break;
|
|
329
|
+
used += cost;
|
|
330
|
+
start = i;
|
|
331
|
+
}
|
|
332
|
+
return { visible: groups.slice(start), omitted: start };
|
|
250
333
|
}
|
|
251
334
|
const DEGRADED_TIER = {
|
|
252
335
|
thinkingKeep: DEGRADED_THINKING_KEEP,
|
|
@@ -331,6 +414,7 @@ function assembleRunCard(state, options, elements) {
|
|
|
331
414
|
*/
|
|
332
415
|
function buildFallbackElements(state, options, tier) {
|
|
333
416
|
const elements = [];
|
|
417
|
+
const outputMax = toolOutputBudget(state);
|
|
334
418
|
elements.push(statusRow(state));
|
|
335
419
|
elements.push(...omittedBlocksHint(state));
|
|
336
420
|
// 单遍分桶:thinking / tool(保持原相对顺序)
|
|
@@ -403,7 +487,7 @@ function buildFallbackElements(state, options, tier) {
|
|
|
403
487
|
}
|
|
404
488
|
else if (group.kind === 'tool') {
|
|
405
489
|
// Show tool individually (each tool is independent now)
|
|
406
|
-
elements.push(renderTool(group.tool,
|
|
490
|
+
elements.push(renderTool(group.tool, outputMax));
|
|
407
491
|
}
|
|
408
492
|
}
|
|
409
493
|
if (elements.length === 0 && tier === EXTREME_TIER) {
|
|
@@ -429,25 +513,87 @@ function buildDegradedElements(state, options) {
|
|
|
429
513
|
function buildExtremeFallbackElements(state, options) {
|
|
430
514
|
return buildFallbackElements(state, options, EXTREME_TIER);
|
|
431
515
|
}
|
|
432
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* 状态行内容:**一行、一个 div、窄屏不换行**。
|
|
518
|
+
*
|
|
519
|
+
* 设计约束(2026-10-19 手机端实测):旧版把「状态标签」和「耗时/指标」放进
|
|
520
|
+
* `column_set` 的 auto + grow 两列,窄屏上 auto 列被挤到只剩两个字宽,
|
|
521
|
+
* "运行中" 被拆成 "运行 / 中"。列式布局的可用宽度取决于对端渲染,服务端无法
|
|
522
|
+
* 保证;所以这里改成单 div,段间用 " · " 分隔——换行只会发生在分隔符处,
|
|
523
|
+
* 而且整行控制在 ~26 个半角宽度内,320px 宽的手机也能放下。
|
|
524
|
+
*
|
|
525
|
+
* 状态标签(运行中/已完成…)不再重复:header 已经写了(`🔧 dsh · 调用工具`
|
|
526
|
+
* / `✅ dsh · 已完成`),终态 summary 还有一次。
|
|
527
|
+
*
|
|
528
|
+
* 导出供窄屏守卫测试断言分段宽度(与 `estimateCardBytes` 同一惯例)。
|
|
529
|
+
*/
|
|
530
|
+
export function statusLine(state) {
|
|
531
|
+
const parts = [];
|
|
532
|
+
// 等待人工决策是「现在卡在哪儿」的唯一线索,值得在状态行重复一次 header
|
|
533
|
+
// (窄屏上用户往往只扫这一行)。其余活动词(思考中/输出中)不重复,header 有。
|
|
534
|
+
if (hasPendingApproval(state) && state.terminal === 'running')
|
|
535
|
+
parts.push('✋ 等待审批');
|
|
536
|
+
const elapsed = elapsedLabel(state);
|
|
537
|
+
if (elapsed)
|
|
538
|
+
parts.push(`⏱ ${elapsed}`);
|
|
539
|
+
// 运行期投影只在 running/finalizing 展示:终态 summary 有权威统计,同一张卡
|
|
540
|
+
// 上出现两次只会互相矛盾。
|
|
541
|
+
const live = state.liveStatus;
|
|
542
|
+
if (live && (state.terminal === 'running' || state.terminal === 'finalizing')) {
|
|
543
|
+
if (live.steps !== undefined)
|
|
544
|
+
parts.push(`🧰 ${live.steps} 步`);
|
|
545
|
+
if (live.contextUsed !== undefined &&
|
|
546
|
+
live.contextLimit !== undefined &&
|
|
547
|
+
live.contextLimit > 0) {
|
|
548
|
+
// 官方口径:projectedTokens ?? pressureTokens,取整封顶 100;`~` 表示投影值。
|
|
549
|
+
const pct = Math.min(100, Math.round((live.contextUsed / live.contextLimit) * 100));
|
|
550
|
+
parts.push(`📊 ~${pct}%`);
|
|
551
|
+
}
|
|
552
|
+
else if (live.contextUsed !== undefined) {
|
|
553
|
+
parts.push(`📊 ${formatTokens(live.contextUsed)}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (parts.length === 0) {
|
|
557
|
+
// 没有任何可用指标时退回状态词(骨架卡/测试构造的状态没有时间戳)。
|
|
558
|
+
return statusTagLabel(state.terminal);
|
|
559
|
+
}
|
|
560
|
+
return parts.join(' · ');
|
|
561
|
+
}
|
|
562
|
+
/** 耗时文案:`12s` / `2m13s` / `1h04m` / `2d3h`。空 = 无时间戳。 */
|
|
563
|
+
function elapsedLabel(state) {
|
|
564
|
+
if (state.startedAt === undefined)
|
|
565
|
+
return '';
|
|
566
|
+
const end = state.finishedAt ?? Date.now();
|
|
567
|
+
const ms = end - state.startedAt;
|
|
568
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
569
|
+
return '';
|
|
570
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
571
|
+
if (totalSeconds < 60)
|
|
572
|
+
return `${totalSeconds}s`;
|
|
573
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
574
|
+
if (minutes < 60)
|
|
575
|
+
return `${minutes}m${String(totalSeconds % 60).padStart(2, '0')}s`;
|
|
576
|
+
const hours = Math.floor(minutes / 60);
|
|
577
|
+
if (hours < 24)
|
|
578
|
+
return `${hours}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
579
|
+
return `${Math.floor(hours / 24)}d${String(hours % 24).padStart(2, '0')}h`;
|
|
580
|
+
}
|
|
581
|
+
/** token 数量的紧凑写法:`950` / `12K` / `1.2M`。 */
|
|
582
|
+
function formatTokens(tokens) {
|
|
583
|
+
if (tokens < 1000)
|
|
584
|
+
return String(tokens);
|
|
585
|
+
if (tokens < 1_000_000)
|
|
586
|
+
return `${Math.round(tokens / 1000)}K`;
|
|
587
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 状态行元素(full/degraded/extreme/skeleton 共用)。
|
|
591
|
+
*
|
|
592
|
+
* 单 div + `notation` 字号:列式布局在窄屏会把短标签挤成竖排(手机端实测),
|
|
593
|
+
* 单行文本的换行点只在 " · " 处,可预期。
|
|
594
|
+
*/
|
|
433
595
|
function statusRow(state) {
|
|
434
|
-
|
|
435
|
-
const durationInfo = buildDurationInfo(state);
|
|
436
|
-
return {
|
|
437
|
-
tag: 'column_set',
|
|
438
|
-
columns: [
|
|
439
|
-
{
|
|
440
|
-
tag: 'column',
|
|
441
|
-
width: 'auto',
|
|
442
|
-
elements: [{ tag: 'div', text: { content: statusLabel, tag: 'lark_md' } }],
|
|
443
|
-
},
|
|
444
|
-
{
|
|
445
|
-
tag: 'column',
|
|
446
|
-
width: 'grow',
|
|
447
|
-
elements: [{ tag: 'div', text: { content: durationInfo, tag: 'lark_md' } }],
|
|
448
|
-
},
|
|
449
|
-
],
|
|
450
|
-
};
|
|
596
|
+
return markdownDiv(statusLine(state), 'notation');
|
|
451
597
|
}
|
|
452
598
|
/**
|
|
453
599
|
* Minimal skeleton card — the final safety net when even the extreme fallback
|
|
@@ -551,11 +697,14 @@ function describePanelGroup(group, prepared) {
|
|
|
551
697
|
}
|
|
552
698
|
/** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
|
|
553
699
|
export function estimateCardBytes(state, options = {}, shared) {
|
|
554
|
-
const finalized = state.terminal !== 'running';
|
|
555
700
|
// renderRunCard 传入共享 groups/prepared 时直接复用(截断只做一次);
|
|
556
701
|
// 独立调用(测试/预算探针)时自建,行为与原先一致。
|
|
557
702
|
const groups = shared?.groups ?? groupBlocks(state.blocks);
|
|
558
703
|
const prepared = shared?.prepared ?? prepareGroupContent(groups);
|
|
704
|
+
// 与 buildChronologicalContent 同源:估算必须测量"将要渲染的内容",而正常
|
|
705
|
+
// 路径会按节点预算省略最旧的组(不模拟这一步会系统性高估)。
|
|
706
|
+
const { visible, omitted } = selectVisibleGroups(groups);
|
|
707
|
+
const outputMax = toolOutputBudget(state);
|
|
559
708
|
// 卡片外壳直接实测 assembleRunCard 空元素产物(schema/config/header/body 结构
|
|
560
709
|
// 与正式渲染完全一致);status/summary/操作行同样实测渲染结构。
|
|
561
710
|
// 注:审批区(approvalArea)不参与估算——待审批通常在途且体积小,超预算由
|
|
@@ -566,11 +715,15 @@ export function estimateCardBytes(state, options = {}, shared) {
|
|
|
566
715
|
measureJson(actionRow(state, options));
|
|
567
716
|
// 块内容:与 buildChronologicalContent 同源(describePanelGroup 单源)
|
|
568
717
|
let renderedAny = false;
|
|
569
|
-
|
|
718
|
+
if (omitted > 0) {
|
|
719
|
+
renderedAny = true;
|
|
720
|
+
total += measureJson(markdownDiv(`_💡 已省略 ${omitted} 个早期步骤_`));
|
|
721
|
+
}
|
|
722
|
+
for (const group of visible) {
|
|
570
723
|
if (group.kind === 'tool') {
|
|
571
724
|
renderedAny = true;
|
|
572
725
|
// tool 直接实测真实渲染元素(复用 renderTool,output ≤1200 字符,物化廉价)
|
|
573
|
-
total += measureJson(renderTool(group.tool,
|
|
726
|
+
total += measureJson(renderTool(group.tool, outputMax));
|
|
574
727
|
continue;
|
|
575
728
|
}
|
|
576
729
|
if (group.kind === 'text') {
|
|
@@ -636,14 +789,15 @@ export function renderRunCard(state, options = {}) {
|
|
|
636
789
|
...approvalArea(state),
|
|
637
790
|
];
|
|
638
791
|
const card = assembleRunCard(state, options, elements);
|
|
639
|
-
// Check budget — stringify
|
|
640
|
-
|
|
792
|
+
// Check budget — stringify 兜底(估算可能低估,仍保留安全网)。
|
|
793
|
+
// 两个维度都必须过:字节(28KB)与元素总数(飞书 11310,实测 203 节点上限)。
|
|
794
|
+
if (withinRunCardBudget(card))
|
|
641
795
|
return card;
|
|
642
796
|
// 超预算 → 继续走 degraded(不 return,fallthrough)
|
|
643
797
|
}
|
|
644
|
-
// 估算 ≥ 阈值 或
|
|
798
|
+
// 估算 ≥ 阈值 或 完整卡超预算 — 走 degraded 渲染
|
|
645
799
|
const degradedCard = assembleRunCard(state, options, buildDegradedElements(state, options));
|
|
646
|
-
if (
|
|
800
|
+
if (withinRunCardBudget(degradedCard)) {
|
|
647
801
|
return degradedCard;
|
|
648
802
|
}
|
|
649
803
|
// Extreme fallback: even degraded exceeds budget
|
|
@@ -651,7 +805,7 @@ export function renderRunCard(state, options = {}) {
|
|
|
651
805
|
// Final safety net: extreme fallback 是最后一层,但高转义内容(反斜杠 4× 膨胀)
|
|
652
806
|
// 可能让 extreme 产物本身 >28KB。补上 stringify 兜底(与 normal/degraded 对称):
|
|
653
807
|
// 超预算则降级到结构性保证 ≤28KB 的 skeleton 卡,避免飞书 11310 整卡不可用。
|
|
654
|
-
if (
|
|
808
|
+
if (withinRunCardBudget(extremeCard)) {
|
|
655
809
|
return extremeCard;
|
|
656
810
|
}
|
|
657
811
|
const skeletonCard = assembleRunCard(state, options, buildSkeletonElements(state, options));
|
|
@@ -675,16 +829,34 @@ export function renderRunCard(state, options = {}) {
|
|
|
675
829
|
};
|
|
676
830
|
return truncatedSkeleton;
|
|
677
831
|
}
|
|
832
|
+
/**
|
|
833
|
+
* Run 卡片的双预算检查:UTF-8 字节 ≤ 28KB **且** 元素数 ≤ `MAX_CARD_ELEMENTS`。
|
|
834
|
+
*
|
|
835
|
+
* 为什么两个维度都要查:字节超限来自内容长(截断即可),元素超限来自元素多
|
|
836
|
+
* (必须删元素)。一张只有几十个短文本的卡片可以远低于 28KB 却把飞书的元素
|
|
837
|
+
* 总数撑爆(实测 24 行工具 + 24 个展开正文 = 103 个元素 → 11310),只查字节
|
|
838
|
+
* 的安全网抓不到它。
|
|
839
|
+
*/
|
|
840
|
+
function withinRunCardBudget(card) {
|
|
841
|
+
if (countCardElements(card) > MAX_CARD_ELEMENTS)
|
|
842
|
+
return false;
|
|
843
|
+
return Buffer.byteLength(JSON.stringify(card), 'utf8') <= CARD_BUDGET_BYTES;
|
|
844
|
+
}
|
|
678
845
|
/** Build content in chronological order using groupBlocks to interleave thinking/text/tools. */
|
|
679
846
|
function buildChronologicalContent(state, prepared, groups) {
|
|
680
847
|
const elements = [];
|
|
681
848
|
// finalized = 非 running(含 finalizing:主结果已出,thinking 折叠)
|
|
682
849
|
const finalized = state.terminal !== 'running';
|
|
850
|
+
// 节点预算内从最新往回选择可见组:超限时只省略最旧的几步,不整卡降级。
|
|
851
|
+
const { visible, omitted } = selectVisibleGroups(groups);
|
|
852
|
+
const outputMax = toolOutputBudget(state);
|
|
853
|
+
if (omitted > 0)
|
|
854
|
+
elements.push(markdownDiv(`_💡 已省略 ${omitted} 个早期步骤_`));
|
|
683
855
|
// renderRunCard 传入同一 groups 数组时直接复用(prepared 的键是组对象
|
|
684
856
|
// 引用,重建数组会让 prepared.get 永远 miss,退化为二次截断)。
|
|
685
857
|
// 面板组描述经 describePanelGroup 与估算共享同一份模板。
|
|
686
858
|
// notices 作为普通消息按到达时间插入内容流(有内容时不再沉到 summary)。
|
|
687
|
-
const contentItems = mergeNoticesIntoContent(
|
|
859
|
+
const contentItems = mergeNoticesIntoContent(visible, state.notices);
|
|
688
860
|
for (const item of contentItems) {
|
|
689
861
|
if (item.kind === 'notice') {
|
|
690
862
|
elements.push(renderNoticeRow(item.notice));
|
|
@@ -692,7 +864,7 @@ function buildChronologicalContent(state, prepared, groups) {
|
|
|
692
864
|
}
|
|
693
865
|
const group = item;
|
|
694
866
|
if (group.kind === 'tool') {
|
|
695
|
-
elements.push(renderTool(group.tool,
|
|
867
|
+
elements.push(renderTool(group.tool, outputMax));
|
|
696
868
|
continue;
|
|
697
869
|
}
|
|
698
870
|
if (group.kind === 'text') {
|
|
@@ -788,28 +960,52 @@ function buildSummaryContent(state, options = {}) {
|
|
|
788
960
|
reasoningTokens: state.reasoningTokens,
|
|
789
961
|
}, { showResult: true, result });
|
|
790
962
|
elements.push(markdownDiv(usageStatsStr + empty));
|
|
963
|
+
// 后台任务:跨 turn 生命周期,必须显式告知并可逐个停止。
|
|
964
|
+
elements.push(...buildBackgroundJobsSection(state));
|
|
791
965
|
}
|
|
792
966
|
return elements;
|
|
793
967
|
}
|
|
794
|
-
/**
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
968
|
+
/** 终态卡最多列几个后台任务(多出来的只报数)。 */
|
|
969
|
+
const MAX_BACKGROUND_JOB_LINES = 3;
|
|
970
|
+
/**
|
|
971
|
+
* 后台任务小节:一行标题 + 每个任务一行(标签 + 停止按钮)。
|
|
972
|
+
*
|
|
973
|
+
* 按钮 payload 只带 runId/jobId:sessionId 与 cwd 由 bridge 的快照解析,卡片
|
|
974
|
+
* 上少放一个可变字段就少一处不一致。
|
|
975
|
+
*/
|
|
976
|
+
function buildBackgroundJobsSection(state) {
|
|
977
|
+
const jobs = state.backgroundJobs ?? [];
|
|
978
|
+
if (jobs.length === 0)
|
|
979
|
+
return [];
|
|
980
|
+
const elements = [
|
|
981
|
+
markdownDiv(`🧵 **后台任务** ${jobs.length} 个仍在运行(本轮结束后继续)`),
|
|
982
|
+
];
|
|
983
|
+
for (const job of jobs.slice(0, MAX_BACKGROUND_JOB_LINES)) {
|
|
984
|
+
// label 是任务自报的一行描述,可能含反引号/regex 反斜杠:按 markdown 安全
|
|
985
|
+
// 形态收敛(markdownDiv 只转义反斜杠,反引号会截断行内代码)。
|
|
986
|
+
const label = truncate(job.label.replace(/[\r\n]+/g, ' ').replace(/`/g, "'"), 60);
|
|
987
|
+
elements.push({
|
|
988
|
+
tag: 'column_set',
|
|
989
|
+
columns: [
|
|
990
|
+
{
|
|
991
|
+
tag: 'column',
|
|
992
|
+
width: 'grow',
|
|
993
|
+
elements: [markdownDiv(`· \`${label}\``, 'notation')],
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
tag: 'column',
|
|
997
|
+
width: 'auto',
|
|
998
|
+
elements: [
|
|
999
|
+
cardButton('⏹ 停止', { cmd: 'job.kill', runId: state.runId, jobId: job.id }, { size: 'small', type: 'danger' }),
|
|
1000
|
+
],
|
|
1001
|
+
},
|
|
1002
|
+
],
|
|
1003
|
+
});
|
|
801
1004
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return
|
|
806
|
-
}
|
|
807
|
-
function footerText(footer) {
|
|
808
|
-
if (footer === 'tool_running')
|
|
809
|
-
return '🧰 工具调用中';
|
|
810
|
-
if (footer === 'streaming')
|
|
811
|
-
return '✍️ 输出中';
|
|
812
|
-
return '🧠 思考中';
|
|
1005
|
+
if (jobs.length > MAX_BACKGROUND_JOB_LINES) {
|
|
1006
|
+
elements.push(markdownDiv(`_…另有 ${jobs.length - MAX_BACKGROUND_JOB_LINES} 个任务_`));
|
|
1007
|
+
}
|
|
1008
|
+
return elements;
|
|
813
1009
|
}
|
|
814
1010
|
/** Status tag label */
|
|
815
1011
|
function statusTagLabel(terminal) {
|
package/dist/card/run-state.d.ts
CHANGED
|
@@ -72,6 +72,10 @@ export interface RunState {
|
|
|
72
72
|
runId: string;
|
|
73
73
|
terminal: RunTerminal;
|
|
74
74
|
footer: RunFooter;
|
|
75
|
+
/** 本 run 开始时间(epoch ms):状态行的「⏱ 12s」由它算出。 */
|
|
76
|
+
startedAt?: number;
|
|
77
|
+
/** 本 run 结束时间(epoch ms):终态后耗时定格,不再随渲染时刻增长。 */
|
|
78
|
+
finishedAt?: number;
|
|
75
79
|
blocks: RunBlock[];
|
|
76
80
|
sessionId?: string;
|
|
77
81
|
resultSubtype?: 'success' | 'error' | 'interrupted';
|
|
@@ -135,6 +139,28 @@ export interface RunState {
|
|
|
135
139
|
* 终态渲染(2026-08-15 事故:标记被消费后卡片误报「已被用户终止」)。
|
|
136
140
|
*/
|
|
137
141
|
approvalExpired?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* 运行期状态投影(DSH `session/control` 的 contextPressure / sessionStats)。
|
|
144
|
+
*
|
|
145
|
+
* 只在 running/finalizing 渲染成状态行的一条(终态卡有权威 summary,不再
|
|
146
|
+
* 重复)。字段缺省 = 该维度暂无数据。
|
|
147
|
+
*/
|
|
148
|
+
liveStatus?: {
|
|
149
|
+
contextUsed?: number;
|
|
150
|
+
contextLimit?: number;
|
|
151
|
+
steps?: number;
|
|
152
|
+
toolMs?: number;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* run 结束时仍在后台运行的任务。跨 turn 生命周期:卡片必须说清楚"这一轮
|
|
156
|
+
* 结束了,但还有东西在跑",并给出逐个停止的入口。
|
|
157
|
+
*/
|
|
158
|
+
backgroundJobs?: Array<{
|
|
159
|
+
id: string;
|
|
160
|
+
label: string;
|
|
161
|
+
status?: string;
|
|
162
|
+
progress?: string;
|
|
163
|
+
}>;
|
|
138
164
|
}
|
|
139
165
|
export interface FinishMeta {
|
|
140
166
|
resultSubtype?: 'success' | 'error' | 'interrupted';
|