mingdao-harness 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/docs/ROADMAP-NEXT.md +13 -13
  2. package/package.json +1 -1
  3. package/skills/release-checklist/SKILL.md +16 -3
  4. package/src/agent.js +102 -30
  5. package/src/audit.js +2 -2
  6. package/src/batch.js +74 -16
  7. package/src/cachestats.js +57 -11
  8. package/src/cli.js +29 -679
  9. package/src/commands/desktop.js +1 -1
  10. package/src/commands/key.js +1 -1
  11. package/src/commands/repl.js +711 -0
  12. package/src/commands/schedule.js +7 -7
  13. package/src/commands/skill.js +18 -16
  14. package/src/commands/sync.js +5 -5
  15. package/src/commands/update.js +22 -9
  16. package/src/commands/workspace.js +2 -2
  17. package/src/compact.js +6 -6
  18. package/src/config.js +7 -4
  19. package/src/context.js +11 -11
  20. package/src/cost-guard.js +15 -5
  21. package/src/credentials.js +6 -0
  22. package/src/hooks.js +11 -11
  23. package/src/mcp-presets.js +5 -5
  24. package/src/mcp.js +19 -19
  25. package/src/memory.js +16 -16
  26. package/src/model-discovery.js +16 -15
  27. package/src/models.js +4 -4
  28. package/src/notify.js +2 -2
  29. package/src/permissions.js +7 -3
  30. package/src/pricing.js +35 -1
  31. package/src/prompts.js +1 -1
  32. package/src/providers/index.js +7 -7
  33. package/src/providers/openai-compatible.js +8 -8
  34. package/src/routing.js +5 -5
  35. package/src/schedule.js +28 -28
  36. package/src/session-index.js +5 -1
  37. package/src/session.js +11 -2
  38. package/src/skill-lib.js +62 -3
  39. package/src/skill-registry.js +10 -9
  40. package/src/skills.js +10 -10
  41. package/src/sync-server.js +86 -3
  42. package/src/sync.js +53 -20
  43. package/src/tasks/worker.js +126 -0
  44. package/src/tasks.js +13 -13
  45. package/src/titles.js +5 -5
  46. package/src/tokenizer.js +22 -4
  47. package/src/tools/bash.js +12 -12
  48. package/src/tools/fs-tools.js +66 -9
  49. package/src/tools/index.js +77 -36
  50. package/src/ui.js +83 -6
  51. package/src/update.js +8 -8
  52. package/src/web/app.js +43 -0
  53. package/src/web/attachments.js +1 -1
  54. package/src/web/index.html +12 -0
  55. package/src/web/routes/api.js +38 -786
  56. package/src/web/routes/domains/config.js +280 -0
  57. package/src/web/routes/domains/misc.js +128 -0
  58. package/src/web/routes/domains/schedule.js +127 -0
  59. package/src/web/routes/domains/sessions.js +105 -0
  60. package/src/web/routes/domains/skills.js +79 -0
  61. package/src/web/routes/domains/sync.js +101 -0
  62. package/src/web/routes/domains/workspace.js +113 -0
  63. package/src/web/server.js +29 -14
  64. package/src/web/web-io.js +54 -12
  65. package/src/workspace.js +14 -14
@@ -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;
@@ -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
- console.log(`✓ 已从 git 仓库安装 ${r.names.length} 个技能:${r.names.join(', ')}`);
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 srcLabel = r.host ? '(线上 registry)' : '';
52
- console.log(`✓ 已安装技能 ${r.name}${srcLabel} → ~/.mingdao/skills/${r.name}/(可编辑/删除,下次会话生效)`);
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
  }
@@ -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;
@@ -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());
@@ -33,7 +33,7 @@ export async function handleUpdateFamily(cmd, args) {
33
33
  return true;
34
34
  }
35
35
 
36
- // Batch API 半价批处理(A1):mingdao batch <问题文件|-> [--model 名] [--max-tokens n]
36
+ // Batch API 半价批处理(A1/B2):mingdao batch <问题文件|-> [--model 名] [--max-tokens n] [--max-cost 元]
37
37
  if (cmd === 'batch') {
38
38
  const { runBatch } = await import('../batch.js');
39
39
  const cfgB = loadConfig();
@@ -44,16 +44,18 @@ export async function handleUpdateFamily(cmd, args) {
44
44
  }
45
45
  let model = cfgB.model || 'deepseek-v4-flash';
46
46
  let maxTokens = 4096;
47
+ let maxCost = 0; // 省钱 B2:预算上限(元),提交前按估算拦截
47
48
  let srcFile = null;
48
49
  for (let i = 0; i < args.length; i++) {
49
50
  const a = args[i];
50
51
  if (a === '--model') model = args[++i];
51
52
  else if (a === '--max-tokens') maxTokens = Number(args[++i]) || 4096;
53
+ else if (a === '--max-cost') maxCost = Number(args[++i]) || 0;
52
54
  else if (a === '--out') { i += 1; /* 输出默认落工作目录 */ }
53
55
  else if (!srcFile) srcFile = a;
54
56
  }
55
57
  if (!srcFile) {
56
- console.log('用法:mingdao batch <问题文件|-> 每行一个问题(--model 名 --max-tokens n)');
58
+ console.log('用法:mingdao batch <问题文件|-> 每行一个问题(--model 名 --max-tokens n --max-cost 元)');
57
59
  process.exitCode = 1;
58
60
  return true;
59
61
  }
@@ -62,7 +64,7 @@ export async function handleUpdateFamily(cmd, args) {
62
64
  const raw = srcFile === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(srcFile, 'utf8');
63
65
  questions = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, 1000);
64
66
  } catch (err) {
65
- console.log('[错误] 读取问题文件失败:' + (err?.message || err));
67
+ console.log('[错误] 读取问题文件失败:' + ((/** @type {any} */ (err))?.message || err));
66
68
  process.exitCode = 1;
67
69
  return true;
68
70
  }
@@ -80,8 +82,9 @@ export async function handleUpdateFamily(cmd, args) {
80
82
  questions,
81
83
  workingDir: process.cwd(),
82
84
  maxTokens,
85
+ maxCost,
83
86
  signal: ac.signal,
84
- onStatus: (st) => console.log(' ' + st),
87
+ onStatus: (/** @type {any} */ st) => console.log(' ' + st),
85
88
  });
86
89
  process.removeAllListeners('SIGINT');
87
90
  if (r.error) {
@@ -89,7 +92,9 @@ export async function handleUpdateFamily(cmd, args) {
89
92
  process.exitCode = 1;
90
93
  return true;
91
94
  }
92
- console.log(`✓ 完成 ${r.results.length} 条 · ↑${r.usage.prompt_tokens} ↓${r.usage.completion_tokens} tokens · 费用 ≈¥${r.cost.toFixed(5)}(已按半价)`);
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)}(已按半价)`);
97
+ if (r.deduped) console.log(` (去重合并 ${r.deduped} 条重复问题,结果已回填全部位置)`);
93
98
  console.log(` 结果文件:${r.outputFile}`);
94
99
  console.log(` 任务 ID:${r.batchId}(结果已计入 /cost 分账)`);
95
100
  return true;
@@ -115,7 +120,7 @@ export async function handleUpdateFamily(cmd, args) {
115
120
  console.log('月度报告导出:mingdao cost report [YYYY-MM|all]');
116
121
  return true;
117
122
  }
118
- 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)));
119
124
  const md = [];
120
125
  for (const r of rows) {
121
126
  const maxDay = Math.max(...r.days.map((d) => d.cost), 1e-9);
@@ -123,13 +128,21 @@ export async function handleUpdateFamily(cmd, args) {
123
128
  md.push('');
124
129
  md.push(`- 实际费用 ≈¥${r.cost.toFixed(5)}${r.batchCost > 0 ? `(含 Batch 半价任务 ≈¥${r.batchCost.toFixed(5)})` : ''}`);
125
130
  md.push(`- 相比全未命中已节省 ≈¥${r.saved.toFixed(5)}${r.rate != null ? ` · 缓存命中率 ${(r.rate * 100).toFixed(0)}%` : ''}`);
126
- md.push(`- Tokens:↑${r.prompt} / ↓${r.completion}`);
131
+ md.push(`- Tokens:↑${r.prompt} / ↓${r.completion}${r.reasoning ? ` · 推理 ${r.reasoning}` : ''}`);
127
132
  md.push('');
128
133
  md.push('## 按模型分账');
129
134
  md.push('');
130
135
  md.push('| 模型 | 轮次 | ↑prompt | ↓completion | 费用 |');
131
136
  md.push('| --- | --- | --- | --- | --- |');
132
137
  for (const m of r.models) md.push(`| ${m.model} | ${m.turns} | ${m.prompt} | ${m.completion} | ≈¥${m.cost.toFixed(5)} |`);
138
+ if (r.tools.length) {
139
+ md.push('');
140
+ md.push('## 按工具分账');
141
+ md.push('');
142
+ md.push('| 工具 | 调用 | 耗时(ms) |');
143
+ md.push('| --- | --- | --- |');
144
+ for (const t of r.tools.slice(0, 10)) md.push(`| ${t.tool} | ${t.calls} | ${t.ms} |`);
145
+ }
133
146
  md.push('');
134
147
  md.push('## 每日费用');
135
148
  md.push('');
@@ -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,12 +60,12 @@ 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);
68
+ const totalOf = (/** @type {any} */ arr) => arr.reduce((/** @type {any} */ s, /** @type {any} */ m) => s + messageTokens(m, count), 0);
69
69
  // —— 轻量语义回收(Hermes B1):预算 >80% 时先对最老消息做无模型压缩 ——
70
70
  // 最老 tool 消息 → 单行摘要;老 assistant 长文 → 前 200 字;逐条向新推进直到回到 80% 线。
71
71
  // 相比直接丢弃,被修改的只有前端旧消息——其后未被触碰的近期尾部字节保持不变,
@@ -104,7 +104,7 @@ export function trimMessages(messages, budget, count = approxTokens) {
104
104
  return cleanToolPairing(system.concat(rest));
105
105
  }
106
106
 
107
- export function clampText(text, maxChars = TOOL_RESULT_LIMIT) {
107
+ export function clampText(/** @type {any} */ text, maxChars = TOOL_RESULT_LIMIT) {
108
108
  const s = String(text);
109
109
  if (s.length <= maxChars) return s;
110
110
  return s.slice(0, maxChars) + `\n…[输出过长已截断,原文共 ${s.length} 字符]`;
package/src/cost-guard.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // 费用护栏(四报告共识 A2/Kimi P-1:把峰谷定价从「展示」升级为「主动约束」):
2
- // config.costGuard = { dailyLimitYuan: 10, warnAtYuan: 8, action: 'warn'|'block' }
2
+ // config.costGuard = { dailyLimitYuan: 10, warnAtYuan: 8, action: 'warn'|'block'|'downgrade', downgradeModel?: 'deepseek-v4-flash' }
3
3
  // 按北京时间自然日累计 cache-stats 实际费用(含 batch 半价与缓存折扣后的真实口径)。
4
- // Agent 每轮开始前检查:超 warn 线提醒、超 limit 且 action=block 时暂停执行。
4
+ // Agent 每轮开始前检查:超 warn 线提醒;超 limit 时按 action 处理——
5
+ // block:暂停执行;downgrade(省钱 B4):自动切换便宜模型继续跑(默认 deepseek-v4-flash)。
5
6
 
6
7
  import fs from 'node:fs';
7
8
  import { listCacheStats, cacheStatsFile } from './cachestats.js';
@@ -41,18 +42,19 @@ export function costGuardStatus() {
41
42
  const cost = todayCost();
42
43
  const limit = Number(g.dailyLimitYuan) || 0;
43
44
  const warnAt = Number(g.warnAtYuan) || (limit > 0 ? limit * 0.8 : 0);
44
- const action = g.action === 'block' ? 'block' : 'warn';
45
+ const action = g.action === 'block' || g.action === 'downgrade' ? g.action : 'warn';
45
46
  return {
46
47
  cost,
47
48
  limit,
48
49
  warnAt,
49
50
  action,
51
+ downgradeModel: String(g.downgradeModel || 'deepseek-v4-flash'),
50
52
  overWarn: limit > 0 && cost >= warnAt,
51
53
  overLimit: limit > 0 && cost >= limit,
52
54
  };
53
55
  }
54
56
 
55
- // Agent 每轮开始前调用:返回 null 放行;blocked=true 表示应暂停本轮
57
+ // Agent 每轮开始前调用:返回 null 放行;blocked=true 应暂停本轮;downgrade=true 应切换便宜模型
56
58
  export function checkCostGuard() {
57
59
  const st = costGuardStatus();
58
60
  if (!st) return null;
@@ -62,10 +64,18 @@ export function checkCostGuard() {
62
64
  message: `今日费用已达上限 ¥${st.limit.toFixed(2)}(实际 ¥${st.cost.toFixed(4)}),已暂停执行——调整 config.costGuard 或明天自动恢复。`,
63
65
  };
64
66
  }
67
+ if (st.overLimit && st.action === 'downgrade') {
68
+ return {
69
+ blocked: false,
70
+ downgrade: true,
71
+ downgradeModel: st.downgradeModel,
72
+ message: `⚠ 费用护栏:今日已用 ¥${st.cost.toFixed(4)} 超上限 ¥${st.limit.toFixed(2)}——已自动降级到便宜模型 ${st.downgradeModel} 继续执行(config.costGuard.action 可改回 warn/block)。`,
73
+ };
74
+ }
65
75
  if (st.overWarn) {
66
76
  return {
67
77
  blocked: false,
68
- message: `⚠ 费用护栏:今日已用 ¥${st.cost.toFixed(4)} / 上限 ¥${st.limit.toFixed(2)}${st.action === 'block' ? '(到达即暂停)' : '(仅提醒)'}。`,
78
+ message: `⚠ 费用护栏:今日已用 ¥${st.cost.toFixed(4)} / 上限 ¥${st.limit.toFixed(2)}${st.action !== 'warn' ? `(${st.action === 'block' ? '到达即暂停' : '到达自动降级 ' + st.downgradeModel})` : '(仅提醒)'}。`,
69
79
  };
70
80
  }
71
81
  return null;
@@ -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) {