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 +1 -1
- package/src/config.js +7 -2
- package/src/executor.js +54 -4
- package/src/mcp/client.js +1 -2
- package/src/server.js +31 -1
- package/src/skill/loader.js +0 -1
- package/src/skill/markdown-loader.js +0 -1
- package/src/skill/registry.js +7 -5
package/package.json
CHANGED
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
}
|
package/src/skill/loader.js
CHANGED
|
@@ -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
|
}
|
package/src/skill/registry.js
CHANGED
|
@@ -75,12 +75,10 @@ export function initializeSkillRegistry() {
|
|
|
75
75
|
skills.clear();
|
|
76
76
|
|
|
77
77
|
// 0. 种子写入 skill-creator 到文件系统(首次运行或版本升级时)
|
|
78
|
-
|
|
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
|
|
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) {
|