mingdao-harness 0.1.69 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mingdao-harness",
3
- "version": "0.1.69",
3
+ "version": "0.2.0",
4
4
  "description": "MingDao Harness —— 开源智能体框架(Agent Harness)。零依赖、开箱即用,针对 DeepSeek-V4 系列优化,开放主流模型接入。",
5
5
  "type": "module",
6
6
  "bin": {
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
- ['update', 'update'], ['rollback', 'update'], ['batch', 'update'], ['cost', 'update'], ['audit', 'update'],
274
- ['tasks', 'schedule'], ['schedule', 'schedule'],
275
- ['workspace', 'workspace'], ['mcp', 'workspace'],
276
- ['sync', 'sync'],
277
- ['skill', 'skill'], ['web', 'skill'], ['sessions', 'skill'],
278
- ['key', 'key'],
279
- ['desktop', 'desktop'],
280
- ];
281
- const hit = dispatchTable.find(([name]) => name === opts.prompt[0]);
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[1]}.js`);
284
- const fn = mod[hit[1] === 'update' ? 'handleUpdateFamily' : hit[1] === 'schedule' ? (opts.prompt[0] === 'tasks' ? 'handleTasks' : 'handleSchedule') : hit[1] === 'workspace' ? (opts.prompt[0] === 'mcp' ? 'handleMcp' : 'handleWorkspace') : hit[1] === 'skill' ? (opts.prompt[0] === 'skill' ? 'handleSkill' : opts.prompt[0] === 'web' ? 'handleWeb' : 'handleSessions') : 'handle' + hit[1][0].toUpperCase() + hit[1].slice(1)];
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
  }
@@ -175,7 +175,13 @@ export async function handleWeb(cmd, args) {
175
175
  return true;
176
176
  }
177
177
  }
178
- await runWebServer({ host, port, authToken });
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
 
@@ -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 rateLimited(req, limit = 20) {
111
- const key = (req.socket?.remoteAddress || 'x') + '|' + req.url;
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 > 5000) rateBuckets.clear();
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(),