@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.
- 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/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 +1083 -0
- package/dist/server/web.js +2306 -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-CCZ6chFx.css +32 -0
- package/dist/ui/assets/index-Cuhnt9D5.js +375 -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 +59 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>BoardLinker Agent</title>
|
|
8
|
+
<!-- Preload Fonts if possible, or load via CSS -->
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
11
|
+
<link
|
|
12
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
13
|
+
rel="stylesheet"
|
|
14
|
+
/>
|
|
15
|
+
<script type="module" crossorigin src="/assets/index-Cuhnt9D5.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CCZ6chFx.css">
|
|
17
|
+
</head>
|
|
18
|
+
<body class="bg-[#1A1C1E] text-[#E2E8F0]">
|
|
19
|
+
<div id="root"></div>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
/**
|
|
6
|
+
* 验证IPv4地址是否有效
|
|
7
|
+
*/
|
|
8
|
+
function isValidIPv4(address) {
|
|
9
|
+
if (!address)
|
|
10
|
+
return false;
|
|
11
|
+
const parts = address.split('.');
|
|
12
|
+
if (parts.length !== 4)
|
|
13
|
+
return false;
|
|
14
|
+
return parts.every((part) => {
|
|
15
|
+
const num = parseInt(part, 10);
|
|
16
|
+
return !isNaN(num) && num >= 0 && num <= 255;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 获取网关地址(跨平台)
|
|
21
|
+
*/
|
|
22
|
+
async function getDefaultGateway() {
|
|
23
|
+
try {
|
|
24
|
+
let command = '';
|
|
25
|
+
if (process.platform === 'linux') {
|
|
26
|
+
command = "ip route | grep default | awk '{print $3}' | head -n1";
|
|
27
|
+
}
|
|
28
|
+
else if (process.platform === 'darwin') {
|
|
29
|
+
command = "route -n get default | grep gateway | awk '{print $2}'";
|
|
30
|
+
}
|
|
31
|
+
else if (process.platform === 'win32') {
|
|
32
|
+
command =
|
|
33
|
+
'powershell "Get-NetRoute | Where-Object { $_.DestinationPrefix -eq \'0.0.0.0/0\' } | Select-Object -ExpandProperty NextHop | Select-Object -First 1"';
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// 不支持的平台
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const { stdout } = await execAsync(command);
|
|
40
|
+
const gateway = stdout.trim();
|
|
41
|
+
return isValidIPv4(gateway) ? gateway : null;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 计算广播地址
|
|
49
|
+
*/
|
|
50
|
+
function calculateBroadcastAddress(ip, netmask) {
|
|
51
|
+
if (!isValidIPv4(ip) || !isValidIPv4(netmask)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const ipParts = ip.split('.').map(Number);
|
|
56
|
+
const maskParts = netmask.split('.').map(Number);
|
|
57
|
+
const broadcastParts = [];
|
|
58
|
+
for (let i = 0; i < 4; i++) {
|
|
59
|
+
broadcastParts.push(ipParts[i] | (~maskParts[i] & 255));
|
|
60
|
+
}
|
|
61
|
+
return broadcastParts.join('.');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 获取网络信息(IPv4 only,包含网关和广播地址,排除回环接口)
|
|
69
|
+
*/
|
|
70
|
+
export async function getNetworkInfo() {
|
|
71
|
+
const nets = os.networkInterfaces();
|
|
72
|
+
const networkInfos = [];
|
|
73
|
+
const seenAddresses = new Set(); // 用于去重
|
|
74
|
+
// 获取默认网关
|
|
75
|
+
const defaultGateway = await getDefaultGateway();
|
|
76
|
+
for (const [name, infos] of Object.entries(nets)) {
|
|
77
|
+
if (!infos)
|
|
78
|
+
continue;
|
|
79
|
+
for (const info of infos) {
|
|
80
|
+
// 只要IPv4地址,排除内部接口(包括回环)
|
|
81
|
+
if (info.family !== 'IPv4' || info.internal) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// 验证地址有效性
|
|
85
|
+
if (!isValidIPv4(info.address)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// 去重检查
|
|
89
|
+
if (seenAddresses.has(info.address)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
seenAddresses.add(info.address);
|
|
93
|
+
// 计算广播地址
|
|
94
|
+
const broadcast = info.netmask
|
|
95
|
+
? calculateBroadcastAddress(info.address, info.netmask)
|
|
96
|
+
: undefined;
|
|
97
|
+
networkInfos.push({
|
|
98
|
+
ip: info.address,
|
|
99
|
+
netmask: info.netmask || '255.255.255.255',
|
|
100
|
+
broadcast: broadcast || undefined,
|
|
101
|
+
gateway: defaultGateway || undefined,
|
|
102
|
+
interface: name,
|
|
103
|
+
internal: !!info.internal,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return networkInfos;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 获取候选IP地址(只返回外部IPv4)
|
|
111
|
+
*/
|
|
112
|
+
export function getAgentIpCandidates() {
|
|
113
|
+
const nets = os.networkInterfaces();
|
|
114
|
+
const candidates = [];
|
|
115
|
+
for (const [name, infos] of Object.entries(nets)) {
|
|
116
|
+
if (!infos)
|
|
117
|
+
continue;
|
|
118
|
+
for (const info of infos) {
|
|
119
|
+
// 只要外部IPv4地址
|
|
120
|
+
if (info.family === 'IPv4' && !info.internal && isValidIPv4(info.address)) {
|
|
121
|
+
if (!candidates.includes(info.address)) {
|
|
122
|
+
candidates.push(info.address);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 如果没有外部地址,添加本地回环
|
|
128
|
+
if (candidates.length === 0) {
|
|
129
|
+
candidates.push('127.0.0.1');
|
|
130
|
+
}
|
|
131
|
+
return candidates;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 获取主要IP地址
|
|
135
|
+
*/
|
|
136
|
+
export function getPrimaryAgentIp() {
|
|
137
|
+
const candidates = getAgentIpCandidates();
|
|
138
|
+
return candidates[0] || '127.0.0.1';
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 获取网络调试信息
|
|
142
|
+
*/
|
|
143
|
+
export function getNetworkDebugInfo() {
|
|
144
|
+
return {
|
|
145
|
+
platform: os.platform(),
|
|
146
|
+
arch: os.arch(),
|
|
147
|
+
nodeVersion: process.version,
|
|
148
|
+
primaryIp: getPrimaryAgentIp(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { platform as nodePlatform, arch as nodeArch } from 'node:os';
|
|
2
|
+
/**
|
|
3
|
+
* 获取当前 agent 运行的平台信息
|
|
4
|
+
* @returns 平台信息对象,包含操作系统和架构
|
|
5
|
+
*/
|
|
6
|
+
export function getPlatformInfo() {
|
|
7
|
+
const platform = normalizePlatform(nodePlatform());
|
|
8
|
+
const arch = normalizeArch(nodeArch());
|
|
9
|
+
return {
|
|
10
|
+
platform,
|
|
11
|
+
arch,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 标准化平台名称
|
|
16
|
+
* @param platform Node.js os.platform() 返回的原始平台名
|
|
17
|
+
* @returns 标准化的平台名称
|
|
18
|
+
*/
|
|
19
|
+
function normalizePlatform(platform) {
|
|
20
|
+
switch (platform) {
|
|
21
|
+
case 'win32':
|
|
22
|
+
return 'Windows';
|
|
23
|
+
case 'darwin':
|
|
24
|
+
return 'macOS';
|
|
25
|
+
case 'linux':
|
|
26
|
+
return 'Linux';
|
|
27
|
+
case 'freebsd':
|
|
28
|
+
return 'FreeBSD';
|
|
29
|
+
case 'openbsd':
|
|
30
|
+
return 'OpenBSD';
|
|
31
|
+
case 'sunos':
|
|
32
|
+
return 'SunOS';
|
|
33
|
+
case 'aix':
|
|
34
|
+
return 'AIX';
|
|
35
|
+
default:
|
|
36
|
+
return platform;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 标准化架构名称
|
|
41
|
+
* @param arch Node.js os.arch() 返回的原始架构名
|
|
42
|
+
* @returns 标准化的架构名称
|
|
43
|
+
*/
|
|
44
|
+
function normalizeArch(arch) {
|
|
45
|
+
switch (arch) {
|
|
46
|
+
case 'x64':
|
|
47
|
+
return 'x86_64';
|
|
48
|
+
case 'x32':
|
|
49
|
+
return 'x86';
|
|
50
|
+
case 'arm':
|
|
51
|
+
return 'ARM';
|
|
52
|
+
case 'arm64':
|
|
53
|
+
return 'ARM64';
|
|
54
|
+
case 'ia32':
|
|
55
|
+
return 'x86';
|
|
56
|
+
case 'mips':
|
|
57
|
+
return 'MIPS';
|
|
58
|
+
case 'mipsel':
|
|
59
|
+
return 'MIPS-LE';
|
|
60
|
+
case 'ppc':
|
|
61
|
+
return 'PowerPC';
|
|
62
|
+
case 'ppc64':
|
|
63
|
+
return 'PowerPC64';
|
|
64
|
+
case 's390':
|
|
65
|
+
return 'S/390';
|
|
66
|
+
case 's390x':
|
|
67
|
+
return 'S/390x';
|
|
68
|
+
case 'riscv32':
|
|
69
|
+
return 'RISC-V 32-bit';
|
|
70
|
+
case 'riscv64':
|
|
71
|
+
return 'RISC-V 64-bit';
|
|
72
|
+
default:
|
|
73
|
+
return arch;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 获取完整的平台描述字符串
|
|
78
|
+
* @returns 完整的平台描述,如 "Linux x86_64"
|
|
79
|
+
*/
|
|
80
|
+
export function getPlatformDescription() {
|
|
81
|
+
const { platform, arch } = getPlatformInfo();
|
|
82
|
+
return `${platform} ${arch}`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import dgram from 'node:dgram';
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
/**
|
|
6
|
+
* 检查UDP端口是否可用
|
|
7
|
+
*/
|
|
8
|
+
export async function checkPortAvailability(port, host = '0.0.0.0') {
|
|
9
|
+
if (port < 1 || port > 65535) {
|
|
10
|
+
throw new Error(`Invalid port number: ${port}`);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const socket = dgram.createSocket('udp4');
|
|
14
|
+
await new Promise((resolve, reject) => {
|
|
15
|
+
socket.bind(port, host, () => {
|
|
16
|
+
socket.close(() => resolve());
|
|
17
|
+
});
|
|
18
|
+
socket.on('error', reject);
|
|
19
|
+
});
|
|
20
|
+
return { available: true };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === 'EADDRINUSE' || error.code === 'EACCES') {
|
|
24
|
+
const processInfo = await getPortProcessInfo(port);
|
|
25
|
+
return {
|
|
26
|
+
available: false,
|
|
27
|
+
occupiedBy: 'process',
|
|
28
|
+
process: processInfo,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 获取占用端口的进程信息
|
|
36
|
+
*/
|
|
37
|
+
async function getPortProcessInfo(port) {
|
|
38
|
+
const platform = process.platform;
|
|
39
|
+
try {
|
|
40
|
+
if (platform === 'linux') {
|
|
41
|
+
const { stdout } = await execAsync(`lsof -i :${port} -n -P | grep LISTEN || lsof -i :${port} -n -P | grep UDP`);
|
|
42
|
+
return parseLinuxLsof(stdout);
|
|
43
|
+
}
|
|
44
|
+
else if (platform === 'darwin') {
|
|
45
|
+
const { stdout } = await execAsync(`lsof -i :${port} -n -P | grep LISTEN || lsof -i :${port} -n -P | grep UDP`);
|
|
46
|
+
return parseLinuxLsof(stdout);
|
|
47
|
+
}
|
|
48
|
+
else if (platform === 'win32') {
|
|
49
|
+
const { stdout } = await execAsync(`netstat -aon | findstr :${port}`);
|
|
50
|
+
return await parseWindowsNetstat(stdout, port);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// 忽略进程信息获取失败,返回空对象
|
|
55
|
+
}
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 解析Linux/macOS的lsof输出
|
|
60
|
+
*/
|
|
61
|
+
function parseLinuxLsof(output) {
|
|
62
|
+
try {
|
|
63
|
+
const lines = output.trim().split('\n');
|
|
64
|
+
if (lines.length === 0 || lines[0] === '')
|
|
65
|
+
return {};
|
|
66
|
+
const line = lines[0];
|
|
67
|
+
const parts = line.trim().split(/\s+/);
|
|
68
|
+
if (parts.length >= 2) {
|
|
69
|
+
const command = parts[0];
|
|
70
|
+
const pid = parseInt(parts[1]);
|
|
71
|
+
if (!isNaN(pid)) {
|
|
72
|
+
return {
|
|
73
|
+
pid,
|
|
74
|
+
name: command,
|
|
75
|
+
command: `${command} (PID: ${pid})`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// 忽略解析错误
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 解析Windows的netstat输出
|
|
87
|
+
*/
|
|
88
|
+
async function parseWindowsNetstat(output, port) {
|
|
89
|
+
try {
|
|
90
|
+
const lines = output.trim().split('\n');
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
if (line.includes(`:${port}`)) {
|
|
93
|
+
const parts = line.trim().split(/\s+/);
|
|
94
|
+
const pidStr = parts[parts.length - 1];
|
|
95
|
+
const pid = parseInt(pidStr);
|
|
96
|
+
if (!isNaN(pid)) {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout: processName } = await execAsync(`tasklist /FI "PID eq ${pid}" /FO CSV | findstr /V "INFO"`);
|
|
99
|
+
const nameMatch = processName.match(/"([^"]+)"/);
|
|
100
|
+
const name = nameMatch ? nameMatch[1] : `Unknown (PID: ${pid})`;
|
|
101
|
+
return {
|
|
102
|
+
pid,
|
|
103
|
+
name,
|
|
104
|
+
command: `${name} (PID: ${pid})`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// 如果无法获取进程名,至少返回PID
|
|
109
|
+
return {
|
|
110
|
+
pid,
|
|
111
|
+
name: `Unknown (PID: ${pid})`,
|
|
112
|
+
command: `Unknown (PID: ${pid})`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
// 忽略解析错误
|
|
121
|
+
}
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 查找一个可用的端口(从指定端口开始)
|
|
126
|
+
*/
|
|
127
|
+
export async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
128
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
129
|
+
const port = startPort + i;
|
|
130
|
+
const result = await checkPortAvailability(port);
|
|
131
|
+
if (result.available) {
|
|
132
|
+
return port;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`No available port found from ${startPort} to ${startPort + maxAttempts - 1}`);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 检查TCP端口是否可用(用于验证)
|
|
139
|
+
*/
|
|
140
|
+
export async function checkTcpPortAvailability(port, host = '127.0.0.1') {
|
|
141
|
+
const net = await import('node:net');
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const server = net.createServer();
|
|
144
|
+
server.on('error', () => {
|
|
145
|
+
resolve(false);
|
|
146
|
+
});
|
|
147
|
+
server.listen(port, host, () => {
|
|
148
|
+
server.close(() => {
|
|
149
|
+
resolve(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
/**
|
|
3
|
+
* 提示用户处理端口冲突
|
|
4
|
+
*/
|
|
5
|
+
export async function promptUserForPortConflict(port, conflictInfo) {
|
|
6
|
+
// 检查是否在TTY环境中
|
|
7
|
+
if (!process.stdin.isTTY) {
|
|
8
|
+
// 非交互模式,自动选择备用端口
|
|
9
|
+
return {
|
|
10
|
+
action: 'change_port',
|
|
11
|
+
newPort: port + 1,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const rl = readline.createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stdout,
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
console.log(`\n❌ 端口 ${port} 被占用:`);
|
|
20
|
+
if (conflictInfo?.process?.command) {
|
|
21
|
+
console.log(` 占用进程: ${conflictInfo.process.command}`);
|
|
22
|
+
}
|
|
23
|
+
console.log(` 端口69是TFTP协议的标准端口`);
|
|
24
|
+
console.log(` 如果这是其他TFTP服务器,建议使用外部服务器模式`);
|
|
25
|
+
console.log('\n请选择处理方式:');
|
|
26
|
+
console.log('1. 使用备用端口启动内置TFTP服务器 (如70)');
|
|
27
|
+
console.log('2. 配置外部TFTP服务器 (指定其他TFTP服务器地址)');
|
|
28
|
+
console.log('3. 停止占用端口的进程后重试 (需要手动操作)');
|
|
29
|
+
console.log('4. 禁用TFTP服务器 (仅保留文件管理功能)');
|
|
30
|
+
const answer = await new Promise((resolve) => {
|
|
31
|
+
rl.question('\n请输入选择 (1-4): ', resolve);
|
|
32
|
+
});
|
|
33
|
+
switch (answer.trim()) {
|
|
34
|
+
case '1':
|
|
35
|
+
const newPort = await askForPort(rl, port + 1, '备用端口');
|
|
36
|
+
return { action: 'change_port', newPort };
|
|
37
|
+
case '2':
|
|
38
|
+
const externalHost = await askForString(rl, '外部TFTP服务器地址', '127.0.0.1');
|
|
39
|
+
const externalPort = await askForPort(rl, 69, '外部TFTP服务器端口');
|
|
40
|
+
return { action: 'use_external', externalHost, externalPort };
|
|
41
|
+
case '3':
|
|
42
|
+
console.log('\n请手动停止占用端口的进程后重新启动Agent:');
|
|
43
|
+
if (conflictInfo?.process?.pid) {
|
|
44
|
+
console.log(` 停止进程: kill -9 ${conflictInfo.process.pid}`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.log(' 查找进程: lsof -i :' + port + ' (Linux/macOS)');
|
|
48
|
+
console.log(' 查找进程: netstat -aon | findstr :' + port + ' (Windows)');
|
|
49
|
+
}
|
|
50
|
+
console.log('\nAgent即将退出...');
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
case '4':
|
|
54
|
+
return { action: 'disable' };
|
|
55
|
+
default:
|
|
56
|
+
console.log('\n❌ 无效选择,将使用备用端口启动');
|
|
57
|
+
const fallbackPort = port + 1;
|
|
58
|
+
return { action: 'change_port', newPort: fallbackPort };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
rl.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 询问端口号
|
|
67
|
+
*/
|
|
68
|
+
async function askForPort(rl, defaultPort, label) {
|
|
69
|
+
while (true) {
|
|
70
|
+
const answer = await new Promise((resolve) => {
|
|
71
|
+
rl.question(`${label} (默认 ${defaultPort}): `, resolve);
|
|
72
|
+
});
|
|
73
|
+
const input = answer.trim();
|
|
74
|
+
if (input === '') {
|
|
75
|
+
return defaultPort;
|
|
76
|
+
}
|
|
77
|
+
const port = parseInt(input);
|
|
78
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
79
|
+
console.log('❌ 无效的端口号,请输入1-65535之间的数字');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
return port;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 询问字符串
|
|
87
|
+
*/
|
|
88
|
+
async function askForString(rl, prompt, defaultValue) {
|
|
89
|
+
const answer = await new Promise((resolve) => {
|
|
90
|
+
rl.question(`${prompt} (默认 ${defaultValue}): `, resolve);
|
|
91
|
+
});
|
|
92
|
+
return answer.trim() || defaultValue;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 询问用户确认 (y/n)
|
|
96
|
+
*/
|
|
97
|
+
export async function askForConfirmation(message, defaultValue = true) {
|
|
98
|
+
if (!process.stdin.isTTY) {
|
|
99
|
+
return defaultValue;
|
|
100
|
+
}
|
|
101
|
+
const rl = readline.createInterface({
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stdout,
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
const defaultStr = defaultValue ? 'Y/n' : 'y/N';
|
|
107
|
+
const answer = await new Promise((resolve) => {
|
|
108
|
+
rl.question(`${message} (${defaultStr}): `, resolve);
|
|
109
|
+
});
|
|
110
|
+
const input = answer.trim().toLowerCase();
|
|
111
|
+
if (input === '') {
|
|
112
|
+
return defaultValue;
|
|
113
|
+
}
|
|
114
|
+
return input === 'y' || input === 'yes';
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
rl.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 显示提示信息并等待用户按回车继续
|
|
122
|
+
*/
|
|
123
|
+
export async function waitForEnter(message = '\n按回车键继续...') {
|
|
124
|
+
if (!process.stdin.isTTY) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const rl = readline.createInterface({
|
|
128
|
+
input: process.stdin,
|
|
129
|
+
output: process.stdout,
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
rl.question(message, () => resolve());
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
rl.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sstar/boardlinker_agent",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "BoardLinker Agent - Board hardware access service for embedded development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"boardlinker": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/**"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"postbuild": "node scripts/inject-proto.mjs",
|
|
19
|
+
"prepack": "npm run build",
|
|
20
|
+
"start": "node dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"embedded",
|
|
27
|
+
"hardware",
|
|
28
|
+
"uart",
|
|
29
|
+
"serial",
|
|
30
|
+
"grpc",
|
|
31
|
+
"mcp",
|
|
32
|
+
"firmware",
|
|
33
|
+
"board"
|
|
34
|
+
],
|
|
35
|
+
"author": "SStar",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/sstar/board_linker.git",
|
|
40
|
+
"directory": "packages/agent"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@grpc/grpc-js": "^1.10.7",
|
|
44
|
+
"@grpc/proto-loader": "^0.7.13",
|
|
45
|
+
"@homebridge/node-pty-prebuilt-multiarch": "0.13.1",
|
|
46
|
+
"@toon-format/toon": "^2.1.0",
|
|
47
|
+
"express": "^4.19.2",
|
|
48
|
+
"serialport": "^12.0.0",
|
|
49
|
+
"ssh2": "^1.15.0",
|
|
50
|
+
"ws": "^8.18.0",
|
|
51
|
+
"yaml": "^2.5.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/express": "^5.0.5",
|
|
55
|
+
"@types/ssh2": "^1.15.5",
|
|
56
|
+
"@types/ws": "^8.18.1",
|
|
57
|
+
"typescript": "^5.6.3"
|
|
58
|
+
}
|
|
59
|
+
}
|