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 +3 -2
- package/package.json +1 -1
- package/src/auth.js +9 -8
- package/src/i18n.js +7 -2
- package/src/locales/cli.json +2 -2
- package/src/locales/en.json +36 -3
- package/src/locales/zh.json +36 -3
- package/src/logger.js +6 -1
- package/src/mcp/mcp-config.js +6 -2
- package/src/mcp/registry.js +19 -6
- package/src/server.js +22 -4
- package/src/skill/loader.js +5 -4
- package/src/skill/markdown-loader.js +96 -22
- package/src/skill/registry.js +26 -13
- package/src/sys-lang.js +29 -0
- package/src/trash.js +9 -8
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
|
|
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.
|
|
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
|
-
//
|
|
7
|
-
let currentLang =
|
|
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]
|
|
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(
|
|
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]
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
18
|
-
const DEFAULT_LANG = (
|
|
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
|
/**
|
package/src/locales/cli.json
CHANGED
|
@@ -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, 默认:
|
|
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:
|
|
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",
|
package/src/locales/en.json
CHANGED
|
@@ -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
|
-
"
|
|
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",
|
package/src/locales/zh.json
CHANGED
|
@@ -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
|
-
"
|
|
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 = (
|
|
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
|
package/src/mcp/mcp-config.js
CHANGED
|
@@ -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:
|
|
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:
|
|
121
|
+
return { success: false, error: T('mcp.serverNotFound', { name: serverId }) };
|
|
118
122
|
}
|
|
119
123
|
server.enabled = enabled;
|
|
120
124
|
await saveMcpConfig(config);
|
package/src/mcp/registry.js
CHANGED
|
@@ -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
|
|
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] ${
|
|
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:
|
|
119
|
+
return { success: false, error: T('mcp.serverNotFound', { name: serverId }) };
|
|
107
120
|
}
|
|
108
121
|
if (!client.connected) {
|
|
109
|
-
return { success: false, error:
|
|
122
|
+
return { success: false, error: T('mcp.serverNotConnected', { name: serverId }) };
|
|
110
123
|
}
|
|
111
124
|
|
|
112
|
-
console.log(`[MCP Registry]
|
|
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:
|
|
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: '
|
|
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小时)+ 启动时清理一次过期文件
|
package/src/skill/loader.js
CHANGED
|
@@ -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
|
-
//
|
|
12
|
-
let currentLang =
|
|
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}"
|
|
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]
|
|
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 {
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
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
|
}
|
package/src/skill/registry.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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:
|
|
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(
|
|
224
|
+
parts.push(T('skill.skillPromptHeader'));
|
|
212
225
|
parts.push('');
|
|
213
|
-
parts.push('
|
|
226
|
+
parts.push(T('skill.skillPromptIntro'));
|
|
214
227
|
parts.push('');
|
|
215
|
-
parts.push('
|
|
216
|
-
parts.push('
|
|
217
|
-
parts.push('
|
|
218
|
-
parts.push('
|
|
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(
|
|
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(
|
|
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 =
|
|
260
|
+
prompt = T('skill.skillPromptWorkdir', { path: skill.dirPath, prompt });
|
|
248
261
|
}
|
|
249
262
|
return {
|
|
250
263
|
success: true,
|
package/src/sys-lang.js
ADDED
|
@@ -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
|
-
//
|
|
10
|
-
let currentLang =
|
|
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(
|
|
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]
|
|
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]
|
|
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]
|
|
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(
|
|
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(
|
|
241
|
+
console.log(`[Trash] ${tr('trash.periodicCleanStarted')}`);
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
/**
|