| | |
| | | fs.mkdirSync(DIST, { recursive: true }); |
| | | for (const key of targets) { |
| | | const t = TARGETS[key]; |
| | | const outDir = path.join(DIST, `jms-connection-service-${t.os}`); |
| | | const outDir = path.join(DIST, "jms-connection-service-" + t.os); |
| | | try { |
| | | fs.rmSync(outDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 500 }); |
| | | } catch { |
| | | console.log(` ⚠ ${t.os} 目录清理失败(可能被占用),尝试增量覆盖...`); |
| | | console.log(" ⚠ " + t.os + " 目录清理失败(可能被占用),尝试增量覆盖..."); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | for (const key of targets) { |
| | | const t = TARGETS[key]; |
| | | const baseName = `jms-connection-service-${t.os}`; |
| | | const outDir = path.join(DIST, `jms-connection-service-${t.os}`); |
| | | const baseName = "jms-connection-service-" + t.os; |
| | | const outDir = path.join(DIST, "jms-connection-service-" + t.os); |
| | | fs.mkdirSync(outDir, { recursive: true }); |
| | | |
| | | const outPath = path.join(outDir, baseName); |
| | | console.log(` 编译 ${t.id} → ${path.relative(ROOT, outPath)}${t.ext}`); |
| | | console.log(" 编译 " + t.id + " → " + path.relative(ROOT, outPath) + t.ext); |
| | | |
| | | try { |
| | | execSync( |
| | | `npx pkg . --targets ${t.id} --output "${outPath}"`, |
| | | { cwd: ROOT, stdio: "inherit", env: { ...process.env, PKG_CACHE_PATH: PKG_CACHE } } |
| | | 'npx pkg . --targets ' + t.id + ' --output "' + outPath + '"', |
| | | { cwd: ROOT, stdio: "inherit", env: Object.assign({}, process.env, { PKG_CACHE_PATH: PKG_CACHE }) } |
| | | ); |
| | | builtFiles.push({ key, outDir, baseName, ext: t.ext, os: t.os }); |
| | | builtFiles.push({ key: key, outDir: outDir, baseName: baseName, ext: t.ext, os: t.os }); |
| | | } catch (err) { |
| | | console.error(` ✗ ${t.id} 编译失败: ${err.message}`); |
| | | console.error(" ✗ " + t.id + " 编译失败: " + err.message); |
| | | process.exit(1); |
| | | } |
| | | } |
| | |
| | | console.log("[4/5] 组装发布包..."); |
| | | |
| | | for (const item of builtFiles) { |
| | | const { outDir } = item; |
| | | const outDir = item.outDir; |
| | | |
| | | // 复制配置模板 |
| | | const configSrc = path.join(ROOT, "config.json"); |
| | |
| | | fs.mkdirSync(path.join(outDir, "logs"), { recursive: true }); |
| | | |
| | | // 写入使用说明 |
| | | const readme = [ |
| | | "JMS 透析机 TCP 联机服务", |
| | | "========================", |
| | | "", |
| | | "快速开始:", |
| | | " 1. 编辑 config.json,配置设备 IP、端口、序列号", |
| | | " 2. 命令行启动:", |
| | | ` Windows: .\\jms-connection-service-windows.exe`, |
| | | ` Linux: ./jms-connection-service-linux`, |
| | | " 3. 浏览器访问 http://localhost:3100 查看监控大屏", |
| | | "", |
| | | "目录结构:", |
| | | ` jms-connection-service-${item.os}/`, |
| | | ` ├── jms-connection-service-${item.os}${item.ext} # 主程序`, |
| | | " ├── config.json # 配置文件(可编辑)", |
| | | " └── logs/ # 日志目录(自动创建)", |
| | | "", |
| | | "注册为系统服务(推荐生产环境):", |
| | | ` Windows: sc create JMSConnection binPath= "完整路径\\jms-connection-service-windows.exe" start= auto`, |
| | | " Linux: 参考 DEPLOY.md 中的 PM2 / systemd 章节", |
| | | "", |
| | | "详细文档见 DEPLOY.md", |
| | | ].join("\n"); |
| | | var readmeLines = []; |
| | | readmeLines.push("JMS 透析机 TCP 联机服务"); |
| | | readmeLines.push("========================"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("快速开始:"); |
| | | readmeLines.push(" 1. 编辑 config.json,配置设备 IP、端口、序列号"); |
| | | readmeLines.push(" 2. 命令行启动:"); |
| | | readmeLines.push(" Windows: .\\jms-connection-service-windows.exe"); |
| | | readmeLines.push(" Linux: ./jms-connection-service-linux"); |
| | | readmeLines.push(" 3. 浏览器访问 http://localhost:3100 查看监控大屏"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("──────────────────────────────────────────────────────"); |
| | | readmeLines.push("config.json 配置项说明"); |
| | | readmeLines.push("──────────────────────────────────────────────────────"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("【全局参数】"); |
| | | readmeLines.push(" pollIntervalMs = 10000 // K 轮询间隔(毫秒)"); |
| | | readmeLines.push(" connectTimeoutMs = 5000 // TCP 握手超时(毫秒)"); |
| | | readmeLines.push(" reconnectBaseMs = 3000 // 重连退避基数(毫秒)"); |
| | | readmeLines.push(" reconnectMaxMs = 60000 // 重连退避上限(毫秒)"); |
| | | readmeLines.push(" logDir = \"./logs\" // 日志目录,基于程序所在目录"); |
| | | readmeLines.push(" logRetentionDays = 30 // 日志保留天数"); |
| | | readmeLines.push(" dashboardPort = 3100 // 监控大屏 HTTP 端口"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("【MQTT 上传(可选,默认关闭)】"); |
| | | readmeLines.push(" mqtt.enabled = false // 是否启用 MQTT"); |
| | | readmeLines.push(" mqtt.brokerUrl = \"mqtt.ihemodialysis.com\""); |
| | | readmeLines.push(" mqtt.port = 62283"); |
| | | readmeLines.push(" mqtt.username = \"data\""); |
| | | readmeLines.push(" mqtt.password = \"data#2018\""); |
| | | readmeLines.push(" mqtt.reconnectPeriod = 5000 // 重连间隔(毫秒)"); |
| | | readmeLines.push(" mqtt.clientCode = \"CLIENT...\" // 客户端标识"); |
| | | readmeLines.push(" mqtt.defaultTopicPrefix = \"touxiji\" // topic = prefix/序列号"); |
| | | readmeLines.push(" mqtt.qos = 1"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("【阿里云 IoT 上传(可选,默认开启)】"); |
| | | readmeLines.push(" aliyun.enabled = true"); |
| | | readmeLines.push(" aliyun.autoRegister = true // 设备不存在时自动注册"); |
| | | readmeLines.push(" aliyun.tupleApiBaseUrl = \"https://things.icoldchain.cn/\""); |
| | | readmeLines.push(" aliyun.tupleApiPath = \"device/info/getAliyunDeviceSecret\""); |
| | | readmeLines.push(" aliyun.tupleRetryCooldownMs = 60000 // 三元组失败冷却(毫秒)"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("【设备列表(必填)】"); |
| | | readmeLines.push(" devices = ["); |
| | | readmeLines.push(" {"); |
| | | readmeLines.push(" \"ip\": \"192.168.1.101\", // 设备 IP(必填)"); |
| | | readmeLines.push(" \"port\": 10001, // 端口(GC-110N 默认 10001)"); |
| | | readmeLines.push(" \"serialNumber\": \"D001\", // 序列号(必填,上传标识)"); |
| | | readmeLines.push(" \"enabled\": true // 是否启用(false=跳过)"); |
| | | readmeLines.push(" },"); |
| | | readmeLines.push(" ...可添加多台设备"); |
| | | readmeLines.push(" ]"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("──────────────────────────────────────────────────────"); |
| | | readmeLines.push("重连策略: 断线后指数退避 3s → 6s → 12s → 24s → 48s → 60s 上限"); |
| | | readmeLines.push("连接成功后计数器重置。"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("目录结构:"); |
| | | readmeLines.push(" jms-connection-service-" + item.os + "/"); |
| | | readmeLines.push(" ├── jms-connection-service-" + item.os + item.ext + " # 主程序"); |
| | | readmeLines.push(" ├── config.json # 配置文件"); |
| | | readmeLines.push(" └── logs/ # 日志目录"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("注册为系统服务(推荐生产环境):"); |
| | | readmeLines.push(" Windows: sc create JMSConnection binPath= \"完整路径\\jms-connection-service-windows.exe\" start= auto"); |
| | | readmeLines.push(" Linux: 参考 DEPLOY.md 中的 PM2 / systemd 章节"); |
| | | readmeLines.push(""); |
| | | readmeLines.push("详细文档见 DEPLOY.md"); |
| | | var readme = readmeLines.join("\n"); |
| | | fs.writeFileSync(path.join(outDir, "README.txt"), readme, "utf-8"); |
| | | |
| | | console.log(` ${item.os}: ${path.relative(ROOT, outDir)}/ 组装完成`); |
| | | console.log(" " + item.os + ": " + path.relative(ROOT, outDir) + "/ 组装完成"); |
| | | } |
| | | |
| | | // ── 5. 打包归档 ── |
| | | console.log("[5/5] 打包归档..."); |
| | | |
| | | for (const item of builtFiles) { |
| | | const { outDir, baseName, os } = item; |
| | | const outDir = item.outDir; |
| | | const baseName = item.baseName; |
| | | const os = item.os; |
| | | const dirName = path.basename(outDir); |
| | | |
| | | if (os === "windows") { |
| | | const zipPath = path.join(DIST, `${baseName}.zip`); |
| | | const zipPath = path.join(DIST, baseName + ".zip"); |
| | | try { |
| | | // 清除旧 zip |
| | | try { fs.unlinkSync(zipPath); } catch {} |
| | | try { fs.unlinkSync(zipPath); } catch (e) {} |
| | | execSync( |
| | | `powershell -Command "Compress-Archive -Path '${outDir}' -DestinationPath '${zipPath}' -Force"`, |
| | | "powershell -Command \"Compress-Archive -Path '" + outDir + "' -DestinationPath '" + zipPath + "' -Force\"", |
| | | { stdio: "ignore" } |
| | | ); |
| | | console.log(` ${path.basename(zipPath)} (${formatSize(fs.statSync(zipPath).size)})`); |
| | | } catch { |
| | | console.log(` ⚠ PowerShell 不可用,跳过 zip (文件在 ${path.relative(ROOT, outDir)}/)`); |
| | | console.log(" " + path.basename(zipPath) + " (" + formatSize(fs.statSync(zipPath).size) + ")"); |
| | | } catch (e) { |
| | | console.log(" ⚠ PowerShell 不可用,跳过 zip (文件在 " + path.relative(ROOT, outDir) + "/)"); |
| | | } |
| | | } else { |
| | | const tgzPath = path.join(DIST, `${baseName}.tar.gz`); |
| | | const tgzPath = path.join(DIST, baseName + ".tar.gz"); |
| | | try { |
| | | try { fs.unlinkSync(tgzPath); } catch {} |
| | | // 用相对路径避免 Windows 盘符问题 |
| | | try { fs.unlinkSync(tgzPath); } catch (e) {} |
| | | execSync( |
| | | `tar -czf "${tgzPath}" -C "${DIST}" "${dirName}"`, |
| | | 'tar -czf "' + tgzPath + '" -C "' + DIST + '" "' + dirName + '"', |
| | | { stdio: "ignore", cwd: DIST } |
| | | ); |
| | | console.log(` ${path.basename(tgzPath)} (${formatSize(fs.statSync(tgzPath).size)})`); |
| | | } catch { |
| | | console.log(` ⚠ tar 不可用,跳过归档 (文件在 ${path.relative(ROOT, outDir)}/)`); |
| | | console.log(" " + path.basename(tgzPath) + " (" + formatSize(fs.statSync(tgzPath).size) + ")"); |
| | | } catch (e) { |
| | | console.log(" ⚠ tar 不可用,跳过归档 (文件在 " + path.relative(ROOT, outDir) + "/)"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // ── 汇总 ── |
| | | console.log("\n======== 打包完成 ========"); |
| | | console.log(`输出目录: ${path.relative(ROOT, DIST)}/`); |
| | | console.log(""); |
| | | console.log("======== 打包完成 ========"); |
| | | console.log("输出目录: " + path.relative(ROOT, DIST) + "/"); |
| | | const distFiles = fs.readdirSync(DIST); |
| | | for (const f of distFiles) { |
| | | const full = path.join(DIST, f); |
| | | const stat = fs.statSync(full); |
| | | for (var i = 0; i < distFiles.length; i++) { |
| | | var f = distFiles[i]; |
| | | var full = path.join(DIST, f); |
| | | var stat = fs.statSync(full); |
| | | if (stat.isDirectory()) { |
| | | const contents = fs.readdirSync(full); |
| | | const exe = contents.find(x => x.endsWith(".exe") || x.startsWith("jms-connection-service-")); |
| | | console.log(` ${f}/ (${exe ? exe + "、" : ""}config.json、logs/、README.txt)`); |
| | | var contents = fs.readdirSync(full); |
| | | var exe = contents.find(function(x) { return x.endsWith(".exe") || x.startsWith("jms-connection-service-"); }); |
| | | console.log(" " + f + "/ (" + (exe || "?") + "、config.json、logs/、README.txt)"); |
| | | } else { |
| | | console.log(` ${f} (${formatSize(stat.size)})`); |
| | | console.log(" " + f + " (" + formatSize(stat.size) + ")"); |
| | | } |
| | | } |
| | | console.log("\n下一步: 进入 dist/ 对应目录,编辑 config.json 后启动程序验证。"); |
| | | console.log(""); |
| | | console.log("下一步: 进入 dist/ 对应目录,编辑 config.json 后启动程序验证。"); |
| | | |
| | | // ── helpers ── |
| | | function formatSize(bytes) { |