"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";
|
}
|