ai-helper-agent 1.0.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 ADDED
@@ -0,0 +1,146 @@
1
+ # AI Helper Agent
2
+
3
+ AI Helper 本地代理服务,为 [AI Helper Chrome 扩展](https://github.com/xiweicheng/ai-helper) 提供本地文件读写和系统命令执行能力。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g ai-helper-agent
9
+ ```
10
+
11
+ 要求 Node.js >= 18.0.0。
12
+
13
+ ## 快速开始
14
+
15
+ ```bash
16
+ # 启动 Agent(默认工作目录为当前目录)
17
+ ai-helper-agent start
18
+
19
+ # 指定工作目录
20
+ ai-helper-agent start --workdir /path/to/your/project
21
+
22
+ # 指定端口
23
+ ai-helper-agent start --port 18911
24
+ ```
25
+
26
+ 启动后终端会显示 4 位配对码,在 Chrome 扩展设置页「本地Agent」标签中填入即可完成配对。
27
+
28
+ ## CLI 命令
29
+
30
+ | 命令 | 说明 |
31
+ |------|------|
32
+ | `start` | 启动 Agent 服务 |
33
+ | `stop` | 停止正在运行的 Agent |
34
+ | `restart` | 重启 Agent 服务 |
35
+ | `status` | 查看运行状态 |
36
+ | `paircode` | 查看配对码提示 |
37
+ | `config` | 查看当前配置 |
38
+ | `help` | 显示帮助信息 |
39
+ | `--version` / `-v` | 显示版本号 |
40
+
41
+ ### 启动选项
42
+
43
+ ```
44
+ --port <端口> 监听端口,默认 18910
45
+ --host <地址> 监听地址,默认 127.0.0.1
46
+ --workdir <目录> 工作目录(文件读写限制在此范围内)
47
+ ```
48
+
49
+ ## 安全机制
50
+
51
+ ### 文件沙箱
52
+ 所有文件操作限制在配置的 `allowedPaths` 白名单目录内。通过 `realpath` 解析防止符号链接绕过,确保物理路径安全。
53
+
54
+ ### 命令分级管控
55
+
56
+ **黑名单** — 直接拦截,不可绕过:
57
+
58
+ | 类型 | 示例 |
59
+ |------|------|
60
+ | 磁盘破坏 | `rm -rf /`、`mkfs.*`、`dd if=... of=/dev/...` |
61
+ | 系统文件覆盖 | `> /etc/passwd`、`> /etc/shadow` |
62
+ | 恶意管道执行 | `curl ... \| bash`、`git clone ... \| sh` |
63
+ | Shell 注入 | 反引号、`$()`、`${}` 命令替换 |
64
+ | Fork bomb | `:(){ :\|:& };:` |
65
+
66
+ **灰名单** — 需要用户确认后才执行:
67
+
68
+ | 命令模式 | 原因 |
69
+ |----------|------|
70
+ | `sudo ...` | 需要管理员权限 |
71
+ | `npm install -g ...` | 全局安装包 |
72
+ | `pip install/uninstall` | Python 包管理 |
73
+ | `chmod -R 777` | 递归修改权限 |
74
+ | `rm -rf ...` | 递归强制删除 |
75
+ | `git push --force` | 强制推送 |
76
+ | `shutdown/reboot` | 关机/重启 |
77
+
78
+ ### 认证机制
79
+
80
+ 首次配对时用户在扩展中输入终端显示的 4 位配对码完成配对。配对码每 30 秒轮换。配对成功后使用 HMAC token 做后续请求认证,存储在 `~/.ai-helper-agent/pairings.json`。
81
+
82
+ ## 配置
83
+
84
+ 配置文件路径:`~/.ai-helper-agent/config.json`
85
+
86
+ ```json
87
+ {
88
+ "port": 18910,
89
+ "host": "127.0.0.1",
90
+ "workdir": "/path/to/project",
91
+ "allowedPaths": [],
92
+ "pairCodeTTL": 30,
93
+ "commandTimeout": 300000,
94
+ "fileMaxSize": 52428800
95
+ }
96
+ ```
97
+
98
+ | 字段 | 说明 | 默认值 |
99
+ |------|------|--------|
100
+ | `port` | 监听端口 | 18910 |
101
+ | `host` | 监听地址 | 127.0.0.1 |
102
+ | `workdir` | 默认工作目录 | 启动时当前目录 |
103
+ | `allowedPaths` | 额外允许访问的目录列表 | `[]` |
104
+ | `pairCodeTTL` | 配对码有效期(秒) | 30 |
105
+ | `commandTimeout` | 命令执行超时(毫秒) | 300000(5分钟) |
106
+ | `fileMaxSize` | 文件读写最大字节数 | 52428800(50MB) |
107
+
108
+ ## API 端点
109
+
110
+ ### 无需认证
111
+
112
+ | 方法 | 路径 | 说明 |
113
+ |------|------|------|
114
+ | POST | `/api/pair` | 配对认证 |
115
+ | GET | `/api/status` | 健康检查(版本号、运行状态) |
116
+
117
+ ### 需要认证(Bearer Token)
118
+
119
+ | 方法 | 路径 | 说明 |
120
+ |------|------|------|
121
+ | POST | `/api/fs/read` | 读取文件内容 |
122
+ | POST | `/api/fs/write` | 写入文件 |
123
+ | POST | `/api/fs/list` | 列出目录 |
124
+ | POST | `/api/fs/delete` | 删除文件/目录 |
125
+ | POST | `/api/exec` | 执行系统命令 |
126
+ | POST | `/api/exec/stop` | 停止命令执行 |
127
+ | GET | `/api/exec/running` | 运行中的进程列表 |
128
+ | GET | `/api/status/detail` | 详细信息(含工作目录) |
129
+ | POST | `/api/shutdown` | 关闭 Agent 服务 |
130
+
131
+ ### WebSocket
132
+
133
+ | 路径 | 说明 |
134
+ |------|------|
135
+ | `ws://127.0.0.1:18910/ws/exec/:execId` | 命令执行实时输出流 |
136
+
137
+ ## 技术栈
138
+
139
+ - Node.js >= 18
140
+ - 原生 `http` 模块(HTTP 服务)
141
+ - `ws` 库(WebSocket 服务)
142
+ - 零外部框架依赖
143
+
144
+ ## License
145
+
146
+ MIT
package/bin/agent.js ADDED
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ // agent/bin/agent.js - CLI 入口
3
+ // 用法: ai-helper-agent <start|stop|restart|status|paircode|config> [options]
4
+ import { join } from 'path';
5
+ import { existsSync, readFileSync } from 'fs';
6
+
7
+ const AGENT_DIR = join(process.env.HOME || '~', '.ai-helper-agent');
8
+ const CONFIG_FILE = join(AGENT_DIR, 'config.json');
9
+
10
+ /**
11
+ * 读取 Agent 配置文件
12
+ */
13
+ function readAgentConfig() {
14
+ try {
15
+ if (existsSync(CONFIG_FILE)) {
16
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
17
+ return JSON.parse(raw);
18
+ }
19
+ } catch {}
20
+ return { port: 18910, host: '127.0.0.1' };
21
+ }
22
+
23
+ /**
24
+ * 检查 Agent 是否在运行
25
+ */
26
+ async function isRunning(config) {
27
+ try {
28
+ const resp = await fetch(`http://${config.host}:${config.port}/api/status`);
29
+ return resp.ok;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 打印帮助信息
37
+ */
38
+ function printHelp() {
39
+ console.log('AI Helper Agent - 本地文件读写和命令执行代理');
40
+ console.log('');
41
+ console.log('用法: ai-helper-agent <命令> [选项]');
42
+ console.log('');
43
+ console.log('命令:');
44
+ console.log(' start 启动 Agent 服务');
45
+ console.log(' stop 停止正在运行的 Agent');
46
+ console.log(' restart 重启 Agent 服务');
47
+ console.log(' status 查看 Agent 运行状态');
48
+ console.log(' paircode 查看当前配对码(需认证状态)');
49
+ console.log(' config 查看当前配置');
50
+ console.log(' help 显示此帮助信息');
51
+ console.log('');
52
+ console.log('启动选项:');
53
+ console.log(' --port <端口> 设置监听端口 (默认: 18910)');
54
+ console.log(' --host <地址> 设置监听地址 (默认: 127.0.0.1)');
55
+ console.log(' --workdir <目录> 设置工作目录 (默认: 当前目录)');
56
+ console.log('');
57
+ console.log('示例:');
58
+ console.log(' ai-helper-agent start');
59
+ console.log(' ai-helper-agent start --port 18911 --workdir /path/to/project');
60
+ console.log(' ai-helper-agent stop');
61
+ console.log(' ai-helper-agent restart');
62
+ console.log(' ai-helper-agent status');
63
+ }
64
+
65
+ const command = process.argv[2] || 'help';
66
+
67
+ // --version / -v
68
+ if (command === '--version' || command === '-v') {
69
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
70
+ console.log(`ai-helper-agent v${pkg.version}`);
71
+ process.exit(0);
72
+ }
73
+
74
+ // ==================== start ====================
75
+ if (command === 'start') {
76
+ const { startServer } = await import('../src/server.js');
77
+ const { loadConfig } = await import('../src/config.js');
78
+
79
+ console.log('[Agent] AI Helper Agent 启动中...');
80
+
81
+ const args = process.argv.slice(3);
82
+ const config = loadConfig();
83
+
84
+ for (let i = 0; i < args.length; i++) {
85
+ const arg = args[i];
86
+ if (arg === '--port' && args[i + 1]) {
87
+ config.port = parseInt(args[i + 1], 10);
88
+ i++;
89
+ } else if (arg === '--host' && args[i + 1]) {
90
+ config.host = args[i + 1];
91
+ i++;
92
+ } else if (arg === '--workdir' && args[i + 1]) {
93
+ config.workdir = args[i + 1];
94
+ i++;
95
+ }
96
+ }
97
+
98
+ console.log(`[Agent] 工作目录: ${config.workdir}`);
99
+ console.log(`[Agent] 监听地址: ${config.host}:${config.port}`);
100
+
101
+ startServer();
102
+
103
+ // ==================== stop ====================
104
+ } else if (command === 'stop') {
105
+ const config = readAgentConfig();
106
+ console.log('[Agent] 正在停止 Agent...');
107
+
108
+ const running = await isRunning(config);
109
+ if (!running) {
110
+ console.log('[Agent] Agent 未在运行');
111
+ process.exit(0);
112
+ }
113
+
114
+ try {
115
+ const resp = await fetch(`http://${config.host}:${config.port}/api/shutdown`, {
116
+ method: 'POST'
117
+ });
118
+ const data = await resp.json();
119
+ console.log(`[Agent] ${data.message || 'Agent 已停止'}`);
120
+ } catch {
121
+ console.log('[Agent] 无法发送停止请求,Agent 可能已退出');
122
+ }
123
+
124
+ // ==================== restart ====================
125
+ } else if (command === 'restart') {
126
+ const config = readAgentConfig();
127
+ const running = await isRunning(config);
128
+
129
+ if (running) {
130
+ console.log('[Agent] 正在停止 Agent...');
131
+ try {
132
+ await fetch(`http://${config.host}:${config.port}/api/shutdown`, { method: 'POST' });
133
+ } catch {}
134
+ // 等待进程退出
135
+ await new Promise(resolve => setTimeout(resolve, 1500));
136
+ }
137
+
138
+ console.log('[Agent] 正在启动 Agent...');
139
+ const { startServer: restartServer } = await import('../src/server.js');
140
+ 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
+ }
150
+
151
+ console.log(`[Agent] 工作目录: ${cfg.workdir}`);
152
+ restartServer();
153
+
154
+ // ==================== status ====================
155
+ } else if (command === 'status') {
156
+ const config = readAgentConfig();
157
+ const running = await isRunning(config);
158
+
159
+ console.log('[Agent] ======== Agent 状态 ========');
160
+ console.log(`[Agent] 监听地址: ${config.host}:${config.port}`);
161
+ console.log(`[Agent] 运行状态: ${running ? '✅ 运行中' : '❌ 未运行'}`);
162
+ console.log(`[Agent] 配置文件: ${CONFIG_FILE}`);
163
+
164
+ if (running) {
165
+ try {
166
+ const resp = await fetch(`http://${config.host}:${config.port}/api/status`);
167
+ const data = await resp.json();
168
+ console.log(`[Agent] 版本: ${data.version}`);
169
+ } catch {}
170
+ }
171
+
172
+ // ==================== paircode ====================
173
+ } else if (command === 'paircode') {
174
+ const config = readAgentConfig();
175
+ const running = await isRunning(config);
176
+
177
+ if (!running) {
178
+ console.log('[Agent] Agent 未在运行,请先启动');
179
+ process.exit(1);
180
+ }
181
+
182
+ // 配对码需要认证,如果没有 token 则直接查看(Agent 启动控制台会打印)
183
+ console.log('[Agent] 配对码已显示在 Agent 启动终端中');
184
+ console.log('[Agent] 请在启动 Agent 的终端窗口中查看配对码');
185
+ console.log('');
186
+ console.log('[Agent] 或查看状态获取更多信息:');
187
+ try {
188
+ const resp = await fetch(`http://${config.host}:${config.port}/api/status`);
189
+ if (resp.ok) {
190
+ const data = await resp.json();
191
+ console.log(`[Agent] 版本: ${data.version}`);
192
+ }
193
+ } catch {}
194
+
195
+ // ==================== config ====================
196
+ } else if (command === 'config') {
197
+ const config = readAgentConfig();
198
+ console.log('[Agent] ======== 当前配置 ========');
199
+ console.log(JSON.stringify(config, null, 2));
200
+ console.log(`[Agent] 配置文件位置: ${CONFIG_FILE}`);
201
+
202
+ // ==================== help ====================
203
+ } else {
204
+ printHelp();
205
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "ai-helper-agent",
3
+ "version": "1.0.0",
4
+ "description": "AI Helper 本地代理服务 - 为 Chrome 扩展提供本地文件读写和系统命令执行能力",
5
+ "type": "module",
6
+ "main": "./src/server.js",
7
+ "bin": {
8
+ "ai-helper-agent": "./bin/agent.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/agent.js start",
17
+ "stop": "node bin/agent.js stop",
18
+ "status": "node bin/agent.js status"
19
+ },
20
+ "dependencies": {
21
+ "ws": "^8.18.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "keywords": [
27
+ "ai-helper",
28
+ "local-agent",
29
+ "chrome-extension",
30
+ "file-manager",
31
+ "command-executor",
32
+ "developer-tools"
33
+ ],
34
+ "author": "",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": ""
39
+ },
40
+ "homepage": "",
41
+ "bugs": {
42
+ "url": ""
43
+ }
44
+ }
package/src/auth.js ADDED
@@ -0,0 +1,124 @@
1
+ // agent/src/auth.js - 配对认证模块
2
+ import crypto from 'crypto';
3
+ import { loadConfig, loadPairings, savePairing } from './config.js';
4
+
5
+ // 当前有效的配对码(30秒刷新一次)
6
+ let currentPairCode = null;
7
+ let pairCodeTimer = null;
8
+
9
+ /**
10
+ * 生成随机配对码(4位数字)
11
+ */
12
+ function generatePairCode() {
13
+ return String(Math.floor(1000 + Math.random() * 9000));
14
+ }
15
+
16
+ /**
17
+ * 生成认证 token(HMAC-SHA256)
18
+ */
19
+ function generateToken(extensionId) {
20
+ const secret = crypto.randomBytes(32).toString('hex');
21
+ const pairings = loadPairings();
22
+ pairings[extensionId] = {
23
+ token: secret,
24
+ pairedAt: Date.now()
25
+ };
26
+ // 直接用 secret 作为 token
27
+ return secret;
28
+ }
29
+
30
+ /**
31
+ * 验证 token 是否有效
32
+ */
33
+ function verifyToken(token) {
34
+ if (!token) return null;
35
+ const pairings = loadPairings();
36
+ for (const [extId, pairing] of Object.entries(pairings)) {
37
+ if (pairing.token === token) {
38
+ return extId;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * 获取当前配对码
46
+ */
47
+ function getCurrentPairCode() {
48
+ return currentPairCode;
49
+ }
50
+
51
+ /**
52
+ * 启动配对码定时刷新
53
+ */
54
+ function startPairCodeRotation() {
55
+ const config = loadConfig();
56
+ const ttl = (config.pairCodeTTL || 30) * 1000;
57
+
58
+ currentPairCode = generatePairCode();
59
+ console.log(`[Agent] 当前配对码: ${currentPairCode}`);
60
+
61
+ if (pairCodeTimer) clearInterval(pairCodeTimer);
62
+ pairCodeTimer = setInterval(() => {
63
+ currentPairCode = generatePairCode();
64
+ console.log(`[Agent] 配对码已更新: ${currentPairCode}`);
65
+ }, ttl);
66
+ }
67
+
68
+ /**
69
+ * 停止配对码定时刷新
70
+ */
71
+ function stopPairCodeRotation() {
72
+ if (pairCodeTimer) {
73
+ clearInterval(pairCodeTimer);
74
+ pairCodeTimer = null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 处理配对请求
80
+ */
81
+ function handlePairRequest(code, extensionId) {
82
+ if (!code || !extensionId) {
83
+ return { success: false, error: '缺少配对码或扩展ID' };
84
+ }
85
+ if (code !== currentPairCode) {
86
+ return { success: false, error: '配对码无效' };
87
+ }
88
+ // 检查是否已有配对
89
+ const pairings = loadPairings();
90
+ if (pairings[extensionId]) {
91
+ return { success: true, token: pairings[extensionId].token, message: '已配对,返回现有 token' };
92
+ }
93
+ // 生成新 token 并保存
94
+ const token = crypto.randomBytes(32).toString('hex');
95
+ savePairing(extensionId, token);
96
+ return { success: true, token, message: '配对成功' };
97
+ }
98
+
99
+ /**
100
+ * Bearer Token 认证中间件(Express)
101
+ */
102
+ function authMiddleware(req, res, next) {
103
+ const authHeader = req.headers.authorization;
104
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
105
+ return res.status(401).json({ success: false, error: '未提供认证 token' });
106
+ }
107
+ const token = authHeader.slice(7);
108
+ const extId = verifyToken(token);
109
+ if (!extId) {
110
+ return res.status(403).json({ success: false, error: '认证 token 无效' });
111
+ }
112
+ req.extensionId = extId;
113
+ next();
114
+ }
115
+
116
+ export {
117
+ generateToken,
118
+ verifyToken,
119
+ getCurrentPairCode,
120
+ startPairCodeRotation,
121
+ stopPairCodeRotation,
122
+ handlePairRequest,
123
+ authMiddleware
124
+ };
package/src/config.js ADDED
@@ -0,0 +1,136 @@
1
+ // agent/src/config.js - 配置管理(带内存缓存)
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
5
+
6
+ const AGENT_DIR = join(homedir(), '.ai-helper-agent');
7
+ const CONFIG_FILE = join(AGENT_DIR, 'config.json');
8
+ const PAIRINGS_FILE = join(AGENT_DIR, 'pairings.json');
9
+
10
+ const DEFAULTS = {
11
+ port: 18910,
12
+ host: '127.0.0.1',
13
+ workdir: process.cwd(),
14
+ allowedPaths: [],
15
+ pairCodeTTL: 30,
16
+ commandTimeout: 300000,
17
+ fileMaxSize: 50 * 1024 * 1024
18
+ };
19
+
20
+ // 内存缓存
21
+ let configCache = null;
22
+ let configCacheMtime = 0;
23
+
24
+ let pairingsCache = null;
25
+ let pairingsCacheMtime = 0;
26
+
27
+ function ensureAgentDir() {
28
+ if (!existsSync(AGENT_DIR)) {
29
+ mkdirSync(AGENT_DIR, { recursive: true });
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 获取文件的 mtime(ms),不存在返回 0
35
+ */
36
+ function getMtime(filePath) {
37
+ try {
38
+ return statSync(filePath).mtimeMs;
39
+ } catch {
40
+ return 0;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 加载配置(带缓存,仅在文件变化时重新读取)
46
+ */
47
+ export function loadConfig() {
48
+ ensureAgentDir();
49
+
50
+ if (!existsSync(CONFIG_FILE)) {
51
+ const defaults = { ...DEFAULTS, allowedPaths: [DEFAULTS.workdir] };
52
+ saveConfig(defaults);
53
+ configCache = defaults;
54
+ configCacheMtime = getMtime(CONFIG_FILE);
55
+ return defaults;
56
+ }
57
+
58
+ const mtime = getMtime(CONFIG_FILE);
59
+ if (configCache && mtime === configCacheMtime) {
60
+ return configCache;
61
+ }
62
+
63
+ try {
64
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
65
+ const userConfig = JSON.parse(raw);
66
+ configCache = { ...DEFAULTS, ...userConfig };
67
+ configCacheMtime = mtime;
68
+ return configCache;
69
+ } catch {
70
+ configCache = { ...DEFAULTS, allowedPaths: [DEFAULTS.workdir] };
71
+ configCacheMtime = 0;
72
+ return configCache;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 保存配置(写入磁盘并更新缓存)
78
+ */
79
+ export function saveConfig(config) {
80
+ ensureAgentDir();
81
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
82
+ configCache = config;
83
+ configCacheMtime = getMtime(CONFIG_FILE);
84
+ }
85
+
86
+ /**
87
+ * 更新单字段配置
88
+ */
89
+ export function updateConfigField(key, value) {
90
+ const config = loadConfig();
91
+ config[key] = value;
92
+ saveConfig(config);
93
+ return config;
94
+ }
95
+
96
+ /**
97
+ * 读取已配对的扩展列表(带缓存)
98
+ */
99
+ export function loadPairings() {
100
+ ensureAgentDir();
101
+
102
+ const mtime = getMtime(PAIRINGS_FILE);
103
+ if (pairingsCache && mtime === pairingsCacheMtime) {
104
+ return pairingsCache;
105
+ }
106
+
107
+ if (!existsSync(PAIRINGS_FILE)) {
108
+ pairingsCache = {};
109
+ pairingsCacheMtime = 0;
110
+ return {};
111
+ }
112
+
113
+ try {
114
+ pairingsCache = JSON.parse(readFileSync(PAIRINGS_FILE, 'utf-8'));
115
+ pairingsCacheMtime = mtime;
116
+ return pairingsCache;
117
+ } catch {
118
+ pairingsCache = {};
119
+ pairingsCacheMtime = 0;
120
+ return {};
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 保存配对(写入磁盘并更新缓存)
126
+ */
127
+ export function savePairing(extensionId, token) {
128
+ const pairings = loadPairings();
129
+ pairings[extensionId] = { token, pairedAt: Date.now() };
130
+ ensureAgentDir();
131
+ writeFileSync(PAIRINGS_FILE, JSON.stringify(pairings, null, 2), 'utf-8');
132
+ pairingsCache = pairings;
133
+ pairingsCacheMtime = getMtime(PAIRINGS_FILE);
134
+ }
135
+
136
+ export { AGENT_DIR, CONFIG_FILE };
@@ -0,0 +1,191 @@
1
+ // agent/src/executor.js - 命令执行器(child_process + 流式输出)
2
+ import { spawn } from 'child_process';
3
+ import crypto from 'crypto';
4
+ import { loadConfig } from './config.js';
5
+
6
+ // 运行中的进程映射:execId → { process, wsClients: Set, timeoutId, forceKillId }
7
+ const runningProcesses = new Map();
8
+
9
+ /**
10
+ * 生成唯一执行 ID
11
+ */
12
+ function generateExecId() {
13
+ return crypto.randomBytes(8).toString('hex');
14
+ }
15
+
16
+ /**
17
+ * 执行命令并通过 WebSocket 流式输出
18
+ * @param {string} command - 完整命令字符串
19
+ * @param {string} [cwd] - 工作目录
20
+ * @param {Object} wsClient - 单个 WebSocket 客户端(用于流式输出)
21
+ * @param {Function} onComplete - 完成回调 ({ execId, exitCode, killed, stdout?, stderr? })
22
+ * @param {boolean} [collectOutput=false] - 是否收集完整 stdout/stderr 到内存
23
+ * @returns {string} execId
24
+ */
25
+ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = false) {
26
+ const config = loadConfig();
27
+ const workdir = cwd || config.workdir;
28
+ const execId = generateExecId();
29
+ const timeout = config.commandTimeout || 300000;
30
+
31
+ const proc = spawn('sh', ['-c', command], {
32
+ cwd: workdir,
33
+ env: { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' },
34
+ stdio: ['pipe', 'pipe', 'pipe']
35
+ });
36
+
37
+ const wsClients = new Set();
38
+ if (wsClient) wsClients.add(wsClient);
39
+
40
+ let stdoutCollected = '';
41
+ let stderrCollected = '';
42
+ let killed = false;
43
+ let forceKillId = null; // 追踪 SIGKILL 的 setTimeout
44
+
45
+ runningProcesses.set(execId, { process: proc, wsClients });
46
+
47
+ // 超时控制
48
+ const timeoutId = setTimeout(() => {
49
+ killed = true;
50
+ killProcess(execId);
51
+ }, timeout);
52
+
53
+ // 广播消息给所有连接的 WS 客户端
54
+ function broadcast(data) {
55
+ const msg = JSON.stringify(data);
56
+ for (const client of wsClients) {
57
+ if (client.readyState === 1) {
58
+ client.send(msg);
59
+ }
60
+ }
61
+ }
62
+
63
+ // stdout 流式输出
64
+ proc.stdout.on('data', (chunk) => {
65
+ const str = chunk.toString();
66
+ if (collectOutput) stdoutCollected += str;
67
+ broadcast({ type: 'stdout', data: str, execId });
68
+ });
69
+
70
+ // stderr 流式输出
71
+ proc.stderr.on('data', (chunk) => {
72
+ const str = chunk.toString();
73
+ if (collectOutput) stderrCollected += str;
74
+ broadcast({ type: 'stderr', data: str, execId });
75
+ });
76
+
77
+ // 清理超时计时器
78
+ function clearTimers() {
79
+ clearTimeout(timeoutId);
80
+ if (forceKillId !== null) {
81
+ clearTimeout(forceKillId);
82
+ forceKillId = null;
83
+ }
84
+ }
85
+
86
+ // 进程结束
87
+ proc.on('close', (exitCode) => {
88
+ clearTimers();
89
+ broadcast({ type: 'exit', exitCode, execId, killed });
90
+ runningProcesses.delete(execId);
91
+ if (onComplete) {
92
+ onComplete({
93
+ execId, exitCode, killed,
94
+ stdout: collectOutput ? stdoutCollected : undefined,
95
+ stderr: collectOutput ? stderrCollected : undefined
96
+ });
97
+ }
98
+ });
99
+
100
+ // 进程错误
101
+ proc.on('error', (err) => {
102
+ clearTimers();
103
+ broadcast({ type: 'error', error: err.message, execId });
104
+ runningProcesses.delete(execId);
105
+ if (onComplete) {
106
+ onComplete({
107
+ execId, exitCode: -1, killed: false,
108
+ error: err.message,
109
+ stdout: collectOutput ? stdoutCollected : undefined,
110
+ stderr: collectOutput ? stderrCollected : undefined
111
+ });
112
+ }
113
+ });
114
+
115
+ return execId;
116
+ }
117
+
118
+ /**
119
+ * 同步执行命令(阻塞等待完成,返回完整输出)
120
+ * @returns {Promise<{execId, exitCode, stdout, stderr, killed, error?}>}
121
+ */
122
+ function executeCommandSync(command, cwd) {
123
+ return new Promise((resolve, reject) => {
124
+ const config = loadConfig();
125
+ const timeout = config.commandTimeout || 300000;
126
+
127
+ let resolved = false;
128
+
129
+ const execId = executeCommand(command, cwd, null, (result) => {
130
+ if (!resolved) {
131
+ resolved = true;
132
+ resolve(result);
133
+ }
134
+ }, true);
135
+
136
+ // 超时兜底
137
+ setTimeout(() => {
138
+ if (!resolved) {
139
+ resolved = true;
140
+ killProcess(execId);
141
+ resolve({ execId, exitCode: -1, killed: true, stdout: '', stderr: '', error: 'Command timed out' });
142
+ }
143
+ }, timeout + 5000);
144
+ });
145
+ }
146
+
147
+ /**
148
+ * 添加 WebSocket 客户端到已有进程的监听中
149
+ */
150
+ function addWsClient(execId, wsClient) {
151
+ const entry = runningProcesses.get(execId);
152
+ if (entry) {
153
+ entry.wsClients.add(wsClient);
154
+ return true;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * 停止正在运行的命令
161
+ */
162
+ function killProcess(execId) {
163
+ const entry = runningProcesses.get(execId);
164
+ if (entry) {
165
+ entry.process.kill('SIGTERM');
166
+ // 5秒后强制 kill,保存 ID 以便后续清理
167
+ const forceKillId = setTimeout(() => {
168
+ try {
169
+ if (entry.process.exitCode === null) {
170
+ entry.process.kill('SIGKILL');
171
+ }
172
+ } catch {}
173
+ }, 5000);
174
+ entry.forceKillId = forceKillId;
175
+ return true;
176
+ }
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * 获取运行中的进程列表
182
+ */
183
+ function getRunningProcesses() {
184
+ const list = [];
185
+ for (const [execId] of runningProcesses) {
186
+ list.push({ execId });
187
+ }
188
+ return list;
189
+ }
190
+
191
+ export { executeCommand, executeCommandSync, addWsClient, killProcess, getRunningProcesses };
@@ -0,0 +1,155 @@
1
+ // agent/src/security.js - 安全管控:文件沙箱 + 命令分级
2
+ import { resolve, normalize } from 'path';
3
+ import { realpathSync } from 'fs';
4
+ import { loadConfig } from './config.js';
5
+
6
+ /**
7
+ * 检查路径是否在白名单内
8
+ * 使用 realpathSync 防止符号链接绕过
9
+ * @returns {{ allowed: boolean, reason?: string, resolved?: string }}
10
+ */
11
+ function checkPath(pathStr) {
12
+ if (!pathStr || typeof pathStr !== 'string') {
13
+ return { allowed: false, reason: '路径无效' };
14
+ }
15
+
16
+ try {
17
+ const resolved = resolve(pathStr);
18
+ const normalized = normalize(resolved);
19
+
20
+ const config = loadConfig();
21
+ const allowedPaths = config.allowedPaths.length > 0 ? config.allowedPaths : [config.workdir];
22
+
23
+ // 先做前缀检查(快速路径)
24
+ let prefixMatch = false;
25
+ let matchedAllowed = null;
26
+ for (const allowed of allowedPaths) {
27
+ const allowedResolved = resolve(allowed);
28
+ if (normalized.startsWith(allowedResolved + '/') || normalized === allowedResolved) {
29
+ prefixMatch = true;
30
+ matchedAllowed = allowedResolved;
31
+ break;
32
+ }
33
+ }
34
+ if (!prefixMatch) {
35
+ return { allowed: false, reason: `路径不在允许的目录范围内` };
36
+ }
37
+
38
+ // realpath 校验:防止符号链接绕过
39
+ // 注意:文件可能尚未创建,realpathSync 会抛出 ENOENT
40
+ let realPath;
41
+ try {
42
+ realPath = realpathSync(resolved);
43
+ } catch (err) {
44
+ if (err.code === 'ENOENT') {
45
+ // 文件不存在时,检查父目录
46
+ const parentDir = resolve(resolved, '..');
47
+ realPath = realpathSync(parentDir) + '/' + normalized.split('/').pop();
48
+ } else {
49
+ return { allowed: false, reason: `无法解析路径: ${err.message}` };
50
+ }
51
+ }
52
+
53
+ // 对 realPath 再次做前缀检查
54
+ for (const allowed of allowedPaths) {
55
+ const allowedResolved = resolve(allowed);
56
+ if (realPath.startsWith(allowedResolved + '/') || realPath === allowedResolved) {
57
+ return { allowed: true, resolved: normalized };
58
+ }
59
+ }
60
+
61
+ return { allowed: false, reason: `路径指向了允许范围之外的位置` };
62
+ } catch (err) {
63
+ return { allowed: false, reason: `路径检查异常: ${err.message}` };
64
+ }
65
+ }
66
+
67
+ // ==================== 命令分级管控 ====================
68
+
69
+ const BLACKLIST_PATTERNS = [
70
+ // 高危写入系统目录
71
+ /^\s*rm\s+-rf\s+\/(\*|\s|$)/,
72
+ /^\s*rm\s+-rf\s+~(\*|\s|$)/,
73
+ /^\s*rm\s+-rf\s+\/dev\//,
74
+ /^\s*rm\s+-rf\s+\/etc\//,
75
+ /^\s*rm\s+-rf\s+\/proc\//,
76
+ /^\s*rm\s+-rf\s+\/sys\//,
77
+ // 格式化/磁盘破坏
78
+ /^\s*mkfs\./,
79
+ /^\s*dd\s+if=.*of=\/dev\//,
80
+ /^\s*>\s*\/dev\/sd/,
81
+ // 覆盖系统文件
82
+ /^\s*>\s*\/etc\/passwd/,
83
+ /^\s*>\s*\/etc\/shadow/,
84
+ /^\s*>\s*\/etc\/sudoers/,
85
+ // fork bomb
86
+ /:\(\)\s*{\s*:\s*\|\s*:\s*&\s*};\s*:/,
87
+ /#!/,
88
+ // 恶意管道执行
89
+ /(curl|wget|lynx|links)\s+.*\|\s*(ba)?sh/,
90
+ /git\s+clone\s+.*\|\s*(ba)?sh/,
91
+ // 修改根目录权限
92
+ /^\s*chmod\s+(-R\s+)?(0?777|a\+rwx)\s+\//,
93
+ /^\s*chown\s+-R\s+\S+\s+\//,
94
+ // Shell 命令替换注入
95
+ /`[^`]*`/, // 反引号命令替换
96
+ /\$\([^)]*\)/, // $() 命令替换
97
+ /\$\{[^}]*\}/, // ${} 变量替换(可能含命令)
98
+ ];
99
+
100
+ const GRAYLIST_PATTERNS = [
101
+ { pattern: /^\s*sudo\s/, reason: '命令需要管理员权限 (sudo)' },
102
+ { pattern: /npm\s+(i|install)\s+-g\s/, reason: '全局安装 npm 包' },
103
+ { pattern: /pip\s+(install|uninstall)\s/, reason: 'pip 安装/卸载包' },
104
+ { pattern: /^\s*chmod\s+-R\s+777\s/, reason: '递归修改权限为 777' },
105
+ { pattern: /^\s*rm\s+-rf\s+(?!\/|~)/, reason: '递归强制删除文件/目录' },
106
+ { pattern: /git\s+push\s+(-f|--force)/, reason: 'Git 强制推送' },
107
+ { pattern: /^\s*(shutdown|reboot|halt|poweroff)/, reason: '关机或重启系统' },
108
+ { pattern: /^\s*>\s*\/etc\/hosts/, reason: '修改系统 hosts 文件' },
109
+ { pattern: /\beval\s+/, reason: '使用 eval 执行命令' },
110
+ ];
111
+
112
+ /**
113
+ * 检查命令安全性
114
+ * @param {boolean} [force=false] - 强制放行(用户确认后的调用)
115
+ * @returns {{ safe: boolean, level: 'allow'|'confirm'|'deny', reason?: string }}
116
+ */
117
+ function checkCommand(command, force = false) {
118
+ if (!command || typeof command !== 'string') {
119
+ return { safe: false, level: 'deny', reason: '命令无效' };
120
+ }
121
+
122
+ const trimmed = command.trim();
123
+
124
+ // 强制模式:跳过所有检查
125
+ if (force) {
126
+ return { safe: true, level: 'allow' };
127
+ }
128
+
129
+ // 1. 先检查黑名单
130
+ for (const pattern of BLACKLIST_PATTERNS) {
131
+ if (pattern.test(trimmed)) {
132
+ return {
133
+ safe: false,
134
+ level: 'deny',
135
+ reason: `高危命令被拦截: "${trimmed.substring(0, 100)}"`
136
+ };
137
+ }
138
+ }
139
+
140
+ // 2. 检查灰名单
141
+ for (const entry of GRAYLIST_PATTERNS) {
142
+ if (entry.pattern.test(trimmed)) {
143
+ return {
144
+ safe: false,
145
+ level: 'confirm',
146
+ reason: entry.reason
147
+ };
148
+ }
149
+ }
150
+
151
+ // 3. 通过安全检查
152
+ return { safe: true, level: 'allow' };
153
+ }
154
+
155
+ export { checkPath, checkCommand };
package/src/server.js ADDED
@@ -0,0 +1,319 @@
1
+ // agent/src/server.js - HTTP Router + WebSocket 服务器
2
+ import http from 'http';
3
+ import { WebSocketServer } from 'ws';
4
+ import { readFileSync, writeFileSync, readdirSync, statSync, unlinkSync, rmdirSync, existsSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { loadConfig } from './config.js';
7
+ import { verifyToken, getCurrentPairCode, startPairCodeRotation, stopPairCodeRotation, handlePairRequest } from './auth.js';
8
+ import { checkPath, checkCommand } from './security.js';
9
+ import { executeCommand, executeCommandSync, addWsClient, killProcess, getRunningProcesses } from './executor.js';
10
+
11
+ const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
12
+
13
+ /**
14
+ * JSON 响应辅助
15
+ */
16
+ function jsonResponse(res, status, data) {
17
+ res.writeHead(status, {
18
+ 'Content-Type': 'application/json; charset=utf-8',
19
+ 'Access-Control-Allow-Origin': '*',
20
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
21
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
22
+ });
23
+ res.end(JSON.stringify(data));
24
+ }
25
+
26
+ /**
27
+ * 解析请求 body(带大小限制)
28
+ */
29
+ function parseBody(req) {
30
+ return new Promise((resolve, reject) => {
31
+ const contentLength = parseInt(req.headers['content-length'] || '0', 10);
32
+ if (contentLength > MAX_BODY_SIZE) {
33
+ reject(new Error('请求体过大'));
34
+ return;
35
+ }
36
+
37
+ const chunks = [];
38
+ let totalSize = 0;
39
+
40
+ req.on('data', (chunk) => {
41
+ totalSize += chunk.length;
42
+ if (totalSize > MAX_BODY_SIZE) {
43
+ reject(new Error('请求体过大'));
44
+ req.destroy();
45
+ return;
46
+ }
47
+ chunks.push(chunk);
48
+ });
49
+
50
+ req.on('end', () => {
51
+ const body = Buffer.concat(chunks).toString();
52
+ try { resolve(JSON.parse(body)); }
53
+ catch { resolve({}); }
54
+ });
55
+
56
+ req.on('error', () => reject(new Error('读取请求失败')));
57
+ });
58
+ }
59
+
60
+ /**
61
+ * 创建并启动服务器
62
+ */
63
+ export function startServer() {
64
+ const config = loadConfig();
65
+ const { port, host } = config;
66
+
67
+ // ==================== HTTP Server ====================
68
+ const server = http.createServer(async (req, res) => {
69
+ // CORS 预检
70
+ if (req.method === 'OPTIONS') {
71
+ res.writeHead(204, {
72
+ 'Access-Control-Allow-Origin': '*',
73
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
74
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
75
+ });
76
+ return res.end();
77
+ }
78
+
79
+ const url = new URL(req.url, `http://${host}:${port}`);
80
+ const pathname = url.pathname;
81
+
82
+ // ---------- 无需认证的接口 ----------
83
+
84
+ // 配对
85
+ if (req.method === 'POST' && pathname === '/api/pair') {
86
+ let body;
87
+ try { body = await parseBody(req); }
88
+ catch (err) { return jsonResponse(res, 400, { success: false, error: err.message }); }
89
+ const result = handlePairRequest(body.code, body.extensionId);
90
+ return jsonResponse(res, result.success ? 200 : 400, result);
91
+ }
92
+
93
+ // 健康检查(不含敏感信息)
94
+ if (req.method === 'GET' && pathname === '/api/status') {
95
+ return jsonResponse(res, 200, {
96
+ success: true,
97
+ version: '1.0.0',
98
+ running: true
99
+ });
100
+ }
101
+
102
+ // ---------- 需要认证的接口 ----------
103
+ const authHeader = req.headers.authorization;
104
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
105
+ return jsonResponse(res, 401, { success: false, error: '未提供认证 token' });
106
+ }
107
+ const token = authHeader.slice(7);
108
+ const extId = verifyToken(token);
109
+ if (!extId) {
110
+ return jsonResponse(res, 403, { success: false, error: '认证 token 无效' });
111
+ }
112
+
113
+ const maxSize = config.fileMaxSize;
114
+
115
+ // 认证后的状态信息(含敏感信息)
116
+ if (req.method === 'GET' && pathname === '/api/status/detail') {
117
+ return jsonResponse(res, 200, {
118
+ success: true,
119
+ version: '1.0.0',
120
+ pairCode: getCurrentPairCode(),
121
+ workdir: config.workdir,
122
+ runningProcesses: getRunningProcesses()
123
+ });
124
+ }
125
+
126
+ if (req.method === 'POST') {
127
+ let body;
128
+ try { body = await parseBody(req); }
129
+ catch (err) { return jsonResponse(res, 400, { success: false, error: err.message }); }
130
+
131
+ // === 文件操作 ===
132
+
133
+ // 读取文件
134
+ if (pathname === '/api/fs/read') {
135
+ const check = checkPath(body.path);
136
+ if (!check.allowed) return jsonResponse(res, 403, { success: false, error: check.reason });
137
+ if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '文件不存在' });
138
+ const stat = statSync(check.resolved);
139
+ if (stat.isDirectory()) return jsonResponse(res, 400, { success: false, error: '路径是目录' });
140
+ if (stat.size > maxSize) return jsonResponse(res, 400, { success: false, error: `文件过大 (${stat.size} > ${maxSize})` });
141
+ const content = readFileSync(check.resolved, 'utf-8');
142
+ return jsonResponse(res, 200, { success: true, content, size: stat.size, path: check.resolved });
143
+ }
144
+
145
+ // 写入文件
146
+ if (pathname === '/api/fs/write') {
147
+ const check = checkPath(body.path);
148
+ if (!check.allowed) return jsonResponse(res, 403, { success: false, error: check.reason });
149
+ const buf = Buffer.from(body.content || '', 'utf-8');
150
+ if (buf.length > maxSize) return jsonResponse(res, 400, { success: false, error: `内容过大 (${buf.length} > ${maxSize})` });
151
+ writeFileSync(check.resolved, body.content || '', 'utf-8');
152
+ return jsonResponse(res, 200, { success: true, size: buf.length, path: check.resolved });
153
+ }
154
+
155
+ // 列出目录
156
+ if (pathname === '/api/fs/list') {
157
+ const check = checkPath(body.path || '.');
158
+ if (!check.allowed) return jsonResponse(res, 403, { success: false, error: check.reason });
159
+ if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '目录不存在' });
160
+ const stat = statSync(check.resolved);
161
+ if (!stat.isDirectory()) return jsonResponse(res, 400, { success: false, error: '路径不是目录' });
162
+ const entries = readdirSync(check.resolved).map(name => {
163
+ const fullPath = join(check.resolved, name);
164
+ try {
165
+ const s = statSync(fullPath);
166
+ return { name, type: s.isDirectory() ? 'directory' : 'file', size: s.size, mtime: s.mtimeMs };
167
+ } catch { return { name, type: 'unknown', size: 0, mtime: 0 }; }
168
+ });
169
+ return jsonResponse(res, 200, { success: true, entries, path: check.resolved });
170
+ }
171
+
172
+ // 删除文件/目录
173
+ if (pathname === '/api/fs/delete') {
174
+ const check = checkPath(body.path);
175
+ if (!check.allowed) return jsonResponse(res, 403, { success: false, error: check.reason });
176
+ if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '文件/目录不存在' });
177
+ const stat = statSync(check.resolved);
178
+ if (stat.isDirectory()) {
179
+ rmdirSync(check.resolved, { recursive: true });
180
+ } else {
181
+ unlinkSync(check.resolved);
182
+ }
183
+ return jsonResponse(res, 200, { success: true, path: check.resolved });
184
+ }
185
+
186
+ // === 命令执行 ===
187
+
188
+ // 发起命令执行
189
+ if (pathname === '/api/exec') {
190
+ const { command, cwd, wait, force } = body;
191
+ if (!command) return jsonResponse(res, 400, { success: false, error: '缺少 command 参数' });
192
+
193
+ // 校验 cwd
194
+ let resolvedCwd = cwd || config.workdir;
195
+ const cwdCheck = checkPath(resolvedCwd);
196
+ if (!cwdCheck.allowed) {
197
+ return jsonResponse(res, 403, { success: false, error: '执行目录校验失败: ' + cwdCheck.reason });
198
+ }
199
+
200
+ // 安全检查
201
+ const cmdCheck = checkCommand(command, force);
202
+ if (cmdCheck.level === 'deny') {
203
+ return jsonResponse(res, 403, { success: false, error: cmdCheck.reason, level: 'deny' });
204
+ }
205
+ if (cmdCheck.level === 'confirm') {
206
+ return jsonResponse(res, 200, { success: true, level: 'confirm', reason: cmdCheck.reason, command, cwd });
207
+ }
208
+
209
+ // 同步等待模式(用于扩展端获取完整输出)
210
+ if (wait) {
211
+ try {
212
+ const result = await executeCommandSync(command, resolvedCwd);
213
+ return jsonResponse(res, 200, {
214
+ success: true,
215
+ level: 'allow',
216
+ execId: result.execId,
217
+ exitCode: result.exitCode,
218
+ stdout: result.stdout || '',
219
+ stderr: result.stderr || '',
220
+ killed: result.killed,
221
+ error: result.error
222
+ });
223
+ } catch (err) {
224
+ return jsonResponse(res, 500, { success: false, error: `命令执行异常: ${err.message}` });
225
+ }
226
+ }
227
+
228
+ // 异步模式(返回 execId,适用于 WebSocket 场景)
229
+ const execId = executeCommand(command, resolvedCwd, null, null);
230
+ return jsonResponse(res, 200, {
231
+ success: true,
232
+ level: 'allow',
233
+ execId,
234
+ wsUrl: `ws://${host}:${port}/ws/exec/${execId}`
235
+ });
236
+ }
237
+
238
+ // 停止命令
239
+ if (pathname === '/api/exec/stop') {
240
+ const stopped = killProcess(body.execId);
241
+ return jsonResponse(res, 200, { success: stopped, execId: body.execId });
242
+ }
243
+ }
244
+
245
+ // 运行中进程列表(需认证)
246
+ if (req.method === 'GET' && pathname === '/api/exec/running') {
247
+ return jsonResponse(res, 200, { success: true, processes: getRunningProcesses() });
248
+ }
249
+
250
+ // Agent 关闭(需认证,POST 方法)
251
+ if (req.method === 'POST' && pathname === '/api/shutdown') {
252
+ jsonResponse(res, 200, { success: true, message: 'Agent 正在关闭...' });
253
+ shutdown();
254
+ return;
255
+ }
256
+
257
+ // 404
258
+ jsonResponse(res, 404, { success: false, error: '未知的 API 路径' });
259
+ });
260
+
261
+ // ==================== WebSocket Server ====================
262
+ const wss = new WebSocketServer({ noServer: true });
263
+
264
+ server.on('upgrade', (request, socket, head) => {
265
+ const url = new URL(request.url, `http://${host}:${port}`);
266
+ const pathParts = url.pathname.split('/');
267
+
268
+ if (pathParts[1] === 'ws' && pathParts[2] === 'exec') {
269
+ const execId = pathParts[3];
270
+
271
+ // Token 验证(从 Authorization header 或 query param)
272
+ const authHeader = request.headers.authorization;
273
+ let token = null;
274
+ if (authHeader && authHeader.startsWith('Bearer ')) {
275
+ token = authHeader.slice(7);
276
+ } else {
277
+ token = url.searchParams.get('token');
278
+ }
279
+ if (!verifyToken(token)) {
280
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
281
+ socket.destroy();
282
+ return;
283
+ }
284
+
285
+ wss.handleUpgrade(request, socket, head, (ws) => {
286
+ const added = addWsClient(execId, ws);
287
+ if (!added) {
288
+ ws.send(JSON.stringify({ type: 'error', error: '进程不存在或已结束', execId }));
289
+ }
290
+ });
291
+ } else {
292
+ socket.destroy();
293
+ }
294
+ });
295
+
296
+ // 启动服务器
297
+ server.listen(port, host, () => {
298
+ console.log(`[Agent] HTTP 服务已启动: http://${host}:${port}`);
299
+ console.log(`[Agent] WebSocket 服务已启动: ws://${host}:${port}`);
300
+ startPairCodeRotation();
301
+ });
302
+
303
+ // 优雅关闭
304
+ function shutdown() {
305
+ console.log('[Agent] 正在关闭...');
306
+ stopPairCodeRotation();
307
+ for (const entry of getRunningProcesses()) {
308
+ killProcess(entry.execId);
309
+ }
310
+ wss.close();
311
+ server.close();
312
+ process.exit(0);
313
+ }
314
+
315
+ process.on('SIGINT', shutdown);
316
+ process.on('SIGTERM', shutdown);
317
+
318
+ return { server, wss, shutdown };
319
+ }