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.
- package/docs/ROADMAP-NEXT.md +13 -13
- package/package.json +1 -1
- package/skills/release-checklist/SKILL.md +16 -3
- package/src/agent.js +102 -30
- package/src/audit.js +2 -2
- package/src/batch.js +74 -16
- package/src/cachestats.js +57 -11
- package/src/cli.js +29 -679
- package/src/commands/desktop.js +1 -1
- package/src/commands/key.js +1 -1
- package/src/commands/repl.js +711 -0
- package/src/commands/schedule.js +7 -7
- package/src/commands/skill.js +18 -16
- package/src/commands/sync.js +5 -5
- package/src/commands/update.js +22 -9
- package/src/commands/workspace.js +2 -2
- package/src/compact.js +6 -6
- package/src/config.js +7 -4
- package/src/context.js +11 -11
- package/src/cost-guard.js +15 -5
- package/src/credentials.js +6 -0
- package/src/hooks.js +11 -11
- package/src/mcp-presets.js +5 -5
- package/src/mcp.js +19 -19
- package/src/memory.js +16 -16
- package/src/model-discovery.js +16 -15
- package/src/models.js +4 -4
- package/src/notify.js +2 -2
- package/src/permissions.js +7 -3
- package/src/pricing.js +35 -1
- package/src/prompts.js +1 -1
- package/src/providers/index.js +7 -7
- package/src/providers/openai-compatible.js +8 -8
- package/src/routing.js +5 -5
- package/src/schedule.js +28 -28
- package/src/session-index.js +5 -1
- package/src/session.js +11 -2
- package/src/skill-lib.js +62 -3
- package/src/skill-registry.js +10 -9
- package/src/skills.js +10 -10
- package/src/sync-server.js +86 -3
- package/src/sync.js +53 -20
- package/src/tasks/worker.js +126 -0
- package/src/tasks.js +13 -13
- package/src/titles.js +5 -5
- package/src/tokenizer.js +22 -4
- package/src/tools/bash.js +12 -12
- package/src/tools/fs-tools.js +66 -9
- package/src/tools/index.js +77 -36
- package/src/ui.js +83 -6
- package/src/update.js +8 -8
- package/src/web/app.js +43 -0
- package/src/web/attachments.js +1 -1
- package/src/web/index.html +12 -0
- package/src/web/routes/api.js +38 -786
- package/src/web/routes/domains/config.js +280 -0
- package/src/web/routes/domains/misc.js +128 -0
- package/src/web/routes/domains/schedule.js +127 -0
- package/src/web/routes/domains/sessions.js +105 -0
- package/src/web/routes/domains/skills.js +79 -0
- package/src/web/routes/domains/sync.js +101 -0
- package/src/web/routes/domains/workspace.js +113 -0
- package/src/web/server.js +29 -14
- package/src/web/web-io.js +54 -12
- 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);
|
package/src/tools/fs-tools.js
CHANGED
|
@@ -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
|
}
|
package/src/tools/index.js
CHANGED
|
@@ -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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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);
|