mingdao-harness 0.2.4 → 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 +5 -5
- package/package.json +1 -1
- package/src/agent.js +19 -19
- package/src/audit.js +2 -2
- package/src/batch.js +7 -7
- package/src/cachestats.js +9 -9
- 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 +7 -6
- 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/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 +7 -7
- package/src/ui.js +83 -6
- package/src/update.js +8 -8
- package/src/web/attachments.js +1 -1
- 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/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
|
@@ -252,10 +252,10 @@ export function buildToolSchemas(usedNames, extra = []) {
|
|
|
252
252
|
return [...strip(TOOLS), ...strip(extra)];
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
function runSkill(args, ctx) {
|
|
255
|
+
function runSkill(/** @type {any} */ args, /** @type {any} */ ctx) {
|
|
256
256
|
const name = String(args.name ?? '').trim();
|
|
257
257
|
const skills = listSkills(ctx.workingDir);
|
|
258
|
-
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' ? '(内置)' : ''}`;
|
|
259
259
|
if (!name) {
|
|
260
260
|
return {
|
|
261
261
|
ok: true,
|
|
@@ -271,7 +271,7 @@ function runSkill(args, ctx) {
|
|
|
271
271
|
return { ok: true, output: `技能 ${name}${s.source === 'builtin' ? '(内置)' : ''}(${s.path}):\n\n${s.content}` };
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
async function runTask(args, ctx) {
|
|
274
|
+
async function runTask(/** @type {any} */ args, /** @type {any} */ ctx) {
|
|
275
275
|
const prompt = String(args.prompt ?? '');
|
|
276
276
|
const description = String(args.description ?? '');
|
|
277
277
|
if (!prompt) return { ok: false, error: '缺少 prompt 参数。' };
|
|
@@ -280,11 +280,11 @@ async function runTask(args, ctx) {
|
|
|
280
280
|
const out = await ctx.spawnTask(prompt, { description, readOnly: args.readOnly === true });
|
|
281
281
|
return { ok: true, output: out };
|
|
282
282
|
} catch (err) {
|
|
283
|
-
return { ok: false, error: `子任务失败:${err?.message || err}` };
|
|
283
|
+
return { ok: false, error: `子任务失败:${(/** @type {any} */ (err))?.message || err}` };
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
function runTodo(args, ctx) {
|
|
287
|
+
function runTodo(/** @type {any} */ args, /** @type {any} */ ctx) {
|
|
288
288
|
const list = Array.isArray(args.todos) ? args.todos : [];
|
|
289
289
|
if (Array.isArray(ctx.todos)) {
|
|
290
290
|
ctx.todos.splice(0, ctx.todos.length, ...list);
|
|
@@ -293,12 +293,12 @@ function runTodo(args, ctx) {
|
|
|
293
293
|
ok: true,
|
|
294
294
|
output:
|
|
295
295
|
`任务清单已更新(${list.length} 项):\n` +
|
|
296
|
-
list.map((t) => `- [${t.status || 'pending'}] ${t.content}`).join('\n'),
|
|
296
|
+
list.map((/** @type {any} */ t) => `- [${t.status || 'pending'}] ${t.content}`).join('\n'),
|
|
297
297
|
todos: list,
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
export async function dispatch(name, args, ctx) {
|
|
301
|
+
export async function dispatch(/** @type {any} */ name, /** @type {any} */ args, /** @type {any} */ ctx) {
|
|
302
302
|
switch (name) {
|
|
303
303
|
case 'read':
|
|
304
304
|
return read(args, ctx);
|
package/src/ui.js
CHANGED
|
@@ -26,6 +26,10 @@ export const C = {
|
|
|
26
26
|
|
|
27
27
|
const stdoutTTY = () => Boolean(process.stdout.isTTY);
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @param {any} text
|
|
31
|
+
* @param {any} code
|
|
32
|
+
*/
|
|
29
33
|
export function style(text, code) {
|
|
30
34
|
if (!stdoutTTY()) {
|
|
31
35
|
// 非 TTY(管道/重定向):剥离所有内嵌 ANSI 码,保证输出干净可解析
|
|
@@ -35,10 +39,11 @@ export function style(text, code) {
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
// ---------- 终端显示宽度(CJK 占 2 列) ----------
|
|
42
|
+
/** @param {any} s */
|
|
38
43
|
function displayWidth(s) {
|
|
39
44
|
let w = 0;
|
|
40
45
|
for (const ch of String(s)) {
|
|
41
|
-
const c = ch.codePointAt(0);
|
|
46
|
+
const c = /** @type {any} */ (ch.codePointAt(0));
|
|
42
47
|
const wide =
|
|
43
48
|
c >= 0x1100 &&
|
|
44
49
|
(c <= 0x115f ||
|
|
@@ -57,10 +62,18 @@ function displayWidth(s) {
|
|
|
57
62
|
return w;
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @param {any} s
|
|
67
|
+
* @param {any} w
|
|
68
|
+
*/
|
|
60
69
|
function padTo(s, w) {
|
|
61
70
|
return String(s) + ' '.repeat(Math.max(0, w - displayWidth(s)));
|
|
62
71
|
}
|
|
63
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {any} s
|
|
75
|
+
* @param {any} prefix
|
|
76
|
+
*/
|
|
64
77
|
function indent(s, prefix) {
|
|
65
78
|
return String(s)
|
|
66
79
|
.split('\n')
|
|
@@ -70,6 +83,7 @@ function indent(s, prefix) {
|
|
|
70
83
|
|
|
71
84
|
// ---------- 轻量代码高亮 ----------
|
|
72
85
|
const HASH_LANGS = new Set(['py', 'python', 'sh', 'bash', 'yaml', 'yml', 'toml', 'markdown', 'md']);
|
|
86
|
+
/** @type {Record<string, string[]>} */
|
|
73
87
|
const LANG_KEYWORDS = {
|
|
74
88
|
js: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'import', 'from', 'export', 'default', 'class', 'extends', 'new', 'await', 'async', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'null', 'undefined', 'true', 'false', 'this', 'super', 'switch', 'case', 'break', 'continue', 'do', 'of', 'in', 'static', 'get', 'set', 'delete', 'void', 'yield'],
|
|
75
89
|
ts: ['interface', 'type', 'enum', 'implements', 'readonly', 'private', 'public', 'protected', 'namespace', 'declare', 'as', 'any', 'unknown', 'never', 'string', 'number', 'boolean'],
|
|
@@ -83,6 +97,10 @@ const LANG_KEYWORDS = {
|
|
|
83
97
|
yaml: [],
|
|
84
98
|
};
|
|
85
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {any} code
|
|
102
|
+
* @param {any} lang
|
|
103
|
+
*/
|
|
86
104
|
export function highlightCode(code, lang = '') {
|
|
87
105
|
if (!stdoutTTY()) return code;
|
|
88
106
|
const kws = new Set([...(LANG_KEYWORDS[lang] || []), ...(LANG_KEYWORDS[lang.replace(/-.*/, '')] || [])]);
|
|
@@ -117,6 +135,7 @@ export function highlightCode(code, lang = '') {
|
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
// ---------- Markdown 行内样式 ----------
|
|
138
|
+
/** @param {any} s */
|
|
120
139
|
function inline(s) {
|
|
121
140
|
const parts = [];
|
|
122
141
|
const re = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*\s][^*]*\*)/g;
|
|
@@ -134,6 +153,7 @@ function inline(s) {
|
|
|
134
153
|
return parts.join('');
|
|
135
154
|
}
|
|
136
155
|
|
|
156
|
+
/** @param {any} line */
|
|
137
157
|
function renderInline(line) {
|
|
138
158
|
const trimmed = line.trim();
|
|
139
159
|
if (/^#{1,6}\s/.test(trimmed)) return style(inline(line), C.bold + C.cyan);
|
|
@@ -145,13 +165,16 @@ function renderInline(line) {
|
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
// ---------- 流式渲染器 ----------
|
|
168
|
+
/** @param {any} io */
|
|
148
169
|
function createStreamRenderer(io) {
|
|
149
170
|
let buf = '';
|
|
150
171
|
let inCode = false;
|
|
151
172
|
let lang = '';
|
|
173
|
+
/** @type {any[]} */
|
|
152
174
|
let codeLines = [];
|
|
153
175
|
let streamed = false;
|
|
154
176
|
|
|
177
|
+
/** @param {any} line */
|
|
155
178
|
function renderLine(line) {
|
|
156
179
|
const trimmed = line.trim();
|
|
157
180
|
if (trimmed.startsWith('```')) {
|
|
@@ -174,6 +197,7 @@ function createStreamRenderer(io) {
|
|
|
174
197
|
}
|
|
175
198
|
|
|
176
199
|
return {
|
|
200
|
+
/** @param {any} text */
|
|
177
201
|
push(text) {
|
|
178
202
|
streamed = true;
|
|
179
203
|
buf += String(text);
|
|
@@ -202,6 +226,10 @@ function createStreamRenderer(io) {
|
|
|
202
226
|
}
|
|
203
227
|
|
|
204
228
|
// ---------- 行级 diff(编辑预览用,仅小区域) ----------
|
|
229
|
+
/**
|
|
230
|
+
* @param {any} a
|
|
231
|
+
* @param {any} b
|
|
232
|
+
*/
|
|
205
233
|
function diffLines(a, b) {
|
|
206
234
|
const n = a.length;
|
|
207
235
|
const m = b.length;
|
|
@@ -253,6 +281,7 @@ const COMMANDS = [
|
|
|
253
281
|
'/quit',
|
|
254
282
|
];
|
|
255
283
|
|
|
284
|
+
/** @param {any} line */
|
|
256
285
|
function completeLine(line) {
|
|
257
286
|
if (line.startsWith('/model ') && line.length >= 7) {
|
|
258
287
|
const pre = line.slice(7);
|
|
@@ -261,6 +290,10 @@ function completeLine(line) {
|
|
|
261
290
|
return [COMMANDS.filter((c) => c.startsWith(line)), line];
|
|
262
291
|
}
|
|
263
292
|
|
|
293
|
+
/**
|
|
294
|
+
* @param {any} name
|
|
295
|
+
* @param {any} args
|
|
296
|
+
*/
|
|
264
297
|
function summarizeArgs(name, args) {
|
|
265
298
|
try {
|
|
266
299
|
if (name === 'bash') return ` ${args.command ?? ''}`;
|
|
@@ -274,12 +307,19 @@ function summarizeArgs(name, args) {
|
|
|
274
307
|
|
|
275
308
|
// ---------- io 工厂 ----------
|
|
276
309
|
export function createIO({ quiet = false } = {}) {
|
|
310
|
+
/** @type {any} */
|
|
277
311
|
let rl = null;
|
|
312
|
+
/** @type {any} */
|
|
278
313
|
let spinnerTimer = null;
|
|
314
|
+
/** @type {any} */
|
|
279
315
|
let renderer = null;
|
|
316
|
+
/** @type {any[]} */
|
|
280
317
|
let askQueue = []; // 非 TTY:挂起的输入请求
|
|
318
|
+
/** @type {any[]} */
|
|
281
319
|
let lineQueue = []; // 非 TTY:已缓冲但未被消费的行
|
|
320
|
+
/** @type {any} */
|
|
282
321
|
let sigintHandler = null; // TTY 下 rl 'SIGINT' 事件转发目标
|
|
322
|
+
/** @type {any} */
|
|
283
323
|
let ttyPending = null;
|
|
284
324
|
let closed = false;
|
|
285
325
|
|
|
@@ -291,6 +331,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
291
331
|
isTTY: Boolean(process.stdin.isTTY),
|
|
292
332
|
showReasoning: true,
|
|
293
333
|
|
|
334
|
+
/** @param {any} v */
|
|
294
335
|
setShowReasoning(v) {
|
|
295
336
|
io.showReasoning = !!v;
|
|
296
337
|
},
|
|
@@ -304,7 +345,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
304
345
|
output: process.stdout,
|
|
305
346
|
terminal,
|
|
306
347
|
historySize: 200,
|
|
307
|
-
completer: (line) => completeLine(line),
|
|
348
|
+
completer: (/** @type {any} */ line) => completeLine(line),
|
|
308
349
|
});
|
|
309
350
|
if (terminal) {
|
|
310
351
|
// 关键:TTY 下 Ctrl+C 由 readline 捕获(raw 模式),需要在这里转发给中断处理器;
|
|
@@ -321,7 +362,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
321
362
|
}
|
|
322
363
|
if (!terminal) {
|
|
323
364
|
// 非 TTY(管道输入):持久 line 监听 + 队列,避免 rl.question 丢弃首行之后的输入
|
|
324
|
-
rl.on('line', (line) => {
|
|
365
|
+
rl.on('line', (/** @type {any} */ line) => {
|
|
325
366
|
const trimmed = line.trim();
|
|
326
367
|
const pending = askQueue.shift();
|
|
327
368
|
if (pending) pending.resolve(trimmed);
|
|
@@ -344,6 +385,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
344
385
|
return rl;
|
|
345
386
|
},
|
|
346
387
|
|
|
388
|
+
/** @param {any} lines */
|
|
347
389
|
setHistory(lines) {
|
|
348
390
|
try {
|
|
349
391
|
const r = io.ensureRl();
|
|
@@ -357,6 +399,10 @@ export function createIO({ quiet = false } = {}) {
|
|
|
357
399
|
if (!quiet) console.log(text);
|
|
358
400
|
},
|
|
359
401
|
|
|
402
|
+
/**
|
|
403
|
+
* @param {any} title
|
|
404
|
+
* @param {any} lines
|
|
405
|
+
*/
|
|
360
406
|
box(title, lines) {
|
|
361
407
|
if (quiet) return;
|
|
362
408
|
if (!stdoutTTY()) {
|
|
@@ -389,6 +435,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
389
435
|
io.stopSpinner();
|
|
390
436
|
},
|
|
391
437
|
|
|
438
|
+
/** @param {any} label */
|
|
392
439
|
startSpinner(label) {
|
|
393
440
|
if (quiet || !stdoutTTY()) return;
|
|
394
441
|
io.stopSpinner();
|
|
@@ -408,6 +455,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
408
455
|
}
|
|
409
456
|
},
|
|
410
457
|
|
|
458
|
+
/** @param {any} text */
|
|
411
459
|
writeText(text) {
|
|
412
460
|
if (quiet) return;
|
|
413
461
|
if (io._reasoningStarted && !io._answerStarted && stdoutTTY()) {
|
|
@@ -417,6 +465,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
417
465
|
renderer?.push(String(text));
|
|
418
466
|
},
|
|
419
467
|
|
|
468
|
+
/** @param {any} text */
|
|
420
469
|
writeReasoning(text) {
|
|
421
470
|
if (quiet || !io.showReasoning) return;
|
|
422
471
|
if (!io._reasoningStarted) {
|
|
@@ -426,6 +475,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
426
475
|
process.stdout.write(style(String(text), C.dim));
|
|
427
476
|
},
|
|
428
477
|
|
|
478
|
+
/** @param {any} todos */
|
|
429
479
|
renderTodo(todos) {
|
|
430
480
|
if (quiet || !todos?.length) return;
|
|
431
481
|
io.print(style(' ☑ 任务清单', C.bold));
|
|
@@ -436,6 +486,10 @@ export function createIO({ quiet = false } = {}) {
|
|
|
436
486
|
}
|
|
437
487
|
},
|
|
438
488
|
|
|
489
|
+
/**
|
|
490
|
+
* @param {any} code
|
|
491
|
+
* @param {any} lang
|
|
492
|
+
*/
|
|
439
493
|
printCodeBlock(code, lang) {
|
|
440
494
|
if (quiet) return;
|
|
441
495
|
const label = lang || 'code';
|
|
@@ -446,6 +500,12 @@ export function createIO({ quiet = false } = {}) {
|
|
|
446
500
|
},
|
|
447
501
|
|
|
448
502
|
// —— 工具执行可视化 ——
|
|
503
|
+
/**
|
|
504
|
+
* @param {any} name
|
|
505
|
+
* @param {any} args
|
|
506
|
+
* @param {any} result
|
|
507
|
+
* @param {any} durationMs
|
|
508
|
+
*/
|
|
449
509
|
renderTool(name, args, result, durationMs) {
|
|
450
510
|
if (quiet) return;
|
|
451
511
|
const r = result && typeof result === 'object' ? result : { ok: false, error: String(result ?? '') };
|
|
@@ -525,6 +585,11 @@ export function createIO({ quiet = false } = {}) {
|
|
|
525
585
|
io.print(style(` ✓ 输出 ${out.length} 字符${ms}`, C.green));
|
|
526
586
|
},
|
|
527
587
|
|
|
588
|
+
/**
|
|
589
|
+
* @param {any} name
|
|
590
|
+
* @param {any} args
|
|
591
|
+
* @param {any} reason
|
|
592
|
+
*/
|
|
528
593
|
renderToolDenied(name, args, reason) {
|
|
529
594
|
if (quiet) return;
|
|
530
595
|
const label = reason === '未授权' || !reason ? '未授权,已跳过' : reason;
|
|
@@ -532,11 +597,12 @@ export function createIO({ quiet = false } = {}) {
|
|
|
532
597
|
},
|
|
533
598
|
|
|
534
599
|
// —— 用量与费用状态行 ——
|
|
600
|
+
/** @param {{ modelName: any, usage: any, durationMs: any }} param0 */
|
|
535
601
|
printUsageLine({ modelName, usage, durationMs }) {
|
|
536
602
|
if (quiet) return;
|
|
537
603
|
const p = usage?.prompt_tokens ?? 0;
|
|
538
604
|
const c = usage?.completion_tokens ?? 0;
|
|
539
|
-
const fmt = (n) => (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n));
|
|
605
|
+
const fmt = (/** @type {any} */ n) => (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n));
|
|
540
606
|
const secs = ((durationMs ?? 0) / 1000).toFixed(1) + 's';
|
|
541
607
|
const costPart = estimateCostLabel(modelName, p, c, usage);
|
|
542
608
|
io.print(
|
|
@@ -550,6 +616,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
550
616
|
// —— SIGINT(Ctrl+C 中断生成,不退出程序) ——
|
|
551
617
|
// TTY 下 readline raw 模式会把 Ctrl+C 转成 rl 的 'SIGINT' 事件(ensureRl 转发到 sigintHandler),
|
|
552
618
|
// 非 TTY/其他来源走 process 级 SIGINT;两层都注册,保证任何路径都能中断
|
|
619
|
+
/** @param {any} fn */
|
|
553
620
|
onSigint(fn) {
|
|
554
621
|
const h = () => {
|
|
555
622
|
try {
|
|
@@ -564,6 +631,10 @@ export function createIO({ quiet = false } = {}) {
|
|
|
564
631
|
};
|
|
565
632
|
},
|
|
566
633
|
|
|
634
|
+
/**
|
|
635
|
+
* @param {any} question
|
|
636
|
+
* @param {any} opts
|
|
637
|
+
*/
|
|
567
638
|
ask(question, opts = {}) {
|
|
568
639
|
if (!process.stdin.isTTY) {
|
|
569
640
|
// 非 TTY:优先消费已缓冲的行(接口关闭后仍可能有剩余行),不打印提示符
|
|
@@ -592,7 +663,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
592
663
|
}
|
|
593
664
|
const entry = { resolve, restore };
|
|
594
665
|
ttyPending = entry;
|
|
595
|
-
r.question(question, (answer) => {
|
|
666
|
+
r.question(question, (/** @type {any} */ answer) => {
|
|
596
667
|
restore();
|
|
597
668
|
if (ttyPending === entry) ttyPending = null;
|
|
598
669
|
resolve(answer.trim());
|
|
@@ -601,6 +672,7 @@ export function createIO({ quiet = false } = {}) {
|
|
|
601
672
|
},
|
|
602
673
|
|
|
603
674
|
// 行尾反斜杠续行,支持多行输入
|
|
675
|
+
/** @param {any} prompt */
|
|
604
676
|
async askMultiline(prompt, continuation = '…> ') {
|
|
605
677
|
const lines = [];
|
|
606
678
|
for (;;) {
|
|
@@ -615,14 +687,19 @@ export function createIO({ quiet = false } = {}) {
|
|
|
615
687
|
}
|
|
616
688
|
},
|
|
617
689
|
|
|
690
|
+
/** @param {any} question */
|
|
618
691
|
async confirm(question) {
|
|
619
692
|
const a = await io.ask(question + ' ');
|
|
620
693
|
return /^y(es)?$/i.test(a);
|
|
621
694
|
},
|
|
622
695
|
|
|
696
|
+
/**
|
|
697
|
+
* @param {any} label
|
|
698
|
+
* @param {any} options
|
|
699
|
+
*/
|
|
623
700
|
async choose(label, options) {
|
|
624
701
|
io.print(style(label, C.bold));
|
|
625
|
-
options.forEach((o, i) => io.print(` ${style(String(i + 1), C.cyan)}. ${o.label}`));
|
|
702
|
+
options.forEach((/** @type {any} */ o, /** @type {any} */ i) => io.print(` ${style(String(i + 1), C.cyan)}. ${o.label}`));
|
|
626
703
|
for (;;) {
|
|
627
704
|
const ans = await io.ask('请输入序号:');
|
|
628
705
|
const n = Number(ans);
|
package/src/update.js
CHANGED
|
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
import { mingdaoHome, ensureHome } from './config.js';
|
|
13
13
|
import { atomicWriteFileSync } from './atomic-write.js';
|
|
14
14
|
|
|
15
|
-
function git(args, cwd) {
|
|
15
|
+
function git(/** @type {any} */ args, /** @type {any} */ cwd) {
|
|
16
16
|
const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 120000 });
|
|
17
17
|
return {
|
|
18
18
|
ok: r.status === 0 && !r.error,
|
|
@@ -36,11 +36,11 @@ export function findRepoRoot() {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function resolveRepo(override) {
|
|
39
|
+
function resolveRepo(/** @type {any} */ override) {
|
|
40
40
|
return override || findRepoRoot();
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function versionAt(repo, ref = 'HEAD') {
|
|
43
|
+
function versionAt(/** @type {any} */ repo, ref = 'HEAD') {
|
|
44
44
|
const r = git(['show', `${ref}:package.json`], repo);
|
|
45
45
|
if (!r.ok) return null;
|
|
46
46
|
try {
|
|
@@ -50,7 +50,7 @@ function versionAt(repo, ref = 'HEAD') {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function compareVersions(a, b) {
|
|
53
|
+
function compareVersions(/** @type {any} */ a, /** @type {any} */ b) {
|
|
54
54
|
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
55
55
|
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
|
|
56
56
|
for (let i = 0; i < 3; i++) {
|
|
@@ -71,7 +71,7 @@ function readState() {
|
|
|
71
71
|
return null;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
function writeState(s) {
|
|
74
|
+
function writeState(/** @type {any} */ s) {
|
|
75
75
|
ensureHome();
|
|
76
76
|
atomicWriteFileSync(stateFile(), JSON.stringify(s, null, 2) + '\n', { mode: 0o600 }); // 质检 H4:更新状态原子写
|
|
77
77
|
}
|
|
@@ -79,20 +79,20 @@ function writeState(s) {
|
|
|
79
79
|
// 直接向远端指定分支要版本(评估 D6):fetch 到 FETCH_HEAD 再读 package.json,
|
|
80
80
|
// 不依赖本地 refs 落盘——部分 Windows git 环境存在「fetch 成功但 refs 不更新」的异常,
|
|
81
81
|
// 按本地 refs 判断会误报「已是最新」。ls-remote 思路的等价实现。
|
|
82
|
-
function remoteVersion(repo, remote, branch) {
|
|
82
|
+
function remoteVersion(/** @type {any} */ repo, /** @type {any} */ remote, /** @type {any} */ branch) {
|
|
83
83
|
const r = git(['fetch', '--quiet', remote, branch], repo);
|
|
84
84
|
if (!r.ok) return null;
|
|
85
85
|
return versionAt(repo, 'FETCH_HEAD');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function listRemotes(repo) {
|
|
88
|
+
function listRemotes(/** @type {any} */ repo) {
|
|
89
89
|
const r = git(['remote'], repo);
|
|
90
90
|
if (!r.ok) return [];
|
|
91
91
|
return r.out.split('\n').filter(Boolean);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// 各远端(main/master 任一存在)中版本号最大的(多镜像部署:github/gitee/gitcode 谁新跟谁)
|
|
95
|
-
function newestRemote(repo) {
|
|
95
|
+
function newestRemote(/** @type {any} */ repo) {
|
|
96
96
|
let latest = '';
|
|
97
97
|
let ref = '';
|
|
98
98
|
for (const remote of listRemotes(repo)) {
|
package/src/web/attachments.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const MAX_IMAGE_DATAURL = 7 * 1024 * 1024; // base64 膨胀 1.33 倍,对应约 5MB 原图
|
|
8
8
|
const MAX_TEXT = 200 * 1024;
|
|
9
9
|
|
|
10
|
-
export function buildUserContent(message, attachments, visionSupported) {
|
|
10
|
+
export function buildUserContent(/** @type {any} */ message, /** @type {any} */ attachments, /** @type {any} */ visionSupported) {
|
|
11
11
|
const text = String(message ?? '').trim();
|
|
12
12
|
const list = Array.isArray(attachments) ? attachments.slice(0, 4) : [];
|
|
13
13
|
const imageParts = [];
|