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/commands/schedule.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { listTasks, killTask, formatTaskRow } from '../tasks.js';
|
|
15
15
|
import { ensureHome } from '../config.js';
|
|
16
16
|
|
|
17
|
-
function printTasks(home) {
|
|
17
|
+
function printTasks(/** @type {any} */ home) {
|
|
18
18
|
const tasks = listTasks(home);
|
|
19
19
|
if (!tasks.length) {
|
|
20
20
|
console.log('暂无任务。启动:mingdao run "<任务>"');
|
|
@@ -26,7 +26,7 @@ function printTasks(home) {
|
|
|
26
26
|
console.log(running ? `\n${running} 个运行中 · mingdao tasks watch 实时刷新 · kill <id> 停止` : '\n无运行中任务');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async function watchTasks(home) {
|
|
29
|
+
async function watchTasks(/** @type {any} */ home) {
|
|
30
30
|
if (!process.stdout.isTTY) {
|
|
31
31
|
printTasks(home);
|
|
32
32
|
return;
|
|
@@ -45,7 +45,7 @@ async function watchTasks(home) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export async function handleTasks(cmd, args) {
|
|
48
|
+
export async function handleTasks(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
49
49
|
const home0 = ensureHome();
|
|
50
50
|
reconcileSchedules(home0);
|
|
51
51
|
const sub = args[0];
|
|
@@ -67,7 +67,7 @@ export async function handleTasks(cmd, args) {
|
|
|
67
67
|
return true;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export async function handleSchedule(cmd, args) {
|
|
70
|
+
export async function handleSchedule(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
71
71
|
const home0 = ensureHome();
|
|
72
72
|
reconcileSchedules(home0);
|
|
73
73
|
const sub = args[0];
|
|
@@ -86,7 +86,7 @@ export async function handleSchedule(cmd, args) {
|
|
|
86
86
|
let at = null;
|
|
87
87
|
let every = null;
|
|
88
88
|
let anchor = null;
|
|
89
|
-
let after = [];
|
|
89
|
+
let /** @type {any} */ after = [];
|
|
90
90
|
let permission = null;
|
|
91
91
|
let model = null;
|
|
92
92
|
let offpeak = false;
|
|
@@ -106,7 +106,7 @@ export async function handleSchedule(cmd, args) {
|
|
|
106
106
|
process.exitCode = 1;
|
|
107
107
|
return true;
|
|
108
108
|
}
|
|
109
|
-
const r = addSchedule(home0, question, { at, every, after, permission, model, cwd: process.cwd(), anchor, offpeak });
|
|
109
|
+
const r = addSchedule(home0, question, { at, every, /** @type {any} */ after, permission, model, cwd: process.cwd(), anchor, offpeak });
|
|
110
110
|
if (r.error) {
|
|
111
111
|
console.log('[错误] ' + r.error);
|
|
112
112
|
process.exitCode = 1;
|
|
@@ -162,7 +162,7 @@ export async function handleSchedule(cmd, args) {
|
|
|
162
162
|
process.exitCode = 1;
|
|
163
163
|
return true;
|
|
164
164
|
}
|
|
165
|
-
const r = chainSchedules(home0, rest);
|
|
165
|
+
const r = /** @type {any} */ (chainSchedules(home0, rest));
|
|
166
166
|
if (r.error) {
|
|
167
167
|
console.log('[错误] ' + r.error);
|
|
168
168
|
process.exitCode = 1;
|
package/src/commands/skill.js
CHANGED
|
@@ -18,14 +18,14 @@ function askAutoStart() {
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export async function handleSkill(cmd, args) {
|
|
21
|
+
export async function handleSkill(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
22
22
|
const sub = args[0] || 'list';
|
|
23
23
|
const arg = args[1];
|
|
24
24
|
if (sub === 'search') {
|
|
25
25
|
const local = searchLibrary(arg || '');
|
|
26
26
|
const remote = await searchRegistry(arg || '');
|
|
27
27
|
const localNames = new Set(local.map((s) => s.name));
|
|
28
|
-
const remoteOnly = remote.skills ? remote.skills.filter((s) => !localNames.has(s.name)) : [];
|
|
28
|
+
const remoteOnly = remote.skills ? remote.skills.filter((/** @type {any} */ s) => !localNames.has(s.name)) : [];
|
|
29
29
|
console.log(
|
|
30
30
|
`技能库匹配:内置 ${local.length}${remote.error ? '' : ` + 线上 ${remoteOnly.length}`} · 安装:mingdao skill install <名称>`
|
|
31
31
|
);
|
|
@@ -41,15 +41,17 @@ export async function handleSkill(cmd, args) {
|
|
|
41
41
|
if (sub === 'install') {
|
|
42
42
|
const r = await installSkill(arg);
|
|
43
43
|
if (r.error) {
|
|
44
|
-
console.log('[错误] ' + r.error);
|
|
44
|
+
console.log('[错误] ' + (/** @type {any} */ (r)).error);
|
|
45
45
|
process.exitCode = 1;
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
48
|
-
if (r.names) {
|
|
49
|
-
|
|
48
|
+
if ((/** @type {any} */ (r)).names) {
|
|
49
|
+
const rn = /** @type {any} */ (r);
|
|
50
|
+
console.log(`✓ 已从 git 仓库安装 ${rn.names.length} 个技能:${rn.names.join(', ')}`);
|
|
50
51
|
} else {
|
|
51
|
-
const
|
|
52
|
-
|
|
52
|
+
const rn = /** @type {any} */ (r);
|
|
53
|
+
const srcLabel = rn.host ? '(线上 registry)' : '';
|
|
54
|
+
console.log(`✓ 已安装技能 ${rn.name}${srcLabel} → ~/.mingdao/skills/${rn.name}/(可编辑/删除,下次会话生效)`);
|
|
53
55
|
}
|
|
54
56
|
return true;
|
|
55
57
|
}
|
|
@@ -61,7 +63,7 @@ export async function handleSkill(cmd, args) {
|
|
|
61
63
|
}
|
|
62
64
|
const r = uninstallSkill(arg);
|
|
63
65
|
if (r.error) {
|
|
64
|
-
console.log('[错误] ' + r.error);
|
|
66
|
+
console.log('[错误] ' + (/** @type {any} */ (r)).error);
|
|
65
67
|
process.exitCode = 1;
|
|
66
68
|
return true;
|
|
67
69
|
}
|
|
@@ -76,11 +78,11 @@ export async function handleSkill(cmd, args) {
|
|
|
76
78
|
}
|
|
77
79
|
const r = await reinstallSkill(arg);
|
|
78
80
|
if (r.error) {
|
|
79
|
-
console.log('[错误] ' + r.error);
|
|
81
|
+
console.log('[错误] ' + (/** @type {any} */ (r)).error);
|
|
80
82
|
process.exitCode = 1;
|
|
81
83
|
return true;
|
|
82
84
|
}
|
|
83
|
-
console.log(`✓ 已更新技能 ${r.name}`);
|
|
85
|
+
console.log(`✓ 已更新技能 ${(/** @type {any} */ (r)).name}`);
|
|
84
86
|
return true;
|
|
85
87
|
}
|
|
86
88
|
if (sub === 'trust') {
|
|
@@ -91,7 +93,7 @@ export async function handleSkill(cmd, args) {
|
|
|
91
93
|
}
|
|
92
94
|
const r = trustSkill(arg);
|
|
93
95
|
if (r.error) {
|
|
94
|
-
console.log('[错误] ' + r.error);
|
|
96
|
+
console.log('[错误] ' + (/** @type {any} */ (r)).error);
|
|
95
97
|
process.exitCode = 1;
|
|
96
98
|
return true;
|
|
97
99
|
}
|
|
@@ -116,7 +118,7 @@ export async function handleSkill(cmd, args) {
|
|
|
116
118
|
// WebUI:mingdao web [端口] [--auth-token <令牌>] [--autostart|--no-autostart](评估 P3-1:参数结构不合法的按提问处理)
|
|
117
119
|
// --autostart/--no-autostart:写 config.web.autoStart——开启后每次运行 mingdao 会自动后台拉起 WebUI;
|
|
118
120
|
// 首次交互运行且未设置时,询问一次「下次是否自动启动」。
|
|
119
|
-
export async function handleWeb(cmd, args) {
|
|
121
|
+
export async function handleWeb(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
120
122
|
let portIndex = -1;
|
|
121
123
|
let tokenSeen = false;
|
|
122
124
|
let autoChoice; // undefined=未指定;true/false=显式指定
|
|
@@ -165,7 +167,7 @@ export async function handleWeb(cmd, args) {
|
|
|
165
167
|
const host = cfg0?.web?.host || '127.0.0.1';
|
|
166
168
|
// 访问令牌优先级:--auth-token 参数 > 环境变量 MINGDAO_WEB_TOKEN > config.json 的 web.token
|
|
167
169
|
let authToken = process.env.MINGDAO_WEB_TOKEN || cfg0?.web?.token || undefined;
|
|
168
|
-
const atIdx = args.findIndex((a) => typeof a === 'string' && a.startsWith('--auth-token'));
|
|
170
|
+
const atIdx = args.findIndex((/** @type {any} */ a) => typeof a === 'string' && a.startsWith('--auth-token'));
|
|
169
171
|
if (atIdx !== -1) {
|
|
170
172
|
const raw = args[atIdx];
|
|
171
173
|
authToken = raw.includes('=') ? raw.slice(raw.indexOf('=') + 1) : args[atIdx + 1];
|
|
@@ -179,14 +181,14 @@ export async function handleWeb(cmd, args) {
|
|
|
179
181
|
await runWebServer({ host, port, authToken });
|
|
180
182
|
} catch (err) {
|
|
181
183
|
// 质检 C2:listen 现在会 reject(如端口占用),CLI 给出明确提示而非挂起
|
|
182
|
-
console.error(`[MingDao] WebUI 启动失败:${err?.message || err}`);
|
|
184
|
+
console.error(`[MingDao] WebUI 启动失败:${(/** @type {any} */ (err))?.message || err}`);
|
|
183
185
|
process.exitCode = 1;
|
|
184
186
|
}
|
|
185
187
|
return true;
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
// 会话检索:mingdao sessions search <关键词>
|
|
189
|
-
export async function handleSessions(cmd, args) {
|
|
191
|
+
export async function handleSessions(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
190
192
|
if (args[0] !== 'search') return false;
|
|
191
193
|
const kw = args.slice(1).join(' ').trim();
|
|
192
194
|
if (!kw) {
|
|
@@ -200,7 +202,7 @@ export async function handleSessions(cmd, args) {
|
|
|
200
202
|
else {
|
|
201
203
|
console.log(`找到 ${hits.length} 个会话:`);
|
|
202
204
|
for (const h of hits) {
|
|
203
|
-
console.log(` ${h.name}(${relativeTime(h.mtime)})\n ${h.snippet}`);
|
|
205
|
+
console.log(` ${h.name}(${relativeTime(h.mtime)})\n ${(/** @type {any} */ (h)).snippet}`);
|
|
204
206
|
}
|
|
205
207
|
console.log(`\n恢复:mingdao --resume(选择器中可见全部会话)`);
|
|
206
208
|
}
|
package/src/commands/sync.js
CHANGED
|
@@ -16,13 +16,13 @@ import {
|
|
|
16
16
|
resolveSyncConflict,
|
|
17
17
|
} from '../sync.js';
|
|
18
18
|
|
|
19
|
-
async function askHidden(question) {
|
|
19
|
+
async function askHidden(/** @type {any} */ question) {
|
|
20
20
|
return new Promise((resolve) => {
|
|
21
21
|
// _writeToOutput 为 readline 内部接口:静音回显(密码输入),类型护栏下显式 any
|
|
22
22
|
const rl = /** @type {any} */ (readline.createInterface({ input: process.stdin, output: process.stdout }));
|
|
23
23
|
const orig = rl._writeToOutput;
|
|
24
24
|
rl._writeToOutput = () => {};
|
|
25
|
-
rl.question(question, (a) => {
|
|
25
|
+
rl.question(question, (/** @type {any} */ a) => {
|
|
26
26
|
if (typeof orig === 'function') rl._writeToOutput = orig;
|
|
27
27
|
rl.close();
|
|
28
28
|
resolve(a.trim());
|
|
@@ -30,7 +30,7 @@ async function askHidden(question) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export async function handleSync(cmd, args) {
|
|
33
|
+
export async function handleSync(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
34
34
|
const sub = args[0] || 'status';
|
|
35
35
|
if (sub === 'login') {
|
|
36
36
|
const username = args[1];
|
|
@@ -191,7 +191,7 @@ export async function handleSync(cmd, args) {
|
|
|
191
191
|
return true;
|
|
192
192
|
}
|
|
193
193
|
if (sub === 'push') {
|
|
194
|
-
const r = await syncPush(args[1]);
|
|
194
|
+
const r = /** @type {any} */ (await syncPush(args[1]));
|
|
195
195
|
if (r.error) {
|
|
196
196
|
console.log('[错误] ' + r.error);
|
|
197
197
|
process.exitCode = 1;
|
|
@@ -201,7 +201,7 @@ export async function handleSync(cmd, args) {
|
|
|
201
201
|
return true;
|
|
202
202
|
}
|
|
203
203
|
if (sub === 'pull') {
|
|
204
|
-
const r = await syncPull(args[1]);
|
|
204
|
+
const r = /** @type {any} */ (await syncPull(args[1]));
|
|
205
205
|
if (r.error) {
|
|
206
206
|
console.log('[错误] ' + r.error);
|
|
207
207
|
process.exitCode = 1;
|
package/src/commands/update.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { loadConfig } from '../config.js';
|
|
6
6
|
|
|
7
|
-
export async function handleUpdateFamily(cmd, args) {
|
|
7
|
+
export async function handleUpdateFamily(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
8
8
|
// 价格表外置刷新(Hermes C1):mingdao update --pricing —— 从 cfg.pricing.source 拉取官方价格 JSON
|
|
9
9
|
if (cmd === 'update' && args.includes('--pricing')) {
|
|
10
10
|
const cfg = loadConfig();
|
|
@@ -17,7 +17,7 @@ export async function handleUpdateFamily(cmd, args) {
|
|
|
17
17
|
}
|
|
18
18
|
// 自更新与回滚(git 安装形态)。
|
|
19
19
|
// 评估 P3-1 子命令劫持防护:参数不合法时回退为普通提问(如 "mingdao update the docs" 是提问而非更新命令)
|
|
20
|
-
if (cmd === 'update' && args.every((a) => a === '--check')) {
|
|
20
|
+
if (cmd === 'update' && args.every((/** @type {any} */ a) => a === '--check')) {
|
|
21
21
|
const { updateCheck, mingdaoUpdate } = await import('../update.js');
|
|
22
22
|
const checkOnly = args.includes('--check');
|
|
23
23
|
const r = await (checkOnly ? updateCheck() : mingdaoUpdate());
|
|
@@ -64,7 +64,7 @@ export async function handleUpdateFamily(cmd, args) {
|
|
|
64
64
|
const raw = srcFile === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(srcFile, 'utf8');
|
|
65
65
|
questions = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, 1000);
|
|
66
66
|
} catch (err) {
|
|
67
|
-
console.log('[错误] 读取问题文件失败:' + (err?.message || err));
|
|
67
|
+
console.log('[错误] 读取问题文件失败:' + ((/** @type {any} */ (err))?.message || err));
|
|
68
68
|
process.exitCode = 1;
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
@@ -84,7 +84,7 @@ export async function handleUpdateFamily(cmd, args) {
|
|
|
84
84
|
maxTokens,
|
|
85
85
|
maxCost,
|
|
86
86
|
signal: ac.signal,
|
|
87
|
-
onStatus: (st) => console.log(' ' + st),
|
|
87
|
+
onStatus: (/** @type {any} */ st) => console.log(' ' + st),
|
|
88
88
|
});
|
|
89
89
|
process.removeAllListeners('SIGINT');
|
|
90
90
|
if (r.error) {
|
|
@@ -92,7 +92,8 @@ export async function handleUpdateFamily(cmd, args) {
|
|
|
92
92
|
process.exitCode = 1;
|
|
93
93
|
return true;
|
|
94
94
|
}
|
|
95
|
-
|
|
95
|
+
const rr = /** @type {any} */ (r);
|
|
96
|
+
console.log(`✓ 完成 ${rr.results.length} 条 · ↑${rr.usage.prompt_tokens} ↓${rr.usage.completion_tokens} tokens · 费用 ≈¥${rr.cost.toFixed(5)}(已按半价)`);
|
|
96
97
|
if (r.deduped) console.log(` (去重合并 ${r.deduped} 条重复问题,结果已回填全部位置)`);
|
|
97
98
|
console.log(` 结果文件:${r.outputFile}`);
|
|
98
99
|
console.log(` 任务 ID:${r.batchId}(结果已计入 /cost 分账)`);
|
|
@@ -119,7 +120,7 @@ export async function handleUpdateFamily(cmd, args) {
|
|
|
119
120
|
console.log('月度报告导出:mingdao cost report [YYYY-MM|all]');
|
|
120
121
|
return true;
|
|
121
122
|
}
|
|
122
|
-
const bar = (cost, max) => '█'.repeat(Math.max(1, Math.round((cost / max) * 20)));
|
|
123
|
+
const bar = (/** @type {any} */ cost, /** @type {any} */ max) => '█'.repeat(Math.max(1, Math.round((cost / max) * 20)));
|
|
123
124
|
const md = [];
|
|
124
125
|
for (const r of rows) {
|
|
125
126
|
const maxDay = Math.max(...r.days.map((d) => d.cost), 1e-9);
|
|
@@ -3,7 +3,7 @@ import { addWorkspace, listWorkspaces, removeWorkspace, workspacePath, touchWork
|
|
|
3
3
|
import { presetList, buildPreset } from '../mcp-presets.js';
|
|
4
4
|
import { ensureHome, loadConfig, saveConfig } from '../config.js';
|
|
5
5
|
|
|
6
|
-
export async function handleWorkspace(cmd, args) {
|
|
6
|
+
export async function handleWorkspace(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
7
7
|
const sub = args[0] || 'list';
|
|
8
8
|
const name = args[1];
|
|
9
9
|
if (sub === 'add') {
|
|
@@ -73,7 +73,7 @@ export async function handleWorkspace(cmd, args) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// MCP 预设:mingdao mcp preset list|add <名称> [参数]
|
|
76
|
-
export async function handleMcp(cmd, args) {
|
|
76
|
+
export async function handleMcp(/** @type {any} */ cmd, /** @type {any} */ args) {
|
|
77
77
|
if (args[0] !== 'preset') return false;
|
|
78
78
|
const home0 = ensureHome();
|
|
79
79
|
if (args[1] === 'add') {
|
package/src/compact.js
CHANGED
|
@@ -23,7 +23,7 @@ const SUMMARY_SYSTEM =
|
|
|
23
23
|
'保留用户目标与关键要求、已完成的步骤与结论、修改/创建的文件、未完成事项、重要约定与决策;' +
|
|
24
24
|
'省略已完成的中间过程与细节。只输出 JSON:{"summary": "摘要内容"}。';
|
|
25
25
|
|
|
26
|
-
export async function summarizeConversation(provider, model, convoText) {
|
|
26
|
+
export async function summarizeConversation(/** @type {any} */ provider, /** @type {any} */ model, /** @type {any} */ convoText) {
|
|
27
27
|
const base = {
|
|
28
28
|
model,
|
|
29
29
|
messages: [
|
|
@@ -54,7 +54,7 @@ export async function summarizeConversation(provider, model, convoText) {
|
|
|
54
54
|
return { text: text.slice(0, SUMMARY_MAX_CHARS), usage };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export async function compactConversation({ messages, budget, count, provider, executorModel, triggerRatio, force = false }) {
|
|
57
|
+
export async function compactConversation(/** @type {any} */ { messages, budget, count, provider, executorModel, triggerRatio, force = false }) {
|
|
58
58
|
if (!messages.length) return null;
|
|
59
59
|
// 各消息 token 与总量;低于触发线(默认预算 80%)无需压缩
|
|
60
60
|
const sizes = [];
|
|
@@ -85,14 +85,14 @@ export async function compactConversation({ messages, budget, count, provider, e
|
|
|
85
85
|
// 被裁段落 → 文本(工具结果截断;tool_calls 带名称标注)
|
|
86
86
|
const convoText = messages
|
|
87
87
|
.slice(1, boundary)
|
|
88
|
-
.map((m) => {
|
|
88
|
+
.map((/** @type {any} */ m) => {
|
|
89
89
|
if (m.role === 'tool') {
|
|
90
90
|
return `工具结果(${m.tool_call_id ?? ''}): ${clampText(String(m.content ?? ''), TOOL_OUTPUT_CAP)}`;
|
|
91
91
|
}
|
|
92
92
|
const head = m.role === 'user' ? '用户' : m.role === 'assistant' ? 'MingDao' : String(m.role);
|
|
93
93
|
const calls =
|
|
94
94
|
Array.isArray(m.tool_calls) && m.tool_calls.length
|
|
95
|
-
? ` [调用: ${m.tool_calls.map((tc) => `${tc.function?.name ?? ''}(${String(tc.function?.arguments ?? '').slice(0, 200)})`).join('; ')}]`
|
|
95
|
+
? ` [调用: ${m.tool_calls.map((/** @type {any} */ tc) => `${tc.function?.name ?? ''}(${String(tc.function?.arguments ?? '').slice(0, 200)})`).join('; ')}]`
|
|
96
96
|
: '';
|
|
97
97
|
return `${head}: ${String(m.content ?? '')}${calls}`;
|
|
98
98
|
})
|
|
@@ -102,7 +102,7 @@ export async function compactConversation({ messages, budget, count, provider, e
|
|
|
102
102
|
// —— 增量压缩(Kimi P2-D):已压缩过的会话只压「旧摘要之后的新增段」,与旧摘要合并(摘要的摘要),
|
|
103
103
|
// 不再反复把早期内容送进摘要输入——第 N 次压缩输入从 O(历史) 降为 O(增量)。 ——
|
|
104
104
|
const sumIdx = messages.findIndex(
|
|
105
|
-
(m) => m.role === 'user' && typeof m.content === 'string' && m.content.includes('<conversation_summary>')
|
|
105
|
+
(/** @type {any} */ m) => m.role === 'user' && typeof m.content === 'string' && m.content.includes('<conversation_summary>')
|
|
106
106
|
);
|
|
107
107
|
let incremental = false;
|
|
108
108
|
let summary = null;
|
|
@@ -113,7 +113,7 @@ export async function compactConversation({ messages, budget, count, provider, e
|
|
|
113
113
|
const oldSummary = (m && m[1]) || '';
|
|
114
114
|
const newText = messages
|
|
115
115
|
.slice(sumIdx + 1, boundary)
|
|
116
|
-
.map((msg) => {
|
|
116
|
+
.map((/** @type {any} */ msg) => {
|
|
117
117
|
if (msg.role === 'tool') {
|
|
118
118
|
return `工具结果(${msg.tool_call_id ?? ''}): ${clampText(String(msg.content ?? ''), TOOL_OUTPUT_CAP)}`;
|
|
119
119
|
}
|
package/src/config.js
CHANGED
|
@@ -54,6 +54,7 @@ export function ensureMinimalConfig() {
|
|
|
54
54
|
return cfg;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** @param {any} cfg */
|
|
57
58
|
export function saveConfig(cfg) {
|
|
58
59
|
ensureHome();
|
|
59
60
|
const p = configPath();
|
|
@@ -63,10 +64,12 @@ export function saveConfig(cfg) {
|
|
|
63
64
|
} catch {}
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/** @param {any} cfg @param {any} providerName */
|
|
66
68
|
export function effectiveApiKey(cfg, providerName) {
|
|
67
69
|
return resolveApiKey(cfg, providerName, providerPreset(providerName)?.envKey);
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
/** @param {any} io */
|
|
70
73
|
export async function runWizard(io) {
|
|
71
74
|
io.box('MingDao Harness 初始化向导', ['① 选服务商 → ② 填 API Key(自动验证)→ ③ 选模型(可跳过)']);
|
|
72
75
|
io.print('');
|
|
@@ -74,9 +77,9 @@ export async function runWizard(io) {
|
|
|
74
77
|
const providerKeys = Object.keys(PROVIDERS);
|
|
75
78
|
const provider = await io.choose(
|
|
76
79
|
'① 选择模型服务商:',
|
|
77
|
-
providerKeys.map((k) => ({ value: k, label: `${k} — ${PROVIDERS[k].label}` }))
|
|
80
|
+
providerKeys.map((k) => ({ value: k, label: `${k} — ${/** @type {Record<string, any>} */ (PROVIDERS)[k].label}` }))
|
|
78
81
|
);
|
|
79
|
-
const pp = PROVIDERS[provider];
|
|
82
|
+
const pp = /** @type {Record<string, any>} */ (PROVIDERS)[provider];
|
|
80
83
|
|
|
81
84
|
let baseUrl = '';
|
|
82
85
|
if (provider === 'custom') {
|
|
@@ -163,13 +166,13 @@ export async function runWizard(io) {
|
|
|
163
166
|
const contextBudget = Number(budgetInput) > 0 ? Number(budgetInput) : defaultBudget;
|
|
164
167
|
|
|
165
168
|
// 注意:config.json 不含任何密钥(可安全分享/提交),密钥只存 credentials.json。
|
|
166
|
-
const cfg = {
|
|
169
|
+
const cfg = /** @type {any} */ ({
|
|
167
170
|
provider,
|
|
168
171
|
baseUrl: baseUrl || pp.baseUrl,
|
|
169
172
|
permission: perm,
|
|
170
173
|
sandbox,
|
|
171
174
|
contextBudget,
|
|
172
|
-
};
|
|
175
|
+
});
|
|
173
176
|
if (model) cfg.model = model;
|
|
174
177
|
if (routing) cfg.routing = routing;
|
|
175
178
|
saveConfig(cfg);
|
package/src/context.js
CHANGED
|
@@ -6,11 +6,11 @@ import { heuristicTokens } from './tokenizer.js';
|
|
|
6
6
|
export const TOOL_RESULT_LIMIT = 20000;
|
|
7
7
|
|
|
8
8
|
// 兼容旧接口的启发式估算(英文≈4 字符/token,CJK≈1 字符/token)
|
|
9
|
-
export function approxTokens(text) {
|
|
9
|
+
export function approxTokens(/** @type {any} */ text) {
|
|
10
10
|
return heuristicTokens(text);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function partTokens(msg, count) {
|
|
13
|
+
function partTokens(/** @type {any} */ msg, /** @type {any} */ count) {
|
|
14
14
|
let total = count(msg.content || '');
|
|
15
15
|
for (const tc of msg.tool_calls || []) {
|
|
16
16
|
total += count(JSON.stringify(tc));
|
|
@@ -24,7 +24,7 @@ function partTokens(msg, count) {
|
|
|
24
24
|
// 模型切换(新计数器)后自动失效重算。
|
|
25
25
|
const messageTokenCache = new WeakMap();
|
|
26
26
|
|
|
27
|
-
export function messageTokens(msg, count = approxTokens) {
|
|
27
|
+
export function messageTokens(/** @type {any} */ msg, count = approxTokens) {
|
|
28
28
|
const hit = messageTokenCache.get(msg);
|
|
29
29
|
if (hit && hit.fn === count) return hit.value;
|
|
30
30
|
const total = partTokens(msg, count) + 4; // 每条消息的协议开销
|
|
@@ -35,20 +35,20 @@ export function messageTokens(msg, count = approxTokens) {
|
|
|
35
35
|
// 从尾部向前保留消息,直到预算用尽;始终保留首条 system 消息。
|
|
36
36
|
// 裁剪可能切断 assistant(tool_calls) ↔ tool 的配对,清洗掉孤立消息,
|
|
37
37
|
// 否则 OpenAI 兼容 API 会因 tool_call_id 找不到对应调用而报 400。
|
|
38
|
-
export function cleanToolPairing(kept) {
|
|
38
|
+
export function cleanToolPairing(/** @type {any} */ kept) {
|
|
39
39
|
const callIds = new Set();
|
|
40
40
|
for (const m of kept) {
|
|
41
41
|
if (m.role === 'assistant' && Array.isArray(m.tool_calls)) {
|
|
42
42
|
for (const tc of m.tool_calls) if (tc?.id) callIds.add(tc.id);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
const clean = kept.filter((m) => m.role !== 'tool' || callIds.has(m.tool_call_id));
|
|
46
|
-
const toolIds = new Set(clean.filter((m) => m.role === 'tool').map((m) => m.tool_call_id));
|
|
45
|
+
const clean = kept.filter((/** @type {any} */ m) => m.role !== 'tool' || callIds.has(m.tool_call_id));
|
|
46
|
+
const toolIds = new Set(clean.filter((/** @type {any} */ m) => m.role === 'tool').map((/** @type {any} */ m) => m.tool_call_id));
|
|
47
47
|
// 注意:裁剪是每次请求的投影,绝不能改写调用方 messages 里的原对象
|
|
48
48
|
// (否则会话历史被永久污染:assistant 消息丢失 tool_calls,下一轮 API 报 400)
|
|
49
|
-
return clean.map((m) => {
|
|
49
|
+
return clean.map((/** @type {any} */ m) => {
|
|
50
50
|
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
51
|
-
const keptCalls = m.tool_calls.filter((tc) => toolIds.has(tc.id));
|
|
51
|
+
const keptCalls = m.tool_calls.filter((/** @type {any} */ tc) => toolIds.has(tc.id));
|
|
52
52
|
if (keptCalls.length !== m.tool_calls.length) {
|
|
53
53
|
if (keptCalls.length) return { ...m, tool_calls: keptCalls };
|
|
54
54
|
const copy = { ...m };
|
|
@@ -60,25 +60,37 @@ export function cleanToolPairing(kept) {
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export function trimMessages(messages, budget, count = approxTokens) {
|
|
63
|
+
export function trimMessages(/** @type {any} */ messages, /** @type {any} */ budget, count = approxTokens) {
|
|
64
64
|
if (!messages.length) return [];
|
|
65
65
|
const hasSystem = messages[0]?.role === 'system';
|
|
66
66
|
const system = hasSystem ? [messages[0]] : [];
|
|
67
67
|
const rest = hasSystem ? messages.slice(1) : messages;
|
|
68
|
-
const totalOf = (arr) => arr.reduce((s, m) => s + messageTokens(m, count), 0);
|
|
69
|
-
// —— 轻量语义回收(Hermes B1
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// 质检 M10:维护增量 total(替换消息后按新旧 token 差调整),不再每步全量重算(原 ≤500×n)。
|
|
68
|
+
const totalOf = (/** @type {any} */ arr) => arr.reduce((/** @type {any} */ s, /** @type {any} */ m) => s + messageTokens(m, count), 0);
|
|
69
|
+
// —— 轻量语义回收(Hermes B1 + workbuddy P0-1 修正)——
|
|
70
|
+
// 只在「真的超预算」时回收(此前 >80% 即动手,能塞下的会话也被白白截断丢内容);
|
|
71
|
+
// 回收边界 = 按尾部保留规则确定将被丢弃的前缀(此前固定扫前 500 条,超长会话时回收全白做);
|
|
72
|
+
// 替换仅当摘要确实更短才接受(防短消息被摘要撑长)。
|
|
74
73
|
const sysTokens = totalOf(system);
|
|
75
74
|
let total = sysTokens + totalOf(rest);
|
|
76
|
-
if (total > budget
|
|
75
|
+
if (total > budget) {
|
|
77
76
|
const out = [...rest];
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
// 丢弃边界:从后往前贪心,确定尾部保留起点 s(s 之前的消息将被丢弃,只对它们回收)
|
|
78
|
+
let s = out.length;
|
|
79
|
+
{
|
|
80
|
+
let acc = sysTokens;
|
|
81
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
82
|
+
const t = messageTokens(out[i], count);
|
|
83
|
+
if (acc + t > budget) { s = i + 1; break; }
|
|
84
|
+
acc += t;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (let i = 0; i < s && i < 500; i++) {
|
|
88
|
+
if (total <= budget) break;
|
|
80
89
|
const m = out[i];
|
|
81
90
|
const c = typeof m.content === 'string' ? m.content : JSON.stringify(m?.content ?? '');
|
|
91
|
+
// 幂等守卫(A4):已回收过的内容跳过——否则「截断标记」把长度再次顶过阈值,逐轮嵌套重截,
|
|
92
|
+
// 被保留消息字节逐轮漂移(前缀缓存连尾部都失配)
|
|
93
|
+
if (c.includes('已回收')) continue;
|
|
82
94
|
let next = null;
|
|
83
95
|
if (m.role === 'tool' && c.length > 40) {
|
|
84
96
|
next = { ...m, content: `[工具 ${m.tool_call_id || ''} 结果摘要:${c.slice(0, 40).replace(/\n/g, ' ')}…(已回收,原 ${c.length} 字)]` };
|
|
@@ -86,8 +98,12 @@ export function trimMessages(messages, budget, count = approxTokens) {
|
|
|
86
98
|
next = { ...m, content: c.slice(0, 200) + ` …[已回收,原 ${c.length} 字]` };
|
|
87
99
|
}
|
|
88
100
|
if (next) {
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
const oldT = messageTokens(m, count);
|
|
102
|
+
const newT = messageTokens(next, count);
|
|
103
|
+
if (newT < oldT) {
|
|
104
|
+
total += newT - oldT;
|
|
105
|
+
out[i] = next;
|
|
106
|
+
}
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
109
|
// 回收后仍超预算 → 从尾部保留(必要时继续丢弃最老,清洗 tool 配对)
|
|
@@ -104,7 +120,7 @@ export function trimMessages(messages, budget, count = approxTokens) {
|
|
|
104
120
|
return cleanToolPairing(system.concat(rest));
|
|
105
121
|
}
|
|
106
122
|
|
|
107
|
-
export function clampText(text, maxChars = TOOL_RESULT_LIMIT) {
|
|
123
|
+
export function clampText(/** @type {any} */ text, maxChars = TOOL_RESULT_LIMIT) {
|
|
108
124
|
const s = String(text);
|
|
109
125
|
if (s.length <= maxChars) return s;
|
|
110
126
|
return s.slice(0, maxChars) + `\n…[输出过长已截断,原文共 ${s.length} 字符]`;
|
package/src/cost-guard.js
CHANGED
|
@@ -32,9 +32,15 @@ export function todayCost() {
|
|
|
32
32
|
todayCache = { mtime: st.mtimeMs, size: st.size, start, sum };
|
|
33
33
|
return sum;
|
|
34
34
|
} catch {
|
|
35
|
-
|
|
35
|
+
// MiniMax P0:统计文件损坏/不可读时返回 null(护栏全部失效)而不是静默 0——并一次性告警
|
|
36
|
+
if (!todayCostWarned) {
|
|
37
|
+
todayCostWarned = true;
|
|
38
|
+
console.warn('[MingDao] 费用统计读取失败:今日费用护栏暂时无法判断(修复 cache-stats.jsonl 或重启后恢复)。');
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
36
41
|
}
|
|
37
42
|
}
|
|
43
|
+
let todayCostWarned = false;
|
|
38
44
|
|
|
39
45
|
export function costGuardStatus() {
|
|
40
46
|
const g = costGuardConfig();
|
|
@@ -49,8 +55,9 @@ export function costGuardStatus() {
|
|
|
49
55
|
warnAt,
|
|
50
56
|
action,
|
|
51
57
|
downgradeModel: String(g.downgradeModel || 'deepseek-v4-flash'),
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
degraded: cost === null, // 统计不可读:护栏降级为「无法判断」
|
|
59
|
+
overWarn: cost !== null && limit > 0 && cost >= warnAt,
|
|
60
|
+
overLimit: cost !== null && limit > 0 && cost >= limit,
|
|
54
61
|
};
|
|
55
62
|
}
|
|
56
63
|
|
|
@@ -58,10 +65,11 @@ export function costGuardStatus() {
|
|
|
58
65
|
export function checkCostGuard() {
|
|
59
66
|
const st = costGuardStatus();
|
|
60
67
|
if (!st) return null;
|
|
68
|
+
if (st.degraded) return null; // 统计不可读:不误拦也不放水(已告警),按无法判断处理
|
|
61
69
|
if (st.overLimit && st.action === 'block') {
|
|
62
70
|
return {
|
|
63
71
|
blocked: true,
|
|
64
|
-
message: `今日费用已达上限 ¥${st.limit.toFixed(2)}(实际 ¥${st.cost.toFixed(4)}),已暂停执行——调整 config.costGuard 或明天自动恢复。`,
|
|
72
|
+
message: `今日费用已达上限 ¥${st.limit.toFixed(2)}(实际 ¥${(st.cost ?? 0).toFixed(4)}),已暂停执行——调整 config.costGuard 或明天自动恢复。`,
|
|
65
73
|
};
|
|
66
74
|
}
|
|
67
75
|
if (st.overLimit && st.action === 'downgrade') {
|
|
@@ -69,13 +77,13 @@ export function checkCostGuard() {
|
|
|
69
77
|
blocked: false,
|
|
70
78
|
downgrade: true,
|
|
71
79
|
downgradeModel: st.downgradeModel,
|
|
72
|
-
message: `⚠ 费用护栏:今日已用 ¥${st.cost.toFixed(4)} 超上限 ¥${st.limit.toFixed(2)}——已自动降级到便宜模型 ${st.downgradeModel} 继续执行(config.costGuard.action 可改回 warn/block)。`,
|
|
80
|
+
message: `⚠ 费用护栏:今日已用 ¥${(st.cost ?? 0).toFixed(4)} 超上限 ¥${st.limit.toFixed(2)}——已自动降级到便宜模型 ${st.downgradeModel} 继续执行(config.costGuard.action 可改回 warn/block)。`,
|
|
73
81
|
};
|
|
74
82
|
}
|
|
75
83
|
if (st.overWarn) {
|
|
76
84
|
return {
|
|
77
85
|
blocked: false,
|
|
78
|
-
message: `⚠ 费用护栏:今日已用 ¥${st.cost.toFixed(4)} / 上限 ¥${st.limit.toFixed(2)}${st.action !== 'warn' ? `(${st.action === 'block' ? '到达即暂停' : '到达自动降级 ' + st.downgradeModel})` : '(仅提醒)'}。`,
|
|
86
|
+
message: `⚠ 费用护栏:今日已用 ¥${(st.cost ?? 0).toFixed(4)} / 上限 ¥${st.limit.toFixed(2)}${st.action !== 'warn' ? `(${st.action === 'block' ? '到达即暂停' : '到达自动降级 ' + st.downgradeModel})` : '(仅提醒)'}。`,
|
|
79
87
|
};
|
|
80
88
|
}
|
|
81
89
|
return null;
|
package/src/credentials.js
CHANGED
|
@@ -24,6 +24,7 @@ export function loadCredentials() {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** @param {any} creds */
|
|
27
28
|
export function saveCredentials(creds) {
|
|
28
29
|
ensureHome();
|
|
29
30
|
const p = credentialsPath();
|
|
@@ -34,10 +35,12 @@ export function saveCredentials(creds) {
|
|
|
34
35
|
} catch {}
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/** @param {any} providerName */
|
|
37
39
|
export function getStoredKey(providerName) {
|
|
38
40
|
return loadCredentials()[providerName] || null;
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
/** @param {any} providerName @param {any} key */
|
|
41
44
|
export function setStoredKey(providerName, key) {
|
|
42
45
|
const creds = loadCredentials();
|
|
43
46
|
if (key) creds[providerName] = String(key);
|
|
@@ -45,11 +48,13 @@ export function setStoredKey(providerName, key) {
|
|
|
45
48
|
saveCredentials(creds);
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/** @param {any} providerName */
|
|
48
52
|
export function removeStoredKey(providerName) {
|
|
49
53
|
setStoredKey(providerName, null);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
// 脱敏展示:只显示首 6 位与末 4 位,永不输出完整 Key。
|
|
57
|
+
/** @param {any} key */
|
|
53
58
|
export function maskKey(key) {
|
|
54
59
|
if (!key) return null;
|
|
55
60
|
const s = String(key);
|
|
@@ -58,6 +63,7 @@ export function maskKey(key) {
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
// 统一的 Key 解析链:环境变量 → 本地凭证库 → config.json 显式字段
|
|
66
|
+
/** @param {any} cfg @param {any} providerName @param {any} envKeyHint */
|
|
61
67
|
export function resolveApiKey(cfg, providerName, envKeyHint) {
|
|
62
68
|
const envKeys = [envKeyHint, 'MINGDAO_API_KEY'].filter((k, i, a) => k && a.indexOf(k) === i);
|
|
63
69
|
for (const k of envKeys) {
|