@sstar/embedlink_agent 0.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 +107 -0
- package/dist/.platform +1 -0
- package/dist/board/docs.js +59 -0
- package/dist/board/notes.js +11 -0
- package/dist/board_uart/history.js +81 -0
- package/dist/board_uart/index.js +66 -0
- package/dist/board_uart/manager.js +313 -0
- package/dist/board_uart/resource.js +578 -0
- package/dist/board_uart/sessions.js +559 -0
- package/dist/config/index.js +341 -0
- package/dist/core/activity.js +7 -0
- package/dist/core/errors.js +45 -0
- package/dist/core/log_stream.js +26 -0
- package/dist/files/__tests__/files_manager.test.js +209 -0
- package/dist/files/artifact_manager.js +68 -0
- package/dist/files/file_operation_logger.js +271 -0
- package/dist/files/files_manager.js +511 -0
- package/dist/files/index.js +87 -0
- package/dist/files/types.js +5 -0
- package/dist/firmware/burn_recover.js +733 -0
- package/dist/firmware/prepare_images.js +184 -0
- package/dist/firmware/user_guide.js +43 -0
- package/dist/index.js +449 -0
- package/dist/logger.js +245 -0
- package/dist/macro/index.js +241 -0
- package/dist/macro/runner.js +168 -0
- package/dist/nfs/index.js +105 -0
- package/dist/plugins/loader.js +30 -0
- package/dist/proto/agent.proto +473 -0
- package/dist/resources/docs/board-interaction.md +115 -0
- package/dist/resources/docs/firmware-upgrade.md +404 -0
- package/dist/resources/docs/nfs-mount-guide.md +78 -0
- package/dist/resources/docs/tftp-transfer-guide.md +81 -0
- package/dist/secrets/index.js +9 -0
- package/dist/server/grpc.js +1069 -0
- package/dist/server/web.js +2284 -0
- package/dist/ssh/adapter.js +126 -0
- package/dist/ssh/candidates.js +85 -0
- package/dist/ssh/index.js +3 -0
- package/dist/ssh/paircheck.js +35 -0
- package/dist/ssh/tunnel.js +111 -0
- package/dist/tftp/client.js +345 -0
- package/dist/tftp/index.js +284 -0
- package/dist/tftp/server.js +731 -0
- package/dist/uboot/index.js +45 -0
- package/dist/ui/assets/index-BlnLVmbt.js +374 -0
- package/dist/ui/assets/index-xMbarYXA.css +32 -0
- package/dist/ui/index.html +21 -0
- package/dist/utils/network.js +150 -0
- package/dist/utils/platform.js +83 -0
- package/dist/utils/port-check.js +153 -0
- package/dist/utils/user-prompt.js +139 -0
- package/package.json +64 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import { TftpServer } from '../tftp/server.js';
|
|
5
|
+
export class ArtifactManager extends EventEmitter {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.tftpServer = null;
|
|
9
|
+
// Default staging dir
|
|
10
|
+
this.stagingDir = path.join(process.cwd(), 'staging_artifacts');
|
|
11
|
+
if (!fs.existsSync(this.stagingDir)) {
|
|
12
|
+
fs.mkdirSync(this.stagingDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
static getInstance() {
|
|
16
|
+
if (!ArtifactManager.instance) {
|
|
17
|
+
ArtifactManager.instance = new ArtifactManager();
|
|
18
|
+
}
|
|
19
|
+
return ArtifactManager.instance;
|
|
20
|
+
}
|
|
21
|
+
getStagingDir() {
|
|
22
|
+
return this.stagingDir;
|
|
23
|
+
}
|
|
24
|
+
async initServers(cfg) {
|
|
25
|
+
// If config is provided, update stagingDir to match tftp.dir (or default)
|
|
26
|
+
if (cfg && cfg.tftp && cfg.tftp.dir) {
|
|
27
|
+
this.stagingDir = cfg.tftp.dir;
|
|
28
|
+
if (!fs.existsSync(this.stagingDir)) {
|
|
29
|
+
try {
|
|
30
|
+
fs.mkdirSync(this.stagingDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.warn(`[ArtifactManager] Failed to create staging dir ${this.stagingDir}: ${e}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 1. Start TFTP
|
|
38
|
+
// TftpServer takes config.
|
|
39
|
+
this.tftpServer = new TftpServer({
|
|
40
|
+
rootDir: this.stagingDir,
|
|
41
|
+
port: 69, // Default
|
|
42
|
+
});
|
|
43
|
+
// TftpServer.start() ? Need to check implementation
|
|
44
|
+
try {
|
|
45
|
+
await this.tftpServer.start();
|
|
46
|
+
console.log(`[ArtifactManager] TFTP server started on port 69, root: ${this.stagingDir}`);
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.warn(`[ArtifactManager] Failed to start TFTP server: ${e}`);
|
|
50
|
+
}
|
|
51
|
+
// 2. Start NFS
|
|
52
|
+
// NFS server is not embedded. We assume OS NFS server is configured if needed.
|
|
53
|
+
// Or we rely on the fact that we just write files to disk, and if the user
|
|
54
|
+
// wants to mount it, they must have set up /etc/exports.
|
|
55
|
+
console.log(`[ArtifactManager] NFS Staging: Files are in ${this.stagingDir}. Ensure this directory is exported if using NFS.`);
|
|
56
|
+
}
|
|
57
|
+
async saveArtifactStream(fileName, stream) {
|
|
58
|
+
const filePath = path.join(this.stagingDir, fileName);
|
|
59
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
60
|
+
for await (const chunk of stream) {
|
|
61
|
+
if (!writeStream.write(chunk)) {
|
|
62
|
+
await new Promise(resolve => writeStream.once('drain', () => resolve()));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
writeStream.end();
|
|
66
|
+
return filePath;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -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
|
+
}
|