mingdao-harness 0.2.3 → 0.2.5

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 (65) hide show
  1. package/docs/ROADMAP-NEXT.md +13 -13
  2. package/package.json +1 -1
  3. package/skills/release-checklist/SKILL.md +16 -3
  4. package/src/agent.js +102 -30
  5. package/src/audit.js +2 -2
  6. package/src/batch.js +74 -16
  7. package/src/cachestats.js +57 -11
  8. package/src/cli.js +29 -679
  9. package/src/commands/desktop.js +1 -1
  10. package/src/commands/key.js +1 -1
  11. package/src/commands/repl.js +711 -0
  12. package/src/commands/schedule.js +7 -7
  13. package/src/commands/skill.js +18 -16
  14. package/src/commands/sync.js +5 -5
  15. package/src/commands/update.js +22 -9
  16. package/src/commands/workspace.js +2 -2
  17. package/src/compact.js +6 -6
  18. package/src/config.js +7 -4
  19. package/src/context.js +11 -11
  20. package/src/cost-guard.js +15 -5
  21. package/src/credentials.js +6 -0
  22. package/src/hooks.js +11 -11
  23. package/src/mcp-presets.js +5 -5
  24. package/src/mcp.js +19 -19
  25. package/src/memory.js +16 -16
  26. package/src/model-discovery.js +16 -15
  27. package/src/models.js +4 -4
  28. package/src/notify.js +2 -2
  29. package/src/permissions.js +7 -3
  30. package/src/pricing.js +35 -1
  31. package/src/prompts.js +1 -1
  32. package/src/providers/index.js +7 -7
  33. package/src/providers/openai-compatible.js +8 -8
  34. package/src/routing.js +5 -5
  35. package/src/schedule.js +28 -28
  36. package/src/session-index.js +5 -1
  37. package/src/session.js +11 -2
  38. package/src/skill-lib.js +62 -3
  39. package/src/skill-registry.js +10 -9
  40. package/src/skills.js +10 -10
  41. package/src/sync-server.js +86 -3
  42. package/src/sync.js +53 -20
  43. package/src/tasks/worker.js +126 -0
  44. package/src/tasks.js +13 -13
  45. package/src/titles.js +5 -5
  46. package/src/tokenizer.js +22 -4
  47. package/src/tools/bash.js +12 -12
  48. package/src/tools/fs-tools.js +66 -9
  49. package/src/tools/index.js +77 -36
  50. package/src/ui.js +83 -6
  51. package/src/update.js +8 -8
  52. package/src/web/app.js +43 -0
  53. package/src/web/attachments.js +1 -1
  54. package/src/web/index.html +12 -0
  55. package/src/web/routes/api.js +38 -786
  56. package/src/web/routes/domains/config.js +280 -0
  57. package/src/web/routes/domains/misc.js +128 -0
  58. package/src/web/routes/domains/schedule.js +127 -0
  59. package/src/web/routes/domains/sessions.js +105 -0
  60. package/src/web/routes/domains/skills.js +79 -0
  61. package/src/web/routes/domains/sync.js +101 -0
  62. package/src/web/routes/domains/workspace.js +113 -0
  63. package/src/web/server.js +29 -14
  64. package/src/web/web-io.js +54 -12
  65. package/src/workspace.js +14 -14
package/src/tools/bash.js CHANGED
@@ -15,22 +15,22 @@ const MAX_TIMEOUT_SECONDS = 600;
15
15
  // 整体关闭(回到完全透传)。
16
16
  const SENSITIVE_ENV_PAIR = /(api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)/i;
17
17
  const SENSITIVE_ENV_SEGMENT = /(^|_)(token|secret|password|passwd|credential|authorization|auth)(_|$)/i;
18
- const isSensitiveEnv = (k) => SENSITIVE_ENV_PAIR.test(k) || SENSITIVE_ENV_SEGMENT.test(k);
18
+ const isSensitiveEnv = (/** @type {any} */ k) => SENSITIVE_ENV_PAIR.test(k) || SENSITIVE_ENV_SEGMENT.test(k);
19
19
 
20
- function buildChildEnv(ctx, filterSensitive) {
20
+ function buildChildEnv(/** @type {any} */ ctx, /** @type {any} */ filterSensitive) {
21
21
  if (!filterSensitive) return process.env;
22
22
  const keep = new Set((ctx?.cfg?.bashEnvKeep || []).map(String));
23
23
  const env = {};
24
24
  for (const [k, v] of Object.entries(process.env)) {
25
- if (!isSensitiveEnv(k) || keep.has(k)) env[k] = v;
25
+ if (!isSensitiveEnv(k) || keep.has(k)) (/** @type {any} */ (env))[k] = v;
26
26
  }
27
27
  return env;
28
28
  }
29
29
 
30
- let sandboxSupport = null;
30
+ let /** @type {any} */ sandboxSupport = null;
31
31
 
32
32
  export function detectSandbox() {
33
- if (sandboxSupport !== null) return sandboxSupport;
33
+ if (/** @type {any} */ sandboxSupport !== null) return /** @type {any} */ sandboxSupport;
34
34
  sandboxSupport = 'none';
35
35
  if (process.platform === 'linux') {
36
36
  try {
@@ -46,12 +46,12 @@ export function detectSandbox() {
46
46
  // 输出折叠(审计 MiniMax §3.3-E / v0.1.48 P1-G):模型回填的 bash 输出先折叠再截断——
47
47
  // 1) 剥离 ANSI 转义序列(CSI/OSC);2) 连续重复行(>3 行相同)折叠为「首行 + 重复标记」。
48
48
  // npm install 类输出通常 30-50KB → 折叠后 5-10KB,单次工具回填省 60-70% prompt token。
49
- function stripAnsi(s) {
49
+ function stripAnsi(/** @type {any} */ s) {
50
50
  return s
51
51
  .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
52
52
  .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
53
53
  }
54
- function foldRepeats(s) {
54
+ function foldRepeats(/** @type {any} */ s) {
55
55
  const lines = s.split('\n');
56
56
  const out = [];
57
57
  let i = 0;
@@ -69,12 +69,12 @@ function foldRepeats(s) {
69
69
  }
70
70
  return out.join('\n');
71
71
  }
72
- function tail(s, n) {
72
+ function tail(/** @type {any} */ s, /** @type {any} */ n) {
73
73
  const folded = foldRepeats(stripAnsi(s));
74
74
  return folded.length > n ? `…[输出过长,已截断头部]\n${folded.slice(-n)}` : folded;
75
75
  }
76
76
 
77
- export function runBash(args, ctx) {
77
+ export function runBash(/** @type {any} */ args, /** @type {any} */ ctx) {
78
78
  const command = String(args.command ?? '');
79
79
  if (!command.trim()) return { ok: false, error: 'command 参数为空。' };
80
80
  const timeoutSec = Math.min(Number(args.timeout) || 120, MAX_TIMEOUT_SECONDS);
@@ -125,13 +125,13 @@ export function runBash(args, ctx) {
125
125
  let done = false;
126
126
  let timedOut = false;
127
127
  // 输出增量截断:超长输出只保留尾部,避免内存无限累积
128
- const cap = (s, d) => {
128
+ const cap = (/** @type {any} */ s, /** @type {any} */ d) => {
129
129
  const t = s + d;
130
130
  return t.length > MAX_OUTPUT * 2 ? t.slice(-MAX_OUTPUT * 2) : t;
131
131
  };
132
- const killGroup = (sig) => {
132
+ const killGroup = (/** @type {any} */ sig) => {
133
133
  try {
134
- process.kill(-child.pid, sig);
134
+ process.kill(-(/** @type {any} */ (child)).pid, sig);
135
135
  } catch {
136
136
  try {
137
137
  child.kill(sig);
@@ -9,11 +9,18 @@ const MAX_GLOB_RESULTS = 1000;
9
9
  const MAX_GREP_MATCHES = 250;
10
10
  const MAX_FILE_BYTES = 5 * 1024 * 1024;
11
11
 
12
+ /**
13
+ * @param {any} cwd
14
+ * @param {any} p
15
+ */
12
16
  function resolvePath(cwd, p) {
13
17
  if (path.isAbsolute(p)) return path.normalize(p);
14
18
  return path.resolve(cwd, p);
15
19
  }
16
20
 
21
+ /**
22
+ * @param {any} buf
23
+ */
17
24
  function isProbablyBinary(buf) {
18
25
  const head = buf.subarray(0, 8192);
19
26
  for (let i = 0; i < head.length; i++) {
@@ -25,6 +32,10 @@ function isProbablyBinary(buf) {
25
32
  // —— undo 备份(会话级,每文件最多 10 份 + 全局上限,质检 M9) ——
26
33
  const UNDO_MAX_FILES = 64;
27
34
  const UNDO_MAX_BYTES = 20 * 1024 * 1024;
35
+ /**
36
+ * @param {any} ctx
37
+ * @param {any} p
38
+ */
28
39
  function backup(ctx, p) {
29
40
  try {
30
41
  const store = ctx?.undoStore?.backups;
@@ -57,6 +68,10 @@ function backup(ctx, p) {
57
68
  } catch {}
58
69
  }
59
70
 
71
+ /**
72
+ * @param {any} args
73
+ * @param {any} ctx
74
+ */
60
75
  export function undo(args, ctx) {
61
76
  const store = ctx?.undoStore?.backups;
62
77
  if (!(store instanceof Map) || !store.size) return { ok: false, error: '没有可撤销的修改。' };
@@ -68,7 +83,7 @@ export function undo(args, ctx) {
68
83
  try {
69
84
  fs.writeFileSync(p, last.content);
70
85
  return { ok: true, output: `已撤销 ${p} 的最近一次修改(该文件剩余备份 ${list.length} 个)。` };
71
- } catch (err) {
86
+ } catch (/** @type {any} */ err) {
72
87
  return { ok: false, error: `撤销失败:${err?.message || err}` };
73
88
  }
74
89
  }
@@ -87,7 +102,7 @@ export function undo(args, ctx) {
87
102
  try {
88
103
  fs.writeFileSync(latestFile, last.content);
89
104
  return { ok: true, output: `已撤销 ${latestFile} 的最近一次修改。` };
90
- } catch (err) {
105
+ } catch (/** @type {any} */ err) {
91
106
  return { ok: false, error: `撤销失败:${err?.message || err}` };
92
107
  }
93
108
  }
@@ -95,10 +110,17 @@ export function undo(args, ctx) {
95
110
  const READ_CACHE_MAX = 200; // 会话级 read 缓存条数上限(超出清最旧,防止无界增长)
96
111
  const readCache = new Map(); // 绝对路径 → { mtimeMs, size, lines }
97
112
 
113
+ /**
114
+ * @param {any} p
115
+ */
98
116
  export function invalidateReadCache(p) {
99
117
  readCache.delete(p);
100
118
  }
101
119
 
120
+ /**
121
+ * @param {any} args
122
+ * @param {any} ctx
123
+ */
102
124
  export function read(args, ctx) {
103
125
  try {
104
126
  const p = resolvePath(ctx.cwd, args.path ?? '');
@@ -145,13 +167,17 @@ export function read(args, ctx) {
145
167
  out.join('\n') + (truncated ? `\n…[共 ${lines.length} 行,已显示第 ${offset}-${end} 行]` : ''),
146
168
  totalLines: lines.length,
147
169
  };
148
- } catch (err) {
170
+ } catch (/** @type {any} */ err) {
149
171
  return { ok: false, error: `读取失败:${err?.message || err}` };
150
172
  }
151
173
  }
152
174
 
153
175
  const MAX_WRITE_BYTES = 2 * 1024 * 1024; // write 单次内容上限(防模型输出超大文件打爆磁盘)
154
176
 
177
+ /**
178
+ * @param {any} args
179
+ * @param {any} ctx
180
+ */
155
181
  export function write(args, ctx) {
156
182
  try {
157
183
  const p = resolvePath(ctx.cwd, args.path ?? '');
@@ -171,12 +197,17 @@ export function write(args, ctx) {
171
197
  fs.writeFileSync(p, content);
172
198
  invalidateReadCache(p);
173
199
  return { ok: true, output: `已写入 ${p}(${Buffer.byteLength(content)} 字节)。` };
174
- } catch (err) {
200
+ } catch (/** @type {any} */ err) {
175
201
  return { ok: false, error: `写入失败:${err?.message || err}` };
176
202
  }
177
203
  }
178
204
 
179
205
  // 提取替换区域(含上下文),供 TUI 渲染变更预览
206
+ /**
207
+ * @param {any} text
208
+ * @param {any} lineStart
209
+ * @param {any} lineCount
210
+ */
180
211
  function regionAround(text, lineStart, lineCount, context = 2) {
181
212
  const lines = text.split('\n');
182
213
  const from = Math.max(0, lineStart - context);
@@ -184,6 +215,10 @@ function regionAround(text, lineStart, lineCount, context = 2) {
184
215
  return lines.slice(from, to).join('\n');
185
216
  }
186
217
 
218
+ /**
219
+ * @param {any} args
220
+ * @param {any} ctx
221
+ */
187
222
  export function edit(args, ctx) {
188
223
  try {
189
224
  const p = resolvePath(ctx.cwd, args.path ?? '');
@@ -218,11 +253,15 @@ export function edit(args, ctx) {
218
253
  output: `已编辑 ${p}(替换 ${count} 处)。`,
219
254
  diff: { before, after },
220
255
  };
221
- } catch (err) {
256
+ } catch (/** @type {any} */ err) {
222
257
  return { ok: false, error: `编辑失败:${err?.message || err}` };
223
258
  }
224
259
  }
225
260
 
261
+ /**
262
+ * @param {any} args
263
+ * @param {any} ctx
264
+ */
226
265
  export function ls(args, ctx) {
227
266
  try {
228
267
  const p = resolvePath(ctx.cwd, args.path || '.');
@@ -243,11 +282,14 @@ export function ls(args, ctx) {
243
282
  return ad - bd || a.localeCompare(b);
244
283
  });
245
284
  return { ok: true, output: rows.join('\n') || '(空目录)' };
246
- } catch (err) {
285
+ } catch (/** @type {any} */ err) {
247
286
  return { ok: false, error: `列目录失败:${err?.message || err}` };
248
287
  }
249
288
  }
250
289
 
290
+ /**
291
+ * @param {any} pattern
292
+ */
251
293
  function globToRegExp(pattern) {
252
294
  let re = '';
253
295
  for (let i = 0; i < pattern.length; i++) {
@@ -275,6 +317,10 @@ function globToRegExp(pattern) {
275
317
  return new RegExp('^' + re + '$');
276
318
  }
277
319
 
320
+ /**
321
+ * @param {any} root
322
+ * @param {(full: any) => any} visitor
323
+ */
278
324
  function walkFiles(root, visitor) {
279
325
  const stack = [root];
280
326
  let scanned = 0;
@@ -300,6 +346,10 @@ function walkFiles(root, visitor) {
300
346
  return scanned;
301
347
  }
302
348
 
349
+ /**
350
+ * @param {any} args
351
+ * @param {any} ctx
352
+ */
303
353
  export function glob(args, ctx) {
304
354
  try {
305
355
  const pattern = String(args.pattern ?? '*');
@@ -307,6 +357,7 @@ export function glob(args, ctx) {
307
357
  if (!fs.existsSync(root)) return { ok: false, error: `目录不存在:${root}` };
308
358
  const re = globToRegExp(pattern);
309
359
  const matchRel = pattern.includes('/');
360
+ /** @type {any[]} */
310
361
  const results = [];
311
362
  let truncated = false;
312
363
  walkFiles(root, (full) => {
@@ -321,11 +372,15 @@ export function glob(args, ctx) {
321
372
  if (!results.length) output = '(无匹配)';
322
373
  if (truncated) output += `\n…[结果超过 ${MAX_GLOB_RESULTS} 条,已截断]`;
323
374
  return { ok: true, output };
324
- } catch (err) {
375
+ } catch (/** @type {any} */ err) {
325
376
  return { ok: false, error: `glob 失败:${err?.message || err}` };
326
377
  }
327
378
  }
328
379
 
380
+ /**
381
+ * @param {any} args
382
+ * @param {any} ctx
383
+ */
329
384
  export function grep(args, ctx) {
330
385
  try {
331
386
  const pattern = String(args.pattern ?? '');
@@ -338,14 +393,16 @@ export function grep(args, ctx) {
338
393
  let re;
339
394
  try {
340
395
  re = new RegExp(pattern);
341
- } catch (e) {
396
+ } catch (/** @type {any} */ e) {
342
397
  return { ok: false, error: `无效的正则表达式:${e.message}` };
343
398
  }
344
399
  // 单行截断:超长行(如压缩的 JS/日志)截到 20KB 再匹配,防止正则开销失控
345
400
  const MAX_LINE = 20 * 1024;
401
+ /** @type {(line: any) => any} */
346
402
  const testLine = (line) => (line.length > MAX_LINE ? re.test(line.slice(0, MAX_LINE)) : re.test(line));
347
403
  const root = resolvePath(ctx.cwd, args.path || '.');
348
404
  const include = args.include ? globToRegExp(String(args.include)) : null;
405
+ /** @type {any[]} */
349
406
  const matches = [];
350
407
  let truncated = false;
351
408
  let scannedFiles = 0;
@@ -386,7 +443,7 @@ export function grep(args, ctx) {
386
443
  ? `\n…[匹配超过 ${MAX_GREP_MATCHES} 条,已截断;已扫描 ${scannedFiles} 个文件]`
387
444
  : `\n[已扫描 ${scannedFiles} 个文件]`;
388
445
  return { ok: true, output };
389
- } catch (err) {
446
+ } catch (/** @type {any} */ err) {
390
447
  return { ok: false, error: `grep 失败:${err?.message || err}` };
391
448
  }
392
449
  }
@@ -11,9 +11,9 @@ export const READONLY_TOOLS = new Set(['read', 'glob', 'grep', 'ls', 'skill']);
11
11
  const READ_SCHEMA = {
12
12
  type: 'object',
13
13
  properties: {
14
- path: { type: 'string', description: '要读取的文件路径(相对或绝对)。' },
14
+ path: { type: 'string', description: '文件路径(相对或绝对)。' },
15
15
  offset: { type: 'integer', description: '起始行号,默认 1。' },
16
- limit: { type: 'integer', description: '最大返回行数,默认 400。' },
16
+ limit: { type: 'integer', description: '最大行数,默认 400。' },
17
17
  },
18
18
  required: ['path'],
19
19
  };
@@ -21,7 +21,7 @@ const READ_SCHEMA = {
21
21
  const WRITE_SCHEMA = {
22
22
  type: 'object',
23
23
  properties: {
24
- path: { type: 'string', description: '要创建或覆盖的文件路径。' },
24
+ path: { type: 'string', description: '文件路径。' },
25
25
  content: { type: 'string', description: '完整文件内容(UTF-8)。' },
26
26
  },
27
27
  required: ['path', 'content'],
@@ -30,10 +30,10 @@ const WRITE_SCHEMA = {
30
30
  const EDIT_SCHEMA = {
31
31
  type: 'object',
32
32
  properties: {
33
- path: { type: 'string', description: '要修改的文件路径。' },
34
- old_string: { type: 'string', description: '文件中真实存在的原文片段,必须逐字符完全一致。' },
35
- new_string: { type: 'string', description: '替换后的文本。' },
36
- replace_all: { type: 'boolean', description: '是否替换所有匹配处,默认 false。' },
33
+ path: { type: 'string', description: '文件路径。' },
34
+ old_string: { type: 'string', description: '原文片段,必须逐字一致。' },
35
+ new_string: { type: 'string', description: '替换后文本。' },
36
+ replace_all: { type: 'boolean', description: '替换所有匹配,默认 false。' },
37
37
  },
38
38
  required: ['path', 'old_string', 'new_string'],
39
39
  };
@@ -41,15 +41,15 @@ const EDIT_SCHEMA = {
41
41
  const LS_SCHEMA = {
42
42
  type: 'object',
43
43
  properties: {
44
- path: { type: 'string', description: '目录路径,默认当前工作目录。' },
44
+ path: { type: 'string', description: '目录路径,默认当前目录。' },
45
45
  },
46
46
  };
47
47
 
48
48
  const GLOB_SCHEMA = {
49
49
  type: 'object',
50
50
  properties: {
51
- pattern: { type: 'string', description: '通配符模式,如 src/**/*.js、*.md。' },
52
- path: { type: 'string', description: '搜索根目录,默认当前工作目录。' },
51
+ pattern: { type: 'string', description: '通配符模式,如 src/**/*.js。' },
52
+ path: { type: 'string', description: '搜索根目录。' },
53
53
  },
54
54
  required: ['pattern'],
55
55
  };
@@ -58,8 +58,8 @@ const GREP_SCHEMA = {
58
58
  type: 'object',
59
59
  properties: {
60
60
  pattern: { type: 'string', description: '正则表达式。' },
61
- path: { type: 'string', description: '搜索根目录,默认当前工作目录。' },
62
- include: { type: 'string', description: '文件名通配符过滤,如 *.js。' },
61
+ path: { type: 'string', description: '搜索根目录。' },
62
+ include: { type: 'string', description: '文件名过滤,如 *.js。' },
63
63
  },
64
64
  required: ['pattern'],
65
65
  };
@@ -67,7 +67,7 @@ const GREP_SCHEMA = {
67
67
  const BASH_SCHEMA = {
68
68
  type: 'object',
69
69
  properties: {
70
- command: { type: 'string', description: '要执行的 shell 命令。执行前会请求用户授权。' },
70
+ command: { type: 'string', description: 'shell 命令。执行前需用户授权。' },
71
71
  timeout: { type: 'integer', description: '超时秒数,默认 120。' },
72
72
  },
73
73
  required: ['command'],
@@ -76,16 +76,16 @@ const BASH_SCHEMA = {
76
76
  const SKILL_SCHEMA = {
77
77
  type: 'object',
78
78
  properties: {
79
- name: { type: 'string', description: '技能名;省略时列出所有可用技能。' },
79
+ name: { type: 'string', description: '技能名;省略则列出全部。' },
80
80
  },
81
81
  };
82
82
 
83
83
  const TASK_SCHEMA = {
84
84
  type: 'object',
85
85
  properties: {
86
- description: { type: 'string', description: '子任务一句话描述(用于进度展示)。' },
87
- prompt: { type: 'string', description: '交给子代理的完整任务说明(它没有当前对话上下文)。' },
88
- readOnly: { type: 'boolean', description: '只读调研任务(不写文件不执行命令)。多个 readOnly 子任务会并行执行。' },
86
+ description: { type: 'string', description: '子任务一句话描述。' },
87
+ prompt: { type: 'string', description: '子代理的完整任务说明。' },
88
+ readOnly: { type: 'boolean', description: '只读调研任务,可并行。' },
89
89
  },
90
90
  required: ['prompt'],
91
91
  };
@@ -112,7 +112,7 @@ const TODO_SCHEMA = {
112
112
  const UNDO_SCHEMA = {
113
113
  type: 'object',
114
114
  properties: {
115
- path: { type: 'string', description: '要撤销的文件;省略时撤销最近一次修改的文件。' },
115
+ path: { type: 'string', description: '要撤销的文件;省略撤销最近一次。' },
116
116
  },
117
117
  };
118
118
 
@@ -121,7 +121,7 @@ const TOOLS = [
121
121
  type: 'function',
122
122
  function: {
123
123
  name: 'read',
124
- description: '读取文本文件,返回带行号的内容。修改文件前务必先读取。',
124
+ description: '读取文件内容(带行号)。修改文件前必须先读。',
125
125
  parameters: READ_SCHEMA,
126
126
  },
127
127
  },
@@ -129,7 +129,7 @@ const TOOLS = [
129
129
  type: 'function',
130
130
  function: {
131
131
  name: 'write',
132
- description: '创建新文件或完全覆盖已有文件(自动创建父目录)。',
132
+ description: '创建新文件或整体覆盖(自动建父目录)。',
133
133
  parameters: WRITE_SCHEMA,
134
134
  },
135
135
  },
@@ -137,7 +137,7 @@ const TOOLS = [
137
137
  type: 'function',
138
138
  function: {
139
139
  name: 'edit',
140
- description: '对文件做精确文本替换。old_string 必须与文件内容完全一致;匹配多处时需 replace_all=true 或提供更精确上下文。',
140
+ description: '精确文本替换:old_string 必须逐字匹配原文;多处匹配需 replace_all=true。',
141
141
  parameters: EDIT_SCHEMA,
142
142
  },
143
143
  },
@@ -145,7 +145,7 @@ const TOOLS = [
145
145
  type: 'function',
146
146
  function: {
147
147
  name: 'ls',
148
- description: '列出目录内容,目录以 / 结尾。',
148
+ description: '列出目录内容(目录以 / 结尾)。',
149
149
  parameters: LS_SCHEMA,
150
150
  },
151
151
  },
@@ -153,7 +153,7 @@ const TOOLS = [
153
153
  type: 'function',
154
154
  function: {
155
155
  name: 'glob',
156
- description: '按通配符模式查找文件路径(支持 * 与 **)。',
156
+ description: '按通配符查找文件路径(支持 * 与 **)。',
157
157
  parameters: GLOB_SCHEMA,
158
158
  },
159
159
  },
@@ -161,7 +161,7 @@ const TOOLS = [
161
161
  type: 'function',
162
162
  function: {
163
163
  name: 'grep',
164
- description: '在文件内容中按正则表达式搜索,返回「文件:行号: 内容」。',
164
+ description: '正则搜索文件内容,返回「文件:行号: 内容」。',
165
165
  parameters: GREP_SCHEMA,
166
166
  },
167
167
  },
@@ -169,7 +169,7 @@ const TOOLS = [
169
169
  type: 'function',
170
170
  function: {
171
171
  name: 'bash',
172
- description: '执行 shell 命令并返回输出(stdout/stderr/退出码)。需要用户授权。',
172
+ description: '执行 shell 命令,返回输出与退出码。需用户授权。',
173
173
  parameters: BASH_SCHEMA,
174
174
  },
175
175
  },
@@ -177,7 +177,7 @@ const TOOLS = [
177
177
  type: 'function',
178
178
  function: {
179
179
  name: 'skill',
180
- description: '加载指定技能的 SKILL.md 全文;省略 name 时列出可用技能。任务与技能匹配时应先加载。',
180
+ description: '加载技能 SKILL.md 全文;省略 name 则列出可用技能。',
181
181
  parameters: SKILL_SCHEMA,
182
182
  },
183
183
  },
@@ -185,7 +185,7 @@ const TOOLS = [
185
185
  type: 'function',
186
186
  function: {
187
187
  name: 'task',
188
- description: '把独立子任务委托给一个全新上下文的子代理,返回其汇报。适合搜索调研、独立实现、复核验证等可并行的工作。',
188
+ description: '把独立子任务委托给新上下文子代理并返回汇报;readOnly 只读任务可并行。',
189
189
  parameters: TASK_SCHEMA,
190
190
  },
191
191
  },
@@ -193,7 +193,7 @@ const TOOLS = [
193
193
  type: 'function',
194
194
  function: {
195
195
  name: 'todo',
196
- description: '维护任务清单(全量替换)。多步骤任务开始前建立清单,完成一项更新一项。',
196
+ description: '维护任务清单(全量替换)。多步任务先建清单再逐项更新。',
197
197
  parameters: TODO_SCHEMA,
198
198
  },
199
199
  },
@@ -201,7 +201,7 @@ const TOOLS = [
201
201
  type: 'function',
202
202
  function: {
203
203
  name: 'undo',
204
- description: '撤销 write/edit 造成的最近一次文件修改;指定 path 则撤销该文件。',
204
+ description: '撤销最近一次 write/edit 修改;指定 path 撤销该文件。',
205
205
  parameters: UNDO_SCHEMA,
206
206
  },
207
207
  },
@@ -211,10 +211,51 @@ export function toolSchemas() {
211
211
  return TOOLS;
212
212
  }
213
213
 
214
- function runSkill(args, ctx) {
214
+ /**
215
+ * 递归去除对象中的全部 description 键(保留类型/required/enum 等结构性字段)。
216
+ * @param {any} obj
217
+ * @returns {any}
218
+ */
219
+ function stripDescriptions(obj) {
220
+ if (Array.isArray(obj)) return obj.map(stripDescriptions);
221
+ if (obj && typeof obj === 'object') {
222
+ const out = /** @type {Record<string, any>} */ ({});
223
+ for (const [k, v] of Object.entries(obj)) {
224
+ if (k === 'description') continue;
225
+ out[k] = stripDescriptions(v);
226
+ }
227
+ return out;
228
+ }
229
+ return obj;
230
+ }
231
+
232
+ /**
233
+ * 按需构建工具 Schema(省钱 B1):本会话已调用过的工具省略全部描述(工具级 + 参数级,
234
+ * 模型已在消息历史里见过其用途,保留 name/parameters 结构与类型即可),未用过的保留完整描述。
235
+ * 返回浅拷贝新数组,不修改原 TOOLS;extra(如 MCP 工具)按同一规则处理。
236
+ * @param {Set<string>} usedNames 已调用工具名(内置名或 MCP 前缀名)
237
+ * @param {Array<object>} [extra] 附加工具 Schema(MCP 等)
238
+ * @returns {Array<any>}
239
+ */
240
+ export function buildToolSchemas(usedNames, extra = []) {
241
+ const strip = (/** @type {any[]} */ arr) =>
242
+ arr.map((/** @type {any} */ t) => {
243
+ const name = t?.function?.name;
244
+ if (name && usedNames.has(name)) {
245
+ return {
246
+ ...t,
247
+ function: { ...t.function, description: '', parameters: stripDescriptions(t.function?.parameters) },
248
+ };
249
+ }
250
+ return t;
251
+ });
252
+ return [...strip(TOOLS), ...strip(extra)];
253
+ }
254
+
255
+ function runSkill(/** @type {any} */ args, /** @type {any} */ ctx) {
215
256
  const name = String(args.name ?? '').trim();
216
257
  const skills = listSkills(ctx.workingDir);
217
- const label = (s) => `${s.name}${s.source === 'user' ? '(用户级)' : s.source === 'builtin' ? '(内置)' : ''}`;
258
+ const label = (/** @type {any} */ s) => `${s.name}${s.source === 'user' ? '(用户级)' : s.source === 'builtin' ? '(内置)' : ''}`;
218
259
  if (!name) {
219
260
  return {
220
261
  ok: true,
@@ -230,7 +271,7 @@ function runSkill(args, ctx) {
230
271
  return { ok: true, output: `技能 ${name}${s.source === 'builtin' ? '(内置)' : ''}(${s.path}):\n\n${s.content}` };
231
272
  }
232
273
 
233
- async function runTask(args, ctx) {
274
+ async function runTask(/** @type {any} */ args, /** @type {any} */ ctx) {
234
275
  const prompt = String(args.prompt ?? '');
235
276
  const description = String(args.description ?? '');
236
277
  if (!prompt) return { ok: false, error: '缺少 prompt 参数。' };
@@ -239,11 +280,11 @@ async function runTask(args, ctx) {
239
280
  const out = await ctx.spawnTask(prompt, { description, readOnly: args.readOnly === true });
240
281
  return { ok: true, output: out };
241
282
  } catch (err) {
242
- return { ok: false, error: `子任务失败:${err?.message || err}` };
283
+ return { ok: false, error: `子任务失败:${(/** @type {any} */ (err))?.message || err}` };
243
284
  }
244
285
  }
245
286
 
246
- function runTodo(args, ctx) {
287
+ function runTodo(/** @type {any} */ args, /** @type {any} */ ctx) {
247
288
  const list = Array.isArray(args.todos) ? args.todos : [];
248
289
  if (Array.isArray(ctx.todos)) {
249
290
  ctx.todos.splice(0, ctx.todos.length, ...list);
@@ -252,12 +293,12 @@ function runTodo(args, ctx) {
252
293
  ok: true,
253
294
  output:
254
295
  `任务清单已更新(${list.length} 项):\n` +
255
- list.map((t) => `- [${t.status || 'pending'}] ${t.content}`).join('\n'),
296
+ list.map((/** @type {any} */ t) => `- [${t.status || 'pending'}] ${t.content}`).join('\n'),
256
297
  todos: list,
257
298
  };
258
299
  }
259
300
 
260
- export async function dispatch(name, args, ctx) {
301
+ export async function dispatch(/** @type {any} */ name, /** @type {any} */ args, /** @type {any} */ ctx) {
261
302
  switch (name) {
262
303
  case 'read':
263
304
  return read(args, ctx);