ai-helper-agent 1.14.5 → 1.14.6
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 +50 -8
- package/package.json +1 -1
- package/src/server.js +34 -1
- package/src/skill/registry.js +60 -3
- package/src/skill/skill-creator-seed.js +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.14.6",
|
|
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;
|
package/src/skill/registry.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
254
|
-
if (!skill
|
|
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.
|
|
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
|
|