gx
chenyc
2026-05-24 a43f8991d3f5fa2ef4e0f3eeeca00fb4afc263c0
gx
8个文件已修改
2个文件已添加
121 ■■■■ 已修改文件
.claude/settings.local.json 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
DEPLOY.md 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
dashboard/public/index.html 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dashboard/server.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lib/logger.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
lib/obfuscate.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/build.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
tools/decode-log.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/settings.local.json
@@ -7,7 +7,9 @@
      "Bash(netstat -ano)",
      "Bash(powershell *)",
      "Bash(curl -s -o nul -w \"%{http_code}\" http://localhost:3100/)",
      "Bash(node scripts/build.js --linux)"
      "Bash(node scripts/build.js --linux)",
      "Bash(node tools/decode-log.js --stdin)",
      "Bash(timeout 3 node -e \"require\\('./lib/logger'\\)\")"
    ]
  }
}
DEPLOY.md
@@ -2,12 +2,12 @@
## 1. 系统概述
JMS 联机服务用于管理与监控多台 **GC-110N 透析设备**。通过持久 TCP 长连接 + 定时 K 指令轮询,实时采集 32 项治疗参数,经 MQTT / 阿里云 IoT 上传至云端,并提供本地 Web 监控大屏。
JMS 联机服务用于管理与监控多台 **GC-110N 透析设备**。通过持久 TCP 长连接 + 定时指令轮询,实时采集 32 项治疗参数,经 MQTT / 阿里云 IoT 上传至云端,并提供本地 Web 监控大屏。
```
┌──────────┐   TCP:10001    ┌─────────────────┐    MQTT     ┌──────────────┐
│ GC-110N  │←──────────────→│  jms-connection  │───────────→│  MQTT Broker  │
│ 透析机 1  │   K 轮询 (10s)  │     -service     │            └──────────────┘
│ 透析机 1  │   轮询 (10s)    │     -service     │            └──────────────┘
└──────────┘                │                  │
     ···                    │  dashboard:3100  │   HTTP     ┌──────────────┐
┌──────────┐                │  ← 浏览器访问      │───────────→│ 阿里云 IoT     │
@@ -233,7 +233,7 @@
```jsonc
{
  // ── 全局参数 ──
  "pollIntervalMs": 10000,       // K 轮询间隔(毫秒),默认 10s
  "pollIntervalMs": 10000,       // 轮询间隔(毫秒),默认 10s
  "connectTimeoutMs": 5000,      // TCP 握手超时(毫秒)
  "reconnectBaseMs": 3000,       // 重连退避基数(毫秒)
  "reconnectMaxMs": 60000,       // 重连退避上限(毫秒)
@@ -414,7 +414,7 @@
| 目标 | 协议 | 端口 | 用途 |
|------|------|------|------|
| GC-110N 设备 | TCP | 10001(可配置) | K 指令轮询 |
| GC-110N 设备 | TCP | 10001(可配置) | 指令轮询 |
| MQTT Broker | TCP | 62283(可配置) | MQTT 上传 |
| 阿里云 IoT | TCP | 443 (TLS) | 属性上报 |
| 三元组 API | HTTPS | 443 | 获取设备凭证 |
@@ -475,8 +475,8 @@
### 9.2 收不到数据 / 字段数为 0
1. 确认设备已进入治疗状态(待机状态可能返回有限数据)
2. 查看日志中 `←` 行,确认报文以 `K` 开头 + 4 位状态码
3. 确认设备固件支持 K 格式协议
2. 查看日志中 `←` 行,确认收到设备报文(混淆格式可通过 `node tools/decode-log.js --stdin` 解码查看)
3. 确认设备固件协议版本兼容
### 9.3 阿里云上传失败
@@ -519,8 +519,8 @@
│   ├── logger.js                  # 日志模块(按天滚动)
│   ├── data-cache.js              # 内存数据缓存(Map<ip, deviceData>)
│   ├── device-manager.js          # 设备管理(遍历创建连接)
│   ├── device-connection.js       # 单设备 TCP 长连接 + K 轮询 + 重连
│   ├── protocol.js                # GC-110N K 格式解析器
│   ├── device-connection.js       # 单设备 TCP 长连接 + 轮询 + 重连
│   ├── protocol.js                # GC-110N 协议解析器
│   └── upload/
│       ├── index.js               # 上传总控(顺序:MQTT → 阿里云)
│       ├── mqtt-uploader.js       # MQTT 单例客户端
config.json
@@ -26,7 +26,7 @@
  },
  "devices": [
    {
      "ip": "192.168.160.1",
      "ip": "169.254.233.58",
      "port": 10001,
      "serialNumber": "xy123",
      "enabled": true
dashboard/public/index.html
@@ -80,16 +80,9 @@
  padding: 4px 8px; border-radius: 4px; background: #161b22;
  font-size: 12px;
}
.detail-panel .field .fid { color: #58a6ff; font-weight: 600; min-width: 18px; }
.detail-panel .field .fname { color: #8b949e; flex: 1; margin: 0 8px; }
.detail-panel .field .fval { color: #f0f6fc; font-family: 'Consolas', monospace; }
.detail-panel .field .funit { color: #484f58; margin-left: 4px; font-size: 11px; }
.detail-panel .raw-frame {
  grid-column: 1 / -1; margin-top: 8px; padding: 8px 12px;
  background: #0d1117; border: 1px solid #30363d; border-radius: 4px;
  font-family: 'Consolas', monospace; font-size: 13px; color: #d2a8ff;
  word-break: break-all; max-height: 100px; overflow-y: auto;
}
.clickable { cursor: pointer; user-select: none; }
.clickable td:first-child { color: #58a6ff; }
@@ -104,7 +97,7 @@
<div class="header">
  <div>
    <h1>JMS GC-110N 联机服务监控</h1>
    <div class="info">协议 Ver.3.0 &middot; K 格式状态轮询</div>
    <div class="info">TCP 长连接 &middot; 定时轮询</div>
  </div>
  <div class="info">
    <span class="dot live"></span>
@@ -236,13 +229,11 @@
          <div class="detail-panel">
            ${fields.length === 0 ? '<div style="color:#484f58;grid-column:1/-1;">暂无解析数据</div>' :
              fields.map(f => `<div class="field">
                <span class="fid">${f.id}</span>
                <span class="fname">${f.name||''}</span>
                <span class="fval">${f.displayValue||f.value||'(空)'}</span>
                <span class="funit">${f.unit||''}</span>
              </div>`).join('')}
            ${dev.rawFrame ? `<div class="raw-frame">${dev.rawFrame}</div>` : ''}
          </div>
                      </div>
        </td>
      </tr>`;
    }
dashboard/server.js
@@ -32,11 +32,15 @@
  }
  function buildSnapshot() {
    const devices = dataCache.getAll().map((dev) => {
      const { rawFrame, ...rest } = dev;
      return rest;
    });
    return {
      type: "snapshot",
      timestamp: new Date().toISOString(),
      summary: dataCache.getSummary(),
      devices: dataCache.getAll(),
      devices,
      config: {
        pollIntervalMs: config.pollIntervalMs,
        reconnectBaseMs: config.reconnectBaseMs,
lib/logger.js
@@ -2,6 +2,7 @@
const fs = require("fs");
const path = require("path");
const { obfuscate } = require("./obfuscate");
const C = { reset: "\x1b[0m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", white: "\x1b[37m" };
@@ -79,10 +80,10 @@
    // ── 数据收发 ──
    sendK(ip) {
      log(ip, C.yellow, "→", `发送 K 请求`);
      log(ip, C.yellow, "→", `发送轮询请求`);
    },
    recvK(ip, rawFrame, fieldCount, statusCode) {
      log(ip, C.green, "←", `${rawFrame}`);
      log(ip, C.green, "←", `${obfuscate(rawFrame)}`);
    },
    // ── 异常 ──
lib/obfuscate.js
New file
@@ -0,0 +1,34 @@
"use strict";
const DEFAULT_KEY = "jms-gc110n-log-obfuscate-default";
function getKey() {
  return process.env.JMS_LOG_KEY || DEFAULT_KEY;
}
function obfuscate(text) {
  if (!text) return text;
  const key = Buffer.from(getKey(), "utf-8");
  const data = Buffer.from(text, "utf-8");
  const out = Buffer.alloc(data.length);
  for (let i = 0; i < data.length; i++) {
    out[i] = data[i] ^ key[i % key.length];
  }
  return "[OBF]" + out.toString("base64");
}
function deobfuscate(encoded) {
  if (!encoded) return encoded;
  if (encoded.startsWith("[OBF]")) {
    encoded = encoded.slice(5);
  }
  const key = Buffer.from(getKey(), "utf-8");
  const data = Buffer.from(encoded, "base64");
  const out = Buffer.alloc(data.length);
  for (let i = 0; i < data.length; i++) {
    out[i] = data[i] ^ key[i % key.length];
  }
  return out.toString("utf-8");
}
module.exports = { obfuscate, deobfuscate };
package.json
@@ -8,7 +8,8 @@
    "dev": "node index.js",
    "build": "node scripts/build.js",
    "build:win": "node scripts/build.js --win",
    "build:linux": "node scripts/build.js --linux"
    "build:linux": "node scripts/build.js --linux",
    "decode-log": "node tools/decode-log.js logs/service.2026-05-18.log"
  },
  "pkg": {
    "assets": [
scripts/build.js
@@ -97,7 +97,7 @@
  readmeLines.push("──────────────────────────────────────────────────────");
  readmeLines.push("");
  readmeLines.push("【全局参数】");
  readmeLines.push("  pollIntervalMs       = 10000   // K 轮询间隔(毫秒)");
  readmeLines.push("  pollIntervalMs       = 10000   // 轮询间隔(毫秒)");
  readmeLines.push("  connectTimeoutMs     = 5000    // TCP 握手超时(毫秒)");
  readmeLines.push("  reconnectBaseMs      = 3000    // 重连退避基数(毫秒)");
  readmeLines.push("  reconnectMaxMs       = 60000   // 重连退避上限(毫秒)");
tools/decode-log.js
New file
@@ -0,0 +1,36 @@
"use strict";
const fs = require("fs");
const { deobfuscate } = require("../lib/obfuscate");
function decode(text) {
  return text.replace(/\[OBF\][A-Za-z0-9+/=]+/g, (match) => {
    return deobfuscate(match);
  });
}
function decodeStdin() {
  const chunks = [];
  process.stdin.setEncoding("utf-8");
  process.stdin.on("data", (chunk) => chunks.push(chunk));
  process.stdin.on("end", () => {
    process.stdout.write(decode(chunks.join("")));
  });
}
function decodeFile(filePath) {
  const content = fs.readFileSync(filePath, "utf-8");
  process.stdout.write(decode(content));
}
if (process.argv.includes("--stdin")) {
  decodeStdin();
} else if (process.argv[2]) {
  decodeFile(process.argv[2]);
} else {
  console.log("用法:");
  console.log("  node tools/decode-log.js <日志文件>    解码日志文件");
  console.log("  node tools/decode-log.js --stdin       解码标准输入");
  console.log("");
  console.log("密钥: JMS_LOG_KEY 环境变量 (未设置时使用默认值)");
}