lark-remote 0.5.7 → 0.5.8

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/bridge/index.d.ts +15 -0
  3. package/dist/bridge/index.js +61 -0
  4. package/dist/bridge/queue-manager.js +41 -58
  5. package/dist/card/approval-render.js +2 -1
  6. package/dist/card/bash-card-session.d.ts +0 -1
  7. package/dist/card/bash-card-session.js +0 -8
  8. package/dist/card/bash-renderer.js +2 -1
  9. package/dist/card/card-budget.js +15 -19
  10. package/dist/card/card-session.d.ts +3 -13
  11. package/dist/card/card-session.js +4 -18
  12. package/dist/card/card-shared.d.ts +11 -0
  13. package/dist/card/card-shared.js +19 -13
  14. package/dist/card/run-card-session.d.ts +0 -1
  15. package/dist/card/run-card-session.js +0 -6
  16. package/dist/card/run-renderer.d.ts +3 -9
  17. package/dist/card/run-renderer.js +99 -27
  18. package/dist/card/text-truncate.d.ts +3 -5
  19. package/dist/card/text-truncate.js +3 -5
  20. package/dist/card/tool-render.js +7 -21
  21. package/dist/common/tool-summary.d.ts +8 -0
  22. package/dist/common/tool-summary.js +38 -20
  23. package/dist/common/turn-failure.d.ts +13 -0
  24. package/dist/common/turn-failure.js +18 -0
  25. package/dist/index.js +5 -7
  26. package/dist/order/index.d.ts +2 -0
  27. package/dist/order/index.js +2 -0
  28. package/dist/router/card-helpers.js +23 -42
  29. package/dist/router/config/codex.js +2 -3
  30. package/dist/router/config/common/model-patch.d.ts +8 -0
  31. package/dist/router/config/common/model-patch.js +13 -0
  32. package/dist/router/config/common/render.js +30 -77
  33. package/dist/router/config/dsh.js +2 -1
  34. package/dist/router/config/kimi.js +3 -3
  35. package/dist/router/index.d.ts +0 -11
  36. package/dist/router/index.js +108 -349
  37. package/dist/router/order-exec-dispatch.js +2 -1
  38. package/dist/runner/common/defaults.d.ts +2 -0
  39. package/dist/runner/common/defaults.js +3 -0
  40. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  41. package/dist/runner/dsh/client.js +1 -2
  42. package/dist/runner/types.d.ts +19 -0
  43. package/dist/runner/types.js +9 -0
  44. package/dist/session/claude/session-index.d.ts +0 -4
  45. package/dist/session/claude/session-index.js +0 -8
  46. package/dist/session/dsh/sessions.js +57 -2
  47. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
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
- import { truncateUtf8, truncateMarkdownTables, CARD_BUDGET_BYTES } from './text-truncate.js';
4
+ import { truncateMarkdownTables, CARD_BUDGET_BYTES } from './text-truncate.js';
5
+ import { truncateUtf8 } from '../common/truncate.js';
5
6
  import { countCardElements, MAX_CARD_ELEMENTS } from './card-budget.js';
6
- import { truncate } from '../common/truncate.js';
7
+ import { displayWidth } from '../common/text-width.js';
8
+ import { isContentFilterFailure } from '../common/turn-failure.js';
7
9
  import { formatClock, formatTimestamp } from './time.js';
8
10
  import { formatUsageStats } from '../router/usage-format.js';
9
11
  import { renderApprovalArea } from './approval-render.js';
@@ -12,15 +14,6 @@ const TEXT_BYTES = 10_000;
12
14
  const DEGRADED_THINKING_KEEP = 2;
13
15
  const DEGRADED_TEXT_BYTES = 5_000;
14
16
  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
17
  const DEGRADED_TOOL_KEEP = 3; // Degraded: keep last 3 tool panels
25
18
  const EXTREME_TOOL_KEEP = 1; // Extreme: keep last 1 tool panel
26
19
  // --- Budget estimate (先估后建) ---
@@ -525,6 +518,9 @@ function buildExtremeFallbackElements(state, options) {
525
518
  * 状态标签(运行中/已完成…)不再重复:header 已经写了(`🔧 dsh · 调用工具`
526
519
  * / `✅ dsh · 已完成`),终态 summary 还有一次。
527
520
  *
521
+ * 终态也保留状态行:耗时定格,DSH 的步数/上下文投影展示最后一次快照
522
+ * (2026-10-21 用户要求)。summary 的最终 usage 与状态行并存。
523
+ *
528
524
  * 导出供窄屏守卫测试断言分段宽度(与 `estimateCardBytes` 同一惯例)。
529
525
  */
530
526
  export function statusLine(state) {
@@ -536,10 +532,11 @@ export function statusLine(state) {
536
532
  const elapsed = elapsedLabel(state);
537
533
  if (elapsed)
538
534
  parts.push(`⏱ ${elapsed}`);
539
- // 运行期投影只在 running/finalizing 展示:终态 summary 有权威统计,同一张卡
540
- // 上出现两次只会互相矛盾。
535
+ // 运行期投影在终态也保留(2026-10-21 用户要求):DSH 卡片完成时,耗时/步数/
536
+ // 上下文百分比是这一跑留下的最后快照,撤掉后用户只能从 summary 的累计口径
537
+ // 重新推算。summary 仍展示最终 usage,两者口径不同、并存不冲突。
541
538
  const live = state.liveStatus;
542
- if (live && (state.terminal === 'running' || state.terminal === 'finalizing')) {
539
+ if (live) {
543
540
  if (live.steps !== undefined)
544
541
  parts.push(`🧰 ${live.steps} 步`);
545
542
  if (live.contextUsed !== undefined &&
@@ -909,7 +906,16 @@ function buildSummaryContent(state, options = {}) {
909
906
  return elements;
910
907
  }
911
908
  if (state.terminal === 'error') {
912
- elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
909
+ // 服务方内容审核截断(多为误报)不是「程序出错」:照抄英文内部报文会让
910
+ // 用户以为代码或配置坏了。文案说清三件事——谁拦的、改动还在、下一步。
911
+ if (isContentFilterFailure(state.errorMsg)) {
912
+ elements.push(markdownDiv('⚠️ **服务方内容审核拦截**\n\n' +
913
+ '模型输出被上游安全策略截断(`content_filter`),多数是误报,不是代码或配置问题。\n' +
914
+ '本回合已中止;已写入磁盘的改动都还在。重试仍被拦截时,可换模型或稍后再试。'));
915
+ }
916
+ else {
917
+ elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
918
+ }
913
919
  }
914
920
  else if (state.terminal === 'interrupted') {
915
921
  // 审批超时(approval_expired 已标记)是「无人响应被自动取消」,不是用户
@@ -968,7 +974,55 @@ function buildSummaryContent(state, options = {}) {
968
974
  /** 终态卡最多列几个后台任务(多出来的只报数)。 */
969
975
  const MAX_BACKGROUND_JOB_LINES = 3;
970
976
  /**
971
- * 后台任务小节:一行标题 + 每个任务一行(标签 + 停止按钮)。
977
+ * 命令在手机上可直接内联的显示宽度上限(半角单位,CJK/emoji 记 2)。
978
+ *
979
+ * 手机内容区约 320–360px,36 个半角单位约一行;再长会折成"每个词一行"。
980
+ * 超过这个宽度改用代码块整段展示(完整、可读),而不是截断。
981
+ */
982
+ const BACKGROUND_JOB_INLINE_MAX = 36;
983
+ /**
984
+ * 代码块再长到这个程度才折叠:约 180 个半角宽度或 6 行。
985
+ *
986
+ * 低于阈值直接展示完整命令(用户一眼能看到,不用点);只有确实很长的
987
+ * 脚本/链式命令才收进折叠面板,避免一张卡被一条命令占满。
988
+ */
989
+ const BACKGROUND_JOB_FOLD_DISPLAY_MAX = 180;
990
+ const BACKGROUND_JOB_FOLD_LINES = 6;
991
+ /**
992
+ * 任务状态标签:目前 DSH 只会给 running/stopping,其他值按运行中处理。
993
+ */
994
+ function backgroundJobStatusLabel(status) {
995
+ return status === 'stopping' ? '停止中' : '运行中';
996
+ }
997
+ /**
998
+ * 生成能包住任意反引号串的代码围栏。命令里的 shell 反引号不能替换成 `'`
999
+ * (那是改内容),只能选比内容里最长反引号串更长的围栏。
1000
+ */
1001
+ function codeFenceFor(content) {
1002
+ let longest = 0;
1003
+ let run = 0;
1004
+ for (const ch of content) {
1005
+ if (ch === '`') {
1006
+ run += 1;
1007
+ longest = Math.max(longest, run);
1008
+ }
1009
+ else {
1010
+ run = 0;
1011
+ }
1012
+ }
1013
+ return '`'.repeat(Math.max(3, longest + 1));
1014
+ }
1015
+ /**
1016
+ * 后台任务小节:命令独占一行,停止按钮单独一行。
1017
+ *
1018
+ * 布局约束(2026-10-21 手机端反馈):
1019
+ * - 命令 + 按钮不再并排(grow + auto):窄屏下 auto 列会被挤成「🟥…」,
1020
+ * 命令也被截断。按钮现在独占一个单列 column_set,宽度由按钮自身决定。
1021
+ * - 命令不 truncate:短命令/单行直接内联;中等长度的多行命令用代码块
1022
+ * 整段展示;只有超长命令(> 180 显示宽度或 > 6 行)才进折叠面板。
1023
+ * 面板折叠只影响视觉,JSON 里始终带全文。
1024
+ * - 含反引号的命令不走行内代码(无法安全包含反引号),改走代码块;
1025
+ * 围栏长度按内容自动加长,命令原文不改写。
972
1026
  *
973
1027
  * 按钮 payload 只带 runId/jobId:sessionId 与 cwd 由 bridge 的快照解析,卡片
974
1028
  * 上少放一个可变字段就少一处不一致。
@@ -978,20 +1032,38 @@ function buildBackgroundJobsSection(state) {
978
1032
  if (jobs.length === 0)
979
1033
  return [];
980
1034
  const elements = [
981
- markdownDiv(`🧵 **后台任务** ${jobs.length} 个仍在运行(本轮结束后继续)`),
1035
+ markdownDiv(`🧵 **后台任务** ${jobs.length} 个仍在运行`),
1036
+ markdownDiv('_本轮结束后继续,可逐个停止_', 'notation'),
982
1037
  ];
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);
1038
+ jobs.slice(0, MAX_BACKGROUND_JOB_LINES).forEach((job, index) => {
1039
+ const label = job.label.trim();
1040
+ const lineCount = label === '' ? 0 : label.split(/\r?\n/).length;
1041
+ const inline = label.length > 0 &&
1042
+ lineCount === 1 &&
1043
+ !label.includes('`') &&
1044
+ displayWidth(label) <= BACKGROUND_JOB_INLINE_MAX;
1045
+ if (inline) {
1046
+ elements.push(markdownDiv(`· \`${label}\``));
1047
+ }
1048
+ else {
1049
+ // 多行/超宽/含反引号:代码块保留完整命令;只有超长才折叠。
1050
+ const fence = codeFenceFor(label);
1051
+ const content = `${fence}\n${label}\n${fence}`;
1052
+ const fold = lineCount > BACKGROUND_JOB_FOLD_LINES ||
1053
+ displayWidth(label) > BACKGROUND_JOB_FOLD_DISPLAY_MAX;
1054
+ elements.push(fold
1055
+ ? collapsibleMarkdownPanel({
1056
+ title: `🧵 任务 ${index + 1} · ${backgroundJobStatusLabel(job.status)} · 点击展开完整命令`,
1057
+ expanded: false,
1058
+ border: 'grey',
1059
+ content,
1060
+ textSize: 'notation',
1061
+ })
1062
+ : markdownDiv(content));
1063
+ }
987
1064
  elements.push({
988
1065
  tag: 'column_set',
989
1066
  columns: [
990
- {
991
- tag: 'column',
992
- width: 'grow',
993
- elements: [markdownDiv(`· \`${label}\``, 'notation')],
994
- },
995
1067
  {
996
1068
  tag: 'column',
997
1069
  width: 'auto',
@@ -1001,7 +1073,7 @@ function buildBackgroundJobsSection(state) {
1001
1073
  },
1002
1074
  ],
1003
1075
  });
1004
- }
1076
+ });
1005
1077
  if (jobs.length > MAX_BACKGROUND_JOB_LINES) {
1006
1078
  elements.push(markdownDiv(`_…另有 ${jobs.length - MAX_BACKGROUND_JOB_LINES} 个任务_`));
1007
1079
  }
@@ -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` (shared with session readers to avoid a
6
- * card→session layer inversion); this module re-exports them and adds the
7
- * card-specific markdown-table helpers.
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` (shared with session readers to avoid a
6
- * card→session layer inversion); this module re-exports them and adds the
7
- * card-specific markdown-table helpers.
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
  /**
@@ -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 'Bash':
166
- case 'bash':
167
- case 'pwsh':
163
+ switch (toolKindOf(tool.name)) {
164
+ case 'shell':
168
165
  return renderBashInput(str);
169
- case 'Read':
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 'WebFetch':
188
- case 'web_fetch':
174
+ case 'webfetch':
189
175
  return renderWebFetchInput(str);
190
- case 'WebSearch':
176
+ case 'websearch':
191
177
  return renderWebSearchInput(str);
192
- case 'web_search':
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
- const HEADER_SUMMARY_MAX = 80;
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 'Bash':
70
- case 'bash':
71
- case 'pwsh':
95
+ switch (toolKindOf(name)) {
96
+ case 'shell':
72
97
  return pick('command');
73
- case 'Read':
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 'WebFetch':
95
- case 'web_fetch':
109
+ case 'webfetch':
96
110
  return pick('url');
97
- case 'WebSearch':
98
- case 'web_search':
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
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { parseCliArgs, resolveConfigDir, setConfigDir, printHelp, printVersion,
5
5
  import { FeishuConnector } from './connector/index.js';
6
6
  import { ClaudeRunner, CodexAppServerRunner, OpencodeAcpRunner, PiRpcRunner, KimiAcpRunner, DshRunner, } from './runner/index.js';
7
7
  import { AgentRegistry } from './runner/registry.js';
8
+ import { AGENT_DISPLAY_NAMES, AGENT_KINDS } from './runner/types.js';
8
9
  import { probeAllAgents } from './runner/probe.js';
9
10
  import { warmCodexCatalogCache } from './config/codex-config.js';
10
11
  import { SessionReaderRegistry, SessionStore } from './session/index.js';
@@ -316,13 +317,10 @@ function initializeRunner(config, configDir, cliArgs) {
316
317
  getLogger().warn(`[dsh] startup probe failed: ${error.message}`);
317
318
  });
318
319
  }
319
- // 2026-07-17: 注册所有 agent 的显示名,实现单点真相
320
- agentRegistry.registerDisplayName('claude', 'Claude');
321
- agentRegistry.registerDisplayName('codex', 'Codex');
322
- agentRegistry.registerDisplayName('opencode', 'Opencode');
323
- agentRegistry.registerDisplayName('pi', 'Pi');
324
- agentRegistry.registerDisplayName('kimi', 'Kimi');
325
- agentRegistry.registerDisplayName('dsh', 'DSH');
320
+ // 显示名单源在 runner/types.ts 的 AGENT_DISPLAY_NAMES(启动注册与回退共用)
321
+ for (const kind of AGENT_KINDS) {
322
+ agentRegistry.registerDisplayName(kind, AGENT_DISPLAY_NAMES[kind]);
323
+ }
326
324
  // 设置全局 registry,供 agentDisplayName 等全局函数使用
327
325
  AgentRegistry.setGlobalInstance(agentRegistry);
328
326
  // Probe agent CLI availability (fire-and-forget, populates cache for /config card).
@@ -1,3 +1,5 @@
1
+ /** 指令已被删除/不存在时的统一用户提示。 */
2
+ export declare const ORDER_NOT_FOUND_TEXT = "\u6307\u4EE4\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664";
1
3
  export interface OrderEntry {
2
4
  id: string;
3
5
  text: string;
@@ -2,6 +2,8 @@ import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import { atomicWriteJson } from '../persistence/atomic-write.js';
4
4
  import { loadJsonFile } from '../persistence/load-json-file.js';
5
+ /** 指令已被删除/不存在时的统一用户提示。 */
6
+ export const ORDER_NOT_FOUND_TEXT = '指令不存在或已被删除';
5
7
  const MAX_TEXT_LENGTH = 200;
6
8
  const MAX_ORDERS = 50;
7
9
  /** 别名名称规则:`[A-Za-z_][A-Za-z0-9_]*`,≤20 字符,不能数字开头。 */
@@ -6,7 +6,7 @@
6
6
  * specific builders (e.g. `sessionEventPanel`) so `router/index.ts` can fold
7
7
  * long session histories into collapsible panels without duplicating logic.
8
8
  */
9
- import { agentDisplayName } from '../card/card-shared.js';
9
+ import { agentDisplayName, cardButton } from '../card/card-shared.js';
10
10
  import { collapsibleMarkdownPanel, markdownDiv } from '../card/collapsible.js';
11
11
  import { formatMonthDayClock } from '../card/time.js';
12
12
  import { displayWidth, truncateMiddleToDisplayWidth } from '../common/text-width.js';
@@ -142,18 +142,7 @@ export function paginationBar(opts) {
142
142
  width: 'auto',
143
143
  vertical_align: 'center',
144
144
  elements: [
145
- {
146
- tag: 'button',
147
- text: { tag: 'plain_text', content: prevText },
148
- type: 'default',
149
- size: 'small',
150
- behaviors: [
151
- {
152
- type: 'callback',
153
- value: { cmd: opts.cmd, offset: opts.offset - opts.pageSize, ...opts.extra },
154
- },
155
- ],
156
- },
145
+ cardButton(prevText, { cmd: opts.cmd, offset: opts.offset - opts.pageSize, ...opts.extra }, { type: 'default', size: 'small' }),
157
146
  ],
158
147
  });
159
148
  }
@@ -185,18 +174,7 @@ export function paginationBar(opts) {
185
174
  width: 'auto',
186
175
  vertical_align: 'center',
187
176
  elements: [
188
- {
189
- tag: 'button',
190
- text: { tag: 'plain_text', content: nextText },
191
- type: 'default',
192
- size: 'small',
193
- behaviors: [
194
- {
195
- type: 'callback',
196
- value: { cmd: opts.cmd, offset: opts.offset + opts.pageSize, ...opts.extra },
197
- },
198
- ],
199
- },
177
+ cardButton(nextText, { cmd: opts.cmd, offset: opts.offset + opts.pageSize, ...opts.extra }, { type: 'default', size: 'small' }),
200
178
  ],
201
179
  });
202
180
  }
@@ -264,18 +242,7 @@ export function searchBar(opts) {
264
242
  width: 'auto',
265
243
  vertical_align: 'center',
266
244
  elements: [
267
- {
268
- tag: 'button',
269
- text: { tag: 'plain_text', content: '清除筛选' },
270
- type: 'default',
271
- size: 'small',
272
- behaviors: [
273
- {
274
- type: 'callback',
275
- value: { cmd: opts.cmd, clearQuery: true, ...opts.extra },
276
- },
277
- ],
278
- },
245
+ cardButton('清除筛选', { cmd: opts.cmd, clearQuery: true, ...opts.extra }, { type: 'default', size: 'small' }),
279
246
  ],
280
247
  });
281
248
  }
@@ -339,15 +306,29 @@ export function buildSessionHistoryCard(state, opts) {
339
306
  const { displayTitle, aiTitle, recap, events, usage } = state;
340
307
  let header = opts.headerText;
341
308
  const sections = [];
309
+ const preview = (text) => (text.length > 200 ? text.slice(0, 197) + '...' : text);
342
310
  // Reader 层不再截断 displayTitle/summary;统一在消费侧这里截到 200 字符。
311
+ //
312
+ // `aiTitle` 与 `displayTitle` 是两种东西:前者是 agent 自己起的会话标题
313
+ // (DSH 的 projections.title / claude 的 ai-title),后者是最近一条用户输入。
314
+ // claude 的 displayTitle 本身就取 aiTitle,两者相等 → 保持旧的「AI 标题」
315
+ // 单段文案;DSH 两者都有(标题 + 末轮 prompt)→ 各占一段,别把用户那句话
316
+ // 挂在「AI 标题」下面(2026-10-21)。
343
317
  if (displayTitle) {
344
- const label = aiTitle ? 'AI 标题' : '最近输入';
345
- const preview = displayTitle.length > 200 ? displayTitle.slice(0, 197) + '...' : displayTitle;
346
- sections.push(`🏷️ **${label}**\n${preview}`);
318
+ if (aiTitle && aiTitle !== displayTitle) {
319
+ sections.push(`🏷️ **标题**\n${preview(aiTitle)}`);
320
+ sections.push(`✏️ **最近输入**\n${preview(displayTitle)}`);
321
+ }
322
+ else {
323
+ const label = aiTitle ? 'AI 标题' : '最近输入';
324
+ sections.push(`🏷️ **${label}**\n${preview(displayTitle)}`);
325
+ }
326
+ }
327
+ else if (aiTitle) {
328
+ sections.push(`🏷️ **AI 标题**\n${preview(aiTitle)}`);
347
329
  }
348
330
  if (recap) {
349
- const recapPreview = recap.length > 200 ? recap.slice(0, 197) + '...' : recap;
350
- sections.push(`📝 **Recap**\n${recapPreview}`);
331
+ sections.push(`📝 **Recap**\n${preview(recap)}`);
351
332
  }
352
333
  if (sections.length > 0) {
353
334
  header += '\n\n' + sections.join('\n\n──\n\n');
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { loadCodexConfig, getReasoningEffortOptions, getDefaultReasoningEffort, } from '../../config/codex-config.js';
5
5
  import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
6
- import { resetModelPatch } from './common/model-patch.js';
6
+ import { pickMiddleValue, resetModelPatch } from './common/model-patch.js';
7
7
  export class CodexConfigBuilder {
8
8
  buildFields(displayConfig) {
9
9
  const fields = [];
@@ -116,8 +116,7 @@ export class CodexConfigBuilder {
116
116
  const isCurrentEffortValid = currentReasoningEffort && newModelSupportedEfforts.includes(currentReasoningEffort);
117
117
  if (isCurrentEffortValid)
118
118
  return null;
119
- const middle = newModelSupportedEfforts[Math.floor((newModelSupportedEfforts.length - 1) / 2)];
120
- const newEffort = middle ?? getDefaultReasoningEffort(newModel);
119
+ const newEffort = pickMiddleValue(newModelSupportedEfforts) ?? getDefaultReasoningEffort(newModel);
121
120
  return { key: 'agents.codex.reasoningEffort', value: newEffort };
122
121
  }
123
122
  }
@@ -19,3 +19,11 @@ export declare function resetModelPatch(modelKey: string, currentModel: string |
19
19
  key: string;
20
20
  value: unknown;
21
21
  } | null;
22
+ /**
23
+ * 取档位列表的中位值(切换模型后当前档位不受支持时的兜底)。
24
+ *
25
+ * 偶数长度的中位口径由调用方显式选择,避免各 builder 各写一套公式后漂移:
26
+ * - `lower`:`(len-1)/2`,kimi/codex 既有口径;
27
+ * - `upper`:`len/2`,DSH 需要偏高的一档(off/low → low)。
28
+ */
29
+ export declare function pickMiddleValue<T>(values: readonly T[], bias?: 'lower' | 'upper'): T | undefined;
@@ -22,4 +22,17 @@ export function resetModelPatch(modelKey, currentModel, newModelOptions) {
22
22
  }
23
23
  return null;
24
24
  }
25
+ /**
26
+ * 取档位列表的中位值(切换模型后当前档位不受支持时的兜底)。
27
+ *
28
+ * 偶数长度的中位口径由调用方显式选择,避免各 builder 各写一套公式后漂移:
29
+ * - `lower`:`(len-1)/2`,kimi/codex 既有口径;
30
+ * - `upper`:`len/2`,DSH 需要偏高的一档(off/low → low)。
31
+ */
32
+ export function pickMiddleValue(values, bias = 'lower') {
33
+ if (values.length === 0)
34
+ return undefined;
35
+ const index = bias === 'upper' ? Math.floor(values.length / 2) : Math.floor((values.length - 1) / 2);
36
+ return values[index];
37
+ }
25
38
  //# sourceMappingURL=model-patch.js.map