mingdao-harness 0.1.54

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -0
  3. package/assets/tokenizer-data.json.gz +0 -0
  4. package/docs/ARCHITECTURE.md +108 -0
  5. package/docs/CONFIG.md +257 -0
  6. package/docs/DESKTOP-EVALUATION.md +48 -0
  7. package/docs/PROVIDERS.md +98 -0
  8. package/docs/QA-REPORT.md +333 -0
  9. package/install.bat +8 -0
  10. package/install.ps1 +57 -0
  11. package/install.sh +154 -0
  12. package/package.json +61 -0
  13. package/skills/api-design/SKILL.md +21 -0
  14. package/skills/code-review/SKILL.md +31 -0
  15. package/skills/debugging/SKILL.md +21 -0
  16. package/skills/docker/SKILL.md +27 -0
  17. package/skills/docx/SKILL.md +28 -0
  18. package/skills/frontend-design/SKILL.md +31 -0
  19. package/skills/git-commit/SKILL.md +32 -0
  20. package/skills/pdf/SKILL.md +30 -0
  21. package/skills/pptx/SKILL.md +24 -0
  22. package/skills/refactoring/SKILL.md +24 -0
  23. package/skills/release-checklist/SKILL.md +20 -0
  24. package/skills/testing/SKILL.md +30 -0
  25. package/skills/webapp-testing/SKILL.md +27 -0
  26. package/skills/xlsx/SKILL.md +28 -0
  27. package/src/agent.js +472 -0
  28. package/src/audit.js +68 -0
  29. package/src/autostart.js +74 -0
  30. package/src/batch.js +182 -0
  31. package/src/cachestats.js +215 -0
  32. package/src/cli.js +1099 -0
  33. package/src/commands/key.js +75 -0
  34. package/src/commands/schedule.js +176 -0
  35. package/src/commands/skill.js +168 -0
  36. package/src/commands/sync.js +222 -0
  37. package/src/commands/update.js +157 -0
  38. package/src/commands/workspace.js +109 -0
  39. package/src/compact.js +112 -0
  40. package/src/config.js +134 -0
  41. package/src/context.js +86 -0
  42. package/src/cost-guard.js +58 -0
  43. package/src/credentials.js +69 -0
  44. package/src/hooks.js +123 -0
  45. package/src/index.js +42 -0
  46. package/src/mcp-presets.js +78 -0
  47. package/src/mcp.js +284 -0
  48. package/src/memory.js +242 -0
  49. package/src/model-discovery.js +153 -0
  50. package/src/models.js +186 -0
  51. package/src/notify.js +38 -0
  52. package/src/permissions.js +83 -0
  53. package/src/pricing.js +156 -0
  54. package/src/prompts.js +58 -0
  55. package/src/providers/index.js +135 -0
  56. package/src/providers/openai-compatible.js +171 -0
  57. package/src/routing.js +126 -0
  58. package/src/schedule.js +434 -0
  59. package/src/session-index.js +122 -0
  60. package/src/session.js +131 -0
  61. package/src/skill-lib.js +350 -0
  62. package/src/skill-registry.js +165 -0
  63. package/src/skills.js +135 -0
  64. package/src/sync-server.js +564 -0
  65. package/src/sync.js +479 -0
  66. package/src/tasks.js +126 -0
  67. package/src/titles.js +75 -0
  68. package/src/tokenizer.js +287 -0
  69. package/src/tools/bash.js +165 -0
  70. package/src/tools/fs-tools.js +346 -0
  71. package/src/tools/index.js +287 -0
  72. package/src/ui.js +643 -0
  73. package/src/update.js +222 -0
  74. package/src/web/attachments.js +49 -0
  75. package/src/web/index.html +993 -0
  76. package/src/web/server.js +1173 -0
  77. package/src/web/web-io.js +107 -0
  78. package/src/workspace.js +157 -0
@@ -0,0 +1,346 @@
1
+ // 文件系统工具:read / write / edit / ls / glob / grep。
2
+ // 所有工具返回 { ok, output? | error? },由 agent 序列化后交给模型。
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const MAX_SCAN_FILES = 20000;
8
+ const MAX_GLOB_RESULTS = 1000;
9
+ const MAX_GREP_MATCHES = 250;
10
+ const MAX_FILE_BYTES = 5 * 1024 * 1024;
11
+
12
+ function resolvePath(cwd, p) {
13
+ if (path.isAbsolute(p)) return path.normalize(p);
14
+ return path.resolve(cwd, p);
15
+ }
16
+
17
+ function isProbablyBinary(buf) {
18
+ const head = buf.subarray(0, 8192);
19
+ for (let i = 0; i < head.length; i++) {
20
+ if (head[i] === 0) return true;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ // —— undo 备份(会话级,每文件最多 10 份) ——
26
+ function backup(ctx, p) {
27
+ try {
28
+ const store = ctx?.undoStore?.backups;
29
+ if (!(store instanceof Map)) return;
30
+ const buf = fs.readFileSync(p);
31
+ const list = store.get(p) || [];
32
+ list.push({ time: Date.now(), content: buf });
33
+ if (list.length > 10) list.shift();
34
+ store.set(p, list);
35
+ } catch {}
36
+ }
37
+
38
+ export function undo(args, ctx) {
39
+ const store = ctx?.undoStore?.backups;
40
+ if (!(store instanceof Map) || !store.size) return { ok: false, error: '没有可撤销的修改。' };
41
+ const p = args.path ? resolvePath(ctx.cwd, args.path) : null;
42
+ if (p) {
43
+ const list = store.get(p);
44
+ if (!list?.length) return { ok: false, error: `${p} 没有可撤销的修改记录。` };
45
+ const last = list.pop();
46
+ try {
47
+ fs.writeFileSync(p, last.content);
48
+ return { ok: true, output: `已撤销 ${p} 的最近一次修改(该文件剩余备份 ${list.length} 个)。` };
49
+ } catch (err) {
50
+ return { ok: false, error: `撤销失败:${err?.message || err}` };
51
+ }
52
+ }
53
+ let latestFile = null;
54
+ let latestTime = 0;
55
+ for (const [file, list] of store) {
56
+ const last = list[list.length - 1];
57
+ if (last && last.time > latestTime) {
58
+ latestTime = last.time;
59
+ latestFile = file;
60
+ }
61
+ }
62
+ if (!latestFile) return { ok: false, error: '没有可撤销的修改。' };
63
+ const list = store.get(latestFile);
64
+ const last = list.pop();
65
+ try {
66
+ fs.writeFileSync(latestFile, last.content);
67
+ return { ok: true, output: `已撤销 ${latestFile} 的最近一次修改。` };
68
+ } catch (err) {
69
+ return { ok: false, error: `撤销失败:${err?.message || err}` };
70
+ }
71
+ }
72
+
73
+ export function read(args, ctx) {
74
+ try {
75
+ const p = resolvePath(ctx.cwd, args.path ?? '');
76
+ if (!p) return { ok: false, error: '缺少 path 参数。' };
77
+ const st = fs.statSync(p);
78
+ if (st.isDirectory()) return { ok: false, error: `"${p}" 是目录,请使用 ls 查看。` };
79
+ if (st.size > MAX_FILE_BYTES) {
80
+ return {
81
+ ok: false,
82
+ error: `"${p}" 大小 ${(st.size / 1024 / 1024).toFixed(1)}MB,超过 ${MAX_FILE_BYTES / 1024 / 1024}MB 上限。请用 grep 搜索或 bash 分块查看。`,
83
+ };
84
+ }
85
+ const buf = fs.readFileSync(p);
86
+ if (isProbablyBinary(buf)) return { ok: false, error: `"${p}" 疑似二进制文件,无法按文本读取。` };
87
+ const lines = buf.toString('utf8').split('\n');
88
+ const offset = Math.max(1, Number(args.offset) || 1);
89
+ const limit = Math.max(1, Number(args.limit) || 400);
90
+ // 审计质量项:offset 超出文件行数时明确提示,而非返回空内容让模型误以为文件为空
91
+ if (offset > lines.length) {
92
+ return { ok: true, output: `(offset=${offset} 超出文件总行数 ${lines.length},文件已读完)`, totalLines: lines.length };
93
+ }
94
+ const end = Math.min(lines.length, offset - 1 + limit);
95
+ const out = [];
96
+ for (let i = offset - 1; i < end; i++) out.push(`${i + 1}\t${lines[i]}`);
97
+ const truncated = end < lines.length;
98
+ return {
99
+ ok: true,
100
+ output:
101
+ out.join('\n') + (truncated ? `\n…[共 ${lines.length} 行,已显示第 ${offset}-${end} 行]` : ''),
102
+ totalLines: lines.length,
103
+ };
104
+ } catch (err) {
105
+ return { ok: false, error: `读取失败:${err?.message || err}` };
106
+ }
107
+ }
108
+
109
+ const MAX_WRITE_BYTES = 2 * 1024 * 1024; // write 单次内容上限(防模型输出超大文件打爆磁盘)
110
+
111
+ export function write(args, ctx) {
112
+ try {
113
+ const p = resolvePath(ctx.cwd, args.path ?? '');
114
+ if (!p) return { ok: false, error: '缺少 path 参数。' };
115
+ const content = String(args.content ?? '');
116
+ if (Buffer.byteLength(content) > MAX_WRITE_BYTES) {
117
+ return { ok: false, error: `内容超过 ${MAX_WRITE_BYTES / 1024 / 1024}MB 上限,请分多次写入。` };
118
+ }
119
+ if (fs.existsSync(p)) {
120
+ const st = fs.statSync(p);
121
+ if (st.size > MAX_FILE_BYTES) {
122
+ return { ok: false, error: `目标文件 ${p} 超过 5MB,请改用 edit 精确修改。` };
123
+ }
124
+ backup(ctx, p);
125
+ }
126
+ fs.mkdirSync(path.dirname(p), { recursive: true });
127
+ fs.writeFileSync(p, content);
128
+ return { ok: true, output: `已写入 ${p}(${Buffer.byteLength(content)} 字节)。` };
129
+ } catch (err) {
130
+ return { ok: false, error: `写入失败:${err?.message || err}` };
131
+ }
132
+ }
133
+
134
+ // 提取替换区域(含上下文),供 TUI 渲染变更预览
135
+ function regionAround(text, lineStart, lineCount, context = 2) {
136
+ const lines = text.split('\n');
137
+ const from = Math.max(0, lineStart - context);
138
+ const to = Math.min(lines.length, lineStart + lineCount + context);
139
+ return lines.slice(from, to).join('\n');
140
+ }
141
+
142
+ export function edit(args, ctx) {
143
+ try {
144
+ const p = resolvePath(ctx.cwd, args.path ?? '');
145
+ const oldString = String(args.old_string ?? '');
146
+ const newString = String(args.new_string ?? '');
147
+ const replaceAll = Boolean(args.replace_all);
148
+ if (!p) return { ok: false, error: '缺少 path 参数。' };
149
+ if (!oldString) return { ok: false, error: '缺少 old_string 参数。' };
150
+ try {
151
+ if (fs.statSync(p).size > MAX_FILE_BYTES) {
152
+ return { ok: false, error: `文件 ${p} 超过 5MB,请先 read 定位后用更小的改动。` };
153
+ }
154
+ } catch {}
155
+ const text = fs.readFileSync(p, 'utf8');
156
+ const count = text.split(oldString).length - 1;
157
+ if (count === 0) {
158
+ return { ok: false, error: `在 ${p} 中未找到 old_string 匹配。文件可能已变化,请先 read 确认最新内容。` };
159
+ }
160
+ if (count > 1 && !replaceAll) {
161
+ return { ok: false, error: `old_string 匹配到 ${count} 处。请提供更精确的上下文,或设置 replace_all=true。` };
162
+ }
163
+ const next = replaceAll ? text.split(oldString).join(newString) : text.replace(oldString, newString);
164
+ backup(ctx, p);
165
+ fs.writeFileSync(p, next);
166
+ const idx = text.indexOf(oldString);
167
+ const lineStart = text.slice(0, idx).split('\n').length - 1;
168
+ const before = regionAround(text, lineStart, oldString.split('\n').length);
169
+ const after = regionAround(next, lineStart, newString.split('\n').length);
170
+ return {
171
+ ok: true,
172
+ output: `已编辑 ${p}(替换 ${count} 处)。`,
173
+ diff: { before, after },
174
+ };
175
+ } catch (err) {
176
+ return { ok: false, error: `编辑失败:${err?.message || err}` };
177
+ }
178
+ }
179
+
180
+ export function ls(args, ctx) {
181
+ try {
182
+ const p = resolvePath(ctx.cwd, args.path || '.');
183
+ const entries = fs.readdirSync(p, { withFileTypes: true });
184
+ const rows = entries
185
+ .map((e) => {
186
+ const full = path.join(p, e.name);
187
+ if (e.isDirectory()) return `${e.name}/`;
188
+ let size = '';
189
+ try {
190
+ size = ` (${fs.statSync(full).size}B)`;
191
+ } catch {}
192
+ return e.name + size;
193
+ })
194
+ .sort((a, b) => {
195
+ const ad = a.endsWith('/') ? 0 : 1;
196
+ const bd = b.endsWith('/') ? 0 : 1;
197
+ return ad - bd || a.localeCompare(b);
198
+ });
199
+ return { ok: true, output: rows.join('\n') || '(空目录)' };
200
+ } catch (err) {
201
+ return { ok: false, error: `列目录失败:${err?.message || err}` };
202
+ }
203
+ }
204
+
205
+ function globToRegExp(pattern) {
206
+ let re = '';
207
+ for (let i = 0; i < pattern.length; i++) {
208
+ const ch = pattern[i];
209
+ if (ch === '*') {
210
+ if (pattern[i + 1] === '*') {
211
+ if (pattern[i + 2] === '/') {
212
+ re += '(?:.*/)?'; // **/ 匹配零层或多层目录
213
+ i += 2;
214
+ } else {
215
+ re += '.*';
216
+ i += 1;
217
+ }
218
+ } else {
219
+ re += '[^/]*';
220
+ }
221
+ } else if (ch === '?') {
222
+ re += '[^/]';
223
+ } else if (/[.+^${}()|[\]\\]/.test(ch)) {
224
+ re += '\\' + ch;
225
+ } else {
226
+ re += ch;
227
+ }
228
+ }
229
+ return new RegExp('^' + re + '$');
230
+ }
231
+
232
+ function walkFiles(root, visitor) {
233
+ const stack = [root];
234
+ let scanned = 0;
235
+ while (stack.length) {
236
+ const dir = stack.pop();
237
+ let entries;
238
+ try {
239
+ entries = fs.readdirSync(dir, { withFileTypes: true });
240
+ } catch {
241
+ continue;
242
+ }
243
+ for (const e of entries) {
244
+ if (scanned++ > MAX_SCAN_FILES) return scanned;
245
+ const full = path.join(dir, e.name);
246
+ if (e.isDirectory()) {
247
+ if (e.name === '.git' || e.name === 'node_modules') continue;
248
+ stack.push(full);
249
+ } else if (e.isFile()) {
250
+ if (visitor(full) === false) return scanned;
251
+ }
252
+ }
253
+ }
254
+ return scanned;
255
+ }
256
+
257
+ export function glob(args, ctx) {
258
+ try {
259
+ const pattern = String(args.pattern ?? '*');
260
+ const root = resolvePath(ctx.cwd, args.path || '.');
261
+ if (!fs.existsSync(root)) return { ok: false, error: `目录不存在:${root}` };
262
+ const re = globToRegExp(pattern);
263
+ const matchRel = pattern.includes('/');
264
+ const results = [];
265
+ let truncated = false;
266
+ walkFiles(root, (full) => {
267
+ if (results.length >= MAX_GLOB_RESULTS) {
268
+ truncated = true;
269
+ return false;
270
+ }
271
+ const rel = path.relative(root, full).split(path.sep).join('/');
272
+ if (re.test(matchRel ? rel : path.basename(full))) results.push(rel);
273
+ });
274
+ let output = results.join('\n');
275
+ if (!results.length) output = '(无匹配)';
276
+ if (truncated) output += `\n…[结果超过 ${MAX_GLOB_RESULTS} 条,已截断]`;
277
+ return { ok: true, output };
278
+ } catch (err) {
279
+ return { ok: false, error: `glob 失败:${err?.message || err}` };
280
+ }
281
+ }
282
+
283
+ export function grep(args, ctx) {
284
+ try {
285
+ const pattern = String(args.pattern ?? '');
286
+ if (!pattern) return { ok: false, error: '缺少 pattern 参数。' };
287
+ if (pattern.length > 500) return { ok: false, error: 'pattern 过长(>500 字符)。' };
288
+ // 拒绝嵌套量词类灾难回溯模式(如 (a+)+b),避免同步 ReDoS 卡死事件循环
289
+ if (/\([^)]*[+*][^)]*\)[+*{]/.test(pattern)) {
290
+ return { ok: false, error: 'pattern 疑似灾难性回溯(嵌套量词),请改写为等价安全形式。' };
291
+ }
292
+ let re;
293
+ try {
294
+ re = new RegExp(pattern);
295
+ } catch (e) {
296
+ return { ok: false, error: `无效的正则表达式:${e.message}` };
297
+ }
298
+ // 单行截断:超长行(如压缩的 JS/日志)截到 20KB 再匹配,防止正则开销失控
299
+ const MAX_LINE = 20 * 1024;
300
+ const testLine = (line) => (line.length > MAX_LINE ? re.test(line.slice(0, MAX_LINE)) : re.test(line));
301
+ const root = resolvePath(ctx.cwd, args.path || '.');
302
+ const include = args.include ? globToRegExp(String(args.include)) : null;
303
+ const matches = [];
304
+ let truncated = false;
305
+ let scannedFiles = 0;
306
+ walkFiles(root, (full) => {
307
+ if (include && !include.test(path.basename(full))) return;
308
+ if (matches.length >= MAX_GREP_MATCHES) {
309
+ truncated = true;
310
+ return false;
311
+ }
312
+ scannedFiles += 1;
313
+ let stat;
314
+ try {
315
+ stat = fs.statSync(full);
316
+ } catch {
317
+ return;
318
+ }
319
+ if (stat.size > MAX_FILE_BYTES) return;
320
+ let text;
321
+ try {
322
+ text = fs.readFileSync(full, 'utf8');
323
+ } catch {
324
+ return;
325
+ }
326
+ const rel = path.relative(root, full).split(path.sep).join('/');
327
+ const lines = text.split('\n');
328
+ for (let i = 0; i < lines.length; i++) {
329
+ if (testLine(lines[i])) {
330
+ matches.push(`${rel}:${i + 1}: ${lines[i].trimEnd()}`);
331
+ if (matches.length >= MAX_GREP_MATCHES) {
332
+ truncated = true;
333
+ return false;
334
+ }
335
+ }
336
+ }
337
+ });
338
+ let output = matches.length ? matches.join('\n') : '(无匹配)';
339
+ output += truncated
340
+ ? `\n…[匹配超过 ${MAX_GREP_MATCHES} 条,已截断;已扫描 ${scannedFiles} 个文件]`
341
+ : `\n[已扫描 ${scannedFiles} 个文件]`;
342
+ return { ok: true, output };
343
+ } catch (err) {
344
+ return { ok: false, error: `grep 失败:${err?.message || err}` };
345
+ }
346
+ }
@@ -0,0 +1,287 @@
1
+ // 工具注册表:模型可见的工具 Schema(OpenAI function-calling 格式)与执行分发器。
2
+ // 文件/命令工具 + 智能体工具(skill 技能加载 / task 子代理 / todo 任务清单 / undo 撤销)。
3
+
4
+ import { read, write, edit, ls, glob, grep, undo } from './fs-tools.js';
5
+ import { runBash } from './bash.js';
6
+ import { listSkills, loadSkill } from '../skills.js';
7
+
8
+ // 只读工具集合的单一来源:permissions.js 引用此导出,新增只读工具时只需改这里
9
+ export const READONLY_TOOLS = new Set(['read', 'glob', 'grep', 'ls', 'skill']);
10
+
11
+ const READ_SCHEMA = {
12
+ type: 'object',
13
+ properties: {
14
+ path: { type: 'string', description: '要读取的文件路径(相对或绝对)。' },
15
+ offset: { type: 'integer', description: '起始行号,默认 1。' },
16
+ limit: { type: 'integer', description: '最大返回行数,默认 400。' },
17
+ },
18
+ required: ['path'],
19
+ };
20
+
21
+ const WRITE_SCHEMA = {
22
+ type: 'object',
23
+ properties: {
24
+ path: { type: 'string', description: '要创建或覆盖的文件路径。' },
25
+ content: { type: 'string', description: '完整文件内容(UTF-8)。' },
26
+ },
27
+ required: ['path', 'content'],
28
+ };
29
+
30
+ const EDIT_SCHEMA = {
31
+ type: 'object',
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。' },
37
+ },
38
+ required: ['path', 'old_string', 'new_string'],
39
+ };
40
+
41
+ const LS_SCHEMA = {
42
+ type: 'object',
43
+ properties: {
44
+ path: { type: 'string', description: '目录路径,默认当前工作目录。' },
45
+ },
46
+ };
47
+
48
+ const GLOB_SCHEMA = {
49
+ type: 'object',
50
+ properties: {
51
+ pattern: { type: 'string', description: '通配符模式,如 src/**/*.js、*.md。' },
52
+ path: { type: 'string', description: '搜索根目录,默认当前工作目录。' },
53
+ },
54
+ required: ['pattern'],
55
+ };
56
+
57
+ const GREP_SCHEMA = {
58
+ type: 'object',
59
+ properties: {
60
+ pattern: { type: 'string', description: '正则表达式。' },
61
+ path: { type: 'string', description: '搜索根目录,默认当前工作目录。' },
62
+ include: { type: 'string', description: '文件名通配符过滤,如 *.js。' },
63
+ },
64
+ required: ['pattern'],
65
+ };
66
+
67
+ const BASH_SCHEMA = {
68
+ type: 'object',
69
+ properties: {
70
+ command: { type: 'string', description: '要执行的 shell 命令。执行前会请求用户授权。' },
71
+ timeout: { type: 'integer', description: '超时秒数,默认 120。' },
72
+ },
73
+ required: ['command'],
74
+ };
75
+
76
+ const SKILL_SCHEMA = {
77
+ type: 'object',
78
+ properties: {
79
+ name: { type: 'string', description: '技能名;省略时列出所有可用技能。' },
80
+ },
81
+ };
82
+
83
+ const TASK_SCHEMA = {
84
+ type: 'object',
85
+ properties: {
86
+ description: { type: 'string', description: '子任务一句话描述(用于进度展示)。' },
87
+ prompt: { type: 'string', description: '交给子代理的完整任务说明(它没有当前对话上下文)。' },
88
+ readOnly: { type: 'boolean', description: '只读调研任务(不写文件不执行命令)。多个 readOnly 子任务会并行执行。' },
89
+ },
90
+ required: ['prompt'],
91
+ };
92
+
93
+ const TODO_SCHEMA = {
94
+ type: 'object',
95
+ properties: {
96
+ todos: {
97
+ type: 'array',
98
+ description: '完整任务清单(全量替换)。',
99
+ items: {
100
+ type: 'object',
101
+ properties: {
102
+ content: { type: 'string', description: '任务描述。' },
103
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed'], description: '任务状态。' },
104
+ },
105
+ required: ['content', 'status'],
106
+ },
107
+ },
108
+ },
109
+ required: ['todos'],
110
+ };
111
+
112
+ const UNDO_SCHEMA = {
113
+ type: 'object',
114
+ properties: {
115
+ path: { type: 'string', description: '要撤销的文件;省略时撤销最近一次修改的文件。' },
116
+ },
117
+ };
118
+
119
+ const TOOLS = [
120
+ {
121
+ type: 'function',
122
+ function: {
123
+ name: 'read',
124
+ description: '读取文本文件,返回带行号的内容。修改文件前务必先读取。',
125
+ parameters: READ_SCHEMA,
126
+ },
127
+ },
128
+ {
129
+ type: 'function',
130
+ function: {
131
+ name: 'write',
132
+ description: '创建新文件或完全覆盖已有文件(自动创建父目录)。',
133
+ parameters: WRITE_SCHEMA,
134
+ },
135
+ },
136
+ {
137
+ type: 'function',
138
+ function: {
139
+ name: 'edit',
140
+ description: '对文件做精确文本替换。old_string 必须与文件内容完全一致;匹配多处时需 replace_all=true 或提供更精确上下文。',
141
+ parameters: EDIT_SCHEMA,
142
+ },
143
+ },
144
+ {
145
+ type: 'function',
146
+ function: {
147
+ name: 'ls',
148
+ description: '列出目录内容,目录以 / 结尾。',
149
+ parameters: LS_SCHEMA,
150
+ },
151
+ },
152
+ {
153
+ type: 'function',
154
+ function: {
155
+ name: 'glob',
156
+ description: '按通配符模式查找文件路径(支持 * 与 **)。',
157
+ parameters: GLOB_SCHEMA,
158
+ },
159
+ },
160
+ {
161
+ type: 'function',
162
+ function: {
163
+ name: 'grep',
164
+ description: '在文件内容中按正则表达式搜索,返回「文件:行号: 内容」。',
165
+ parameters: GREP_SCHEMA,
166
+ },
167
+ },
168
+ {
169
+ type: 'function',
170
+ function: {
171
+ name: 'bash',
172
+ description: '执行 shell 命令并返回输出(stdout/stderr/退出码)。需要用户授权。',
173
+ parameters: BASH_SCHEMA,
174
+ },
175
+ },
176
+ {
177
+ type: 'function',
178
+ function: {
179
+ name: 'skill',
180
+ description: '加载指定技能的 SKILL.md 全文;省略 name 时列出可用技能。任务与技能匹配时应先加载。',
181
+ parameters: SKILL_SCHEMA,
182
+ },
183
+ },
184
+ {
185
+ type: 'function',
186
+ function: {
187
+ name: 'task',
188
+ description: '把独立子任务委托给一个全新上下文的子代理,返回其汇报。适合搜索调研、独立实现、复核验证等可并行的工作。',
189
+ parameters: TASK_SCHEMA,
190
+ },
191
+ },
192
+ {
193
+ type: 'function',
194
+ function: {
195
+ name: 'todo',
196
+ description: '维护任务清单(全量替换)。多步骤任务开始前建立清单,完成一项更新一项。',
197
+ parameters: TODO_SCHEMA,
198
+ },
199
+ },
200
+ {
201
+ type: 'function',
202
+ function: {
203
+ name: 'undo',
204
+ description: '撤销 write/edit 造成的最近一次文件修改;指定 path 则撤销该文件。',
205
+ parameters: UNDO_SCHEMA,
206
+ },
207
+ },
208
+ ];
209
+
210
+ export function toolSchemas() {
211
+ return TOOLS;
212
+ }
213
+
214
+ function runSkill(args, ctx) {
215
+ const name = String(args.name ?? '').trim();
216
+ const skills = listSkills(ctx.workingDir);
217
+ const label = (s) => `${s.name}${s.source === 'user' ? '(用户级)' : s.source === 'builtin' ? '(内置)' : ''}`;
218
+ if (!name) {
219
+ return {
220
+ ok: true,
221
+ output: skills.length
222
+ ? skills.map((s) => `- ${label(s)}:${s.description || '(无描述)'}`).join('\n')
223
+ : '(未安装任何技能。目录:~/.mingdao/skills/、<项目>/.mingdao/skills/)',
224
+ };
225
+ }
226
+ const s = loadSkill(ctx.workingDir, name);
227
+ if (!s) {
228
+ return { ok: false, error: `未找到技能 "${name}"。可用:${skills.map((x) => x.name).join(', ') || '(无)'}` };
229
+ }
230
+ return { ok: true, output: `技能 ${name}${s.source === 'builtin' ? '(内置)' : ''}(${s.path}):\n\n${s.content}` };
231
+ }
232
+
233
+ async function runTask(args, ctx) {
234
+ const prompt = String(args.prompt ?? '');
235
+ const description = String(args.description ?? '');
236
+ if (!prompt) return { ok: false, error: '缺少 prompt 参数。' };
237
+ if (typeof ctx.spawnTask !== 'function') return { ok: false, error: '当前环境不支持子代理。' };
238
+ try {
239
+ const out = await ctx.spawnTask(prompt, { description, readOnly: args.readOnly === true });
240
+ return { ok: true, output: out };
241
+ } catch (err) {
242
+ return { ok: false, error: `子任务失败:${err?.message || err}` };
243
+ }
244
+ }
245
+
246
+ function runTodo(args, ctx) {
247
+ const list = Array.isArray(args.todos) ? args.todos : [];
248
+ if (Array.isArray(ctx.todos)) {
249
+ ctx.todos.splice(0, ctx.todos.length, ...list);
250
+ }
251
+ return {
252
+ ok: true,
253
+ output:
254
+ `任务清单已更新(${list.length} 项):\n` +
255
+ list.map((t) => `- [${t.status || 'pending'}] ${t.content}`).join('\n'),
256
+ todos: list,
257
+ };
258
+ }
259
+
260
+ export async function dispatch(name, args, ctx) {
261
+ switch (name) {
262
+ case 'read':
263
+ return read(args, ctx);
264
+ case 'write':
265
+ return write(args, ctx);
266
+ case 'edit':
267
+ return edit(args, ctx);
268
+ case 'ls':
269
+ return ls(args, ctx);
270
+ case 'glob':
271
+ return glob(args, ctx);
272
+ case 'grep':
273
+ return grep(args, ctx);
274
+ case 'bash':
275
+ return runBash(args, ctx);
276
+ case 'skill':
277
+ return runSkill(args, ctx);
278
+ case 'task':
279
+ return runTask(args, ctx);
280
+ case 'todo':
281
+ return runTodo(args, ctx);
282
+ case 'undo':
283
+ return undo(args, ctx);
284
+ default:
285
+ return { ok: false, error: `未知工具:${name}` };
286
+ }
287
+ }