ai-helper-agent 1.10.0 → 1.12.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/bin/agent.js CHANGED
@@ -399,6 +399,128 @@ if (command === 'start') {
399
399
  console.log(`[Agent] 工作目录: ${cfg.workdir}`);
400
400
  restartServer();
401
401
 
402
+ // ==================== _restart-helper(内部命令,两阶段重启包装器) ====================
403
+ } else if (command === '_restart-helper') {
404
+ // 由 /api/agent/restart 和 /api/agent/update 调用,detached 运行
405
+ // 职责:等待老进程退出 + 端口释放后,再以 --background 模式启动新进程
406
+ // 解决直接 spawn 新进程时 isRunning 检查劝退、端口 EADDRINUSE 的时序问题
407
+ const helperConfig = applyCliArgs({ port: 18910, host: '127.0.0.1' }, process.argv.slice(3));
408
+ const oldPidIdx = process.argv.indexOf('--old-pid');
409
+ const oldPid = oldPidIdx > -1 ? parseInt(process.argv[oldPidIdx + 1], 10) : 0;
410
+ const scriptIdx = process.argv.indexOf('--script');
411
+ const agentScript = scriptIdx > -1 ? process.argv[scriptIdx + 1] : process.argv[1];
412
+
413
+ const port = helperConfig.port;
414
+ const host = helperConfig.host;
415
+ const workdir = helperConfig.workdir || process.cwd();
416
+
417
+ const log = (msg) => console.log(`[Agent Restart Helper] ${msg}`);
418
+ log(`等待老进程 ${oldPid} 退出...`);
419
+
420
+ // 1. 等待老进程退出(轮询 PID 探活,process.kill(pid,0) 不发信号只检测存在性)
421
+ const waitForOldExit = (pid, timeoutMs = 15000) => new Promise((resolve) => {
422
+ if (!pid) return resolve(true);
423
+ const start = Date.now();
424
+ const timer = setInterval(() => {
425
+ try {
426
+ process.kill(pid, 0);
427
+ } catch {
428
+ clearInterval(timer);
429
+ resolve(true);
430
+ return;
431
+ }
432
+ if (Date.now() - start > timeoutMs) {
433
+ clearInterval(timer);
434
+ log('等待老进程退出超时,尝试强制终止...');
435
+ try { process.kill(pid, 'SIGKILL'); } catch {}
436
+ resolve(false);
437
+ }
438
+ }, 200);
439
+ });
440
+
441
+ // 2. 等待端口释放(fetch /api/status 失败即视为释放)
442
+ const waitForPortRelease = async (host, port, timeoutMs = 10000) => {
443
+ const start = Date.now();
444
+ while (Date.now() - start < timeoutMs) {
445
+ try {
446
+ const resp = await fetch(`http://${host}:${port}/api/status`);
447
+ if (resp.ok) {
448
+ await new Promise(r => setTimeout(r, 300));
449
+ continue;
450
+ }
451
+ } catch {
452
+ return true;
453
+ }
454
+ }
455
+ return false;
456
+ };
457
+
458
+ (async () => {
459
+ await waitForOldExit(oldPid);
460
+ await waitForPortRelease(host, port);
461
+ // 额外等待,确保 socket 完全回收
462
+ await new Promise(r => setTimeout(r, 500));
463
+
464
+ log('老进程已退出,端口已释放,启动新进程...');
465
+
466
+ // 清理旧 PID 文件,避免新进程 isRunning 检查误判
467
+ removePidFile();
468
+ ensureAgentDir();
469
+
470
+ // 直接以前台模式启动服务进程(不经过 --background 中间层)
471
+ // 原因:--background 会多 spawn 一层中间进程,中间进程退出后其 PID 会被
472
+ // 本 helper 覆盖写入 PID 文件,导致 stop/restart 读到已死的中间进程 PID。
473
+ // 前台模式下 child.pid 就是服务进程 PID,与服务进程自身写入的 PID 一致。
474
+ // 前台 start 不会 isRunning 检查,但本 helper 已确保老进程退出 + 端口释放。
475
+ const child = spawn(process.execPath, [
476
+ ...process.execArgv,
477
+ agentScript,
478
+ 'start',
479
+ '--port', String(port),
480
+ '--host', host,
481
+ '--workdir', workdir
482
+ ], {
483
+ detached: true,
484
+ stdio: 'ignore',
485
+ cwd: process.cwd(),
486
+ env: { ...process.env },
487
+ windowsHide: true
488
+ });
489
+
490
+ let spawnFailed = false;
491
+ child.on('error', (err) => {
492
+ spawnFailed = true;
493
+ log(`启动失败: ${err.message}`);
494
+ process.exit(1);
495
+ });
496
+
497
+ await new Promise(resolve => setTimeout(resolve, 1000));
498
+
499
+ // 健康检查:确认服务进程已成功监听端口,避免写入已崩溃进程的 PID
500
+ // (若端口意外未释放导致 EADDRINUSE,服务进程会崩溃,此时不应写错误 PID)
501
+ let healthy = false;
502
+ if (!spawnFailed && child.pid) {
503
+ try {
504
+ const resp = await fetch(`http://${host}:${port}/api/status`);
505
+ healthy = resp.ok;
506
+ } catch {}
507
+ }
508
+
509
+ if (healthy && child.pid) {
510
+ try {
511
+ writeFileSync(PID_FILE, String(child.pid));
512
+ } catch (err) {
513
+ log(`PID 文件写入失败: ${err.message}`);
514
+ }
515
+ child.unref();
516
+ log(`新进程已启动 (PID: ${child.pid})`);
517
+ } else if (!spawnFailed && child.pid) {
518
+ log(`警告:新进程健康检查失败,未更新 PID 文件(可能端口仍被占用或进程已崩溃)`);
519
+ }
520
+
521
+ process.exit(spawnFailed ? 1 : 0);
522
+ })();
523
+
402
524
  // ==================== status ====================
403
525
  } else if (command === 'status') {
404
526
  const config = readAgentConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-helper-agent",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "AI Helper 代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
@@ -21,7 +21,9 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.29.0",
24
+ "archiver": "^8.0.0",
24
25
  "ws": "^8.18.0",
26
+ "xlsx": "^0.18.5",
25
27
  "zod": "^4.4.3"
26
28
  },
27
29
  "engines": {
package/src/auth.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import crypto from 'crypto';
3
3
  import { loadConfig, loadPairings, savePairing } from './config.js';
4
4
 
5
- // 当前有效的配对码(30秒刷新一次)
5
+ // 当前有效的配对码(定时刷新 + 一次性使用)
6
6
  let currentPairCode = null;
7
7
  let pairCodeTimer = null;
8
8
 
@@ -12,11 +12,26 @@ let lastAuthTime = 0;
12
12
  // 判定"活跃"的时间窗口:此时间内有过认证则认为仍有扩展连接
13
13
  const ACTIVE_WINDOW_MS = 5 * 60 * 1000; // 5 分钟
14
14
 
15
+ // 配对码字符表:排除易混淆字符(0/O、1/I/l);6 位字母数字(≈21亿种,抗暴破)
16
+ const PAIR_CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
17
+ const PAIR_CODE_LEN = 6;
18
+
19
+ // 配对失败锁定:连续失败 PAIR_FAIL_LIMIT 次后锁定 PAIR_LOCK_DURATION_MS 毫秒
20
+ let pairFailCount = 0;
21
+ let pairLockUntil = 0;
22
+ const PAIR_FAIL_LIMIT = 5;
23
+ const PAIR_LOCK_DURATION_MS = 60 * 1000; // 60 秒
24
+
15
25
  /**
16
- * 生成随机配对码(4位数字,使用密码学安全的随机数)
26
+ * 生成随机配对码(6位字母数字,使用密码学安全的随机数,排除易混淆字符)
17
27
  */
18
28
  function generatePairCode() {
19
- return String(crypto.randomInt(1000, 10000));
29
+ const bytes = crypto.randomBytes(PAIR_CODE_LEN);
30
+ let code = '';
31
+ for (let i = 0; i < PAIR_CODE_LEN; i++) {
32
+ code += PAIR_CODE_CHARS[bytes[i] % PAIR_CODE_CHARS.length];
33
+ }
34
+ return code;
20
35
  }
21
36
 
22
37
  /**
@@ -39,21 +54,15 @@ function startPairCodeRotation() {
39
54
 
40
55
  // 有配对且最近有活跃连接,无需刷新配对码
41
56
  if (hasPairings && isRecentlyActive) {
42
- console.log(`[Agent] 已有 ${Object.keys(pairings).length} 个配对扩展,最近 ${Math.round((Date.now() - lastAuthTime) / 1000)}s 前有认证活动,跳过配对码刷新`);
43
57
  currentPairCode = generatePairCode();
44
- console.log(`[Agent] 当前配对码(用于新增配对): ${currentPairCode}`);
58
+ console.log(`\n[Agent] 配对码: ${currentPairCode}\n`);
45
59
  return;
46
60
  }
47
61
 
48
- // 有配对但无活跃连接,给出提示
49
- if (hasPairings && !isRecentlyActive) {
50
- console.log(`[Agent] 存在配对记录但无活跃连接(超过 ${ACTIVE_WINDOW_MS / 60000} 分钟),恢复配对码刷新`);
51
- }
52
-
53
62
  const ttl = (config.pairCodeTTL || 30) * 1000;
54
63
 
55
64
  currentPairCode = generatePairCode();
56
- console.log(`[Agent] 当前配对码: ${currentPairCode}`);
65
+ console.log(`\n[Agent] 配对码: ${currentPairCode}\n`);
57
66
 
58
67
  if (pairCodeTimer) clearInterval(pairCodeTimer);
59
68
  pairCodeTimer = setInterval(() => {
@@ -89,17 +98,38 @@ function verifyToken(token) {
89
98
 
90
99
  /**
91
100
  * 处理配对请求(异步,支持写入锁)
101
+ * 安全策略:
102
+ * - 失败锁定:连续失败 PAIR_FAIL_LIMIT 次后临时锁定,并立即轮换配对码使已观测码失效
103
+ * - 一次性使用:配对成功后立即轮换配对码,防止配对码被重复利用
92
104
  */
93
105
  async function handlePairRequest(code, extensionId) {
106
+ // 锁定期内直接拒绝(不泄露是否处于锁定状态细节)
107
+ if (Date.now() < pairLockUntil) {
108
+ return { success: false, error: '配对尝试过于频繁,请稍后再试' };
109
+ }
94
110
  if (!code || !extensionId) {
95
111
  return { success: false, error: '缺少配对码或扩展ID' };
96
112
  }
97
- if (code !== currentPairCode) {
113
+ if (code.toUpperCase() !== currentPairCode) {
114
+ pairFailCount++;
115
+ if (pairFailCount >= PAIR_FAIL_LIMIT) {
116
+ pairLockUntil = Date.now() + PAIR_LOCK_DURATION_MS;
117
+ pairFailCount = 0;
118
+ // 锁定时立即轮换配对码,使攻击者已观测到的配对码失效
119
+ currentPairCode = generatePairCode();
120
+ console.log('[Agent] 配对失败次数过多,已临时锁定 60s 并轮换配对码');
121
+ }
98
122
  return { success: false, error: '配对码无效' };
99
123
  }
124
+ // 配对码正确:重置失败计数
125
+ pairFailCount = 0;
126
+
100
127
  // 检查是否已有配对
101
128
  const pairings = loadPairings();
102
129
  if (pairings[extensionId]) {
130
+ // 一次性使用:成功后立即轮换配对码
131
+ currentPairCode = generatePairCode();
132
+ console.log('[Agent] 配对码校验通过,配对码已轮换(一次性使用)');
103
133
  return { success: true, token: pairings[extensionId].token, message: '已配对,返回现有 token' };
104
134
  }
105
135
  // 生成新 token 并保存
@@ -109,6 +139,9 @@ async function handlePairRequest(code, extensionId) {
109
139
  } catch (err) {
110
140
  return { success: false, error: `配对保存失败: ${err.message}` };
111
141
  }
142
+ // 一次性使用:成功后立即轮换配对码
143
+ currentPairCode = generatePairCode();
144
+ console.log('[Agent] 配对成功,配对码已轮换(一次性使用)');
112
145
  return { success: true, token, message: '配对成功' };
113
146
  }
114
147
 
package/src/executor.js CHANGED
@@ -54,8 +54,25 @@ function getShellForExec() {
54
54
  // 运行中的进程映射:execId → { process, wsClients: Set, timeoutId, forceKillId, stdoutBuf, stderrBuf }
55
55
  const runningProcesses = new Map();
56
56
 
57
- // 已完成进程缓存(解决竞态条件:进程完成太快,WebSocket 客户端来不及连接)
58
- // execId → { stdoutBuf, stderrBuf, exitCode, killed, cleanupTimeout }
57
+ // 输出缓冲区上限(5MB),防止长时间命令内存无限增长
58
+ const MAX_OUTPUT_BUFFER = 5 * 1024 * 1024;
59
+
60
+ /**
61
+ * 安全追加字符串到缓冲区,超过上限时截断并标记
62
+ * @param {string} buf - 当前缓冲区
63
+ * @param {string} chunk - 要追加的字符串
64
+ * @param {object} truncState - 截断状态对象 { truncated: boolean }
65
+ * @returns {string} 更新后的缓冲区
66
+ */
67
+ function appendWithLimit(buf, chunk, truncState) {
68
+ if (truncState.truncated) return buf;
69
+ if (buf.length + chunk.length > MAX_OUTPUT_BUFFER) {
70
+ truncState.truncated = true;
71
+ const remaining = MAX_OUTPUT_BUFFER - buf.length;
72
+ return buf + (remaining > 0 ? chunk.slice(0, remaining) : '');
73
+ }
74
+ return buf + chunk;
75
+ }
59
76
  const completedProcesses = new Map();
60
77
  const COMPLETED_CACHE_TTL = 30000; // 30秒后清理
61
78
 
@@ -120,6 +137,7 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
120
137
  cwd: workdir,
121
138
  env: buildSafeEnv(),
122
139
  stdio: ['ignore', 'pipe', 'pipe'],
140
+ detached: true, // 独立进程组,防止 kill 父进程后子进程成为孤儿
123
141
  windowsHide: true
124
142
  });
125
143
 
@@ -133,7 +151,8 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
133
151
 
134
152
  // 存储到 entry 中供 clearTimers 读取
135
153
  // stdoutBuf/stderrBuf 始终收集,用于回放给延迟连接的 WS 客户端
136
- const entry = { process: proc, wsClients, stdoutBuf: '', stderrBuf: '' };
154
+ // command/startTime 用于状态详情面板展示运行中进程
155
+ const entry = { process: proc, wsClients, stdoutBuf: '', stderrBuf: '', truncStdout: false, truncStderr: false, command, startTime: Date.now() };
137
156
  runningProcesses.set(execId, entry);
138
157
 
139
158
  // 超时控制
@@ -166,7 +185,12 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
166
185
  proc.stdout.on('data', (chunk) => {
167
186
  const str = chunk.toString();
168
187
  if (collectOutput) stdoutCollected += str;
169
- entry.stdoutBuf += str;
188
+ const truncState = { truncated: entry.truncStdout };
189
+ entry.stdoutBuf = appendWithLimit(entry.stdoutBuf, str, truncState);
190
+ entry.truncStdout = truncState.truncated;
191
+ if (truncState.truncated) {
192
+ broadcast({ type: 'stdout_truncated', message: 'stdout 输出超过 5MB 上限,已截断', execId });
193
+ }
170
194
  broadcast({ type: 'stdout', data: str, execId });
171
195
  });
172
196
 
@@ -174,7 +198,12 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
174
198
  proc.stderr.on('data', (chunk) => {
175
199
  const str = chunk.toString();
176
200
  if (collectOutput) stderrCollected += str;
177
- entry.stderrBuf += str;
201
+ const truncState = { truncated: entry.truncStderr };
202
+ entry.stderrBuf = appendWithLimit(entry.stderrBuf, str, truncState);
203
+ entry.truncStderr = truncState.truncated;
204
+ if (truncState.truncated) {
205
+ broadcast({ type: 'stderr_truncated', message: 'stderr 输出超过 5MB 上限,已截断', execId });
206
+ }
178
207
  broadcast({ type: 'stderr', data: str, execId });
179
208
  });
180
209
 
@@ -326,9 +355,9 @@ function disconnectWsClient(execId, wsClient) {
326
355
  }
327
356
 
328
357
  /**
329
- * 停止正在运行的命令(跨平台兼容)
330
- * - macOS/Linux: SIGTERM(优雅终止)→ 5s → SIGKILL(强制杀)
331
- * - Windows: taskkill /PID(优雅终止)→ 5s → taskkill /F /PID(强制杀)
358
+ * 停止正在运行的命令(跨平台兼容,递归终止全部子进程)
359
+ * - macOS/Linux: SIGTERM to process group(-pid)→ 5s → SIGKILL to process group
360
+ * - Windows: taskkill /T /PID(递归终止进程树)→ 5s → taskkill /F /T /PID
332
361
  */
333
362
  function killProcess(execId) {
334
363
  const entry = runningProcesses.get(execId);
@@ -340,18 +369,20 @@ function killProcess(execId) {
340
369
  const pid = entry.process.pid;
341
370
 
342
371
  if (isWin) {
343
- // Windows: taskkill 支持优雅终止(/PID 发送 WM_CLOSE)和强制终止(/F)
344
- exec(`taskkill /PID ${pid}`, (err) => {
372
+ // Windows: /T 递归终止整个进程树
373
+ exec(`taskkill /T /PID ${pid}`, (err) => {
345
374
  if (err) {
346
- // 尝试强制杀
347
- exec(`taskkill /F /PID ${pid}`, () => {});
375
+ exec(`taskkill /F /T /PID ${pid}`, () => {});
348
376
  }
349
377
  });
350
378
  } else {
351
- // macOS/Linux: SIGTERM 优雅终止
379
+ // Unix: 负 PID 表示杀整个进程组(detached + 未调用 setsid 时有效)
352
380
  try {
353
- entry.process.kill('SIGTERM');
354
- } catch {}
381
+ process.kill(-pid, 'SIGTERM');
382
+ } catch {
383
+ // 回退到单进程
384
+ try { entry.process.kill('SIGTERM'); } catch {}
385
+ }
355
386
  }
356
387
 
357
388
  // 清理旧的定时器
@@ -363,14 +394,15 @@ function killProcess(execId) {
363
394
  clearTimeout(entry.forceKillId);
364
395
  }
365
396
 
366
- // 5秒后强制杀(绑定到 entry 对象防止闭包引用旧变量)
397
+ // 5秒后强制杀整个进程组
367
398
  entry.forceKillId = setTimeout(() => {
368
399
  try {
369
400
  if (entry.process.exitCode === null) {
370
401
  if (isWin) {
371
- exec(`taskkill /F /PID ${pid}`, () => {});
402
+ exec(`taskkill /F /T /PID ${pid}`, () => {});
372
403
  } else {
373
- entry.process.kill('SIGKILL');
404
+ try { process.kill(-pid, 'SIGKILL'); }
405
+ catch { try { entry.process.kill('SIGKILL'); } catch {} }
374
406
  }
375
407
  }
376
408
  } catch {}
@@ -382,11 +414,19 @@ function killProcess(execId) {
382
414
 
383
415
  /**
384
416
  * 获取运行中的进程列表
417
+ * 返回每个进程的 execId、pid、command、startTime、duration(已运行毫秒数)
385
418
  */
386
419
  function getRunningProcesses() {
420
+ const now = Date.now();
387
421
  const list = [];
388
- for (const [execId] of runningProcesses) {
389
- list.push({ execId });
422
+ for (const [execId, entry] of runningProcesses) {
423
+ list.push({
424
+ execId,
425
+ pid: entry.process && entry.process.pid ? entry.process.pid : null,
426
+ command: entry.command || '',
427
+ startTime: entry.startTime || null,
428
+ duration: entry.startTime ? now - entry.startTime : 0
429
+ });
390
430
  }
391
431
  return list;
392
432
  }
package/src/mcp/client.js CHANGED
@@ -33,7 +33,6 @@ export class McpClient {
33
33
  case TRANSPORT_SSE: {
34
34
  const url = this.serverConfig.url;
35
35
  if (!url) throw new Error('SSE 传输需要提供 url');
36
- console.log(`[MCP:${this.serverId}] 使用 SSE 传输,URL: ${url}`);
37
36
  const opts = {};
38
37
  if (Object.keys(headers).length > 0) {
39
38
  opts.requestInit = { headers };
@@ -44,7 +43,6 @@ export class McpClient {
44
43
  case TRANSPORT_STREAMABLE_HTTP: {
45
44
  const url = this.serverConfig.url;
46
45
  if (!url) throw new Error('StreamableHTTP 传输需要提供 url');
47
- console.log(`[MCP:${this.serverId}] 使用 StreamableHTTP 传输,URL: ${url}`);
48
46
  const opts = {};
49
47
  if (Object.keys(headers).length > 0) {
50
48
  opts.requestInit = { headers };
@@ -55,7 +53,6 @@ export class McpClient {
55
53
  case TRANSPORT_WEBSOCKET: {
56
54
  const url = this.serverConfig.url;
57
55
  if (!url) throw new Error('WebSocket 传输需要提供 url');
58
- console.log(`[MCP:${this.serverId}] 使用 WebSocket 传输,URL: ${url}`);
59
56
  return new WebSocketClientTransport(new URL(url));
60
57
  }
61
58
 
@@ -63,7 +60,6 @@ export class McpClient {
63
60
  default: {
64
61
  const command = this.serverConfig.command;
65
62
  if (!command) throw new Error('stdio 传输需要提供 command');
66
- console.log(`[MCP:${this.serverId}] 使用 stdio 传输,命令: ${command}`);
67
63
  return new StdioClientTransport({
68
64
  command: this.serverConfig.command,
69
65
  args: this.serverConfig.args || [],
@@ -92,12 +88,10 @@ export class McpClient {
92
88
  serverInfo: this.client.getServerVersion(),
93
89
  capabilities: this.client.getServerCapabilities()
94
90
  };
95
- console.log(`[MCP:${this.serverId}] Initialize 成功, server:`, this.serverInfo.serverInfo?.name);
96
91
 
97
92
  const toolsResult = await this.client.listTools({});
98
93
  if (toolsResult && toolsResult.tools) {
99
94
  this.tools = toolsResult.tools;
100
- console.log(`[MCP:${this.serverId}] 发现 ${this.tools.length} 个工具`);
101
95
  } else {
102
96
  this.tools = [];
103
97
  }
@@ -105,7 +99,6 @@ export class McpClient {
105
99
  this.connected = true;
106
100
  return { success: true, toolCount: this.tools.length };
107
101
  } catch (err) {
108
- console.error(`[MCP:${this.serverId}] 连接失败:`, err.message);
109
102
  this.disconnect();
110
103
  return { success: false, error: err.message };
111
104
  }
@@ -155,8 +148,6 @@ export class McpClient {
155
148
  try { await this.transport.close(); } catch {}
156
149
  this.transport = null;
157
150
  }
158
-
159
- console.log(`[MCP:${this.serverId}] 已断开连接`);
160
151
  }
161
152
 
162
153
  getStatus() {
@@ -14,14 +14,10 @@ export async function initializeMcpRegistry() {
14
14
  const config = loadMcpConfig();
15
15
  let connected = 0;
16
16
  let failed = 0;
17
-
18
- console.log(`[MCP Registry] 初始化,共配置 ${config.servers.length} 个 MCP Server`);
17
+ const failedServers = [];
19
18
 
20
19
  for (const server of config.servers) {
21
- if (!server.enabled) {
22
- console.log(`[MCP Registry] 跳过禁用的 Server: ${server.id}`);
23
- continue;
24
- }
20
+ if (!server.enabled) continue;
25
21
 
26
22
  const client = new McpClient(server);
27
23
  const result = await client.connect();
@@ -29,12 +25,19 @@ export async function initializeMcpRegistry() {
29
25
  clients.set(server.id, client);
30
26
  connected++;
31
27
  } else {
32
- console.error(`[MCP Registry] 连接失败 "${server.id}":`, result.error);
33
28
  failed++;
29
+ failedServers.push({ id: server.id, error: result.error });
34
30
  }
35
31
  }
36
32
 
37
- console.log(`[MCP Registry] 初始化完成: ${connected} 已连接, ${failed} 失败`);
33
+ if (config.servers.length > 0) {
34
+ if (failed > 0) {
35
+ const failedList = failedServers.map(fs => `${fs.id}(${fs.error})`).join(', ');
36
+ console.warn(`[MCP] ${connected}/${config.servers.length} 已连接,${failed} 个失败: ${failedList}`);
37
+ } else if (connected > 0) {
38
+ console.log(`[MCP] ${connected} 个 Server 已连接`);
39
+ }
40
+ }
38
41
  return { connected, failed };
39
42
  }
40
43
 
@@ -154,7 +157,6 @@ export async function disconnectMcpServer(serverId) {
154
157
  * 关闭所有 MCP 连接(Agent 关闭时调用)
155
158
  */
156
159
  export async function shutdownMcpRegistry() {
157
- console.log('[MCP Registry] 关闭所有 MCP 连接...');
158
160
  for (const [id, client] of clients) {
159
161
  try { await client.disconnect(); } catch {}
160
162
  }
package/src/search.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // agent/src/search.js - 文件搜索(fd/rg 优先 + Node.js 回退)
2
2
  import { spawn } from 'child_process';
3
3
  import { readdir, stat, readFile } from 'fs/promises';
4
+ import { statSync } from 'fs';
4
5
  import { join, basename, extname, resolve } from 'path';
5
6
  import { checkPath } from './security.js';
6
7
 
@@ -43,8 +44,6 @@ export async function initSearchTools() {
43
44
  const [fd, rg] = await Promise.all([checkFdAvailable(), checkRgAvailable()]);
44
45
  fdAvailable = fd;
45
46
  rgAvailable = rg;
46
- console.log(`[Agent] fd (文件搜索): ${fd ? '✅ 可用' : '❌ 未安装,使用 Node.js 回退'}`);
47
- console.log(`[Agent] rg (内容搜索): ${rg ? '✅ 可用' : '❌ 未安装,使用 Node.js 回退'}`);
48
47
  return { fd: fdAvailable, rg: rgAvailable };
49
48
  })();
50
49
  return searchToolsInitPromise;
@@ -81,11 +80,15 @@ function matchGlob(filename, pattern) {
81
80
  }
82
81
 
83
82
  /**
84
- * 使用 fd 搜索文件(快速)
83
+ * 使用 fd 搜索文件和目录(快速)
85
84
  */
86
85
  function searchWithFd(rootPath, filePattern, recursive, maxResults) {
87
86
  return new Promise((resolve) => {
88
- const args = ['--glob', filePattern, '--type', 'f', '--max-results', String(maxResults)];
87
+ const args = ['--glob', filePattern, '--type', 'f', '--type', 'd', '--max-results', String(maxResults)];
88
+ // 排除默认忽略的目录
89
+ for (const dir of DEFAULT_IGNORE_DIRS) {
90
+ args.push('--exclude', dir);
91
+ }
89
92
  if (!recursive) args.push('--max-depth', '1');
90
93
  args.push(rootPath);
91
94
 
@@ -109,11 +112,12 @@ function searchWithFd(rootPath, filePattern, recursive, maxResults) {
109
112
  const output = Buffer.concat(stdout).toString('utf-8').trim();
110
113
  const lines = output ? output.split('\n').filter(Boolean) : [];
111
114
  const results = lines.map(p => {
115
+ const absPath = join(rootPath, p);
112
116
  try {
113
- const s = statSync(p);
114
- return { path: p, name: basename(p), size: s.size, mtime: s.mtimeMs };
117
+ const s = statSync(absPath);
118
+ return { path: absPath, name: basename(p), type: s.isDirectory() ? 'directory' : 'file', size: s.size, mtime: s.mtimeMs };
115
119
  } catch {
116
- return { path: p, name: basename(p), size: 0, mtime: 0 };
120
+ return { path: absPath, name: basename(p), type: 'file', size: 0, mtime: 0 };
117
121
  }
118
122
  });
119
123
  resolve(results);
@@ -131,7 +135,7 @@ function yieldEventLoop() {
131
135
  }
132
136
 
133
137
  /**
134
- * Node.js 原生递归搜索文件(回退方案,异步 + 分片防止阻塞事件循环)
138
+ * Node.js 原生递归搜索文件和目录(回退方案,异步 + 分片防止阻塞事件循环)
135
139
  */
136
140
  async function searchFilesNative(rootPath, filePattern, recursive, maxResults) {
137
141
  const results = [];
@@ -154,14 +158,23 @@ async function searchFilesNative(rootPath, filePattern, recursive, maxResults) {
154
158
  if (entry.isDirectory()) {
155
159
  if (DEFAULT_IGNORE_DIRS.has(entry.name)) continue;
156
160
  if (entry.name.startsWith('.')) continue;
161
+ // 目录也参与搜索匹配
162
+ if (matchGlob(entry.name, filePattern)) {
163
+ try {
164
+ const s = await stat(fullPath);
165
+ results.push({ path: fullPath, name: entry.name, type: 'directory', size: s.size, mtime: s.mtimeMs });
166
+ } catch {
167
+ results.push({ path: fullPath, name: entry.name, type: 'directory', size: 0, mtime: 0 });
168
+ }
169
+ }
157
170
  await walk(fullPath, depth + 1);
158
171
  } else if (entry.isFile()) {
159
172
  if (matchGlob(entry.name, filePattern)) {
160
173
  try {
161
174
  const s = await stat(fullPath);
162
- results.push({ path: fullPath, name: entry.name, size: s.size, mtime: s.mtimeMs });
175
+ results.push({ path: fullPath, name: entry.name, type: 'file', size: s.size, mtime: s.mtimeMs });
163
176
  } catch {
164
- results.push({ path: fullPath, name: entry.name, size: 0, mtime: 0 });
177
+ results.push({ path: fullPath, name: entry.name, type: 'file', size: 0, mtime: 0 });
165
178
  }
166
179
  }
167
180
  }
package/src/security.js CHANGED
@@ -28,6 +28,9 @@ const HARD_BLOCKED_PATHS = [
28
28
  join(AGENT_DIR, 'pairings.json'), // 配对记录
29
29
  join(AGENT_DIR, 'mcp_servers.json'),// MCP 服务器配置
30
30
  join(AGENT_DIR, 'logs') + sep, // 日志目录
31
+ join(AGENT_DIR, '.trash') + sep, // 回收站目录(通过 /api/trash 端点管理)
32
+ join(AGENT_DIR, 'builtin_skills_state.json'), // 内置技能状态
33
+ join(AGENT_DIR, 'disabled_skills.json'), // 停用技能状态
31
34
  ];
32
35
 
33
36
  /**