ai-helper-agent 1.14.5 → 1.14.7

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // agent/bin/agent.js - CLI entry point
3
3
  // Usage: ai-helper-agent <start|stop|restart|status|paircode|config> [options]
4
- import { join, dirname } from 'path';
4
+ import { join, dirname, resolve } from 'path';
5
5
  import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
6
6
  import { spawn } from 'child_process';
7
7
  import { homedir } from 'os';
@@ -38,12 +38,14 @@ function resolveLang(args) {
38
38
  return detectSystemLang();
39
39
  }
40
40
 
41
- // Remove --lang <val> from args to avoid passing to server
41
+ // Remove --lang <val> and background flags from args to avoid passing to server
42
42
  function filterLangArgs(args) {
43
43
  const filtered = [];
44
44
  for (let i = 0; i < args.length; i++) {
45
45
  if (args[i] === '--lang' && args[i + 1]) {
46
46
  i++; // skip the value too
47
+ } else if (args[i] === '-b' || args[i] === '--background') {
48
+ // 过滤后台标志,避免子进程递归 fork 孙进程
47
49
  } else {
48
50
  filtered.push(args[i]);
49
51
  }
@@ -51,6 +53,29 @@ function filterLangArgs(args) {
51
53
  return filtered;
52
54
  }
53
55
 
56
+ // 健康检查:轮询 GET /api/status,直到成功或超时(最多 8 秒)
57
+ async function healthCheck(port, host, timeoutMs = 8000, intervalMs = 500) {
58
+ const endpoint = `http://${host}:${port}/api/status`;
59
+ const maxAttempts = Math.ceil(timeoutMs / intervalMs);
60
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
61
+ try {
62
+ const res = await fetch(endpoint);
63
+ if (res.ok) return true;
64
+ } catch {
65
+ // 服务尚未就绪,继续等待
66
+ }
67
+ if (attempt < maxAttempts) {
68
+ await new Promise((r) => setTimeout(r, intervalMs));
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+
74
+ // 等待后台服务就绪(带超时),成功返回 true
75
+ async function waitForAgentReady(port, host) {
76
+ return healthCheck(port, host);
77
+ }
78
+
54
79
  const lang = resolveLang(process.argv);
55
80
  process.env.AI_HELPER_LANG = lang;
56
81
  const translations = loadCliTranslations();
@@ -272,9 +297,11 @@ if (command === 'start') {
272
297
  process.exit(0);
273
298
  }
274
299
 
300
+ const agentScript = resolve(process.argv[1]);
301
+
275
302
  const child = spawn(process.execPath, [
276
303
  ...process.execArgv,
277
- process.argv[1],
304
+ agentScript,
278
305
  'start',
279
306
  ...passArgs
280
307
  ], {
@@ -293,9 +320,11 @@ if (command === 'start') {
293
320
  process.exit(1);
294
321
  });
295
322
 
296
- await new Promise(resolve => setTimeout(resolve, 500));
323
+ // 健康检查:轮询 /api/status,最多等待 8 秒,确保服务真正就绪
324
+ const readyConfig = applyCliArgs(readAgentConfig(), passArgs);
325
+ const healthy = !spawnFailed && child.pid ? await waitForAgentReady(readyConfig.port, readyConfig.host) : false;
297
326
 
298
- if (!spawnFailed && child.pid) {
327
+ if (!spawnFailed && child.pid && healthy) {
299
328
  try {
300
329
  writeFileSync(PID_FILE, String(child.pid));
301
330
  } catch (err) {
@@ -306,6 +335,11 @@ if (command === 'start') {
306
335
  console.log(`[Agent] ${t('agentStartedBg', { pid: child.pid })}`);
307
336
  console.log(`[Agent] ${t('stopHint')}`);
308
337
  console.log(`[Agent] ${t('statusHint')}`);
338
+ } else if (!spawnFailed && child.pid && !healthy) {
339
+ // 服务未在超时内就绪,清理 PID 文件并报错
340
+ try { unlinkSync(PID_FILE); } catch {}
341
+ console.error(`[Agent] ${t('backgroundStartFailed')}: health check timed out`);
342
+ process.exit(1);
309
343
  }
310
344
 
311
345
  process.exit(spawnFailed ? 1 : 0);
@@ -398,9 +432,11 @@ if (command === 'start') {
398
432
  if (isBg) {
399
433
  ensureAgentDir();
400
434
 
435
+ const agentScript = resolve(process.argv[1]);
436
+
401
437
  const child = spawn(process.execPath, [
402
438
  ...process.execArgv,
403
- process.argv[1],
439
+ agentScript,
404
440
  'start',
405
441
  ...passArgs
406
442
  ], {
@@ -419,9 +455,11 @@ if (command === 'start') {
419
455
  process.exit(1);
420
456
  });
421
457
 
422
- await new Promise(resolve => setTimeout(resolve, 500));
458
+ // 健康检查:轮询 /api/status,最多等待 8 秒,确保服务真正就绪
459
+ const readyConfig = applyCliArgs(readAgentConfig(), passArgs);
460
+ const healthy = !spawnFailed && child.pid ? await waitForAgentReady(readyConfig.port, readyConfig.host) : false;
423
461
 
424
- if (!spawnFailed && child.pid) {
462
+ if (!spawnFailed && child.pid && healthy) {
425
463
  try {
426
464
  writeFileSync(PID_FILE, String(child.pid));
427
465
  } catch (err) {
@@ -431,6 +469,10 @@ if (command === 'start') {
431
469
  child.unref();
432
470
  console.log(`[Agent] ${t('agentRestartedBg', { pid: child.pid })}`);
433
471
  console.log(`[Agent] ${t('stopHint')}`);
472
+ } else if (!spawnFailed && child.pid && !healthy) {
473
+ try { unlinkSync(PID_FILE); } catch {}
474
+ console.error(`[Agent] ${t('backgroundRestartFailed')}: health check timed out`);
475
+ process.exit(1);
434
476
  }
435
477
 
436
478
  process.exit(spawnFailed ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-helper-agent",
3
- "version": "1.14.5",
3
+ "version": "1.14.7",
4
4
  "description": "AI Helper Agent - Local file access and system command execution proxy for Chrome extension / AI Helper 代理服务,为 Chrome 扩展提供本地文件读写和系统命令执行",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
package/src/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // agent/src/server.js - HTTP Router + WebSocket 服务器
2
2
  import http from 'http';
3
3
  import { WebSocketServer } from 'ws';
4
- import { readFileSync, createWriteStream, createReadStream, statSync, existsSync } from 'fs';
4
+ import { readFileSync, createWriteStream, createReadStream, statSync, existsSync, watch } from 'fs';
5
5
  import { readFile, writeFile, readdir, stat, unlink, rmdir, chmod, mkdir, rename } from 'fs/promises';
6
6
  import { join, dirname, basename, resolve, isAbsolute, relative } from 'path';
7
7
  import { fileURLToPath, pathToFileURL } from 'url';
@@ -2069,12 +2069,45 @@ export function startServer() {
2069
2069
 
2070
2070
  try {
2071
2071
  await initializeSkillRegistry();
2072
+ startSkillDirWatcher();
2072
2073
  } catch (err) {
2073
2074
  console.error(`[Agent] ${ln('skillInitFailed')}:`, err.message);
2074
2075
  }
2075
2076
  })();
2076
2077
  });
2077
2078
 
2079
+ /**
2080
+ * 监听技能目录(~/.ai-helper-agent/skills/)的文件变更,
2081
+ * 当对话中通过写文件创建/更新技能后,自动重新扫描并加载,无需手动「重新加载」。
2082
+ */
2083
+ let skillDirWatchTimer = null;
2084
+ let skillDirWatcher = null;
2085
+ function startSkillDirWatcher() {
2086
+ try {
2087
+ const dir = getSkillsDir();
2088
+ if (!existsSync(dir)) return;
2089
+ skillDirWatcher = watch(dir, { recursive: true }, (_event, filename) => {
2090
+ if (!filename) return;
2091
+ // 仅关心 SKILL.md / _meta.json 等技能相关文件变更
2092
+ if (!/\.md$|\.json$/.test(filename)) return;
2093
+ if (skillDirWatchTimer) clearTimeout(skillDirWatchTimer);
2094
+ skillDirWatchTimer = setTimeout(async () => {
2095
+ try {
2096
+ await reloadSkills();
2097
+ logSystem('skill_auto_reload', { reason: 'dir_change', file: filename });
2098
+ } catch (err) {
2099
+ console.error(`[Agent] Skill auto-reload failed:`, err.message);
2100
+ }
2101
+ }, 800);
2102
+ });
2103
+ skillDirWatcher.on('error', (err) => {
2104
+ console.warn(`[Agent] Skill dir watcher error:`, err.message);
2105
+ });
2106
+ } catch (err) {
2107
+ console.warn(`[Agent] Failed to start skill dir watcher:`, err.message);
2108
+ }
2109
+ }
2110
+
2078
2111
  // 优雅关闭(异步 + 防并发 + 超时兜底)
2079
2112
  async function shutdown() {
2080
2113
  if (shuttingDown) return;
@@ -124,22 +124,18 @@ async function executeToolCall(toolName, args) {
124
124
  if (cmdCheck.level === 'confirm') {
125
125
  return { success: false, error: tr('skill.commandNeedsConfirm', { reason: cmdCheck.reason }) };
126
126
  }
127
- const { exec } = await import('child_process');
128
- return new Promise((resolve) => {
129
- exec(args.command, {
130
- cwd: args.cwd || process.cwd(),
131
- encoding: 'utf-8',
132
- timeout: 30000,
133
- maxBuffer: 10 * 1024 * 1024,
134
- windowsHide: true
135
- }, (error, stdout, stderr) => {
136
- if (error) {
137
- resolve({ success: false, error: error.message, stdout, stderr });
138
- } else {
139
- resolve({ success: true, stdout, exitCode: 0, stderr });
140
- }
141
- });
142
- });
127
+ // 走统一命令执行器(支持 Git Bash 等环境下 Bash 花括号扩展等特性)
128
+ const { executeCommandSync } = await import('../executor.js');
129
+ const result = await executeCommandSync(args.command, args.cwd || process.cwd());
130
+ const stdout = result.stdout || '';
131
+ const stderr = result.stderr || '';
132
+ if (result.error) {
133
+ return { success: false, error: result.error, stdout, stderr };
134
+ }
135
+ if (result.exitCode !== 0) {
136
+ return { success: false, error: stderr || tr('skill.execFailed'), stdout, stderr };
137
+ }
138
+ return { success: true, stdout, exitCode: result.exitCode, stderr };
143
139
  }
144
140
 
145
141
  case 'agent_search':
@@ -22,6 +22,53 @@ export function setSkillRegistryLang(lang) {
22
22
  // Skill 映射表: name → skill定义
23
23
  const skills = new Map();
24
24
 
25
+ /**
26
+ * 名称归一化:转小写 + 空格/连字符/下划线统一为连字符
27
+ * 用于大小写/分隔符不敏感的模糊匹配
28
+ * @param {string} name
29
+ * @returns {string}
30
+ */
31
+ function normalizeName(name) {
32
+ return String(name || '').toLowerCase().replace(/[\s_]+/g, '-').trim();
33
+ }
34
+
35
+ /**
36
+ * 三级模糊查找 Agent Skill
37
+ * 解决 SKILL.md frontmatter `name`(如 "Agent Browser")与目录名/slug(如 `agent-browser`)不一致的问题
38
+ *
39
+ * 匹配顺序:
40
+ * ① 精确 name 匹配(skills Map 的 key)
41
+ * ② 目录名匹配(basename(dirPath))
42
+ * ③ 归一化后大小写/空格/连字符/下划线不敏感匹配
43
+ *
44
+ * @param {string} name - AI 调用时使用的名称(可能是 frontmatter name、目录名或 slug)
45
+ * @returns {Object|undefined} 匹配到的 skill 定义,未找到返回 undefined
46
+ */
47
+ function findAgentSkillByName(name) {
48
+ if (!name) return undefined;
49
+
50
+ // ① 精确 name 匹配
51
+ const exact = skills.get(name);
52
+ if (exact && exact.type === 'agent') return exact;
53
+
54
+ // ② 目录名匹配
55
+ for (const [, skill] of skills) {
56
+ if (skill.type !== 'agent') continue;
57
+ if (skill.dirPath && basename(skill.dirPath) === name) return skill;
58
+ }
59
+
60
+ // ③ 归一化后模糊匹配
61
+ const target = normalizeName(name);
62
+ if (!target) return undefined;
63
+ for (const [, skill] of skills) {
64
+ if (skill.type !== 'agent') continue;
65
+ if (normalizeName(skill.name) === target) return skill;
66
+ if (skill.dirPath && normalizeName(basename(skill.dirPath)) === target) return skill;
67
+ }
68
+
69
+ return undefined;
70
+ }
71
+
25
72
  // 内置技能状态持久化文件
26
73
  const BUILTIN_STATE_FILE = join(dirname(SKILLS_DIR), 'builtin_skills_state.json');
27
74
 
@@ -167,6 +214,10 @@ export function getSkillList(type) {
167
214
  } else if (skill.type === 'agent') {
168
215
  item.resourceCount = skill.resources?.length || 0;
169
216
  item.resources = (skill.resources || []).map(r => ({ name: r.name, type: r.type, size: r.size }));
217
+ // 暴露目录名(slug),方便前端展示和 AI 调用
218
+ if (skill.dirPath) {
219
+ item.dirName = basename(skill.dirPath);
220
+ }
170
221
  }
171
222
 
172
223
  list.push(item);
@@ -232,7 +283,13 @@ export function getAgentSkillPrompts() {
232
283
  parts.push('');
233
284
 
234
285
  for (const skill of agentSkills) {
235
- parts.push(`### ${skill.name}`);
286
+ // 当 frontmatter name 与目录名(slug)不一致时,标题同时显示 slug,引导 AI 用正确名称调用
287
+ const dirName = skill.dirPath ? basename(skill.dirPath) : '';
288
+ if (dirName && dirName !== skill.name) {
289
+ parts.push(`### ${skill.name} (slug: \`${dirName}\`)`);
290
+ } else {
291
+ parts.push(`### ${skill.name}`);
292
+ }
236
293
  parts.push(T('skill.skillPromptDesc', { description: skill.description }));
237
294
  if (skill.resources && skill.resources.length > 0) {
238
295
  const resourceNames = skill.resources.map(r => `\`${r.name}\``).join(', ');
@@ -250,8 +307,8 @@ export function getAgentSkillPrompts() {
250
307
  * @returns {{ success: boolean, error?: string, skill?: Object, prompt?: string }}
251
308
  */
252
309
  export function getAgentSkillPrompt(name) {
253
- const skill = skills.get(name);
254
- if (!skill || skill.type !== 'agent') {
310
+ const skill = findAgentSkillByName(name);
311
+ if (!skill) {
255
312
  return { success: false, error: `Skill "${name}" 不存在或不是 Agent Skill` };
256
313
  }
257
314
  // 如果 skill 有 dirPath,在 prompt 前注入工作目录信息,方便 AI 解析相对路径引用
@@ -8,7 +8,7 @@ export const SKILL_CREATOR_SEED_MD = `---
8
8
  name: skill-creator
9
9
  description: "创建新技能的元技能。当用户要求创建技能、沉淀技能、新增技能、或任何创建/添加技能的意图时,必须立即调用此技能。"
10
10
  enabled: true
11
- version: 1.2.1
11
+ version: 1.2.2
12
12
  ---
13
13
 
14
14
  # Skill Creator
@@ -114,7 +114,7 @@ enabled: true
114
114
 
115
115
  **推荐做法**:先用 \`agent_exec\` 获取用户主目录的绝对路径,再拼接完整路径。
116
116
 
117
- 6. **告知用户**:创建完成后,告知用户技能已就绪,说明触发关键词。建议用户在工具箱页面点击「重新加载」使技能生效。
117
+ 6. **告知用户**:创建完成后,技能会被自动加载并立即生效,告知用户技能已就绪、说明触发关键词即可,无需手动重新加载。
118
118
 
119
119
  ## 更新流程(已有技能)
120
120
 
@@ -125,7 +125,7 @@ enabled: true
125
125
  3. **分析更新需求**:结合当前对话上下文,识别需要更新的部分
126
126
  4. **合并内容**:保留原技能的核心结构,将新内容融入适当位置
127
127
  5. **覆盖写入**:使用 \`agent_file\`(action: write)覆盖写入 SKILL.md(路径规则同上,必须使用绝对路径)
128
- 6. **告知用户**:告知更新内容,建议重新加载
128
+ 6. **告知用户**:覆盖写入后技能会被自动重新加载并立即生效,告知用户更新内容即可,无需手动重新加载。
129
129
 
130
130
  ## 更新注意事项
131
131