"use strict"; const { execSync } = require("child_process"); const fs = require("fs"); const path = require("path"); const ROOT = path.resolve(__dirname, ".."); const DIST = path.join(ROOT, "dist"); const PKG_CACHE = path.join(ROOT, ".pkg-cache"); const TARGETS = { win: { id: "node18-win-x64", ext: ".exe", os: "windows" }, linux:{ id: "node18-linux-x64", ext: "", os: "linux" }, }; const args = process.argv.slice(2); const winOnly = args.includes("--win"); const linuxOnly = args.includes("--linux"); const targets = winOnly ? ["win"] : linuxOnly ? ["linux"] : ["win", "linux"]; // ── 1. 清理 ── console.log("[1/5] 清理输出目录..."); fs.mkdirSync(DIST, { recursive: true }); for (const key of targets) { const t = TARGETS[key]; 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 + " 目录清理失败(可能被占用),尝试增量覆盖..."); } } // ── 2. 检查 pkg ── console.log("[2/5] 检查 pkg..."); const pkgBin = path.join(ROOT, "node_modules", ".bin", "pkg"); if (!fs.existsSync(pkgBin) && !fs.existsSync(pkgBin + ".cmd")) { console.log(" pkg 未安装,正在安装..."); execSync("npm install --save-dev pkg", { cwd: ROOT, stdio: "inherit" }); } // ── 3. pkg 编译 ── console.log("[3/5] 编译可执行文件..."); const builtFiles = []; 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); fs.mkdirSync(outDir, { recursive: true }); const outPath = path.join(outDir, baseName); console.log(" 编译 " + t.id + " → " + path.relative(ROOT, outPath) + t.ext); try { execSync( 'npx pkg . --targets ' + t.id + ' --output "' + outPath + '"', { cwd: ROOT, stdio: "inherit", env: Object.assign({}, process.env, { PKG_CACHE_PATH: PKG_CACHE }) } ); builtFiles.push({ key: key, outDir: outDir, baseName: baseName, ext: t.ext, os: t.os }); } catch (err) { console.error(" ✗ " + t.id + " 编译失败: " + err.message); process.exit(1); } } // ── 4. 组装发布包 ── console.log("[4/5] 组装发布包..."); for (const item of builtFiles) { const outDir = item.outDir; // 复制配置模板 const configSrc = path.join(ROOT, "config.json"); const configDst = path.join(outDir, "config.json"); fs.copyFileSync(configSrc, configDst); // 创建 logs 目录 fs.mkdirSync(path.join(outDir, "logs"), { recursive: true }); // 写入使用说明 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 // 轮询间隔(毫秒)"); 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) + "/ 组装完成"); } // ── 5. 打包归档 ── console.log("[5/5] 打包归档..."); for (const item of builtFiles) { 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"); try { try { fs.unlinkSync(zipPath); } catch (e) {} execSync( "powershell -Command \"Compress-Archive -Path '" + outDir + "' -DestinationPath '" + zipPath + "' -Force\"", { stdio: "ignore" } ); 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"); try { try { fs.unlinkSync(tgzPath); } catch (e) {} execSync( 'tar -czf "' + tgzPath + '" -C "' + DIST + '" "' + dirName + '"', { stdio: "ignore", cwd: DIST } ); console.log(" " + path.basename(tgzPath) + " (" + formatSize(fs.statSync(tgzPath).size) + ")"); } catch (e) { console.log(" ⚠ tar 不可用,跳过归档 (文件在 " + path.relative(ROOT, outDir) + "/)"); } } } // ── 汇总 ── console.log(""); console.log("======== 打包完成 ========"); console.log("输出目录: " + path.relative(ROOT, DIST) + "/"); const distFiles = fs.readdirSync(DIST); 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()) { 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(""); console.log("下一步: 进入 dist/ 对应目录,编辑 config.json 后启动程序验证。"); // ── helpers ── function formatSize(bytes) { if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + " MB"; if (bytes >= 1024) return (bytes / 1024).toFixed(1) + " KB"; return bytes + " B"; }