ai-helper-agent 1.0.0 → 1.1.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 +46 -0
- package/package.json +1 -1
- package/src/logger.js +149 -0
- package/src/server.js +88 -21
package/README.md
CHANGED
|
@@ -105,6 +105,50 @@ ai-helper-agent start --port 18911
|
|
|
105
105
|
| `commandTimeout` | 命令执行超时(毫秒) | 300000(5分钟) |
|
|
106
106
|
| `fileMaxSize` | 文件读写最大字节数 | 52428800(50MB) |
|
|
107
107
|
|
|
108
|
+
## 审计日志
|
|
109
|
+
|
|
110
|
+
所有操作自动记录到审计日志中,日志文件位于 `~/.ai-helper-agent/logs/`。
|
|
111
|
+
|
|
112
|
+
### 日志格式
|
|
113
|
+
|
|
114
|
+
JSON Lines 格式,每行一条记录,文件按日命名 `agent-YYYY-MM-DD.log`。
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{"timestamp":"2026-01-15T10:30:00.123Z","level":"info","category":"fs","action":"read","path":"/home/user/project/src/index.js","size":2048}
|
|
118
|
+
{"timestamp":"2026-01-15T10:30:05.456Z","level":"info","category":"exec","action":"completed","command":"npm test","cwd":"/home/user/project","execId":"a1b2c3d4","exitCode":0,"killed":false,"stdoutLen":1024,"stderrLen":0}
|
|
119
|
+
{"timestamp":"2026-01-15T10:31:00.789Z","level":"warn","category":"security","action":"exec_denied","command":"rm -rf /","reason":"高危命令被拦截"}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 日志分类
|
|
123
|
+
|
|
124
|
+
| category | 说明 | 包含的操作 |
|
|
125
|
+
|----------|------|-----------|
|
|
126
|
+
| `auth` | 认证事件 | 配对成功/失败 |
|
|
127
|
+
| `fs` | 文件操作 | read, write, list, delete |
|
|
128
|
+
| `exec` | 命令执行 | started, completed, stopped, error |
|
|
129
|
+
| `security` | 安全事件 | deny, confirm, auth 失败, 路径越权拦截 |
|
|
130
|
+
| `system` | 系统事件 | server_start, server_stop, shutdown |
|
|
131
|
+
|
|
132
|
+
### 日志查询 API
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
GET /api/logs?date=2026-01-15&category=security&limit=50&offset=0
|
|
136
|
+
GET /api/logs/dates
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `date` - 日期筛选 (YYYY-MM-DD),不传默认今天
|
|
140
|
+
- `category` - 分类筛选,不传返回全部
|
|
141
|
+
- `limit` - 返回条数上限,默认 200
|
|
142
|
+
- `offset` - 分页偏移
|
|
143
|
+
|
|
144
|
+
返回按时间倒序排列(最新的在前)。
|
|
145
|
+
|
|
146
|
+
### 自动清理
|
|
147
|
+
|
|
148
|
+
- 最多保留 30 个日志文件
|
|
149
|
+
- 单文件超过 10MB 自动删除
|
|
150
|
+
- 每次写入日志时触发检查
|
|
151
|
+
|
|
108
152
|
## API 端点
|
|
109
153
|
|
|
110
154
|
### 无需认证
|
|
@@ -127,6 +171,8 @@ ai-helper-agent start --port 18911
|
|
|
127
171
|
| GET | `/api/exec/running` | 运行中的进程列表 |
|
|
128
172
|
| GET | `/api/status/detail` | 详细信息(含工作目录) |
|
|
129
173
|
| POST | `/api/shutdown` | 关闭 Agent 服务 |
|
|
174
|
+
| GET | `/api/logs` | 查询审计日志(支持 date/category/limit/offset 参数) |
|
|
175
|
+
| GET | `/api/logs/dates` | 获取可用日志日期列表 |
|
|
130
176
|
|
|
131
177
|
### WebSocket
|
|
132
178
|
|
package/package.json
CHANGED
package/src/logger.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// agent/src/logger.js - 审计日志模块(JSON Lines 格式,按日轮转,自动清理)
|
|
2
|
+
import { appendFileSync, mkdirSync, existsSync, readdirSync, statSync, unlinkSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
|
|
6
|
+
const LOG_DIR = join(homedir(), '.ai-helper-agent', 'logs');
|
|
7
|
+
const MAX_LOG_FILES = 30; // 最多保留 30 个日志文件
|
|
8
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 单文件最大 10MB
|
|
9
|
+
|
|
10
|
+
function ensureLogDir() {
|
|
11
|
+
if (!existsSync(LOG_DIR)) {
|
|
12
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 获取当天日志文件路径
|
|
18
|
+
*/
|
|
19
|
+
function getLogFile() {
|
|
20
|
+
const now = new Date();
|
|
21
|
+
const date = now.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
22
|
+
return join(LOG_DIR, `agent-${date}.log`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 清理旧日志文件(保留最近 MAX_LOG_FILES 个,删除超大文件)
|
|
27
|
+
*/
|
|
28
|
+
function cleanOldLogs() {
|
|
29
|
+
try {
|
|
30
|
+
const files = readdirSync(LOG_DIR)
|
|
31
|
+
.filter(f => f.endsWith('.log'))
|
|
32
|
+
.map(f => {
|
|
33
|
+
const path = join(LOG_DIR, f);
|
|
34
|
+
const stat = statSync(path);
|
|
35
|
+
return { name: f, path, mtime: stat.mtimeMs, size: stat.size };
|
|
36
|
+
})
|
|
37
|
+
.sort((a, b) => b.mtime - a.mtime); // 最新的在前面
|
|
38
|
+
|
|
39
|
+
let deleted = 0;
|
|
40
|
+
for (let i = 0; i < files.length; i++) {
|
|
41
|
+
const f = files[i];
|
|
42
|
+
// 超过保留数量,或单文件超过大小限制
|
|
43
|
+
if (i >= MAX_LOG_FILES || f.size > MAX_LOG_SIZE) {
|
|
44
|
+
try { unlinkSync(f.path); deleted++; } catch {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (deleted > 0) {
|
|
48
|
+
console.log(`[Logger] 清理了 ${deleted} 个旧日志文件`);
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 写入一条日志
|
|
55
|
+
* @param {'info'|'warn'|'error'} level - 日志级别
|
|
56
|
+
* @param {'auth'|'fs'|'exec'|'security'|'system'} category - 日志分类
|
|
57
|
+
* @param {string} action - 操作名称
|
|
58
|
+
* @param {Object} details - 详细信息
|
|
59
|
+
*/
|
|
60
|
+
function log(level, category, action, details = {}) {
|
|
61
|
+
try {
|
|
62
|
+
ensureLogDir();
|
|
63
|
+
cleanOldLogs(); // 每次写入时顺便清理(频率不高,影响可忽略)
|
|
64
|
+
|
|
65
|
+
const entry = {
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
level,
|
|
68
|
+
category,
|
|
69
|
+
action,
|
|
70
|
+
...details
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
appendFileSync(getLogFile(), JSON.stringify(entry) + '\n', 'utf-8');
|
|
74
|
+
} catch (err) {
|
|
75
|
+
// 日志写入失败不应影响主流程
|
|
76
|
+
console.error('[Logger] 日志写入失败:', err.message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ==================== 便捷方法 ====================
|
|
81
|
+
|
|
82
|
+
function logAuth(action, details) { log('info', 'auth', action, details); }
|
|
83
|
+
function logFs(action, details) { log('info', 'fs', action, details); }
|
|
84
|
+
function logExec(action, details) { log('info', 'exec', action, details); }
|
|
85
|
+
function logSecurity(action, details) { log('warn', 'security', action, details); }
|
|
86
|
+
function logSystem(action, details) { log('info', 'system', action, details); }
|
|
87
|
+
function logError(category, action, details) { log('error', category, action, details); }
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 查询日志(用于 /api/logs 端点)
|
|
91
|
+
* @param {Object} options - 查询选项
|
|
92
|
+
* @param {string} [options.date] - 日期 (YYYY-MM-DD)
|
|
93
|
+
* @param {string} [options.category] - 分类过滤
|
|
94
|
+
* @param {number} [options.limit=200] - 返回条数上限
|
|
95
|
+
* @param {number} [options.offset=0] - 偏移量
|
|
96
|
+
* @returns {{ entries: Array, total: number }}
|
|
97
|
+
*/
|
|
98
|
+
function queryLogs(options = {}) {
|
|
99
|
+
const { date, category, limit = 200, offset = 0 } = options;
|
|
100
|
+
|
|
101
|
+
let filePath;
|
|
102
|
+
if (date) {
|
|
103
|
+
filePath = join(LOG_DIR, `agent-${date}.log`);
|
|
104
|
+
} else {
|
|
105
|
+
filePath = getLogFile();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let entries = [];
|
|
109
|
+
try {
|
|
110
|
+
if (existsSync(filePath)) {
|
|
111
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
112
|
+
const lines = raw.trim().split('\n');
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
try {
|
|
115
|
+
const entry = JSON.parse(line);
|
|
116
|
+
if (category && entry.category !== category) continue;
|
|
117
|
+
entries.push(entry);
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
|
|
123
|
+
// 按时间倒序(最新的在前)
|
|
124
|
+
entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
125
|
+
|
|
126
|
+
const total = entries.length;
|
|
127
|
+
entries = entries.slice(offset, offset + limit);
|
|
128
|
+
|
|
129
|
+
return { entries, total };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取可用日志日期列表
|
|
134
|
+
*/
|
|
135
|
+
function getLogDates() {
|
|
136
|
+
try {
|
|
137
|
+
ensureLogDir();
|
|
138
|
+
const files = readdirSync(LOG_DIR)
|
|
139
|
+
.filter(f => f.match(/^agent-\d{4}-\d{2}-\d{2}\.log$/))
|
|
140
|
+
.map(f => f.replace('agent-', '').replace('.log', ''))
|
|
141
|
+
.sort()
|
|
142
|
+
.reverse();
|
|
143
|
+
return files;
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { log, logAuth, logFs, logExec, logSecurity, logSystem, logError, queryLogs, getLogDates };
|
package/src/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadConfig } from './config.js';
|
|
|
7
7
|
import { verifyToken, getCurrentPairCode, startPairCodeRotation, stopPairCodeRotation, handlePairRequest } from './auth.js';
|
|
8
8
|
import { checkPath, checkCommand } from './security.js';
|
|
9
9
|
import { executeCommand, executeCommandSync, addWsClient, killProcess, getRunningProcesses } from './executor.js';
|
|
10
|
+
import { logAuth, logFs, logExec, logSecurity, logSystem, logError, queryLogs, getLogDates } from './logger.js';
|
|
10
11
|
|
|
11
12
|
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
|
|
12
13
|
|
|
@@ -64,6 +65,8 @@ export function startServer() {
|
|
|
64
65
|
const config = loadConfig();
|
|
65
66
|
const { port, host } = config;
|
|
66
67
|
|
|
68
|
+
logSystem('server_start', { port, host, workdir: config.workdir });
|
|
69
|
+
|
|
67
70
|
// ==================== HTTP Server ====================
|
|
68
71
|
const server = http.createServer(async (req, res) => {
|
|
69
72
|
// CORS 预检
|
|
@@ -87,10 +90,15 @@ export function startServer() {
|
|
|
87
90
|
try { body = await parseBody(req); }
|
|
88
91
|
catch (err) { return jsonResponse(res, 400, { success: false, error: err.message }); }
|
|
89
92
|
const result = handlePairRequest(body.code, body.extensionId);
|
|
93
|
+
if (result.success) {
|
|
94
|
+
logAuth('pair_success', { extensionId: body.extensionId });
|
|
95
|
+
} else {
|
|
96
|
+
logAuth('pair_failed', { extensionId: body.extensionId, reason: result.error });
|
|
97
|
+
}
|
|
90
98
|
return jsonResponse(res, result.success ? 200 : 400, result);
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
//
|
|
101
|
+
// 健康检查
|
|
94
102
|
if (req.method === 'GET' && pathname === '/api/status') {
|
|
95
103
|
return jsonResponse(res, 200, {
|
|
96
104
|
success: true,
|
|
@@ -102,17 +110,19 @@ export function startServer() {
|
|
|
102
110
|
// ---------- 需要认证的接口 ----------
|
|
103
111
|
const authHeader = req.headers.authorization;
|
|
104
112
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
113
|
+
logSecurity('auth_missing', { path: pathname });
|
|
105
114
|
return jsonResponse(res, 401, { success: false, error: '未提供认证 token' });
|
|
106
115
|
}
|
|
107
116
|
const token = authHeader.slice(7);
|
|
108
117
|
const extId = verifyToken(token);
|
|
109
118
|
if (!extId) {
|
|
119
|
+
logSecurity('auth_invalid', { path: pathname });
|
|
110
120
|
return jsonResponse(res, 403, { success: false, error: '认证 token 无效' });
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
const maxSize = config.fileMaxSize;
|
|
114
124
|
|
|
115
|
-
//
|
|
125
|
+
// 认证后的状态信息
|
|
116
126
|
if (req.method === 'GET' && pathname === '/api/status/detail') {
|
|
117
127
|
return jsonResponse(res, 200, {
|
|
118
128
|
success: true,
|
|
@@ -133,32 +143,44 @@ export function startServer() {
|
|
|
133
143
|
// 读取文件
|
|
134
144
|
if (pathname === '/api/fs/read') {
|
|
135
145
|
const check = checkPath(body.path);
|
|
136
|
-
if (!check.allowed)
|
|
146
|
+
if (!check.allowed) {
|
|
147
|
+
logSecurity('fs_read_blocked', { path: body.path, reason: check.reason });
|
|
148
|
+
return jsonResponse(res, 403, { success: false, error: check.reason });
|
|
149
|
+
}
|
|
137
150
|
if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '文件不存在' });
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
if (
|
|
151
|
+
const fstat = statSync(check.resolved);
|
|
152
|
+
if (fstat.isDirectory()) return jsonResponse(res, 400, { success: false, error: '路径是目录' });
|
|
153
|
+
if (fstat.size > maxSize) return jsonResponse(res, 400, { success: false, error: `文件过大 (${fstat.size} > ${maxSize})` });
|
|
141
154
|
const content = readFileSync(check.resolved, 'utf-8');
|
|
142
|
-
|
|
155
|
+
logFs('read', { path: check.resolved, size: fstat.size });
|
|
156
|
+
return jsonResponse(res, 200, { success: true, content, size: fstat.size, path: check.resolved });
|
|
143
157
|
}
|
|
144
158
|
|
|
145
159
|
// 写入文件
|
|
146
160
|
if (pathname === '/api/fs/write') {
|
|
147
161
|
const check = checkPath(body.path);
|
|
148
|
-
if (!check.allowed)
|
|
162
|
+
if (!check.allowed) {
|
|
163
|
+
logSecurity('fs_write_blocked', { path: body.path, reason: check.reason });
|
|
164
|
+
return jsonResponse(res, 403, { success: false, error: check.reason });
|
|
165
|
+
}
|
|
149
166
|
const buf = Buffer.from(body.content || '', 'utf-8');
|
|
150
167
|
if (buf.length > maxSize) return jsonResponse(res, 400, { success: false, error: `内容过大 (${buf.length} > ${maxSize})` });
|
|
151
168
|
writeFileSync(check.resolved, body.content || '', 'utf-8');
|
|
169
|
+
logFs('write', { path: check.resolved, size: buf.length });
|
|
152
170
|
return jsonResponse(res, 200, { success: true, size: buf.length, path: check.resolved });
|
|
153
171
|
}
|
|
154
172
|
|
|
155
173
|
// 列出目录
|
|
156
174
|
if (pathname === '/api/fs/list') {
|
|
157
|
-
const
|
|
158
|
-
|
|
175
|
+
const dirPath = body.path || '.';
|
|
176
|
+
const check = checkPath(dirPath);
|
|
177
|
+
if (!check.allowed) {
|
|
178
|
+
logSecurity('fs_list_blocked', { path: dirPath, reason: check.reason });
|
|
179
|
+
return jsonResponse(res, 403, { success: false, error: check.reason });
|
|
180
|
+
}
|
|
159
181
|
if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '目录不存在' });
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
182
|
+
const dstat = statSync(check.resolved);
|
|
183
|
+
if (!dstat.isDirectory()) return jsonResponse(res, 400, { success: false, error: '路径不是目录' });
|
|
162
184
|
const entries = readdirSync(check.resolved).map(name => {
|
|
163
185
|
const fullPath = join(check.resolved, name);
|
|
164
186
|
try {
|
|
@@ -166,26 +188,31 @@ export function startServer() {
|
|
|
166
188
|
return { name, type: s.isDirectory() ? 'directory' : 'file', size: s.size, mtime: s.mtimeMs };
|
|
167
189
|
} catch { return { name, type: 'unknown', size: 0, mtime: 0 }; }
|
|
168
190
|
});
|
|
191
|
+
logFs('list', { path: check.resolved, entryCount: entries.length });
|
|
169
192
|
return jsonResponse(res, 200, { success: true, entries, path: check.resolved });
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
// 删除文件/目录
|
|
173
196
|
if (pathname === '/api/fs/delete') {
|
|
174
197
|
const check = checkPath(body.path);
|
|
175
|
-
if (!check.allowed)
|
|
198
|
+
if (!check.allowed) {
|
|
199
|
+
logSecurity('fs_delete_blocked', { path: body.path, reason: check.reason });
|
|
200
|
+
return jsonResponse(res, 403, { success: false, error: check.reason });
|
|
201
|
+
}
|
|
176
202
|
if (!existsSync(check.resolved)) return jsonResponse(res, 404, { success: false, error: '文件/目录不存在' });
|
|
177
|
-
const
|
|
178
|
-
|
|
203
|
+
const fstat2 = statSync(check.resolved);
|
|
204
|
+
const isDir = fstat2.isDirectory();
|
|
205
|
+
if (isDir) {
|
|
179
206
|
rmdirSync(check.resolved, { recursive: true });
|
|
180
207
|
} else {
|
|
181
208
|
unlinkSync(check.resolved);
|
|
182
209
|
}
|
|
210
|
+
logFs('delete', { path: check.resolved, type: isDir ? 'directory' : 'file', size: fstat2.size });
|
|
183
211
|
return jsonResponse(res, 200, { success: true, path: check.resolved });
|
|
184
212
|
}
|
|
185
213
|
|
|
186
214
|
// === 命令执行 ===
|
|
187
215
|
|
|
188
|
-
// 发起命令执行
|
|
189
216
|
if (pathname === '/api/exec') {
|
|
190
217
|
const { command, cwd, wait, force } = body;
|
|
191
218
|
if (!command) return jsonResponse(res, 400, { success: false, error: '缺少 command 参数' });
|
|
@@ -194,22 +221,34 @@ export function startServer() {
|
|
|
194
221
|
let resolvedCwd = cwd || config.workdir;
|
|
195
222
|
const cwdCheck = checkPath(resolvedCwd);
|
|
196
223
|
if (!cwdCheck.allowed) {
|
|
224
|
+
logSecurity('exec_cwd_blocked', { command, cwd: resolvedCwd, reason: cwdCheck.reason });
|
|
197
225
|
return jsonResponse(res, 403, { success: false, error: '执行目录校验失败: ' + cwdCheck.reason });
|
|
198
226
|
}
|
|
199
227
|
|
|
200
228
|
// 安全检查
|
|
201
229
|
const cmdCheck = checkCommand(command, force);
|
|
202
230
|
if (cmdCheck.level === 'deny') {
|
|
231
|
+
logSecurity('exec_denied', { command, reason: cmdCheck.reason });
|
|
203
232
|
return jsonResponse(res, 403, { success: false, error: cmdCheck.reason, level: 'deny' });
|
|
204
233
|
}
|
|
205
234
|
if (cmdCheck.level === 'confirm') {
|
|
235
|
+
logSecurity('exec_confirm_required', { command, reason: cmdCheck.reason });
|
|
206
236
|
return jsonResponse(res, 200, { success: true, level: 'confirm', reason: cmdCheck.reason, command, cwd });
|
|
207
237
|
}
|
|
208
238
|
|
|
209
|
-
//
|
|
239
|
+
// 同步等待模式
|
|
210
240
|
if (wait) {
|
|
211
241
|
try {
|
|
212
242
|
const result = await executeCommandSync(command, resolvedCwd);
|
|
243
|
+
logExec('completed', {
|
|
244
|
+
command,
|
|
245
|
+
cwd: resolvedCwd,
|
|
246
|
+
execId: result.execId,
|
|
247
|
+
exitCode: result.exitCode,
|
|
248
|
+
killed: result.killed,
|
|
249
|
+
stdoutLen: (result.stdout || '').length,
|
|
250
|
+
stderrLen: (result.stderr || '').length
|
|
251
|
+
});
|
|
213
252
|
return jsonResponse(res, 200, {
|
|
214
253
|
success: true,
|
|
215
254
|
level: 'allow',
|
|
@@ -221,12 +260,22 @@ export function startServer() {
|
|
|
221
260
|
error: result.error
|
|
222
261
|
});
|
|
223
262
|
} catch (err) {
|
|
263
|
+
logError('exec', 'error', { command, error: err.message });
|
|
224
264
|
return jsonResponse(res, 500, { success: false, error: `命令执行异常: ${err.message}` });
|
|
225
265
|
}
|
|
226
266
|
}
|
|
227
267
|
|
|
228
|
-
//
|
|
229
|
-
const execId = executeCommand(command, resolvedCwd, null,
|
|
268
|
+
// 异步模式
|
|
269
|
+
const execId = executeCommand(command, resolvedCwd, null, (result) => {
|
|
270
|
+
logExec('completed', {
|
|
271
|
+
command,
|
|
272
|
+
cwd: resolvedCwd,
|
|
273
|
+
execId: result.execId,
|
|
274
|
+
exitCode: result.exitCode,
|
|
275
|
+
killed: result.killed
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
logExec('started', { command, cwd: resolvedCwd, execId });
|
|
230
279
|
return jsonResponse(res, 200, {
|
|
231
280
|
success: true,
|
|
232
281
|
level: 'allow',
|
|
@@ -238,6 +287,7 @@ export function startServer() {
|
|
|
238
287
|
// 停止命令
|
|
239
288
|
if (pathname === '/api/exec/stop') {
|
|
240
289
|
const stopped = killProcess(body.execId);
|
|
290
|
+
logExec('stopped', { execId: body.execId, success: stopped });
|
|
241
291
|
return jsonResponse(res, 200, { success: stopped, execId: body.execId });
|
|
242
292
|
}
|
|
243
293
|
}
|
|
@@ -247,8 +297,24 @@ export function startServer() {
|
|
|
247
297
|
return jsonResponse(res, 200, { success: true, processes: getRunningProcesses() });
|
|
248
298
|
}
|
|
249
299
|
|
|
250
|
-
//
|
|
300
|
+
// 日志查询(需认证)
|
|
301
|
+
if (req.method === 'GET' && pathname === '/api/logs') {
|
|
302
|
+
const date = url.searchParams.get('date') || undefined;
|
|
303
|
+
const category = url.searchParams.get('category') || undefined;
|
|
304
|
+
const limit = parseInt(url.searchParams.get('limit') || '200', 10);
|
|
305
|
+
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
|
306
|
+
const result = queryLogs({ date, category, limit, offset });
|
|
307
|
+
return jsonResponse(res, 200, { success: true, ...result });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 日志日期列表(需认证)
|
|
311
|
+
if (req.method === 'GET' && pathname === '/api/logs/dates') {
|
|
312
|
+
return jsonResponse(res, 200, { success: true, dates: getLogDates() });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Agent 关闭
|
|
251
316
|
if (req.method === 'POST' && pathname === '/api/shutdown') {
|
|
317
|
+
logSystem('shutdown', { reason: 'api_request' });
|
|
252
318
|
jsonResponse(res, 200, { success: true, message: 'Agent 正在关闭...' });
|
|
253
319
|
shutdown();
|
|
254
320
|
return;
|
|
@@ -268,7 +334,6 @@ export function startServer() {
|
|
|
268
334
|
if (pathParts[1] === 'ws' && pathParts[2] === 'exec') {
|
|
269
335
|
const execId = pathParts[3];
|
|
270
336
|
|
|
271
|
-
// Token 验证(从 Authorization header 或 query param)
|
|
272
337
|
const authHeader = request.headers.authorization;
|
|
273
338
|
let token = null;
|
|
274
339
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
@@ -277,6 +342,7 @@ export function startServer() {
|
|
|
277
342
|
token = url.searchParams.get('token');
|
|
278
343
|
}
|
|
279
344
|
if (!verifyToken(token)) {
|
|
345
|
+
logSecurity('ws_auth_invalid', { execId });
|
|
280
346
|
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
281
347
|
socket.destroy();
|
|
282
348
|
return;
|
|
@@ -309,6 +375,7 @@ export function startServer() {
|
|
|
309
375
|
}
|
|
310
376
|
wss.close();
|
|
311
377
|
server.close();
|
|
378
|
+
logSystem('server_stop', { reason: 'shutdown' });
|
|
312
379
|
process.exit(0);
|
|
313
380
|
}
|
|
314
381
|
|