ai-helper-agent 1.14.4 → 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 CHANGED
@@ -1,11 +1,12 @@
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';
8
8
  import { fileURLToPath } from 'url';
9
+ import { detectSystemLang } from '../src/sys-lang.js';
9
10
 
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const AGENT_DIR = join(homedir(), '.ai-helper-agent');
@@ -25,7 +26,7 @@ function loadCliTranslations() {
25
26
  }
26
27
 
27
28
  function resolveLang(args) {
28
- // Priority: --lang flag > AI_HELPER_LANG env > default 'en'
29
+ // Priority: --lang flag > AI_HELPER_LANG env > system language detection > default 'en'
29
30
  for (let i = 0; i < args.length; i++) {
30
31
  if (args[i] === '--lang' && args[i + 1]) {
31
32
  const val = args[i + 1].toLowerCase();
@@ -34,15 +35,17 @@ function resolveLang(args) {
34
35
  }
35
36
  const envLang = process.env.AI_HELPER_LANG;
36
37
  if (envLang && (envLang === 'zh' || envLang === 'en')) return envLang;
37
- return 'en';
38
+ return detectSystemLang();
38
39
  }
39
40
 
40
- // Remove --lang <val> from args to avoid passing to server
41
+ // Remove --lang <val> and background flags from args to avoid passing to server
41
42
  function filterLangArgs(args) {
42
43
  const filtered = [];
43
44
  for (let i = 0; i < args.length; i++) {
44
45
  if (args[i] === '--lang' && args[i + 1]) {
45
46
  i++; // skip the value too
47
+ } else if (args[i] === '-b' || args[i] === '--background') {
48
+ // 过滤后台标志,避免子进程递归 fork 孙进程
46
49
  } else {
47
50
  filtered.push(args[i]);
48
51
  }
@@ -50,6 +53,29 @@ function filterLangArgs(args) {
50
53
  return filtered;
51
54
  }
52
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
+
53
79
  const lang = resolveLang(process.argv);
54
80
  process.env.AI_HELPER_LANG = lang;
55
81
  const translations = loadCliTranslations();
@@ -271,9 +297,11 @@ if (command === 'start') {
271
297
  process.exit(0);
272
298
  }
273
299
 
300
+ const agentScript = resolve(process.argv[1]);
301
+
274
302
  const child = spawn(process.execPath, [
275
303
  ...process.execArgv,
276
- process.argv[1],
304
+ agentScript,
277
305
  'start',
278
306
  ...passArgs
279
307
  ], {
@@ -292,9 +320,11 @@ if (command === 'start') {
292
320
  process.exit(1);
293
321
  });
294
322
 
295
- 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;
296
326
 
297
- if (!spawnFailed && child.pid) {
327
+ if (!spawnFailed && child.pid && healthy) {
298
328
  try {
299
329
  writeFileSync(PID_FILE, String(child.pid));
300
330
  } catch (err) {
@@ -305,6 +335,11 @@ if (command === 'start') {
305
335
  console.log(`[Agent] ${t('agentStartedBg', { pid: child.pid })}`);
306
336
  console.log(`[Agent] ${t('stopHint')}`);
307
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);
308
343
  }
309
344
 
310
345
  process.exit(spawnFailed ? 1 : 0);
@@ -397,9 +432,11 @@ if (command === 'start') {
397
432
  if (isBg) {
398
433
  ensureAgentDir();
399
434
 
435
+ const agentScript = resolve(process.argv[1]);
436
+
400
437
  const child = spawn(process.execPath, [
401
438
  ...process.execArgv,
402
- process.argv[1],
439
+ agentScript,
403
440
  'start',
404
441
  ...passArgs
405
442
  ], {
@@ -418,9 +455,11 @@ if (command === 'start') {
418
455
  process.exit(1);
419
456
  });
420
457
 
421
- 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;
422
461
 
423
- if (!spawnFailed && child.pid) {
462
+ if (!spawnFailed && child.pid && healthy) {
424
463
  try {
425
464
  writeFileSync(PID_FILE, String(child.pid));
426
465
  } catch (err) {
@@ -430,6 +469,10 @@ if (command === 'start') {
430
469
  child.unref();
431
470
  console.log(`[Agent] ${t('agentRestartedBg', { pid: child.pid })}`);
432
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);
433
476
  }
434
477
 
435
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.4",
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/auth.js CHANGED
@@ -2,9 +2,10 @@
2
2
  import crypto from 'crypto';
3
3
  import { loadConfig, loadPairings, savePairing } from './config.js';
4
4
  import { t as translate } from './i18n.js';
5
+ import { detectSystemLang } from './sys-lang.js';
5
6
 
6
- // 默认使用 zh 语言(独立调用场景);server.js 调用时会传入 req 的 lang
7
- let currentLang = 'zh';
7
+ // 默认跟随系统语言(独立调用场景);server.js 调用时会传入 req 的 lang
8
+ let currentLang = detectSystemLang();
8
9
 
9
10
  /**
10
11
  * 设置 auth 模块当前使用的语言(由 server.js 在请求入口处调用)
@@ -77,7 +78,7 @@ function startPairCodeRotation() {
77
78
 
78
79
  // Generate a pairing code immediately on startup (for initial pairing; lastAuthTime=0 ensures refresh)
79
80
  currentPairCode = generatePairCode();
80
- console.log(`\n[Agent] Pairing code: ${currentPairCode}\n`);
81
+ console.log(`\n[Agent] ${tr('auth.pairCode', { code: currentPairCode })}\n`);
81
82
 
82
83
  if (pairCodeTimer) clearInterval(pairCodeTimer);
83
84
  pairCodeTimer = setInterval(() => {
@@ -89,14 +90,14 @@ function startPairCodeRotation() {
89
90
  if (hasPairings && isRecentlyActive) {
90
91
  if (currentPairCode !== null) {
91
92
  currentPairCode = null;
92
- console.log('[Agent] Active connection detected, stopping pairing code rotation');
93
+ console.log(`[Agent] ${tr('auth.activeConnectionDetected')}`);
93
94
  }
94
95
  return;
95
96
  }
96
97
 
97
98
  // No pairing or extension offline timeout: rotate pairing code to allow new extension pairing
98
99
  currentPairCode = generatePairCode();
99
- console.log(`[Agent] Pairing code updated: ${currentPairCode}`);
100
+ console.log(`[Agent] ${tr('auth.pairCodeUpdated', { code: currentPairCode })}`);
100
101
  }, ttl);
101
102
  }
102
103
 
@@ -149,7 +150,7 @@ async function handlePairRequest(code, extensionId, tFn) {
149
150
  pairFailCount = 0;
150
151
  // 锁定时立即轮换配对码,使攻击者已观测到的配对码失效
151
152
  currentPairCode = generatePairCode();
152
- console.log('[Agent] Too many failed pairing attempts, locked for 60s and pairing code rotated');
153
+ console.log(`[Agent] ${tr('auth.tooManyFailedAttempts')}`);
153
154
  }
154
155
  return { success: false, error: tr('auth.invalidCode', undefined, tFn) };
155
156
  }
@@ -161,7 +162,7 @@ async function handlePairRequest(code, extensionId, tFn) {
161
162
  if (pairings[extensionId]) {
162
163
  // 一次性使用:成功后立即轮换配对码
163
164
  currentPairCode = generatePairCode();
164
- console.log('[Agent] Pairing code verified, code rotated (one-time use)');
165
+ console.log(`[Agent] ${tr('auth.codeVerifiedRotated')}`);
165
166
  return { success: true, token: pairings[extensionId].token, message: tr('auth.alreadyPaired', undefined, tFn) };
166
167
  }
167
168
  // 生成新 token 并保存
@@ -173,7 +174,7 @@ async function handlePairRequest(code, extensionId, tFn) {
173
174
  }
174
175
  // 一次性使用:成功后立即轮换配对码
175
176
  currentPairCode = generatePairCode();
176
- console.log('[Agent] Pairing successful, pairing code rotated (one-time use)');
177
+ console.log(`[Agent] ${tr('auth.pairSuccessRotated')}`);
177
178
  return { success: true, token, message: tr('auth.pairSuccess', undefined, tFn) };
178
179
  }
179
180
 
package/src/i18n.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { readFileSync } from 'fs';
6
6
  import { join, dirname } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
+ import { detectSystemLang } from './sys-lang.js';
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
 
@@ -14,8 +15,12 @@ const translations = {
14
15
  en: JSON.parse(readFileSync(join(__dirname, 'locales', 'en.json'), 'utf-8')),
15
16
  };
16
17
 
17
- // Default language: check env var (set by CLI), fallback to 'en'
18
- const DEFAULT_LANG = (process.env.AI_HELPER_LANG === 'zh') ? 'zh' : 'en';
18
+ // Default language: --lang / AI_HELPER_LANG env (set by CLI) > system language detection > 'en'
19
+ const DEFAULT_LANG = (() => {
20
+ const env = process.env.AI_HELPER_LANG;
21
+ if (env === 'zh' || env === 'en') return env;
22
+ return detectSystemLang();
23
+ })();
19
24
  const SUPPORTED_LANGS = Object.keys(translations);
20
25
 
21
26
  /**
@@ -15,7 +15,7 @@
15
15
  "optPort": " --port <端口> 设置监听端口 (默认: 18910)",
16
16
  "optHost": " --host <地址> 设置监听地址 (默认: 127.0.0.1)",
17
17
  "optWorkdir": " --workdir <目录> 设置工作目录 (默认: 当前目录)",
18
- "optLang": " --lang <语言> 设置界面语言 (zh/en, 默认: en)",
18
+ "optLang": " --lang <语言> 设置界面语言 (zh/en, 默认: 自动检测系统语言)",
19
19
  "examples": "示例:",
20
20
  "configReadFailed": "配置文件读取失败",
21
21
  "configDirCreateFailed": "无法创建配置目录",
@@ -82,7 +82,7 @@
82
82
  "optPort": " --port <port> Set listening port (default: 18910)",
83
83
  "optHost": " --host <address> Set listening address (default: 127.0.0.1)",
84
84
  "optWorkdir": " --workdir <dir> Set working directory (default: current dir)",
85
- "optLang": " --lang <lang> Set UI language (zh/en, default: en)",
85
+ "optLang": " --lang <lang> Set UI language (zh/en, default: detect system language)",
86
86
  "examples": "Examples:",
87
87
  "configReadFailed": "Failed to read config file",
88
88
  "configDirCreateFailed": "Cannot create config directory",
@@ -106,7 +106,13 @@
106
106
  "invalidCode": "Invalid pairing code",
107
107
  "alreadyPaired": "Already paired, returning existing token",
108
108
  "saveFailed": "Pairing save failed: {message}",
109
- "pairSuccess": "Pairing successful"
109
+ "pairSuccess": "Pairing successful",
110
+ "pairCode": "Pairing code: {code}",
111
+ "pairCodeUpdated": "Pairing code updated: {code}",
112
+ "activeConnectionDetected": "Active connection detected, stopping pairing code rotation",
113
+ "tooManyFailedAttempts": "Too many failed pairing attempts, locked for 60s and pairing code rotated",
114
+ "codeVerifiedRotated": "Pairing code verified, code rotated (one-time use)",
115
+ "pairSuccessRotated": "Pairing successful, pairing code rotated (one-time use)"
110
116
  },
111
117
  "exec": {
112
118
  "requestTimeout": "Request timeout ({ms}ms)",
@@ -149,7 +155,15 @@
149
155
  "entryNotFound": "Trash entry does not exist or has expired",
150
156
  "fileNotFound": "Trash file no longer exists",
151
157
  "originalPathOccupied": "Original location already has a file: {path}",
152
- "restoreFailed": "Restore failed: {message}"
158
+ "restoreFailed": "Restore failed: {message}",
159
+ "cannotCreateDir": "Cannot create trash directory: {message}",
160
+ "cleanedUp": "Cleaned up {count} expired trash entries",
161
+ "movedToTrashLog": "Moved to trash: {source} -> {target} ({size} bytes, {type})",
162
+ "typeDirectory": "directory",
163
+ "typeFile": "file",
164
+ "restoredLog": "Restored: {path}",
165
+ "periodicCleanStarted": "Periodic cleanup started (interval: 6 hours)",
166
+ "periodicCleanError": "Periodic cleanup error: {message}"
153
167
  },
154
168
  "search": {
155
169
  "pathNotExist": "Search path does not exist",
@@ -166,9 +180,24 @@
166
180
  "execError": "Execution error: {message}",
167
181
  "execIdNotFound": "Execution ID \"{execId}\" does not exist",
168
182
  "validationFailed": "Validation failed: {errors}",
183
+ "loadFailed": "Load failed: {message}",
184
+ "registered": "{count} registered ({workflow} Workflow + {agent} Agent), {enabled} enabled",
169
185
  "saveFailed": "Save failed: {message}",
170
186
  "deleteFailed": "Delete failed: {message}",
171
187
  "skillNotFound": "Skill \"{name}\" does not exist",
188
+ "skillNotExistOrDisabled": "Skill \"{name}\" does not exist or is not enabled",
189
+ "agentSkillNotRunnable": "\"{name}\" is an Agent Skill and cannot be executed directly. Agent Skills are called autonomously by the AI during conversations based on the SKILL.md description.",
190
+ "skillNotAgentSkill": "Skill \"{name}\" does not exist or is not an Agent Skill",
191
+ "builtinNotDeletable": "\"{name}\" is a built-in skill and cannot be deleted",
192
+ "skillPromptHeader": "## Available Skills",
193
+ "skillPromptIntro": "The following Agent Skills (AI capability extensions) are available. When a user request closely matches a skill, use the `agent_skill` tool (action: load) to load the full instructions for that skill, then complete the task autonomously following the instructions.",
194
+ "skillPromptStrategy": "**Usage strategy**:",
195
+ "skillPromptStrategy1": "- Only load a skill when the user request clearly matches its description; do not load skills for general programming/technical questions",
196
+ "skillPromptStrategy2": "- Load only one skill at a time, then follow the SKILL.md instructions after loading",
197
+ "skillPromptStrategy3": "- Loading skills consumes significant tokens, avoid loading unnecessarily",
198
+ "skillPromptDesc": "**Description**: {description}",
199
+ "skillPromptResources": "**Available resources**: {resources}",
200
+ "skillPromptWorkdir": "[Skill working directory: {path}]\n\n{prompt}",
172
201
  "missingNameField": "Missing name field",
173
202
  "missingPromptContent": "Missing prompt content",
174
203
  "missingDescriptionField": "Missing description field",
@@ -209,7 +238,11 @@
209
238
  "stdioRequiresCommand": "stdio transport requires a command",
210
239
  "toolCallError": "Tool call error: {message}",
211
240
  "serverNotConnected": "MCP Server \"{name}\" not connected",
212
- "toolCallFailed": "Tool call failed"
241
+ "serverNotFound": "MCP Server \"{name}\" does not exist",
242
+ "toolCallFailed": "Tool call failed",
243
+ "serversConnected": "{count} MCP servers connected",
244
+ "someFailed": "{connected}/{total} connected, {failed} failed: {list}",
245
+ "callingTool": "Calling tool: {serverId}/{toolName}"
213
246
  },
214
247
  "server": {
215
248
  "requestException": "Request processing error",
@@ -106,7 +106,13 @@
106
106
  "invalidCode": "配对码无效",
107
107
  "alreadyPaired": "已配对,返回现有 token",
108
108
  "saveFailed": "配对保存失败: {message}",
109
- "pairSuccess": "配对成功"
109
+ "pairSuccess": "配对成功",
110
+ "pairCode": "配对码: {code}",
111
+ "pairCodeUpdated": "配对码已更新: {code}",
112
+ "activeConnectionDetected": "检测到活跃连接,停止配对码轮换",
113
+ "tooManyFailedAttempts": "配对失败次数过多,已锁定 60 秒并轮换配对码",
114
+ "codeVerifiedRotated": "配对码验证成功,配对码已轮换(一次性使用)",
115
+ "pairSuccessRotated": "配对成功,配对码已轮换(一次性使用)"
110
116
  },
111
117
  "exec": {
112
118
  "requestTimeout": "请求超时 ({ms}ms)",
@@ -149,7 +155,15 @@
149
155
  "entryNotFound": "回收站条目不存在或已过期",
150
156
  "fileNotFound": "回收站文件已不存在",
151
157
  "originalPathOccupied": "原位置已有文件: {path}",
152
- "restoreFailed": "恢复失败: {message}"
158
+ "restoreFailed": "恢复失败: {message}",
159
+ "cannotCreateDir": "无法创建回收站目录: {message}",
160
+ "cleanedUp": "已清理 {count} 个过期的回收站条目",
161
+ "movedToTrashLog": "已移至回收站: {source} -> {target} ({size} 字节, {type})",
162
+ "typeDirectory": "目录",
163
+ "typeFile": "文件",
164
+ "restoredLog": "已恢复: {path}",
165
+ "periodicCleanStarted": "定期清理已启动(间隔: 6 小时)",
166
+ "periodicCleanError": "定期清理异常: {message}"
153
167
  },
154
168
  "search": {
155
169
  "pathNotExist": "搜索路径不存在",
@@ -166,9 +180,24 @@
166
180
  "execError": "执行异常: {message}",
167
181
  "execIdNotFound": "执行 ID \"{execId}\" 不存在",
168
182
  "validationFailed": "校验失败: {errors}",
183
+ "loadFailed": "加载失败: {message}",
184
+ "registered": "已注册 {count} 个技能({workflow} 个 Workflow + {agent} 个 Agent),启用 {enabled} 个",
169
185
  "saveFailed": "保存失败: {message}",
170
186
  "deleteFailed": "删除失败: {message}",
171
187
  "skillNotFound": "Skill \"{name}\" 不存在",
188
+ "skillNotExistOrDisabled": "Skill \"{name}\" 不存在或未启用",
189
+ "agentSkillNotRunnable": "\"{name}\" 是 Agent Skill,无法直接执行。Agent Skill 由 AI 在对话中根据 SKILL.md 描述自主调用。",
190
+ "skillNotAgentSkill": "Skill \"{name}\" 不存在或不是 Agent Skill",
191
+ "builtinNotDeletable": "\"{name}\" 是内置技能,不可删除",
192
+ "skillPromptHeader": "## 可用技能 (Skills)",
193
+ "skillPromptIntro": "以下是可用的 Agent Skill(AI 能力扩展)列表。当用户需求与某个技能高度匹配时,使用 `agent_skill` 工具(action: load)加载该技能的完整说明,然后根据说明自主完成任务。",
194
+ "skillPromptStrategy": "**使用策略**:",
195
+ "skillPromptStrategy1": "- 仅在用户需求明确匹配技能描述时才加载,不要为一般性的编程/技术问题加载技能",
196
+ "skillPromptStrategy2": "- 一次只加载一个技能,加载后根据 SKILL.md 说明执行",
197
+ "skillPromptStrategy3": "- 技能加载会消耗较多 token,避免无故加载",
198
+ "skillPromptDesc": "**描述**: {description}",
199
+ "skillPromptResources": "**可用资源**: {resources}",
200
+ "skillPromptWorkdir": "[技能工作目录: {path}]\n\n{prompt}",
172
201
  "missingNameField": "缺少 name 字段",
173
202
  "missingPromptContent": "缺少 prompt 内容",
174
203
  "missingDescriptionField": "缺少 description 字段",
@@ -209,7 +238,11 @@
209
238
  "stdioRequiresCommand": "stdio 传输需要提供 command",
210
239
  "toolCallError": "工具调用异常: {message}",
211
240
  "serverNotConnected": "MCP Server \"{name}\" 未连接",
212
- "toolCallFailed": "工具调用失败"
241
+ "serverNotFound": "MCP Server \"{name}\" 不存在",
242
+ "toolCallFailed": "工具调用失败",
243
+ "serversConnected": "{count} 个 MCP 服务器已连接",
244
+ "someFailed": "已连接 {connected}/{total} 个服务器,{failed} 个失败: {list}",
245
+ "callingTool": "调用工具: {serverId}/{toolName}"
213
246
  },
214
247
  "server": {
215
248
  "requestException": "请求处理异常",
package/src/logger.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { appendFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync, readFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { homedir } from 'os';
5
+ import { detectSystemLang } from './sys-lang.js';
5
6
 
6
7
  const LOG_DIR = join(homedir(), '.ai-helper-agent', 'logs');
7
8
  const MAX_LOG_FILES = 30; // Max log files to retain
@@ -28,7 +29,11 @@ const CATEGORY_LABELS_EN = {
28
29
  auth: 'Auth', fs: 'File', exec: 'Exec', security: 'Security', system: 'System'
29
30
  };
30
31
 
31
- let currentLocale = (process.env.AI_HELPER_LANG === 'zh') ? 'zh' : 'en';
32
+ let currentLocale = (() => {
33
+ const env = process.env.AI_HELPER_LANG;
34
+ if (env === 'zh' || env === 'en') return env;
35
+ return detectSystemLang();
36
+ })();
32
37
 
33
38
  /**
34
39
  * Set locale for log category labels
@@ -3,6 +3,10 @@
3
3
  import { readFileSync, writeFileSync, existsSync, statSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { homedir } from 'os';
6
+ import { t as translate, parseAcceptLanguage } from '../i18n.js';
7
+
8
+ const LANG = parseAcceptLanguage();
9
+ const T = (key, params) => translate(LANG, key, params);
6
10
 
7
11
  const AGENT_DIR = join(homedir(), '.ai-helper-agent');
8
12
  const MCP_CONFIG_FILE = join(AGENT_DIR, 'mcp_servers.json');
@@ -100,7 +104,7 @@ export async function removeMcpServer(serverId) {
100
104
  const config = loadMcpConfig();
101
105
  const idx = config.servers.findIndex(s => s.id === serverId);
102
106
  if (idx < 0) {
103
- return { success: false, error: `MCP Server "${serverId}" 不存在` };
107
+ return { success: false, error: T('mcp.serverNotFound', { name: serverId }) };
104
108
  }
105
109
  config.servers.splice(idx, 1);
106
110
  await saveMcpConfig(config);
@@ -114,7 +118,7 @@ export async function toggleMcpServer(serverId, enabled) {
114
118
  const config = loadMcpConfig();
115
119
  const server = config.servers.find(s => s.id === serverId);
116
120
  if (!server) {
117
- return { success: false, error: `MCP Server "${serverId}" 不存在` };
121
+ return { success: false, error: T('mcp.serverNotFound', { name: serverId }) };
118
122
  }
119
123
  server.enabled = enabled;
120
124
  await saveMcpConfig(config);
@@ -2,6 +2,19 @@
2
2
  // 管理所有 MCP Client 实例,提供统一的工具发现和调用接口
3
3
  import { McpClient } from './client.js';
4
4
  import { loadMcpConfig, addMcpServer, removeMcpServer, toggleMcpServer } from './mcp-config.js';
5
+ import { t as translate, parseAcceptLanguage } from '../i18n.js';
6
+
7
+ // 默认跟随系统语言;server.js 启动时可用 setMcpRegistryLang 同步 serverLang
8
+ let LANG = parseAcceptLanguage();
9
+ const T = (key, params) => translate(LANG, key, params);
10
+
11
+ /**
12
+ * 设置 MCP 注册表当前使用的语言(由 server.js 在启动时调用)
13
+ * @param {string} lang - 语言代码('zh' | 'en')
14
+ */
15
+ export function setMcpRegistryLang(lang) {
16
+ if (lang) LANG = lang;
17
+ }
5
18
 
6
19
  // 已连接的 MCP Client 映射表: serverId → McpClient
7
20
  const clients = new Map();
@@ -33,9 +46,9 @@ export async function initializeMcpRegistry() {
33
46
  if (config.servers.length > 0) {
34
47
  if (failed > 0) {
35
48
  const failedList = failedServers.map(fs => `${fs.id}(${fs.error})`).join(', ');
36
- console.warn(`[MCP] ${connected}/${config.servers.length} connected, ${failed} failed: ${failedList}`);
49
+ console.warn(`[MCP] ${T('mcp.someFailed', { connected, total: config.servers.length, failed, list: failedList })}`);
37
50
  } else if (connected > 0) {
38
- console.log(`[MCP] ${connected} servers connected`);
51
+ console.log(`[MCP] ${T('mcp.serversConnected', { count: connected })}`);
39
52
  }
40
53
  }
41
54
  return { connected, failed };
@@ -103,13 +116,13 @@ export function getMcpTools(serverId) {
103
116
  export async function callMcpTool(serverId, toolName, args) {
104
117
  const client = clients.get(serverId);
105
118
  if (!client) {
106
- return { success: false, error: `MCP Server "${serverId}" 不存在` };
119
+ return { success: false, error: T('mcp.serverNotFound', { name: serverId }) };
107
120
  }
108
121
  if (!client.connected) {
109
- return { success: false, error: `MCP Server "${serverId}" 未连接` };
122
+ return { success: false, error: T('mcp.serverNotConnected', { name: serverId }) };
110
123
  }
111
124
 
112
- console.log(`[MCP Registry] Calling tool: ${serverId}/${toolName}`, args);
125
+ console.log(`[MCP Registry] ${T('mcp.callingTool', { serverId, toolName })}`, args);
113
126
  const result = await client.callTool(toolName, args);
114
127
  return result;
115
128
  }
@@ -121,7 +134,7 @@ export async function connectMcpServer(serverId) {
121
134
  const config = loadMcpConfig();
122
135
  const server = config.servers.find(s => s.id === serverId);
123
136
  if (!server) {
124
- return { success: false, error: `MCP Server "${serverId}" 不存在` };
137
+ return { success: false, error: T('mcp.serverNotFound', { name: serverId }) };
125
138
  }
126
139
 
127
140
  // 先断开已有连接
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';
@@ -15,7 +15,7 @@ import { verifyToken, startPairCodeRotation, stopPairCodeRotation, handlePairReq
15
15
  import { checkPath, checkCommand, normalizePathFormat, setSecurityLang } from './security.js';
16
16
  import { moveToTrash, restoreFromTrash, listTrash, startPeriodicCleanup, stopPeriodicCleanup, setTrashLang } from './trash.js';
17
17
  import { executeCommand, executeCommandSync, addWsClient, disconnectWsClient, killProcess, getRunningProcesses, setExecutorLang } from './executor.js';
18
- import { setConsoleOutput, logAuth, logFs, logExec, logSecurity, logSystem, logError, queryLogs, getLogDates } from './logger.js';
18
+ import { setConsoleOutput, setLoggerLocale, logAuth, logFs, logExec, logSecurity, logSystem, logError, queryLogs, getLogDates } from './logger.js';
19
19
  import { initSearchTools, getSearchToolsAvailable, searchFiles, searchContent, setSearchLang } from './search.js';
20
20
  import {
21
21
  initializeMcpRegistry,
@@ -28,7 +28,8 @@ import {
28
28
  loadMcpConfig,
29
29
  addMcpServer,
30
30
  removeMcpServer,
31
- toggleMcpServer
31
+ toggleMcpServer,
32
+ setMcpRegistryLang
32
33
  } from './mcp/registry.js';
33
34
  import {
34
35
  initializeSkillRegistry,
@@ -41,7 +42,8 @@ import {
41
42
  reloadSkills,
42
43
  getAgentSkillPrompts,
43
44
  getAgentSkillPrompt,
44
- getSkillsDir
45
+ getSkillsDir,
46
+ setSkillRegistryLang
45
47
  } from './skill/registry.js';
46
48
  import { saveMarkdownSkill, importMarkdownSkillFromZip, importMarkdownSkillFromUrl, setSkillLoaderLang } from './skill/loader.js';
47
49
  import { setSkillExecutorLang } from './skill/executor.js';
@@ -842,7 +844,7 @@ export function startServer() {
842
844
  // 禁止设为 Agent 系统目录本身(防敏感文件暴露)
843
845
  const AGENT_DIR_PATH = join(homedir(), '.ai-helper-agent');
844
846
  if (resolvedWorkdir === AGENT_DIR_PATH) {
845
- logSecurity('workdir_switch_blocked', { path: resolvedWorkdir, reason: '禁止设为 Agent 系统目录' });
847
+ logSecurity('workdir_switch_blocked', { path: resolvedWorkdir, reason: t('error.workdirIsSystemDir') });
846
848
  return jsonResponse(res, 403, { success: false, error: t('error.workdirIsSystemDir') });
847
849
  }
848
850
 
@@ -2036,6 +2038,22 @@ export function startServer() {
2036
2038
  server.listen(port, host, () => {
2037
2039
  console.log(`[Agent] ${ln('httpStarted', { host, port })}`);
2038
2040
  console.log(`[Agent] ${ln('wsStarted', { host, port })}`);
2041
+
2042
+ // 同步 server 级语言到各子模块,确保启动日志语言一致(--lang / AI_HELPER_LANG / 系统检测)
2043
+ setLoggerLocale(serverLang);
2044
+ setAuthLang(serverLang);
2045
+ setTrashLang(serverLang);
2046
+ setConfigLang(serverLang);
2047
+ setExecutorLang(serverLang);
2048
+ setSecurityLang(serverLang);
2049
+ setSearchLang(serverLang);
2050
+ setSkillLoaderLang(serverLang);
2051
+ setSkillRegistryLang(serverLang);
2052
+ setSkillExecutorLang(serverLang);
2053
+ setMarkdownLoaderLang(serverLang);
2054
+ setMcpRegistryLang(serverLang);
2055
+ setMcpClientLang(serverLang);
2056
+
2039
2057
  startPairCodeRotation();
2040
2058
 
2041
2059
  // 启动回收站定期清理(每6小时)+ 启动时清理一次过期文件
@@ -2051,12 +2069,45 @@ export function startServer() {
2051
2069
 
2052
2070
  try {
2053
2071
  await initializeSkillRegistry();
2072
+ startSkillDirWatcher();
2054
2073
  } catch (err) {
2055
2074
  console.error(`[Agent] ${ln('skillInitFailed')}:`, err.message);
2056
2075
  }
2057
2076
  })();
2058
2077
  });
2059
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
+
2060
2111
  // 优雅关闭(异步 + 防并发 + 超时兜底)
2061
2112
  async function shutdown() {
2062
2113
  if (shuttingDown) return;
@@ -7,9 +7,10 @@ import { homedir } from 'os';
7
7
  import { loadAllMarkdownSkills, saveMarkdownSkill, deleteMarkdownSkillDir, importMarkdownSkillFromZip, importMarkdownSkillFromUrl } from './markdown-loader.js';
8
8
  import { SKILL_CREATOR_SEED_MD, SKILL_CREATOR_DIR } from './skill-creator-seed.js';
9
9
  import { t as translate } from '../i18n.js';
10
+ import { detectSystemLang } from '../sys-lang.js';
10
11
 
11
- // 当前模块使用的语言(由 server.js 在请求入口处设置)
12
- let currentLang = 'en';
12
+ // 当前模块使用的语言(默认跟随系统语言;server.js 在请求入口处设置)
13
+ let currentLang = detectSystemLang();
13
14
 
14
15
  /**
15
16
  * 设置 skill loader 模块当前使用的语言(由 server.js 在请求入口处调用)
@@ -151,7 +152,7 @@ function loadSkillFile(filePath) {
151
152
 
152
153
  const validation = validateWorkflowSkill(skill);
153
154
  if (!validation.valid) {
154
- console.warn(`[Skill Loader] "${filePath}" validation failed:`, validation.errors.join(', '));
155
+ console.warn(`[Skill Loader] "${filePath}" ${tr('skill.validationFailed', { errors: validation.errors.join(', ') })}`);
155
156
  return null;
156
157
  }
157
158
 
@@ -160,7 +161,7 @@ function loadSkillFile(filePath) {
160
161
  skill.type = 'workflow';
161
162
  return skill;
162
163
  } catch (err) {
163
- console.warn(`[Skill Loader] Load "${filePath}" failed:`, err.message);
164
+ console.warn(`[Skill Loader] "${filePath}" ${tr('skill.loadFailed', { message: err.message })}`);
164
165
  return null;
165
166
  }
166
167
  }
@@ -1,9 +1,8 @@
1
1
  // skill/markdown-loader.js - Markdown Skill 加载器
2
2
  // 扫描子目录中的 SKILL.md,解析 YAML frontmatter 和正文内容
3
3
  import { readFileSync, readdirSync, existsSync, statSync, writeFileSync, mkdirSync, rmSync, cpSync } from 'fs';
4
- import { join, basename, extname, normalize, sep } from 'path';
5
- import { execFile } from 'child_process';
6
- import { promisify } from 'util';
4
+ import { join, basename, extname, normalize, sep, dirname } from 'path';
5
+ import { inflateRawSync } from 'zlib';
7
6
  import { randomBytes } from 'crypto';
8
7
  import { tmpdir } from 'os';
9
8
  import { lookup as dnsLookup } from 'dns/promises';
@@ -11,8 +10,6 @@ import https from 'https';
11
10
  import http from 'http';
12
11
  import { t as translate } from '../i18n.js';
13
12
 
14
- const execFileAsync = promisify(execFile);
15
-
16
13
  // 当前模块使用的语言(由 server.js 在请求入口处设置)
17
14
  let currentLang = 'zh';
18
15
 
@@ -365,6 +362,96 @@ function sanitizeSkillName(name) {
365
362
  return name;
366
363
  }
367
364
 
365
+ /**
366
+ * 纯 JavaScript ZIP 解压,替换外部命令(powershell/unzip),消除 spawn EPERM 问题
367
+ * 支持 stored(无压缩)和 deflate(压缩方法 8)两种方式
368
+ * 内置路径穿越安全校验
369
+ * @param {Buffer} zipBuffer - ZIP 文件内容
370
+ * @param {string} destDir - 解压目标目录
371
+ * @throws {Error} 解压或安全校验失败时抛出
372
+ */
373
+ function extractZipPureJs(zipBuffer, destDir) {
374
+ const EOCD_SIG = 0x06054b50;
375
+ const CD_SIG = 0x02014b50;
376
+ const LFH_SIG = 0x04034b50;
377
+
378
+ // 1. 从末尾反向扫描 EOCD(End of Central Directory)
379
+ let eocdOffset = -1;
380
+ const maxComment = Math.min(65535, zipBuffer.length - 22);
381
+ for (let i = zipBuffer.length - 22; i >= Math.max(0, zipBuffer.length - 22 - maxComment); i--) {
382
+ if (zipBuffer.readUInt32LE(i) === EOCD_SIG) {
383
+ eocdOffset = i;
384
+ break;
385
+ }
386
+ }
387
+ if (eocdOffset === -1) throw new Error('Invalid ZIP: EOCD not found');
388
+
389
+ // 2. 读取 Central Directory 元信息
390
+ const cdOffset = zipBuffer.readUInt32LE(eocdOffset + 16);
391
+ const cdSize = zipBuffer.readUInt32LE(eocdOffset + 12);
392
+ const totalEntries = zipBuffer.readUInt16LE(eocdOffset + 8);
393
+
394
+ // 3. 解析 Central Directory 条目,收集文件信息
395
+ let cdPos = cdOffset;
396
+ const entries = [];
397
+
398
+ for (let i = 0; i < totalEntries; i++) {
399
+ if (zipBuffer.readUInt32LE(cdPos) !== CD_SIG) throw new Error('Invalid central directory entry');
400
+
401
+ const compressionMethod = zipBuffer.readUInt16LE(cdPos + 10);
402
+ const compressedSize = zipBuffer.readUInt32LE(cdPos + 20);
403
+ const uncompressedSize = zipBuffer.readUInt32LE(cdPos + 24);
404
+ const fileNameLen = zipBuffer.readUInt16LE(cdPos + 28);
405
+ const extraLen = zipBuffer.readUInt16LE(cdPos + 30);
406
+ const commentLen = zipBuffer.readUInt16LE(cdPos + 32);
407
+ const localHeaderOffset = zipBuffer.readUInt32LE(cdPos + 42);
408
+
409
+ const fileName = zipBuffer.slice(cdPos + 46, cdPos + 46 + fileNameLen).toString('utf8');
410
+
411
+ entries.push({ fileName, compressionMethod, localHeaderOffset, compressedSize, uncompressedSize });
412
+
413
+ cdPos += 46 + fileNameLen + extraLen + commentLen;
414
+ }
415
+
416
+ // 4. 逐文件解压并写入磁盘
417
+ for (const entry of entries) {
418
+ // 跳过目录条目(以 / 结尾)
419
+ if (entry.fileName.endsWith('/') || entry.fileName.endsWith('\\')) continue;
420
+
421
+ // 路径穿越安全校验:拒绝 .. 或绝对路径
422
+ const normalizedName = normalize(entry.fileName).replace(/^[/\\]+/, '');
423
+ if (normalizedName.startsWith('..') || entry.fileName.startsWith('/')) {
424
+ throw new Error(`Path traversal detected: ${entry.fileName}`);
425
+ }
426
+
427
+ // 读取本地文件头(Local File Header)
428
+ const lfh = entry.localHeaderOffset;
429
+ if (zipBuffer.readUInt32LE(lfh) !== LFH_SIG) throw new Error(`Invalid local file header for: ${entry.fileName}`);
430
+
431
+ const lfhFileNameLen = zipBuffer.readUInt16LE(lfh + 26);
432
+ const lfhExtraLen = zipBuffer.readUInt16LE(lfh + 28);
433
+ const dataOffset = lfh + 30 + lfhFileNameLen + lfhExtraLen;
434
+
435
+ // 根据压缩方法提取数据
436
+ let fileData;
437
+ if (entry.compressionMethod === 0) {
438
+ // Stored(无压缩)
439
+ fileData = zipBuffer.slice(dataOffset, dataOffset + entry.uncompressedSize);
440
+ } else if (entry.compressionMethod === 8) {
441
+ // Deflate 压缩(zlib inflateRawSync 可直接解压)
442
+ const compressed = zipBuffer.slice(dataOffset, dataOffset + entry.compressedSize);
443
+ fileData = inflateRawSync(compressed);
444
+ } else {
445
+ throw new Error(`Unsupported compression method ${entry.compressionMethod} for: ${entry.fileName}`);
446
+ }
447
+
448
+ // 写入目标文件
449
+ const destPath = join(destDir, normalizedName);
450
+ mkdirSync(dirname(destPath), { recursive: true });
451
+ writeFileSync(destPath, fileData);
452
+ }
453
+ }
454
+
368
455
  /**
369
456
  * 从 Zip 文件导入 Skill
370
457
  * 要求 zip 包内有一个顶层目录,目录中包含 SKILL.md
@@ -375,24 +462,12 @@ function sanitizeSkillName(name) {
375
462
  */
376
463
  export async function importMarkdownSkillFromZip(skillsDir, zipBuffer, skillName) {
377
464
  const tmpDir = join(tmpdir(), `ai-helper-skill-import-${Date.now()}-${randomBytes(4).toString('hex')}`);
378
- const tmpZip = join(tmpdir(), `ai-helper-skill-${Date.now()}-${randomBytes(4).toString('hex')}.zip`);
379
465
 
380
466
  try {
381
467
  mkdirSync(tmpDir, { recursive: true });
382
- writeFileSync(tmpZip, zipBuffer);
383
-
384
- // 解压到临时目录(跨平台:Windows 用 PowerShell Expand-Archive,Unix 用 unzip)
385
- if (process.platform === 'win32') {
386
- // PowerShell 单引号转义:路径中的单引号用两个单引号表示(tmpZip/tmpDir 由程序生成,正常不含单引号)
387
- const esc = (s) => s.replace(/'/g, "''");
388
- await execFileAsync('powershell.exe',
389
- ['-NoProfile', '-NonInteractive', '-Command',
390
- `Expand-Archive -LiteralPath '${esc(tmpZip)}' -DestinationPath '${esc(tmpDir)}' -Force`],
391
- { timeout: 30000, windowsHide: true });
392
- } else {
393
- // execFile 数组参数,不经 shell,杜绝命令注入
394
- await execFileAsync('unzip', ['-o', tmpZip, '-d', tmpDir], { timeout: 30000, windowsHide: true });
395
- }
468
+
469
+ // 纯 JavaScript ZIP 解压(无需外部命令,消除 spawn EPERM)
470
+ extractZipPureJs(zipBuffer, tmpDir);
396
471
 
397
472
  // 查找 SKILL.md
398
473
  const entries = readdirSync(tmpDir);
@@ -445,7 +520,7 @@ export async function importMarkdownSkillFromZip(skillsDir, zipBuffer, skillName
445
520
  rmSync(destDir, { recursive: true, force: true });
446
521
  }
447
522
 
448
- // 复制(跨平台:fs.cpSync 替代 cp -r,Windows 无 cp 命令;destDir 已被 rmSync 清空)
523
+ // 复制(跨平台:fs.cpSync 替代 cp -r,Windows 无 cp 命令)
449
524
  cpSync(skillDir, destDir, { recursive: true });
450
525
 
451
526
  // 重新加载
@@ -457,7 +532,6 @@ export async function importMarkdownSkillFromZip(skillsDir, zipBuffer, skillName
457
532
  // 清理临时文件
458
533
  try {
459
534
  rmSync(tmpDir, { recursive: true, force: true });
460
- if (existsSync(tmpZip)) rmSync(tmpZip);
461
535
  } catch { /* ignore cleanup errors */ }
462
536
  }
463
537
  }
@@ -5,10 +5,70 @@ import { executeSkill } from './executor.js';
5
5
  import { SKILLS_DIR } from './loader.js';
6
6
  import { readFileSync, writeFileSync, existsSync } from 'fs';
7
7
  import { join, basename, dirname } from 'path';
8
+ import { t as translate, parseAcceptLanguage } from '../i18n.js';
9
+
10
+ // 默认跟随系统语言;server.js 启动时可用 setSkillRegistryLang 同步 serverLang
11
+ let LANG = parseAcceptLanguage();
12
+ const T = (key, params) => translate(LANG, key, params);
13
+
14
+ /**
15
+ * 设置 Skill 注册表当前使用的语言(由 server.js 在启动时调用)
16
+ * @param {string} lang - 语言代码('zh' | 'en')
17
+ */
18
+ export function setSkillRegistryLang(lang) {
19
+ if (lang) LANG = lang;
20
+ }
8
21
 
9
22
  // Skill 映射表: name → skill定义
10
23
  const skills = new Map();
11
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
+
12
72
  // 内置技能状态持久化文件
13
73
  const BUILTIN_STATE_FILE = join(dirname(SKILLS_DIR), 'builtin_skills_state.json');
14
74
 
@@ -37,7 +97,7 @@ function saveDisabledState(disabledSet) {
37
97
  try {
38
98
  writeFileSync(DISABLED_STATE_FILE, JSON.stringify({ disabled: [...disabledSet] }, null, 2), 'utf-8');
39
99
  } catch (e) {
40
- console.error('[Skill Registry] Cannot save disabled state:', e.message);
100
+ console.error(`[Skill Registry] ${T('skill.saveFailed', { message: e.message })}`);
41
101
  }
42
102
  }
43
103
 
@@ -62,7 +122,7 @@ function saveBuiltinState(state) {
62
122
  try {
63
123
  writeFileSync(BUILTIN_STATE_FILE, JSON.stringify(state, null, 2), 'utf-8');
64
124
  } catch (e) {
65
- console.error('[Skill Registry] Cannot save builtin skill state:', e.message);
125
+ console.error(`[Skill Registry] ${T('skill.saveFailed', { message: e.message })}`);
66
126
  }
67
127
  }
68
128
 
@@ -123,7 +183,7 @@ export function initializeSkillRegistry() {
123
183
  const enabledCount = [...skills.values()].filter(s => s.enabled !== false).length;
124
184
 
125
185
  if (skills.size > 0) {
126
- console.log(`[Skill] ${skills.size} registered (${workflowCount} Workflow + ${agentCount} Agent), ${enabledCount} enabled`);
186
+ console.log(`[Skill] ${T('skill.registered', { count: skills.size, workflow: workflowCount, agent: agentCount, enabled: enabledCount })}`);
127
187
  }
128
188
  return skills.size;
129
189
  }
@@ -154,6 +214,10 @@ export function getSkillList(type) {
154
214
  } else if (skill.type === 'agent') {
155
215
  item.resourceCount = skill.resources?.length || 0;
156
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
+ }
157
221
  }
158
222
 
159
223
  list.push(item);
@@ -181,7 +245,7 @@ export function getSkill(name) {
181
245
  export async function runSkill(name, params = {}, onStepUpdate) {
182
246
  const skill = skills.get(name);
183
247
  if (!skill) {
184
- return { success: false, error: `Skill "${name}" 不存在或未启用` };
248
+ return { success: false, error: T('skill.skillNotExistOrDisabled', { name }) };
185
249
  }
186
250
 
187
251
  if (skill.type === 'agent') {
@@ -208,22 +272,28 @@ export function getAgentSkillPrompts() {
208
272
  if (agentSkills.length === 0) return '';
209
273
 
210
274
  const parts = [];
211
- parts.push('## 可用技能 (Skills)');
275
+ parts.push(T('skill.skillPromptHeader'));
212
276
  parts.push('');
213
- parts.push('以下是可用的 Agent Skill(AI 能力扩展)列表。当用户需求与某个技能高度匹配时,使用 `agent_skill` 工具(action: load)加载该技能的完整说明,然后根据说明自主完成任务。');
277
+ parts.push(T('skill.skillPromptIntro'));
214
278
  parts.push('');
215
- parts.push('**使用策略**:');
216
- parts.push('- 仅在用户需求明确匹配技能描述时才加载,不要为一般性的编程/技术问题加载技能');
217
- parts.push('- 一次只加载一个技能,加载后根据 SKILL.md 说明执行');
218
- parts.push('- 技能加载会消耗较多 token,避免无故加载');
279
+ parts.push(T('skill.skillPromptStrategy'));
280
+ parts.push(T('skill.skillPromptStrategy1'));
281
+ parts.push(T('skill.skillPromptStrategy2'));
282
+ parts.push(T('skill.skillPromptStrategy3'));
219
283
  parts.push('');
220
284
 
221
285
  for (const skill of agentSkills) {
222
- parts.push(`### ${skill.name}`);
223
- parts.push(`**描述**: ${skill.description}`);
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
+ }
293
+ parts.push(T('skill.skillPromptDesc', { description: skill.description }));
224
294
  if (skill.resources && skill.resources.length > 0) {
225
295
  const resourceNames = skill.resources.map(r => `\`${r.name}\``).join(', ');
226
- parts.push(`**可用资源**: ${resourceNames}`);
296
+ parts.push(T('skill.skillPromptResources', { resources: resourceNames }));
227
297
  }
228
298
  parts.push('');
229
299
  }
@@ -237,14 +307,14 @@ export function getAgentSkillPrompts() {
237
307
  * @returns {{ success: boolean, error?: string, skill?: Object, prompt?: string }}
238
308
  */
239
309
  export function getAgentSkillPrompt(name) {
240
- const skill = skills.get(name);
241
- if (!skill || skill.type !== 'agent') {
310
+ const skill = findAgentSkillByName(name);
311
+ if (!skill) {
242
312
  return { success: false, error: `Skill "${name}" 不存在或不是 Agent Skill` };
243
313
  }
244
314
  // 如果 skill 有 dirPath,在 prompt 前注入工作目录信息,方便 AI 解析相对路径引用
245
315
  let prompt = skill.fullPrompt || skill.prompt || '';
246
316
  if (skill.dirPath) {
247
- prompt = `[技能工作目录: ${skill.dirPath}]\n\n${prompt}`;
317
+ prompt = T('skill.skillPromptWorkdir', { path: skill.dirPath, prompt });
248
318
  }
249
319
  return {
250
320
  success: true,
@@ -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
 
@@ -0,0 +1,29 @@
1
+ // agent/src/sys-lang.js - Detect system language from the local environment
2
+ // Used as the final fallback when no explicit --lang flag or AI_HELPER_LANG env is set.
3
+ // This module is intentionally side-effect free so it can be imported before
4
+ // bin/agent.js sets process.env.AI_HELPER_LANG.
5
+ // Returns 'zh' or 'en'.
6
+
7
+ /**
8
+ * Detect system language from environment signals.
9
+ * Priority:
10
+ * 1. LANGUAGE / LC_ALL / LC_MESSAGES / LANG env vars (Unix-like: macOS / Linux)
11
+ * 2. Intl runtime default locale (fallback, useful on Windows)
12
+ * @returns {'zh'|'en'}
13
+ */
14
+ export function detectSystemLang() {
15
+ for (const envName of ['LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG']) {
16
+ const val = process.env[envName];
17
+ if (!val) continue;
18
+ if (/^zh/i.test(val)) return 'zh';
19
+ if (/^en/i.test(val)) return 'en';
20
+ }
21
+ try {
22
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale || '';
23
+ if (/^zh/i.test(locale)) return 'zh';
24
+ if (/^en/i.test(locale)) return 'en';
25
+ } catch {
26
+ // Intl unavailable, fall through to 'en'
27
+ }
28
+ return 'en';
29
+ }
package/src/trash.js CHANGED
@@ -5,9 +5,10 @@ import { join, basename } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { existsSync } from 'fs';
7
7
  import { t as translate } from './i18n.js';
8
+ import { detectSystemLang } from './sys-lang.js';
8
9
 
9
- // 默认使用 zh 语言(独立调用场景);server.js 调用时会传入 req 的 lang
10
- let currentLang = 'zh';
10
+ // 默认跟随系统语言(独立调用场景);server.js 调用时会传入 req 的 lang
11
+ let currentLang = detectSystemLang();
11
12
 
12
13
  /**
13
14
  * 设置 trash 模块当前使用的语言(由 server.js 在请求入口处调用)
@@ -45,7 +46,7 @@ async function ensureTrashDir() {
45
46
  await mkdir(TRASH_DIR, { recursive: true });
46
47
  }
47
48
  } catch (err) {
48
- console.error('[Trash] Cannot create trash directory:', err.message);
49
+ console.error(`[Trash] ${tr('trash.cannotCreateDir', { message: err.message })}`);
49
50
  throw err;
50
51
  }
51
52
  }
@@ -120,7 +121,7 @@ async function cleanExpiredTrash() {
120
121
 
121
122
  if (expired.length > 0) {
122
123
  await saveMetadata(kept);
123
- console.log(`[Trash] Cleaned up ${expired.length} expired trash entries`);
124
+ console.log(`[Trash] ${tr('trash.cleanedUp', { count: expired.length })}`);
124
125
  }
125
126
  }
126
127
 
@@ -161,7 +162,7 @@ export async function moveToTrash(sourcePath, tFn) {
161
162
  });
162
163
  await saveMetadata(entries);
163
164
 
164
- console.log(`[Trash] Moved to trash: ${sourcePath} -> ${trashPath} (${size} bytes, ${isDir ? 'directory' : 'file'})`);
165
+ console.log(`[Trash] ${tr('trash.movedToTrashLog', { source: sourcePath, target: trashPath, size, type: tr(isDir ? 'trash.typeDirectory' : 'trash.typeFile') })}`);
165
166
  return { success: true, trashId: id, isDir };
166
167
  } catch (err) {
167
168
  return { success: false, error: tr('trash.moveFailed', { message: err.message }, tFn) };
@@ -199,7 +200,7 @@ export async function restoreFromTrash(trashId, tFn) {
199
200
  entries.splice(idx, 1);
200
201
  await saveMetadata(entries);
201
202
 
202
- console.log(`[Trash] Restored: ${entry.originalPath}`);
203
+ console.log(`[Trash] ${tr('trash.restoredLog', { path: entry.originalPath })}`);
203
204
  return { success: true, restoredPath: entry.originalPath };
204
205
  } catch (err) {
205
206
  return { success: false, error: tr('trash.restoreFailed', { message: err.message }, tFn) };
@@ -233,11 +234,11 @@ export function startPeriodicCleanup() {
233
234
  try {
234
235
  await cleanExpiredTrash();
235
236
  } catch (err) {
236
- console.error('[Trash] Periodic cleanup error:', err.message);
237
+ console.error(`[Trash] ${tr('trash.periodicCleanError', { message: err.message })}`);
237
238
  }
238
239
  }, PERIODIC_CLEAN_INTERVAL_MS);
239
240
  if (periodicCleanTimer.unref) periodicCleanTimer.unref();
240
- console.log('[Trash] Periodic cleanup started (interval: 6 hours)');
241
+ console.log(`[Trash] ${tr('trash.periodicCleanStarted')}`);
241
242
  }
242
243
 
243
244
  /**