@sstar/boardlinker_agent 0.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.
Files changed (51) hide show
  1. package/README.md +107 -0
  2. package/dist/.platform +1 -0
  3. package/dist/board/docs.js +59 -0
  4. package/dist/board/notes.js +11 -0
  5. package/dist/board_uart/history.js +81 -0
  6. package/dist/board_uart/index.js +66 -0
  7. package/dist/board_uart/manager.js +313 -0
  8. package/dist/board_uart/resource.js +578 -0
  9. package/dist/board_uart/sessions.js +559 -0
  10. package/dist/config/index.js +341 -0
  11. package/dist/core/activity.js +7 -0
  12. package/dist/core/errors.js +45 -0
  13. package/dist/core/log_stream.js +26 -0
  14. package/dist/files/file_operation_logger.js +271 -0
  15. package/dist/files/files_manager.js +511 -0
  16. package/dist/files/index.js +87 -0
  17. package/dist/files/types.js +5 -0
  18. package/dist/firmware/burn_recover.js +733 -0
  19. package/dist/firmware/prepare_images.js +184 -0
  20. package/dist/firmware/user_guide.js +43 -0
  21. package/dist/index.js +449 -0
  22. package/dist/logger.js +245 -0
  23. package/dist/macro/index.js +241 -0
  24. package/dist/macro/runner.js +168 -0
  25. package/dist/nfs/index.js +105 -0
  26. package/dist/plugins/loader.js +30 -0
  27. package/dist/proto/agent.proto +473 -0
  28. package/dist/resources/docs/board-interaction.md +115 -0
  29. package/dist/resources/docs/firmware-upgrade.md +404 -0
  30. package/dist/resources/docs/nfs-mount-guide.md +78 -0
  31. package/dist/resources/docs/tftp-transfer-guide.md +81 -0
  32. package/dist/secrets/index.js +9 -0
  33. package/dist/server/grpc.js +1083 -0
  34. package/dist/server/web.js +2306 -0
  35. package/dist/ssh/adapter.js +126 -0
  36. package/dist/ssh/candidates.js +85 -0
  37. package/dist/ssh/index.js +3 -0
  38. package/dist/ssh/paircheck.js +35 -0
  39. package/dist/ssh/tunnel.js +111 -0
  40. package/dist/tftp/client.js +345 -0
  41. package/dist/tftp/index.js +284 -0
  42. package/dist/tftp/server.js +731 -0
  43. package/dist/uboot/index.js +45 -0
  44. package/dist/ui/assets/index-CCZ6chFx.css +32 -0
  45. package/dist/ui/assets/index-Cuhnt9D5.js +375 -0
  46. package/dist/ui/index.html +21 -0
  47. package/dist/utils/network.js +150 -0
  48. package/dist/utils/platform.js +83 -0
  49. package/dist/utils/port-check.js +153 -0
  50. package/dist/utils/user-prompt.js +139 -0
  51. package/package.json +59 -0
@@ -0,0 +1,341 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import YAML from 'yaml';
5
+ import { DefaultTimeouts } from '../core/errors.js';
6
+ import { LogLevel } from '../logger.js';
7
+ const HOME_CONFIG_PATH = path.join(os.homedir(), '.config', 'board_linker', 'config.json');
8
+ function defaultTftpDir() {
9
+ return getDefaultAgentFilesDir();
10
+ }
11
+ function defaultShareUsername() {
12
+ const envUser = process.env.USER || process.env.LOGNAME || process.env.USERNAME;
13
+ if (envUser && envUser.trim())
14
+ return envUser.replace(/[\\/]/g, '_');
15
+ try {
16
+ const info = os.userInfo();
17
+ if (info.username && info.username.trim())
18
+ return info.username.replace(/[\\/]/g, '_');
19
+ }
20
+ catch { }
21
+ return 'user';
22
+ }
23
+ function getDefaultAgentFilesDir() {
24
+ return path.join(os.homedir(), '.local', 'board_linker', 'agent_files');
25
+ }
26
+ export const DEFAULT_CONFIG = {
27
+ grpc: { host: '0.0.0.0', port: 10000 },
28
+ web: { host: '0.0.0.0', port: 8080 },
29
+ boardUart: {
30
+ mock: false,
31
+ port: '',
32
+ baud: 115200,
33
+ },
34
+ network: {},
35
+ tftp: {
36
+ dir: getDefaultAgentFilesDir(),
37
+ server: {
38
+ enabled: true,
39
+ mode: 'builtin',
40
+ host: '0.0.0.0',
41
+ port: 69,
42
+ },
43
+ },
44
+ nfs: {
45
+ enabled: true,
46
+ localPath: getDefaultAgentFilesDir(),
47
+ exportName: '/shared',
48
+ description: 'Default NFS export',
49
+ username: defaultShareUsername(),
50
+ password: '',
51
+ },
52
+ flash: {},
53
+ ssh: {
54
+ enablePasswordFallback: true,
55
+ defaultKeyPath: '',
56
+ },
57
+ timeouts: DefaultTimeouts,
58
+ files: {
59
+ rootDir: getDefaultAgentFilesDir(),
60
+ maxFileSize: 100 * 1024 * 1024, // 100MB
61
+ allowedPaths: [getDefaultAgentFilesDir()],
62
+ maxTotalBytes: 5 * 1024 * 1024 * 1024, // 5GB
63
+ maxTotalFiles: 50000,
64
+ skipHiddenByDefault: false,
65
+ },
66
+ logging: {
67
+ files: {
68
+ enabled: true,
69
+ level: LogLevel.DEBUG,
70
+ includeMetadata: true,
71
+ maxLogSize: 50 * 1024 * 1024, // 50MB
72
+ detailedDebug: true,
73
+ pathResolution: true,
74
+ securityChecks: true,
75
+ performanceMetrics: true,
76
+ },
77
+ },
78
+ };
79
+ let lastConfigPath = null;
80
+ function defaultKey() {
81
+ const home = os.homedir();
82
+ return path.join(home, '.ssh', 'id_rsa');
83
+ }
84
+ function normalize(p) {
85
+ return path.normalize(p);
86
+ }
87
+ function ensureTftpServerConfig(cfg) {
88
+ if (!cfg.tftp) {
89
+ cfg.tftp = {
90
+ dir: defaultTftpDir(),
91
+ ttlHours: 24,
92
+ cleanupIntervalMinutes: 60,
93
+ server: {
94
+ enabled: true,
95
+ mode: 'builtin',
96
+ host: '0.0.0.0',
97
+ port: 69,
98
+ blockSize: 1024, // 1KB块大小(原512字节)
99
+ timeout: 15000, // 15秒超时(原5000ms)
100
+ retryCount: 8, // 8次重试(原3次)
101
+ },
102
+ };
103
+ return;
104
+ }
105
+ if (!cfg.tftp.server) {
106
+ cfg.tftp.server = {
107
+ enabled: true,
108
+ mode: 'builtin',
109
+ host: '0.0.0.0',
110
+ port: 69,
111
+ blockSize: 512, // 默认 512 字节
112
+ timeout: 5000, // 默认 5 秒超时
113
+ retryCount: 3, // 默认 3 次重试
114
+ };
115
+ }
116
+ else {
117
+ if (typeof cfg.tftp.server.enabled !== 'boolean')
118
+ cfg.tftp.server.enabled = true;
119
+ if (!cfg.tftp.server.mode) {
120
+ cfg.tftp.server.mode = cfg.tftp.server.enabled === false ? 'disabled' : 'builtin';
121
+ }
122
+ if (cfg.tftp.server.mode === 'builtin') {
123
+ if (!cfg.tftp.server.host)
124
+ cfg.tftp.server.host = '0.0.0.0';
125
+ if (!cfg.tftp.server.port)
126
+ cfg.tftp.server.port = 69;
127
+ if (!cfg.tftp.server.blockSize)
128
+ cfg.tftp.server.blockSize = 512; // 默认 512 字节
129
+ if (!cfg.tftp.server.timeout)
130
+ cfg.tftp.server.timeout = 5000; // 默认 5 秒超时
131
+ if (!cfg.tftp.server.retryCount)
132
+ cfg.tftp.server.retryCount = 3; // 默认 3 次重试
133
+ }
134
+ if (cfg.tftp.server.mode === 'external') {
135
+ if (!cfg.tftp.server.externalPort)
136
+ cfg.tftp.server.externalPort = 69;
137
+ }
138
+ if (cfg.tftp.server.mode === 'disabled') {
139
+ cfg.tftp.server.enabled = false;
140
+ }
141
+ }
142
+ }
143
+ function ensureNfsConfig(cfg) {
144
+ if (!cfg.nfs) {
145
+ cfg.nfs = {
146
+ enabled: true,
147
+ localPath: cfg.tftp?.dir || getDefaultAgentFilesDir(),
148
+ exportName: '/shared',
149
+ description: 'Default NFS export',
150
+ username: defaultShareUsername(),
151
+ password: '',
152
+ };
153
+ return;
154
+ }
155
+ if (typeof cfg.nfs.enabled !== 'boolean')
156
+ cfg.nfs.enabled = true;
157
+ if (!cfg.nfs.localPath)
158
+ cfg.nfs.localPath = cfg.tftp?.dir || getDefaultAgentFilesDir();
159
+ if (!cfg.nfs.exportName)
160
+ cfg.nfs.exportName = '/shared';
161
+ if (!cfg.nfs.description)
162
+ cfg.nfs.description = 'Default NFS export';
163
+ if (!cfg.nfs.username || !cfg.nfs.username.trim())
164
+ cfg.nfs.username = defaultShareUsername();
165
+ if (typeof cfg.nfs.password !== 'string')
166
+ cfg.nfs.password = '';
167
+ }
168
+ function ensureNetworkConfig(cfg) {
169
+ if (!cfg.network) {
170
+ cfg.network = {};
171
+ return;
172
+ }
173
+ if (typeof cfg.network.preferredInterface !== 'string')
174
+ cfg.network.preferredInterface = '';
175
+ if (typeof cfg.network.preferredIp !== 'string')
176
+ cfg.network.preferredIp = '';
177
+ }
178
+ export async function loadConfig() {
179
+ const cwd = process.cwd();
180
+ const candidates = [
181
+ HOME_CONFIG_PATH,
182
+ path.join(cwd, 'agent.config.yaml'),
183
+ path.join(cwd, 'agent.config.yml'),
184
+ path.join(cwd, 'agent.config.json'),
185
+ ];
186
+ let cfg = DEFAULT_CONFIG;
187
+ for (const p of candidates) {
188
+ try {
189
+ const raw = await fs.readFile(p, 'utf8');
190
+ const y = p.endsWith('.json') ? JSON.parse(raw) : YAML.parse(raw);
191
+ cfg = { ...cfg, ...y };
192
+ lastConfigPath = p;
193
+ break;
194
+ }
195
+ catch { }
196
+ }
197
+ // ENV overrides
198
+ if (process.env.BOARDLINKER_GRPC_PORT)
199
+ cfg.grpc.port = Number(process.env.BOARDLINKER_GRPC_PORT);
200
+ if (process.env.BOARDLINKER_WEB_HOST)
201
+ cfg.web.host = process.env.BOARDLINKER_WEB_HOST;
202
+ if (process.env.BOARDLINKER_WEB_PORT)
203
+ cfg.web.port = Number(process.env.BOARDLINKER_WEB_PORT);
204
+ if (process.env.BOARDLINKER_TFTP_DIR)
205
+ cfg.tftp.dir = process.env.BOARDLINKER_TFTP_DIR;
206
+ if (process.env.BOARDLINKER_TFTP_TTL_HOURS)
207
+ cfg.tftp.ttlHours = Number(process.env.BOARDLINKER_TFTP_TTL_HOURS);
208
+ if (process.env.BOARDLINKER_TFTP_CLEANUP_INTERVAL_MIN)
209
+ cfg.tftp.cleanupIntervalMinutes = Number(process.env.BOARDLINKER_TFTP_CLEANUP_INTERVAL_MIN);
210
+ ensureTftpServerConfig(cfg);
211
+ // Board UART defaults / overrides
212
+ if (process.env.BOARDLINKER_BOARD_UART_PORT)
213
+ cfg.boardUart.port = process.env.BOARDLINKER_BOARD_UART_PORT;
214
+ if (process.env.BOARDLINKER_BOARD_UART_BAUD)
215
+ cfg.boardUart.baud = Number(process.env.BOARDLINKER_BOARD_UART_BAUD);
216
+ cfg.tftp.dir = normalize(cfg.tftp.dir);
217
+ ensureNfsConfig(cfg);
218
+ cfg.nfs.localPath = normalize(cfg.nfs.localPath);
219
+ // SSH 默认密钥路径:始终基于当前用户主目录计算,可通过环境变量覆盖
220
+ const envDefaultKey = (process.env.BOARDLINKER_SSH_DEFAULT_KEY || '').trim();
221
+ if (!cfg.ssh) {
222
+ cfg.ssh = {
223
+ enablePasswordFallback: true,
224
+ defaultKeyPath: envDefaultKey || defaultKey(),
225
+ };
226
+ }
227
+ else {
228
+ cfg.ssh.enablePasswordFallback =
229
+ typeof cfg.ssh.enablePasswordFallback === 'boolean' ? cfg.ssh.enablePasswordFallback : true;
230
+ cfg.ssh.defaultKeyPath = envDefaultKey || defaultKey();
231
+ }
232
+ if (!cfg.agent)
233
+ cfg.agent = { id: defaultAgentId() };
234
+ if (!cfg.agent.id)
235
+ cfg.agent.id = makeDefaultAgentId(os.hostname?.() || 'host', cfg.grpc.port);
236
+ if (process.env.BOARDLINKER_AGENT_ID)
237
+ cfg.agent.id = process.env.BOARDLINKER_AGENT_ID;
238
+ ensureNetworkConfig(cfg);
239
+ // 文件操作日志配置的环境变量支持
240
+ if (!cfg.logging)
241
+ cfg.logging = {};
242
+ if (!cfg.logging.files)
243
+ cfg.logging.files = DEFAULT_CONFIG.logging.files;
244
+ // 环境变量覆盖文件操作日志配置
245
+ if (process.env.BOARDLINKER_FILES_LOG_ENABLED !== undefined && cfg.logging.files) {
246
+ cfg.logging.files.enabled =
247
+ process.env.BOARDLINKER_FILES_LOG_ENABLED === '1' ||
248
+ process.env.BOARDLINKER_FILES_LOG_ENABLED === 'true';
249
+ }
250
+ if (process.env.BOARDLINKER_FILES_LOG_LEVEL && cfg.logging.files) {
251
+ cfg.logging.files.level = process.env.BOARDLINKER_FILES_LOG_LEVEL;
252
+ }
253
+ if (process.env.BOARDLINKER_FILES_LOG_INCLUDE_METADATA !== undefined && cfg.logging.files) {
254
+ cfg.logging.files.includeMetadata =
255
+ process.env.BOARDLINKER_FILES_LOG_INCLUDE_METADATA === '1' ||
256
+ process.env.BOARDLINKER_FILES_LOG_INCLUDE_METADATA === 'true';
257
+ }
258
+ if (process.env.BOARDLINKER_FILES_LOG_MAX_SIZE && cfg.logging.files) {
259
+ const sizeStr = process.env.BOARDLINKER_FILES_LOG_MAX_SIZE;
260
+ const units = {
261
+ B: 1,
262
+ KB: 1024,
263
+ MB: 1024 * 1024,
264
+ GB: 1024 * 1024 * 1024,
265
+ };
266
+ const match = sizeStr.match(/^(\d+)(B|KB|MB|GB)?$/i);
267
+ if (match) {
268
+ const value = parseInt(match[1]);
269
+ const unit = match[2]?.toUpperCase() || 'B';
270
+ cfg.logging.files.maxLogSize = value * (units[unit] || 1);
271
+ }
272
+ }
273
+ if (process.env.BOARDLINKER_FILES_LOG_DETAILED_DEBUG !== undefined && cfg.logging.files) {
274
+ cfg.logging.files.detailedDebug =
275
+ process.env.BOARDLINKER_FILES_LOG_DETAILED_DEBUG === '1' ||
276
+ process.env.BOARDLINKER_FILES_LOG_DETAILED_DEBUG === 'true';
277
+ }
278
+ if (process.env.BOARDLINKER_FILES_LOG_PATH_RESOLUTION !== undefined && cfg.logging.files) {
279
+ cfg.logging.files.pathResolution =
280
+ process.env.BOARDLINKER_FILES_LOG_PATH_RESOLUTION === '1' ||
281
+ process.env.BOARDLINKER_FILES_LOG_PATH_RESOLUTION === 'true';
282
+ }
283
+ if (process.env.BOARDLINKER_FILES_LOG_SECURITY_CHECKS !== undefined && cfg.logging.files) {
284
+ cfg.logging.files.securityChecks =
285
+ process.env.BOARDLINKER_FILES_LOG_SECURITY_CHECKS === '1' ||
286
+ process.env.BOARDLINKER_FILES_LOG_SECURITY_CHECKS === 'true';
287
+ }
288
+ if (process.env.BOARDLINKER_FILES_LOG_PERFORMANCE_METRICS !== undefined && cfg.logging.files) {
289
+ cfg.logging.files.performanceMetrics =
290
+ process.env.BOARDLINKER_FILES_LOG_PERFORMANCE_METRICS === '1' ||
291
+ process.env.BOARDLINKER_FILES_LOG_PERFORMANCE_METRICS === 'true';
292
+ }
293
+ // 统一文件存储路径:确保 Files 系统使用与 TFTP/NFS 相同的配置路径
294
+ // 这符合 Settings 页面"Local Artifacts Path"的设计意图
295
+ if (!cfg.files) {
296
+ cfg.files = DEFAULT_CONFIG.files;
297
+ }
298
+ // 如果用户在 Settings 页面配置了统一的 artifacts 路径(通过 tftp.dir),
299
+ // 则 Files 系统也使用相同路径,实现统一的文件存储管理
300
+ if (cfg.tftp && cfg.tftp.dir) {
301
+ cfg.files.rootDir = cfg.tftp.dir;
302
+ // 同时更新 allowedPaths,确保 Files 系统可以访问该路径
303
+ if (!cfg.files.allowedPaths || cfg.files.allowedPaths.length === 0) {
304
+ cfg.files.allowedPaths = [cfg.tftp.dir];
305
+ }
306
+ else if (!cfg.files.allowedPaths.includes(cfg.tftp.dir)) {
307
+ // 将统一路径添加到 allowedPaths 的开头,优先使用
308
+ cfg.files.allowedPaths = [
309
+ cfg.tftp.dir,
310
+ ...cfg.files.allowedPaths.filter((p) => p !== cfg.tftp.dir),
311
+ ];
312
+ }
313
+ }
314
+ if (!lastConfigPath)
315
+ lastConfigPath = HOME_CONFIG_PATH;
316
+ return cfg;
317
+ }
318
+ export function getConfigPath() {
319
+ return lastConfigPath || HOME_CONFIG_PATH;
320
+ }
321
+ export async function saveConfig(cfg, targetPath) {
322
+ const outPath = targetPath || getConfigPath();
323
+ const content = outPath.endsWith('.json') || outPath.endsWith('.json5')
324
+ ? JSON.stringify(cfg, null, 2)
325
+ : YAML.stringify(cfg);
326
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
327
+ await fs.writeFile(outPath, content, 'utf8');
328
+ lastConfigPath = outPath;
329
+ return outPath;
330
+ }
331
+ function defaultAgentId() {
332
+ const seed = `${process.cwd()}|${process.pid}|${Date.now()}`;
333
+ let h = 5381;
334
+ for (let i = 0; i < seed.length; i++)
335
+ h = (h << 5) + h + seed.charCodeAt(i);
336
+ return `agent-${(h >>> 0).toString(16)}`;
337
+ }
338
+ function makeDefaultAgentId(hostname, port) {
339
+ const raw = `agent-${hostname}-${port}`;
340
+ return raw.replace(/[^A-Za-z0-9._-]/g, '-');
341
+ }
@@ -0,0 +1,7 @@
1
+ export let lastGrpcActivity = 0;
2
+ export function markGrpcActivity() {
3
+ lastGrpcActivity = Date.now();
4
+ }
5
+ export function getGrpcLastActivity() {
6
+ return lastGrpcActivity;
7
+ }
@@ -0,0 +1,45 @@
1
+ export const ErrorCodes = {
2
+ EL_AGENT_UNAVAILABLE: 'EL_AGENT_UNAVAILABLE',
3
+ EL_PROCEDURE_TIMEOUT: 'EL_PROCEDURE_TIMEOUT',
4
+ EL_INVALID_PARAMS: 'EL_INVALID_PARAMS',
5
+ EL_BOARD_UART_OPEN_FAILED: 'EL_BOARD_UART_OPEN_FAILED',
6
+ EL_BOARD_UART_WRITE_FAILED: 'EL_BOARD_UART_WRITE_FAILED',
7
+ EL_BOARD_UART_READ_IN_PROGRESS: 'EL_BOARD_UART_READ_IN_PROGRESS',
8
+ EL_BOARD_UART_READ_TIMEOUT: 'EL_BOARD_UART_READ_TIMEOUT',
9
+ EL_BOARD_UART_MODULE_MISSING: 'EL_BOARD_UART_MODULE_MISSING',
10
+ EL_BOARD_UART_NO_DEVICE: 'EL_BOARD_UART_NO_DEVICE',
11
+ EL_BOARD_UART_DISABLED: 'EL_BOARD_UART_DISABLED',
12
+ EL_BOARD_UART_SESSION_NOT_FOUND: 'EL_BOARD_UART_SESSION_NOT_FOUND',
13
+ EL_BOARD_UART_SESSION_TEMPORARILY_UNAVAILABLE: 'EL_BOARD_UART_SESSION_TEMPORARILY_UNAVAILABLE',
14
+ EL_BOARD_UART_READONLY_SESSION: 'EL_BOARD_UART_READONLY_SESSION',
15
+ EL_BOARD_UART_SUSPENDED: 'EL_BOARD_UART_SUSPENDED',
16
+ EL_TFTP_UPLOAD_FAILED: 'EL_TFTP_UPLOAD_FAILED',
17
+ EL_TFTP_VERIFY_FAILED: 'EL_TFTP_VERIFY_FAILED',
18
+ EL_TFTP_DISABLED: 'EL_TFTP_DISABLED',
19
+ EL_TFTP_SERVER_ALREADY_RUNNING: 'EL_TFTP_SERVER_ALREADY_RUNNING',
20
+ EL_TFTP_SERVER_START_FAILED: 'EL_TFTP_SERVER_START_FAILED',
21
+ EL_UBOOT_BREAK_TIMEOUT: 'EL_UBOOT_BREAK_TIMEOUT',
22
+ EL_UBOOT_CMD_FAILED: 'EL_UBOOT_CMD_FAILED',
23
+ EL_FLASH_WRITE_FAILED: 'EL_FLASH_WRITE_FAILED',
24
+ EL_FLASH_VERIFY_FAILED: 'EL_FLASH_VERIFY_FAILED',
25
+ EL_REBOOT_TIMEOUT: 'EL_REBOOT_TIMEOUT',
26
+ EL_SSH_CONNECT_FAILED: 'EL_SSH_CONNECT_FAILED',
27
+ EL_SSH_TUNNEL_BROKEN: 'EL_SSH_TUNNEL_BROKEN',
28
+ EL_NFS_DISABLED: 'EL_NFS_DISABLED',
29
+ };
30
+ export const DefaultTimeouts = {
31
+ boardUart: { open: 3000, read: 5000, write: 5000 },
32
+ tftp: { upload: 120000 },
33
+ uboot: { break: 5000, cmd: 10000 },
34
+ flash: { write: 300000 },
35
+ reboot: { wait: 60000 },
36
+ ssh: { connect: 15000 },
37
+ };
38
+ export function error(code, message, data) {
39
+ const fullMessage = `[${code}] ${message}`;
40
+ const e = new Error(fullMessage);
41
+ e.code = code;
42
+ if (data)
43
+ e.data = data;
44
+ return e;
45
+ }
@@ -0,0 +1,26 @@
1
+ const MAX_LOG_BUFFER = 1000;
2
+ const agentLogBuf = [];
3
+ const hostLogBuf = [];
4
+ const listeners = new Set();
5
+ function pushBounded(buf, entry) {
6
+ buf.push(entry);
7
+ if (buf.length > MAX_LOG_BUFFER)
8
+ buf.splice(0, buf.length - MAX_LOG_BUFFER);
9
+ }
10
+ export function publishAgentLog(entry) {
11
+ pushBounded(agentLogBuf, entry);
12
+ for (const l of listeners)
13
+ l('agent', entry);
14
+ }
15
+ export function publishHostLog(entry) {
16
+ pushBounded(hostLogBuf, entry);
17
+ for (const l of listeners)
18
+ l('host', entry);
19
+ }
20
+ export function getLogSnapshot(source) {
21
+ return source === 'agent' ? [...agentLogBuf] : [...hostLogBuf];
22
+ }
23
+ export function subscribeLogs(listener) {
24
+ listeners.add(listener);
25
+ return () => listeners.delete(listener);
26
+ }
@@ -0,0 +1,271 @@
1
+ import path from 'node:path';
2
+ import { LogLevel, logAgentDebug } from '../logger.js';
3
+ // 文件操作日志事件类型
4
+ export var FileOperationEvent;
5
+ (function (FileOperationEvent) {
6
+ // 通用事件
7
+ FileOperationEvent["OPERATION_START"] = "files.operation.start";
8
+ FileOperationEvent["OPERATION_SUCCESS"] = "files.operation.success";
9
+ FileOperationEvent["OPERATION_ERROR"] = "files.operation.error";
10
+ // PUT操作事件
11
+ FileOperationEvent["PUT_VALIDATION_START"] = "files.put.validation.start";
12
+ FileOperationEvent["PUT_VALIDATION_SUCCESS"] = "files.put.validation.success";
13
+ FileOperationEvent["PUT_META_SENT"] = "files.put.meta.sent";
14
+ FileOperationEvent["PUT_FILE_START"] = "files.put.file.start";
15
+ FileOperationEvent["PUT_FILE_PROGRESS"] = "files.put.file.progress";
16
+ FileOperationEvent["PUT_FILE_SUCCESS"] = "files.put.file.success";
17
+ FileOperationEvent["PUT_CLEANUP"] = "files.put.cleanup";
18
+ // GET操作事件
19
+ FileOperationEvent["GET_VALIDATION_START"] = "files.get.validation.start";
20
+ FileOperationEvent["GET_VALIDATION_SUCCESS"] = "files.get.validation.success";
21
+ FileOperationEvent["GET_REQUEST_SENT"] = "files.get.request.sent";
22
+ FileOperationEvent["GET_FILE_START"] = "files.get.file.start";
23
+ FileOperationEvent["GET_FILE_PROGRESS"] = "files.get.file.progress";
24
+ FileOperationEvent["GET_FILE_SUCCESS"] = "files.get.file.success";
25
+ // 路径解析和安全检查
26
+ FileOperationEvent["PATH_RESOLUTION_START"] = "files.path.resolution.start";
27
+ FileOperationEvent["PATH_RESOLUTION_SUCCESS"] = "files.path.resolution.success";
28
+ FileOperationEvent["PATH_NORMALIZATION"] = "files.path.normalization";
29
+ FileOperationEvent["PATH_SECURITY_CHECK"] = "files.path.security_check";
30
+ FileOperationEvent["PATH_FINAL_CONSTRUCTION"] = "files.path.final_construction";
31
+ FileOperationEvent["SECURITY_CHECK_START"] = "files.security.check.start";
32
+ FileOperationEvent["SECURITY_CHECK_SUCCESS"] = "files.security.check.success";
33
+ FileOperationEvent["SECURITY_CHECK_FAILED"] = "files.security.check.failed";
34
+ // 文件操作过程
35
+ FileOperationEvent["COPY_START"] = "files.copy.start";
36
+ FileOperationEvent["COPY_SUCCESS"] = "files.copy.success";
37
+ FileOperationEvent["DIRECTORY_CREATED"] = "files.directory.created";
38
+ // 重试和错误处理
39
+ FileOperationEvent["RETRY_ATTEMPT"] = "files.retry.attempt";
40
+ FileOperationEvent["RETRY_SUCCESS"] = "files.retry.success";
41
+ FileOperationEvent["RETRY_EXHAUSTED"] = "files.retry.exhausted";
42
+ FileOperationEvent["RESOURCE_CLEANUP"] = "files.resource.cleanup";
43
+ // 其他操作
44
+ FileOperationEvent["LIST_START"] = "files.list.start";
45
+ FileOperationEvent["LIST_SUCCESS"] = "files.list.success";
46
+ FileOperationEvent["MKDIR_START"] = "files.mkdir.start";
47
+ FileOperationEvent["MKDIR_SUCCESS"] = "files.mkdir.success";
48
+ FileOperationEvent["RM_START"] = "files.rm.start";
49
+ FileOperationEvent["RM_SUCCESS"] = "files.rm.success";
50
+ })(FileOperationEvent || (FileOperationEvent = {}));
51
+ // 文件操作上下文管理
52
+ export class FileOperationContext {
53
+ constructor(requestId, operation, initialData = {}) {
54
+ this.completed = false;
55
+ this.success = false;
56
+ this.requestId = requestId;
57
+ this.operation = operation;
58
+ this.startTime = Date.now();
59
+ this.agentPid = process.pid;
60
+ this.data = {
61
+ ...initialData,
62
+ operation,
63
+ requestId,
64
+ startTime: this.startTime,
65
+ agentPid: this.agentPid,
66
+ stage: 'started',
67
+ };
68
+ }
69
+ // 更新上下文数据
70
+ update(newData) {
71
+ this.data = { ...this.data, ...newData };
72
+ }
73
+ // 标记操作成功
74
+ markSuccess() {
75
+ this.completed = true;
76
+ this.success = true;
77
+ this.data.duration = Date.now() - this.startTime;
78
+ this.data.stage = 'success';
79
+ }
80
+ // 标记操作失败
81
+ markError(error) {
82
+ this.completed = true;
83
+ this.success = false;
84
+ this.error = error;
85
+ this.data.duration = Date.now() - this.startTime;
86
+ this.data.stage = 'error';
87
+ this.data.errorCode = error.name;
88
+ this.data.errorMessage = error.message;
89
+ this.data.errorStack = error.stack;
90
+ }
91
+ // 转换为日志数据格式
92
+ toLogData() {
93
+ return {
94
+ operation: this.operation,
95
+ requestId: this.requestId,
96
+ startTime: this.startTime,
97
+ agentPid: this.agentPid,
98
+ duration: this.data.duration,
99
+ stage: this.data.stage || 'unknown',
100
+ ...this.data,
101
+ };
102
+ }
103
+ // 检查是否已完成
104
+ isCompleted() {
105
+ return this.completed;
106
+ }
107
+ // 检查是否成功
108
+ isSuccess() {
109
+ return this.success;
110
+ }
111
+ // 获取错误信息
112
+ getError() {
113
+ return this.error;
114
+ }
115
+ }
116
+ // 文件操作日志记录器
117
+ export class FileOperationLogger {
118
+ constructor(agentLogger, agentId = 'unknown') {
119
+ this.activeOperations = new Map();
120
+ this.agentLogger = agentLogger;
121
+ this.agentId = agentId;
122
+ }
123
+ // 开始操作记录
124
+ startOperation(operation, data = {}) {
125
+ const requestId = this.generateRequestId();
126
+ const context = new FileOperationContext(requestId, operation, {
127
+ ...data,
128
+ agentId: this.agentId,
129
+ });
130
+ this.activeOperations.set(requestId, context);
131
+ this.agentLogger.write(LogLevel.DEBUG, FileOperationEvent.OPERATION_START, {
132
+ ...context.toLogData(),
133
+ stage: 'start',
134
+ });
135
+ return requestId;
136
+ }
137
+ // 记录操作成功
138
+ logSuccess(requestId, data = {}) {
139
+ const context = this.activeOperations.get(requestId);
140
+ if (!context) {
141
+ logAgentDebug('file_operation.logger', {
142
+ warning: 'Attempted to log success for unknown requestId',
143
+ requestId,
144
+ });
145
+ return;
146
+ }
147
+ context.update(data);
148
+ context.markSuccess();
149
+ this.agentLogger.write(LogLevel.INFO, FileOperationEvent.OPERATION_SUCCESS, {
150
+ ...context.toLogData(),
151
+ stage: 'success',
152
+ });
153
+ this.activeOperations.delete(requestId);
154
+ }
155
+ // 记录操作失败
156
+ logError(requestId, error, data = {}) {
157
+ const context = this.activeOperations.get(requestId);
158
+ if (!context) {
159
+ logAgentDebug('file_operation.logger', {
160
+ warning: 'Attempted to log error for unknown requestId',
161
+ requestId,
162
+ errorMessage: error.message,
163
+ });
164
+ return;
165
+ }
166
+ context.update(data);
167
+ context.markError(error);
168
+ this.agentLogger.write(LogLevel.ERROR, FileOperationEvent.OPERATION_ERROR, {
169
+ ...context.toLogData(),
170
+ stage: 'error',
171
+ errorCode: error.name,
172
+ errorMessage: error.message,
173
+ errorStack: error.stack,
174
+ });
175
+ this.activeOperations.delete(requestId);
176
+ }
177
+ // 记录调试信息
178
+ logDebug(requestId, event, data = {}) {
179
+ const context = this.activeOperations.get(requestId);
180
+ if (!context) {
181
+ logAgentDebug('file_operation.logger', {
182
+ warning: 'Attempted to log debug for unknown requestId',
183
+ requestId,
184
+ event,
185
+ });
186
+ return;
187
+ }
188
+ context.update(data);
189
+ this.agentLogger.write(LogLevel.DEBUG, event, {
190
+ ...context.toLogData(),
191
+ stage: 'debug',
192
+ });
193
+ }
194
+ // 记录路径转换过程
195
+ logPathTransformation(requestId, inputPath, rootDir) {
196
+ const transformation = {
197
+ input: inputPath,
198
+ normalized: '',
199
+ securityChecked: '',
200
+ final: '',
201
+ };
202
+ // 1. 路径标准化
203
+ transformation.normalized = path.normalize(inputPath);
204
+ this.logDebug(requestId, FileOperationEvent.PATH_NORMALIZATION, {
205
+ input: transformation.input,
206
+ normalized: transformation.normalized,
207
+ debugInfo: {
208
+ explanation: 'path.normalize() applied to remove redundant separators and resolve relative references',
209
+ },
210
+ });
211
+ // 2. 安全路径检查
212
+ transformation.securityChecked = transformation.normalized.replace(/^(\.\.[\/\\])+/, '');
213
+ this.logDebug(requestId, FileOperationEvent.PATH_SECURITY_CHECK, {
214
+ normalized: transformation.normalized,
215
+ securityChecked: transformation.securityChecked,
216
+ debugInfo: {
217
+ explanation: 'Removed potential directory traversal patterns',
218
+ },
219
+ });
220
+ // 3. 最终路径构建
221
+ transformation.final = path.join(rootDir, transformation.securityChecked);
222
+ this.logDebug(requestId, FileOperationEvent.PATH_FINAL_CONSTRUCTION, {
223
+ rootDir: rootDir,
224
+ securityChecked: transformation.securityChecked,
225
+ final: transformation.final,
226
+ debugInfo: {
227
+ explanation: `Joined with root directory: ${rootDir}`,
228
+ },
229
+ });
230
+ return transformation;
231
+ }
232
+ // 生成唯一请求ID
233
+ generateRequestId() {
234
+ return `files_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
235
+ }
236
+ // 获取活跃操作数量
237
+ getActiveOperationCount() {
238
+ return this.activeOperations.size;
239
+ }
240
+ // 获取指定操作的上下文
241
+ getOperationContext(requestId) {
242
+ return this.activeOperations.get(requestId);
243
+ }
244
+ // 清理超时操作(防止内存泄漏)
245
+ cleanupTimeoutOperations(timeoutMs = 300000) {
246
+ // 默认5分钟超时
247
+ const now = Date.now();
248
+ const timeoutIds = [];
249
+ for (const [requestId, context] of this.activeOperations.entries()) {
250
+ if (now - context.startTime > timeoutMs) {
251
+ timeoutIds.push(requestId);
252
+ }
253
+ }
254
+ for (const requestId of timeoutIds) {
255
+ const context = this.activeOperations.get(requestId);
256
+ if (context) {
257
+ this.logDebug(requestId, 'files.operation.timeout', {
258
+ warning: 'Operation timed out and cleaned up',
259
+ duration: now - context.startTime,
260
+ });
261
+ this.activeOperations.delete(requestId);
262
+ }
263
+ }
264
+ if (timeoutIds.length > 0) {
265
+ logAgentDebug('file_operation.logger', {
266
+ message: `Cleaned up ${timeoutIds.length} timeout operations`,
267
+ timeoutIds,
268
+ });
269
+ }
270
+ }
271
+ }