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.
- package/docs/ROADMAP-NEXT.md +5 -5
- package/package.json +1 -1
- package/src/agent.js +19 -19
- package/src/audit.js +2 -2
- package/src/batch.js +7 -7
- package/src/cachestats.js +9 -9
- package/src/cli.js +29 -679
- package/src/commands/desktop.js +1 -1
- package/src/commands/key.js +1 -1
- package/src/commands/repl.js +711 -0
- package/src/commands/schedule.js +7 -7
- package/src/commands/skill.js +18 -16
- package/src/commands/sync.js +5 -5
- package/src/commands/update.js +7 -6
- package/src/commands/workspace.js +2 -2
- package/src/compact.js +6 -6
- package/src/config.js +7 -4
- package/src/context.js +11 -11
- package/src/credentials.js +6 -0
- package/src/hooks.js +11 -11
- package/src/mcp-presets.js +5 -5
- package/src/mcp.js +19 -19
- package/src/memory.js +16 -16
- package/src/model-discovery.js +16 -15
- package/src/models.js +4 -4
- package/src/notify.js +2 -2
- package/src/permissions.js +7 -3
- package/src/pricing.js +35 -1
- package/src/prompts.js +1 -1
- package/src/providers/index.js +7 -7
- package/src/providers/openai-compatible.js +8 -8
- package/src/routing.js +5 -5
- package/src/schedule.js +28 -28
- package/src/session-index.js +5 -1
- package/src/session.js +11 -2
- package/src/skill-lib.js +62 -3
- package/src/skill-registry.js +10 -9
- package/src/skills.js +10 -10
- package/src/sync-server.js +86 -3
- package/src/sync.js +53 -20
- package/src/tasks/worker.js +126 -0
- package/src/tasks.js +13 -13
- package/src/titles.js +5 -5
- package/src/tokenizer.js +22 -4
- package/src/tools/bash.js +12 -12
- package/src/tools/fs-tools.js +66 -9
- package/src/tools/index.js +7 -7
- package/src/ui.js +83 -6
- package/src/update.js +8 -8
- package/src/web/attachments.js +1 -1
- package/src/web/routes/api.js +38 -786
- package/src/web/routes/domains/config.js +280 -0
- package/src/web/routes/domains/misc.js +128 -0
- package/src/web/routes/domains/schedule.js +127 -0
- package/src/web/routes/domains/sessions.js +105 -0
- package/src/web/routes/domains/skills.js +79 -0
- package/src/web/routes/domains/sync.js +101 -0
- package/src/web/routes/domains/workspace.js +113 -0
- package/src/web/server.js +29 -14
- package/src/web/web-io.js +54 -12
- 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, {
|
package/src/model-discovery.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/permissions.js
CHANGED
|
@@ -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;
|
package/src/providers/index.js
CHANGED
|
@@ -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 || '');
|