mingdao-harness 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/docs/ROADMAP-NEXT.md +5 -5
  2. package/package.json +1 -1
  3. package/src/agent.js +19 -19
  4. package/src/audit.js +2 -2
  5. package/src/batch.js +7 -7
  6. package/src/cachestats.js +9 -9
  7. package/src/cli.js +29 -679
  8. package/src/commands/desktop.js +1 -1
  9. package/src/commands/key.js +1 -1
  10. package/src/commands/repl.js +711 -0
  11. package/src/commands/schedule.js +7 -7
  12. package/src/commands/skill.js +18 -16
  13. package/src/commands/sync.js +5 -5
  14. package/src/commands/update.js +7 -6
  15. package/src/commands/workspace.js +2 -2
  16. package/src/compact.js +6 -6
  17. package/src/config.js +7 -4
  18. package/src/context.js +11 -11
  19. package/src/credentials.js +6 -0
  20. package/src/hooks.js +11 -11
  21. package/src/mcp-presets.js +5 -5
  22. package/src/mcp.js +19 -19
  23. package/src/memory.js +16 -16
  24. package/src/model-discovery.js +16 -15
  25. package/src/models.js +4 -4
  26. package/src/notify.js +2 -2
  27. package/src/permissions.js +7 -3
  28. package/src/pricing.js +35 -1
  29. package/src/prompts.js +1 -1
  30. package/src/providers/index.js +7 -7
  31. package/src/providers/openai-compatible.js +8 -8
  32. package/src/routing.js +5 -5
  33. package/src/schedule.js +28 -28
  34. package/src/session-index.js +5 -1
  35. package/src/session.js +11 -2
  36. package/src/skill-lib.js +62 -3
  37. package/src/skill-registry.js +10 -9
  38. package/src/skills.js +10 -10
  39. package/src/sync-server.js +86 -3
  40. package/src/sync.js +53 -20
  41. package/src/tasks/worker.js +126 -0
  42. package/src/tasks.js +13 -13
  43. package/src/titles.js +5 -5
  44. package/src/tokenizer.js +22 -4
  45. package/src/tools/bash.js +12 -12
  46. package/src/tools/fs-tools.js +66 -9
  47. package/src/tools/index.js +7 -7
  48. package/src/ui.js +83 -6
  49. package/src/update.js +8 -8
  50. package/src/web/attachments.js +1 -1
  51. package/src/web/routes/api.js +38 -786
  52. package/src/web/routes/domains/config.js +280 -0
  53. package/src/web/routes/domains/misc.js +128 -0
  54. package/src/web/routes/domains/schedule.js +127 -0
  55. package/src/web/routes/domains/sessions.js +105 -0
  56. package/src/web/routes/domains/skills.js +79 -0
  57. package/src/web/routes/domains/sync.js +101 -0
  58. package/src/web/routes/domains/workspace.js +113 -0
  59. package/src/web/server.js +29 -14
  60. package/src/web/web-io.js +54 -12
  61. package/src/workspace.js +14 -14
package/src/mcp.js CHANGED
@@ -25,13 +25,13 @@ const HANDSHAKE_TIMEOUT_MS = 20000;
25
25
  let nextId = 1;
26
26
 
27
27
  export class McpClient {
28
- constructor(name, { command, args = [], env = {} }, workingDir) {
28
+ constructor(/** @type {any} */ name, /** @type {any} */ { command, args = [], env = {} }, /** @type {any} */ workingDir) {
29
29
  this.name = name;
30
30
  this.command = command;
31
31
  this.args = args;
32
32
  this.env = env;
33
33
  this.workingDir = workingDir;
34
- this.tools = [];
34
+ this.tools = /** @type {any[]} */ ([]);
35
35
  this.child = null;
36
36
  this.pending = new Map();
37
37
  this.buf = '';
@@ -66,14 +66,14 @@ export class McpClient {
66
66
  await this._handshake();
67
67
  await this._listTools();
68
68
  } catch (err) {
69
- this.error = String(err?.message || err);
69
+ this.error = String((/** @type {any} */ (err))?.message || err);
70
70
  this.stop(); // 审计 P1-3:握手/列工具失败时清理 detached 子进程树,防孤儿
71
71
  throw err;
72
72
  }
73
73
  return this;
74
74
  }
75
75
 
76
- _onData(d) {
76
+ _onData(/** @type {any} */ d) {
77
77
  this.buf += d.toString('utf8');
78
78
  if (this.buf.length > 20 * 1024 * 1024) {
79
79
  // 恶意/异常服务器灌入超量数据:放弃解析,避免内存膨胀
@@ -104,7 +104,7 @@ export class McpClient {
104
104
  }
105
105
  }
106
106
 
107
- _failAll(reason) {
107
+ _failAll(/** @type {any} */ reason) {
108
108
  for (const [, p] of this.pending) {
109
109
  clearTimeout(p.timer);
110
110
  p.reject(new Error(reason));
@@ -112,7 +112,7 @@ export class McpClient {
112
112
  this.pending.clear();
113
113
  }
114
114
 
115
- request(method, params = {}) {
115
+ request(/** @type {any} */ method, params = {}) {
116
116
  if (!this.child || this.child.killed) {
117
117
  return Promise.reject(new Error(`MCP 服务器 ${this.name} 未运行`));
118
118
  }
@@ -124,16 +124,16 @@ export class McpClient {
124
124
  }, REQUEST_TIMEOUT_MS);
125
125
  this.pending.set(id, { resolve, reject, timer });
126
126
  try {
127
- this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
127
+ (/** @type {any} */ (this.child)).stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
128
128
  } catch (err) {
129
129
  clearTimeout(timer);
130
130
  this.pending.delete(id);
131
- reject(new Error(`MCP [${this.name}] 写入失败:${err.message}`));
131
+ reject(new Error(`MCP [${this.name}] 写入失败:${(/** @type {any} */ (err)).message}`));
132
132
  }
133
133
  });
134
134
  }
135
135
 
136
- notify(method, params = {}) {
136
+ notify(/** @type {any} */ method, params = {}) {
137
137
  try {
138
138
  this.child?.stdin?.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
139
139
  } catch {}
@@ -165,7 +165,7 @@ export class McpClient {
165
165
  this.tools = Array.isArray(res?.tools) ? res.tools : [];
166
166
  }
167
167
 
168
- prefixedName(toolName) {
168
+ prefixedName(/** @type {any} */ toolName) {
169
169
  return `mcp__${this.name}__${toolName}`;
170
170
  }
171
171
 
@@ -182,18 +182,18 @@ export class McpClient {
182
182
  }));
183
183
  }
184
184
 
185
- isReadonly(toolName) {
185
+ isReadonly(/** @type {any} */ toolName) {
186
186
  const t = this.tools.find((x) => x.name === toolName);
187
187
  return Boolean(t?.annotations?.readOnlyHint);
188
188
  }
189
189
 
190
- async callTool(toolName, args) {
190
+ async callTool(/** @type {any} */ toolName, /** @type {any} */ args) {
191
191
  const res = await this.request('tools/call', { name: toolName, arguments: args });
192
192
  if (res?.isError) {
193
- const text = (res.content || []).map((c) => c.text || '').join('\n');
193
+ const text = (res.content || []).map((/** @type {any} */ c) => c.text || '').join('\n');
194
194
  return { ok: false, error: text || 'MCP 工具返回错误' };
195
195
  }
196
- let parts = (res?.content || []).map((c) => (c.type === 'text' ? c.text : `[${c.type}] ${JSON.stringify(c)}`));
196
+ let parts = (res?.content || []).map((/** @type {any} */ c) => (c.type === 'text' ? c.text : `[${c.type}] ${JSON.stringify(c)}`));
197
197
  let text = parts.join('\n') || '(无输出)';
198
198
  if (text.length > 100 * 1024) text = text.slice(0, 100 * 1024) + `\n…[MCP 输出过长已截断,原文共 ${text.length} 字符]`;
199
199
  return { ok: true, output: text };
@@ -214,7 +214,7 @@ export class McpClient {
214
214
  }
215
215
 
216
216
  // 多服务器管理器:部分服务器启动失败不影响其余
217
- export async function startMcpServers(mcpCfg, workingDir) {
217
+ export async function startMcpServers(/** @type {any} */ mcpCfg, /** @type {any} */ workingDir) {
218
218
  const clients = new Map();
219
219
  const entries = Object.entries(mcpCfg || {});
220
220
  await Promise.all(
@@ -225,7 +225,7 @@ export async function startMcpServers(mcpCfg, workingDir) {
225
225
  try {
226
226
  await client.start();
227
227
  } catch (err) {
228
- client.error = String(err?.message || err);
228
+ client.error = String((/** @type {any} */ (err))?.message || err);
229
229
  }
230
230
  })
231
231
  );
@@ -246,7 +246,7 @@ export async function startMcpServers(mcpCfg, workingDir) {
246
246
  }
247
247
  return out;
248
248
  },
249
- lookup(prefixedName) {
249
+ lookup(/** @type {any} */ prefixedName) {
250
250
  const exact = managerExact.get(prefixedName);
251
251
  if (exact) {
252
252
  const c = clients.get(exact.server);
@@ -255,12 +255,12 @@ export async function startMcpServers(mcpCfg, workingDir) {
255
255
  }
256
256
  return null;
257
257
  },
258
- async call(prefixedName, args) {
258
+ async call(/** @type {any} */ prefixedName, /** @type {any} */ args) {
259
259
  const found = this.lookup(prefixedName);
260
260
  if (!found) return { ok: false, error: `MCP 工具不可用:${prefixedName}` };
261
261
  return found.client.callTool(found.toolName, args);
262
262
  },
263
- isReadonly(prefixedName) {
263
+ isReadonly(/** @type {any} */ prefixedName) {
264
264
  const found = this.lookup(prefixedName);
265
265
  return found ? found.client.isReadonly(found.toolName) : false;
266
266
  },
package/src/memory.js CHANGED
@@ -25,14 +25,14 @@ export function loadMemory() {
25
25
  }
26
26
  }
27
27
 
28
- export function appendMemory(lines) {
29
- const add = lines.map((l) => l.trim()).filter(Boolean);
28
+ export function appendMemory(/** @type {any} */ lines) {
29
+ const add = lines.map((/** @type {any} */ l) => l.trim()).filter(Boolean);
30
30
  if (!add.length) return 0;
31
31
  ensureHome();
32
32
  // 审计 B9 + 质检 M8:与费用统计同口径,用配置时区(默认北京时间)自然日做记忆日期戳
33
33
  const bp = beijingParts(new Date());
34
34
  const date = `${bp.year}-${String(bp.month).padStart(2, '0')}-${String(bp.day).padStart(2, '0')}`;
35
- fs.appendFileSync(memoryFile(), add.map((l) => (l.startsWith('-') ? `- [${date}] ${l.slice(1).trim()}` : `- [${date}] ${l}`)).join('\n') + '\n');
35
+ fs.appendFileSync(memoryFile(), add.map((/** @type {any} */ l) => (l.startsWith('-') ? `- [${date}] ${l.slice(1).trim()}` : `- [${date}] ${l}`)).join('\n') + '\n');
36
36
  return add.length;
37
37
  }
38
38
 
@@ -43,7 +43,7 @@ function backupMemory() {
43
43
  }
44
44
 
45
45
  // 整体覆写(编辑面板保存用,自动备份)
46
- export function writeMemory(content) {
46
+ export function writeMemory(/** @type {any} */ content) {
47
47
  backupMemory();
48
48
  ensureHome();
49
49
  fs.writeFileSync(memoryFile(), String(content ?? ''));
@@ -75,7 +75,7 @@ export function dedupeMemory() {
75
75
  }
76
76
 
77
77
  // 删除包含关键词的条目
78
- export function removeMemoryLines(keyword) {
78
+ export function removeMemoryLines(/** @type {any} */ keyword) {
79
79
  const raw = loadMemory();
80
80
  const kw = String(keyword).trim().toLowerCase();
81
81
  if (!raw.trim() || !kw) return 0;
@@ -97,7 +97,7 @@ export function removeMemoryLines(keyword) {
97
97
 
98
98
  let journalCount = 0; // 内存计数(评估 P3-2):避免每次写入都整文件读一遍只为查行数
99
99
 
100
- export function appendJournal(home, entry) {
100
+ export function appendJournal(/** @type {any} */ home, /** @type {any} */ entry) {
101
101
  try {
102
102
  // journalFile() 已含 mingdaoHome()(home 参数仅为兼容旧签名保留)。
103
103
  // 不能 path.join(home, dirname(journalFile())):journalFile() 是绝对路径,join 会拼出
@@ -108,7 +108,7 @@ export function appendJournal(home, entry) {
108
108
  journalCount += 1;
109
109
  } catch (err) {
110
110
  // 静默吞错面收窄(评估建议 3):调试开关可见原因,正常使用仍零打扰
111
- if (process.env.MINGDAO_DEBUG) console.warn('[MingDao] journal 写入失败:' + (err?.message || err));
111
+ if (process.env.MINGDAO_DEBUG) console.warn('[MingDao] journal 写入失败:' + ((/** @type {any} */ (err))?.message || err));
112
112
  }
113
113
  // 低频截断:超过 600 行时重写保留最近 500 行(跨过上限后每 200 条检查一次)
114
114
  if (journalCount > 600 && journalCount % 200 === 0) {
@@ -123,7 +123,7 @@ export function appendJournal(home, entry) {
123
123
  }
124
124
  }
125
125
 
126
- export function recentJournal(home, n = 3) {
126
+ export function recentJournal(/** @type {any} */ home, n = 3) {
127
127
  try {
128
128
  const raw = fs.readFileSync(journalFile(), 'utf8');
129
129
  return raw
@@ -143,7 +143,7 @@ export function recentJournal(home, n = 3) {
143
143
  }
144
144
  }
145
145
 
146
- export function recentJournalBlock(home) {
146
+ export function recentJournalBlock(/** @type {any} */ home) {
147
147
  const entries = recentJournal(home, 3);
148
148
  if (!entries.length) return '';
149
149
  return (
@@ -160,12 +160,12 @@ export function recentJournalBlock(home) {
160
160
  }
161
161
 
162
162
  // 自动记忆提取:从对话提炼值得长期记住的用户偏好/事实(去重:已有记忆会先提供给模型)
163
- export async function extractMemory(provider, model, messages, existingMemory) {
163
+ export async function extractMemory(/** @type {any} */ provider, /** @type {any} */ model, /** @type {any} */ messages, /** @type {any} */ existingMemory) {
164
164
  try {
165
165
  const convo = messages
166
- .filter((m) => m.role === 'user' || m.role === 'assistant')
166
+ .filter((/** @type {any} */ m) => m.role === 'user' || m.role === 'assistant')
167
167
  .slice(-20)
168
- .map((m) => `${m.role === 'user' ? '用户' : 'MingDao'}:${String(m.content || '').slice(0, 500)}`)
168
+ .map((/** @type {any} */ m) => `${m.role === 'user' ? '用户' : 'MingDao'}:${String(m.content || '').slice(0, 500)}`)
169
169
  .join('\n');
170
170
  const msgs = [
171
171
  {
@@ -182,9 +182,9 @@ export async function extractMemory(provider, model, messages, existingMemory) {
182
182
  const j = JSON.parse(String(res.text || '').trim());
183
183
  const items = Array.isArray(j?.items) ? j.items : [];
184
184
  const clean = items
185
- .map((l) => String(l).trim())
185
+ .map((/** @type {any} */ l) => String(l).trim())
186
186
  .filter(Boolean)
187
- .map((l) => (/^[·•]/.test(l) ? '- ' + l.replace(/^[·•]\s*/, '') : l.startsWith('-') ? l : '- ' + l)) // 审计 Q4:非前缀条目规整为 "- ",不再二次回退调用
187
+ .map((/** @type {any} */ l) => (/^[·•]/.test(l) ? '- ' + l.replace(/^[·•]\s*/, '') : l.startsWith('-') ? l : '- ' + l)) // 审计 Q4:非前缀条目规整为 "- ",不再二次回退调用
188
188
  .slice(0, 10);
189
189
  if (clean.length || items.length === 0) return clean; // 空数组 = 无新增;有内容即返回
190
190
  } catch {}
@@ -217,8 +217,8 @@ export async function extractMemory(provider, model, messages, existingMemory) {
217
217
  }
218
218
 
219
219
  // 会话收尾:写日志 + 自动记忆(turns 太少的一次性会话只记日志不提取)
220
- export async function finalizeSession({ cfg, provider, model, home, workingDir, messages, turns, lastText }) {
221
- const firstUser = messages.find((m) => m.role === 'user')?.content || '';
220
+ export async function finalizeSession(/** @type {any} */ { cfg, provider, model, home, workingDir, messages, turns, lastText }) {
221
+ const firstUser = messages.find((/** @type {any} */ m) => m.role === 'user')?.content || '';
222
222
  try {
223
223
  const wsName = null; // 由调用方通过 currentWorkspace 提供会更好,这里保持轻量
224
224
  appendJournal(home, {
@@ -25,38 +25,38 @@ function loadCache() {
25
25
  }
26
26
  }
27
27
 
28
- function saveCache(data) {
28
+ function saveCache(/** @type {any} */ data) {
29
29
  try {
30
30
  ensureHome();
31
31
  fs.writeFileSync(modelCacheFile(), JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
32
32
  } catch {}
33
33
  }
34
34
 
35
- export function providerHasKey(providerName) {
36
- const pp = PROVIDERS[providerName];
35
+ export function providerHasKey(/** @type {any} */ providerName) {
36
+ const pp = /** @type {any} */ (PROVIDERS)[providerName];
37
37
  if (!pp) return false;
38
38
  if (getStoredKey(providerName)) return true;
39
39
  if (pp.envKey && process.env[pp.envKey]) return true;
40
40
  return false;
41
41
  }
42
42
 
43
- export function providerBaseUrl(cfg, providerName) {
44
- const pp = PROVIDERS[providerName];
43
+ export function providerBaseUrl(/** @type {any} */ cfg, /** @type {any} */ providerName) {
44
+ const pp = /** @type {any} */ (PROVIDERS)[providerName];
45
45
  if (!pp) return '';
46
46
  // 当前服务商的 baseUrl 覆盖优先
47
47
  if (providerName === cfg?.provider && cfg?.baseUrl) return cfg.baseUrl;
48
48
  return pp.baseUrl || '';
49
49
  }
50
50
 
51
- export function providerApiKey(providerName) {
52
- const pp = PROVIDERS[providerName];
51
+ export function providerApiKey(/** @type {any} */ providerName) {
52
+ const pp = /** @type {any} */ (PROVIDERS)[providerName];
53
53
  const stored = getStoredKey(providerName);
54
54
  if (stored) return stored;
55
55
  if (pp?.envKey && process.env[pp.envKey]) return process.env[pp.envKey];
56
56
  return '';
57
57
  }
58
58
 
59
- function isChatModel(id) {
59
+ function isChatModel(/** @type {any} */ id) {
60
60
  return !/embedding|rerank|moderation/i.test(id);
61
61
  }
62
62
 
@@ -64,7 +64,7 @@ function isChatModel(id) {
64
64
  // 返回 { models: [名称], fromCache, fetchedAt };失败返回 { error }
65
65
  const inflight = new Map(); // 同服务商并发去重:合并为同一请求
66
66
 
67
- export async function fetchProviderModels(cfg, providerName, { force = false } = {}) {
67
+ export async function fetchProviderModels(/** @type {any} */ cfg, /** @type {any} */ providerName, { force = false } = {}) {
68
68
  const base = providerBaseUrl(cfg, providerName).replace(/\/+$/, '');
69
69
  const key = providerApiKey(providerName);
70
70
  if (!base || !key) return { error: '该服务商未设置 API Key' };
@@ -86,8 +86,8 @@ export async function fetchProviderModels(cfg, providerName, { force = false } =
86
86
  if (!res.ok) return { error: `HTTP ${res.status}` };
87
87
  const j = /** @type {any} */ (await res.json().catch(() => null));
88
88
  const list = (j?.data || [])
89
- .map((m) => String(m?.id || '').trim())
90
- .filter((id) => id && isChatModel(id))
89
+ .map((/** @type {any} */ m) => String(m?.id || '').trim())
90
+ .filter((/** @type {any} */ id) => id && isChatModel(id))
91
91
  .slice(0, 200);
92
92
  if (!list.length) return { error: '接口未返回可用模型' };
93
93
  const cache2 = loadCache(); // 重新读取:避免与并发写入互相覆盖
@@ -95,7 +95,8 @@ export async function fetchProviderModels(cfg, providerName, { force = false } =
95
95
  saveCache(cache2);
96
96
  return { models: list, fromCache: false, fetchedAt: Date.now() };
97
97
  } catch (e) {
98
- return { error: e.name === 'AbortError' ? '请求超时(8s)' : e.message };
98
+ const ee = /** @type {any} */ (e);
99
+ return { error: ee.name === 'AbortError' ? '请求超时(8s)' : ee.message };
99
100
  } finally {
100
101
  clearTimeout(timer);
101
102
  }
@@ -109,7 +110,7 @@ export async function fetchProviderModels(cfg, providerName, { force = false } =
109
110
  }
110
111
 
111
112
  // 合并可用模型列表:只含已设置 Key 的服务商;动态名单优先、预设回退;自定义模型恒在。
112
- export async function availableModels(cfg, currentModel) {
113
+ export async function availableModels(/** @type {any} */ cfg, /** @type {any} */ currentModel) {
113
114
  const out = [];
114
115
  const seen = new Set();
115
116
  for (const [pname, pp] of Object.entries(PROVIDERS)) {
@@ -120,13 +121,13 @@ export async function availableModels(cfg, currentModel) {
120
121
  for (const n of names) {
121
122
  if (seen.has(n)) continue;
122
123
  seen.add(n);
123
- const preset = MODELS[n];
124
+ const preset = /** @type {any} */ (MODELS)[n];
124
125
  out.push({
125
126
  name: n,
126
127
  label: preset ? `${n} — ${preset.label}` : `${n}(线上最新)`,
127
128
  provider: pname,
128
129
  providerLabel: pp.label,
129
- dynamic: !MODELS[n],
130
+ dynamic: !(/** @type {any} */ (MODELS))[n],
130
131
  });
131
132
  }
132
133
  }
package/src/models.js CHANGED
@@ -183,10 +183,10 @@ export const MODELS = {
183
183
  },
184
184
  };
185
185
 
186
- export function modelPreset(name) {
187
- return MODELS[name] || null;
186
+ export function modelPreset(/** @type {any} */ name) {
187
+ return /** @type {any} */ (MODELS)[name] || null;
188
188
  }
189
189
 
190
- export function providerPreset(name) {
191
- return PROVIDERS[name] || null;
190
+ export function providerPreset(/** @type {any} */ name) {
191
+ return /** @type {any} */ (PROVIDERS)[name] || null;
192
192
  }
package/src/notify.js CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { spawn } from 'node:child_process';
8
8
 
9
- export function notify(title, message) {
9
+ export function notify(/** @type {any} */ title, /** @type {any} */ message) {
10
10
  try {
11
11
  const text = String(message).replace(/\s+/g, ' ').slice(0, 120);
12
12
  // 审计 P2-12:title 同样转义(mac 反斜杠/引号、win 单引号),防注入破坏命令
@@ -33,6 +33,6 @@ export function notify(title, message) {
33
33
  }
34
34
  }
35
35
 
36
- export function notifyTaskDone(question, status) {
36
+ export function notifyTaskDone(/** @type {any} */ question, /** @type {any} */ status) {
37
37
  notify('MingDao', `${status === 'done' ? '✓ 完成' : '✖ 失败'}:${String(question).slice(0, 60)}`);
38
38
  }
@@ -12,6 +12,7 @@
12
12
  import { style, C } from './ui.js';
13
13
  import { READONLY_TOOLS } from './tools/index.js';
14
14
 
15
+ /** @param {any} name @param {any} args */
15
16
  function summarize(name, args) {
16
17
  try {
17
18
  if (name === 'bash') return ` ${args.command ?? ''}`;
@@ -22,6 +23,7 @@ function summarize(name, args) {
22
23
  }
23
24
  }
24
25
 
26
+ /** @param {any} rule @param {any} name @param {any} args */
25
27
  function ruleMatches(rule, name, args) {
26
28
  const idx = rule.indexOf(':');
27
29
  if (idx > 0) {
@@ -37,6 +39,7 @@ function ruleMatches(rule, name, args) {
37
39
  return rule === name;
38
40
  }
39
41
 
42
+ /** @param {any} rawMode @param {any} io */
40
43
  export function createPermission(rawMode, io) {
41
44
  let mode = 'ask';
42
45
  let allow = [];
@@ -52,8 +55,9 @@ export function createPermission(rawMode, io) {
52
55
 
53
56
  return {
54
57
  mode,
58
+ /** @param {any} name @param {any} args @param {any} label */
55
59
  async check(name, args = {}, label = '') {
56
- const askOverride = async (question) => {
60
+ const askOverride = async (/** @type {any} */ question) => {
57
61
  try {
58
62
  const answer = await io.ask(style(question, C.yellow));
59
63
  return /^y(es)?$/i.test(answer);
@@ -63,10 +67,10 @@ export function createPermission(rawMode, io) {
63
67
  };
64
68
  // 需要特殊授权时弹出对话框与用户交互,而不是静默拒绝:
65
69
  // 1) 被 deny 规则拦截 → 询问是否本次强制放行
66
- if (deny.some((r) => ruleMatches(r, name, args))) {
70
+ if (deny.some((/** @type {any} */ r) => ruleMatches(r, name, args))) {
67
71
  return askOverride(`规则拦截了 ${name}${summarize(name, args)},是否本次强制放行?[y/N] `);
68
72
  }
69
- if (allow.some((r) => ruleMatches(r, name, args))) return true;
73
+ if (allow.some((/** @type {any} */ r) => ruleMatches(r, name, args))) return true;
70
74
  if (mode === 'auto') return true;
71
75
  if (mode === 'readonly') {
72
76
  // 2) 只读模式下的写操作 → 询问是否本次放行
package/src/pricing.js CHANGED
@@ -16,6 +16,7 @@ export const PRICE_DATA_AS_OF = '2026-08';
16
16
  // cfg.pricing.source 拉取(TTL 默认 7 天,cfg.pricing.ttlDays 可调);TTL 内覆盖内置表,
17
17
  // 过期自动回退内置并置 stale 标记(/cost 与费用标签会提示)。
18
18
  function pricingFilePath() { return path.join(mingdaoHome(), 'pricing.json'); }
19
+ /** @type {{ mtime: number, data: any, stale: boolean }} */
19
20
  let extCache = { mtime: -1, data: null, stale: false };
20
21
  function externalPricing() {
21
22
  try {
@@ -35,6 +36,9 @@ function externalPricing() {
35
36
  export function pricingDataStale() { return externalPricing().stale; }
36
37
 
37
38
  // 拉取官方价格表(零依赖 fetch):cfg.pricing.source 为 JSON 地址,返回 { models: { <模型名>: { input, output, cacheHit, peak? } } }
39
+ /**
40
+ * @param {any} cfg
41
+ */
38
42
  export async function refreshPricingFromSource(cfg) {
39
43
  const src = String(cfg?.pricing?.source || '').trim();
40
44
  if (!src) return { ok: false, lines: ['未配置 pricing.source(config.json 的 pricing.source 填官方价格 JSON 地址后重试)'] };
@@ -58,6 +62,7 @@ export async function refreshPricingFromSource(cfg) {
58
62
  // 用本机时区判断会让海外用户计费错位。config.pricing.timezone 可覆盖时区、
59
63
  // config.pricing.peakWindows 可覆盖高峰窗口([[起,止],...] 北京时间整点),mtime 缓存避免每次读盘。
60
64
  const DEFAULT_PEAK_WINDOWS = [[9, 12], [14, 18]];
65
+ /** @type {{ mtime: number, timezone: string, peakWindows: number[][], ttlDays?: number }} */
61
66
  let tzCache = { mtime: 0, timezone: 'Asia/Shanghai', peakWindows: DEFAULT_PEAK_WINDOWS };
62
67
  function peakCfg() {
63
68
  try {
@@ -116,11 +121,14 @@ export function beijingParts(date = new Date()) {
116
121
  second: '2-digit',
117
122
  hourCycle: 'h23',
118
123
  }).formatToParts(date);
119
- const g = (t) => Number(parts.find((p) => p.type === t)?.value);
124
+ const g = /** @type {(t: any) => number} */ ((t) => Number(parts.find((p) => p.type === t)?.value));
120
125
  return { year: g('year'), month: g('month'), day: g('day'), hour: g('hour'), minute: g('minute'), second: g('second') };
121
126
  }
122
127
 
123
128
  // 北京时间墙钟 → Date(UTC+8)
129
+ /**
130
+ * @param {any} parts
131
+ */
124
132
  export function beijingToDate(parts) {
125
133
  return new Date(Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second) - 8 * 3600 * 1000);
126
134
  }
@@ -153,6 +161,11 @@ export function beijingDayStart(date = new Date()) {
153
161
 
154
162
  // —— Batch API 半价计价:批量任务无缓存语义,按闲时全未命中 × 0.5 ——
155
163
  export const BATCH_DISCOUNT = 0.5;
164
+ /**
165
+ * @param {any} modelName
166
+ * @param {any} promptTokens
167
+ * @param {any} completionTokens
168
+ */
156
169
  export function estimateBatchCost(modelName, promptTokens, completionTokens) {
157
170
  const pricing = effectivePricing(modelName);
158
171
  if (!pricing) return 0;
@@ -160,6 +173,9 @@ export function estimateBatchCost(modelName, promptTokens, completionTokens) {
160
173
  }
161
174
 
162
175
  // usage 中的缓存拆分(DeepSeek 返回字段)
176
+ /**
177
+ * @param {any} usage
178
+ */
163
179
  export function cacheSplit(usage) {
164
180
  const hit = usage?.prompt_cache_hit_tokens;
165
181
  const miss = usage?.prompt_cache_miss_tokens;
@@ -170,6 +186,7 @@ export function cacheSplit(usage) {
170
186
  }
171
187
 
172
188
  // 合并内置价与用户覆盖:offpeak 字段覆盖 offpeak,over.peak 覆盖 peak(缺省沿用 offpeak)
189
+ /** @type {{ mtime: number, overrides: Record<string, any> }} */
173
190
  let overridesCache = { mtime: 0, overrides: {} };
174
191
  function pricingOverrides() {
175
192
  try {
@@ -182,6 +199,9 @@ function pricingOverrides() {
182
199
  } catch {}
183
200
  return overridesCache.overrides;
184
201
  }
202
+ /**
203
+ * @param {any} modelName
204
+ */
185
205
  function effectivePricing(modelName) {
186
206
  const preset = modelPreset(modelName);
187
207
  const ext = externalPricing().data?.models?.[modelName] || null;
@@ -189,6 +209,7 @@ function effectivePricing(modelName) {
189
209
  const base = ext || preset?.pricing || null;
190
210
  if (!base) return null;
191
211
  const over = pricingOverrides()[modelName] || {}; // 审计 Q3:mtime 缓存替代每轮读盘
212
+ /** @type {(b: any, o?: any) => { input: number, output: number, cacheHit: number }} */
192
213
  const merge = (b, o = {}) => ({
193
214
  input: Number(o.input ?? b?.input ?? 0),
194
215
  output: Number(o.output ?? b?.output ?? 0),
@@ -208,6 +229,13 @@ function effectivePricing(modelName) {
208
229
  };
209
230
  }
210
231
 
232
+ /**
233
+ * @param {any} modelName
234
+ * @param {any} promptTokens
235
+ * @param {any} completionTokens
236
+ * @param {{ hit: number, miss: number, rate?: number } | null} [cache]
237
+ * @param {Date} [date]
238
+ */
211
239
  export function estimateCost(modelName, promptTokens, completionTokens, cache = null, date = new Date()) {
212
240
  const pricing = effectivePricing(modelName);
213
241
  if (!pricing) return 0;
@@ -221,6 +249,12 @@ export function estimateCost(modelName, promptTokens, completionTokens, cache =
221
249
  return (promptTokens * price.input + completionTokens * price.output) / 1e6;
222
250
  }
223
251
 
252
+ /**
253
+ * @param {any} modelName
254
+ * @param {any} promptTokens
255
+ * @param {any} completionTokens
256
+ * @param {any} [usage]
257
+ */
224
258
  export function estimateCostLabel(modelName, promptTokens, completionTokens, usage = null) {
225
259
  const pricing = effectivePricing(modelName);
226
260
  if (!pricing) return '';
package/src/prompts.js CHANGED
@@ -21,7 +21,7 @@ const BASE = `你是 MingDao Harness,一个由 MingDao Harness 驱动的 AI
21
21
  9. 善用工具:能用工具确认的事实就用工具确认;多个只读工具可连续调用以提高效率。
22
22
  10. 交付收尾:完成工具操作后必须给出简短交付总结——生成了哪些文件(路径)、如何运行/使用(如「浏览器打开 angry-birds.html 即可游玩」或运行命令)、以及未完成或注意事项(若有)。绝不能不声不响结束。`;
23
23
 
24
- function loadFile(p, cap) {
24
+ function loadFile(/** @type {any} */ p, /** @type {any} */ cap) {
25
25
  try {
26
26
  const s = fs.readFileSync(p, 'utf8');
27
27
  return s.length > cap ? s.slice(0, cap) + '\n…[过长已截断]' : s;
@@ -16,7 +16,7 @@ import { modelPreset, providerPreset } from '../models.js';
16
16
  import { mingdaoHome } from '../config.js';
17
17
  import { resolveApiKey } from '../credentials.js';
18
18
 
19
- export function resolveProviderConfig(cfg, modelName) {
19
+ export function resolveProviderConfig(/** @type {any} */ cfg, /** @type {any} */ modelName) {
20
20
  // 自定义模型(config.customModels,WebUI 可增删改):优先于内置预设
21
21
  const cm = cfg?.customModels?.[modelName];
22
22
  if (cm) {
@@ -47,16 +47,16 @@ export function resolveProviderConfig(cfg, modelName) {
47
47
  };
48
48
  }
49
49
 
50
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
50
+ const sleep = (/** @type {any} */ ms) => new Promise((r) => setTimeout(r, ms));
51
51
 
52
- function isTransient(err) {
52
+ function isTransient(/** @type {any} */ err) {
53
53
  const status = err?.status;
54
54
  if (status === 429 || (status >= 500 && status <= 504)) return true;
55
55
  // 注意:不含 abort —— 用户 Ctrl+C 主动中断不应触发重试
56
56
  return /timeout|超时|ECONNRESET|fetch failed/i.test(String(err?.message || ''));
57
57
  }
58
58
 
59
- export async function createProvider(cfg, modelName, { timeoutMs = 300000, retries = 2 } = {}) {
59
+ export async function createProvider(/** @type {any} */ cfg, /** @type {any} */ modelName, { timeoutMs = 300000, retries = 2 } = {}) {
60
60
  const pc = resolveProviderConfig(cfg, modelName);
61
61
 
62
62
  // 自定义 Provider 模块优先(仅普通自定义端点;custom:<模型名> 走 OpenAI 兼容直连)
@@ -77,7 +77,7 @@ export async function createProvider(cfg, modelName, { timeoutMs = 300000, retri
77
77
  return {
78
78
  name: pc.name,
79
79
  config: pc,
80
- async chat(opts) {
80
+ async chat(/** @type {any} */ opts) {
81
81
  let attempt = 0;
82
82
  for (;;) {
83
83
  const ac = new AbortController();
@@ -105,7 +105,7 @@ export async function createProvider(cfg, modelName, { timeoutMs = 300000, retri
105
105
  attempt += 1;
106
106
  // 指数退避 + 尊重 Retry-After(评估 P3-1):基础 1s/2s,服务端指定时取其值(封顶 30s)
107
107
  let backoff = 1000 * attempt;
108
- const ra = Number(err?.headers?.get?.('retry-after'));
108
+ const ra = Number((/** @type {any} */ (err))?.headers?.get?.('retry-after'));
109
109
  if (Number.isFinite(ra) && ra > 0) backoff = Math.max(backoff, ra * 1000);
110
110
  backoff = Math.min(backoff, 30000);
111
111
  await sleep(backoff);
@@ -121,7 +121,7 @@ export async function createProvider(cfg, modelName, { timeoutMs = 300000, retri
121
121
  // 辅助调用(标题生成/记忆提取等)的 provider 解析(评估 P2-2):辅助模型与当前模型
122
122
  // 分属不同服务商时单独创建 provider——避免拿 deepseek-v4-flash 模型名去自定义网关请求
123
123
  // 而 404 被静默吞掉(标题缺失、记忆永不沉淀且零感知)。
124
- export async function helperProvider(cfg, helperModel, currentProvider) {
124
+ export async function helperProvider(/** @type {any} */ cfg, /** @type {any} */ helperModel, /** @type {any} */ currentProvider) {
125
125
  try {
126
126
  const curName = String(currentProvider?.name || '');
127
127
  const helperName = String(resolveProviderConfig(cfg, helperModel)?.name || '');