ai-helper-agent 1.14.4 → 1.14.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/bin/agent.js CHANGED
@@ -6,6 +6,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from '
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,7 +35,7 @@ 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
41
  // Remove --lang <val> from args to avoid passing to server
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.5",
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
@@ -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小时)+ 启动时清理一次过期文件
@@ -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,6 +5,19 @@ 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();
@@ -37,7 +50,7 @@ function saveDisabledState(disabledSet) {
37
50
  try {
38
51
  writeFileSync(DISABLED_STATE_FILE, JSON.stringify({ disabled: [...disabledSet] }, null, 2), 'utf-8');
39
52
  } catch (e) {
40
- console.error('[Skill Registry] Cannot save disabled state:', e.message);
53
+ console.error(`[Skill Registry] ${T('skill.saveFailed', { message: e.message })}`);
41
54
  }
42
55
  }
43
56
 
@@ -62,7 +75,7 @@ function saveBuiltinState(state) {
62
75
  try {
63
76
  writeFileSync(BUILTIN_STATE_FILE, JSON.stringify(state, null, 2), 'utf-8');
64
77
  } catch (e) {
65
- console.error('[Skill Registry] Cannot save builtin skill state:', e.message);
78
+ console.error(`[Skill Registry] ${T('skill.saveFailed', { message: e.message })}`);
66
79
  }
67
80
  }
68
81
 
@@ -123,7 +136,7 @@ export function initializeSkillRegistry() {
123
136
  const enabledCount = [...skills.values()].filter(s => s.enabled !== false).length;
124
137
 
125
138
  if (skills.size > 0) {
126
- console.log(`[Skill] ${skills.size} registered (${workflowCount} Workflow + ${agentCount} Agent), ${enabledCount} enabled`);
139
+ console.log(`[Skill] ${T('skill.registered', { count: skills.size, workflow: workflowCount, agent: agentCount, enabled: enabledCount })}`);
127
140
  }
128
141
  return skills.size;
129
142
  }
@@ -181,7 +194,7 @@ export function getSkill(name) {
181
194
  export async function runSkill(name, params = {}, onStepUpdate) {
182
195
  const skill = skills.get(name);
183
196
  if (!skill) {
184
- return { success: false, error: `Skill "${name}" 不存在或未启用` };
197
+ return { success: false, error: T('skill.skillNotExistOrDisabled', { name }) };
185
198
  }
186
199
 
187
200
  if (skill.type === 'agent') {
@@ -208,22 +221,22 @@ export function getAgentSkillPrompts() {
208
221
  if (agentSkills.length === 0) return '';
209
222
 
210
223
  const parts = [];
211
- parts.push('## 可用技能 (Skills)');
224
+ parts.push(T('skill.skillPromptHeader'));
212
225
  parts.push('');
213
- parts.push('以下是可用的 Agent Skill(AI 能力扩展)列表。当用户需求与某个技能高度匹配时,使用 `agent_skill` 工具(action: load)加载该技能的完整说明,然后根据说明自主完成任务。');
226
+ parts.push(T('skill.skillPromptIntro'));
214
227
  parts.push('');
215
- parts.push('**使用策略**:');
216
- parts.push('- 仅在用户需求明确匹配技能描述时才加载,不要为一般性的编程/技术问题加载技能');
217
- parts.push('- 一次只加载一个技能,加载后根据 SKILL.md 说明执行');
218
- parts.push('- 技能加载会消耗较多 token,避免无故加载');
228
+ parts.push(T('skill.skillPromptStrategy'));
229
+ parts.push(T('skill.skillPromptStrategy1'));
230
+ parts.push(T('skill.skillPromptStrategy2'));
231
+ parts.push(T('skill.skillPromptStrategy3'));
219
232
  parts.push('');
220
233
 
221
234
  for (const skill of agentSkills) {
222
235
  parts.push(`### ${skill.name}`);
223
- parts.push(`**描述**: ${skill.description}`);
236
+ parts.push(T('skill.skillPromptDesc', { description: skill.description }));
224
237
  if (skill.resources && skill.resources.length > 0) {
225
238
  const resourceNames = skill.resources.map(r => `\`${r.name}\``).join(', ');
226
- parts.push(`**可用资源**: ${resourceNames}`);
239
+ parts.push(T('skill.skillPromptResources', { resources: resourceNames }));
227
240
  }
228
241
  parts.push('');
229
242
  }
@@ -244,7 +257,7 @@ export function getAgentSkillPrompt(name) {
244
257
  // 如果 skill 有 dirPath,在 prompt 前注入工作目录信息,方便 AI 解析相对路径引用
245
258
  let prompt = skill.fullPrompt || skill.prompt || '';
246
259
  if (skill.dirPath) {
247
- prompt = `[技能工作目录: ${skill.dirPath}]\n\n${prompt}`;
260
+ prompt = T('skill.skillPromptWorkdir', { path: skill.dirPath, prompt });
248
261
  }
249
262
  return {
250
263
  success: true,
@@ -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
  /**