ai-helper-agent 1.1.0 → 1.2.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/src/auth.js CHANGED
@@ -7,38 +7,10 @@ let currentPairCode = null;
7
7
  let pairCodeTimer = null;
8
8
 
9
9
  /**
10
- * 生成随机配对码(4位数字)
10
+ * 生成随机配对码(4位数字,使用密码学安全的随机数)
11
11
  */
12
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;
13
+ return String(crypto.randomInt(1000, 10000));
42
14
  }
43
15
 
44
16
  /**
@@ -76,9 +48,23 @@ function stopPairCodeRotation() {
76
48
  }
77
49
 
78
50
  /**
79
- * 处理配对请求
51
+ * 验证 token 是否有效
52
+ */
53
+ function verifyToken(token) {
54
+ if (!token || typeof token !== 'string') return null;
55
+ const pairings = loadPairings();
56
+ for (const [extId, pairing] of Object.entries(pairings)) {
57
+ if (pairing.token === token) {
58
+ return extId;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * 处理配对请求(异步,支持写入锁)
80
66
  */
81
- function handlePairRequest(code, extensionId) {
67
+ async function handlePairRequest(code, extensionId) {
82
68
  if (!code || !extensionId) {
83
69
  return { success: false, error: '缺少配对码或扩展ID' };
84
70
  }
@@ -92,33 +78,18 @@ function handlePairRequest(code, extensionId) {
92
78
  }
93
79
  // 生成新 token 并保存
94
80
  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' });
81
+ try {
82
+ await savePairing(extensionId, token);
83
+ } catch (err) {
84
+ return { success: false, error: `配对保存失败: ${err.message}` };
106
85
  }
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();
86
+ return { success: true, token, message: '配对成功' };
114
87
  }
115
88
 
116
89
  export {
117
- generateToken,
118
90
  verifyToken,
119
91
  getCurrentPairCode,
120
92
  startPairCodeRotation,
121
93
  stopPairCodeRotation,
122
- handlePairRequest,
123
- authMiddleware
94
+ handlePairRequest
124
95
  };
package/src/config.js CHANGED
@@ -1,4 +1,4 @@
1
- // agent/src/config.js - 配置管理(带内存缓存)
1
+ // agent/src/config.js - 配置管理(带内存缓存 + 写入互斥锁)
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
@@ -17,6 +17,8 @@ const DEFAULTS = {
17
17
  fileMaxSize: 50 * 1024 * 1024
18
18
  };
19
19
 
20
+ const VALID_CONFIG_KEYS = Object.keys(DEFAULTS);
21
+
20
22
  // 内存缓存
21
23
  let configCache = null;
22
24
  let configCacheMtime = 0;
@@ -24,9 +26,24 @@ let configCacheMtime = 0;
24
26
  let pairingsCache = null;
25
27
  let pairingsCacheMtime = 0;
26
28
 
29
+ // 写入互斥锁(防止并发写互相覆盖)
30
+ let writeLock = Promise.resolve();
31
+
32
+ function withWriteLock(fn) {
33
+ const prev = writeLock;
34
+ let release;
35
+ writeLock = new Promise(resolve => { release = resolve; });
36
+ return prev.then(() => fn().finally(release));
37
+ }
38
+
27
39
  function ensureAgentDir() {
28
40
  if (!existsSync(AGENT_DIR)) {
29
- mkdirSync(AGENT_DIR, { recursive: true });
41
+ try {
42
+ mkdirSync(AGENT_DIR, { recursive: true });
43
+ } catch (err) {
44
+ console.error('[Config] 无法创建 Agent 目录:', err.message);
45
+ throw err;
46
+ }
30
47
  }
31
48
  }
32
49
 
@@ -49,7 +66,7 @@ export function loadConfig() {
49
66
 
50
67
  if (!existsSync(CONFIG_FILE)) {
51
68
  const defaults = { ...DEFAULTS, allowedPaths: [DEFAULTS.workdir] };
52
- saveConfig(defaults);
69
+ saveConfigSync(defaults);
53
70
  configCache = defaults;
54
71
  configCacheMtime = getMtime(CONFIG_FILE);
55
72
  return defaults;
@@ -66,7 +83,8 @@ export function loadConfig() {
66
83
  configCache = { ...DEFAULTS, ...userConfig };
67
84
  configCacheMtime = mtime;
68
85
  return configCache;
69
- } catch {
86
+ } catch (err) {
87
+ console.error('[Config] 配置文件解析失败:', err.message);
70
88
  configCache = { ...DEFAULTS, allowedPaths: [DEFAULTS.workdir] };
71
89
  configCacheMtime = 0;
72
90
  return configCache;
@@ -74,23 +92,48 @@ export function loadConfig() {
74
92
  }
75
93
 
76
94
  /**
77
- * 保存配置(写入磁盘并更新缓存)
95
+ * 同步写入磁盘(内部方法,不加锁)
78
96
  */
79
- export function saveConfig(config) {
97
+ function saveConfigSync(config) {
80
98
  ensureAgentDir();
81
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
82
- configCache = config;
83
- configCacheMtime = getMtime(CONFIG_FILE);
99
+ const safe = {};
100
+ for (const key of VALID_CONFIG_KEYS) {
101
+ if (config[key] !== undefined) safe[key] = config[key];
102
+ }
103
+ writeFileSync(CONFIG_FILE, JSON.stringify(safe, null, 2), 'utf-8');
84
104
  }
85
105
 
86
106
  /**
87
- * 更新单字段配置
107
+ * 保存配置(写入磁盘并更新缓存,加写锁防并发覆盖)
88
108
  */
89
- export function updateConfigField(key, value) {
90
- const config = loadConfig();
91
- config[key] = value;
92
- saveConfig(config);
93
- return config;
109
+ export async function saveConfig(config) {
110
+ return withWriteLock(async () => {
111
+ try {
112
+ saveConfigSync(config);
113
+ // 从磁盘重新加载确保缓存与磁盘一致
114
+ const mtime = getMtime(CONFIG_FILE);
115
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
116
+ configCache = { ...DEFAULTS, ...JSON.parse(raw) };
117
+ configCacheMtime = mtime;
118
+ } catch (err) {
119
+ console.error('[Config] 保存配置失败:', err.message);
120
+ throw err;
121
+ }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * 更新单字段配置(加写锁防 TOCTOU)
127
+ */
128
+ export async function updateConfigField(key, value) {
129
+ if (!VALID_CONFIG_KEYS.includes(key)) {
130
+ throw new Error(`无效配置项: ${key}`);
131
+ }
132
+ return withWriteLock(async () => {
133
+ const config = loadConfig();
134
+ config[key] = value;
135
+ return saveConfig(config).then(() => config);
136
+ });
94
137
  }
95
138
 
96
139
  /**
@@ -114,7 +157,8 @@ export function loadPairings() {
114
157
  pairingsCache = JSON.parse(readFileSync(PAIRINGS_FILE, 'utf-8'));
115
158
  pairingsCacheMtime = mtime;
116
159
  return pairingsCache;
117
- } catch {
160
+ } catch (err) {
161
+ console.error('[Config] 配对文件解析失败:', err.message);
118
162
  pairingsCache = {};
119
163
  pairingsCacheMtime = 0;
120
164
  return {};
@@ -122,15 +166,22 @@ export function loadPairings() {
122
166
  }
123
167
 
124
168
  /**
125
- * 保存配对(写入磁盘并更新缓存)
169
+ * 保存配对(加写锁防并发覆盖)
126
170
  */
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);
171
+ export async function savePairing(extensionId, token) {
172
+ return withWriteLock(async () => {
173
+ const pairings = loadPairings();
174
+ pairings[extensionId] = { token, pairedAt: Date.now() };
175
+ ensureAgentDir();
176
+ try {
177
+ writeFileSync(PAIRINGS_FILE, JSON.stringify(pairings, null, 2), 'utf-8');
178
+ pairingsCache = pairings;
179
+ pairingsCacheMtime = getMtime(PAIRINGS_FILE);
180
+ } catch (err) {
181
+ console.error('[Config] 保存配对失败:', err.message);
182
+ throw err;
183
+ }
184
+ });
134
185
  }
135
186
 
136
187
  export { AGENT_DIR, CONFIG_FILE };
package/src/executor.js CHANGED
@@ -6,6 +6,40 @@ import { loadConfig } from './config.js';
6
6
  // 运行中的进程映射:execId → { process, wsClients: Set, timeoutId, forceKillId }
7
7
  const runningProcesses = new Map();
8
8
 
9
+ // 安全的子进程环境变量白名单(不泄露宿主敏感信息)
10
+ const SAFE_ENV_KEYS = [
11
+ 'PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'LC_CTYPE',
12
+ 'PWD', 'OLDPWD', 'NODE_PATH', 'MANPATH', 'INFOPATH',
13
+ 'XDG_SESSION_TYPE', 'DISPLAY', 'SSH_AUTH_SOCK', 'COLORTERM',
14
+ 'EDITOR', 'VISUAL', 'PAGER', 'BROWSER',
15
+ 'GIT_EDITOR', 'GIT_PAGER', 'GIT_SSH_COMMAND',
16
+ // 平台相关
17
+ 'TMPDIR', 'TEMPDIR', 'TEMP', 'TMP',
18
+ // 常用工具
19
+ 'NVM_DIR', 'NVM_BIN', 'JAVA_HOME', 'GOPATH', 'GOROOT', 'CARGO_HOME',
20
+ 'RUSTUP_HOME', 'PYTHONPATH', 'VIRTUAL_ENV', 'CONDA_PREFIX',
21
+ 'ANDROID_HOME', 'ANDROID_SDK_ROOT', 'GRADLE_HOME',
22
+ 'PKG_CONFIG_PATH', 'LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH',
23
+ // 颜色/格式化
24
+ 'NO_COLOR', 'CLICOLOR', 'CLICOLOR_FORCE', 'FORCE_COLOR'
25
+ ];
26
+
27
+ /**
28
+ * 构建安全的子进程环境变量
29
+ */
30
+ function buildSafeEnv() {
31
+ const env = {};
32
+ for (const key of SAFE_ENV_KEYS) {
33
+ if (process.env[key] !== undefined) {
34
+ env[key] = process.env[key];
35
+ }
36
+ }
37
+ // 强制覆盖
38
+ env.TERM = 'dumb';
39
+ env.FORCE_COLOR = '0';
40
+ return env;
41
+ }
42
+
9
43
  /**
10
44
  * 生成唯一执行 ID
11
45
  */
@@ -30,7 +64,7 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
30
64
 
31
65
  const proc = spawn('sh', ['-c', command], {
32
66
  cwd: workdir,
33
- env: { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' },
67
+ env: buildSafeEnv(),
34
68
  stdio: ['pipe', 'pipe', 'pipe']
35
69
  });
36
70
 
@@ -40,12 +74,14 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
40
74
  let stdoutCollected = '';
41
75
  let stderrCollected = '';
42
76
  let killed = false;
43
- let forceKillId = null; // 追踪 SIGKILL 的 setTimeout
77
+ let finished = false; // 防止 close/error 重复处理
44
78
 
45
- runningProcesses.set(execId, { process: proc, wsClients });
79
+ // 存储到 entry 中供 clearTimers 读取
80
+ const entry = { process: proc, wsClients };
81
+ runningProcesses.set(execId, entry);
46
82
 
47
83
  // 超时控制
48
- const timeoutId = setTimeout(() => {
84
+ entry.timeoutId = setTimeout(() => {
49
85
  killed = true;
50
86
  killProcess(execId);
51
87
  }, timeout);
@@ -60,6 +96,16 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
60
96
  }
61
97
  }
62
98
 
99
+ // 清理所有定时器
100
+ function clearTimers() {
101
+ clearTimeout(entry.timeoutId);
102
+ entry.timeoutId = null;
103
+ if (entry.forceKillId !== null && entry.forceKillId !== undefined) {
104
+ clearTimeout(entry.forceKillId);
105
+ entry.forceKillId = null;
106
+ }
107
+ }
108
+
63
109
  // stdout 流式输出
64
110
  proc.stdout.on('data', (chunk) => {
65
111
  const str = chunk.toString();
@@ -74,23 +120,18 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
74
120
  broadcast({ type: 'stderr', data: str, execId });
75
121
  });
76
122
 
77
- // 清理超时计时器
78
- function clearTimers() {
79
- clearTimeout(timeoutId);
80
- if (forceKillId !== null) {
81
- clearTimeout(forceKillId);
82
- forceKillId = null;
83
- }
84
- }
85
-
86
123
  // 进程结束
87
124
  proc.on('close', (exitCode) => {
125
+ if (finished) return;
126
+ finished = true;
88
127
  clearTimers();
89
- broadcast({ type: 'exit', exitCode, execId, killed });
128
+ broadcast({ type: 'exit', exitCode: typeof exitCode === 'number' ? exitCode : -1, execId, killed });
90
129
  runningProcesses.delete(execId);
91
130
  if (onComplete) {
92
131
  onComplete({
93
- execId, exitCode, killed,
132
+ execId,
133
+ exitCode: typeof exitCode === 'number' ? exitCode : -1,
134
+ killed,
94
135
  stdout: collectOutput ? stdoutCollected : undefined,
95
136
  stderr: collectOutput ? stderrCollected : undefined
96
137
  });
@@ -99,6 +140,8 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
99
140
 
100
141
  // 进程错误
101
142
  proc.on('error', (err) => {
143
+ if (finished) return;
144
+ finished = true;
102
145
  clearTimers();
103
146
  broadcast({ type: 'error', error: err.message, execId });
104
147
  runningProcesses.delete(execId);
@@ -120,21 +163,23 @@ function executeCommand(command, cwd, wsClient, onComplete, collectOutput = fals
120
163
  * @returns {Promise<{execId, exitCode, stdout, stderr, killed, error?}>}
121
164
  */
122
165
  function executeCommandSync(command, cwd) {
123
- return new Promise((resolve, reject) => {
166
+ return new Promise((resolve) => {
124
167
  const config = loadConfig();
125
168
  const timeout = config.commandTimeout || 300000;
126
169
 
127
170
  let resolved = false;
171
+ let syncTimeoutId = null;
128
172
 
129
173
  const execId = executeCommand(command, cwd, null, (result) => {
174
+ clearTimeout(syncTimeoutId);
130
175
  if (!resolved) {
131
176
  resolved = true;
132
177
  resolve(result);
133
178
  }
134
179
  }, true);
135
180
 
136
- // 超时兜底
137
- setTimeout(() => {
181
+ // 超时兜底(超时后额外 5s 用于 kill 完成)
182
+ syncTimeoutId = setTimeout(() => {
138
183
  if (!resolved) {
139
184
  resolved = true;
140
185
  killProcess(execId);
@@ -161,20 +206,32 @@ function addWsClient(execId, wsClient) {
161
206
  */
162
207
  function killProcess(execId) {
163
208
  const entry = runningProcesses.get(execId);
164
- if (entry) {
209
+ if (!entry) return false;
210
+
211
+ if (entry.process.exitCode !== null) return false; // 已退出
212
+
213
+ try {
165
214
  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;
215
+ } catch {}
216
+
217
+ // 5秒后强制 kill(绑定到 entry 对象防止闭包引用旧变量)
218
+ if (entry.timeoutId) {
219
+ clearTimeout(entry.timeoutId);
220
+ entry.timeoutId = null;
176
221
  }
177
- return false;
222
+ if (entry.forceKillId) {
223
+ clearTimeout(entry.forceKillId);
224
+ }
225
+ entry.forceKillId = setTimeout(() => {
226
+ try {
227
+ if (entry.process.exitCode === null) {
228
+ entry.process.kill('SIGKILL');
229
+ }
230
+ } catch {}
231
+ entry.forceKillId = null;
232
+ }, 5000);
233
+
234
+ return true;
178
235
  }
179
236
 
180
237
  /**
package/src/logger.js CHANGED
@@ -6,6 +6,43 @@ import { homedir } from 'os';
6
6
  const LOG_DIR = join(homedir(), '.ai-helper-agent', 'logs');
7
7
  const MAX_LOG_FILES = 30; // 最多保留 30 个日志文件
8
8
  const MAX_LOG_SIZE = 10 * 1024 * 1024; // 单文件最大 10MB
9
+ const CLEAN_INTERVAL_MS = 60_000; // 清理间隔(60 秒)
10
+ const QUERY_MAX_LIMIT = 1000; // 单次查询最大条数
11
+
12
+ // 是否输出到终端 stderr
13
+ let consoleOutput = false;
14
+
15
+ /**
16
+ * 设置是否在终端输出日志(前台运行时开启)
17
+ */
18
+ function setConsoleOutput(enabled) {
19
+ consoleOutput = enabled;
20
+ }
21
+
22
+ const CATEGORY_LABELS = {
23
+ auth: '认证', fs: '文件', exec: '命令', security: '安全', system: '系统'
24
+ };
25
+
26
+ const LEVEL_LABELS = { info: 'INFO', warn: 'WARN', error: 'ERROR' };
27
+
28
+ /**
29
+ * 格式化详情为可读字符串
30
+ */
31
+ function formatDetails(details) {
32
+ if (!details || Object.keys(details).length === 0) return '';
33
+ const parts = [];
34
+ for (const [key, val] of Object.entries(details)) {
35
+ if (val !== undefined && val !== null) {
36
+ const str = typeof val === 'string' ? val : JSON.stringify(val);
37
+ if (str.length > 120) {
38
+ parts.push(`${key}=${str.slice(0, 120)}...`);
39
+ } else {
40
+ parts.push(`${key}=${str}`);
41
+ }
42
+ }
43
+ }
44
+ return parts.join(' ');
45
+ }
9
46
 
10
47
  function ensureLogDir() {
11
48
  if (!existsSync(LOG_DIR)) {
@@ -16,17 +53,27 @@ function ensureLogDir() {
16
53
  /**
17
54
  * 获取当天日志文件路径
18
55
  */
19
- function getLogFile() {
56
+ function getLogFile(date) {
57
+ if (date) return join(LOG_DIR, `agent-${date}.log`);
20
58
  const now = new Date();
21
- const date = now.toISOString().slice(0, 10); // YYYY-MM-DD
22
- return join(LOG_DIR, `agent-${date}.log`);
59
+ const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
60
+ return join(LOG_DIR, `agent-${dateStr}.log`);
23
61
  }
24
62
 
63
+ // 清理节流:记录上次清理时间
64
+ let lastCleanTime = 0;
65
+
25
66
  /**
26
- * 清理旧日志文件(保留最近 MAX_LOG_FILES 个,删除超大文件)
67
+ * 清理旧日志文件(保留最近 MAX_LOG_FILES 个文件 + 删除超大文件)
68
+ * 节流执行:至少间隔 CLEAN_INTERVAL_MS
27
69
  */
28
70
  function cleanOldLogs() {
71
+ const now = Date.now();
72
+ if (now - lastCleanTime < CLEAN_INTERVAL_MS) return;
73
+ lastCleanTime = now;
74
+
29
75
  try {
76
+ ensureLogDir();
30
77
  const files = readdirSync(LOG_DIR)
31
78
  .filter(f => f.endsWith('.log'))
32
79
  .map(f => {
@@ -45,7 +92,7 @@ function cleanOldLogs() {
45
92
  }
46
93
  }
47
94
  if (deleted > 0) {
48
- console.log(`[Logger] 清理了 ${deleted} 个旧日志文件`);
95
+ console.error(`[Logger] 清理了 ${deleted} 个旧日志文件`);
49
96
  }
50
97
  } catch {}
51
98
  }
@@ -58,18 +105,30 @@ function cleanOldLogs() {
58
105
  * @param {Object} details - 详细信息
59
106
  */
60
107
  function log(level, category, action, details = {}) {
108
+ const entry = {
109
+ timestamp: new Date().toISOString(),
110
+ level,
111
+ category,
112
+ action,
113
+ ...details
114
+ };
115
+
116
+ // 输出到终端 stderr(前台运行时可见)
117
+ if (consoleOutput) {
118
+ const time = entry.timestamp.slice(11, 19); // HH:MM:SS
119
+ const levelTag = LEVEL_LABELS[level] || level.toUpperCase();
120
+ const catTag = CATEGORY_LABELS[category] || category;
121
+ const detailStr = formatDetails(details);
122
+ const msg = detailStr
123
+ ? `[${time}] [${levelTag}] [${catTag}:${action}] ${detailStr}`
124
+ : `[${time}] [${levelTag}] [${catTag}:${action}]`;
125
+ process.stderr.write(msg + '\n');
126
+ }
127
+
128
+ // 写入文件
61
129
  try {
62
130
  ensureLogDir();
63
- cleanOldLogs(); // 每次写入时顺便清理(频率不高,影响可忽略)
64
-
65
- const entry = {
66
- timestamp: new Date().toISOString(),
67
- level,
68
- category,
69
- action,
70
- ...details
71
- };
72
-
131
+ cleanOldLogs();
73
132
  appendFileSync(getLogFile(), JSON.stringify(entry) + '\n', 'utf-8');
74
133
  } catch (err) {
75
134
  // 日志写入失败不应影响主流程
@@ -96,14 +155,10 @@ function logError(category, action, details) { log('error', category, action, de
96
155
  * @returns {{ entries: Array, total: number }}
97
156
  */
98
157
  function queryLogs(options = {}) {
99
- const { date, category, limit = 200, offset = 0 } = options;
158
+ const { date, category, limit: rawLimit = 200, offset = 0 } = options;
159
+ const limit = Math.min(rawLimit, QUERY_MAX_LIMIT);
100
160
 
101
- let filePath;
102
- if (date) {
103
- filePath = join(LOG_DIR, `agent-${date}.log`);
104
- } else {
105
- filePath = getLogFile();
106
- }
161
+ const filePath = getLogFile(date);
107
162
 
108
163
  let entries = [];
109
164
  try {
@@ -121,7 +176,7 @@ function queryLogs(options = {}) {
121
176
  } catch {}
122
177
 
123
178
  // 按时间倒序(最新的在前)
124
- entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
179
+ entries.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
125
180
 
126
181
  const total = entries.length;
127
182
  entries = entries.slice(offset, offset + limit);
@@ -146,4 +201,4 @@ function getLogDates() {
146
201
  }
147
202
  }
148
203
 
149
- export { log, logAuth, logFs, logExec, logSecurity, logSystem, logError, queryLogs, getLogDates };
204
+ export { setConsoleOutput, log, logAuth, logFs, logExec, logSecurity, logSystem, logError, queryLogs, getLogDates };