mingdao-harness 0.1.70 → 0.2.1
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/QUALITY-REVIEW-0.1.68.md +3 -2
- package/package.json +8 -3
- package/src/cli.js +22 -13
- package/src/commands/skill.js +7 -1
- package/src/sync-server.js +25 -3
- package/src/web/app.js +1026 -0
- package/src/web/index.html +10 -1016
- package/src/web/routes/api.js +878 -0
- package/src/web/server.js +71 -780
|
@@ -87,8 +87,9 @@
|
|
|
87
87
|
- L1/L2/L3/L6/L11/L12/L13 顺手修
|
|
88
88
|
- 验收:94 组测试 + 38 组 bench 全绿、tsc 0 错误、三平台 CI 过
|
|
89
89
|
|
|
90
|
-
### Phase 2 —「结构治理」v0.2.0(≈4–6
|
|
91
|
-
|
|
90
|
+
### Phase 2 —「结构治理」v0.2.0(≈4–6 天)✅ 已实施(2026-08-29)
|
|
91
|
+
实施项:server.js 拆路由模块(src/web/routes/api.js,1299→579 行)、cli.js 命令分发显式化、SPA JS 外置 app.js + CSP 收紧、SSRF 目标校验、inflight 并发、会话写互斥、listen Promise + 桌面端口重试、sync-server 限流加固、API Key 即时生效、更新下载失败可见化。
|
|
92
|
+
(cli.js 的 REPL/worker 进一步拆分与 routes/api.js 按域细分留待 0.2.1 渐进完成。)
|
|
92
93
|
- H5 server.js 拆 `src/web/routes/*.js`(chat/sessions/config/sync/schedule/skills/workspace/fs-browse 等)+ 共享中间件
|
|
93
94
|
- M13 cli.js 命令分发重构 + REPL/worker 拆分 + 重复逻辑抽取
|
|
94
95
|
- M14 index.html JS 外置 `app.js` + CSP 收紧(去 unsafe-inline)+ 图标表/双拉取去重
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mingdao-harness",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,13 +48,18 @@
|
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"scripts": {
|
|
50
50
|
"start": "node src/cli.js",
|
|
51
|
-
"test": "node test/
|
|
51
|
+
"test": "node test/run-all.mjs",
|
|
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
55
|
"desktop:dist": "node scripts/sync-versions.mjs && npm --prefix desktop run dist:dir",
|
|
56
56
|
"desktop:sync": "node scripts/sync-versions.mjs",
|
|
57
|
-
"bench": "node test/bench/bench-tokenizer.mjs && node test/bench/bench-routing.mjs && node test/bench/bench-compaction.mjs"
|
|
57
|
+
"bench": "node test/bench/bench-tokenizer.mjs && node test/bench/bench-routing.mjs && node test/bench/bench-compaction.mjs",
|
|
58
|
+
"coverage": "node test/run-all.mjs --coverage && node scripts/coverage-report.mjs",
|
|
59
|
+
"typecheck:strict": "node scripts/strict-ratchet.mjs",
|
|
60
|
+
"preversion": "node test/smoke.js && node test/e2e-local.js && npm run bench",
|
|
61
|
+
"version": "node scripts/bump-changelog.mjs",
|
|
62
|
+
"postversion": "git add package.json package-lock.json CHANGELOG.md 2>/dev/null; git commit -m \"chore: release v$npm_package_version\" --no-verify || true"
|
|
58
63
|
},
|
|
59
64
|
"devDependencies": {
|
|
60
65
|
"@types/node": "^26.2.0",
|
package/src/cli.js
CHANGED
|
@@ -267,21 +267,30 @@ async function main() {
|
|
|
267
267
|
const withJournal = Boolean(opts.journal);
|
|
268
268
|
|
|
269
269
|
// —— 命令分发(已拆至 src/commands/,评估 P0-1 拆 cli.js)——
|
|
270
|
-
// 各 handler 返回 true = 已处理;false =
|
|
270
|
+
// 各 handler 返回 true = 已处理;false = 按普通提问继续(保留词劫持防护)。
|
|
271
|
+
// 质检 M3:显式命令映射(命令 → {module, handler}),模块与命令一一对应、handler 名显式可查
|
|
271
272
|
{
|
|
272
|
-
const dispatchTable =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
273
|
+
const dispatchTable = {
|
|
274
|
+
update: { module: 'update', handler: 'handleUpdateFamily' },
|
|
275
|
+
rollback: { module: 'update', handler: 'handleUpdateFamily' },
|
|
276
|
+
batch: { module: 'update', handler: 'handleUpdateFamily' },
|
|
277
|
+
cost: { module: 'update', handler: 'handleUpdateFamily' },
|
|
278
|
+
audit: { module: 'update', handler: 'handleUpdateFamily' },
|
|
279
|
+
tasks: { module: 'schedule', handler: 'handleTasks' },
|
|
280
|
+
schedule: { module: 'schedule', handler: 'handleSchedule' },
|
|
281
|
+
workspace: { module: 'workspace', handler: 'handleWorkspace' },
|
|
282
|
+
mcp: { module: 'workspace', handler: 'handleMcp' },
|
|
283
|
+
sync: { module: 'sync', handler: 'handleSync' },
|
|
284
|
+
skill: { module: 'skill', handler: 'handleSkill' },
|
|
285
|
+
web: { module: 'skill', handler: 'handleWeb' },
|
|
286
|
+
sessions: { module: 'skill', handler: 'handleSessions' },
|
|
287
|
+
key: { module: 'key', handler: 'handleKey' },
|
|
288
|
+
desktop: { module: 'desktop', handler: 'handleDesktop' },
|
|
289
|
+
};
|
|
290
|
+
const hit = dispatchTable[opts.prompt[0]];
|
|
282
291
|
if (hit) {
|
|
283
|
-
const mod = await import(`./commands/${hit
|
|
284
|
-
const fn = mod[hit
|
|
292
|
+
const mod = await import(`./commands/${hit.module}.js`);
|
|
293
|
+
const fn = mod[hit.handler];
|
|
285
294
|
const handled = await fn(opts.prompt[0], opts.prompt.slice(1));
|
|
286
295
|
if (handled) return;
|
|
287
296
|
}
|
package/src/commands/skill.js
CHANGED
|
@@ -175,7 +175,13 @@ export async function handleWeb(cmd, args) {
|
|
|
175
175
|
return true;
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
try {
|
|
179
|
+
await runWebServer({ host, port, authToken });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// 质检 C2:listen 现在会 reject(如端口占用),CLI 给出明确提示而非挂起
|
|
182
|
+
console.error(`[MingDao] WebUI 启动失败:${err?.message || err}`);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
}
|
|
179
185
|
return true;
|
|
180
186
|
}
|
|
181
187
|
|
package/src/sync-server.js
CHANGED
|
@@ -106,9 +106,20 @@ function isValidSessionName(n) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
// ---------- 限速(防爆破/枚举;内存表,进程级) ----------
|
|
109
|
+
const TRUST_PROXY = process.env.SYNC_TRUST_PROXY === '1'; // 仅部署在可信反代后时开启
|
|
109
110
|
const rateBuckets = new Map();
|
|
110
|
-
function
|
|
111
|
-
|
|
111
|
+
function clientKey(req) {
|
|
112
|
+
let ip = req.socket?.remoteAddress || 'x';
|
|
113
|
+
if (TRUST_PROXY) {
|
|
114
|
+
const xff = String(req.headers['x-forwarded-for'] || '').split(',')[0].trim();
|
|
115
|
+
if (/^[\d.]+$/.test(xff) || xff.includes(':')) ip = xff;
|
|
116
|
+
}
|
|
117
|
+
return ip;
|
|
118
|
+
}
|
|
119
|
+
function rateLimited(req, limit = 20, extra = '') {
|
|
120
|
+
// 质检 M5:键 = IP + 路由 + 用户名——单账号爆破按用户名限(分布式 IP 无法绕过);
|
|
121
|
+
// 表满按最旧淘汰而非整表 clear(整表 clear 可被攻击者重置全员限额)
|
|
122
|
+
const key = clientKey(req) + '|' + req.url + '|' + String(extra || '');
|
|
112
123
|
const now = Date.now();
|
|
113
124
|
const b = rateBuckets.get(key);
|
|
114
125
|
if (b && now - b.t0 < 60000) {
|
|
@@ -116,7 +127,14 @@ function rateLimited(req, limit = 20) {
|
|
|
116
127
|
if (b.n > limit) return true;
|
|
117
128
|
} else {
|
|
118
129
|
rateBuckets.set(key, { t0: now, n: 1 });
|
|
119
|
-
if (rateBuckets.size >
|
|
130
|
+
if (rateBuckets.size > 10000) {
|
|
131
|
+
let oldestKey = null;
|
|
132
|
+
let oldestT = Infinity;
|
|
133
|
+
for (const [k, v] of rateBuckets) {
|
|
134
|
+
if (v.t0 < oldestT) { oldestT = v.t0; oldestKey = k; }
|
|
135
|
+
}
|
|
136
|
+
if (oldestKey !== null) rateBuckets.delete(oldestKey);
|
|
137
|
+
}
|
|
120
138
|
}
|
|
121
139
|
return false;
|
|
122
140
|
}
|
|
@@ -423,6 +441,7 @@ async function handle(req, res) {
|
|
|
423
441
|
if (rateLimited(req, 10)) return json(res, 429, { error: '尝试过于频繁,请稍后再试' });
|
|
424
442
|
const body = await parseBody(req);
|
|
425
443
|
if (body.__error) return json(res, 400, { error: 'JSON 解析失败' });
|
|
444
|
+
if (rateLimited(req, 5, String(body.username || '').toLowerCase())) return json(res, 429, { error: '该用户名的尝试过于频繁,请稍后再试' });
|
|
426
445
|
if (REGISTRATION === 'closed') return json(res, 403, { error: '注册已关闭(管理员已禁用自助注册)' });
|
|
427
446
|
if (REGISTRATION === 'invite') {
|
|
428
447
|
const code = String(body.inviteCode || '').trim();
|
|
@@ -437,6 +456,7 @@ async function handle(req, res) {
|
|
|
437
456
|
if (rateLimited(req, 10)) return json(res, 429, { error: '尝试过于频繁,请稍后再试' });
|
|
438
457
|
const body = await parseBody(req);
|
|
439
458
|
if (body.__error) return json(res, 400, { error: 'JSON 解析失败' });
|
|
459
|
+
if (rateLimited(req, 5, String(body.username || '').toLowerCase())) return json(res, 429, { error: '该用户名的尝试过于频繁,请稍后再试' });
|
|
440
460
|
const r = doPair(body);
|
|
441
461
|
if (r.notFound) return json(res, 404, r);
|
|
442
462
|
if (r.unauthorized) return json(res, 401, r);
|
|
@@ -549,6 +569,8 @@ export function runSyncServer({ port, host, dataDir, cert, key } = {}) {
|
|
|
549
569
|
server = http.createServer(handler);
|
|
550
570
|
console.log(new Date().toISOString(), '警告:HTTP 明文模式(仅限内网/过渡,公网请配置 SYNC_CERT/SYNC_KEY)');
|
|
551
571
|
}
|
|
572
|
+
// 质检 M5:全局并发连接上限(防连接耗尽)
|
|
573
|
+
if (typeof server.maxConnections === 'number') server.maxConnections = 500;
|
|
552
574
|
server.listen(listenPort, listenHost, () => {
|
|
553
575
|
console.log(
|
|
554
576
|
new Date().toISOString(),
|