mingdao-harness 0.1.70 → 0.2.0
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/package.json +1 -1
- 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 +1008 -0
- package/src/web/index.html +7 -1010
- package/src/web/routes/api.js +832 -0
- package/src/web/server.js +71 -780
package/package.json
CHANGED
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(),
|