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 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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 质检日期:2026-08-18
4
4
  > 项目路径:`/home/YouLi/AI/DeepSeek-harness-Space/MingDao-Harness`
5
- > 版本:0.4.0(纯 ESM,零 npm 依赖,Node.js ≥ 18.17)
5
+ > 版本:0.1.54(历史归档;现行 0.1.x 口径见根 README 与各版本 tag。纯 ESM,零 npm 依赖,Node.js ≥ 18.17)
6
6
  > 质检方式:全量源码走读 + 冒烟/端到端测试 + 针对性缺陷验证脚本
7
7
 
8
8
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mingdao-harness",
3
- "version": "0.1.59",
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
- // 执行一次批处理。questions: string[]。返回 { ok, outputFile, results, usage, cost, batchId }
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
- const systemPrompt = buildSystemPrompt({ workingDir });
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
- // 轮询(间隔可用 MINGDAO_BATCH_POLL_MS 覆盖,测试用);连续失败 10 次 → 报错,绝不无限重试
126
- const interval = Math.max(500, Number(process.env.MINGDAO_BATCH_POLL_MS) || 5000);
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, interval));
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
- if (st !== 'in_progress' && st !== 'validating' && st !== 'finalizing') {
153
- // 未知状态:继续等待但报告
154
- onStatus?.(`状态:${st}`);
155
- }
156
- await new Promise((r) => setTimeout(r, interval));
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 raw = fs.readFileSync(cacheStatsFile(), 'utf8');
55
- const out = [];
56
- for (const l of raw.split('\n')) {
57
- if (!l.trim()) continue;
58
- try {
59
- out.push(JSON.parse(l));
60
- } catch {}
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 out.slice(-limit);
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)
@@ -18,7 +18,8 @@ import {
18
18
 
19
19
  async function askHidden(question) {
20
20
  return new Promise((resolve) => {
21
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
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 res = await provider.chat({
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
- maxTokens: 2048, // 审计 Q2:与 SUMMARY_MAX_CHARS(1600 字) 匹配,避免模型侧先截断
36
- });
37
- const text = String(res?.text || '').trim();
38
- if (!text) return { text: null, usage: res?.usage || null };
39
- return { text: text.slice(0, SUMMARY_MAX_CHARS), usage: res?.usage || null };
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
@@ -151,7 +151,7 @@ export class McpClient {
151
151
  this.notify('notifications/initialized');
152
152
  this.ready = true;
153
153
  clearTimeout(timer);
154
- resolve();
154
+ resolve(undefined);
155
155
  })
156
156
  .catch((err) => {
157
157
  clearTimeout(timer);
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 的记忆提取器。从对话中提取值得长期记住的用户偏好与事实(工具链、代码风格、项目背景、个人约定、常用指令等)。每条一行,以 - 开头,≤30 字,只输出新条目(与「已有记忆」重复或对话中未提及的不要输出);没有新增时 items 为空数组。\n已有记忆:\n' +
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) },
@@ -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
- const agentsMd = loadFile(path.join(workingDir, 'AGENTS.md'), 20000);
55
- if (agentsMd) prompt += `\n\n<agents_md>\n${agentsMd}\n</agents_md>`;
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
- // 新建调度任务;after: 依赖的任务 ID(全部成功后才启动,任一失败则跳过)
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
- fs.writeFileSync(daemonPidFile(home), `${child.pid} ${nonce}`);
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;
@@ -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
- return s.length > n ? `…[输出过长,已截断头部]\n${s.slice(-n)}` : s;
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();
@@ -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('');