lark-remote 0.5.7 → 0.5.9
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 +35 -0
- package/dist/bridge/index.d.ts +47 -7
- package/dist/bridge/index.js +300 -129
- package/dist/bridge/queue-manager.d.ts +6 -0
- package/dist/bridge/queue-manager.js +61 -63
- package/dist/card/approval-render.js +2 -1
- package/dist/card/bash-card-session.d.ts +0 -1
- package/dist/card/bash-card-session.js +0 -8
- package/dist/card/bash-renderer.js +2 -1
- package/dist/card/card-budget.js +15 -19
- package/dist/card/card-session.d.ts +3 -13
- package/dist/card/card-session.js +4 -18
- package/dist/card/card-shared.d.ts +11 -0
- package/dist/card/card-shared.js +23 -13
- package/dist/card/run-card-session.d.ts +0 -1
- package/dist/card/run-card-session.js +0 -6
- package/dist/card/run-renderer.d.ts +11 -10
- package/dist/card/run-renderer.js +198 -35
- package/dist/card/run-state.d.ts +29 -2
- package/dist/card/run-state.js +23 -4
- package/dist/card/text-truncate.d.ts +3 -5
- package/dist/card/text-truncate.js +3 -5
- package/dist/card/tool-render.js +7 -21
- package/dist/common/tool-summary.d.ts +8 -0
- package/dist/common/tool-summary.js +38 -20
- package/dist/common/turn-failure.d.ts +13 -0
- package/dist/common/turn-failure.js +18 -0
- package/dist/index.js +13 -14
- package/dist/order/index.d.ts +2 -0
- package/dist/order/index.js +2 -0
- package/dist/router/card-helpers.js +23 -42
- package/dist/router/config/codex.js +2 -3
- package/dist/router/config/common/model-patch.d.ts +8 -0
- package/dist/router/config/common/model-patch.js +13 -0
- package/dist/router/config/common/render.js +30 -77
- package/dist/router/config/dsh.js +2 -1
- package/dist/router/config/kimi.js +3 -3
- package/dist/router/index.d.ts +0 -11
- package/dist/router/index.js +119 -360
- package/dist/router/order-exec-dispatch.js +2 -1
- package/dist/router/usage-format.d.ts +5 -0
- package/dist/router/usage-format.js +1 -1
- package/dist/runner/common/defaults.d.ts +2 -0
- package/dist/runner/common/defaults.js +3 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
- package/dist/runner/dsh/client.d.ts +15 -0
- package/dist/runner/dsh/client.js +28 -2
- package/dist/runner/dsh/goal-view.d.ts +36 -0
- package/dist/runner/dsh/goal-view.js +106 -0
- package/dist/runner/dsh/runner.js +51 -2
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/types.d.ts +59 -1
- package/dist/runner/types.js +9 -0
- package/dist/session/claude/session-index.d.ts +0 -4
- package/dist/session/claude/session-index.js +0 -8
- package/dist/session/dsh/sessions.js +58 -2
- package/package.json +1 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { isRunTerminal, } from './run-state.js';
|
|
1
2
|
import { terminalToColor, terminalToLabel, newSessionButton, stopButton, compactButton, cardButton, agentDisplayName, } from './card-shared.js';
|
|
2
3
|
import { collapsibleMarkdownPanel, markdownDiv } from './collapsible.js';
|
|
3
4
|
import { toolBodyMd, toolHeaderText } from './tool-render.js';
|
|
4
|
-
import {
|
|
5
|
+
import { truncateMarkdownTables, CARD_BUDGET_BYTES } from './text-truncate.js';
|
|
6
|
+
import { truncateUtf8 } from '../common/truncate.js';
|
|
5
7
|
import { countCardElements, MAX_CARD_ELEMENTS } from './card-budget.js';
|
|
6
|
-
import {
|
|
8
|
+
import { displayWidth, truncateToDisplayWidth } from '../common/text-width.js';
|
|
9
|
+
import { isContentFilterFailure } from '../common/turn-failure.js';
|
|
7
10
|
import { formatClock, formatTimestamp } from './time.js';
|
|
8
11
|
import { formatUsageStats } from '../router/usage-format.js';
|
|
9
12
|
import { renderApprovalArea } from './approval-render.js';
|
|
@@ -12,15 +15,6 @@ const TEXT_BYTES = 10_000;
|
|
|
12
15
|
const DEGRADED_THINKING_KEEP = 2;
|
|
13
16
|
const DEGRADED_TEXT_BYTES = 5_000;
|
|
14
17
|
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;
|
|
24
18
|
const DEGRADED_TOOL_KEEP = 3; // Degraded: keep last 3 tool panels
|
|
25
19
|
const EXTREME_TOOL_KEEP = 1; // Extreme: keep last 1 tool panel
|
|
26
20
|
// --- Budget estimate (先估后建) ---
|
|
@@ -52,10 +46,75 @@ function shouldShowCompactButton(state, options) {
|
|
|
52
46
|
return false;
|
|
53
47
|
if (options.compactSupported === false)
|
|
54
48
|
return false;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
// 没有可反查的会话 = 点了必然失败:「/stop 落在 init 之前」的那类卡
|
|
50
|
+
// 上会留一个注定回「该任务已结束」的死按钮。本轮 sessionId 优先;没有时看
|
|
51
|
+
// bridge 在开跑前探到的 store 会话(`sessionFallbackAvailable`,缺省视为可用
|
|
52
|
+
// 以保持渲染层独立调用方的既有语义)。
|
|
53
|
+
if (!state.sessionId && options.sessionFallbackAvailable === false)
|
|
54
|
+
return false;
|
|
55
|
+
// goal 仍 active = 宿主会自动起下一轮,会话永远不 idle,压缩必被拒(DSH 的
|
|
56
|
+
// 手动压缩要求 agent 空闲)。给按钮就是给一个注定失败的点击:用户点
|
|
57
|
+
// compact 只会拿到"还有一轮在跑"。
|
|
58
|
+
if (state.goal?.phase === 'active')
|
|
59
|
+
return false;
|
|
60
|
+
return isRunTerminal(state.terminal);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* goal 区块:目标是什么、跑到第几轮、当前什么状态。
|
|
64
|
+
*
|
|
65
|
+
* 为什么放在状态行下面(而不是摘要区):手机上用户只会扫前两屏。goal 是解释
|
|
66
|
+
* "这轮为什么结束却没收尾/为什么不能压缩"的唯一线索,必须靠前。
|
|
67
|
+
*
|
|
68
|
+
* 窄屏约束:每行一个 div,段间用 ` · `,不做 auto+grow 并排。
|
|
69
|
+
*/
|
|
70
|
+
const GOAL_INLINE_WIDTH = 64;
|
|
71
|
+
const GOAL_FULL_MAX_BYTES = 2_400;
|
|
72
|
+
/** 目标阶段的短标签。 */
|
|
73
|
+
function goalPhaseLabel(goal) {
|
|
74
|
+
if (goal.phase === 'active')
|
|
75
|
+
return '目标进行中';
|
|
76
|
+
if (goal.phase === 'paused')
|
|
77
|
+
return '目标已暂停';
|
|
78
|
+
if (goal.phase === 'blocked')
|
|
79
|
+
return '目标被阻断';
|
|
80
|
+
return '目标已完成';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build the goal block (empty when the session has no goal).
|
|
84
|
+
*
|
|
85
|
+
* 状态来源是宿主的 `goal` 投影/`goal/change`,不是桥的推断:把"我这一轮结束
|
|
86
|
+
* 了"当任务结束,就是"卡片写完成、任务还在跑"那类缺陷的来源。
|
|
87
|
+
*/
|
|
88
|
+
function buildGoalSection(state) {
|
|
89
|
+
const goal = state.goal;
|
|
90
|
+
if (!goal)
|
|
91
|
+
return [];
|
|
92
|
+
const elements = [];
|
|
93
|
+
const round = `第 ${goal.roundsStarted}/${goal.maxGoalRounds} 轮`;
|
|
94
|
+
elements.push(markdownDiv(`🎯 **${goalPhaseLabel(goal)}** · ${round}`));
|
|
95
|
+
// 目标全文可能有换行/超长:行内压成单行并截断,全文进折叠面板。
|
|
96
|
+
const singleLine = goal.objective.replace(/\s+/gu, ' ').trim();
|
|
97
|
+
const inline = displayWidth(singleLine) <= GOAL_INLINE_WIDTH
|
|
98
|
+
? singleLine
|
|
99
|
+
: truncateToDisplayWidth(singleLine, GOAL_INLINE_WIDTH);
|
|
100
|
+
elements.push(markdownDiv(`目标:${inline}`, 'notation'));
|
|
101
|
+
if (inline !== singleLine) {
|
|
102
|
+
elements.push(collapsibleMarkdownPanel({
|
|
103
|
+
title: '🎯 目标全文',
|
|
104
|
+
expanded: false,
|
|
105
|
+
border: 'grey',
|
|
106
|
+
content: truncateUtf8(goal.objective, GOAL_FULL_MAX_BYTES),
|
|
107
|
+
textSize: 'notation',
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
if (goal.phase === 'blocked' && goal.blockedReason) {
|
|
111
|
+
elements.push(markdownDiv(`⛔ 阻断:${goal.blockedReason.code} — ${goal.blockedReason.message}`, 'notation'));
|
|
112
|
+
}
|
|
113
|
+
if (goal.phase === 'active' && state.terminal === 'goal_continuing') {
|
|
114
|
+
// 这句话就是整张卡的核心信息:本轮结束 ≠ 任务结束。
|
|
115
|
+
elements.push(markdownDiv(`_本轮已结束;DSH 会自动开始第 ${goal.roundsStarted + 1} 轮_`, 'notation'));
|
|
116
|
+
}
|
|
117
|
+
return elements;
|
|
59
118
|
}
|
|
60
119
|
/**
|
|
61
120
|
* Wrap text block in a collapsible panel with timestamp in the header.
|
|
@@ -404,9 +463,22 @@ function assembleRunCard(state, options, elements) {
|
|
|
404
463
|
template: headerTemplate(state),
|
|
405
464
|
title: { content: headerTitle(state, options), tag: 'plain_text' },
|
|
406
465
|
},
|
|
407
|
-
|
|
466
|
+
// goal 区块紧随状态行(所有预算档位都以此为第一元素),让"目标是什么/
|
|
467
|
+
// 第几轮"出现在折叠面板之前;它只有 2–3 个元素,兜底档位也扛得住。
|
|
468
|
+
body: { elements: insertAfterStatusRow(elements, buildGoalSection(state)) },
|
|
408
469
|
};
|
|
409
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* 把目标区块插到状态行之后。所有预算档位的 elements 都以 statusRow 开头;
|
|
473
|
+
* 空数组时原样返回,避免把 `undefined` 塞进飞书卡片数组。
|
|
474
|
+
*/
|
|
475
|
+
function insertAfterStatusRow(elements, extra) {
|
|
476
|
+
if (extra.length === 0)
|
|
477
|
+
return elements;
|
|
478
|
+
if (elements.length === 0)
|
|
479
|
+
return extra;
|
|
480
|
+
return [elements[0], ...extra, ...elements.slice(1)];
|
|
481
|
+
}
|
|
410
482
|
/**
|
|
411
483
|
* Build a budget-fallback tier (degraded or extreme): keep the last N thinking
|
|
412
484
|
* blocks and last N tools, truncate text/thinking per tier, then render in
|
|
@@ -525,6 +597,9 @@ function buildExtremeFallbackElements(state, options) {
|
|
|
525
597
|
* 状态标签(运行中/已完成…)不再重复:header 已经写了(`🔧 dsh · 调用工具`
|
|
526
598
|
* / `✅ dsh · 已完成`),终态 summary 还有一次。
|
|
527
599
|
*
|
|
600
|
+
* 终态也保留状态行:耗时定格,DSH 的步数/上下文投影展示最后一次快照
|
|
601
|
+
* (2026-10-21 用户要求)。summary 的最终 usage 与状态行并存。
|
|
602
|
+
*
|
|
528
603
|
* 导出供窄屏守卫测试断言分段宽度(与 `estimateCardBytes` 同一惯例)。
|
|
529
604
|
*/
|
|
530
605
|
export function statusLine(state) {
|
|
@@ -536,10 +611,11 @@ export function statusLine(state) {
|
|
|
536
611
|
const elapsed = elapsedLabel(state);
|
|
537
612
|
if (elapsed)
|
|
538
613
|
parts.push(`⏱ ${elapsed}`);
|
|
539
|
-
//
|
|
540
|
-
//
|
|
614
|
+
// 运行期投影在终态也保留(2026-10-21 用户要求):DSH 卡片完成时,耗时/步数/
|
|
615
|
+
// 上下文百分比是这一跑留下的最后快照,撤掉后用户只能从 summary 的累计口径
|
|
616
|
+
// 重新推算。summary 仍展示最终 usage,两者口径不同、并存不冲突。
|
|
541
617
|
const live = state.liveStatus;
|
|
542
|
-
if (live
|
|
618
|
+
if (live) {
|
|
543
619
|
if (live.steps !== undefined)
|
|
544
620
|
parts.push(`🧰 ${live.steps} 步`);
|
|
545
621
|
if (live.contextUsed !== undefined &&
|
|
@@ -909,7 +985,16 @@ function buildSummaryContent(state, options = {}) {
|
|
|
909
985
|
return elements;
|
|
910
986
|
}
|
|
911
987
|
if (state.terminal === 'error') {
|
|
912
|
-
|
|
988
|
+
// 服务方内容审核截断(多为误报)不是「程序出错」:照抄英文内部报文会让
|
|
989
|
+
// 用户以为代码或配置坏了。文案说清三件事——谁拦的、改动还在、下一步。
|
|
990
|
+
if (isContentFilterFailure(state.errorMsg)) {
|
|
991
|
+
elements.push(markdownDiv('⚠️ **服务方内容审核拦截**\n\n' +
|
|
992
|
+
'模型输出被上游安全策略截断(`content_filter`),多数是误报,不是代码或配置问题。\n' +
|
|
993
|
+
'本回合已中止;已写入磁盘的改动都还在。重试仍被拦截时,可换模型或稍后再试。'));
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
|
|
997
|
+
}
|
|
913
998
|
}
|
|
914
999
|
else if (state.terminal === 'interrupted') {
|
|
915
1000
|
// 审批超时(approval_expired 已标记)是「无人响应被自动取消」,不是用户
|
|
@@ -958,7 +1043,14 @@ function buildSummaryContent(state, options = {}) {
|
|
|
958
1043
|
model: state.model,
|
|
959
1044
|
costUsd: state.costUsd,
|
|
960
1045
|
reasoningTokens: state.reasoningTokens,
|
|
961
|
-
}, {
|
|
1046
|
+
}, {
|
|
1047
|
+
showResult: true,
|
|
1048
|
+
result,
|
|
1049
|
+
// 本轮结束但目标继续:不能报"已完成",也不能说"出错"。
|
|
1050
|
+
...(state.terminal === 'goal_continuing'
|
|
1051
|
+
? { completedLabel: '⏳ 本轮结束 · 目标仍在继续' }
|
|
1052
|
+
: {}),
|
|
1053
|
+
});
|
|
962
1054
|
elements.push(markdownDiv(usageStatsStr + empty));
|
|
963
1055
|
// 后台任务:跨 turn 生命周期,必须显式告知并可逐个停止。
|
|
964
1056
|
elements.push(...buildBackgroundJobsSection(state));
|
|
@@ -968,7 +1060,55 @@ function buildSummaryContent(state, options = {}) {
|
|
|
968
1060
|
/** 终态卡最多列几个后台任务(多出来的只报数)。 */
|
|
969
1061
|
const MAX_BACKGROUND_JOB_LINES = 3;
|
|
970
1062
|
/**
|
|
971
|
-
*
|
|
1063
|
+
* 命令在手机上可直接内联的显示宽度上限(半角单位,CJK/emoji 记 2)。
|
|
1064
|
+
*
|
|
1065
|
+
* 手机内容区约 320–360px,36 个半角单位约一行;再长会折成"每个词一行"。
|
|
1066
|
+
* 超过这个宽度改用代码块整段展示(完整、可读),而不是截断。
|
|
1067
|
+
*/
|
|
1068
|
+
const BACKGROUND_JOB_INLINE_MAX = 36;
|
|
1069
|
+
/**
|
|
1070
|
+
* 代码块再长到这个程度才折叠:约 180 个半角宽度或 6 行。
|
|
1071
|
+
*
|
|
1072
|
+
* 低于阈值直接展示完整命令(用户一眼能看到,不用点);只有确实很长的
|
|
1073
|
+
* 脚本/链式命令才收进折叠面板,避免一张卡被一条命令占满。
|
|
1074
|
+
*/
|
|
1075
|
+
const BACKGROUND_JOB_FOLD_DISPLAY_MAX = 180;
|
|
1076
|
+
const BACKGROUND_JOB_FOLD_LINES = 6;
|
|
1077
|
+
/**
|
|
1078
|
+
* 任务状态标签:目前 DSH 只会给 running/stopping,其他值按运行中处理。
|
|
1079
|
+
*/
|
|
1080
|
+
function backgroundJobStatusLabel(status) {
|
|
1081
|
+
return status === 'stopping' ? '停止中' : '运行中';
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* 生成能包住任意反引号串的代码围栏。命令里的 shell 反引号不能替换成 `'`
|
|
1085
|
+
* (那是改内容),只能选比内容里最长反引号串更长的围栏。
|
|
1086
|
+
*/
|
|
1087
|
+
function codeFenceFor(content) {
|
|
1088
|
+
let longest = 0;
|
|
1089
|
+
let run = 0;
|
|
1090
|
+
for (const ch of content) {
|
|
1091
|
+
if (ch === '`') {
|
|
1092
|
+
run += 1;
|
|
1093
|
+
longest = Math.max(longest, run);
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
run = 0;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return '`'.repeat(Math.max(3, longest + 1));
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* 后台任务小节:命令独占一行,停止按钮单独一行。
|
|
1103
|
+
*
|
|
1104
|
+
* 布局约束(2026-10-21 手机端反馈):
|
|
1105
|
+
* - 命令 + 按钮不再并排(grow + auto):窄屏下 auto 列会被挤成「🟥…」,
|
|
1106
|
+
* 命令也被截断。按钮现在独占一个单列 column_set,宽度由按钮自身决定。
|
|
1107
|
+
* - 命令不 truncate:短命令/单行直接内联;中等长度的多行命令用代码块
|
|
1108
|
+
* 整段展示;只有超长命令(> 180 显示宽度或 > 6 行)才进折叠面板。
|
|
1109
|
+
* 面板折叠只影响视觉,JSON 里始终带全文。
|
|
1110
|
+
* - 含反引号的命令不走行内代码(无法安全包含反引号),改走代码块;
|
|
1111
|
+
* 围栏长度按内容自动加长,命令原文不改写。
|
|
972
1112
|
*
|
|
973
1113
|
* 按钮 payload 只带 runId/jobId:sessionId 与 cwd 由 bridge 的快照解析,卡片
|
|
974
1114
|
* 上少放一个可变字段就少一处不一致。
|
|
@@ -978,20 +1118,38 @@ function buildBackgroundJobsSection(state) {
|
|
|
978
1118
|
if (jobs.length === 0)
|
|
979
1119
|
return [];
|
|
980
1120
|
const elements = [
|
|
981
|
-
markdownDiv(`🧵 **后台任务** ${jobs.length}
|
|
1121
|
+
markdownDiv(`🧵 **后台任务** ${jobs.length} 个仍在运行`),
|
|
1122
|
+
markdownDiv('_本轮结束后继续,可逐个停止_', 'notation'),
|
|
982
1123
|
];
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const
|
|
1124
|
+
jobs.slice(0, MAX_BACKGROUND_JOB_LINES).forEach((job, index) => {
|
|
1125
|
+
const label = job.label.trim();
|
|
1126
|
+
const lineCount = label === '' ? 0 : label.split(/\r?\n/).length;
|
|
1127
|
+
const inline = label.length > 0 &&
|
|
1128
|
+
lineCount === 1 &&
|
|
1129
|
+
!label.includes('`') &&
|
|
1130
|
+
displayWidth(label) <= BACKGROUND_JOB_INLINE_MAX;
|
|
1131
|
+
if (inline) {
|
|
1132
|
+
elements.push(markdownDiv(`· \`${label}\``));
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
// 多行/超宽/含反引号:代码块保留完整命令;只有超长才折叠。
|
|
1136
|
+
const fence = codeFenceFor(label);
|
|
1137
|
+
const content = `${fence}\n${label}\n${fence}`;
|
|
1138
|
+
const fold = lineCount > BACKGROUND_JOB_FOLD_LINES ||
|
|
1139
|
+
displayWidth(label) > BACKGROUND_JOB_FOLD_DISPLAY_MAX;
|
|
1140
|
+
elements.push(fold
|
|
1141
|
+
? collapsibleMarkdownPanel({
|
|
1142
|
+
title: `🧵 任务 ${index + 1} · ${backgroundJobStatusLabel(job.status)} · 点击展开完整命令`,
|
|
1143
|
+
expanded: false,
|
|
1144
|
+
border: 'grey',
|
|
1145
|
+
content,
|
|
1146
|
+
textSize: 'notation',
|
|
1147
|
+
})
|
|
1148
|
+
: markdownDiv(content));
|
|
1149
|
+
}
|
|
987
1150
|
elements.push({
|
|
988
1151
|
tag: 'column_set',
|
|
989
1152
|
columns: [
|
|
990
|
-
{
|
|
991
|
-
tag: 'column',
|
|
992
|
-
width: 'grow',
|
|
993
|
-
elements: [markdownDiv(`· \`${label}\``, 'notation')],
|
|
994
|
-
},
|
|
995
1153
|
{
|
|
996
1154
|
tag: 'column',
|
|
997
1155
|
width: 'auto',
|
|
@@ -1001,7 +1159,7 @@ function buildBackgroundJobsSection(state) {
|
|
|
1001
1159
|
},
|
|
1002
1160
|
],
|
|
1003
1161
|
});
|
|
1004
|
-
}
|
|
1162
|
+
});
|
|
1005
1163
|
if (jobs.length > MAX_BACKGROUND_JOB_LINES) {
|
|
1006
1164
|
elements.push(markdownDiv(`_…另有 ${jobs.length - MAX_BACKGROUND_JOB_LINES} 个任务_`));
|
|
1007
1165
|
}
|
|
@@ -1014,8 +1172,13 @@ function statusTagLabel(terminal) {
|
|
|
1014
1172
|
/** CardKit 2.0 header title — agent-aware via options.agentKind. */
|
|
1015
1173
|
function headerTitle(state, options = {}) {
|
|
1016
1174
|
const agent = agentDisplayName(options.agentKind ?? 'claude');
|
|
1017
|
-
|
|
1018
|
-
|
|
1175
|
+
// 本轮 turn 结束但 goal 仍 active:这是"一轮结束",不是"任务完成"。标题
|
|
1176
|
+
// 必须说清楚,否则用户会以为活儿干完了。
|
|
1177
|
+
if (state.terminal === 'goal_continuing')
|
|
1178
|
+
return `⏳ ${agent} · 本轮结束(目标继续)`;
|
|
1179
|
+
if (state.terminal === 'done') {
|
|
1180
|
+
return state.goal?.phase === 'complete' ? `✅ ${agent} · 目标已完成` : `✅ ${agent} · 已完成`;
|
|
1181
|
+
}
|
|
1019
1182
|
if (state.terminal === 'error')
|
|
1020
1183
|
return `⚠️ ${agent} · 出错`;
|
|
1021
1184
|
if (state.terminal === 'interrupted')
|
package/dist/card/run-state.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
import type { AgentEvent, ApprovalView, ToolHint } from '../runner/index.js';
|
|
2
|
-
export type RunTerminal = 'running' | 'finalizing' | 'done' | 'error' | 'interrupted' | 'idle_timeout'
|
|
1
|
+
import type { AgentEvent, ApprovalView, ToolHint, GoalView } from '../runner/index.js';
|
|
2
|
+
export type RunTerminal = 'running' | 'finalizing' | 'done' | 'error' | 'interrupted' | 'idle_timeout'
|
|
3
|
+
/**
|
|
4
|
+
* 本轮 turn 已结束,但会话的 goal 仍 active:宿主会自动开始下一轮,任务没有
|
|
5
|
+
* 完成,卡片不得写"完成"。与 done 的差别只在语义与渲染,
|
|
6
|
+
* 卡片同样停止更新。
|
|
7
|
+
*/
|
|
8
|
+
| 'goal_continuing';
|
|
9
|
+
/**
|
|
10
|
+
* 已收尾的 run 终态集合(卡片不再更新)。
|
|
11
|
+
*
|
|
12
|
+
* 单一来源:事件消费守卫、可压缩记账白名单、Compact 按钮可见性三处共用,
|
|
13
|
+
* 禁止各自再抄一份字面量——三处漂移过一次(goal_continuing 只进白名单不进
|
|
14
|
+
* 按钮判据),新增终态时必漏一处的风险是结构性的。
|
|
15
|
+
*/
|
|
16
|
+
export declare const RUN_TERMINAL_STATES: readonly ["done", "error", "interrupted", "idle_timeout", "goal_continuing"];
|
|
17
|
+
export type RunFinalTerminal = (typeof RUN_TERMINAL_STATES)[number];
|
|
18
|
+
/** 该状态是否已收尾(非 running/finalizing)。 */
|
|
19
|
+
export declare function isRunTerminal(terminal: RunTerminal): terminal is RunFinalTerminal;
|
|
3
20
|
export type RunFooter = 'thinking' | 'tool_running' | 'streaming' | null;
|
|
4
21
|
/** 中断来源(interrupted 终态的原因),供卡片如实归因。 */
|
|
5
22
|
export type InterruptedReason = 'approval_timeout' | 'user_stop' | 'approval_cancelled';
|
|
@@ -161,6 +178,16 @@ export interface RunState {
|
|
|
161
178
|
status?: string;
|
|
162
179
|
progress?: string;
|
|
163
180
|
}>;
|
|
181
|
+
/**
|
|
182
|
+
* 会话级长期目标(DSH goal)。跨 turn 生命周期:goal 仍 active 时,本轮结束
|
|
183
|
+
* 只是"一轮结束",宿主会自动续跑,卡片必须显示目标是什么、跑到第几轮。
|
|
184
|
+
*/
|
|
185
|
+
goal?: GoalView;
|
|
186
|
+
/**
|
|
187
|
+
* goal 是否被成功观测过。`false` = 探测失败,渲染层不得据此断言"没有目标/
|
|
188
|
+
* 已完成"(把读不到当成没有,就退回了"任务在跑、卡片写完成"的旧缺陷)。
|
|
189
|
+
*/
|
|
190
|
+
goalKnown?: boolean;
|
|
164
191
|
}
|
|
165
192
|
export interface FinishMeta {
|
|
166
193
|
resultSubtype?: 'success' | 'error' | 'interrupted';
|
package/dist/card/run-state.js
CHANGED
|
@@ -3,6 +3,24 @@ const MAX_REASONING_CHARS = 4_000;
|
|
|
3
3
|
const MAX_TEXT_CHARS = 12_000;
|
|
4
4
|
const MAX_TOOL_DETAIL_CHARS = 3_000;
|
|
5
5
|
const MAX_BLOCKS = 24;
|
|
6
|
+
/**
|
|
7
|
+
* 已收尾的 run 终态集合(卡片不再更新)。
|
|
8
|
+
*
|
|
9
|
+
* 单一来源:事件消费守卫、可压缩记账白名单、Compact 按钮可见性三处共用,
|
|
10
|
+
* 禁止各自再抄一份字面量——三处漂移过一次(goal_continuing 只进白名单不进
|
|
11
|
+
* 按钮判据),新增终态时必漏一处的风险是结构性的。
|
|
12
|
+
*/
|
|
13
|
+
export const RUN_TERMINAL_STATES = [
|
|
14
|
+
'done',
|
|
15
|
+
'error',
|
|
16
|
+
'interrupted',
|
|
17
|
+
'idle_timeout',
|
|
18
|
+
'goal_continuing',
|
|
19
|
+
];
|
|
20
|
+
/** 该状态是否已收尾(非 running/finalizing)。 */
|
|
21
|
+
export function isRunTerminal(terminal) {
|
|
22
|
+
return RUN_TERMINAL_STATES.includes(terminal);
|
|
23
|
+
}
|
|
6
24
|
export function createInitialRunState(runId) {
|
|
7
25
|
return {
|
|
8
26
|
runId,
|
|
@@ -17,16 +35,17 @@ export function reduceRunState(state, event) {
|
|
|
17
35
|
// 只有终态才阻止事件处理。finalizing 是非终态,
|
|
18
36
|
// 仍需累积 assistant 内容(thinking、text、tool_use、后台任务输出)。
|
|
19
37
|
const terminal = state.terminal;
|
|
20
|
-
if (terminal
|
|
21
|
-
terminal === 'error' ||
|
|
22
|
-
terminal === 'interrupted' ||
|
|
23
|
-
terminal === 'idle_timeout') {
|
|
38
|
+
if (isRunTerminal(terminal)) {
|
|
24
39
|
return state;
|
|
25
40
|
}
|
|
26
41
|
if (event.type === 'system')
|
|
27
42
|
return reduceSystemEvent(state, event);
|
|
28
43
|
if (event.type === 'run_status')
|
|
29
44
|
return reduceRunStatusEvent(state, event);
|
|
45
|
+
if (event.type === 'goal_state') {
|
|
46
|
+
// 覆盖写:每次观测都是宿主的整份快照(phase/轮次/目标全文),不做增量。
|
|
47
|
+
return { ...state, goal: event.goal ?? undefined, goalKnown: event.known };
|
|
48
|
+
}
|
|
30
49
|
if (event.type === 'background_jobs')
|
|
31
50
|
return reduceBackgroundJobsEvent(state, event);
|
|
32
51
|
if (event.type === 'result')
|
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
* UTF-8 byte-safe text truncation primitives.
|
|
3
3
|
*
|
|
4
4
|
* The core `fitUtf8` / `truncateUtf8` / `DEFAULT_TRUNCATE_SUFFIX` primitives
|
|
5
|
-
* live in `src/common/truncate.ts
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* live in `src/common/truncate.ts`; this module only adds the card-specific
|
|
6
|
+
* markdown-table helpers. Card callers import the truncation primitives
|
|
7
|
+
* directly from common (no re-export shim).
|
|
8
8
|
*
|
|
9
9
|
* Why byte-safe: Feishu card size limits are measured in UTF-8 bytes, and a
|
|
10
10
|
* naive `str.slice(0, n)` can split a multi-byte CJK/emoji codepoint, yielding
|
|
11
11
|
* invalid UTF-8. `fitUtf8` iterates by Unicode codepoint (via `Array.from` /
|
|
12
12
|
* `for...of`) so it never breaks a surrogate pair or a multi-byte sequence.
|
|
13
13
|
*/
|
|
14
|
-
import { DEFAULT_TRUNCATE_SUFFIX, truncateUtf8 } from '../common/truncate.js';
|
|
15
|
-
export { DEFAULT_TRUNCATE_SUFFIX, truncateUtf8 };
|
|
16
14
|
/** Feishu 11310 limit: max markdown tables per card. */
|
|
17
15
|
export declare const FEISHU_MAX_TABLES = 5;
|
|
18
16
|
/**
|
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
* UTF-8 byte-safe text truncation primitives.
|
|
3
3
|
*
|
|
4
4
|
* The core `fitUtf8` / `truncateUtf8` / `DEFAULT_TRUNCATE_SUFFIX` primitives
|
|
5
|
-
* live in `src/common/truncate.ts
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* live in `src/common/truncate.ts`; this module only adds the card-specific
|
|
6
|
+
* markdown-table helpers. Card callers import the truncation primitives
|
|
7
|
+
* directly from common (no re-export shim).
|
|
8
8
|
*
|
|
9
9
|
* Why byte-safe: Feishu card size limits are measured in UTF-8 bytes, and a
|
|
10
10
|
* naive `str.slice(0, n)` can split a multi-byte CJK/emoji codepoint, yielding
|
|
11
11
|
* invalid UTF-8. `fitUtf8` iterates by Unicode codepoint (via `Array.from` /
|
|
12
12
|
* `for...of`) so it never breaks a surrogate pair or a multi-byte sequence.
|
|
13
13
|
*/
|
|
14
|
-
import { DEFAULT_TRUNCATE_SUFFIX, truncateUtf8 } from '../common/truncate.js';
|
|
15
|
-
export { DEFAULT_TRUNCATE_SUFFIX, truncateUtf8 };
|
|
16
14
|
/** Feishu 11310 limit: max markdown tables per card. */
|
|
17
15
|
export const FEISHU_MAX_TABLES = 5;
|
|
18
16
|
/**
|
package/dist/card/tool-render.js
CHANGED
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
* like `✅ **Bash** — pwd` so collapsed panels stay informative.
|
|
11
11
|
*/
|
|
12
12
|
import { truncate } from '../common/truncate.js';
|
|
13
|
-
import { asRecord, summarizeToolInput } from '../common/tool-summary.js';
|
|
13
|
+
import { asRecord, HEADER_SUMMARY_MAX, summarizeToolInput, toolKindOf, } from '../common/tool-summary.js';
|
|
14
14
|
import { truncateMiddleToDisplayWidth } from '../common/text-width.js';
|
|
15
15
|
// 这两个宽度工具原本住在这里;resume 卡片也要用,已下沉到 common。
|
|
16
16
|
export { displayWidth, truncateMiddleToDisplayWidth, truncateToDisplayWidth, } from '../common/text-width.js';
|
|
17
|
-
const HEADER_SUMMARY_MAX = 80;
|
|
18
17
|
/**
|
|
19
18
|
* 面板标题的显示宽度上限(半角单位,CJK/emoji 记 2)。
|
|
20
19
|
*
|
|
@@ -161,35 +160,22 @@ function renderInput(tool) {
|
|
|
161
160
|
default:
|
|
162
161
|
break;
|
|
163
162
|
}
|
|
164
|
-
switch (tool.name) {
|
|
165
|
-
case '
|
|
166
|
-
case 'bash':
|
|
167
|
-
case 'pwsh':
|
|
163
|
+
switch (toolKindOf(tool.name)) {
|
|
164
|
+
case 'shell':
|
|
168
165
|
return renderBashInput(str);
|
|
169
|
-
case '
|
|
170
|
-
case 'Edit':
|
|
171
|
-
case 'Write':
|
|
172
|
-
case 'NotebookEdit':
|
|
173
|
-
case 'read':
|
|
174
|
-
case 'edit':
|
|
175
|
-
case 'write':
|
|
176
|
-
case 'read_image':
|
|
166
|
+
case 'file':
|
|
177
167
|
return renderFileInput(str);
|
|
178
|
-
case 'Grep':
|
|
179
168
|
case 'grep':
|
|
180
169
|
return renderGrepInput(str);
|
|
181
|
-
case 'Glob':
|
|
182
170
|
case 'glob':
|
|
183
|
-
case 'find':
|
|
184
171
|
return renderGlobInput(str);
|
|
185
172
|
case 'ls':
|
|
186
173
|
return renderLsInput(str);
|
|
187
|
-
case '
|
|
188
|
-
case 'web_fetch':
|
|
174
|
+
case 'webfetch':
|
|
189
175
|
return renderWebFetchInput(str);
|
|
190
|
-
case '
|
|
176
|
+
case 'websearch':
|
|
191
177
|
return renderWebSearchInput(str);
|
|
192
|
-
case '
|
|
178
|
+
case 'websearch_queries':
|
|
193
179
|
return renderWebQueriesInput(rec.queries) || renderWebSearchInput(str);
|
|
194
180
|
default:
|
|
195
181
|
return '';
|
|
@@ -7,5 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 只做"取哪个字段",不做 agent 分支:工具名与参数名的对应表就是这张 switch。
|
|
9
9
|
*/
|
|
10
|
+
/** 工具标题/摘要共用的单行宽度上限。 */
|
|
11
|
+
export declare const HEADER_SUMMARY_MAX = 80;
|
|
12
|
+
/**
|
|
13
|
+
* 工具别名 → 渲染类别。run 卡面板(`card/tool-render.ts`)与摘要
|
|
14
|
+
* (本文件)共用这一张表,新增别名只改这里,避免两处 switch 漂移。
|
|
15
|
+
*/
|
|
16
|
+
export type ToolKind = 'shell' | 'file' | 'grep' | 'glob' | 'ls' | 'webfetch' | 'websearch' | 'websearch_queries';
|
|
17
|
+
export declare function toolKindOf(name: string): ToolKind | undefined;
|
|
10
18
|
export declare function asRecord(input: unknown): Record<string, unknown> | null;
|
|
11
19
|
export declare function summarizeToolInput(name: string, input: unknown): string;
|
|
@@ -8,7 +8,34 @@
|
|
|
8
8
|
* 只做"取哪个字段",不做 agent 分支:工具名与参数名的对应表就是这张 switch。
|
|
9
9
|
*/
|
|
10
10
|
import { truncate } from './truncate.js';
|
|
11
|
-
|
|
11
|
+
/** 工具标题/摘要共用的单行宽度上限。 */
|
|
12
|
+
export const HEADER_SUMMARY_MAX = 80;
|
|
13
|
+
const TOOL_KIND_BY_NAME = {
|
|
14
|
+
Bash: 'shell',
|
|
15
|
+
bash: 'shell',
|
|
16
|
+
pwsh: 'shell',
|
|
17
|
+
Read: 'file',
|
|
18
|
+
Edit: 'file',
|
|
19
|
+
Write: 'file',
|
|
20
|
+
NotebookEdit: 'file',
|
|
21
|
+
read: 'file',
|
|
22
|
+
edit: 'file',
|
|
23
|
+
write: 'file',
|
|
24
|
+
read_image: 'file',
|
|
25
|
+
Grep: 'grep',
|
|
26
|
+
grep: 'grep',
|
|
27
|
+
Glob: 'glob',
|
|
28
|
+
glob: 'glob',
|
|
29
|
+
find: 'glob',
|
|
30
|
+
ls: 'ls',
|
|
31
|
+
WebFetch: 'webfetch',
|
|
32
|
+
web_fetch: 'webfetch',
|
|
33
|
+
WebSearch: 'websearch',
|
|
34
|
+
web_search: 'websearch_queries',
|
|
35
|
+
};
|
|
36
|
+
export function toolKindOf(name) {
|
|
37
|
+
return TOOL_KIND_BY_NAME[name];
|
|
38
|
+
}
|
|
12
39
|
export function asRecord(input) {
|
|
13
40
|
if (!input)
|
|
14
41
|
return null;
|
|
@@ -65,38 +92,29 @@ export function summarizeToolInput(name, input) {
|
|
|
65
92
|
}
|
|
66
93
|
return '';
|
|
67
94
|
};
|
|
68
|
-
switch (name) {
|
|
69
|
-
case '
|
|
70
|
-
case 'bash':
|
|
71
|
-
case 'pwsh':
|
|
95
|
+
switch (toolKindOf(name)) {
|
|
96
|
+
case 'shell':
|
|
72
97
|
return pick('command');
|
|
73
|
-
case '
|
|
74
|
-
case 'Edit':
|
|
75
|
-
case 'Write':
|
|
76
|
-
case 'NotebookEdit':
|
|
77
|
-
case 'read':
|
|
78
|
-
case 'edit':
|
|
79
|
-
case 'write':
|
|
80
|
-
case 'read_image':
|
|
98
|
+
case 'file':
|
|
81
99
|
return pick('file_path') || pick('path');
|
|
82
|
-
case 'Grep':
|
|
83
100
|
case 'grep': {
|
|
84
101
|
const pat = pick('pattern', 40);
|
|
85
102
|
const p = pick('path', 30);
|
|
86
103
|
return p ? `${pat} in ${p}` : pat;
|
|
87
104
|
}
|
|
88
|
-
case 'Glob':
|
|
89
105
|
case 'glob':
|
|
90
|
-
case 'find':
|
|
91
106
|
return pick('pattern');
|
|
92
107
|
case 'ls':
|
|
93
108
|
return pick('path');
|
|
94
|
-
case '
|
|
95
|
-
case 'web_fetch':
|
|
109
|
+
case 'webfetch':
|
|
96
110
|
return pick('url');
|
|
97
|
-
case '
|
|
98
|
-
case '
|
|
111
|
+
case 'websearch':
|
|
112
|
+
case 'websearch_queries':
|
|
99
113
|
return pickFirst('queries', 60) || pick('query', 60);
|
|
114
|
+
default:
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
switch (name) {
|
|
100
118
|
case 'Agent':
|
|
101
119
|
case 'Task':
|
|
102
120
|
case 'subagent':
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent turn 失败原因的判据(卡片渲染与自动续跑共用)。
|
|
3
|
+
*
|
|
4
|
+
* 上游把 `finish_reason: content_filter` 归一到普通 error(DSH 侧 code 为
|
|
5
|
+
* `PI_AI_ERROR`):它既不是限流也不是故障,而是模型输出被服务方安全策略截断,
|
|
6
|
+
* 多数是误报。桥接若把它混进「运行出错」,用户看到的是英文内部报文、不知道
|
|
7
|
+
* 该做什么;桥侧也就无从判断该不该自动续跑。
|
|
8
|
+
*
|
|
9
|
+
* 判据只留一处:渲染层(换文案)与桥接层(自动续跑一次)必须同一个口径,
|
|
10
|
+
* 否则改文案和改重试语义会各自漂移。
|
|
11
|
+
*/
|
|
12
|
+
/** 该 errorMsg 是否表示「模型输出被上游内容审核截断」。 */
|
|
13
|
+
export declare function isContentFilterFailure(errorMsg: string | undefined): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent turn 失败原因的判据(卡片渲染与自动续跑共用)。
|
|
3
|
+
*
|
|
4
|
+
* 上游把 `finish_reason: content_filter` 归一到普通 error(DSH 侧 code 为
|
|
5
|
+
* `PI_AI_ERROR`):它既不是限流也不是故障,而是模型输出被服务方安全策略截断,
|
|
6
|
+
* 多数是误报。桥接若把它混进「运行出错」,用户看到的是英文内部报文、不知道
|
|
7
|
+
* 该做什么;桥侧也就无从判断该不该自动续跑。
|
|
8
|
+
*
|
|
9
|
+
* 判据只留一处:渲染层(换文案)与桥接层(自动续跑一次)必须同一个口径,
|
|
10
|
+
* 否则改文案和改重试语义会各自漂移。
|
|
11
|
+
*/
|
|
12
|
+
/** 服务方内容审核截断的报文特征:`Provider finish_reason: content_filter`。 */
|
|
13
|
+
const CONTENT_FILTER_RE = /finish_reason:\s*["']?content_filter\b/i;
|
|
14
|
+
/** 该 errorMsg 是否表示「模型输出被上游内容审核截断」。 */
|
|
15
|
+
export function isContentFilterFailure(errorMsg) {
|
|
16
|
+
return typeof errorMsg === 'string' && CONTENT_FILTER_RE.test(errorMsg);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=turn-failure.js.map
|