lark-remote 0.5.6 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +30 -6
  2. package/README.en.md +2 -2
  3. package/README.md +2 -2
  4. package/dist/bridge/index.d.ts +48 -1
  5. package/dist/bridge/index.js +86 -14
  6. package/dist/card/card-budget.d.ts +37 -0
  7. package/dist/card/card-budget.js +128 -5
  8. package/dist/card/run-renderer.d.ts +24 -0
  9. package/dist/card/run-renderer.js +256 -60
  10. package/dist/card/run-state.d.ts +26 -0
  11. package/dist/card/run-state.js +45 -0
  12. package/dist/card/time.d.ts +14 -0
  13. package/dist/card/time.js +30 -0
  14. package/dist/card/tool-render.d.ts +15 -2
  15. package/dist/card/tool-render.js +40 -66
  16. package/dist/common/text-width.d.ts +24 -0
  17. package/dist/common/text-width.js +93 -0
  18. package/dist/common/tool-summary.d.ts +11 -0
  19. package/dist/common/tool-summary.js +153 -0
  20. package/dist/config/index.d.ts +1 -1
  21. package/dist/config/index.js +1 -1
  22. package/dist/connector/index.js +1 -1
  23. package/dist/index.js +61 -6
  24. package/dist/platform/command.js +1 -1
  25. package/dist/platform/shell.d.ts +2 -2
  26. package/dist/platform/spawn.d.ts +1 -1
  27. package/dist/platform/spawn.js +2 -2
  28. package/dist/platform/types.d.ts +1 -1
  29. package/dist/router/card-helpers.d.ts +18 -16
  30. package/dist/router/card-helpers.js +65 -4
  31. package/dist/router/index.d.ts +9 -0
  32. package/dist/router/index.js +57 -7
  33. package/dist/runner/claude/session.d.ts +1 -1
  34. package/dist/runner/claude/session.js +3 -3
  35. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  36. package/dist/runner/common/jsonrpc/transport.js +3 -3
  37. package/dist/runner/common/spawning-runner.d.ts +1 -1
  38. package/dist/runner/common/spawning-runner.js +2 -2
  39. package/dist/runner/dsh/auth.js +2 -1
  40. package/dist/runner/dsh/client.d.ts +100 -3
  41. package/dist/runner/dsh/client.js +289 -4
  42. package/dist/runner/dsh/exit-cancel.d.ts +32 -0
  43. package/dist/runner/dsh/exit-cancel.js +72 -0
  44. package/dist/runner/dsh/inflight.d.ts +62 -0
  45. package/dist/runner/dsh/inflight.js +140 -0
  46. package/dist/runner/dsh/runner.d.ts +90 -4
  47. package/dist/runner/dsh/runner.js +436 -18
  48. package/dist/runner/dsh/translator.d.ts +1 -1
  49. package/dist/runner/dsh/translator.js +103 -1
  50. package/dist/runner/dsh/types.d.ts +60 -0
  51. package/dist/runner/types.d.ts +76 -2
  52. package/dist/session/claude/sessions.js +2 -2
  53. package/dist/session/dsh/sessions.js +100 -15
  54. package/dist/update/install.js +1 -1
  55. package/package.json +1 -1
@@ -10,7 +10,21 @@
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';
14
+ import { truncateMiddleToDisplayWidth } from '../common/text-width.js';
15
+ // 这两个宽度工具原本住在这里;resume 卡片也要用,已下沉到 common。
16
+ export { displayWidth, truncateMiddleToDisplayWidth, truncateToDisplayWidth, } from '../common/text-width.js';
13
17
  const HEADER_SUMMARY_MAX = 80;
18
+ /**
19
+ * 面板标题的显示宽度上限(半角单位,CJK/emoji 记 2)。
20
+ *
21
+ * 手机(~390px 内容宽)上折叠面板的标题只有**一行**可用:减去图标与内边距约
22
+ * 320px,按 ~7.5px/半角算 ≈ 42 个半角宽度(iPhone SE 更窄,超出时按 `·`/空格
23
+ * 折行仍可读,但不能整段换行)。超过就折行,断点常常落在 URL 或命令
24
+ * 中间(2026-10-19 截图反馈)。完整参数在展开后的正文里,标题只负责"哪个工具、
25
+ * 动了什么",并带上 `HH:mm` 时间戳(一起计入预算)。
26
+ */
27
+ export const HEADER_DISPLAY_MAX = 42;
14
28
  const BODY_FIELD_MAX = 600;
15
29
  const OUTPUT_MAX = 1200;
16
30
  /**
@@ -27,11 +41,11 @@ const BODY_TOTAL_MAX = 2500;
27
41
  * `✅ **Read** — /repo/a.ts`
28
42
  * `❌ **Grep** — pattern in path`
29
43
  */
30
- export function toolHeaderText(tool) {
44
+ export function toolHeaderText(tool, suffix = '') {
31
45
  const icon = tool.status === 'ok' ? '✅' : tool.status === 'error' ? '❌' : '⏳';
32
46
  // use the cached parsed record when available (avoids per-render parse).
33
47
  const input = tool.parsedInput !== undefined ? tool.parsedInput : tool.input;
34
- const summary = summarizeInput(tool.name, input);
48
+ const summary = summarizeToolInput(tool.name, input);
35
49
  // 信息保真 ACP kind 映射后 name 与原始 title 不同时,title 以
36
50
  // ` — {summary}` 追加在标题末尾(截断沿用 HEADER_SUMMARY_MAX)。
37
51
  // 用 truncate(suffix 为空)而非裸 slice:裸 slice 按 UTF-16 码元切,
@@ -44,9 +58,14 @@ export function toolHeaderText(tool) {
44
58
  // `✅ **Bash** — ls` instead of `✅ **Bash** — ls — ls`.
45
59
  const parts = [summary, blockSummary].filter(Boolean);
46
60
  const uniqueParts = parts.filter((part, i) => parts.indexOf(part) === i);
47
- return uniqueParts.length > 0
61
+ const title = uniqueParts.length > 0
48
62
  ? `${icon} **${tool.name}** — ${uniqueParts.join(' — ')}`
49
63
  : `${icon} **${tool.name}**`;
64
+ // 折叠面板的标题必须能在一行内放下(手机窄屏):超宽就按显示宽度截断,
65
+ // 完整值仍在展开后的正文里。`suffix`(时间戳)一起计入预算,否则"标题压到
66
+ // 60 宽 + 18 宽时间戳"在手机上照样折行。
67
+ // 中间省略:路径/URL 的结尾(文件名)比开头更有信息量。
68
+ return truncateMiddleToDisplayWidth(`${title}${suffix}`, HEADER_DISPLAY_MAX);
50
69
  }
51
70
  /**
52
71
  * Structured body markdown for a tool call. Renders input fields by tool name
@@ -56,13 +75,14 @@ export function toolHeaderText(tool) {
56
75
  * for unknown tools: `default` returns '' and unknown tools only surface their
57
76
  * args through the panel header's `summarizeInput` fallback.
58
77
  */
59
- export function toolBodyMd(tool) {
78
+ export function toolBodyMd(tool, opts = {}) {
60
79
  const parts = [];
61
80
  const inputMd = renderInput(tool);
62
81
  if (inputMd)
63
82
  parts.push(inputMd);
64
83
  if (tool.output) {
65
- const truncated = truncate(tool.output, OUTPUT_MAX);
84
+ const outputMax = Math.min(opts.outputMax ?? OUTPUT_MAX, OUTPUT_MAX);
85
+ const truncated = truncate(tool.output, outputMax);
66
86
  if (tool.status === 'error') {
67
87
  parts.push(`**Error**\n\`\`\`\n${truncated}\n\`\`\``);
68
88
  }
@@ -79,51 +99,6 @@ export function toolBodyMd(tool) {
79
99
  // 同上:走 truncate 保证切点不落在代理对中间(末尾 '…' 由 truncate 附加以对齐预算)。
80
100
  return `${truncate(body, BODY_TOTAL_MAX)}\n\n_(body 已截断,完整内容查日志)_`;
81
101
  }
82
- function summarizeInput(name, input) {
83
- const rec = asRecord(input);
84
- if (!rec)
85
- return '';
86
- const pick = (key, max = HEADER_SUMMARY_MAX) => {
87
- const v = rec[key];
88
- if (typeof v !== 'string')
89
- return '';
90
- const oneLine = v.replace(/\s+/g, ' ').trim();
91
- return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
92
- };
93
- switch (name) {
94
- case 'Bash':
95
- case 'bash':
96
- return pick('command');
97
- case 'Read':
98
- case 'Edit':
99
- case 'Write':
100
- case 'NotebookEdit':
101
- case 'read':
102
- case 'edit':
103
- case 'write':
104
- return pick('file_path') || pick('path');
105
- case 'Grep':
106
- case 'grep': {
107
- const pat = pick('pattern', 40);
108
- const p = pick('path', 30);
109
- return p ? `${pat} in ${p}` : pat;
110
- }
111
- case 'Glob':
112
- case 'find':
113
- return pick('pattern');
114
- case 'ls':
115
- return pick('path');
116
- case 'WebFetch':
117
- return pick('url');
118
- case 'WebSearch':
119
- return pick('query', 60);
120
- case 'Agent':
121
- case 'Task':
122
- return pick('description') || pick('subagent_type');
123
- default:
124
- return pick('command') || pick('file_path') || pick('path') || pick('query');
125
- }
126
- }
127
102
  // --- Named render bodies (shared by tool-name cases and toolHint branches) ---
128
103
  function renderBashInput(str) {
129
104
  const cmd = str('command');
@@ -155,6 +130,15 @@ function renderWebFetchInput(str) {
155
130
  function renderWebSearchInput(str) {
156
131
  return str('query') ? `**Query** \`${truncate(str('query'), BODY_FIELD_MAX)}\`` : '';
157
132
  }
133
+ /** DSH `web_search` 的批量查询参数(`queries: string[]`)。 */
134
+ function renderWebQueriesInput(queries) {
135
+ if (!Array.isArray(queries))
136
+ return '';
137
+ const items = queries.filter((q) => typeof q === 'string' && q.trim().length > 0);
138
+ if (items.length === 0)
139
+ return '';
140
+ return `**Query** \`${truncate(items.join('` / `'), BODY_FIELD_MAX)}\``;
141
+ }
158
142
  function renderInput(tool) {
159
143
  // use the cached parsed record when available (avoids per-render parse).
160
144
  const input = tool.parsedInput !== undefined ? tool.parsedInput : tool.input;
@@ -180,6 +164,7 @@ function renderInput(tool) {
180
164
  switch (tool.name) {
181
165
  case 'Bash':
182
166
  case 'bash':
167
+ case 'pwsh':
183
168
  return renderBashInput(str);
184
169
  case 'Read':
185
170
  case 'Edit':
@@ -188,37 +173,26 @@ function renderInput(tool) {
188
173
  case 'read':
189
174
  case 'edit':
190
175
  case 'write':
176
+ case 'read_image':
191
177
  return renderFileInput(str);
192
178
  case 'Grep':
193
179
  case 'grep':
194
180
  return renderGrepInput(str);
195
181
  case 'Glob':
182
+ case 'glob':
196
183
  case 'find':
197
184
  return renderGlobInput(str);
198
185
  case 'ls':
199
186
  return renderLsInput(str);
200
187
  case 'WebFetch':
188
+ case 'web_fetch':
201
189
  return renderWebFetchInput(str);
202
190
  case 'WebSearch':
203
191
  return renderWebSearchInput(str);
192
+ case 'web_search':
193
+ return renderWebQueriesInput(rec.queries) || renderWebSearchInput(str);
204
194
  default:
205
195
  return '';
206
196
  }
207
197
  }
208
- function asRecord(input) {
209
- if (!input)
210
- return null;
211
- if (typeof input === 'object')
212
- return input;
213
- if (typeof input === 'string') {
214
- try {
215
- const p = JSON.parse(input);
216
- return p && typeof p === 'object' ? p : null;
217
- }
218
- catch {
219
- return null;
220
- }
221
- }
222
- return null;
223
- }
224
198
  //# sourceMappingURL=tool-render.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 文本的**显示宽度**工具(半角 1 / 全角与 emoji 2)。
3
+ *
4
+ * 为什么单列一个模块:卡片标题、状态行、resume 卡片标题都要按"手机上能放几行"
5
+ * 来收敛,而卡片层(`src/card/`)与会话读取层(`src/session/`)都要用——放在
6
+ * 任何一侧都会让另一侧反向依赖。
7
+ */
8
+ /**
9
+ * 按**显示宽度**截断(半角 1 / 全角与 emoji 2)。
10
+ *
11
+ * markdown 的 `**` 与 emoji 都要按视觉宽度算:只按字符数截断时,一行 34 个
12
+ * CJK 就是 34 个字宽,手机上必然折行。
13
+ */
14
+ export declare function truncateToDisplayWidth(text: string, maxUnits: number): string;
15
+ /** 字符串的显示宽度(半角 1 / 全角与 emoji 2)。 */
16
+ export declare function displayWidth(text: string): number;
17
+ /**
18
+ * 中间省略截断:保留开头与结尾,只在中间打 `…`。
19
+ *
20
+ * 标题里的路径/URL 必须保留**结尾**才有信息量(`~/code/notes-app/.../notes/
21
+ * 2026-10-19-vision-remaining.md` 里真正有用的是文件名),尾部省略会把文件名
22
+ * 整个切掉;纯尾部截断只适合"开头即结论"的句子(命令)。
23
+ */
24
+ export declare function truncateMiddleToDisplayWidth(text: string, maxUnits: number): string;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 文本的**显示宽度**工具(半角 1 / 全角与 emoji 2)。
3
+ *
4
+ * 为什么单列一个模块:卡片标题、状态行、resume 卡片标题都要按"手机上能放几行"
5
+ * 来收敛,而卡片层(`src/card/`)与会话读取层(`src/session/`)都要用——放在
6
+ * 任何一侧都会让另一侧反向依赖。
7
+ */
8
+ /**
9
+ * 按**显示宽度**截断(半角 1 / 全角与 emoji 2)。
10
+ *
11
+ * markdown 的 `**` 与 emoji 都要按视觉宽度算:只按字符数截断时,一行 34 个
12
+ * CJK 就是 34 个字宽,手机上必然折行。
13
+ */
14
+ export function truncateToDisplayWidth(text, maxUnits) {
15
+ if (displayWidth(text) <= maxUnits)
16
+ return text;
17
+ let used = 0;
18
+ let out = '';
19
+ for (const ch of text) {
20
+ const w = charWidth(ch);
21
+ // 预留 1 个单位给省略号
22
+ if (used + w > maxUnits - 1)
23
+ break;
24
+ out += ch;
25
+ used += w;
26
+ }
27
+ return `${out}…`;
28
+ }
29
+ /** 字符串的显示宽度(半角 1 / 全角与 emoji 2)。 */
30
+ export function displayWidth(text) {
31
+ let total = 0;
32
+ for (const ch of text)
33
+ total += charWidth(ch);
34
+ return total;
35
+ }
36
+ function charWidth(ch) {
37
+ const code = ch.codePointAt(0) ?? 0;
38
+ // CJK 统一表意文字、假名、全角标点、常见 emoji 都按两个半角算。
39
+ if (code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a))
40
+ return 2;
41
+ if (code >= 0x2e80 && code <= 0xa4cf)
42
+ return 2;
43
+ if (code >= 0xac00 && code <= 0xd7a3)
44
+ return 2;
45
+ if (code >= 0xf900 && code <= 0xfaff)
46
+ return 2;
47
+ if (code >= 0xfe30 && code <= 0xfe6f)
48
+ return 2;
49
+ if (code >= 0xff00 && code <= 0xff60)
50
+ return 2;
51
+ if (code >= 0xffe0 && code <= 0xffe6)
52
+ return 2;
53
+ if (code >= 0x1f300 && code <= 0x1faff)
54
+ return 2;
55
+ if (code >= 0x2600 && code <= 0x27bf)
56
+ return 2;
57
+ return 1;
58
+ }
59
+ /**
60
+ * 中间省略截断:保留开头与结尾,只在中间打 `…`。
61
+ *
62
+ * 标题里的路径/URL 必须保留**结尾**才有信息量(`~/code/notes-app/.../notes/
63
+ * 2026-10-19-vision-remaining.md` 里真正有用的是文件名),尾部省略会把文件名
64
+ * 整个切掉;纯尾部截断只适合"开头即结论"的句子(命令)。
65
+ */
66
+ export function truncateMiddleToDisplayWidth(text, maxUnits) {
67
+ if (displayWidth(text) <= maxUnits)
68
+ return text;
69
+ const budget = Math.max(1, maxUnits - 1); // 给省略号留 1 个单位
70
+ const headBudget = Math.ceil(budget / 2);
71
+ const tailBudget = budget - headBudget;
72
+ let head = '';
73
+ let used = 0;
74
+ for (const ch of text) {
75
+ const w = charWidth(ch);
76
+ if (used + w > headBudget)
77
+ break;
78
+ head += ch;
79
+ used += w;
80
+ }
81
+ let tail = '';
82
+ used = 0;
83
+ const chars = [...text];
84
+ for (let i = chars.length - 1; i >= 0; i--) {
85
+ const w = charWidth(chars[i]);
86
+ if (used + w > tailBudget)
87
+ break;
88
+ tail = chars[i] + tail;
89
+ used += w;
90
+ }
91
+ return `${head}…${tail}`;
92
+ }
93
+ //# sourceMappingURL=text-width.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 工具调用参数 → 一行人话摘要(`bash — npm test`、`read — src/a.ts`)。
3
+ *
4
+ * 为什么要抽到 common:run 卡的面板标题(`src/card/tool-render.ts`)和
5
+ * resume 卡的「🔧 工具调用」标题(`src/session/<agent>/sessions.ts`)都要同一套口径;
6
+ * 各写一份必然漂移(同一个 `glob` 在一处显示 pattern、另一处显示目录)。
7
+ *
8
+ * 只做"取哪个字段",不做 agent 分支:工具名与参数名的对应表就是这张 switch。
9
+ */
10
+ export declare function asRecord(input: unknown): Record<string, unknown> | null;
11
+ export declare function summarizeToolInput(name: string, input: unknown): string;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * 工具调用参数 → 一行人话摘要(`bash — npm test`、`read — src/a.ts`)。
3
+ *
4
+ * 为什么要抽到 common:run 卡的面板标题(`src/card/tool-render.ts`)和
5
+ * resume 卡的「🔧 工具调用」标题(`src/session/<agent>/sessions.ts`)都要同一套口径;
6
+ * 各写一份必然漂移(同一个 `glob` 在一处显示 pattern、另一处显示目录)。
7
+ *
8
+ * 只做"取哪个字段",不做 agent 分支:工具名与参数名的对应表就是这张 switch。
9
+ */
10
+ import { truncate } from './truncate.js';
11
+ const HEADER_SUMMARY_MAX = 80;
12
+ export function asRecord(input) {
13
+ if (!input)
14
+ return null;
15
+ if (typeof input === 'object')
16
+ return input;
17
+ if (typeof input === 'string') {
18
+ try {
19
+ const p = JSON.parse(input);
20
+ return p && typeof p === 'object' ? p : null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ export function summarizeToolInput(name, input) {
29
+ const rec = asRecord(input);
30
+ if (!rec)
31
+ return '';
32
+ const pick = (key, max = HEADER_SUMMARY_MAX) => {
33
+ const v = rec[key];
34
+ if (typeof v !== 'string')
35
+ return '';
36
+ const oneLine = v.replace(/\s+/g, ' ').trim();
37
+ return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
38
+ };
39
+ /** First non-empty of several keys(DSH 同名工具跨版本改过参数名)。 */
40
+ const pickAny = (keys, max = HEADER_SUMMARY_MAX) => {
41
+ for (const key of keys) {
42
+ const v = pick(key, max);
43
+ if (v)
44
+ return v;
45
+ }
46
+ return '';
47
+ };
48
+ /** 数组参数的第一个字符串项(DSH web_search 的 queries、present 的 files)。 */
49
+ const pickFirst = (key, max = HEADER_SUMMARY_MAX) => {
50
+ const v = rec[key];
51
+ if (typeof v === 'string')
52
+ return pick(key, max);
53
+ if (!Array.isArray(v))
54
+ return '';
55
+ for (const item of v) {
56
+ if (typeof item === 'string' && item.trim()) {
57
+ const oneLine = item.replace(/\s+/g, ' ').trim();
58
+ return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
59
+ }
60
+ if (item && typeof item === 'object') {
61
+ const path = item.path;
62
+ if (typeof path === 'string' && path.trim())
63
+ return path;
64
+ }
65
+ }
66
+ return '';
67
+ };
68
+ switch (name) {
69
+ case 'Bash':
70
+ case 'bash':
71
+ case 'pwsh':
72
+ 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':
81
+ return pick('file_path') || pick('path');
82
+ case 'Grep':
83
+ case 'grep': {
84
+ const pat = pick('pattern', 40);
85
+ const p = pick('path', 30);
86
+ return p ? `${pat} in ${p}` : pat;
87
+ }
88
+ case 'Glob':
89
+ case 'glob':
90
+ case 'find':
91
+ return pick('pattern');
92
+ case 'ls':
93
+ return pick('path');
94
+ case 'WebFetch':
95
+ case 'web_fetch':
96
+ return pick('url');
97
+ case 'WebSearch':
98
+ case 'web_search':
99
+ return pickFirst('queries', 60) || pick('query', 60);
100
+ case 'Agent':
101
+ case 'Task':
102
+ case 'subagent':
103
+ case 'workflow':
104
+ return pick('description') || pick('subagent_type');
105
+ case 'todo_write':
106
+ return summarizeTodos(rec);
107
+ case 'job_output':
108
+ case 'job_kill':
109
+ return pick('job_id');
110
+ case 'job_list':
111
+ return '(列出任务)';
112
+ case 'present':
113
+ return pickFirst('files');
114
+ case 'skill':
115
+ return pick('name');
116
+ case 'run_code':
117
+ return pick('description') || pick('language');
118
+ case 'terminal_send':
119
+ return pick('text');
120
+ case 'ask_user_question': {
121
+ const questions = rec.questions;
122
+ return Array.isArray(questions) ? `${questions.length} 个问题` : '';
123
+ }
124
+ case 'send_message':
125
+ case 'interrupt_agent':
126
+ return pick('description') || pick('agent_id');
127
+ default:
128
+ return pickAny(['command', 'file_path', 'path', 'query', 'url']);
129
+ }
130
+ }
131
+ /** todo_write 摘要:完成进度 + 当前 in_progress 项主题。 */
132
+ function summarizeTodos(rec) {
133
+ const todos = rec.todos;
134
+ if (!Array.isArray(todos))
135
+ return '';
136
+ let done = 0;
137
+ let active = '';
138
+ for (const item of todos) {
139
+ if (!item || typeof item !== 'object')
140
+ continue;
141
+ const todo = item;
142
+ if (todo.status === 'completed')
143
+ done += 1;
144
+ if (!active && todo.status === 'in_progress' && typeof todo.content === 'string') {
145
+ active = todo.content.replace(/\s+/g, ' ').trim();
146
+ }
147
+ }
148
+ const progress = `${done}/${todos.length}`;
149
+ if (!active)
150
+ return `已完成 ${progress}`;
151
+ return truncate(`已完成 ${progress} · ${active}`, HEADER_SUMMARY_MAX, { suffix: '…' });
152
+ }
153
+ //# sourceMappingURL=tool-summary.js.map
@@ -28,7 +28,7 @@ export declare const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES: 30;
28
28
  * 单文件上限默认 100MB:50MB 对视频偏小(2026-09-15 事故就是 mp4 被丢弃)。
29
29
  * 注意飞书侧 `type=file` 超过 100MB 必须用 Range 分片下载,而 SDK 的
30
30
  * `downloadResourceToFile` 不分片(必报 234037),所以 100 是实际可用上限
31
- * (inbound-message-matrix.md §4.4)。
31
+ * (inbound-message-matrix.md §4 第 4 条)。
32
32
  */
33
33
  export declare const DEFAULT_INBOUND_MEDIA_DIR_NAME = ".lark-remote-temp";
34
34
  export declare const DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB = 100;
@@ -56,7 +56,7 @@ export const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES = DEFAULTS.TURN_IDLE_TIMEOUT_MINU
56
56
  * 单文件上限默认 100MB:50MB 对视频偏小(2026-09-15 事故就是 mp4 被丢弃)。
57
57
  * 注意飞书侧 `type=file` 超过 100MB 必须用 Range 分片下载,而 SDK 的
58
58
  * `downloadResourceToFile` 不分片(必报 234037),所以 100 是实际可用上限
59
- * (inbound-message-matrix.md §4.4)。
59
+ * (inbound-message-matrix.md §4 第 4 条)。
60
60
  */
61
61
  export const DEFAULT_INBOUND_MEDIA_DIR_NAME = '.lark-remote-temp';
62
62
  export const DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB = 100;
@@ -424,7 +424,7 @@ export class FeishuConnector {
424
424
  kind: res.kind,
425
425
  fileName: res.fileName,
426
426
  // 飞书侧 >100MB 必须 Range 分片(SDK 不分片),到上限就是下不动:
427
- // 提示写清楚,避免用户以为"再放大上限就能存"(matrix §4.4/§2 #11)。
427
+ // 提示写清楚,避免用户以为"再放大上限就能存"(inbound-message-matrix.md §4 第 4 条)。
428
428
  reason: `超过 ${limitMb}MB 大小限制` +
429
429
  (limitMb >= 100 ? '(飞书 >100MB 的文件需分片下载,暂不支持)' : ''),
430
430
  });
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { SessionReaderRegistry, SessionStore } from './session/index.js';
11
11
  import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
12
12
  import { CommandRouter, isImmediateAction, DIRECT_RETURN_CMDS, SESSION_MUTATING_ACTION_CMDS, COMPACT_ACTION_CMDS, } from './router/index.js';
13
13
  import { DshDaemon } from './runner/dsh/daemon.js';
14
+ import { DshClient } from './runner/dsh/client.js';
15
+ import { DshInflightStore, dshInflightPath, reconcileInflightDshRuns, } from './runner/dsh/inflight.js';
14
16
  import { dispatchOrderExecForQueue } from './router/order-exec-dispatch.js';
15
17
  import { buildCardActionFullValue } from './router/card-action-payload.js';
16
18
  import { actionFeedbackText } from './router/card-action-feedback.js';
@@ -29,7 +31,7 @@ import { WorkspaceStore } from './workspace/index.js';
29
31
  import { InboundTurnAssembler } from './inbound/turn-assembler.js';
30
32
  import { InboundAuthorizer } from './inbound/authorize.js';
31
33
  import { stripPlaceholders } from './inbound/placeholder.js';
32
- import { buildSessionHistoryCard } from './router/card-helpers.js';
34
+ import { buildSessionHistoryCard, pickResumeCardEvents } from './router/card-helpers.js';
33
35
  import { newSessionButton, resumeCompactButton, agentDisplayName } from './card/card-shared.js';
34
36
  import path from 'node:path';
35
37
  import fs from 'node:fs';
@@ -43,16 +45,18 @@ function isStopCommand(text) {
43
45
  return t === '/stop' || t === '/t';
44
46
  }
45
47
  /** Send auto-resume card for a restored session */
46
- async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false) {
48
+ async function sendAutoResumeCard(connector, contact, cwd, session, state, compactSupported = false, stillRunning = false) {
47
49
  if (!contact.userId)
48
50
  return;
49
51
  const recipient = contact.chatId ?? contact.userId;
50
52
  if (!recipient)
51
53
  return;
52
- // Limit history events to prevent card size exceeded error (11310)
54
+ // Limit history events to prevent card size exceeded error (11310).
55
+ // 窗口按"最后 N 条"截,但强制保住最后一条用户消息与最后一条 assistant 正文,
56
+ // 否则工具往返会把它俩一起挤出卡(2026-10-19 恢复卡只剩 5 个 bash 面板)。
53
57
  const MAX_HISTORY_EVENTS = 5;
54
- const eventsToShow = state.events.slice(-MAX_HISTORY_EVENTS);
55
- const hiddenCount = state.events.length - MAX_HISTORY_EVENTS;
58
+ const eventsToShow = pickResumeCardEvents(state.events, MAX_HISTORY_EVENTS);
59
+ const hiddenCount = state.events.length - eventsToShow.length;
56
60
  // Action buttons: Compact (runCompact-capable runner) + new session.
57
61
  const agentKind = state.agentKind ?? 'claude';
58
62
  const actionButtons = [];
@@ -73,6 +77,7 @@ async function sendAutoResumeCard(connector, contact, cwd, session, state, compa
73
77
  headerText: `📂 \`${cwd}\`\n已恢复最近会话: **${session.sessionId}**`,
74
78
  title: `🔁 自动恢复会话 · ${agentDisplayName(agentKind)}`,
75
79
  hiddenCount,
80
+ stillRunning,
76
81
  actions: actionButtons,
77
82
  });
78
83
  await connector.sendWithRetry(recipient, { card });
@@ -300,6 +305,9 @@ function initializeRunner(config, configDir, cliArgs) {
300
305
  provider: dshConf?.provider,
301
306
  model: dshConf?.model,
302
307
  reasoningEffort: dshConf?.reasoningEffort,
308
+ // Durable markers for turns that outlive this process; reconciled on the
309
+ // next startup (see reconcileInflightDshRuns).
310
+ inflightPath: dshInflightPath(configDir),
303
311
  });
304
312
  });
305
313
  sessionReaderRegistry.register('dsh', dshSessionReader);
@@ -834,6 +842,33 @@ async function main() {
834
842
  })();
835
843
  }
836
844
  }
845
+ // 上次进程可能死在 DSH turn 中途:清掉残留的待执行 prompt,并对仍在跑的
846
+ // turn 提示 owner(不自动 cancel——它可能是 Web UI 的 turn)。清理不依赖是否
847
+ // 已绑定;没有收件人时只是不发提示。
848
+ const dshConf = getAgentConfig(config, 'dsh');
849
+ if (dshConf) {
850
+ const contact = startupContactStore.getContact();
851
+ const recipient = contact?.chatId ?? contact?.userId;
852
+ void reconcileInflightDshRuns({
853
+ store: new DshInflightStore(dshInflightPath(configDir)),
854
+ client: new DshClient(dshConf.host ?? 'http://127.0.0.1:3080', {
855
+ dshHome: dshConf.dshHome,
856
+ token: dshConf.token,
857
+ credentialsFile: dshConf.credentialsFile,
858
+ // Host may be down: bound the probe instead of waiting the default 60s.
859
+ requestTimeoutMs: 3_000,
860
+ }),
861
+ ...(recipient
862
+ ? {
863
+ notify: async (text) => {
864
+ await connector.sendWithRetry(recipient, { text });
865
+ },
866
+ }
867
+ : {}),
868
+ }).catch((err) => {
869
+ logger.warn(`[startup] dsh inflight reconcile failed: ${err.message}`);
870
+ });
871
+ }
837
872
  // Auto-restore: resume the persisted sessionId if available
838
873
  const restoredContact = startupContactStore.getContact();
839
874
  if (restoredContact?.userId) {
@@ -845,6 +880,26 @@ async function main() {
845
880
  const startupReader = sessionReaderRegistry.get(config.defaultAgent);
846
881
  sessionStore.setSessionIdAndCwd(restoredContact.userId, config.defaultAgent, sessionIdToResume, restoredCwd);
847
882
  const content = startupReader.readSessionContent(sessionIdToResume, restoredCwd);
883
+ // 该会话可能仍有在途 turn(上一个实例退出/崩溃后留在常驻 host 里)。
884
+ // 不取消、不接管,但必须在卡上说明,否则用户会把它当"卡住了"。
885
+ //
886
+ // 只有 DSH reader 的 isSessionActive() 是服务端的 running 事实。其他
887
+ // reader 的同名方法只表示"最近一段时间有写入",拿它当 turn 在跑会在
888
+ // 每次自动恢复时误报。
889
+ const stillRunning = config.defaultAgent === 'dsh'
890
+ ? (() => {
891
+ try {
892
+ return startupReader.isSessionActive(sessionIdToResume, restoredCwd);
893
+ }
894
+ catch (err) {
895
+ getLogger().warn(`[startup] isSessionActive failed: ${err.message}`);
896
+ return false;
897
+ }
898
+ })()
899
+ : false;
900
+ if (stillRunning) {
901
+ getLogger().warn(`[startup] session ${sessionIdToResume} still has a running turn; auto-resume card is a snapshot`);
902
+ }
848
903
  // 完整透传 usage(含 inputTokens/outputTokens/totalTokens),
849
904
  // 让 formatUsageStats 走真实值路径,与终态卡片//resume 卡片口径一致。
850
905
  const usage = content.usage;
@@ -862,7 +917,7 @@ async function main() {
862
917
  agentKind: config.defaultAgent,
863
918
  },
864
919
  // Compact 能力探测与 bridge 侧一致(runner 有 runCompact 才渲染)。
865
- bridge.hasRunCompact(restoredCwd, config.defaultAgent));
920
+ bridge.hasRunCompact(restoredCwd, config.defaultAgent), stillRunning);
866
921
  }
867
922
  catch (err) {
868
923
  getLogger().warn('[startup] sendAutoResumeCard failed:', err);
@@ -5,7 +5,7 @@
5
5
  * 自更新安装源检测 update/install.ts)。**执行**统一走 platform/spawn.ts 的
6
6
  * cross-spawn 包装(PATHEXT 解析 + .cmd 经 cmd.exe 受控执行),本模块不再
7
7
  * 解析 .cmd/.bat 垫片内容——垫片格式是 npm/pnpm 的上游实现细节(历史变过、
8
- * pnpm 生成形态不同),手写解析器等于耦合该细节(v2 §4.2)。
8
+ * pnpm 生成形态不同),手写解析器等于耦合该细节。
9
9
  *
10
10
  * posix:PATH 目录顺序 + X_OK(现状语义迁移)。
11
11
  * win32:PATH 目录顺序 × PATHEXT(.exe/.cmd/.bat) 解析,.cmd 命中即报告垫片
@@ -8,7 +8,7 @@
8
8
  * 由调用方转成明确错误卡(提示需要 Git Bash)。
9
9
  *
10
10
  * kimi ACP 的 terminal/create 固定 `bash -c`,因此 Git Bash 不是 `!` 的专属
11
- * 需求,而是 kimi agent 在 Windows 上可用的前置条件(§4.4)。
11
+ * 需求,而是 kimi agent 在 Windows 上可用的前置条件。
12
12
  */
13
13
  import type { ChildProcess, SpawnOptions } from 'node:child_process';
14
14
  import { type ResolveOptions, type LaunchSpec } from './command.js';
@@ -31,7 +31,7 @@ export interface ShellBackend {
31
31
  * - posix:原样透传(与迁移前 `spawnProcess(command, args, ...)` 逐字一致);
32
32
  * - win32:首 token 是 shell(bash/sh/zsh/dash)时按 Git Bash 解析,缺失则抛
33
33
  * {@link ShellUnavailableError}——kimi agent 在 Windows 上可用的前置条件
34
- * (§4.4)。其余命令交给 cross-spawn 做 PATH/PATHEXT 解析,不做命令翻译。
34
+ * 。其余命令交给 cross-spawn 做 PATH/PATHEXT 解析,不做命令翻译。
35
35
  */
36
36
  spawnArgv(file: string, args: readonly string[], opts?: {
37
37
  cwd?: string;
@@ -35,7 +35,7 @@ export declare function mergeProcessEnv(base?: NodeJS.ProcessEnv, overrides?: No
35
35
  */
36
36
  export declare function useDetachedProcessGroup(platform?: NodeJS.Platform): boolean;
37
37
  /**
38
- * win32 command-not-found 行嗅探(§4.4):命中即视为「命令缺失」失败而非
38
+ * win32 command-not-found 行嗅探:命中即视为「命令缺失」失败而非
39
39
  * 普通非零退出。platform 注入使其在任意宿主可测。
40
40
  */
41
41
  export declare function isWindowsCommandNotFoundLine(line: string, platform?: NodeJS.Platform): boolean;