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 +53 -10
- 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 +56 -5
- package/src/skill/loader.js +5 -4
- package/src/skill/markdown-loader.js +96 -22
- package/src/skill/registry.js +86 -16
- package/src/skill/skill-creator-seed.js +3 -3
- package/src/sys-lang.js +29 -0
- package/src/trash.js +9 -8
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
@@ -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: '
|
|
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;
|
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,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(
|
|
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(
|
|
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
|
|
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:
|
|
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(
|
|
275
|
+
parts.push(T('skill.skillPromptHeader'));
|
|
212
276
|
parts.push('');
|
|
213
|
-
parts.push('
|
|
277
|
+
parts.push(T('skill.skillPromptIntro'));
|
|
214
278
|
parts.push('');
|
|
215
|
-
parts.push('
|
|
216
|
-
parts.push('
|
|
217
|
-
parts.push('
|
|
218
|
-
parts.push('
|
|
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
|
-
|
|
223
|
-
|
|
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(
|
|
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 =
|
|
241
|
-
if (!skill
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
/**
|