mingdao-harness 0.1.59 → 0.1.60
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/README.md +8 -0
- package/docs/QA-REPORT.md +1 -1
- package/package.json +3 -2
- package/src/agent.js +8 -1
- package/src/batch.js +22 -15
- package/src/cachestats.js +18 -8
- package/src/cli.js +12 -3
- package/src/commands/sync.js +2 -1
- package/src/compact.js +23 -8
- package/src/config.js +2 -0
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/model-discovery.js +1 -1
- package/src/prompts.js +11 -4
- package/src/schedule.js +7 -3
- package/src/sync-server.js +2 -1
- package/src/sync.js +3 -3
- package/src/tasks.js +1 -1
- package/src/tools/bash.js +28 -1
- package/src/update.js +3 -3
- package/src/web/index.html +1 -0
- package/src/web/server.js +5 -4
package/README.md
CHANGED
|
@@ -101,6 +101,14 @@ git clone https://gitee.com/MingDaoTCM/MingDao-harness.git MingDao-Harness && cd
|
|
|
101
101
|
node src/cli.js # 直接运行,无需安装
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
开发者护栏(提交前建议执行;CI 会强制跑全套):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm install # 仅装 devDependencies(typescript + @types/node),运行时依旧零依赖
|
|
108
|
+
npm run typecheck # tsc --checkJs 类型护栏(覆盖 agent/cli/commands/provider/cachestats 等核心模块)
|
|
109
|
+
node test/smoke.js && node test/e2e-local.js && node test/e2e-web.js && node test/e2e-schedule.js
|
|
110
|
+
```
|
|
111
|
+
|
|
104
112
|
### 验证安装
|
|
105
113
|
|
|
106
114
|
```bash
|
package/docs/QA-REPORT.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mingdao-harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.60",
|
|
4
4
|
"description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"typecheck": "tsc -p tsconfig.json",
|
|
53
53
|
"prepublishOnly": "node test/smoke.js && node test/e2e-local.js && node test/e2e-schedule.js",
|
|
54
54
|
"desktop": "npm --prefix desktop start",
|
|
55
|
-
"desktop:dist": "npm --prefix desktop run dist:dir"
|
|
55
|
+
"desktop:dist": "node scripts/sync-versions.mjs && npm --prefix desktop run dist:dir",
|
|
56
|
+
"desktop:sync": "node scripts/sync-versions.mjs"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/node": "^26.2.0",
|
package/src/agent.js
CHANGED
|
@@ -16,6 +16,11 @@ import { checkCostGuard } from './cost-guard.js';
|
|
|
16
16
|
const MAX_STEPS = 24;
|
|
17
17
|
const SUBAGENT_MAX_STEPS = 12;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 创建 Agent 循环(调用方只需传 provider/permission/io/modelName/workingDir,其余可选)
|
|
21
|
+
* @param {{ provider: any, permission: any, io: any, modelName: any, workingDir: any,
|
|
22
|
+
* cfg?: any, undoStore?: any, maxSteps?: number, mcp?: any, onCompact?: any, sessionRef?: any }} params
|
|
23
|
+
*/
|
|
19
24
|
export function createAgent({ provider, permission, io, modelName, workingDir, cfg = {}, undoStore, maxSteps, mcp, onCompact, sessionRef }) {
|
|
20
25
|
const preset = modelPreset(modelName) || {};
|
|
21
26
|
const budget = cfg.contextBudget || preset.budgetTokens || 128000;
|
|
@@ -174,8 +179,10 @@ export function createAgent({ provider, permission, io, modelName, workingDir, c
|
|
|
174
179
|
io.startSpinner('正在思考…');
|
|
175
180
|
|
|
176
181
|
let res;
|
|
182
|
+
// 审计(tsc 扩面发现):llmT0 此前在 try 内声明、catch 内引用——chat 抛错时
|
|
183
|
+
// catch 自身 ReferenceError,掩盖原始错误且计时丢失;提到 try 外声明。
|
|
184
|
+
const llmT0 = Date.now();
|
|
177
185
|
try {
|
|
178
|
-
const llmT0 = Date.now();
|
|
179
186
|
res = await provider.chat({
|
|
180
187
|
model: modelName,
|
|
181
188
|
messages: trimmed,
|
package/src/batch.js
CHANGED
|
@@ -11,7 +11,6 @@ import path from 'node:path';
|
|
|
11
11
|
import { resolveProviderConfig } from './providers/index.js';
|
|
12
12
|
import { estimateBatchCost, BATCH_DISCOUNT } from './pricing.js';
|
|
13
13
|
import { recordCacheStats } from './cachestats.js';
|
|
14
|
-
import { buildSystemPrompt } from './prompts.js';
|
|
15
14
|
|
|
16
15
|
const DEFAULT_WINDOW = '24h';
|
|
17
16
|
const DEFAULT_ENDPOINT = '/v1/chat/completions';
|
|
@@ -26,15 +25,16 @@ function batchBase(cfg, model) {
|
|
|
26
25
|
return pc.name === 'deepseek' ? base.replace(/\/v1\/?$/, '') : base;
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
/** @returns {Promise<any>} */
|
|
29
29
|
async function api(base, apiKey, methodPath, payload, httpMethod = 'POST') {
|
|
30
30
|
const res = await fetch(base + methodPath, {
|
|
31
31
|
method: httpMethod,
|
|
32
32
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
33
33
|
body: payload === undefined ? undefined : JSON.stringify(payload),
|
|
34
34
|
});
|
|
35
|
-
const j = await res.json().catch(() => ({}));
|
|
35
|
+
const j = /** @type {any} */ (await res.json().catch(() => ({})));
|
|
36
36
|
if (!res.ok) {
|
|
37
|
-
const e = new Error(j?.error?.message || j?.message || `HTTP ${res.status}`);
|
|
37
|
+
const e = /** @type {Error & { status?: number }} */ (new Error(j?.error?.message || j?.message || `HTTP ${res.status}`));
|
|
38
38
|
e.status = res.status;
|
|
39
39
|
throw e;
|
|
40
40
|
}
|
|
@@ -50,9 +50,9 @@ async function uploadFile(base, apiKey, jsonl) {
|
|
|
50
50
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
51
51
|
body: form,
|
|
52
52
|
});
|
|
53
|
-
const j = await res.json().catch(() => ({}));
|
|
53
|
+
const j = /** @type {any} */ (await res.json().catch(() => ({})));
|
|
54
54
|
if (!res.ok) {
|
|
55
|
-
const e = new Error(j?.error?.message || j?.message || `上传失败 HTTP ${res.status}`);
|
|
55
|
+
const e = /** @type {Error & { status?: number }} */ (new Error(j?.error?.message || j?.message || `上传失败 HTTP ${res.status}`));
|
|
56
56
|
e.status = res.status;
|
|
57
57
|
throw e;
|
|
58
58
|
}
|
|
@@ -84,7 +84,8 @@ async function downloadResults(base, apiKey, batch) {
|
|
|
84
84
|
throw new Error('批处理结果文件不可用(端点不支持或文件已过期)');
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
/** 执行一次批处理。questions: string[]。返回 { ok, outputFile, results, usage, cost, batchId }
|
|
88
|
+
* @param {{ cfg: any, model: any, questions: any, workingDir?: string, maxTokens?: number, temperature?: any, signal?: any, onStatus?: any }} opts */
|
|
88
89
|
export async function runBatch({ cfg, model, questions, workingDir = process.cwd(), maxTokens = 4096, temperature, signal, onStatus }) {
|
|
89
90
|
const list = (questions || []).map((q) => String(q).trim()).filter(Boolean);
|
|
90
91
|
if (!list.length) return { error: '没有可批处理的问题(每行一个问题)' };
|
|
@@ -92,7 +93,10 @@ export async function runBatch({ cfg, model, questions, workingDir = process.cwd
|
|
|
92
93
|
if (!pc.apiKey) return { error: `模型 ${model} 没有可用 API Key(mingdao key set ${pc.name})` };
|
|
93
94
|
const base = batchBase(cfg, model);
|
|
94
95
|
const apiKey = pc.apiKey;
|
|
95
|
-
|
|
96
|
+
// 审计(workbuddy P2-1):批量任务无缓存语义,每个问题的 system 都按 input 全价计费——
|
|
97
|
+
// 不再携带完整系统提示(技能清单/用户记忆/AGENTS.md 对无工具批任务毫无意义,1000 问
|
|
98
|
+
// 可白烧 50-80 万 token);改用一行精简角色提示,剩余能力损失为零。
|
|
99
|
+
const systemPrompt = '你是 MingDao Harness 编程助手。直接针对每个问题给出准确、完整的答案,不要复述问题、不要解释过程。';
|
|
96
100
|
const bodyTemplate = {
|
|
97
101
|
model,
|
|
98
102
|
messages: null, // 逐行填充
|
|
@@ -122,11 +126,14 @@ export async function runBatch({ cfg, model, questions, workingDir = process.cwd
|
|
|
122
126
|
completion_window: cfg?.batchWindow || DEFAULT_WINDOW,
|
|
123
127
|
});
|
|
124
128
|
onStatus?.(`任务已创建:${batch.id}`);
|
|
125
|
-
//
|
|
126
|
-
|
|
129
|
+
// 轮询:指数退避(审计 workbuddy P3-3)——基础间隔 5s(MINGDAO_BATCH_POLL_MS 可覆盖,测试用),
|
|
130
|
+
// ×1.5 逐次翻倍、30s 封顶:24h 窗口内轮询请求量从 ~1.7 万次降到 ~3 千次;
|
|
131
|
+
// 连续失败 10 次 → 报错,绝不无限重试。每次轮询报告进度(含已处理 X/Y)。
|
|
132
|
+
const baseInterval = Math.max(500, Number(process.env.MINGDAO_BATCH_POLL_MS) || 5000);
|
|
127
133
|
const t0 = Date.now();
|
|
128
134
|
let st = batch.status;
|
|
129
135
|
let failures = 0;
|
|
136
|
+
let polls = 0;
|
|
130
137
|
for (;;) {
|
|
131
138
|
if (signal?.aborted) return { error: '已取消轮询(任务仍在服务端运行)', batchId: batch.id };
|
|
132
139
|
if (Date.now() - t0 > 24 * 3600 * 1000) return { error: '批处理超过 24h 窗口', batchId: batch.id };
|
|
@@ -137,7 +144,7 @@ export async function runBatch({ cfg, model, questions, workingDir = process.cwd
|
|
|
137
144
|
} catch (err) {
|
|
138
145
|
failures += 1;
|
|
139
146
|
if (failures >= 10) return { error: `轮询失败:${err?.message || err}(任务仍在服务端,ID ${batch.id})`, batchId: batch.id };
|
|
140
|
-
await new Promise((r) => setTimeout(r,
|
|
147
|
+
await new Promise((r) => setTimeout(r, Math.min(baseInterval * 1.5 ** failures, 30000)));
|
|
141
148
|
continue;
|
|
142
149
|
}
|
|
143
150
|
st = j.status;
|
|
@@ -149,11 +156,11 @@ export async function runBatch({ cfg, model, questions, workingDir = process.cwd
|
|
|
149
156
|
const detail = j?.errors?.data?.[0]?.message || j?.errors?.message || '';
|
|
150
157
|
return { error: `批处理失败:${st}${detail ? '(' + detail + ')' : ''}`, batchId: batch.id };
|
|
151
158
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
await new Promise((r) => setTimeout(r,
|
|
159
|
+
polls += 1;
|
|
160
|
+
const rc = j?.request_counts || {};
|
|
161
|
+
const done = rc.completed != null && rc.total != null ? `${rc.completed}/${rc.total}` : '';
|
|
162
|
+
onStatus?.(`状态:${st}${done ? `(已处理 ${done})` : ''}`);
|
|
163
|
+
await new Promise((r) => setTimeout(r, Math.min(baseInterval * 1.5 ** polls, 30000)));
|
|
157
164
|
}
|
|
158
165
|
onStatus?.('下载结果…');
|
|
159
166
|
const results = await downloadResults(base, apiKey, batch);
|
package/src/cachestats.js
CHANGED
|
@@ -49,17 +49,27 @@ export function recordCacheStats(entry) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// listCacheStats 读取缓存(审计:costGuard 每步 / WebUI 每 15s / /cost 命令都调用——
|
|
53
|
+
// 四报告共识 P2-2/§3.3-A:全文件读+逐行 parse 会随 JSONL 增长线性放大 IO。
|
|
54
|
+
// 按 mtimeMs+size 双键缓存;写入追加会改两者,轮转重写同样改,缓存自动失效)
|
|
55
|
+
let _statsCache = null;
|
|
56
|
+
|
|
52
57
|
export function listCacheStats(limit = 2000) {
|
|
53
58
|
try {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const file = cacheStatsFile();
|
|
60
|
+
const st = fs.statSync(file);
|
|
61
|
+
if (!_statsCache || _statsCache.mtimeMs !== st.mtimeMs || _statsCache.size !== st.size || _statsCache.limit < limit) {
|
|
62
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const l of raw.split('\n')) {
|
|
65
|
+
if (!l.trim()) continue;
|
|
66
|
+
try {
|
|
67
|
+
out.push(JSON.parse(l));
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
_statsCache = { mtimeMs: st.mtimeMs, size: st.size, limit, lines: out };
|
|
61
71
|
}
|
|
62
|
-
return
|
|
72
|
+
return _statsCache.lines.slice(-limit);
|
|
63
73
|
} catch {
|
|
64
74
|
return [];
|
|
65
75
|
}
|
package/src/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import { enableAutostart, disableAutostart, autostartStatus, autostartPath } fro
|
|
|
23
23
|
import { notifyTaskDone } from './notify.js';
|
|
24
24
|
import { addWorkspace, removeWorkspace, workspacePath, touchWorkspace, listWorkspaces, currentWorkspace } from './workspace.js';
|
|
25
25
|
import { finalizeSession, extractMemory, loadMemory, appendMemory, recentJournal, dedupeMemory, removeMemoryLines } from './memory.js';
|
|
26
|
-
import { recordUsage, listCacheStats, summarizeCacheStats, formatCacheSummary } from './cachestats.js';
|
|
26
|
+
import { recordUsage, listCacheStats, summarizeCacheStats, formatCacheSummary, costBreakdown } from './cachestats.js';
|
|
27
27
|
import { presetList, buildPreset } from './mcp-presets.js';
|
|
28
28
|
import {
|
|
29
29
|
addSchedule,
|
|
@@ -508,10 +508,12 @@ async function main() {
|
|
|
508
508
|
const route = await routeTask({ cfg, provider, currentModel: modelName, text: question });
|
|
509
509
|
if (route.model !== modelName) {
|
|
510
510
|
if (!jsonMode) io.print(style(`⤷ 自动路由 → ${route.model}(${route.reason})`, C.dim));
|
|
511
|
+
// 审计 P1-2(第五轮复审实证):先按新模型重建 provider、再改模型名——
|
|
512
|
+
// 此前顺序颠倒(先赋值再判断),条件恒假,跨服务商路由池会把 executor 模型名
|
|
513
|
+
// 发到 planner 的 baseUrl/key 上(401/404)。默认同服务商配置不受影响。
|
|
514
|
+
provider = await createProvider(cfg, route.model);
|
|
511
515
|
modelName = route.model;
|
|
512
516
|
}
|
|
513
|
-
// 审计 P1-2:自动路由改换模型后必须重建对应 provider(此前用旧模型的 baseUrl/key 发请求)
|
|
514
|
-
if (route.model !== modelName) provider = await createProvider(cfg, modelName);
|
|
515
517
|
// JSON 模式:关闭流式输出,结果以单行 JSON 输出(脚本/管道友好)
|
|
516
518
|
const turnIo = jsonMode ? createIO({ quiet: true }) : io;
|
|
517
519
|
const session = createSession(home);
|
|
@@ -712,6 +714,9 @@ async function main() {
|
|
|
712
714
|
if (input.startsWith('/')) {
|
|
713
715
|
const [cmd, ...rest] = input.split(/\s+/);
|
|
714
716
|
const arg = rest.join(' ');
|
|
717
|
+
// 审计(第五轮 P1-1 教训):斜杠命令统一 try/catch——单条命令异常只提示不退出,
|
|
718
|
+
// 绝不再因一条命令的错误杀死整个 REPL 会话(历史 P1-1 曾导致会话上下文全丢)
|
|
719
|
+
try {
|
|
715
720
|
if (cmd === '/exit' || cmd === '/quit') break;
|
|
716
721
|
else if (cmd === '/help') printHelpLines(io.print);
|
|
717
722
|
else if (cmd === '/clear') {
|
|
@@ -993,6 +998,10 @@ async function main() {
|
|
|
993
998
|
io.print(style('未知命令,输入 /help 查看可用命令。', C.yellow));
|
|
994
999
|
}
|
|
995
1000
|
continue;
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
io.print(style('[错误] 命令执行失败:' + (err?.message || err), C.red));
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
996
1005
|
}
|
|
997
1006
|
|
|
998
1007
|
// 自动路由:规划类任务切 planner,执行类走 executor(会话粘滞 + 分类缓存见 routing.js)
|
package/src/commands/sync.js
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
|
|
19
19
|
async function askHidden(question) {
|
|
20
20
|
return new Promise((resolve) => {
|
|
21
|
-
|
|
21
|
+
// _writeToOutput 为 readline 内部接口:静音回显(密码输入),类型护栏下显式 any
|
|
22
|
+
const rl = /** @type {any} */ (readline.createInterface({ input: process.stdin, output: process.stdout }));
|
|
22
23
|
const orig = rl._writeToOutput;
|
|
23
24
|
rl._writeToOutput = () => {};
|
|
24
25
|
rl.question(question, (a) => {
|
package/src/compact.js
CHANGED
|
@@ -19,12 +19,12 @@ const INPUT_MAX_CHARS = 30000; // 摘要输入上限(防超长工具输出撑
|
|
|
19
19
|
const TOOL_OUTPUT_CAP = 300; // 摘要输入中每条工具结果截断长度
|
|
20
20
|
|
|
21
21
|
const SUMMARY_SYSTEM =
|
|
22
|
-
'你是 MingDao 的会话压缩器。把对话记录压成紧凑中文摘要(≤500 字,要点列表):' +
|
|
22
|
+
'你是 MingDao Harness 的会话压缩器。把对话记录压成紧凑中文摘要(≤500 字,要点列表):' +
|
|
23
23
|
'保留用户目标与关键要求、已完成的步骤与结论、修改/创建的文件、未完成事项、重要约定与决策;' +
|
|
24
|
-
'
|
|
24
|
+
'省略已完成的中间过程与细节。只输出 JSON:{"summary": "摘要内容"}。';
|
|
25
25
|
|
|
26
26
|
export async function summarizeConversation(provider, model, convoText) {
|
|
27
|
-
const
|
|
27
|
+
const base = {
|
|
28
28
|
model,
|
|
29
29
|
messages: [
|
|
30
30
|
{ role: 'system', content: SUMMARY_SYSTEM },
|
|
@@ -32,11 +32,26 @@ export async function summarizeConversation(provider, model, convoText) {
|
|
|
32
32
|
],
|
|
33
33
|
tools: [],
|
|
34
34
|
temperature: 0.2,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
};
|
|
36
|
+
// 结构化输出(审计 MiniMax §3.3-D):压缩是 30K 输入 × pro 价的大开销,与标题/记忆/路由
|
|
37
|
+
// 一致改 json_object + maxTokens 2048→1024(1600 字摘要足够);网关不支持时回退纯文本。
|
|
38
|
+
let text = '';
|
|
39
|
+
let usage = null;
|
|
40
|
+
try {
|
|
41
|
+
const res = await provider.chat({ ...base, maxTokens: 1024, responseFormat: { type: 'json_object' } });
|
|
42
|
+
const j = JSON.parse(String(res?.text || '').trim());
|
|
43
|
+
text = String(j?.summary || '').trim();
|
|
44
|
+
usage = res?.usage || null;
|
|
45
|
+
} catch {}
|
|
46
|
+
if (!text) {
|
|
47
|
+
try {
|
|
48
|
+
const res = await provider.chat({ ...base, maxTokens: 1024 });
|
|
49
|
+
text = String(res?.text || '').trim();
|
|
50
|
+
usage = res?.usage || null;
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
if (!text) return { text: null, usage };
|
|
54
|
+
return { text: text.slice(0, SUMMARY_MAX_CHARS), usage };
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
export async function compactConversation({ messages, budget, count, provider, executorModel, triggerRatio }) {
|
package/src/config.js
CHANGED
|
@@ -23,6 +23,8 @@ export function configPath() {
|
|
|
23
23
|
return path.join(mingdaoHome(), 'config.json');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** 读取配置对象(不存在/损坏返回 null);返回值为用户可编辑的任意 JSON 配置,类型不定
|
|
27
|
+
* @returns {any} */
|
|
26
28
|
export function loadConfig() {
|
|
27
29
|
try {
|
|
28
30
|
return JSON.parse(fs.readFileSync(configPath(), 'utf8'));
|
package/src/mcp.js
CHANGED
package/src/memory.js
CHANGED
|
@@ -171,7 +171,7 @@ export async function extractMemory(provider, model, messages, existingMemory) {
|
|
|
171
171
|
{
|
|
172
172
|
role: 'system',
|
|
173
173
|
content:
|
|
174
|
-
'你是 MingDao
|
|
174
|
+
'你是 MingDao Harness 的记忆提取器。从对话中提取值得长期记住的用户偏好与事实(工具链、代码风格、项目背景、个人约定、常用指令等)。每条 ≤30 字,只输出新条目(与「已有记忆」重复或对话中未提及的不要输出)。只输出 JSON:{"items": ["条目1", "条目2"]};没有新增时输出 {"items": []}。\n已有记忆:\n' +
|
|
175
175
|
(existingMemory || '(空)'),
|
|
176
176
|
},
|
|
177
177
|
{ role: 'user', content: convo.slice(0, 8000) },
|
package/src/model-discovery.js
CHANGED
|
@@ -84,7 +84,7 @@ export async function fetchProviderModels(cfg, providerName, { force = false } =
|
|
|
84
84
|
redirect: 'follow',
|
|
85
85
|
});
|
|
86
86
|
if (!res.ok) return { error: `HTTP ${res.status}` };
|
|
87
|
-
const j = await res.json().catch(() => null);
|
|
87
|
+
const j = /** @type {any} */ (await res.json().catch(() => null));
|
|
88
88
|
const list = (j?.data || [])
|
|
89
89
|
.map((m) => String(m?.id || '').trim())
|
|
90
90
|
.filter((id) => id && isChatModel(id))
|
package/src/prompts.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { skillsRegistryBlock } from './skills.js';
|
|
7
|
-
import { mingdaoHome } from './config.js';
|
|
7
|
+
import { mingdaoHome, loadConfig } from './config.js';
|
|
8
8
|
import { recentJournalBlock } from './memory.js';
|
|
9
9
|
|
|
10
10
|
const BASE = `你是 MingDao Harness,一个由 MingDao Harness 驱动的 AI 编程助手。你在用户的电脑上工作:通过工具读写文件、搜索代码、执行命令,帮助用户完成编程、调试与自动化任务。
|
|
@@ -29,6 +29,7 @@ function loadFile(p, cap) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** @param {{ workingDir: any, withJournal?: boolean, [key: string]: any }} opts */
|
|
32
33
|
export function buildSystemPrompt({ workingDir, withJournal = false }) {
|
|
33
34
|
// 前缀字节稳定性(评估 P1-1/P1-2,四份评估一致的最高价值项):
|
|
34
35
|
// 系统提示不含「当前模型」「当前日期」等易变字段——DeepSeek 上下文缓存按前缀字节匹配,
|
|
@@ -50,9 +51,15 @@ export function buildSystemPrompt({ workingDir, withJournal = false }) {
|
|
|
50
51
|
// 技能清单(渐进披露:仅名称+描述,按需加载全文)
|
|
51
52
|
prompt += skillsRegistryBlock(workingDir);
|
|
52
53
|
|
|
53
|
-
// 项目约定(./AGENTS.md
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// 项目约定(./AGENTS.md)——体积可配置(审计 MiniMax §3.3-B,v0.1.48 P0-D):
|
|
55
|
+
// 典型项目 6-12K 的 AGENTS.md 全量进 system 每轮按缓存价计费;默认截 4K,超长部分
|
|
56
|
+
// 模型可 read 工具按需读全文。config.maxAgentsMdChars 可调(0 表示不注入)。
|
|
57
|
+
const cfg = loadConfig();
|
|
58
|
+
const agentsMdCap = cfg && Number.isFinite(Number(cfg.maxAgentsMdChars)) ? Math.max(0, Number(cfg.maxAgentsMdChars)) : 4000;
|
|
59
|
+
if (agentsMdCap > 0) {
|
|
60
|
+
const agentsMd = loadFile(path.join(workingDir, 'AGENTS.md'), agentsMdCap);
|
|
61
|
+
if (agentsMd) prompt += `\n\n<agents_md>\n${agentsMd}\n</agents_md>`;
|
|
62
|
+
}
|
|
56
63
|
|
|
57
64
|
return prompt;
|
|
58
65
|
}
|
package/src/schedule.js
CHANGED
|
@@ -93,8 +93,8 @@ export function writeSchedule(home, job) {
|
|
|
93
93
|
return job;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
export function addSchedule(home, question, { at, every, after, permission, model, cwd, anchor, offpeak }) {
|
|
96
|
+
/** 新建调度任务;after: 依赖的任务 ID(全部成功后才启动,任一失败则跳过) */
|
|
97
|
+
export function addSchedule(home, question, /** @type {any} */ { at, every, after, permission, model, cwd, anchor, offpeak }) {
|
|
98
98
|
const id = 'sc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
99
99
|
const interval = every != null ? parseInterval(every) : null;
|
|
100
100
|
const afterList = Array.isArray(after) ? after.filter(Boolean).map(String) : after ? String(after).split(',').map((x) => x.trim()).filter(Boolean) : [];
|
|
@@ -276,7 +276,11 @@ export function spawnDaemon(home) {
|
|
|
276
276
|
env: { ...process.env, MINGDAO_HOME: home },
|
|
277
277
|
});
|
|
278
278
|
try {
|
|
279
|
-
|
|
279
|
+
// 原子写(审计 workbuddy P3-4):tmp+rename 与 writeSchedule 同款——崩溃不留半截 pid 文件
|
|
280
|
+
const target = daemonPidFile(home);
|
|
281
|
+
const tmp = target + '.tmp';
|
|
282
|
+
fs.writeFileSync(tmp, `${child.pid} ${nonce}`);
|
|
283
|
+
fs.renameSync(tmp, target);
|
|
280
284
|
} catch {}
|
|
281
285
|
child.unref();
|
|
282
286
|
return true;
|
package/src/sync-server.js
CHANGED
|
@@ -193,7 +193,7 @@ async function doRegister(body) {
|
|
|
193
193
|
// 审计 P2-10:注册用进程内互斥,避免并发同名注册双双成功(后写覆盖)
|
|
194
194
|
if (!registerLock) {
|
|
195
195
|
registerLock = new Promise((resolve) => {
|
|
196
|
-
queueMicrotask(resolve);
|
|
196
|
+
queueMicrotask(() => resolve(undefined));
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
const prev = registerLock;
|
|
@@ -530,6 +530,7 @@ async function handle(req, res) {
|
|
|
530
530
|
}
|
|
531
531
|
|
|
532
532
|
// ---------- 启动 ----------
|
|
533
|
+
/** @param {{ port?: any, host?: any, dataDir?: any, cert?: any, key?: any }} [opts] */
|
|
533
534
|
export function runSyncServer({ port, host, dataDir, cert, key } = {}) {
|
|
534
535
|
const dir = dataDir || DEFAULT_DATA_DIR;
|
|
535
536
|
ACTIVE_DIR = dir;
|
package/src/sync.js
CHANGED
|
@@ -63,7 +63,7 @@ async function apiCall(baseUrl, method, payload, token, timeoutMs = TIMEOUT_MS,
|
|
|
63
63
|
if (insecure) {
|
|
64
64
|
const { status, json: j } = await rawRequest(target, { headers, body, timeoutMs, insecure: true });
|
|
65
65
|
if (status !== 200) {
|
|
66
|
-
const err = new Error(j.error || `HTTP ${status}`);
|
|
66
|
+
const err = /** @type {Error & { status?: number, body?: any }} */ (new Error(j.error || `HTTP ${status}`));
|
|
67
67
|
err.status = status;
|
|
68
68
|
err.body = j;
|
|
69
69
|
throw err;
|
|
@@ -79,9 +79,9 @@ async function apiCall(baseUrl, method, payload, token, timeoutMs = TIMEOUT_MS,
|
|
|
79
79
|
body,
|
|
80
80
|
signal: ctrl.signal,
|
|
81
81
|
});
|
|
82
|
-
const j = await res.json().catch(() => ({}));
|
|
82
|
+
const j = /** @type {any} */ (await res.json().catch(() => ({})));
|
|
83
83
|
if (!res.ok) {
|
|
84
|
-
const err = new Error(j.error || `HTTP ${res.status}`);
|
|
84
|
+
const err = /** @type {Error & { status?: number, body?: any }} */ (new Error(j.error || `HTTP ${res.status}`));
|
|
85
85
|
err.status = res.status;
|
|
86
86
|
err.body = j;
|
|
87
87
|
throw err;
|
package/src/tasks.js
CHANGED
|
@@ -54,7 +54,7 @@ export function isValidTaskId(id) {
|
|
|
54
54
|
return typeof id === 'string' && /^[a-z0-9]+$/.test(id) && id.length >= 4 && id.length <= 40;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function startTask(home, question, { permission, model, cwd, offpeak } = {}) {
|
|
57
|
+
export function startTask(home, question, { permission, model, cwd, offpeak } = /** @type {any} */ ({})) {
|
|
58
58
|
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) + process.pid.toString(36);
|
|
59
59
|
const task = {
|
|
60
60
|
id,
|
package/src/tools/bash.js
CHANGED
|
@@ -43,8 +43,35 @@ export function detectSandbox() {
|
|
|
43
43
|
return sandboxSupport;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// 输出折叠(审计 MiniMax §3.3-E / v0.1.48 P1-G):模型回填的 bash 输出先折叠再截断——
|
|
47
|
+
// 1) 剥离 ANSI 转义序列(CSI/OSC);2) 连续重复行(>3 行相同)折叠为「首行 + 重复标记」。
|
|
48
|
+
// npm install 类输出通常 30-50KB → 折叠后 5-10KB,单次工具回填省 60-70% prompt token。
|
|
49
|
+
function stripAnsi(s) {
|
|
50
|
+
return s
|
|
51
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
52
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
|
|
53
|
+
}
|
|
54
|
+
function foldRepeats(s) {
|
|
55
|
+
const lines = s.split('\n');
|
|
56
|
+
const out = [];
|
|
57
|
+
let i = 0;
|
|
58
|
+
while (i < lines.length) {
|
|
59
|
+
let j = i;
|
|
60
|
+
while (j + 1 < lines.length && lines[j + 1] === lines[i]) j += 1;
|
|
61
|
+
const run = j - i + 1;
|
|
62
|
+
if (run > 3) {
|
|
63
|
+
out.push(lines[i], `…(以上重复 ${run} 行,已折叠)`);
|
|
64
|
+
i = j + 1;
|
|
65
|
+
} else {
|
|
66
|
+
out.push(lines[i]);
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out.join('\n');
|
|
71
|
+
}
|
|
46
72
|
function tail(s, n) {
|
|
47
|
-
|
|
73
|
+
const folded = foldRepeats(stripAnsi(s));
|
|
74
|
+
return folded.length > n ? `…[输出过长,已截断头部]\n${folded.slice(-n)}` : folded;
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
export function runBash(args, ctx) {
|
package/src/update.js
CHANGED
|
@@ -111,7 +111,7 @@ const NPM_HINT =
|
|
|
111
111
|
'源码包安装请重新运行安装脚本(install.sh);或从任意平台仓库克隆后 `npm link`:' +
|
|
112
112
|
'https://gitee.com/MingDaoTCM/MingDao-harness · https://gitcode.com/MingDaoTCM/MingDao-Harness · https://github.com/MingDaoTCM/MingDao-Harness';
|
|
113
113
|
|
|
114
|
-
export async function updateCheck({ repo } = {}) {
|
|
114
|
+
export async function updateCheck({ repo } = /** @type {any} */ ({})) {
|
|
115
115
|
const lines = [];
|
|
116
116
|
const root = resolveRepo(repo);
|
|
117
117
|
if (!root) return { ok: false, lines: [NPM_HINT] };
|
|
@@ -130,7 +130,7 @@ export async function updateCheck({ repo } = {}) {
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
export async function mingdaoUpdate({ repo } = {}) {
|
|
133
|
+
export async function mingdaoUpdate({ repo } = /** @type {any} */ ({})) {
|
|
134
134
|
const lines = [];
|
|
135
135
|
const root = resolveRepo(repo);
|
|
136
136
|
if (!root) return { ok: false, lines: [NPM_HINT] };
|
|
@@ -195,7 +195,7 @@ export async function mingdaoUpdate({ repo } = {}) {
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
export function mingdaoRollback({ repo } = {}) {
|
|
198
|
+
export function mingdaoRollback({ repo } = /** @type {any} */ ({})) {
|
|
199
199
|
const root = resolveRepo(repo);
|
|
200
200
|
if (!root) return { ok: false, lines: [NPM_HINT] };
|
|
201
201
|
const st = readState();
|
package/src/web/index.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'">
|
|
6
7
|
<title>MingDao Harness</title>
|
|
7
8
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
8
9
|
<link rel="icon" href="/icon-192.png" type="image/png">
|
package/src/web/server.js
CHANGED
|
@@ -74,7 +74,7 @@ function readBody(req) {
|
|
|
74
74
|
let size = 0;
|
|
75
75
|
// 审计 P2-6:慢速连接防护——60s 未传完请求体即断开,防占满 socket
|
|
76
76
|
const slowTimer = setTimeout(() => {
|
|
77
|
-
const err = new Error('请求体上传超时(60s)');
|
|
77
|
+
const err = /** @type {Error & { status?: number }} */ (new Error('请求体上传超时(60s)'));
|
|
78
78
|
err.status = 408;
|
|
79
79
|
req.destroy();
|
|
80
80
|
reject(err);
|
|
@@ -84,7 +84,7 @@ function readBody(req) {
|
|
|
84
84
|
req.on('data', (d) => {
|
|
85
85
|
size += d.length;
|
|
86
86
|
if (size > MAX_BODY) {
|
|
87
|
-
const err = new Error('请求体过大(>40MB)');
|
|
87
|
+
const err = /** @type {Error & { status?: number }} */ (new Error('请求体过大(>40MB)'));
|
|
88
88
|
err.status = 413;
|
|
89
89
|
req.destroy();
|
|
90
90
|
reject(err);
|
|
@@ -104,6 +104,7 @@ function readBody(req) {
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/** @param {{ host?: string, port?: number, authToken?: string|null, [key: string]: any }} [opts] */
|
|
107
108
|
export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken } = {}) {
|
|
108
109
|
const home = ensureHome();
|
|
109
110
|
const cfg = loadConfig();
|
|
@@ -1138,7 +1139,7 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
|
|
|
1138
1139
|
});
|
|
1139
1140
|
});
|
|
1140
1141
|
|
|
1141
|
-
server.on('error', (err) => {
|
|
1142
|
+
server.on('error', (/** @type {Error & { code?: string }} */ err) => {
|
|
1142
1143
|
if (err.code === 'EADDRINUSE') {
|
|
1143
1144
|
console.error(`[MingDao] 端口 ${port} 已被占用,请换一个端口:mingdao web <端口号>`);
|
|
1144
1145
|
} else {
|
|
@@ -1148,7 +1149,7 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken
|
|
|
1148
1149
|
});
|
|
1149
1150
|
|
|
1150
1151
|
server.listen(port, host, () => {
|
|
1151
|
-
const actual = server.address().port;
|
|
1152
|
+
const actual = /** @type {import('node:net').AddressInfo} */ (server.address()).port;
|
|
1152
1153
|
boundPort = actual;
|
|
1153
1154
|
const displayHost = host === '0.0.0.0' ? '127.0.0.1' : host;
|
|
1154
1155
|
console.log('');
|