ai-helper-agent 1.9.3 → 1.9.5

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": "ai-helper-agent",
3
- "version": "1.9.3",
3
+ "version": "1.9.5",
4
4
  "description": "AI Helper 代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
package/src/config.js CHANGED
@@ -13,8 +13,8 @@ const MEMORY_DIR = join(AGENT_DIR, 'memory');
13
13
  const DEFAULTS = {
14
14
  port: 18910,
15
15
  host: '127.0.0.1',
16
- workdir: process.cwd(),
17
- allowedPaths: [],
16
+ workdir: WORKSPACE_DIR,
17
+ allowedPaths: [WORKSPACE_DIR],
18
18
  pairCodeTTL: 30,
19
19
  commandTimeout: 300000,
20
20
  fileMaxSize: 50 * 1024 * 1024
@@ -103,6 +103,11 @@ export function loadConfig() {
103
103
  configCache.allowedPaths.push(SKILLS_DIR);
104
104
  }
105
105
 
106
+ // 自动将 workspace 目录加入白名单,确保 AI 可以访问工作目录
107
+ if (!configCache.allowedPaths.includes(WORKSPACE_DIR)) {
108
+ configCache.allowedPaths.push(WORKSPACE_DIR);
109
+ }
110
+
106
111
  return configCache;
107
112
  }
108
113
 
package/src/executor.js CHANGED
@@ -51,9 +51,14 @@ function getShellForExec() {
51
51
  return { shell: '/bin/bash', args: ['-c'] };
52
52
  }
53
53
 
54
- // 运行中的进程映射:execId → { process, wsClients: Set, timeoutId, forceKillId }
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 }
59
+ const completedProcesses = new Map();
60
+ const COMPLETED_CACHE_TTL = 30000; // 30秒后清理
61
+
57
62
  // 安全的子进程环境变量白名单(不泄露宿主敏感信息)
58
63
  const SAFE_ENV_KEYS = [
59
64
  'PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'LC_CTYPE',
@@ -127,7 +132,8 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
127
132
  let finished = false; // 防止 close/error 重复处理
128
133
 
129
134
  // 存储到 entry 中供 clearTimers 读取
130
- const entry = { process: proc, wsClients };
135
+ // stdoutBuf/stderrBuf 始终收集,用于回放给延迟连接的 WS 客户端
136
+ const entry = { process: proc, wsClients, stdoutBuf: '', stderrBuf: '' };
131
137
  runningProcesses.set(execId, entry);
132
138
 
133
139
  // 超时控制
@@ -160,6 +166,7 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
160
166
  proc.stdout.on('data', (chunk) => {
161
167
  const str = chunk.toString();
162
168
  if (collectOutput) stdoutCollected += str;
169
+ entry.stdoutBuf += str;
163
170
  broadcast({ type: 'stdout', data: str, execId });
164
171
  });
165
172
 
@@ -167,6 +174,7 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
167
174
  proc.stderr.on('data', (chunk) => {
168
175
  const str = chunk.toString();
169
176
  if (collectOutput) stderrCollected += str;
177
+ entry.stderrBuf += str;
170
178
  broadcast({ type: 'stderr', data: str, execId });
171
179
  });
172
180
 
@@ -175,12 +183,24 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
175
183
  if (finished) return;
176
184
  finished = true;
177
185
  clearTimers();
178
- broadcast({ type: 'exit', exitCode: typeof exitCode === 'number' ? exitCode : -1, execId, killed });
186
+ const finalExitCode = typeof exitCode === 'number' ? exitCode : -1;
187
+ broadcast({ type: 'exit', exitCode: finalExitCode, execId, killed });
188
+ // 移入已完成缓存,解决竞态条件:延迟连接的 WS 客户端可回放输出
189
+ const cleanupTimeout = setTimeout(() => {
190
+ completedProcesses.delete(execId);
191
+ }, COMPLETED_CACHE_TTL);
192
+ completedProcesses.set(execId, {
193
+ stdoutBuf: entry.stdoutBuf,
194
+ stderrBuf: entry.stderrBuf,
195
+ exitCode: finalExitCode,
196
+ killed,
197
+ cleanupTimeout
198
+ });
179
199
  runningProcesses.delete(execId);
180
200
  if (onComplete) {
181
201
  onComplete({
182
202
  execId,
183
- exitCode: typeof exitCode === 'number' ? exitCode : -1,
203
+ exitCode: finalExitCode,
184
204
  killed,
185
205
  stdout: collectOutput ? stdoutCollected : undefined,
186
206
  stderr: collectOutput ? stderrCollected : undefined
@@ -194,6 +214,18 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
194
214
  finished = true;
195
215
  clearTimers();
196
216
  broadcast({ type: 'error', error: err.message, execId });
217
+ // 移入已完成缓存
218
+ const cleanupTimeout = setTimeout(() => {
219
+ completedProcesses.delete(execId);
220
+ }, COMPLETED_CACHE_TTL);
221
+ completedProcesses.set(execId, {
222
+ stdoutBuf: entry.stdoutBuf,
223
+ stderrBuf: entry.stderrBuf,
224
+ exitCode: -1,
225
+ killed: false,
226
+ error: err.message,
227
+ cleanupTimeout
228
+ });
197
229
  runningProcesses.delete(execId);
198
230
  if (onComplete) {
199
231
  onComplete({
@@ -241,6 +273,7 @@ function executeCommandSync(command, cwd) {
241
273
 
242
274
  /**
243
275
  * 添加 WebSocket 客户端到已有进程的监听中
276
+ * 如果进程已结束,从 completedProcesses 缓存回放全部输出
244
277
  */
245
278
  function addWsClient(execId, wsClient) {
246
279
  const entry = runningProcesses.get(execId);
@@ -248,6 +281,23 @@ function addWsClient(execId, wsClient) {
248
281
  entry.wsClients.add(wsClient);
249
282
  return true;
250
283
  }
284
+ // 进程已结束,检查缓存并回放全部输出
285
+ const completed = completedProcesses.get(execId);
286
+ if (completed) {
287
+ if (completed.stdoutBuf) {
288
+ wsClient.send(JSON.stringify({ type: 'stdout', data: completed.stdoutBuf, execId }));
289
+ }
290
+ if (completed.stderrBuf) {
291
+ wsClient.send(JSON.stringify({ type: 'stderr', data: completed.stderrBuf, execId }));
292
+ }
293
+ if (completed.error) {
294
+ wsClient.send(JSON.stringify({ type: 'error', error: completed.error, execId }));
295
+ }
296
+ wsClient.send(JSON.stringify({ type: 'exit', exitCode: completed.exitCode, execId, killed: completed.killed }));
297
+ clearTimeout(completed.cleanupTimeout);
298
+ completedProcesses.delete(execId);
299
+ return true;
300
+ }
251
301
  return false;
252
302
  }
253
303
 
package/src/mcp/client.js CHANGED
@@ -97,8 +97,7 @@ export class McpClient {
97
97
  const toolsResult = await this.client.listTools({});
98
98
  if (toolsResult && toolsResult.tools) {
99
99
  this.tools = toolsResult.tools;
100
- console.log(`[MCP:${this.serverId}] 发现 ${this.tools.length} 个工具:`,
101
- this.tools.map(t => t.name).join(', '));
100
+ console.log(`[MCP:${this.serverId}] 发现 ${this.tools.length} 个工具`);
102
101
  } else {
103
102
  this.tools = [];
104
103
  }
package/src/server.js CHANGED
@@ -966,6 +966,14 @@ export function startServer() {
966
966
  jsonResponse(res, 404, { success: false, error: '未知的 API 路径' });
967
967
  }
968
968
 
969
+ // 追踪所有活跃 socket 连接,用于优雅关闭时强制断开
970
+ const activeSockets = new Set();
971
+
972
+ server.on('connection', (socket) => {
973
+ activeSockets.add(socket);
974
+ socket.on('close', () => activeSockets.delete(socket));
975
+ });
976
+
969
977
  // ==================== WebSocket Server ====================
970
978
  const wss = new WebSocketServer({ noServer: true });
971
979
 
@@ -1000,6 +1008,9 @@ export function startServer() {
1000
1008
  const added = addWsClient(execId, ws);
1001
1009
  if (!added) {
1002
1010
  ws.send(JSON.stringify({ type: 'error', error: '进程不存在或已结束', execId }));
1011
+ // 进程已不存在,发送错误后关闭 WebSocket,避免客户端空等
1012
+ ws.close();
1013
+ return;
1003
1014
  }
1004
1015
  // 客户端断开时,从监听列表中移除;所有客户端都断开后取消超时
1005
1016
  ws.on('close', () => {
@@ -1042,11 +1053,19 @@ export function startServer() {
1042
1053
  }
1043
1054
  });
1044
1055
 
1045
- // 优雅关闭(异步 + 防并发)
1056
+ // 优雅关闭(异步 + 防并发 + 超时兜底)
1046
1057
  async function shutdown() {
1047
1058
  if (shuttingDown) return;
1048
1059
  shuttingDown = true;
1049
1060
  console.log('[Agent] 正在关闭...');
1061
+
1062
+ // 超时兜底:10 秒后强制退出,防止 server.close() 等待连接挂起
1063
+ const forceExitTimer = setTimeout(() => {
1064
+ console.log('[Agent] 优雅关闭超时,强制退出');
1065
+ process.exit(1);
1066
+ }, 10000);
1067
+ forceExitTimer.unref();
1068
+
1050
1069
  stopPairCodeRotation();
1051
1070
 
1052
1071
  // 终止所有运行中的进程
@@ -1057,6 +1076,16 @@ export function startServer() {
1057
1076
  // 关闭所有 MCP 连接
1058
1077
  try { await shutdownMcpRegistry(); } catch {}
1059
1078
 
1079
+ // 强制断开所有 WebSocket 客户端
1080
+ wss.clients.forEach((client) => {
1081
+ try { client.terminate(); } catch {}
1082
+ });
1083
+
1084
+ // 强制销毁所有活跃 HTTP socket,避免 server.close() 挂起等待
1085
+ for (const socket of activeSockets) {
1086
+ try { socket.destroy(); } catch {}
1087
+ }
1088
+
1060
1089
  // 等待 server 和 wss 关闭
1061
1090
  await Promise.all([
1062
1091
  new Promise(resolve => server.close(resolve)),
@@ -1066,6 +1095,7 @@ export function startServer() {
1066
1095
  // 清理 PID 文件
1067
1096
  try { if (await exists(PID_FILE)) await unlink(PID_FILE); } catch {}
1068
1097
 
1098
+ clearTimeout(forceExitTimer);
1069
1099
  logSystem('server_stop', { reason: 'shutdown' });
1070
1100
  process.exit(0);
1071
1101
  }
@@ -174,7 +174,6 @@ export function loadAllSkills() {
174
174
  if (skill) {
175
175
  skill.type = 'workflow';
176
176
  allSkills.push(skill);
177
- console.log(`[Skill Loader] 加载 Workflow Skill: "${skill.name}" v${skill.version} (${skill.steps.length} 步骤)`);
178
177
  }
179
178
  }
180
179
  }
@@ -197,7 +197,6 @@ export function loadAllMarkdownSkills(skillsDir) {
197
197
  const skill = loadMarkdownSkill(entryPath);
198
198
  if (skill) {
199
199
  skills.push(skill);
200
- console.log(`[Markdown Loader] 加载 Agent Skill: "${skill.name}" v${skill.version} (${skill.resources.length} 个资源)`);
201
200
  }
202
201
  } catch { /* skip */ }
203
202
  }
@@ -75,12 +75,10 @@ export function initializeSkillRegistry() {
75
75
  skills.clear();
76
76
 
77
77
  // 0. 种子写入 skill-creator 到文件系统(首次运行或版本升级时)
78
- const seeded = seedSkillCreator();
79
- console.log(`[Skill Registry] seedSkillCreator 返回: ${seeded}`);
78
+ seedSkillCreator();
80
79
 
81
80
  // 1. 加载文件系统中的技能
82
81
  const loaded = loadAllSkills();
83
- console.log(`[Skill Registry] loadAllSkills 返回 ${loaded.length} 个技能: ${loaded.map(s => s.name).join(', ') || '(无)'}`);
84
82
 
85
83
  for (const skill of loaded) {
86
84
  skills.set(skill.name, skill);
@@ -91,7 +89,6 @@ export function initializeSkillRegistry() {
91
89
  for (const skill of builtinSkills) {
92
90
  if (!skills.has(skill.name)) {
93
91
  skills.set(skill.name, skill);
94
- console.log(`[Skill Registry] 注册内置 Skill: "${skill.name}"`);
95
92
  }
96
93
  }
97
94
 
@@ -212,7 +209,12 @@ export function getAgentSkillPrompts() {
212
209
  const parts = [];
213
210
  parts.push('## 可用技能 (Skills)');
214
211
  parts.push('');
215
- parts.push('以下是可用的 Agent Skill(AI 能力扩展)列表。当用户需求匹配某个技能时,使用 `agent_skill_load` 工具加载该技能的完整说明,然后由 AI 根据说明自主调用相关工具完成任务。');
212
+ parts.push('以下是可用的 Agent Skill(AI 能力扩展)列表。当用户需求与某个技能高度匹配时,使用 `agent_skill_load` 工具加载该技能的完整说明,然后根据说明自主完成任务。');
213
+ parts.push('');
214
+ parts.push('**使用策略**:');
215
+ parts.push('- 仅在用户需求明确匹配技能描述时才加载,不要为一般性的编程/技术问题加载技能');
216
+ parts.push('- 一次只加载一个技能,加载后根据 SKILL.md 说明执行');
217
+ parts.push('- 技能加载会消耗较多 token,避免无故加载');
216
218
  parts.push('');
217
219
 
218
220
  for (const skill of agentSkills) {