gx
chenyc
2026-05-24 a43f8991d3f5fa2ef4e0f3eeeca00fb4afc263c0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
"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";
}