ai-helper-agent 1.14.2 → 1.14.4
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 +157 -119
- package/package.json +2 -2
- package/src/auth.js +43 -16
- package/src/config.js +20 -8
- package/src/executor.js +15 -3
- package/src/i18n.js +94 -0
- package/src/locales/cli.json +136 -0
- package/src/locales/en.json +227 -0
- package/src/locales/zh.json +227 -0
- package/src/logger.js +54 -33
- package/src/mcp/client.js +29 -7
- package/src/mcp/mcp-config.js +2 -2
- package/src/mcp/registry.js +3 -3
- package/src/search.js +34 -8
- package/src/security.js +64 -37
- package/src/server.js +203 -167
- package/src/skill/executor.js +38 -16
- package/src/skill/loader.js +35 -13
- package/src/skill/markdown-loader.js +48 -26
- package/src/skill/registry.js +4 -4
- package/src/trash.js +39 -13
package/bin/agent.js
CHANGED
|
@@ -1,19 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// agent/bin/agent.js - CLI
|
|
3
|
-
//
|
|
4
|
-
import { join } from 'path';
|
|
2
|
+
// agent/bin/agent.js - CLI entry point
|
|
3
|
+
// Usage: ai-helper-agent <start|stop|restart|status|paircode|config> [options]
|
|
4
|
+
import { join, dirname } 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
|
+
import { fileURLToPath } from 'url';
|
|
8
9
|
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
const AGENT_DIR = join(homedir(), '.ai-helper-agent');
|
|
10
12
|
const CONFIG_FILE = join(AGENT_DIR, 'config.json');
|
|
11
13
|
const PID_FILE = join(AGENT_DIR, 'agent.pid');
|
|
12
14
|
const SKILLS_DIR = join(AGENT_DIR, 'skills');
|
|
13
15
|
const WORKSPACE_DIR = join(AGENT_DIR, 'workspace');
|
|
14
16
|
|
|
17
|
+
// ==================== CLI i18n ====================
|
|
18
|
+
function loadCliTranslations() {
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(join(__dirname, '..', 'src', 'locales', 'cli.json'), 'utf-8');
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveLang(args) {
|
|
28
|
+
// Priority: --lang flag > AI_HELPER_LANG env > default 'en'
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
if (args[i] === '--lang' && args[i + 1]) {
|
|
31
|
+
const val = args[i + 1].toLowerCase();
|
|
32
|
+
if (val === 'zh' || val === 'en') return val;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const envLang = process.env.AI_HELPER_LANG;
|
|
36
|
+
if (envLang && (envLang === 'zh' || envLang === 'en')) return envLang;
|
|
37
|
+
return 'en';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Remove --lang <val> from args to avoid passing to server
|
|
41
|
+
function filterLangArgs(args) {
|
|
42
|
+
const filtered = [];
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
if (args[i] === '--lang' && args[i + 1]) {
|
|
45
|
+
i++; // skip the value too
|
|
46
|
+
} else {
|
|
47
|
+
filtered.push(args[i]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return filtered;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lang = resolveLang(process.argv);
|
|
54
|
+
process.env.AI_HELPER_LANG = lang;
|
|
55
|
+
const translations = loadCliTranslations();
|
|
56
|
+
const msgs = translations ? translations[lang] : null;
|
|
57
|
+
|
|
58
|
+
function t(key, vars) {
|
|
59
|
+
if (!msgs || !msgs[key]) return key;
|
|
60
|
+
let msg = msgs[key];
|
|
61
|
+
if (vars) {
|
|
62
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
63
|
+
msg = msg.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return msg;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tRestart(key, vars) {
|
|
70
|
+
if (!msgs?.restartHelper || !msgs.restartHelper[key]) return key;
|
|
71
|
+
let msg = msgs.restartHelper[key];
|
|
72
|
+
if (vars) {
|
|
73
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
74
|
+
msg = msg.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return msg;
|
|
78
|
+
}
|
|
79
|
+
|
|
15
80
|
/**
|
|
16
|
-
*
|
|
81
|
+
* Read Agent config file
|
|
17
82
|
*/
|
|
18
83
|
function readAgentConfig() {
|
|
19
84
|
try {
|
|
@@ -22,39 +87,38 @@ function readAgentConfig() {
|
|
|
22
87
|
return JSON.parse(raw);
|
|
23
88
|
}
|
|
24
89
|
} catch (err) {
|
|
25
|
-
console.error(`[Agent]
|
|
90
|
+
console.error(`[Agent] ${t('configReadFailed')}: ${err.message}`);
|
|
26
91
|
}
|
|
27
92
|
return { port: 18910, host: '127.0.0.1' };
|
|
28
93
|
}
|
|
29
94
|
|
|
30
95
|
/**
|
|
31
|
-
*
|
|
96
|
+
* Ensure Agent config directory exists
|
|
32
97
|
*/
|
|
33
98
|
function ensureAgentDir() {
|
|
34
99
|
try {
|
|
35
100
|
if (!existsSync(AGENT_DIR)) {
|
|
36
101
|
mkdirSync(AGENT_DIR, { recursive: true });
|
|
37
102
|
}
|
|
38
|
-
// 确保 skills 和 workspace 子目录存在
|
|
39
103
|
for (const dir of [SKILLS_DIR, WORKSPACE_DIR]) {
|
|
40
104
|
if (!existsSync(dir)) {
|
|
41
105
|
mkdirSync(dir, { recursive: true });
|
|
42
106
|
}
|
|
43
107
|
}
|
|
44
108
|
} catch (err) {
|
|
45
|
-
console.error(`[Agent]
|
|
109
|
+
console.error(`[Agent] ${t('configDirCreateFailed')} ${AGENT_DIR}: ${err.message}`);
|
|
46
110
|
}
|
|
47
111
|
}
|
|
48
112
|
|
|
49
113
|
/**
|
|
50
|
-
*
|
|
114
|
+
* Read PID from PID file (atomic read, no TOCTOU)
|
|
51
115
|
*/
|
|
52
116
|
function getPidFromFile() {
|
|
53
117
|
try {
|
|
54
118
|
const raw = readFileSync(PID_FILE, 'utf-8').trim();
|
|
55
119
|
const pid = parseInt(raw, 10);
|
|
56
120
|
if (isNaN(pid) || pid <= 0) {
|
|
57
|
-
console.error(`[Agent]
|
|
121
|
+
console.error(`[Agent] ${t('invalidPidContent')}: "${raw}"`);
|
|
58
122
|
return null;
|
|
59
123
|
}
|
|
60
124
|
return pid;
|
|
@@ -64,18 +128,18 @@ function getPidFromFile() {
|
|
|
64
128
|
}
|
|
65
129
|
|
|
66
130
|
/**
|
|
67
|
-
*
|
|
131
|
+
* Delete PID file
|
|
68
132
|
*/
|
|
69
133
|
function removePidFile() {
|
|
70
134
|
try {
|
|
71
135
|
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
|
72
136
|
} catch (err) {
|
|
73
|
-
console.error(`[Agent]
|
|
137
|
+
console.error(`[Agent] ${t('deletePidFailed')}: ${err.message}`);
|
|
74
138
|
}
|
|
75
139
|
}
|
|
76
140
|
|
|
77
141
|
/**
|
|
78
|
-
*
|
|
142
|
+
* Kill process by PID
|
|
79
143
|
*/
|
|
80
144
|
function killByPid(pid) {
|
|
81
145
|
if (!pid || typeof pid !== 'number' || pid <= 0) return false;
|
|
@@ -88,14 +152,14 @@ function killByPid(pid) {
|
|
|
88
152
|
}
|
|
89
153
|
|
|
90
154
|
/**
|
|
91
|
-
*
|
|
155
|
+
* Validate port number
|
|
92
156
|
*/
|
|
93
157
|
function isValidPort(port) {
|
|
94
158
|
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
95
159
|
}
|
|
96
160
|
|
|
97
161
|
/**
|
|
98
|
-
*
|
|
162
|
+
* Check if Agent is running (poll until confirmed)
|
|
99
163
|
*/
|
|
100
164
|
async function isRunning(config, retries = 2) {
|
|
101
165
|
for (let i = 0; i < retries; i++) {
|
|
@@ -109,7 +173,7 @@ async function isRunning(config, retries = 2) {
|
|
|
109
173
|
}
|
|
110
174
|
|
|
111
175
|
/**
|
|
112
|
-
*
|
|
176
|
+
* Wait for Agent to exit (poll port release)
|
|
113
177
|
*/
|
|
114
178
|
async function waitForExit(config, timeoutMs = 5000) {
|
|
115
179
|
const start = Date.now();
|
|
@@ -122,29 +186,30 @@ async function waitForExit(config, timeoutMs = 5000) {
|
|
|
122
186
|
}
|
|
123
187
|
|
|
124
188
|
/**
|
|
125
|
-
*
|
|
189
|
+
* Print help message
|
|
126
190
|
*/
|
|
127
191
|
function printHelp() {
|
|
128
|
-
console.log('
|
|
192
|
+
console.log(t('title'));
|
|
129
193
|
console.log('');
|
|
130
|
-
console.log('
|
|
194
|
+
console.log(t('usage'));
|
|
131
195
|
console.log('');
|
|
132
|
-
console.log('
|
|
133
|
-
console.log('
|
|
134
|
-
console.log('
|
|
135
|
-
console.log('
|
|
136
|
-
console.log('
|
|
137
|
-
console.log('
|
|
138
|
-
console.log('
|
|
139
|
-
console.log('
|
|
196
|
+
console.log(t('commands'));
|
|
197
|
+
console.log(t('cmdStart'));
|
|
198
|
+
console.log(t('cmdStop'));
|
|
199
|
+
console.log(t('cmdRestart'));
|
|
200
|
+
console.log(t('cmdStatus'));
|
|
201
|
+
console.log(t('cmdPaircode'));
|
|
202
|
+
console.log(t('cmdConfig'));
|
|
203
|
+
console.log(t('cmdHelp'));
|
|
140
204
|
console.log('');
|
|
141
|
-
console.log('
|
|
142
|
-
console.log('
|
|
143
|
-
console.log(
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(
|
|
205
|
+
console.log(t('startupOptions'));
|
|
206
|
+
console.log(t('optBackground'));
|
|
207
|
+
console.log(t('optPort'));
|
|
208
|
+
console.log(t('optHost'));
|
|
209
|
+
console.log(t('optWorkdir'));
|
|
210
|
+
console.log(t('optLang'));
|
|
146
211
|
console.log('');
|
|
147
|
-
console.log('
|
|
212
|
+
console.log(t('examples'));
|
|
148
213
|
console.log(' ai-helper-agent start');
|
|
149
214
|
console.log(' ai-helper-agent start --background');
|
|
150
215
|
console.log(' ai-helper-agent start -b --port 18911');
|
|
@@ -154,7 +219,7 @@ function printHelp() {
|
|
|
154
219
|
}
|
|
155
220
|
|
|
156
221
|
/**
|
|
157
|
-
*
|
|
222
|
+
* Apply CLI args to config, with validation
|
|
158
223
|
*/
|
|
159
224
|
function applyCliArgs(config, args) {
|
|
160
225
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -162,7 +227,7 @@ function applyCliArgs(config, args) {
|
|
|
162
227
|
if (arg === '--port' && args[i + 1]) {
|
|
163
228
|
const port = parseInt(args[i + 1], 10);
|
|
164
229
|
if (!isValidPort(port)) {
|
|
165
|
-
console.error(`[Agent]
|
|
230
|
+
console.error(`[Agent] ${t('invalidPort', { port: args[i + 1], defaultPort: config.port })}`);
|
|
166
231
|
} else {
|
|
167
232
|
config.port = port;
|
|
168
233
|
}
|
|
@@ -178,7 +243,9 @@ function applyCliArgs(config, args) {
|
|
|
178
243
|
return config;
|
|
179
244
|
}
|
|
180
245
|
|
|
181
|
-
const
|
|
246
|
+
const rawArgs = process.argv.slice(2);
|
|
247
|
+
const command = rawArgs[0] || 'help';
|
|
248
|
+
const passArgs = filterLangArgs(rawArgs.slice(1));
|
|
182
249
|
|
|
183
250
|
// --version / -v
|
|
184
251
|
if (command === '--version' || command === '-v') {
|
|
@@ -186,7 +253,7 @@ if (command === '--version' || command === '-v') {
|
|
|
186
253
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
187
254
|
console.log(`ai-helper-agent v${pkg.version}`);
|
|
188
255
|
} catch (err) {
|
|
189
|
-
console.error(`[Agent]
|
|
256
|
+
console.error(`[Agent] ${t('readVersionFailed')}: ${err.message}`);
|
|
190
257
|
}
|
|
191
258
|
process.exit(0);
|
|
192
259
|
}
|
|
@@ -195,20 +262,15 @@ if (command === '--version' || command === '-v') {
|
|
|
195
262
|
if (command === 'start') {
|
|
196
263
|
const isBg = process.argv.includes('--background') || process.argv.includes('-b');
|
|
197
264
|
|
|
198
|
-
// 过滤掉 --background/-b,其余参数传给子进程
|
|
199
|
-
const passArgs = process.argv.slice(3).filter(a => a !== '--background' && a !== '-b');
|
|
200
|
-
|
|
201
265
|
if (isBg) {
|
|
202
266
|
ensureAgentDir();
|
|
203
267
|
|
|
204
|
-
// 检查是否已有 Agent 在运行
|
|
205
268
|
const existingConfig = readAgentConfig();
|
|
206
269
|
if (await isRunning(existingConfig)) {
|
|
207
|
-
console.log(
|
|
270
|
+
console.log(`[Agent] ${t('agentAlreadyRunning')}`);
|
|
208
271
|
process.exit(0);
|
|
209
272
|
}
|
|
210
273
|
|
|
211
|
-
// 以 detached 模式启动子进程
|
|
212
274
|
const child = spawn(process.execPath, [
|
|
213
275
|
...process.execArgv,
|
|
214
276
|
process.argv[1],
|
|
@@ -222,89 +284,85 @@ if (command === 'start') {
|
|
|
222
284
|
windowsHide: true
|
|
223
285
|
});
|
|
224
286
|
|
|
225
|
-
// 监听 spawn 错误
|
|
226
287
|
let spawnFailed = false;
|
|
227
288
|
child.on('error', (err) => {
|
|
228
289
|
spawnFailed = true;
|
|
229
|
-
console.error(`[Agent]
|
|
290
|
+
console.error(`[Agent] ${t('backgroundStartFailed')}: ${err.message}`);
|
|
230
291
|
try { unlinkSync(PID_FILE); } catch {}
|
|
231
292
|
process.exit(1);
|
|
232
293
|
});
|
|
233
294
|
|
|
234
|
-
// 等待进程启动确认
|
|
235
295
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
236
296
|
|
|
237
297
|
if (!spawnFailed && child.pid) {
|
|
238
298
|
try {
|
|
239
299
|
writeFileSync(PID_FILE, String(child.pid));
|
|
240
300
|
} catch (err) {
|
|
241
|
-
console.error(`[Agent]
|
|
301
|
+
console.error(`[Agent] ${t('pidWriteFailed')}: ${err.message}`);
|
|
242
302
|
process.exit(1);
|
|
243
303
|
}
|
|
244
304
|
child.unref();
|
|
245
|
-
console.log(`[Agent]
|
|
246
|
-
console.log(`[Agent]
|
|
247
|
-
console.log(`[Agent]
|
|
305
|
+
console.log(`[Agent] ${t('agentStartedBg', { pid: child.pid })}`);
|
|
306
|
+
console.log(`[Agent] ${t('stopHint')}`);
|
|
307
|
+
console.log(`[Agent] ${t('statusHint')}`);
|
|
248
308
|
}
|
|
249
309
|
|
|
250
310
|
process.exit(spawnFailed ? 1 : 0);
|
|
251
311
|
}
|
|
252
312
|
|
|
253
|
-
//
|
|
313
|
+
// Foreground mode
|
|
254
314
|
ensureAgentDir();
|
|
255
315
|
try {
|
|
256
316
|
writeFileSync(PID_FILE, String(process.pid));
|
|
257
317
|
} catch (err) {
|
|
258
|
-
console.error(`[Agent]
|
|
318
|
+
console.error(`[Agent] ${t('pidWriteFailed')}: ${err.message}`);
|
|
259
319
|
}
|
|
260
320
|
|
|
261
321
|
const { startServer } = await import('../src/server.js');
|
|
262
322
|
const { loadConfig } = await import('../src/config.js');
|
|
263
323
|
|
|
264
|
-
console.log(
|
|
324
|
+
console.log(`[Agent] ${t('agentStarting')}`);
|
|
265
325
|
|
|
266
326
|
const config = applyCliArgs(loadConfig(), passArgs);
|
|
267
327
|
|
|
268
|
-
console.log(`[Agent]
|
|
269
|
-
console.log(`[Agent]
|
|
328
|
+
console.log(`[Agent] ${t('workingDir', { dir: config.workdir })}`);
|
|
329
|
+
console.log(`[Agent] ${t('listeningAddr', { host: config.host, port: config.port })}`);
|
|
270
330
|
|
|
271
331
|
startServer();
|
|
272
332
|
|
|
273
333
|
// ==================== stop ====================
|
|
274
334
|
} else if (command === 'stop') {
|
|
275
335
|
const config = readAgentConfig();
|
|
276
|
-
console.log(
|
|
336
|
+
console.log(`[Agent] ${t('stoppingAgent')}`);
|
|
277
337
|
|
|
278
338
|
const running = await isRunning(config);
|
|
279
339
|
if (running) {
|
|
280
|
-
// 优先通过 API 优雅关闭
|
|
281
340
|
try {
|
|
282
341
|
const resp = await fetch(`http://${config.host}:${config.port}/api/shutdown`, {
|
|
283
342
|
method: 'POST'
|
|
284
343
|
});
|
|
285
344
|
if (resp.ok) {
|
|
286
345
|
const data = await resp.json();
|
|
287
|
-
console.log(`[Agent] ${data.message || '
|
|
346
|
+
console.log(`[Agent] ${data.message || t('agentStopped')}`);
|
|
288
347
|
removePidFile();
|
|
289
348
|
process.exit(0);
|
|
290
349
|
}
|
|
291
350
|
} catch (err) {
|
|
292
|
-
console.error(`[Agent]
|
|
351
|
+
console.error(`[Agent] ${t('apiShutdownFailed')}: ${err.message}`);
|
|
293
352
|
}
|
|
294
|
-
console.log(
|
|
353
|
+
console.log(`[Agent] ${t('apiShutdownFallback')}`);
|
|
295
354
|
}
|
|
296
355
|
|
|
297
|
-
// 回退:通过 PID 文件终止
|
|
298
356
|
const pid = getPidFromFile();
|
|
299
357
|
if (pid) {
|
|
300
358
|
if (killByPid(pid)) {
|
|
301
|
-
console.log(`[Agent]
|
|
359
|
+
console.log(`[Agent] ${t('sigtermSent', { pid })}`);
|
|
302
360
|
} else {
|
|
303
|
-
console.log(`[Agent]
|
|
361
|
+
console.log(`[Agent] ${t('processGone', { pid })}`);
|
|
304
362
|
}
|
|
305
363
|
removePidFile();
|
|
306
364
|
} else if (!running) {
|
|
307
|
-
console.log(
|
|
365
|
+
console.log(`[Agent] ${t('agentNotRunning')}`);
|
|
308
366
|
}
|
|
309
367
|
|
|
310
368
|
process.exit(0);
|
|
@@ -313,25 +371,22 @@ if (command === 'start') {
|
|
|
313
371
|
} else if (command === 'restart') {
|
|
314
372
|
const isBg = process.argv.includes('--background') || process.argv.includes('-b');
|
|
315
373
|
|
|
316
|
-
// 先停止现有实例
|
|
317
374
|
const config = readAgentConfig();
|
|
318
375
|
const running = await isRunning(config);
|
|
319
376
|
|
|
320
377
|
if (running) {
|
|
321
|
-
console.log(
|
|
378
|
+
console.log(`[Agent] ${t('stoppingAgent')}`);
|
|
322
379
|
try {
|
|
323
380
|
await fetch(`http://${config.host}:${config.port}/api/shutdown`, { method: 'POST' });
|
|
324
381
|
} catch (err) {
|
|
325
|
-
console.error(`[Agent]
|
|
382
|
+
console.error(`[Agent] ${t('apiShutdownFailed')}: ${err.message}`);
|
|
326
383
|
}
|
|
327
|
-
// 等待进程退出(轮询而非硬延迟)
|
|
328
384
|
const exited = await waitForExit(config, 8000);
|
|
329
385
|
if (!exited) {
|
|
330
|
-
console.log(
|
|
386
|
+
console.log(`[Agent] ${t('waitTimeoutForceKill')}`);
|
|
331
387
|
}
|
|
332
388
|
}
|
|
333
389
|
|
|
334
|
-
// 再通过 PID 兜底
|
|
335
390
|
const pid = getPidFromFile();
|
|
336
391
|
if (pid) {
|
|
337
392
|
killByPid(pid);
|
|
@@ -339,10 +394,8 @@ if (command === 'start') {
|
|
|
339
394
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
340
395
|
}
|
|
341
396
|
|
|
342
|
-
// 重新启动
|
|
343
397
|
if (isBg) {
|
|
344
398
|
ensureAgentDir();
|
|
345
|
-
const passArgs = process.argv.slice(3).filter(a => a !== '--background' && a !== '-b');
|
|
346
399
|
|
|
347
400
|
const child = spawn(process.execPath, [
|
|
348
401
|
...process.execArgv,
|
|
@@ -360,7 +413,7 @@ if (command === 'start') {
|
|
|
360
413
|
let spawnFailed = false;
|
|
361
414
|
child.on('error', (err) => {
|
|
362
415
|
spawnFailed = true;
|
|
363
|
-
console.error(`[Agent]
|
|
416
|
+
console.error(`[Agent] ${t('backgroundRestartFailed')}: ${err.message}`);
|
|
364
417
|
try { unlinkSync(PID_FILE); } catch {}
|
|
365
418
|
process.exit(1);
|
|
366
419
|
});
|
|
@@ -371,40 +424,36 @@ if (command === 'start') {
|
|
|
371
424
|
try {
|
|
372
425
|
writeFileSync(PID_FILE, String(child.pid));
|
|
373
426
|
} catch (err) {
|
|
374
|
-
console.error(`[Agent]
|
|
427
|
+
console.error(`[Agent] ${t('pidWriteFailed')}: ${err.message}`);
|
|
375
428
|
process.exit(1);
|
|
376
429
|
}
|
|
377
430
|
child.unref();
|
|
378
|
-
console.log(`[Agent]
|
|
379
|
-
console.log(`[Agent]
|
|
431
|
+
console.log(`[Agent] ${t('agentRestartedBg', { pid: child.pid })}`);
|
|
432
|
+
console.log(`[Agent] ${t('stopHint')}`);
|
|
380
433
|
}
|
|
381
434
|
|
|
382
435
|
process.exit(spawnFailed ? 1 : 0);
|
|
383
436
|
}
|
|
384
437
|
|
|
385
|
-
//
|
|
386
|
-
console.log(
|
|
438
|
+
// Foreground mode
|
|
439
|
+
console.log(`[Agent] ${t('agentRestarting')}`);
|
|
387
440
|
ensureAgentDir();
|
|
388
441
|
try {
|
|
389
442
|
writeFileSync(PID_FILE, String(process.pid));
|
|
390
443
|
} catch (err) {
|
|
391
|
-
console.error(`[Agent]
|
|
444
|
+
console.error(`[Agent] ${t('pidWriteFailed')}: ${err.message}`);
|
|
392
445
|
}
|
|
393
446
|
|
|
394
447
|
const { startServer: restartServer } = await import('../src/server.js');
|
|
395
448
|
const { loadConfig: restartConfig } = await import('../src/config.js');
|
|
396
|
-
const cfg = applyCliArgs(restartConfig(),
|
|
397
|
-
process.argv.slice(3).filter(a => a !== '--background' && a !== '-b'));
|
|
449
|
+
const cfg = applyCliArgs(restartConfig(), passArgs);
|
|
398
450
|
|
|
399
|
-
console.log(`[Agent]
|
|
451
|
+
console.log(`[Agent] ${t('workingDir', { dir: cfg.workdir })}`);
|
|
400
452
|
restartServer();
|
|
401
453
|
|
|
402
|
-
// ==================== _restart-helper
|
|
454
|
+
// ==================== _restart-helper (internal, two-phase restart wrapper) ====================
|
|
403
455
|
} else if (command === '_restart-helper') {
|
|
404
|
-
|
|
405
|
-
// 职责:等待老进程退出 + 端口释放后,再以 --background 模式启动新进程
|
|
406
|
-
// 解决直接 spawn 新进程时 isRunning 检查劝退、端口 EADDRINUSE 的时序问题
|
|
407
|
-
const helperConfig = applyCliArgs({ port: 18910, host: '127.0.0.1' }, process.argv.slice(3));
|
|
456
|
+
const helperConfig = applyCliArgs({ port: 18910, host: '127.0.0.1' }, passArgs);
|
|
408
457
|
const oldPidIdx = process.argv.indexOf('--old-pid');
|
|
409
458
|
const oldPid = oldPidIdx > -1 ? parseInt(process.argv[oldPidIdx + 1], 10) : 0;
|
|
410
459
|
const scriptIdx = process.argv.indexOf('--script');
|
|
@@ -415,9 +464,8 @@ if (command === 'start') {
|
|
|
415
464
|
const workdir = helperConfig.workdir || process.cwd();
|
|
416
465
|
|
|
417
466
|
const log = (msg) => console.log(`[Agent Restart Helper] ${msg}`);
|
|
418
|
-
log(
|
|
467
|
+
log(tRestart('waitingOldExit', { pid: oldPid }));
|
|
419
468
|
|
|
420
|
-
// 1. 等待老进程退出(轮询 PID 探活,process.kill(pid,0) 不发信号只检测存在性)
|
|
421
469
|
const waitForOldExit = (pid, timeoutMs = 15000) => new Promise((resolve) => {
|
|
422
470
|
if (!pid) return resolve(true);
|
|
423
471
|
const start = Date.now();
|
|
@@ -431,14 +479,13 @@ if (command === 'start') {
|
|
|
431
479
|
}
|
|
432
480
|
if (Date.now() - start > timeoutMs) {
|
|
433
481
|
clearInterval(timer);
|
|
434
|
-
log('
|
|
482
|
+
log(tRestart('waitOldTimeout'));
|
|
435
483
|
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
436
484
|
resolve(false);
|
|
437
485
|
}
|
|
438
486
|
}, 200);
|
|
439
487
|
});
|
|
440
488
|
|
|
441
|
-
// 2. 等待端口释放(fetch /api/status 失败即视为释放)
|
|
442
489
|
const waitForPortRelease = async (host, port, timeoutMs = 10000) => {
|
|
443
490
|
const start = Date.now();
|
|
444
491
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -458,20 +505,13 @@ if (command === 'start') {
|
|
|
458
505
|
(async () => {
|
|
459
506
|
await waitForOldExit(oldPid);
|
|
460
507
|
await waitForPortRelease(host, port);
|
|
461
|
-
// 额外等待,确保 socket 完全回收
|
|
462
508
|
await new Promise(r => setTimeout(r, 500));
|
|
463
509
|
|
|
464
|
-
log('
|
|
510
|
+
log(tRestart('oldExited'));
|
|
465
511
|
|
|
466
|
-
// 清理旧 PID 文件,避免新进程 isRunning 检查误判
|
|
467
512
|
removePidFile();
|
|
468
513
|
ensureAgentDir();
|
|
469
514
|
|
|
470
|
-
// 直接以前台模式启动服务进程(不经过 --background 中间层)
|
|
471
|
-
// 原因:--background 会多 spawn 一层中间进程,中间进程退出后其 PID 会被
|
|
472
|
-
// 本 helper 覆盖写入 PID 文件,导致 stop/restart 读到已死的中间进程 PID。
|
|
473
|
-
// 前台模式下 child.pid 就是服务进程 PID,与服务进程自身写入的 PID 一致。
|
|
474
|
-
// 前台 start 不会 isRunning 检查,但本 helper 已确保老进程退出 + 端口释放。
|
|
475
515
|
const child = spawn(process.execPath, [
|
|
476
516
|
...process.execArgv,
|
|
477
517
|
agentScript,
|
|
@@ -490,14 +530,12 @@ if (command === 'start') {
|
|
|
490
530
|
let spawnFailed = false;
|
|
491
531
|
child.on('error', (err) => {
|
|
492
532
|
spawnFailed = true;
|
|
493
|
-
log(
|
|
533
|
+
log(`${tRestart('startupFailed')}: ${err.message}`);
|
|
494
534
|
process.exit(1);
|
|
495
535
|
});
|
|
496
536
|
|
|
497
537
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
498
538
|
|
|
499
|
-
// 健康检查:确认服务进程已成功监听端口,避免写入已崩溃进程的 PID
|
|
500
|
-
// (若端口意外未释放导致 EADDRINUSE,服务进程会崩溃,此时不应写错误 PID)
|
|
501
539
|
let healthy = false;
|
|
502
540
|
if (!spawnFailed && child.pid) {
|
|
503
541
|
try {
|
|
@@ -510,12 +548,12 @@ if (command === 'start') {
|
|
|
510
548
|
try {
|
|
511
549
|
writeFileSync(PID_FILE, String(child.pid));
|
|
512
550
|
} catch (err) {
|
|
513
|
-
log(
|
|
551
|
+
log(`${tRestart('pidWriteFailed')}: ${err.message}`);
|
|
514
552
|
}
|
|
515
553
|
child.unref();
|
|
516
|
-
log(
|
|
554
|
+
log(tRestart('newProcessStarted', { pid: child.pid }));
|
|
517
555
|
} else if (!spawnFailed && child.pid) {
|
|
518
|
-
log(
|
|
556
|
+
log(tRestart('healthCheckFailed'));
|
|
519
557
|
}
|
|
520
558
|
|
|
521
559
|
process.exit(spawnFailed ? 1 : 0);
|
|
@@ -526,18 +564,18 @@ if (command === 'start') {
|
|
|
526
564
|
const config = readAgentConfig();
|
|
527
565
|
const running = await isRunning(config);
|
|
528
566
|
|
|
529
|
-
console.log(
|
|
530
|
-
console.log(`[Agent]
|
|
531
|
-
console.log(`[Agent]
|
|
532
|
-
console.log(`[Agent]
|
|
567
|
+
console.log(`[Agent] ${t('agentStatus')}`);
|
|
568
|
+
console.log(`[Agent] ${t('listeningAddr', { host: config.host, port: config.port })}`);
|
|
569
|
+
console.log(`[Agent] ${t('runningStatus')}: ${running ? t('runningYes') : t('runningNo')}`);
|
|
570
|
+
console.log(`[Agent] ${t('configFile')}: ${CONFIG_FILE}`);
|
|
533
571
|
|
|
534
572
|
if (running) {
|
|
535
573
|
try {
|
|
536
574
|
const resp = await fetch(`http://${config.host}:${config.port}/api/status`);
|
|
537
575
|
const data = await resp.json();
|
|
538
|
-
console.log(`[Agent]
|
|
576
|
+
console.log(`[Agent] ${t('version')}: ${data.version}`);
|
|
539
577
|
} catch (err) {
|
|
540
|
-
console.error(`[Agent]
|
|
578
|
+
console.error(`[Agent] ${t('getStatusFailed')}: ${err.message}`);
|
|
541
579
|
}
|
|
542
580
|
}
|
|
543
581
|
|
|
@@ -547,30 +585,30 @@ if (command === 'start') {
|
|
|
547
585
|
const running = await isRunning(config);
|
|
548
586
|
|
|
549
587
|
if (!running) {
|
|
550
|
-
console.log(
|
|
588
|
+
console.log(`[Agent] ${t('agentNotRunningStartFirst')}`);
|
|
551
589
|
process.exit(1);
|
|
552
590
|
}
|
|
553
591
|
|
|
554
|
-
console.log(
|
|
555
|
-
console.log(
|
|
592
|
+
console.log(`[Agent] ${t('paircodeInTerminal')}`);
|
|
593
|
+
console.log(`[Agent] ${t('checkPaircodeInTerminal')}`);
|
|
556
594
|
console.log('');
|
|
557
|
-
console.log(
|
|
595
|
+
console.log(`[Agent] ${t('orCheckStatus')}`);
|
|
558
596
|
try {
|
|
559
597
|
const resp = await fetch(`http://${config.host}:${config.port}/api/status`);
|
|
560
598
|
if (resp.ok) {
|
|
561
599
|
const data = await resp.json();
|
|
562
|
-
console.log(`[Agent]
|
|
600
|
+
console.log(`[Agent] ${t('version')}: ${data.version}`);
|
|
563
601
|
}
|
|
564
602
|
} catch (err) {
|
|
565
|
-
console.error(`[Agent]
|
|
603
|
+
console.error(`[Agent] ${t('getStatusFailed')}: ${err.message}`);
|
|
566
604
|
}
|
|
567
605
|
|
|
568
606
|
// ==================== config ====================
|
|
569
607
|
} else if (command === 'config') {
|
|
570
608
|
const config = readAgentConfig();
|
|
571
|
-
console.log(
|
|
609
|
+
console.log(`[Agent] ${t('currentConfig')}`);
|
|
572
610
|
console.log(JSON.stringify(config, null, 2));
|
|
573
|
-
console.log(`[Agent]
|
|
611
|
+
console.log(`[Agent] ${t('configFileLocation', { path: CONFIG_FILE })}`);
|
|
574
612
|
|
|
575
613
|
// ==================== help ====================
|
|
576
614
|
} else {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-helper-agent",
|
|
3
|
-
"version": "1.14.
|
|
4
|
-
"description": "AI Helper
|
|
3
|
+
"version": "1.14.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",
|
|
7
7
|
"bin": {
|