ai-helper-agent 1.9.1 → 1.9.3

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,12 +1,12 @@
1
1
  {
2
2
  "name": "ai-helper-agent",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "AI Helper 代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
7
7
  "bin": {
8
- "ai-helper-agent": "./bin/agent.js",
9
- "aha": "./bin/agent.js"
8
+ "ai-helper-agent": "bin/agent.js",
9
+ "aha": "bin/agent.js"
10
10
  },
11
11
  "files": [
12
12
  "bin/",
package/src/executor.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // agent/src/executor.js - 命令执行器(child_process + 流式输出)
2
- import { spawn } from 'child_process';
2
+ import { spawn, exec } from 'child_process';
3
3
  import crypto from 'crypto';
4
4
  import os from 'os';
5
5
  import { loadConfig } from './config.js';
@@ -252,7 +252,33 @@ function addWsClient(execId, wsClient) {
252
252
  }
253
253
 
254
254
  /**
255
- * 停止正在运行的命令
255
+ * 断开 WebSocket 客户端连接
256
+ * 当所有客户端都断开后,取消超时定时器,让进程自由运行(不再自动杀)
257
+ */
258
+ function disconnectWsClient(execId, wsClient) {
259
+ const entry = runningProcesses.get(execId);
260
+ if (!entry) return false;
261
+
262
+ entry.wsClients.delete(wsClient);
263
+
264
+ // 所有客户端都断开了 → 取消超时,进程继续运行
265
+ if (entry.wsClients.size === 0) {
266
+ if (entry.timeoutId) {
267
+ clearTimeout(entry.timeoutId);
268
+ entry.timeoutId = null;
269
+ }
270
+ if (entry.forceKillId) {
271
+ clearTimeout(entry.forceKillId);
272
+ entry.forceKillId = null;
273
+ }
274
+ }
275
+ return true;
276
+ }
277
+
278
+ /**
279
+ * 停止正在运行的命令(跨平台兼容)
280
+ * - macOS/Linux: SIGTERM(优雅终止)→ 5s → SIGKILL(强制杀)
281
+ * - Windows: taskkill /PID(优雅终止)→ 5s → taskkill /F /PID(强制杀)
256
282
  */
257
283
  function killProcess(execId) {
258
284
  const entry = runningProcesses.get(execId);
@@ -260,11 +286,25 @@ function killProcess(execId) {
260
286
 
261
287
  if (entry.process.exitCode !== null) return false; // 已退出
262
288
 
263
- try {
264
- entry.process.kill('SIGTERM');
265
- } catch {}
289
+ const isWin = os.platform() === 'win32';
290
+ const pid = entry.process.pid;
291
+
292
+ if (isWin) {
293
+ // Windows: taskkill 支持优雅终止(/PID 发送 WM_CLOSE)和强制终止(/F)
294
+ exec(`taskkill /PID ${pid}`, (err) => {
295
+ if (err) {
296
+ // 尝试强制杀
297
+ exec(`taskkill /F /PID ${pid}`, () => {});
298
+ }
299
+ });
300
+ } else {
301
+ // macOS/Linux: SIGTERM 优雅终止
302
+ try {
303
+ entry.process.kill('SIGTERM');
304
+ } catch {}
305
+ }
266
306
 
267
- // 5秒后强制 kill(绑定到 entry 对象防止闭包引用旧变量)
307
+ // 清理旧的定时器
268
308
  if (entry.timeoutId) {
269
309
  clearTimeout(entry.timeoutId);
270
310
  entry.timeoutId = null;
@@ -272,10 +312,16 @@ function killProcess(execId) {
272
312
  if (entry.forceKillId) {
273
313
  clearTimeout(entry.forceKillId);
274
314
  }
315
+
316
+ // 5秒后强制杀(绑定到 entry 对象防止闭包引用旧变量)
275
317
  entry.forceKillId = setTimeout(() => {
276
318
  try {
277
319
  if (entry.process.exitCode === null) {
278
- entry.process.kill('SIGKILL');
320
+ if (isWin) {
321
+ exec(`taskkill /F /PID ${pid}`, () => {});
322
+ } else {
323
+ entry.process.kill('SIGKILL');
324
+ }
279
325
  }
280
326
  } catch {}
281
327
  entry.forceKillId = null;
@@ -295,4 +341,4 @@ function getRunningProcesses() {
295
341
  return list;
296
342
  }
297
343
 
298
- export { executeCommand, executeCommandSync, addWsClient, killProcess, getRunningProcesses };
344
+ export { executeCommand, executeCommandSync, addWsClient, disconnectWsClient, killProcess, getRunningProcesses };
package/src/mcp/client.js CHANGED
@@ -143,15 +143,20 @@ export class McpClient {
143
143
  }
144
144
  }
145
145
 
146
- disconnect() {
146
+ async disconnect() {
147
147
  this.connected = false;
148
148
  this.tools = [];
149
149
 
150
150
  if (this.client) {
151
- try { this.client.close(); } catch {}
151
+ try { await this.client.close(); } catch {}
152
152
  this.client = null;
153
153
  }
154
154
 
155
+ if (this.transport) {
156
+ try { await this.transport.close(); } catch {}
157
+ this.transport = null;
158
+ }
159
+
155
160
  console.log(`[MCP:${this.serverId}] 已断开连接`);
156
161
  }
157
162
 
@@ -124,7 +124,7 @@ export async function connectMcpServer(serverId) {
124
124
  // 先断开已有连接
125
125
  const existingClient = clients.get(serverId);
126
126
  if (existingClient) {
127
- existingClient.disconnect();
127
+ await existingClient.disconnect();
128
128
  clients.delete(serverId);
129
129
  }
130
130
 
@@ -140,10 +140,10 @@ export async function connectMcpServer(serverId) {
140
140
  /**
141
141
  * 断开指定 MCP Server
142
142
  */
143
- export function disconnectMcpServer(serverId) {
143
+ export async function disconnectMcpServer(serverId) {
144
144
  const client = clients.get(serverId);
145
145
  if (client) {
146
- client.disconnect();
146
+ await client.disconnect();
147
147
  clients.delete(serverId);
148
148
  return { success: true };
149
149
  }
@@ -153,10 +153,10 @@ export function disconnectMcpServer(serverId) {
153
153
  /**
154
154
  * 关闭所有 MCP 连接(Agent 关闭时调用)
155
155
  */
156
- export function shutdownMcpRegistry() {
156
+ export async function shutdownMcpRegistry() {
157
157
  console.log('[MCP Registry] 关闭所有 MCP 连接...');
158
158
  for (const [id, client] of clients) {
159
- try { client.disconnect(); } catch {}
159
+ try { await client.disconnect(); } catch {}
160
160
  }
161
161
  clients.clear();
162
162
  }
package/src/search.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // agent/src/search.js - 文件搜索(fd/rg 优先 + Node.js 回退)
2
2
  import { spawn } from 'child_process';
3
- import { readdirSync, statSync, readFileSync, existsSync } from 'fs';
3
+ import { readdir, stat, readFile } from 'fs/promises';
4
4
  import { join, basename, extname, resolve } from 'path';
5
5
  import { checkPath } from './security.js';
6
6
 
@@ -124,19 +124,29 @@ function searchWithFd(rootPath, filePattern, recursive, maxResults) {
124
124
  }
125
125
 
126
126
  /**
127
- * Node.js 原生递归搜索文件(回退方案)
127
+ * 让出事件循环,防止长时间同步操作阻塞其他请求
128
128
  */
129
- function searchFilesNative(rootPath, filePattern, recursive, maxResults) {
129
+ function yieldEventLoop() {
130
+ return new Promise(resolve => setImmediate(resolve));
131
+ }
132
+
133
+ /**
134
+ * Node.js 原生递归搜索文件(回退方案,异步 + 分片防止阻塞事件循环)
135
+ */
136
+ async function searchFilesNative(rootPath, filePattern, recursive, maxResults) {
130
137
  const results = [];
131
138
 
132
- function walk(dir, depth) {
139
+ async function walk(dir, depth) {
133
140
  if (results.length >= maxResults) return;
134
141
  if (!recursive && depth > 1) return;
135
142
 
136
143
  let entries;
137
- try { entries = readdirSync(dir, { withFileTypes: true }); }
144
+ try { entries = await readdir(dir, { withFileTypes: true }); }
138
145
  catch { return; }
139
146
 
147
+ // 每处理一个目录后让出事件循环
148
+ await yieldEventLoop();
149
+
140
150
  for (const entry of entries) {
141
151
  if (results.length >= maxResults) return;
142
152
  const fullPath = join(dir, entry.name);
@@ -144,11 +154,11 @@ function searchFilesNative(rootPath, filePattern, recursive, maxResults) {
144
154
  if (entry.isDirectory()) {
145
155
  if (DEFAULT_IGNORE_DIRS.has(entry.name)) continue;
146
156
  if (entry.name.startsWith('.')) continue;
147
- walk(fullPath, depth + 1);
157
+ await walk(fullPath, depth + 1);
148
158
  } else if (entry.isFile()) {
149
159
  if (matchGlob(entry.name, filePattern)) {
150
160
  try {
151
- const s = statSync(fullPath);
161
+ const s = await stat(fullPath);
152
162
  results.push({ path: fullPath, name: entry.name, size: s.size, mtime: s.mtimeMs });
153
163
  } catch {
154
164
  results.push({ path: fullPath, name: entry.name, size: 0, mtime: 0 });
@@ -158,7 +168,7 @@ function searchFilesNative(rootPath, filePattern, recursive, maxResults) {
158
168
  }
159
169
  }
160
170
 
161
- walk(rootPath, 0);
171
+ await walk(rootPath, 0);
162
172
  return results;
163
173
  }
164
174
 
@@ -171,17 +181,19 @@ function searchFilesNative(rootPath, filePattern, recursive, maxResults) {
171
181
  * @returns {Array<{path, name, size, mtime}>}
172
182
  */
173
183
  export async function searchFiles(rootPath, filePattern = '*', recursive = true, maxResults = 200) {
174
- const pathCheck = checkPath(rootPath);
184
+ const pathCheck = await checkPath(rootPath);
175
185
  if (!pathCheck.allowed) {
176
186
  return { success: false, error: pathCheck.reason };
177
187
  }
178
188
 
179
189
  const resolved = pathCheck.resolved;
180
- if (!existsSync(resolved)) {
190
+ try {
191
+ await stat(resolved);
192
+ } catch {
181
193
  return { success: false, error: '搜索路径不存在' };
182
194
  }
183
195
 
184
- const s = statSync(resolved);
196
+ const s = await stat(resolved);
185
197
  if (!s.isDirectory()) {
186
198
  return { success: false, error: '搜索路径不是目录' };
187
199
  }
@@ -190,7 +202,7 @@ export async function searchFiles(rootPath, filePattern = '*', recursive = true,
190
202
  if (fdAvailable) {
191
203
  results = await searchWithFd(resolved, filePattern, recursive, maxResults);
192
204
  } else {
193
- results = searchFilesNative(resolved, filePattern, recursive, maxResults);
205
+ results = await searchFilesNative(resolved, filePattern, recursive, maxResults);
194
206
  }
195
207
 
196
208
  return {
@@ -271,19 +283,22 @@ function parseRgOutput(output, contextLines) {
271
283
  }
272
284
 
273
285
  /**
274
- * Node.js 原生搜索文件内容(回退方案)
286
+ * Node.js 原生搜索文件内容(回退方案,异步 + 分片防止阻塞事件循环)
275
287
  */
276
- function searchContentNative(rootPath, pattern, filePattern, caseSensitive, maxResults, contextLines) {
288
+ async function searchContentNative(rootPath, pattern, filePattern, caseSensitive, maxResults, contextLines) {
277
289
  const results = [];
278
290
  const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
279
291
 
280
- function walk(dir) {
292
+ async function walk(dir) {
281
293
  if (results.length >= maxResults) return;
282
294
 
283
295
  let entries;
284
- try { entries = readdirSync(dir, { withFileTypes: true }); }
296
+ try { entries = await readdir(dir, { withFileTypes: true }); }
285
297
  catch { return; }
286
298
 
299
+ // 每处理一个目录后让出事件循环
300
+ await yieldEventLoop();
301
+
287
302
  for (const entry of entries) {
288
303
  if (results.length >= maxResults) return;
289
304
  const fullPath = join(dir, entry.name);
@@ -291,7 +306,7 @@ function searchContentNative(rootPath, pattern, filePattern, caseSensitive, maxR
291
306
  if (entry.isDirectory()) {
292
307
  if (DEFAULT_IGNORE_DIRS.has(entry.name)) continue;
293
308
  if (entry.name.startsWith('.')) continue;
294
- walk(fullPath);
309
+ await walk(fullPath);
295
310
  } else if (entry.isFile()) {
296
311
  if (filePattern && !matchGlob(entry.name, filePattern)) continue;
297
312
  // 跳过压缩和映射文件
@@ -299,10 +314,10 @@ function searchContentNative(rootPath, pattern, filePattern, caseSensitive, maxR
299
314
  if (entry.name.endsWith('.map')) continue;
300
315
 
301
316
  try {
302
- const s = statSync(fullPath);
317
+ const s = await stat(fullPath);
303
318
  if (s.size > 1024 * 1024) continue; // 跳过 1MB 以上的文件
304
319
 
305
- const content = readFileSync(fullPath, 'utf-8');
320
+ const content = await readFile(fullPath, 'utf-8');
306
321
  const fileLines = content.split('\n');
307
322
 
308
323
  for (let i = 0; i < fileLines.length; i++) {
@@ -328,7 +343,7 @@ function searchContentNative(rootPath, pattern, filePattern, caseSensitive, maxR
328
343
  }
329
344
  }
330
345
 
331
- walk(rootPath);
346
+ await walk(rootPath);
332
347
  return results;
333
348
  }
334
349
 
@@ -343,13 +358,15 @@ function searchContentNative(rootPath, pattern, filePattern, caseSensitive, maxR
343
358
  * @returns {{ success: boolean, results: Array, total: number, engine: string }}
344
359
  */
345
360
  export async function searchContent(rootPath, pattern, filePattern = null, caseSensitive = false, maxResults = 100, contextLines = 2) {
346
- const pathCheck = checkPath(rootPath);
361
+ const pathCheck = await checkPath(rootPath);
347
362
  if (!pathCheck.allowed) {
348
363
  return { success: false, error: pathCheck.reason };
349
364
  }
350
365
 
351
366
  const resolved = pathCheck.resolved;
352
- if (!existsSync(resolved)) {
367
+ try {
368
+ await stat(resolved);
369
+ } catch {
353
370
  return { success: false, error: '搜索路径不存在' };
354
371
  }
355
372
 
@@ -361,7 +378,7 @@ export async function searchContent(rootPath, pattern, filePattern = null, caseS
361
378
  if (rgAvailable) {
362
379
  results = await searchContentWithRg(resolved, pattern, filePattern, maxResults, contextLines);
363
380
  } else {
364
- results = searchContentNative(resolved, pattern, filePattern, caseSensitive, maxResults, contextLines);
381
+ results = await searchContentNative(resolved, pattern, filePattern, caseSensitive, maxResults, contextLines);
365
382
  }
366
383
 
367
384
  return {
package/src/security.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // agent/src/security.js - 安全管控:文件沙箱 + 命令分级
2
2
  import { resolve, normalize, sep, isAbsolute, join, dirname } from 'path';
3
- import { realpathSync } from 'fs';
3
+ import { realpath } from 'fs/promises';
4
4
  import { homedir, platform } from 'os';
5
5
  import { loadConfig, MEMORY_DIR } from './config.js';
6
6
 
@@ -52,10 +52,10 @@ function isHardBlocked(normalizedPath) {
52
52
 
53
53
  /**
54
54
  * 检查路径是否在白名单内
55
- * 使用 realpathSync 防止符号链接绕过
55
+ * 使用 realpath 防止符号链接绕过
56
56
  * @returns {{ allowed: boolean, reason?: string, resolved?: string }}
57
57
  */
58
- function checkPath(pathStr) {
58
+ async function checkPath(pathStr) {
59
59
  if (!pathStr || typeof pathStr !== 'string') {
60
60
  return { allowed: false, reason: '路径无效' };
61
61
  }
@@ -106,7 +106,7 @@ function checkPath(pathStr) {
106
106
  // realpath 校验:防止符号链接绕过
107
107
  let realPath;
108
108
  try {
109
- realPath = realpathSync(resolved);
109
+ realPath = await realpath(resolved);
110
110
  } catch (err) {
111
111
  if (err.code === 'ENOENT') {
112
112
  let existing = resolved;
@@ -116,7 +116,7 @@ function checkPath(pathStr) {
116
116
  break;
117
117
  }
118
118
  try {
119
- realPath = realpathSync(parent) + sep + normalized.slice(resolve(parent).length).replace(/^[/\\]/, '');
119
+ realPath = await realpath(parent) + sep + normalized.slice(resolve(parent).length).replace(/^[/\\]/, '');
120
120
  break;
121
121
  } catch (parentErr) {
122
122
  if (parentErr.code === 'ENOENT') {
package/src/server.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // agent/src/server.js - HTTP Router + WebSocket 服务器
2
2
  import http from 'http';
3
3
  import { WebSocketServer } from 'ws';
4
- import { readFileSync, writeFileSync, readdirSync, statSync, unlinkSync, rmdirSync, existsSync, chmodSync, mkdirSync } from 'fs';
4
+ import { readFileSync } from 'fs';
5
+ import { readFile, writeFile, readdir, stat, unlink, rmdir, chmod, mkdir } from 'fs/promises';
5
6
  import { join, dirname } from 'path';
6
7
  import { fileURLToPath } from 'url';
7
8
  import { homedir } from 'os';
@@ -10,7 +11,7 @@ import os from 'os';
10
11
  import { loadConfig } from './config.js';
11
12
  import { verifyToken, getCurrentPairCode, startPairCodeRotation, stopPairCodeRotation, handlePairRequest } from './auth.js';
12
13
  import { checkPath, checkCommand } from './security.js';
13
- import { executeCommand, executeCommandSync, addWsClient, killProcess, getRunningProcesses } from './executor.js';
14
+ import { executeCommand, executeCommandSync, addWsClient, disconnectWsClient, killProcess, getRunningProcesses } from './executor.js';
14
15
  import { setConsoleOutput, logAuth, logFs, logExec, logSecurity, logSystem, logError, queryLogs, getLogDates } from './logger.js';
15
16
  import { initSearchTools, getSearchToolsAvailable, searchFiles, searchContent } from './search.js';
16
17
  import {
@@ -98,6 +99,14 @@ function detectShell() {
98
99
  return '/bin/bash';
99
100
  }
100
101
 
102
+ /**
103
+ * 异步检查文件/目录是否存在
104
+ */
105
+ async function exists(path) {
106
+ try { await stat(path); return true; }
107
+ catch { return false; }
108
+ }
109
+
101
110
  const PLATFORM_INFO = {
102
111
  platform: os.platform(),
103
112
  platformName: (() => {
@@ -361,21 +370,21 @@ export function startServer() {
361
370
 
362
371
  // 去重:如果文件已存在,添加后缀
363
372
  let finalPath = destPath;
364
- if (existsSync(finalPath)) {
373
+ if (await exists(finalPath)) {
365
374
  const ext = safeName.lastIndexOf('.') > 0 ? safeName.substring(safeName.lastIndexOf('.')) : '';
366
375
  const base = safeName.substring(0, safeName.length - ext.length) || safeName;
367
376
  let counter = 1;
368
- while (existsSync(finalPath)) {
377
+ while (await exists(finalPath)) {
369
378
  finalPath = join(config.workdir, `${base}_${counter}${ext}`);
370
379
  counter++;
371
380
  }
372
381
  }
373
382
 
374
383
  // 确保工作目录存在
375
- mkdirSync(config.workdir, { recursive: true });
384
+ await mkdir(config.workdir, { recursive: true });
376
385
 
377
386
  // 写入文件
378
- writeFileSync(finalPath, fileBuffer);
387
+ await writeFile(finalPath, fileBuffer);
379
388
 
380
389
  logFs('upload', { path: finalPath, size: fileBuffer.length, mimeType });
381
390
 
@@ -446,23 +455,23 @@ export function startServer() {
446
455
 
447
456
  // 读取文件
448
457
  if (pathname === '/api/fs/read') {
449
- const check = checkPath(body.path);
458
+ const check = await checkPath(body.path);
450
459
  if (!check.allowed) {
451
460
  logSecurity('fs_read_blocked', { path: body.path, reason: check.reason });
452
461
  return jsonResponse(res, 403, { success: false, error: check.reason });
453
462
  }
454
- if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '文件不存在' });
455
- const fstat = statSync(check.resolved);
463
+ if (!await exists(check.resolved)) return jsonResponse(res, 404, { success: false, error: '文件不存在' });
464
+ const fstat = await stat(check.resolved);
456
465
  if (fstat.isDirectory()) return jsonResponse(res, 400, { success: false, error: '路径是目录' });
457
466
  if (fstat.size > maxSize) return jsonResponse(res, 400, { success: false, error: `文件过大 (${fstat.size} > ${maxSize})` });
458
- const content = readFileSync(check.resolved, 'utf-8');
467
+ const content = await readFile(check.resolved, 'utf-8');
459
468
  logFs('read', { path: check.resolved, size: fstat.size });
460
469
  return jsonResponse(res, 200, { success: true, content, size: fstat.size, path: check.resolved });
461
470
  }
462
471
 
463
472
  // 写入文件
464
473
  if (pathname === '/api/fs/write') {
465
- const check = checkPath(body.path);
474
+ const check = await checkPath(body.path);
466
475
  if (!check.allowed) {
467
476
  logSecurity('fs_write_blocked', { path: body.path, reason: check.reason });
468
477
  return jsonResponse(res, 403, { success: false, error: check.reason });
@@ -471,8 +480,8 @@ export function startServer() {
471
480
  const buf = Buffer.from(rawContent, 'utf-8');
472
481
  if (buf.length > maxSize) return jsonResponse(res, 400, { success: false, error: `内容过大 (${buf.length} > ${maxSize})` });
473
482
  // 确保父目录存在
474
- mkdirSync(dirname(check.resolved), { recursive: true });
475
- writeFileSync(check.resolved, rawContent, 'utf-8');
483
+ await mkdir(dirname(check.resolved), { recursive: true });
484
+ await writeFile(check.resolved, rawContent, 'utf-8');
476
485
 
477
486
  // 如果写入的是脚本文件,剥离执行权限防止直接运行
478
487
  const SCRIPT_EXT_RE = /\.(sh|bash|zsh|py|js|mjs|rb|pl|php|lua)$/i;
@@ -480,7 +489,7 @@ export function startServer() {
480
489
  const hasShebang = rawContent.startsWith('#!');
481
490
  if (isScriptExt || hasShebang) {
482
491
  try {
483
- chmodSync(check.resolved, 0o644);
492
+ await chmod(check.resolved, 0o644);
484
493
  } catch {}
485
494
  }
486
495
 
@@ -491,39 +500,40 @@ export function startServer() {
491
500
  // 列出目录
492
501
  if (pathname === '/api/fs/list') {
493
502
  const dirPath = body.path || '.';
494
- const check = checkPath(dirPath);
503
+ const check = await checkPath(dirPath);
495
504
  if (!check.allowed) {
496
505
  logSecurity('fs_list_blocked', { path: dirPath, reason: check.reason });
497
506
  return jsonResponse(res, 403, { success: false, error: check.reason });
498
507
  }
499
- if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '目录不存在' });
500
- const dstat = statSync(check.resolved);
508
+ if (!await exists(check.resolved)) return jsonResponse(res, 404, { success: false, error: '目录不存在' });
509
+ const dstat = await stat(check.resolved);
501
510
  if (!dstat.isDirectory()) return jsonResponse(res, 400, { success: false, error: '路径不是目录' });
502
- const entries = readdirSync(check.resolved).map(name => {
511
+ const names = await readdir(check.resolved);
512
+ const entries = await Promise.all(names.map(async (name) => {
503
513
  const fullPath = join(check.resolved, name);
504
514
  try {
505
- const s = statSync(fullPath);
515
+ const s = await stat(fullPath);
506
516
  return { name, type: s.isDirectory() ? 'directory' : 'file', size: s.size, mtime: s.mtimeMs };
507
517
  } catch { return { name, type: 'unknown', size: 0, mtime: 0 }; }
508
- });
518
+ }));
509
519
  logFs('list', { path: check.resolved, entryCount: entries.length });
510
520
  return jsonResponse(res, 200, { success: true, entries, path: check.resolved });
511
521
  }
512
522
 
513
523
  // 删除文件/目录
514
524
  if (pathname === '/api/fs/delete') {
515
- const check = checkPath(body.path);
525
+ const check = await checkPath(body.path);
516
526
  if (!check.allowed) {
517
527
  logSecurity('fs_delete_blocked', { path: body.path, reason: check.reason });
518
528
  return jsonResponse(res, 403, { success: false, error: check.reason });
519
529
  }
520
- if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '文件/目录不存在' });
521
- const fstat2 = statSync(check.resolved);
530
+ if (!await exists(check.resolved)) return jsonResponse(res, 404, { success: false, error: '文件/目录不存在' });
531
+ const fstat2 = await stat(check.resolved);
522
532
  const isDir = fstat2.isDirectory();
523
533
  if (isDir) {
524
- rmdirSync(check.resolved, { recursive: true });
534
+ await rmdir(check.resolved, { recursive: true });
525
535
  } else {
526
- unlinkSync(check.resolved);
536
+ await unlink(check.resolved);
527
537
  }
528
538
  logFs('delete', { path: check.resolved, type: isDir ? 'directory' : 'file', size: fstat2.size });
529
539
  return jsonResponse(res, 200, { success: true, path: check.resolved });
@@ -535,12 +545,12 @@ export function startServer() {
535
545
  if (!targetPath || typeof targetPath !== 'string') {
536
546
  return jsonResponse(res, 400, { success: false, error: '缺少 path 参数' });
537
547
  }
538
- const check = checkPath(targetPath);
548
+ const check = await checkPath(targetPath);
539
549
  if (!check.allowed) {
540
550
  logSecurity('browser_open_blocked', { path: targetPath, reason: check.reason });
541
551
  return jsonResponse(res, 403, { success: false, error: check.reason });
542
552
  }
543
- if (!existsSync(check.resolved)) {
553
+ if (!await exists(check.resolved)) {
544
554
  return jsonResponse(res, 404, { success: false, error: '文件不存在' });
545
555
  }
546
556
 
@@ -575,7 +585,7 @@ export function startServer() {
575
585
 
576
586
  // 校验 cwd
577
587
  let resolvedCwd = cwd || config.workdir;
578
- const cwdCheck = checkPath(resolvedCwd);
588
+ const cwdCheck = await checkPath(resolvedCwd);
579
589
  if (!cwdCheck.allowed) {
580
590
  logSecurity('exec_cwd_blocked', { command, cwd: resolvedCwd, reason: cwdCheck.reason });
581
591
  return jsonResponse(res, 403, { success: false, error: '执行目录校验失败: ' + cwdCheck.reason });
@@ -932,7 +942,7 @@ export function startServer() {
932
942
  return jsonResponse(res, 404, { success: false, error: `资源 "${resource}" 不存在` });
933
943
  }
934
944
  try {
935
- const content = readFileSync(resInfo.path, 'utf-8');
945
+ const content = await readFile(resInfo.path, 'utf-8');
936
946
  return jsonResponse(res, 200, { success: true, name: resource, content, size: resInfo.size });
937
947
  } catch (err) {
938
948
  return jsonResponse(res, 500, { success: false, error: `读取资源失败: ${err.message}` });
@@ -991,6 +1001,10 @@ export function startServer() {
991
1001
  if (!added) {
992
1002
  ws.send(JSON.stringify({ type: 'error', error: '进程不存在或已结束', execId }));
993
1003
  }
1004
+ // 客户端断开时,从监听列表中移除;所有客户端都断开后取消超时
1005
+ ws.on('close', () => {
1006
+ disconnectWsClient(execId, ws);
1007
+ });
994
1008
  });
995
1009
  } else {
996
1010
  socket.destroy();
@@ -1041,7 +1055,7 @@ export function startServer() {
1041
1055
  }
1042
1056
 
1043
1057
  // 关闭所有 MCP 连接
1044
- try { shutdownMcpRegistry(); } catch {}
1058
+ try { await shutdownMcpRegistry(); } catch {}
1045
1059
 
1046
1060
  // 等待 server 和 wss 关闭
1047
1061
  await Promise.all([
@@ -1050,7 +1064,7 @@ export function startServer() {
1050
1064
  ]);
1051
1065
 
1052
1066
  // 清理 PID 文件
1053
- try { if (existsSync(PID_FILE)) unlinkSync(PID_FILE); } catch {}
1067
+ try { if (await exists(PID_FILE)) await unlink(PID_FILE); } catch {}
1054
1068
 
1055
1069
  logSystem('server_stop', { reason: 'shutdown' });
1056
1070
  process.exit(0);
@@ -1,7 +1,7 @@
1
1
  // skill/executor.js - Skill 执行引擎
2
2
  // 解析 Skill 定义的步骤 DAG,按拓扑排序执行,支持并行
3
3
  import { render } from './template.js';
4
- import { readFileSync } from 'fs';
4
+ import { readFile, writeFile, readdir, stat } from 'fs/promises';
5
5
  import { checkPath, checkCommand } from '../security.js';
6
6
  import { join } from 'path';
7
7
 
@@ -48,28 +48,27 @@ async function executeToolCall(toolName, args) {
48
48
  try {
49
49
  switch (toolName) {
50
50
  case 'agent_read_file': {
51
- const check = checkPath(args.path);
51
+ const check = await checkPath(args.path);
52
52
  if (!check.allowed) return { success: false, error: check.reason };
53
- const content = readFileSync(check.resolved, 'utf-8');
53
+ const content = await readFile(check.resolved, 'utf-8');
54
54
  return { success: true, content, size: content.length, path: check.resolved };
55
55
  }
56
56
 
57
57
  case 'agent_write_file': {
58
- const check = checkPath(args.path);
58
+ const check = await checkPath(args.path);
59
59
  if (!check.allowed) return { success: false, error: check.reason };
60
- const { writeFileSync } = await import('fs');
61
- writeFileSync(check.resolved, String(args.content || ''), 'utf-8');
60
+ await writeFile(check.resolved, String(args.content || ''), 'utf-8');
62
61
  return { success: true, message: `文件已写入: ${check.resolved}` };
63
62
  }
64
63
 
65
64
  case 'agent_list_dir': {
66
- const check = checkPath(args.path || '.');
65
+ const check = await checkPath(args.path || '.');
67
66
  if (!check.allowed) return { success: false, error: check.reason };
68
- const { readdirSync, statSync } = await import('fs');
69
- const entries = readdirSync(check.resolved).map(name => {
70
- const s = statSync(join(check.resolved, name));
67
+ const names = await readdir(check.resolved);
68
+ const entries = await Promise.all(names.map(async (name) => {
69
+ const s = await stat(join(check.resolved, name));
71
70
  return { name, type: s.isDirectory() ? 'directory' : 'file', size: s.size };
72
- });
71
+ }));
73
72
  return { success: true, entries, path: check.resolved };
74
73
  }
75
74
 
@@ -82,14 +81,21 @@ async function executeToolCall(toolName, args) {
82
81
  if (cmdCheck.level === 'confirm') {
83
82
  return { success: false, error: `需要用户确认的命令: ${cmdCheck.reason}` };
84
83
  }
85
- const { execSync } = await import('child_process');
86
- const result = execSync(args.command, {
87
- cwd: args.cwd || process.cwd(),
88
- encoding: 'utf-8',
89
- timeout: 30000,
90
- maxBuffer: 10 * 1024 * 1024
84
+ const { exec } = await import('child_process');
85
+ return new Promise((resolve) => {
86
+ exec(args.command, {
87
+ cwd: args.cwd || process.cwd(),
88
+ encoding: 'utf-8',
89
+ timeout: 30000,
90
+ maxBuffer: 10 * 1024 * 1024
91
+ }, (error, stdout, stderr) => {
92
+ if (error) {
93
+ resolve({ success: false, error: error.message, stdout, stderr });
94
+ } else {
95
+ resolve({ success: true, stdout, exitCode: 0, stderr });
96
+ }
97
+ });
91
98
  });
92
- return { success: true, stdout: result, exitCode: 0 };
93
99
  }
94
100
 
95
101
  case 'agent_search_files': {
@@ -212,7 +212,7 @@ export function getAgentSkillPrompts() {
212
212
  const parts = [];
213
213
  parts.push('## 可用技能 (Skills)');
214
214
  parts.push('');
215
- parts.push('以下是可用的专业技能列表。当用户需求匹配某个技能时,使用 `agent_skill_load` 工具加载该技能的完整说明。');
215
+ parts.push('以下是可用的 Agent Skill(AI 能力扩展)列表。当用户需求匹配某个技能时,使用 `agent_skill_load` 工具加载该技能的完整说明,然后由 AI 根据说明自主调用相关工具完成任务。');
216
216
  parts.push('');
217
217
 
218
218
  for (const skill of agentSkills) {