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.
Files changed (66) hide show
  1. package/docs/EVALUATION-ROUND2.md +58 -0
  2. package/docs/ROADMAP-NEXT.md +5 -5
  3. package/package.json +2 -2
  4. package/src/agent.js +74 -36
  5. package/src/atomic-write.js +8 -2
  6. package/src/audit.js +2 -2
  7. package/src/batch.js +7 -7
  8. package/src/cachestats.js +9 -9
  9. package/src/cli.js +42 -688
  10. package/src/commands/desktop.js +1 -1
  11. package/src/commands/key.js +1 -1
  12. package/src/commands/repl.js +711 -0
  13. package/src/commands/schedule.js +7 -7
  14. package/src/commands/skill.js +18 -16
  15. package/src/commands/sync.js +5 -5
  16. package/src/commands/update.js +7 -6
  17. package/src/commands/workspace.js +2 -2
  18. package/src/compact.js +6 -6
  19. package/src/config.js +7 -4
  20. package/src/context.js +37 -21
  21. package/src/cost-guard.js +14 -6
  22. package/src/credentials.js +6 -0
  23. package/src/hooks.js +11 -11
  24. package/src/mcp-presets.js +5 -5
  25. package/src/mcp.js +19 -19
  26. package/src/memory.js +16 -16
  27. package/src/model-discovery.js +16 -15
  28. package/src/models.js +4 -4
  29. package/src/notify.js +2 -2
  30. package/src/permissions.js +7 -3
  31. package/src/pricing.js +35 -1
  32. package/src/prompts.js +1 -1
  33. package/src/providers/index.js +7 -7
  34. package/src/providers/openai-compatible.js +13 -11
  35. package/src/routing.js +8 -8
  36. package/src/schedule.js +30 -30
  37. package/src/session-index.js +5 -1
  38. package/src/session.js +11 -2
  39. package/src/skill-lib.js +62 -3
  40. package/src/skill-registry.js +10 -9
  41. package/src/skills.js +14 -12
  42. package/src/sync-server.js +86 -3
  43. package/src/sync.js +53 -20
  44. package/src/tasks/worker.js +126 -0
  45. package/src/tasks.js +14 -14
  46. package/src/titles.js +5 -5
  47. package/src/tokenizer.js +23 -5
  48. package/src/tools/bash.js +12 -12
  49. package/src/tools/fs-tools.js +66 -9
  50. package/src/tools/index.js +7 -7
  51. package/src/ui.js +83 -6
  52. package/src/update.js +8 -8
  53. package/src/web/app.js +16 -5
  54. package/src/web/attachments.js +1 -1
  55. package/src/web/index.html +21 -1
  56. package/src/web/routes/api.js +38 -786
  57. package/src/web/routes/domains/config.js +280 -0
  58. package/src/web/routes/domains/misc.js +129 -0
  59. package/src/web/routes/domains/schedule.js +127 -0
  60. package/src/web/routes/domains/sessions.js +105 -0
  61. package/src/web/routes/domains/skills.js +79 -0
  62. package/src/web/routes/domains/sync.js +101 -0
  63. package/src/web/routes/domains/workspace.js +119 -0
  64. package/src/web/server.js +73 -31
  65. package/src/web/web-io.js +54 -12
  66. 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());
@@ -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
- 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)}(已按半价)`);
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):预算 >80% 时先对最老消息做无模型压缩 ——
70
- // 最老 tool 消息 → 单行摘要;老 assistant 长文 → 前 200 字;逐条向新推进直到回到 80% 线。
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 * 0.8) {
75
+ if (total > budget) {
77
76
  const out = [...rest];
78
- for (let i = 0; i < out.length && i < 500; i++) {
79
- if (total <= budget * 0.8) break;
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
- total += messageTokens(next, count) - messageTokens(m, count);
90
- out[i] = next;
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
- return 0;
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
- overWarn: limit > 0 && cost >= warnAt,
53
- overLimit: limit > 0 && cost >= limit,
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;
@@ -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) {