#!/usr/bin/env python3 # -*- coding: utf-8 -*- """一次性修复: 观澜·如东样板 v2 启动器的 UTF-8 BOM 崩溃 + serve.json 中文乱码。 报错: guanlan.py cfg() -> json.loads(p.read_text(encoding="utf-8")) json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig) 根因 (install.ps1 第 4 步 "写配置", Windows PowerShell 5.1): 1) `Set-Content configs\\serve.json -Encoding UTF8` 写出的是 **带 BOM** 的 UTF-8; guanlan.py 用裸 utf-8 读 -> 直接 JSONDecodeError, 启动器连 check 都跑不了。 2) 紧随其前的 `Get-Content` 对无 BOM 的 UTF-8 按 ANSI 代码页 (中文机 = GBK) 解码, 把 serve.json 里的中文注释读成乱码再写回 -> 注释字段损坏 (部分字节已被替换成 '?', 不可逆)。 修复: A. guanlan.py : 所有 JSON 配置改走 jload() = utf-8-sig (兼容有/无 BOM); 配置损坏时打印提示并退回 内置默认端口, 不再抛栈 (hand-edit 配置写坏 BOM 也不该让产品起不来)。 B. install.ps1 : 第 4 步改由 venv 里的 Python 读写配置 (读 utf-8-sig / 写无 BOM 的 UTF-8), 不再用 PS 的 cmdlet 碰 serve.json; 保持文件 UTF-8 带 BOM + CRLF 的既有约定。 C. serve.json : 去掉 BOM, 还原两个 _note 中文注释 (依据原文件残留可逆部分 + 说明书 §7)。 幂等: 重复运行只报 "已修过"。原文件备份为 *.bak-bomfix。 """ from __future__ import annotations import json import pathlib import shutil import sys ROOT = pathlib.Path(r"F:\temp\guanlan-rudong-v2_0.2.0") GUANLAN = ROOT / "guanlan.py" INSTALL_PS1 = ROOT / "install.ps1" SERVE_JSON = ROOT / "configs" / "serve.json" BAK = ".bak-bomfix" report: list[str] = [] def fail(msg: str) -> None: print(" [X] " + msg) sys.exit(1) def backup(p: pathlib.Path) -> None: b = p.with_name(p.name + BAK) if not b.exists(): shutil.copy2(p, b) print(f" 备份 {p.name} -> {b.name}") def read_bytes(p: pathlib.Path) -> bytes: if not p.exists(): fail(f"文件不存在: {p}") return p.read_bytes() def has_bom(b: bytes) -> bool: return b.startswith(b"\xef\xbb\xbf") # ---------------------------------------------------------------- A. guanlan.py def fix_guanlan() -> None: raw = read_bytes(GUANLAN) if has_bom(raw): fail("guanlan.py 竟然带 BOM, 先人工确认再修") src = raw.decode("utf-8") # 该文件是 UTF-8 无 BOM + LF if "def jload(" in src: print(" [=] guanlan.py 已修过 (存在 jload), 跳过") return backup(GUANLAN) anchor_cfg = ( 'def cfg():\n' ' c = dict(DEFAULT); p = ROOT / "configs/serve.json"\n' ' if p.exists(): c.update(json.loads(p.read_text(encoding="utf-8")))\n' ' return c\n' ) new_cfg = ( 'def jload(p: Path):\n' ' """读 JSON 配置。必须用 utf-8-sig: 记事本和 PowerShell 5.1 的 `Set-Content -Encoding UTF8`\n' ' 写出的都是 **带 BOM** 的 UTF-8, 裸 utf-8 读会 JSONDecodeError (Unexpected UTF-8 BOM)。"""\n' ' return json.loads(Path(p).read_text(encoding="utf-8-sig"))\n' '\n' '\n' 'def cfg():\n' ' c = dict(DEFAULT); p = ROOT / "configs/serve.json"\n' ' if p.exists():\n' ' try: c.update(jload(p))\n' ' except Exception as ex: # 配置写坏不该让整个产品起不来: 退回内置默认端口, 但要说清楚\n' ' print(f" [X] {p} 读不了 ({ex.__class__.__name__}: {str(ex)[:100]}); 本次用内置默认端口。"\n' ' f" 重跑 install 脚本可重写该文件")\n' ' return c\n' ) # (旧串, 新串, 期望出现次数) edits = [ (anchor_cfg, new_cfg, 1), ('mcfg = json.loads((ROOT / "configs/models.json").read_text(encoding="utf-8"))', 'mcfg = jload(ROOT / "configs/models.json")', 1), ('pids = json.loads(PIDS.read_text(encoding="utf-8")) if PIDS.exists() else {}', 'pids = jload(PIDS) if PIDS.exists() else {}', 2), ('pids = json.loads(PIDS.read_text(encoding="utf-8"))', 'pids = jload(PIDS)', 1), ] for old, new, want in edits: got = src.count(old) if got != want: fail(f"guanlan.py 锚点命中 {got} 次 (期望 {want}), 未改动: {old[:60]!r}") src = src.replace(old, new) left = src.count('read_text(encoding="utf-8")') # 修完不该再有裸 utf-8 的 JSON 读 if left: fail(f"guanlan.py 仍有 {left} 处裸 utf-8 读 JSON") compile(src, str(GUANLAN), "exec") # 语法自检, 不过就不落盘 GUANLAN.write_bytes(src.encode("utf-8")) report.append("guanlan.py: JSON 配置改走 utf-8-sig; 配置损坏时给提示不抛栈") print(" [OK] guanlan.py 已修") # --------------------------------------------------------------- B. install.ps1 NEW_STEP4 = '''Say "== 4/5 写配置" # 别改回 Get-Content / Set-Content -Encoding UTF8 (2026-09-10 实机踩过): # PS 5.1 的 Get-Content 把无 BOM 的 UTF-8 当 ANSI(GBK) 解码 -> 中文注释变乱码; # Set-Content -Encoding UTF8 又写出 BOM -> Python 侧用裸 utf-8 json.loads 直接 JSONDecodeError。 # 交给 venv 里的 Python 读写: 读 utf-8-sig (有 BOM 也认), 写无 BOM 的 UTF-8。 # 下面 Python 代码故意只用单引号, 避开 PS 5.1 传参时的引号转义坑。 $cfgFix = @' import json, pathlib, sys p = pathlib.Path('configs/serve.json') c = json.loads(p.read_text(encoding='utf-8-sig')) c['python'] = sys.executable p.write_text(json.dumps(c, ensure_ascii=False, indent=2) + '\\n', encoding='utf-8') print(' configs/serve.json -> ' + sys.executable) '@ & $vpy -c $cfgFix if ($LASTEXITCODE -ne 0) { Say " X 写 configs\\serve.json 失败"; exit 4 } ''' OLD_STEP4 = ( 'Say "== 4/5 写配置"\n' '$cfg = Get-Content configs\\serve.json -Raw | ConvertFrom-Json\n' '$cfg.python = $vpy\n' '$cfg | ConvertTo-Json -Depth 5 | Set-Content configs\\serve.json -Encoding UTF8\n' ) def fix_install_ps1() -> None: raw = read_bytes(INSTALL_PS1) if not has_bom(raw): fail("install.ps1 丢了 BOM —— 文件头注释要求 UTF-8 带 BOM + CRLF, 先人工确认") text = raw.decode("utf-8-sig").replace("\r\n", "\n") if "cfgFix" in text: print(" [=] install.ps1 已修过 (存在 cfgFix), 跳过") return backup(INSTALL_PS1) if text.count(OLD_STEP4) != 1: fail(f"install.ps1 第 4 步锚点命中 {text.count(OLD_STEP4)} 次, 未改动") text = text.replace(OLD_STEP4, NEW_STEP4.replace("\n", "\n")) # 恢复该文件的编码约定: UTF-8 带 BOM + CRLF out = text.replace("\r\n", "\n").replace("\n", "\r\n") INSTALL_PS1.write_bytes(out.encode("utf-8-sig")) report.append("install.ps1: 第 4 步改由 venv Python 写配置 (utf-8-sig 读 / 无 BOM 写)") print(" [OK] install.ps1 已修 (保持 UTF-8 BOM + CRLF)") # ------------------------------------------------------ C. configs/serve.json NOTE = ("端口只在这里改 (网关路由表 scripts/guanlan_gateway.py 里的上游端口须同步; " "v0.1 仍是硬编码, 见说明书 §7)") NOTE_RAW = (r"原始 SCADA/技术资料目录 (相对本目录或绝对路径, 如 D:\guanlan\data\raw); " r"启动器导出为 WINDSCADA_RUDONG_SRC; 原始件不随包分发") def fix_serve_json() -> None: raw = read_bytes(SERVE_JSON) was_bom = has_bom(raw) try: cfg = json.loads(raw.decode("utf-8-sig")) except Exception as ex: fail(f"serve.json 内容已经是坏 JSON ({ex}); 从 configs\\serve.json{BAK} 或重跑 install 恢复") if not isinstance(cfg, dict): fail("serve.json 不是对象") if not was_bom and cfg.get("_note") == NOTE and cfg.get("_note_raw") == NOTE_RAW: print(" [=] serve.json 已修过 (无 BOM 且注释正常), 跳过") return backup(SERVE_JSON) cfg["_note"] = NOTE cfg["_note_raw"] = NOTE_RAW SERVE_JSON.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") report.append("serve.json: 去掉 BOM, 还原 _note/_note_raw 中文注释") print(f" [OK] serve.json 已修 (原 BOM={was_bom}, python={cfg.get('python')})") def verify() -> None: print("\n== 校验") # 1) serve.json: 无 BOM, 且裸 utf-8 也能解析 (即 guanlan.py 老写法也不会再炸) b = SERVE_JSON.read_bytes() print(f" serve.json BOM={has_bom(b)} bytes={len(b)}") if has_bom(b): fail("serve.json 仍带 BOM") cfg = json.loads(b.decode("utf-8")) for k in ("_note", "_note_raw"): if "\ufffd" in cfg[k] or "?" in cfg[k]: fail(f"{k} 仍有乱码: {cfg[k]!r}") if not cfg.get("python", "").lower().endswith(r".venv\scripts\python.exe"): fail(f"python 路径不对: {cfg.get('python')!r}") print(f" python -> {cfg['python']}") print(f" _note : {cfg['_note']}") print(f" _note_raw : {cfg['_note_raw']}") # 2) guanlan.py: 编译 + 现场跑一遍 cfg() src = GUANLAN.read_bytes().decode("utf-8") compile(src, str(GUANLAN), "exec") ns: dict = {"__name__": "not_main", "__file__": str(GUANLAN)} exec(compile(src, str(GUANLAN), "exec"), ns) c = ns["cfg"]() print(f" guanlan.cfg() -> gateway={c['gateway']} python={c['python']} (无异常)") # 3) install.ps1: 字节约定 pb = INSTALL_PS1.read_bytes() txt = pb.decode("utf-8-sig") crlf, lf = txt.count("\r\n"), txt.count("\n") print(f" install.ps1 BOM={has_bom(pb)} CRLF={crlf} LF-only={lf - crlf}") if not has_bom(pb) or crlf != lf: fail("install.ps1 不再是 UTF-8 BOM + 全 CRLF") def main() -> int: print(f"目标: {ROOT}") for p in (GUANLAN, INSTALL_PS1, SERVE_JSON): if not p.exists(): fail(f"缺文件 {p}") print("\n== A guanlan.py"); fix_guanlan() print("\n== B install.ps1"); fix_install_ps1() print("\n== C configs/serve.json"); fix_serve_json() verify() print("\n完成:") for r in report: print(" - " + r) if not report: print(" (无需改动, 之前已修)") return 0 if __name__ == "__main__": sys.exit(main())