mingdao-harness 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/AUDIT-v0.4.6.md +179 -0
- package/docs/PACK-API.md +250 -0
- package/docs/SAVINGS-BENCHMARK.md +12 -5
- package/docs/STRATEGY-0.5.md +262 -0
- package/docs/STRATEGY-NEXT.md +1 -1
- package/package.json +2 -1
- package/skills-lib/bash/SKILL.md +26 -0
- package/skills-lib/changelog/SKILL.md +24 -0
- package/skills-lib/ci-cd/SKILL.md +25 -0
- package/skills-lib/data-analysis/SKILL.md +25 -0
- package/skills-lib/database-design/SKILL.md +25 -0
- package/skills-lib/docs/SKILL.md +25 -0
- package/skills-lib/email/SKILL.md +25 -0
- package/skills-lib/file-organize/SKILL.md +27 -0
- package/skills-lib/git-workflow/SKILL.md +26 -0
- package/skills-lib/i18n/SKILL.md +24 -0
- package/skills-lib/markdown/SKILL.md +26 -0
- package/skills-lib/meeting-notes/SKILL.md +25 -0
- package/skills-lib/nodejs/SKILL.md +26 -0
- package/skills-lib/performance/SKILL.md +25 -0
- package/skills-lib/python/SKILL.md +26 -0
- package/skills-lib/readme/SKILL.md +25 -0
- package/skills-lib/regex/SKILL.md +25 -0
- package/skills-lib/report/SKILL.md +25 -0
- package/skills-lib/resume/SKILL.md +24 -0
- package/skills-lib/security-audit/SKILL.md +28 -0
- package/skills-lib/sql/SKILL.md +25 -0
- package/skills-lib/translation/SKILL.md +25 -0
- package/src/agent.js +95 -9
- package/src/atomic-write.js +9 -0
- package/src/autostart.js +37 -3
- package/src/batch.js +9 -1
- package/src/cachestats.js +32 -8
- package/src/cli.js +29 -14
- package/src/commands/repl.js +5 -3
- package/src/commands/update.js +1 -1
- package/src/compact.js +16 -1
- package/src/context.js +8 -0
- package/src/cost-guard.js +26 -6
- package/src/hooks.js +20 -4
- package/src/log-writer.js +19 -7
- package/src/mcp.js +4 -0
- package/src/memory.js +6 -5
- package/src/model-caps.js +48 -4
- package/src/permissions.js +10 -4
- package/src/presets.js +13 -3
- package/src/pricing.js +78 -18
- package/src/providers/index.js +43 -13
- package/src/providers/openai-compatible.js +42 -1
- package/src/redact.js +5 -0
- package/src/schedule.js +62 -30
- package/src/session.js +14 -1
- package/src/skill-lib.js +23 -4
- package/src/skills.js +2 -2
- package/src/sync-server.js +29 -1
- package/src/sync.js +6 -3
- package/src/tasks/worker.js +4 -1
- package/src/tasks.js +4 -1
- package/src/tokenizer.js +48 -11
- package/src/tools/bash.js +9 -2
- package/src/tools/fetch.js +97 -8
- package/src/tools/fs-tools.js +48 -3
- package/src/tools/git.js +17 -1
- package/src/tools/index.js +12 -5
- package/src/web/constants.js +10 -0
- package/src/web/routes/api.js +3 -1
- package/src/web/routes/domains/misc.js +6 -0
- package/src/web/server.js +41 -31
package/src/web/server.js
CHANGED
|
@@ -25,9 +25,10 @@ import { createProvider, resolveProviderConfig, helperProvider } from '../provid
|
|
|
25
25
|
import { MODELS, modelPreset, PROVIDERS } from '../models.js';
|
|
26
26
|
import { routeTask, routingConfig } from '../routing.js';
|
|
27
27
|
import { buildUserContent } from './attachments.js';
|
|
28
|
-
import { MAX_CONCURRENT } from './constants.js';
|
|
28
|
+
import { MAX_CONCURRENT, SECURITY_HEADERS } from './constants.js';
|
|
29
29
|
import { createAgent } from '../agent.js';
|
|
30
30
|
import { createPermission } from '../permissions.js';
|
|
31
|
+
import { isPrivateHost as sharedIsPrivateHost } from '../tools/fetch.js';
|
|
31
32
|
import { buildSystemPrompt } from '../prompts.js';
|
|
32
33
|
import { loadProjectMemory, loadProjectMemoryEntries, retrieveRelevant, extractAndAppendProjectMemory } from '../memory.js';
|
|
33
34
|
import { saveTaskStateMerge, clearTaskState, loadTaskState, resumePrompt } from '../task-state.js';
|
|
@@ -69,7 +70,7 @@ const INDEX_HTML = path.join(path.dirname(fileURLToPath(import.meta.url)), 'inde
|
|
|
69
70
|
|
|
70
71
|
/** @param {any} res @param {any} code @param {any} obj */
|
|
71
72
|
function json(res, code, obj) {
|
|
72
|
-
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
73
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', ...SECURITY_HEADERS });
|
|
73
74
|
res.end(JSON.stringify(obj));
|
|
74
75
|
// 返回 truthy:域路由 `return json(...)` 必须被编排器视为「已处理」——
|
|
75
76
|
// 否则兜底 404 会对已结束的响应二次写入(Node 18 下 write-after-end 未捕获 → 整服务崩溃)
|
|
@@ -85,15 +86,30 @@ function readBody(req, limit = 40 * 1024 * 1024) {
|
|
|
85
86
|
/** @type {any[]} */
|
|
86
87
|
const chunks = [];
|
|
87
88
|
let size = 0;
|
|
89
|
+
let settled = false;
|
|
90
|
+
const settle = (/** @type {any} */ fn, /** @type {any} */ v) => {
|
|
91
|
+
if (settled) return;
|
|
92
|
+
settled = true;
|
|
93
|
+
clearTimeout(slowTimer);
|
|
94
|
+
fn(v);
|
|
95
|
+
};
|
|
88
96
|
// 审计 P2-6:慢速连接防护——60s 未传完请求体即断开,防占满 socket
|
|
89
97
|
const slowTimer = setTimeout(() => {
|
|
90
98
|
const err = /** @type {Error & { status?: number }} */ (new Error('请求体上传超时(60s)'));
|
|
91
99
|
err.status = 408;
|
|
92
100
|
req.destroy();
|
|
93
|
-
reject
|
|
101
|
+
settle(reject, err);
|
|
94
102
|
}, 60000);
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
// P1-6(v0.4.5):body 上传中断(客户端刷新/abort)时 Node 通常只触发 close 不发 error/end——
|
|
104
|
+
// 此前 close 仅 clearTimeout 不 reject → Promise 永久 pending → /api/chat 的 inflight 槽永久泄漏,
|
|
105
|
+
// 重复 N 次后服务对所有会话 429。close 且未 settle 时 reject(status 499)。
|
|
106
|
+
req.on('close', () => {
|
|
107
|
+
if (!settled) {
|
|
108
|
+
const err = /** @type {Error & { status?: number }} */ (new Error('请求体上传中断(客户端断开)'));
|
|
109
|
+
err.status = 499;
|
|
110
|
+
settle(reject, err);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
97
113
|
req.on('data', (/** @type {any} */ d) => {
|
|
98
114
|
size += d.length;
|
|
99
115
|
if (size > MAX_BODY) {
|
|
@@ -102,7 +118,7 @@ function readBody(req, limit = 40 * 1024 * 1024) {
|
|
|
102
118
|
// 排空残余数据但保留连接:让上层 catch 返回真 413(此前 destroy 导致客户端只收到连接重置)
|
|
103
119
|
req.pause();
|
|
104
120
|
req.resume();
|
|
105
|
-
reject
|
|
121
|
+
settle(reject, err);
|
|
106
122
|
return;
|
|
107
123
|
}
|
|
108
124
|
chunks.push(d);
|
|
@@ -110,12 +126,12 @@ function readBody(req, limit = 40 * 1024 * 1024) {
|
|
|
110
126
|
req.on('end', () => {
|
|
111
127
|
const raw = Buffer.concat(chunks).toString('utf8');
|
|
112
128
|
try {
|
|
113
|
-
resolve
|
|
129
|
+
settle(resolve, raw ? JSON.parse(raw) : {});
|
|
114
130
|
} catch {
|
|
115
|
-
resolve
|
|
131
|
+
settle(resolve, {});
|
|
116
132
|
}
|
|
117
133
|
});
|
|
118
|
-
req.on('error', reject);
|
|
134
|
+
req.on('error', (/** @type {any} */ e) => settle(reject, e));
|
|
119
135
|
});
|
|
120
136
|
}
|
|
121
137
|
|
|
@@ -260,23 +276,12 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken,
|
|
|
260
276
|
const srvlog = createLogWriter(path.join(mingdaoHome(), 'logs', 'web-server.log'));
|
|
261
277
|
|
|
262
278
|
// —— SSRF 防护(质检 S1):远端地址校验 ——
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (h.includes(':')) {
|
|
270
|
-
// IPv6:回环 ::1、链路本地 fe80::/10、唯一本地 fc00::/7、IPv4 映射 ::ffff:私网
|
|
271
|
-
if (/^::ffff:/.test(h)) return isPrivateHost(h.slice(7));
|
|
272
|
-
return /^fe[89ab]/.test(h) || /^f[cd]/.test(h) || h === '::' || h === '::1';
|
|
273
|
-
}
|
|
274
|
-
const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
275
|
-
if (!m) return false; // 域名:由 validateRemoteUrl 的 DNS 解析复检
|
|
276
|
-
const a = Number(m[1]);
|
|
277
|
-
const b = Number(m[2]);
|
|
278
|
-
return a === 10 || a === 127 || a === 0 || a >= 224 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);
|
|
279
|
-
}
|
|
279
|
+
// v0.4.6 P1:本文件的 isPrivateHost 副本已删除,改用 tools/fetch.js 的单一实现。
|
|
280
|
+
// 副本只识别 `::ffff:` + 点分四段,而 URL 解析器会把该形态规范化成十六进制
|
|
281
|
+
// ([::ffff:127.0.0.1] → [::ffff:7f00:1]),副本判为公网 → 私网目标放行。
|
|
282
|
+
// 单一来源同时覆盖 skill-lib.installFromUrl 与 fetch 工具,避免复现同款缺口。
|
|
283
|
+
const isPrivateHost = (/** @type {any} */ h) => sharedIsPrivateHost(h);
|
|
284
|
+
const isIpLiteral = (/** @type {any} */ h) => /^\d{1,3}(\.\d{1,3}){3}$/.test(String(h)) || String(h).includes(':');
|
|
280
285
|
/** @param {any} raw */
|
|
281
286
|
async function validateRemoteUrl(raw) {
|
|
282
287
|
let u;
|
|
@@ -293,7 +298,7 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken,
|
|
|
293
298
|
if (!cfg.web?.allowPrivateEndpoints && !serverBoundLocal) {
|
|
294
299
|
const h = String(u.hostname || '').toLowerCase();
|
|
295
300
|
let blocked = isPrivateHost(h);
|
|
296
|
-
if (!blocked && h && h !== 'localhost' &&
|
|
301
|
+
if (!blocked && h && h !== 'localhost' && !isIpLiteral(h)) {
|
|
297
302
|
try {
|
|
298
303
|
const { lookup } = await import('node:dns/promises');
|
|
299
304
|
const addrs = await lookup(h, { all: true, verbatim: true });
|
|
@@ -475,8 +480,12 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken,
|
|
|
475
480
|
// 权限/选择类交互:发 ask 事件(带 taskId),等待 POST /api/permission 应答
|
|
476
481
|
const askHandler = (/** @type {any} */ { question, hidden, options, label, confirm }) =>
|
|
477
482
|
new Promise((resolve) => {
|
|
478
|
-
|
|
479
|
-
|
|
483
|
+
// P1 修复(v0.4.6):ask id 必须是不可猜测的随机值,且服务端在应答时校验它——
|
|
484
|
+
// 此前用 Math.random().toString(36) 且 /api/permission 从不核对 id,导致任何能访问
|
|
485
|
+
// API 的一方(局域网共享 token、或回环下的本机进程)只要知道可枚举的 taskId,
|
|
486
|
+
// 就能替他人的挂起确认直接答「允许」,把默认 ask 档这道唯一的人工闸门整个绕过。
|
|
487
|
+
const id = crypto.randomBytes(16).toString('hex');
|
|
488
|
+
entry.pendingAsk = { id, options: options || null, resolve };
|
|
480
489
|
send({
|
|
481
490
|
type: 'ask',
|
|
482
491
|
id,
|
|
@@ -513,14 +522,15 @@ export async function runWebServer({ host = '127.0.0.1', port = 3820, authToken,
|
|
|
513
522
|
return;
|
|
514
523
|
}
|
|
515
524
|
// v0.4.4:长任务费用逐轮入账——记录已按轮入账的累计 usage,最终补记最后一轮 + 兜底总结的剩余。
|
|
525
|
+
// v0.4.5:onUsage 收 (modelName, delta)——主/子代理各自按实际模型归属(子代理经 spawnTask 透传,费用不漏计)。
|
|
516
526
|
const recorded = { prompt_tokens: 0, completion_tokens: 0, prompt_cache_hit_tokens: 0, prompt_cache_miss_tokens: 0 };
|
|
517
|
-
const onUsage = (/** @type {any} */ delta) => {
|
|
527
|
+
const onUsage = (/** @type {string} */ modelName, /** @type {any} */ delta) => {
|
|
518
528
|
recorded.prompt_tokens += delta?.prompt_tokens || 0;
|
|
519
529
|
recorded.completion_tokens += delta?.completion_tokens || 0;
|
|
520
530
|
recorded.prompt_cache_hit_tokens += delta?.prompt_cache_hit_tokens || 0;
|
|
521
531
|
recorded.prompt_cache_miss_tokens += delta?.prompt_cache_miss_tokens || 0;
|
|
522
532
|
try {
|
|
523
|
-
recordUsage(
|
|
533
|
+
recordUsage(modelName, delta, null);
|
|
524
534
|
} catch {}
|
|
525
535
|
};
|
|
526
536
|
const agent = createAgent({
|