mingdao-harness 0.2.4 → 0.2.6
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/EVALUATION-ROUND2.md +58 -0
- package/docs/ROADMAP-NEXT.md +5 -5
- package/package.json +2 -2
- package/src/agent.js +74 -36
- package/src/atomic-write.js +8 -2
- package/src/audit.js +2 -2
- package/src/batch.js +7 -7
- package/src/cachestats.js +9 -9
- package/src/cli.js +42 -688
- 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 +37 -21
- package/src/cost-guard.js +14 -6
- 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 +13 -11
- package/src/routing.js +8 -8
- package/src/schedule.js +30 -30
- 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 +14 -12
- package/src/sync-server.js +86 -3
- package/src/sync.js +53 -20
- package/src/tasks/worker.js +126 -0
- package/src/tasks.js +14 -14
- package/src/titles.js +5 -5
- package/src/tokenizer.js +23 -5
- 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/app.js +16 -5
- package/src/web/attachments.js +1 -1
- package/src/web/index.html +21 -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 +129 -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 +119 -0
- package/src/web/server.js +73 -31
- 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
|
@@ -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);
|