ai-helper-agent 1.1.0 → 1.3.0
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/README.md +79 -15
- package/bin/agent.js +295 -54
- package/package.json +1 -1
- package/src/auth.js +24 -53
- package/src/config.js +75 -24
- package/src/executor.js +87 -30
- package/src/logger.js +79 -24
- package/src/search.js +375 -0
- package/src/security.js +34 -24
- package/src/server.js +181 -29
package/README.md
CHANGED
|
@@ -13,14 +13,15 @@ npm install -g ai-helper-agent
|
|
|
13
13
|
## 快速开始
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
#
|
|
16
|
+
# 前台启动(终端显示实时运行日志,适合调试)
|
|
17
17
|
ai-helper-agent start
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
ai-helper-agent start --
|
|
19
|
+
# 后台启动(守护进程模式,终端立即返回)
|
|
20
|
+
ai-helper-agent start --background
|
|
21
|
+
ai-helper-agent start -b
|
|
21
22
|
|
|
22
|
-
#
|
|
23
|
-
ai-helper-agent start --port 18911
|
|
23
|
+
# 指定工作目录和端口
|
|
24
|
+
ai-helper-agent start --workdir /path/to/your/project --port 18911
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
启动后终端会显示 4 位配对码,在 Chrome 扩展设置页「本地Agent」标签中填入即可完成配对。
|
|
@@ -29,9 +30,10 @@ ai-helper-agent start --port 18911
|
|
|
29
30
|
|
|
30
31
|
| 命令 | 说明 |
|
|
31
32
|
|------|------|
|
|
32
|
-
| `start` | 启动 Agent
|
|
33
|
+
| `start` | 启动 Agent 服务(前台运行,显示实时日志) |
|
|
34
|
+
| `start --background` / `start -b` | 后台启动(守护进程模式) |
|
|
33
35
|
| `stop` | 停止正在运行的 Agent |
|
|
34
|
-
| `restart` | 重启 Agent
|
|
36
|
+
| `restart` | 重启 Agent 服务(支持 -b 后台重启) |
|
|
35
37
|
| `status` | 查看运行状态 |
|
|
36
38
|
| `paircode` | 查看配对码提示 |
|
|
37
39
|
| `config` | 查看当前配置 |
|
|
@@ -41,14 +43,40 @@ ai-helper-agent start --port 18911
|
|
|
41
43
|
### 启动选项
|
|
42
44
|
|
|
43
45
|
```
|
|
44
|
-
--
|
|
45
|
-
--
|
|
46
|
-
--
|
|
46
|
+
--background, -b 后台运行(守护进程模式)
|
|
47
|
+
--port <端口> 监听端口,默认 18910
|
|
48
|
+
--host <地址> 监听地址,默认 127.0.0.1
|
|
49
|
+
--workdir <目录> 工作目录(文件读写限制在此范围内)
|
|
47
50
|
```
|
|
48
51
|
|
|
52
|
+
### 前台 vs 后台运行
|
|
53
|
+
|
|
54
|
+
| 模式 | 命令 | 终端行为 | 适用场景 |
|
|
55
|
+
|------|------|----------|----------|
|
|
56
|
+
| 前台 | `start` | 阻塞终端,实时显示运行日志 | 开发调试、问题排查 |
|
|
57
|
+
| 后台 | `start --background` | 立即返回,终端无日志输出 | 生产环境、日常使用 |
|
|
58
|
+
|
|
59
|
+
前台运行时,所有文件读写、命令执行、安全拦截等操作日志会以格式化形式实时输出到终端:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
[12:30:01] [INFO] [文件:read] path=/project/src/index.js size=2048
|
|
63
|
+
[12:30:05] [INFO] [命令:started] command="npm test" cwd=/project execId=a1b2c3d4
|
|
64
|
+
[12:30:12] [INFO] [命令:completed] execId=a1b2c3d4 exitCode=0 killed=false
|
|
65
|
+
[12:31:00] [WARN] [安全:exec_denied] command="rm -rf /" reason=高危命令被拦截
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 进程管理
|
|
69
|
+
|
|
70
|
+
Agent 通过 PID 文件(`~/.ai-helper-agent/agent.pid`)管理进程生命周期:
|
|
71
|
+
|
|
72
|
+
- 启动时自动写入 PID 文件
|
|
73
|
+
- `stop` 命令优先通过 API 优雅关闭,失败则通过 PID 文件 kill 进程
|
|
74
|
+
- 正常关闭时自动清理 PID 文件
|
|
75
|
+
|
|
49
76
|
## 安全机制
|
|
50
77
|
|
|
51
78
|
### 文件沙箱
|
|
79
|
+
|
|
52
80
|
所有文件操作限制在配置的 `allowedPaths` 白名单目录内。通过 `realpath` 解析防止符号链接绕过,确保物理路径安全。
|
|
53
81
|
|
|
54
82
|
### 命令分级管控
|
|
@@ -105,10 +133,32 @@ ai-helper-agent start --port 18911
|
|
|
105
133
|
| `commandTimeout` | 命令执行超时(毫秒) | 300000(5分钟) |
|
|
106
134
|
| `fileMaxSize` | 文件读写最大字节数 | 52428800(50MB) |
|
|
107
135
|
|
|
136
|
+
## 文件搜索
|
|
137
|
+
|
|
138
|
+
Agent 优先使用系统原生搜索工具,不可用时自动回退到 Node.js 实现:
|
|
139
|
+
|
|
140
|
+
| 引擎 | 用途 | 检测命令 |
|
|
141
|
+
|------|------|----------|
|
|
142
|
+
| `fd` | 文件名搜索(快速) | `fd --version` |
|
|
143
|
+
| `rg` (ripgrep) | 文件内容搜索(快速) | `rg --version` |
|
|
144
|
+
|
|
145
|
+
状态接口返回当前可用的搜索工具:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{ "searchTools": { "fd": true, "rg": true } }
|
|
149
|
+
```
|
|
150
|
+
|
|
108
151
|
## 审计日志
|
|
109
152
|
|
|
110
153
|
所有操作自动记录到审计日志中,日志文件位于 `~/.ai-helper-agent/logs/`。
|
|
111
154
|
|
|
155
|
+
### 双通道输出
|
|
156
|
+
|
|
157
|
+
- **终端输出**(前台模式):格式化可读格式,输出到 stderr
|
|
158
|
+
- **文件输出**:JSON Lines 格式,写入日志文件
|
|
159
|
+
|
|
160
|
+
两种通道同时工作,互不影响。
|
|
161
|
+
|
|
112
162
|
### 日志格式
|
|
113
163
|
|
|
114
164
|
JSON Lines 格式,每行一条记录,文件按日命名 `agent-YYYY-MM-DD.log`。
|
|
@@ -124,10 +174,10 @@ JSON Lines 格式,每行一条记录,文件按日命名 `agent-YYYY-MM-DD.lo
|
|
|
124
174
|
| category | 说明 | 包含的操作 |
|
|
125
175
|
|----------|------|-----------|
|
|
126
176
|
| `auth` | 认证事件 | 配对成功/失败 |
|
|
127
|
-
| `fs` | 文件操作 | read, write, list, delete |
|
|
177
|
+
| `fs` | 文件操作 | read, write, list, delete, search_files, search_content |
|
|
128
178
|
| `exec` | 命令执行 | started, completed, stopped, error |
|
|
129
179
|
| `security` | 安全事件 | deny, confirm, auth 失败, 路径越权拦截 |
|
|
130
|
-
| `system` | 系统事件 | server_start, server_stop, shutdown |
|
|
180
|
+
| `system` | 系统事件 | server_start, server_stop, shutdown, server_error, uncaught_exception, unhandled_rejection |
|
|
131
181
|
|
|
132
182
|
### 日志查询 API
|
|
133
183
|
|
|
@@ -156,7 +206,8 @@ GET /api/logs/dates
|
|
|
156
206
|
| 方法 | 路径 | 说明 |
|
|
157
207
|
|------|------|------|
|
|
158
208
|
| POST | `/api/pair` | 配对认证 |
|
|
159
|
-
| GET | `/api/status` |
|
|
209
|
+
| GET | `/api/status` | 健康检查(版本号、平台信息、搜索工具可用性) |
|
|
210
|
+
| POST | `/api/shutdown` | 关闭 Agent 服务(仅限本地访问) |
|
|
160
211
|
|
|
161
212
|
### 需要认证(Bearer Token)
|
|
162
213
|
|
|
@@ -166,11 +217,12 @@ GET /api/logs/dates
|
|
|
166
217
|
| POST | `/api/fs/write` | 写入文件 |
|
|
167
218
|
| POST | `/api/fs/list` | 列出目录 |
|
|
168
219
|
| POST | `/api/fs/delete` | 删除文件/目录 |
|
|
220
|
+
| POST | `/api/fs/search_files` | 按文件名模式搜索(glob) |
|
|
221
|
+
| POST | `/api/fs/search_content` | 搜索文件内容(rg 优先,Node.js 回退) |
|
|
169
222
|
| POST | `/api/exec` | 执行系统命令 |
|
|
170
223
|
| POST | `/api/exec/stop` | 停止命令执行 |
|
|
171
224
|
| GET | `/api/exec/running` | 运行中的进程列表 |
|
|
172
|
-
| GET | `/api/status/detail` |
|
|
173
|
-
| POST | `/api/shutdown` | 关闭 Agent 服务 |
|
|
225
|
+
| GET | `/api/status/detail` | 详细信息(含工作目录、配对码、搜索工具) |
|
|
174
226
|
| GET | `/api/logs` | 查询审计日志(支持 date/category/limit/offset 参数) |
|
|
175
227
|
| GET | `/api/logs/dates` | 获取可用日志日期列表 |
|
|
176
228
|
|
|
@@ -180,12 +232,24 @@ GET /api/logs/dates
|
|
|
180
232
|
|------|------|
|
|
181
233
|
| `ws://127.0.0.1:18910/ws/exec/:execId` | 命令执行实时输出流 |
|
|
182
234
|
|
|
235
|
+
## 健壮性
|
|
236
|
+
|
|
237
|
+
Agent 内置多层异常保护,防止单个错误导致整个服务崩溃:
|
|
238
|
+
|
|
239
|
+
- **请求级保护**:每个 HTTP 请求处理外层包裹异常捕获,异常时返回 500 而非崩溃
|
|
240
|
+
- **URL 解析保护**:畸形 URL 不会导致进程崩溃,返回 400
|
|
241
|
+
- **服务器级保护**:端口占用等服务器错误有专门处理,优雅报错退出
|
|
242
|
+
- **全局兜底**:`uncaughtException` 和 `unhandledRejection` 全局捕获,记录错误日志但不退出进程
|
|
243
|
+
- **文件 I/O 保护**:配置文件读写失败不影响服务运行
|
|
244
|
+
- **进程管理保护**:`SIGTERM`/`SIGKILL` 发送对已退出进程有 try-catch 保护
|
|
245
|
+
|
|
183
246
|
## 技术栈
|
|
184
247
|
|
|
185
248
|
- Node.js >= 18
|
|
186
249
|
- 原生 `http` 模块(HTTP 服务)
|
|
187
250
|
- `ws` 库(WebSocket 服务)
|
|
188
251
|
- 零外部框架依赖
|
|
252
|
+
- 可选依赖:`fd`、`rg`(ripgrep)— 用于加速文件搜索
|
|
189
253
|
|
|
190
254
|
## License
|
|
191
255
|
|
package/bin/agent.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
// agent/bin/agent.js - CLI 入口
|
|
3
3
|
// 用法: ai-helper-agent <start|stop|restart|status|paircode|config> [options]
|
|
4
4
|
import { join } from 'path';
|
|
5
|
-
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { homedir } from 'os';
|
|
6
8
|
|
|
7
|
-
const AGENT_DIR = join(
|
|
9
|
+
const AGENT_DIR = join(homedir(), '.ai-helper-agent');
|
|
8
10
|
const CONFIG_FILE = join(AGENT_DIR, 'config.json');
|
|
11
|
+
const PID_FILE = join(AGENT_DIR, 'agent.pid');
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* 读取 Agent 配置文件
|
|
@@ -16,22 +19,100 @@ function readAgentConfig() {
|
|
|
16
19
|
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
17
20
|
return JSON.parse(raw);
|
|
18
21
|
}
|
|
19
|
-
} catch {
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(`[Agent] 配置文件读取失败: ${err.message}`);
|
|
24
|
+
}
|
|
20
25
|
return { port: 18910, host: '127.0.0.1' };
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
|
-
*
|
|
29
|
+
* 确保 Agent 配置目录存在
|
|
30
|
+
*/
|
|
31
|
+
function ensureAgentDir() {
|
|
32
|
+
if (!existsSync(AGENT_DIR)) {
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(AGENT_DIR, { recursive: true });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(`[Agent] 无法创建配置目录 ${AGENT_DIR}: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 从 PID 文件读取进程 ID(原子化读取,无 TOCTOU)
|
|
25
43
|
*/
|
|
26
|
-
|
|
44
|
+
function getPidFromFile() {
|
|
27
45
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
46
|
+
const raw = readFileSync(PID_FILE, 'utf-8').trim();
|
|
47
|
+
const pid = parseInt(raw, 10);
|
|
48
|
+
if (isNaN(pid) || pid <= 0) {
|
|
49
|
+
console.error(`[Agent] PID 文件内容无效: "${raw}"`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return pid;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 删除 PID 文件
|
|
60
|
+
*/
|
|
61
|
+
function removePidFile() {
|
|
62
|
+
try {
|
|
63
|
+
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`[Agent] 删除 PID 文件失败: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 通过 PID 终止进程
|
|
71
|
+
*/
|
|
72
|
+
function killByPid(pid) {
|
|
73
|
+
if (!pid || typeof pid !== 'number' || pid <= 0) return false;
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, 'SIGTERM');
|
|
76
|
+
return true;
|
|
30
77
|
} catch {
|
|
31
78
|
return false;
|
|
32
79
|
}
|
|
33
80
|
}
|
|
34
81
|
|
|
82
|
+
/**
|
|
83
|
+
* 校验端口号
|
|
84
|
+
*/
|
|
85
|
+
function isValidPort(port) {
|
|
86
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 检查 Agent 是否在运行(轮询直到确认)
|
|
91
|
+
*/
|
|
92
|
+
async function isRunning(config, retries = 2) {
|
|
93
|
+
for (let i = 0; i < retries; i++) {
|
|
94
|
+
try {
|
|
95
|
+
const resp = await fetch(`http://${config.host}:${config.port}/api/status`);
|
|
96
|
+
if (resp.ok) return true;
|
|
97
|
+
} catch {}
|
|
98
|
+
if (i < retries - 1) await new Promise(r => setTimeout(r, 300));
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 等待 Agent 退出(轮询端口释放)
|
|
105
|
+
*/
|
|
106
|
+
async function waitForExit(config, timeoutMs = 5000) {
|
|
107
|
+
const start = Date.now();
|
|
108
|
+
while (Date.now() - start < timeoutMs) {
|
|
109
|
+
const alive = await isRunning(config, 1);
|
|
110
|
+
if (!alive) return true;
|
|
111
|
+
await new Promise(r => setTimeout(r, 200));
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
35
116
|
/**
|
|
36
117
|
* 打印帮助信息
|
|
37
118
|
*/
|
|
@@ -41,7 +122,7 @@ function printHelp() {
|
|
|
41
122
|
console.log('用法: ai-helper-agent <命令> [选项]');
|
|
42
123
|
console.log('');
|
|
43
124
|
console.log('命令:');
|
|
44
|
-
console.log(' start 启动 Agent
|
|
125
|
+
console.log(' start 启动 Agent 服务(前台运行)');
|
|
45
126
|
console.log(' stop 停止正在运行的 Agent');
|
|
46
127
|
console.log(' restart 重启 Agent 服务');
|
|
47
128
|
console.log(' status 查看 Agent 运行状态');
|
|
@@ -50,51 +131,131 @@ function printHelp() {
|
|
|
50
131
|
console.log(' help 显示此帮助信息');
|
|
51
132
|
console.log('');
|
|
52
133
|
console.log('启动选项:');
|
|
134
|
+
console.log(' --background, -b 后台运行(守护进程模式)');
|
|
53
135
|
console.log(' --port <端口> 设置监听端口 (默认: 18910)');
|
|
54
136
|
console.log(' --host <地址> 设置监听地址 (默认: 127.0.0.1)');
|
|
55
137
|
console.log(' --workdir <目录> 设置工作目录 (默认: 当前目录)');
|
|
56
138
|
console.log('');
|
|
57
139
|
console.log('示例:');
|
|
58
140
|
console.log(' ai-helper-agent start');
|
|
59
|
-
console.log(' ai-helper-agent start --
|
|
141
|
+
console.log(' ai-helper-agent start --background');
|
|
142
|
+
console.log(' ai-helper-agent start -b --port 18911');
|
|
60
143
|
console.log(' ai-helper-agent stop');
|
|
61
|
-
console.log(' ai-helper-agent restart');
|
|
144
|
+
console.log(' ai-helper-agent restart -b');
|
|
62
145
|
console.log(' ai-helper-agent status');
|
|
63
146
|
}
|
|
64
147
|
|
|
148
|
+
/**
|
|
149
|
+
* 从命令行参数提取端口配置,校验后覆盖 config
|
|
150
|
+
*/
|
|
151
|
+
function applyCliArgs(config, args) {
|
|
152
|
+
for (let i = 0; i < args.length; i++) {
|
|
153
|
+
const arg = args[i];
|
|
154
|
+
if (arg === '--port' && args[i + 1]) {
|
|
155
|
+
const port = parseInt(args[i + 1], 10);
|
|
156
|
+
if (!isValidPort(port)) {
|
|
157
|
+
console.error(`[Agent] 无效端口号: ${args[i + 1]},使用默认端口 ${config.port}`);
|
|
158
|
+
} else {
|
|
159
|
+
config.port = port;
|
|
160
|
+
}
|
|
161
|
+
i++;
|
|
162
|
+
} else if (arg === '--host' && args[i + 1]) {
|
|
163
|
+
config.host = args[i + 1];
|
|
164
|
+
i++;
|
|
165
|
+
} else if (arg === '--workdir' && args[i + 1]) {
|
|
166
|
+
config.workdir = args[i + 1];
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return config;
|
|
171
|
+
}
|
|
172
|
+
|
|
65
173
|
const command = process.argv[2] || 'help';
|
|
66
174
|
|
|
67
175
|
// --version / -v
|
|
68
176
|
if (command === '--version' || command === '-v') {
|
|
69
|
-
|
|
70
|
-
|
|
177
|
+
try {
|
|
178
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
179
|
+
console.log(`ai-helper-agent v${pkg.version}`);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error(`[Agent] 无法读取版本信息: ${err.message}`);
|
|
182
|
+
}
|
|
71
183
|
process.exit(0);
|
|
72
184
|
}
|
|
73
185
|
|
|
74
186
|
// ==================== start ====================
|
|
75
187
|
if (command === 'start') {
|
|
76
|
-
const
|
|
77
|
-
const { loadConfig } = await import('../src/config.js');
|
|
188
|
+
const isBg = process.argv.includes('--background') || process.argv.includes('-b');
|
|
78
189
|
|
|
79
|
-
|
|
190
|
+
// 过滤掉 --background/-b,其余参数传给子进程
|
|
191
|
+
const passArgs = process.argv.slice(3).filter(a => a !== '--background' && a !== '-b');
|
|
80
192
|
|
|
81
|
-
|
|
82
|
-
|
|
193
|
+
if (isBg) {
|
|
194
|
+
ensureAgentDir();
|
|
83
195
|
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
196
|
+
// 检查是否已有 Agent 在运行
|
|
197
|
+
const existingConfig = readAgentConfig();
|
|
198
|
+
if (await isRunning(existingConfig)) {
|
|
199
|
+
console.log('[Agent] Agent 已在运行中,无需重复启动');
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 以 detached 模式启动子进程
|
|
204
|
+
const child = spawn(process.execPath, [
|
|
205
|
+
...process.execArgv,
|
|
206
|
+
process.argv[1],
|
|
207
|
+
'start',
|
|
208
|
+
...passArgs
|
|
209
|
+
], {
|
|
210
|
+
detached: true,
|
|
211
|
+
stdio: 'ignore',
|
|
212
|
+
cwd: process.cwd(),
|
|
213
|
+
env: { ...process.env }
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// 监听 spawn 错误
|
|
217
|
+
let spawnFailed = false;
|
|
218
|
+
child.on('error', (err) => {
|
|
219
|
+
spawnFailed = true;
|
|
220
|
+
console.error(`[Agent] 后台启动失败: ${err.message}`);
|
|
221
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// 等待进程启动确认
|
|
226
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
227
|
+
|
|
228
|
+
if (!spawnFailed && child.pid) {
|
|
229
|
+
try {
|
|
230
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(`[Agent] PID 文件写入失败: ${err.message}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
child.unref();
|
|
236
|
+
console.log(`[Agent] Agent 已在后台启动 (PID: ${child.pid})`);
|
|
237
|
+
console.log(`[Agent] 使用 'ai-helper-agent stop' 停止服务`);
|
|
238
|
+
console.log(`[Agent] 使用 'ai-helper-agent status' 查看状态`);
|
|
95
239
|
}
|
|
240
|
+
|
|
241
|
+
process.exit(spawnFailed ? 1 : 0);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 前台模式:写入 PID 文件以便 stop 能找到
|
|
245
|
+
ensureAgentDir();
|
|
246
|
+
try {
|
|
247
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error(`[Agent] PID 文件写入失败: ${err.message}`);
|
|
96
250
|
}
|
|
97
251
|
|
|
252
|
+
const { startServer } = await import('../src/server.js');
|
|
253
|
+
const { loadConfig } = await import('../src/config.js');
|
|
254
|
+
|
|
255
|
+
console.log('[Agent] AI Helper Agent 启动中...');
|
|
256
|
+
|
|
257
|
+
const config = applyCliArgs(loadConfig(), passArgs);
|
|
258
|
+
|
|
98
259
|
console.log(`[Agent] 工作目录: ${config.workdir}`);
|
|
99
260
|
console.log(`[Agent] 监听地址: ${config.host}:${config.port}`);
|
|
100
261
|
|
|
@@ -106,23 +267,44 @@ if (command === 'start') {
|
|
|
106
267
|
console.log('[Agent] 正在停止 Agent...');
|
|
107
268
|
|
|
108
269
|
const running = await isRunning(config);
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
270
|
+
if (running) {
|
|
271
|
+
// 优先通过 API 优雅关闭
|
|
272
|
+
try {
|
|
273
|
+
const resp = await fetch(`http://${config.host}:${config.port}/api/shutdown`, {
|
|
274
|
+
method: 'POST'
|
|
275
|
+
});
|
|
276
|
+
if (resp.ok) {
|
|
277
|
+
const data = await resp.json();
|
|
278
|
+
console.log(`[Agent] ${data.message || 'Agent 已停止'}`);
|
|
279
|
+
removePidFile();
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error(`[Agent] API 关闭请求失败: ${err.message}`);
|
|
284
|
+
}
|
|
285
|
+
console.log('[Agent] API 关闭失败,尝试通过 PID 终止...');
|
|
112
286
|
}
|
|
113
287
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
288
|
+
// 回退:通过 PID 文件终止
|
|
289
|
+
const pid = getPidFromFile();
|
|
290
|
+
if (pid) {
|
|
291
|
+
if (killByPid(pid)) {
|
|
292
|
+
console.log(`[Agent] 已向 PID ${pid} 发送终止信号`);
|
|
293
|
+
} else {
|
|
294
|
+
console.log(`[Agent] 进程 ${pid} 已不存在`);
|
|
295
|
+
}
|
|
296
|
+
removePidFile();
|
|
297
|
+
} else if (!running) {
|
|
298
|
+
console.log('[Agent] Agent 未在运行');
|
|
122
299
|
}
|
|
123
300
|
|
|
301
|
+
process.exit(0);
|
|
302
|
+
|
|
124
303
|
// ==================== restart ====================
|
|
125
304
|
} else if (command === 'restart') {
|
|
305
|
+
const isBg = process.argv.includes('--background') || process.argv.includes('-b');
|
|
306
|
+
|
|
307
|
+
// 先停止现有实例
|
|
126
308
|
const config = readAgentConfig();
|
|
127
309
|
const running = await isRunning(config);
|
|
128
310
|
|
|
@@ -130,23 +312,79 @@ if (command === 'start') {
|
|
|
130
312
|
console.log('[Agent] 正在停止 Agent...');
|
|
131
313
|
try {
|
|
132
314
|
await fetch(`http://${config.host}:${config.port}/api/shutdown`, { method: 'POST' });
|
|
133
|
-
} catch {
|
|
134
|
-
|
|
135
|
-
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error(`[Agent] API 关闭请求失败: ${err.message}`);
|
|
317
|
+
}
|
|
318
|
+
// 等待进程退出(轮询而非硬延迟)
|
|
319
|
+
const exited = await waitForExit(config, 8000);
|
|
320
|
+
if (!exited) {
|
|
321
|
+
console.log('[Agent] 等待超时,尝试强制终止...');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 再通过 PID 兜底
|
|
326
|
+
const pid = getPidFromFile();
|
|
327
|
+
if (pid) {
|
|
328
|
+
killByPid(pid);
|
|
329
|
+
removePidFile();
|
|
330
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
136
331
|
}
|
|
137
332
|
|
|
333
|
+
// 重新启动
|
|
334
|
+
if (isBg) {
|
|
335
|
+
ensureAgentDir();
|
|
336
|
+
const passArgs = process.argv.slice(3).filter(a => a !== '--background' && a !== '-b');
|
|
337
|
+
|
|
338
|
+
const child = spawn(process.execPath, [
|
|
339
|
+
...process.execArgv,
|
|
340
|
+
process.argv[1],
|
|
341
|
+
'start',
|
|
342
|
+
...passArgs
|
|
343
|
+
], {
|
|
344
|
+
detached: true,
|
|
345
|
+
stdio: 'ignore',
|
|
346
|
+
cwd: process.cwd(),
|
|
347
|
+
env: { ...process.env }
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
let spawnFailed = false;
|
|
351
|
+
child.on('error', (err) => {
|
|
352
|
+
spawnFailed = true;
|
|
353
|
+
console.error(`[Agent] 后台重启失败: ${err.message}`);
|
|
354
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
355
|
+
process.exit(1);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
359
|
+
|
|
360
|
+
if (!spawnFailed && child.pid) {
|
|
361
|
+
try {
|
|
362
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.error(`[Agent] PID 文件写入失败: ${err.message}`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
child.unref();
|
|
368
|
+
console.log(`[Agent] Agent 已在后台重启 (PID: ${child.pid})`);
|
|
369
|
+
console.log(`[Agent] 使用 'ai-helper-agent stop' 停止服务`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
process.exit(spawnFailed ? 1 : 0);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 前台模式
|
|
138
376
|
console.log('[Agent] 正在启动 Agent...');
|
|
377
|
+
ensureAgentDir();
|
|
378
|
+
try {
|
|
379
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.error(`[Agent] PID 文件写入失败: ${err.message}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
139
384
|
const { startServer: restartServer } = await import('../src/server.js');
|
|
140
385
|
const { loadConfig: restartConfig } = await import('../src/config.js');
|
|
141
|
-
const cfg = restartConfig()
|
|
142
|
-
|
|
143
|
-
// 应用命令行参数
|
|
144
|
-
const rargs = process.argv.slice(3);
|
|
145
|
-
for (let i = 0; i < rargs.length; i++) {
|
|
146
|
-
if (rargs[i] === '--port' && rargs[i + 1]) { cfg.port = parseInt(rargs[i + 1], 10); i++; }
|
|
147
|
-
else if (rargs[i] === '--host' && rargs[i + 1]) { cfg.host = rargs[i + 1]; i++; }
|
|
148
|
-
else if (rargs[i] === '--workdir' && rargs[i + 1]) { cfg.workdir = rargs[i + 1]; i++; }
|
|
149
|
-
}
|
|
386
|
+
const cfg = applyCliArgs(restartConfig(),
|
|
387
|
+
process.argv.slice(3).filter(a => a !== '--background' && a !== '-b'));
|
|
150
388
|
|
|
151
389
|
console.log(`[Agent] 工作目录: ${cfg.workdir}`);
|
|
152
390
|
restartServer();
|
|
@@ -166,7 +404,9 @@ if (command === 'start') {
|
|
|
166
404
|
const resp = await fetch(`http://${config.host}:${config.port}/api/status`);
|
|
167
405
|
const data = await resp.json();
|
|
168
406
|
console.log(`[Agent] 版本: ${data.version}`);
|
|
169
|
-
} catch {
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.error(`[Agent] 获取状态失败: ${err.message}`);
|
|
409
|
+
}
|
|
170
410
|
}
|
|
171
411
|
|
|
172
412
|
// ==================== paircode ====================
|
|
@@ -179,7 +419,6 @@ if (command === 'start') {
|
|
|
179
419
|
process.exit(1);
|
|
180
420
|
}
|
|
181
421
|
|
|
182
|
-
// 配对码需要认证,如果没有 token 则直接查看(Agent 启动控制台会打印)
|
|
183
422
|
console.log('[Agent] 配对码已显示在 Agent 启动终端中');
|
|
184
423
|
console.log('[Agent] 请在启动 Agent 的终端窗口中查看配对码');
|
|
185
424
|
console.log('');
|
|
@@ -190,7 +429,9 @@ if (command === 'start') {
|
|
|
190
429
|
const data = await resp.json();
|
|
191
430
|
console.log(`[Agent] 版本: ${data.version}`);
|
|
192
431
|
}
|
|
193
|
-
} catch {
|
|
432
|
+
} catch (err) {
|
|
433
|
+
console.error(`[Agent] 获取状态失败: ${err.message}`);
|
|
434
|
+
}
|
|
194
435
|
|
|
195
436
|
// ==================== config ====================
|
|
196
437
|
} else if (command === 'config') {
|