@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.
Files changed (53) 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/__tests__/files_manager.test.js +209 -0
  15. package/dist/files/artifact_manager.js +68 -0
  16. package/dist/files/file_operation_logger.js +271 -0
  17. package/dist/files/files_manager.js +511 -0
  18. package/dist/files/index.js +87 -0
  19. package/dist/files/types.js +5 -0
  20. package/dist/firmware/burn_recover.js +733 -0
  21. package/dist/firmware/prepare_images.js +184 -0
  22. package/dist/firmware/user_guide.js +43 -0
  23. package/dist/index.js +449 -0
  24. package/dist/logger.js +245 -0
  25. package/dist/macro/index.js +241 -0
  26. package/dist/macro/runner.js +168 -0
  27. package/dist/nfs/index.js +105 -0
  28. package/dist/plugins/loader.js +30 -0
  29. package/dist/proto/agent.proto +473 -0
  30. package/dist/resources/docs/board-interaction.md +115 -0
  31. package/dist/resources/docs/firmware-upgrade.md +404 -0
  32. package/dist/resources/docs/nfs-mount-guide.md +78 -0
  33. package/dist/resources/docs/tftp-transfer-guide.md +81 -0
  34. package/dist/secrets/index.js +9 -0
  35. package/dist/server/grpc.js +1069 -0
  36. package/dist/server/web.js +2284 -0
  37. package/dist/ssh/adapter.js +126 -0
  38. package/dist/ssh/candidates.js +85 -0
  39. package/dist/ssh/index.js +3 -0
  40. package/dist/ssh/paircheck.js +35 -0
  41. package/dist/ssh/tunnel.js +111 -0
  42. package/dist/tftp/client.js +345 -0
  43. package/dist/tftp/index.js +284 -0
  44. package/dist/tftp/server.js +731 -0
  45. package/dist/uboot/index.js +45 -0
  46. package/dist/ui/assets/index-BlnLVmbt.js +374 -0
  47. package/dist/ui/assets/index-xMbarYXA.css +32 -0
  48. package/dist/ui/index.html +21 -0
  49. package/dist/utils/network.js +150 -0
  50. package/dist/utils/platform.js +83 -0
  51. package/dist/utils/port-check.js +153 -0
  52. package/dist/utils/user-prompt.js +139 -0
  53. package/package.json +64 -0
@@ -0,0 +1,345 @@
1
+ import dgram from 'node:dgram';
2
+ const OPCODES = {
3
+ RRQ: 1,
4
+ WRQ: 2,
5
+ DATA: 3,
6
+ ACK: 4,
7
+ ERROR: 5,
8
+ };
9
+ const DEFAULT_BLOCK_SIZE = 512;
10
+ const DEFAULT_TIMEOUT_MS = 5000;
11
+ // 统一的客户端重试配置(保持默认轻量重试)
12
+ const TFTP_CLIENT_CONFIG = {
13
+ MAX_RETRIES: 3,
14
+ TIMEOUT_MS: DEFAULT_TIMEOUT_MS,
15
+ INITIAL_BACKOFF_MS: 500,
16
+ MAX_BACKOFF_MS: 3000,
17
+ };
18
+ function createRequestPacket(opcode, filename, mode) {
19
+ const nameBuf = Buffer.from(filename, 'ascii');
20
+ const modeBuf = Buffer.from(mode, 'ascii');
21
+ const buf = Buffer.alloc(2 + nameBuf.length + 1 + modeBuf.length + 1);
22
+ buf.writeUInt16BE(opcode, 0);
23
+ let offset = 2;
24
+ nameBuf.copy(buf, offset);
25
+ offset += nameBuf.length;
26
+ buf[offset++] = 0;
27
+ modeBuf.copy(buf, offset);
28
+ offset += modeBuf.length;
29
+ buf[offset++] = 0;
30
+ return buf;
31
+ }
32
+ function createDataPacket(blockNumber, data) {
33
+ const buf = Buffer.alloc(4 + data.length);
34
+ buf.writeUInt16BE(OPCODES.DATA, 0);
35
+ buf.writeUInt16BE(blockNumber, 2);
36
+ data.copy(buf, 4);
37
+ return buf;
38
+ }
39
+ function createAckPacket(blockNumber) {
40
+ const buf = Buffer.alloc(4);
41
+ buf.writeUInt16BE(OPCODES.ACK, 0);
42
+ buf.writeUInt16BE(blockNumber, 2);
43
+ return buf;
44
+ }
45
+ function parseErrorPacket(buf) {
46
+ const code = buf.readUInt16BE(2);
47
+ const msg = buf.slice(4, buf.length - 1).toString('ascii');
48
+ return { code, message: msg };
49
+ }
50
+ // 通用重试机制函数
51
+ function createRetryWrapper(operation, operationName, maxRetries = TFTP_CLIENT_CONFIG.MAX_RETRIES) {
52
+ return new Promise((resolve, reject) => {
53
+ let retryCount = 0;
54
+ let backoffMs = TFTP_CLIENT_CONFIG.INITIAL_BACKOFF_MS;
55
+ function attempt() {
56
+ operation()
57
+ .then(resolve)
58
+ .catch((err) => {
59
+ retryCount++;
60
+ if (retryCount >= maxRetries) {
61
+ reject(new Error(`${operationName} failed after ${maxRetries} retries: ${err.message}`));
62
+ return;
63
+ }
64
+ setTimeout(attempt, backoffMs);
65
+ backoffMs = Math.min(backoffMs * 2, TFTP_CLIENT_CONFIG.MAX_BACKOFF_MS);
66
+ });
67
+ }
68
+ attempt();
69
+ });
70
+ }
71
+ export async function tftpPut(host, port, remotePath, content) {
72
+ const targetPort = port || 69;
73
+ const fileName = remotePath || '';
74
+ const blockSize = DEFAULT_BLOCK_SIZE;
75
+ const totalBlocks = Math.max(1, Math.ceil(content.length / blockSize));
76
+ return new Promise((resolve, reject) => {
77
+ const socket = dgram.createSocket('udp4');
78
+ let remoteAddress = host;
79
+ let remotePort = targetPort;
80
+ let sessionAddress = null;
81
+ let sessionPort = null;
82
+ let currentBlock = 0;
83
+ let retries = 0;
84
+ let timeout = null;
85
+ let closed = false;
86
+ function clearTimer() {
87
+ if (timeout) {
88
+ clearTimeout(timeout);
89
+ timeout = null;
90
+ }
91
+ }
92
+ function cleanup(err) {
93
+ if (closed)
94
+ return;
95
+ closed = true;
96
+ clearTimer();
97
+ try {
98
+ socket.close();
99
+ }
100
+ catch { }
101
+ if (err)
102
+ reject(err);
103
+ else
104
+ resolve();
105
+ }
106
+ function armTimeout(onTimeout) {
107
+ clearTimer();
108
+ timeout = setTimeout(onTimeout, TFTP_CLIENT_CONFIG.TIMEOUT_MS);
109
+ }
110
+ // WRQ发送(只对初始请求使用重试)
111
+ let wrqRetries = 0;
112
+ function sendWrq() {
113
+ const pkt = createRequestPacket(OPCODES.WRQ, fileName, 'octet');
114
+ socket.send(pkt, targetPort, host, (err) => {
115
+ if (err) {
116
+ cleanup(err);
117
+ return;
118
+ }
119
+ armTimeout(() => {
120
+ wrqRetries++;
121
+ if (wrqRetries >= TFTP_CLIENT_CONFIG.MAX_RETRIES) {
122
+ cleanup(new Error(`TFTP WRQ timeout to ${host}:${targetPort} after ${TFTP_CLIENT_CONFIG.MAX_RETRIES} retries`));
123
+ }
124
+ else {
125
+ // 指数退避重试
126
+ const backoff = Math.min(TFTP_CLIENT_CONFIG.INITIAL_BACKOFF_MS * Math.pow(2, wrqRetries - 1), TFTP_CLIENT_CONFIG.MAX_BACKOFF_MS);
127
+ setTimeout(sendWrq, backoff);
128
+ }
129
+ });
130
+ });
131
+ }
132
+ // 数据块发送(带重试机制)
133
+ let dataRetries = 0;
134
+ function sendBlock(blockNo) {
135
+ currentBlock = blockNo;
136
+ const start = (blockNo - 1) * blockSize;
137
+ const end = blockNo === totalBlocks ? content.length : Math.min(start + blockSize, content.length);
138
+ const slice = content.subarray(start, end);
139
+ const pkt = createDataPacket(blockNo, slice);
140
+ socket.send(pkt, remotePort, remoteAddress, (err) => {
141
+ if (err) {
142
+ cleanup(err);
143
+ return;
144
+ }
145
+ armTimeout(() => {
146
+ dataRetries++;
147
+ if (dataRetries >= TFTP_CLIENT_CONFIG.MAX_RETRIES) {
148
+ cleanup(new Error(`TFTP DATA block ${blockNo} timeout after ${TFTP_CLIENT_CONFIG.MAX_RETRIES} retries`));
149
+ }
150
+ else {
151
+ // 重新发送同一个数据块
152
+ sendBlock(blockNo);
153
+ }
154
+ });
155
+ });
156
+ }
157
+ socket.on('error', (err) => {
158
+ cleanup(err);
159
+ });
160
+ socket.on('message', (msg, rinfo) => {
161
+ if (closed)
162
+ return;
163
+ if (msg.length < 4)
164
+ return;
165
+ const opcode = msg.readUInt16BE(0);
166
+ if (opcode === OPCODES.ERROR) {
167
+ const { code, message } = parseErrorPacket(msg);
168
+ cleanup(new Error(`TFTP ERROR ${code}: ${message}`));
169
+ return;
170
+ }
171
+ if (sessionPort == null) {
172
+ sessionPort = rinfo.port;
173
+ sessionAddress = rinfo.address;
174
+ remotePort = sessionPort;
175
+ remoteAddress = sessionAddress;
176
+ }
177
+ if (rinfo.port !== remotePort || rinfo.address !== remoteAddress) {
178
+ // 忽略来自未知 TID 的数据
179
+ return;
180
+ }
181
+ if (opcode === OPCODES.ACK) {
182
+ clearTimer();
183
+ dataRetries = 0; // 重置数据重试计数器
184
+ const ackBlock = msg.readUInt16BE(2);
185
+ if (ackBlock === 0 && currentBlock === 0) {
186
+ // 收到对 WRQ 的 ACK block 0,开始发送第一个数据块
187
+ sendBlock(1);
188
+ return;
189
+ }
190
+ if (ackBlock === currentBlock) {
191
+ if (ackBlock >= totalBlocks) {
192
+ cleanup();
193
+ }
194
+ else {
195
+ sendBlock(currentBlock + 1);
196
+ }
197
+ return;
198
+ }
199
+ // 重复 ACK 或过时 ACK,忽略
200
+ return;
201
+ }
202
+ });
203
+ sendWrq();
204
+ });
205
+ }
206
+ export async function tftpGet(host, port, remotePath) {
207
+ const targetPort = port || 69;
208
+ const fileName = remotePath || '';
209
+ const blockSize = DEFAULT_BLOCK_SIZE;
210
+ return new Promise((resolve, reject) => {
211
+ const socket = dgram.createSocket('udp4');
212
+ const chunks = [];
213
+ let remoteAddress = host;
214
+ let remotePort = targetPort;
215
+ let sessionAddress = null;
216
+ let sessionPort = null;
217
+ let expectedBlock = 1;
218
+ let timeout = null;
219
+ let closed = false;
220
+ function clearTimer() {
221
+ if (timeout) {
222
+ clearTimeout(timeout);
223
+ timeout = null;
224
+ }
225
+ }
226
+ function cleanupOk() {
227
+ if (closed)
228
+ return;
229
+ closed = true;
230
+ clearTimer();
231
+ try {
232
+ socket.close();
233
+ }
234
+ catch { }
235
+ resolve(Buffer.concat(chunks));
236
+ }
237
+ function cleanupErr(err) {
238
+ if (closed)
239
+ return;
240
+ closed = true;
241
+ clearTimer();
242
+ try {
243
+ socket.close();
244
+ }
245
+ catch { }
246
+ reject(err);
247
+ }
248
+ function armTimeout(onTimeout) {
249
+ clearTimer();
250
+ timeout = setTimeout(onTimeout, TFTP_CLIENT_CONFIG.TIMEOUT_MS);
251
+ }
252
+ // RRQ发送(只对初始请求使用重试)
253
+ let rrqRetries = 0;
254
+ function sendRrq() {
255
+ const pkt = createRequestPacket(OPCODES.RRQ, fileName, 'octet');
256
+ socket.send(pkt, targetPort, host, (err) => {
257
+ if (err) {
258
+ cleanupErr(err);
259
+ return;
260
+ }
261
+ armTimeout(() => {
262
+ rrqRetries++;
263
+ if (rrqRetries >= TFTP_CLIENT_CONFIG.MAX_RETRIES) {
264
+ cleanupErr(new Error(`TFTP RRQ timeout from ${host}:${targetPort} after ${TFTP_CLIENT_CONFIG.MAX_RETRIES} retries`));
265
+ }
266
+ else {
267
+ // 指数退避重试
268
+ const backoff = Math.min(TFTP_CLIENT_CONFIG.INITIAL_BACKOFF_MS * Math.pow(2, rrqRetries - 1), TFTP_CLIENT_CONFIG.MAX_BACKOFF_MS);
269
+ setTimeout(sendRrq, backoff);
270
+ }
271
+ });
272
+ });
273
+ }
274
+ // ACK发送(带重试机制)
275
+ let ackRetries = 0;
276
+ function sendAck(blockNo) {
277
+ const pkt = createAckPacket(blockNo);
278
+ socket.send(pkt, remotePort, remoteAddress, (err) => {
279
+ if (err) {
280
+ cleanupErr(err);
281
+ return;
282
+ }
283
+ armTimeout(() => {
284
+ ackRetries++;
285
+ if (ackRetries >= TFTP_CLIENT_CONFIG.MAX_RETRIES) {
286
+ cleanupErr(new Error(`TFTP DATA timeout waiting after ACK ${blockNo} after ${TFTP_CLIENT_CONFIG.MAX_RETRIES} retries`));
287
+ }
288
+ else {
289
+ // 重新发送同一个ACK
290
+ sendAck(blockNo);
291
+ }
292
+ });
293
+ });
294
+ }
295
+ socket.on('error', (err) => {
296
+ cleanupErr(err);
297
+ });
298
+ socket.on('message', (msg, rinfo) => {
299
+ if (closed)
300
+ return;
301
+ if (msg.length < 4)
302
+ return;
303
+ const opcode = msg.readUInt16BE(0);
304
+ if (opcode === OPCODES.ERROR) {
305
+ const { code, message } = parseErrorPacket(msg);
306
+ cleanupErr(new Error(`TFTP ERROR ${code}: ${message}`));
307
+ return;
308
+ }
309
+ if (opcode !== OPCODES.DATA) {
310
+ // 非 DATA 包,忽略
311
+ return;
312
+ }
313
+ if (sessionPort == null) {
314
+ sessionPort = rinfo.port;
315
+ sessionAddress = rinfo.address;
316
+ remotePort = sessionPort;
317
+ remoteAddress = sessionAddress;
318
+ }
319
+ if (rinfo.port !== remotePort || rinfo.address !== remoteAddress) {
320
+ // 来自未知 TID 的数据,忽略
321
+ return;
322
+ }
323
+ clearTimer();
324
+ ackRetries = 0; // 重置ACK重试计数器
325
+ const blockNo = msg.readUInt16BE(2);
326
+ const data = msg.slice(4);
327
+ if (blockNo === expectedBlock) {
328
+ chunks.push(data);
329
+ sendAck(blockNo);
330
+ if (data.length < blockSize) {
331
+ // 最后一个数据块
332
+ cleanupOk();
333
+ }
334
+ else {
335
+ expectedBlock++;
336
+ }
337
+ }
338
+ else if (blockNo < expectedBlock) {
339
+ // 重复数据块,重新 ACK
340
+ sendAck(blockNo);
341
+ }
342
+ });
343
+ sendRrq();
344
+ });
345
+ }
@@ -0,0 +1,284 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { error, ErrorCodes } from '../core/errors.js';
5
+ import { TftpServer } from './server.js';
6
+ import { checkPortAvailability } from '../utils/port-check.js';
7
+ import { tftpGet, tftpPut } from './client.js';
8
+ export async function ensureDir(dir) {
9
+ await fs.mkdir(dir, { recursive: true });
10
+ }
11
+ function normalizePosixSubpath(subpath) {
12
+ if (!subpath)
13
+ return '';
14
+ const replaced = subpath.replace(/\\/g, '/');
15
+ const parts = replaced.split('/').filter((p) => p && p !== '.' && p !== '..');
16
+ return parts.join('/');
17
+ }
18
+ function resolveUnderRoot(root, subpath) {
19
+ const rel = normalizePosixSubpath(subpath);
20
+ if (!rel)
21
+ return root;
22
+ const segments = rel.split('/');
23
+ return path.join(root, ...segments);
24
+ }
25
+ export async function uploadToTftp(rootDir, remoteSubpath, content) {
26
+ try {
27
+ const full = resolveUnderRoot(rootDir, remoteSubpath);
28
+ const dir = path.dirname(full);
29
+ await ensureDir(dir);
30
+ await fs.writeFile(full, content);
31
+ const stat = await fs.stat(full);
32
+ const size = stat.size;
33
+ const sha256 = crypto.createHash('sha256').update(content).digest('hex');
34
+ return { path: full, size, sha256 };
35
+ }
36
+ catch (e) {
37
+ throw error(ErrorCodes.EL_TFTP_UPLOAD_FAILED, e?.message || String(e));
38
+ }
39
+ }
40
+ export async function downloadFromTftp(rootDir, remoteSubpath) {
41
+ const full = resolveUnderRoot(rootDir, remoteSubpath);
42
+ try {
43
+ const buf = await fs.readFile(full);
44
+ const stat = await fs.stat(full);
45
+ return { content: buf, size: stat.size };
46
+ }
47
+ catch (e) {
48
+ throw error(ErrorCodes.EL_TFTP_VERIFY_FAILED, e?.message || String(e));
49
+ }
50
+ }
51
+ export async function uploadToExternalTftp(host, port, remoteSubpath, content) {
52
+ const pathPosix = normalizePosixSubpath(remoteSubpath);
53
+ try {
54
+ await tftpPut(host, port || 69, pathPosix, content);
55
+ const sha256 = crypto.createHash('sha256').update(content).digest('hex');
56
+ return { remoteSubpath: pathPosix, size: content.byteLength, sha256 };
57
+ }
58
+ catch (e) {
59
+ throw error(ErrorCodes.EL_TFTP_UPLOAD_FAILED, e?.message || String(e) || 'external TFTP upload failed');
60
+ }
61
+ }
62
+ export async function downloadFromExternalTftp(host, port, remoteSubpath) {
63
+ const pathPosix = normalizePosixSubpath(remoteSubpath);
64
+ try {
65
+ const buf = await tftpGet(host, port || 69, pathPosix);
66
+ return { content: buf, size: buf.byteLength };
67
+ }
68
+ catch (e) {
69
+ throw error(ErrorCodes.EL_TFTP_VERIFY_FAILED, e?.message || String(e) || 'external TFTP download failed');
70
+ }
71
+ }
72
+ export async function listTftpEntries(rootDir, baseSubpath, depth, maxEntries = 1000) {
73
+ const entries = [];
74
+ let truncated = false;
75
+ const start = resolveUnderRoot(rootDir, baseSubpath);
76
+ const basePrefix = path.resolve(rootDir);
77
+ async function walk(currentPath, currentDepth) {
78
+ if (entries.length >= maxEntries) {
79
+ truncated = true;
80
+ return;
81
+ }
82
+ const relFull = path.relative(basePrefix, currentPath) || '';
83
+ const name = path.basename(currentPath) || '';
84
+ const st = await fs.stat(currentPath).catch(() => null);
85
+ if (!st)
86
+ return;
87
+ const isDir = st.isDirectory();
88
+ const toPosix = (p) => p.split(path.sep).join('/');
89
+ entries.push({
90
+ path: toPosix(relFull),
91
+ name,
92
+ isDirectory: isDir,
93
+ sizeBytes: isDir ? 0 : st.size,
94
+ modifiedAtMs: st.mtimeMs || 0,
95
+ });
96
+ if (!isDir)
97
+ return;
98
+ if (depth === 0 || currentDepth >= depth)
99
+ return;
100
+ const children = await fs.readdir(currentPath, { withFileTypes: true });
101
+ for (const ent of children) {
102
+ const childFull = path.join(currentPath, ent.name);
103
+ await walk(childFull, currentDepth + 1);
104
+ if (truncated)
105
+ return;
106
+ }
107
+ }
108
+ try {
109
+ await walk(start, 0);
110
+ }
111
+ catch {
112
+ // 如果目录不存在,返回空列表
113
+ }
114
+ return { entries, truncated };
115
+ }
116
+ export async function cleanupTftpDir(dir, ttlMs) {
117
+ const now = Date.now();
118
+ const removed = [];
119
+ try {
120
+ const entries = await fs.readdir(dir, { withFileTypes: true });
121
+ for (const ent of entries) {
122
+ if (!ent.isFile())
123
+ continue;
124
+ const full = path.join(dir, ent.name);
125
+ try {
126
+ const stat = await fs.stat(full);
127
+ const mtimeMs = stat.mtimeMs || stat.ctimeMs || 0;
128
+ if (mtimeMs && now - mtimeMs > ttlMs) {
129
+ await fs.unlink(full);
130
+ removed.push(full);
131
+ }
132
+ }
133
+ catch {
134
+ // ignore individual file errors
135
+ }
136
+ }
137
+ }
138
+ catch {
139
+ // directory may not exist yet; treat as no-op
140
+ }
141
+ return removed;
142
+ }
143
+ // TFTP服务器管理
144
+ let globalTftpServer = null;
145
+ let lastTftpStatus = {
146
+ mode: 'builtin',
147
+ enabled: false,
148
+ isRunning: false,
149
+ };
150
+ /**
151
+ * 启动TFTP服务器
152
+ */
153
+ export async function startTftpServer(cfg) {
154
+ const serverConfig = cfg.tftp.server;
155
+ if (!serverConfig || serverConfig.enabled === false || serverConfig.mode === 'disabled') {
156
+ // 完全禁用模式
157
+ globalTftpServer = null;
158
+ lastTftpStatus = {
159
+ mode: 'disabled',
160
+ enabled: false,
161
+ isRunning: false,
162
+ };
163
+ return;
164
+ }
165
+ if (serverConfig.mode === 'external') {
166
+ await startExternalTftpServer(cfg);
167
+ return;
168
+ }
169
+ await startBuiltinTftpServer(cfg);
170
+ }
171
+ /**
172
+ * 启动内置TFTP服务器
173
+ */
174
+ async function startBuiltinTftpServer(cfg) {
175
+ const serverConfig = cfg.tftp.server;
176
+ const host = serverConfig.host || '0.0.0.0';
177
+ const port = serverConfig.port || 69;
178
+ // 先检查端口可用性(包含端口占用及权限不足)
179
+ try {
180
+ const portCheck = await checkPortAvailability(port, host);
181
+ if (!portCheck.available) {
182
+ lastTftpStatus = {
183
+ mode: 'builtin',
184
+ enabled: true,
185
+ isRunning: false,
186
+ host,
187
+ port,
188
+ lastError: portCheck.process?.command
189
+ ? `port ${port} unavailable, occupied by ${portCheck.process.command}`
190
+ : `port ${port} unavailable`,
191
+ conflictProcess: portCheck.process,
192
+ };
193
+ globalTftpServer = null;
194
+ return;
195
+ }
196
+ }
197
+ catch (e) {
198
+ // 端口检查出现异常,一并记录,但不中断 Agent 其它功能
199
+ lastTftpStatus = {
200
+ mode: 'builtin',
201
+ enabled: true,
202
+ isRunning: false,
203
+ host,
204
+ port,
205
+ lastError: e?.message || String(e),
206
+ };
207
+ globalTftpServer = null;
208
+ return;
209
+ }
210
+ const tftpConfig = {
211
+ port,
212
+ host,
213
+ rootDir: cfg.tftp.dir,
214
+ timeout: serverConfig.timeout,
215
+ retryCount: serverConfig.retryCount,
216
+ blockSize: serverConfig.blockSize,
217
+ };
218
+ try {
219
+ const server = new TftpServer(tftpConfig);
220
+ globalTftpServer = server;
221
+ await server.start();
222
+ const stats = server.getStats();
223
+ lastTftpStatus = {
224
+ mode: 'builtin',
225
+ enabled: true,
226
+ isRunning: true,
227
+ host: stats.host,
228
+ port: stats.port,
229
+ uptimeMs: stats.uptime,
230
+ requestsHandled: stats.requestsHandled,
231
+ errors: stats.errors,
232
+ activeConnections: stats.activeConnections,
233
+ };
234
+ }
235
+ catch (e) {
236
+ lastTftpStatus = {
237
+ mode: 'builtin',
238
+ enabled: true,
239
+ isRunning: false,
240
+ host,
241
+ port,
242
+ lastError: e?.message || String(e),
243
+ };
244
+ globalTftpServer = null;
245
+ }
246
+ }
247
+ /**
248
+ * 启动外部TFTP服务器模式
249
+ */
250
+ async function startExternalTftpServer(cfg) {
251
+ const serverConfig = cfg.tftp.server;
252
+ lastTftpStatus = {
253
+ mode: 'external',
254
+ enabled: true,
255
+ isRunning: true,
256
+ externalHost: serverConfig.externalHost,
257
+ externalPort: serverConfig.externalPort,
258
+ };
259
+ // external 模式下不在本机监听 UDP 端口
260
+ globalTftpServer = null;
261
+ }
262
+ /**
263
+ * 获取TFTP服务器状态
264
+ */
265
+ export async function getTftpServerStats() {
266
+ return lastTftpStatus;
267
+ }
268
+ /**
269
+ * 停止TFTP服务器
270
+ */
271
+ export async function stopTftpServer() {
272
+ if (globalTftpServer) {
273
+ await globalTftpServer.stop();
274
+ globalTftpServer = null;
275
+ }
276
+ if (lastTftpStatus.mode === 'builtin') {
277
+ lastTftpStatus = {
278
+ ...lastTftpStatus,
279
+ isRunning: false,
280
+ uptimeMs: 0,
281
+ activeConnections: 0,
282
+ };
283
+ }
284
+ }