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