zhouyang.xie 743d4db77c 修非法转义序列 (未来 Python 会 SyntaxError) + 自检加"源码可编译"一行 4 weeks ago
..
README.md 743d4db77c 修非法转义序列 (未来 Python 会 SyntaxError) + 自检加"源码可编译"一行 4 weeks ago
fix_guanlan_bom.py c5cb06a962 观澜·如东样板 v2 v0.2.0 基线入库 (含 2026-09-11 现场修复) 1 month ago
fix_guanlan_pythonpath.py c5cb06a962 观澜·如东样板 v2 v0.2.0 基线入库 (含 2026-09-11 现场修复) 1 month ago
fix_guanlan_startbat.py c5cb06a962 观澜·如东样板 v2 v0.2.0 基线入库 (含 2026-09-11 现场修复) 1 month ago
restore_release_rudong.py 10323a3507 bug-2: 恢复 v0.2.0 漏发的 release/如东/ 交付件 (门户 #documents 坏链) 1 month ago
scan_portal_links.py 10323a3507 bug-2: 恢复 v0.2.0 漏发的 release/如东/ 交付件 (门户 #documents 坏链) 1 month ago
verify_bug2.py 10323a3507 bug-2: 恢复 v0.2.0 漏发的 release/如东/ 交付件 (门户 #documents 坏链) 1 month ago
verify_guanlan_simsys.py c5cb06a962 观澜·如东样板 v2 v0.2.0 基线入库 (含 2026-09-11 现场修复) 1 month ago

README.md

观澜·如东样板 v2 (v0.2.0) 启动报错修复记录 — 2026-09-11

本目录不属于原始交付包, 是现场修完之后留下的记录: 每个脚本都可重复运行 (幂等), 用来复现 "改了什么" 或在别的机器/另一份拷贝上重放同一批修复。

现场现象

powershell -ExecutionPolicy Bypass -File install.ps1     -> 装完最后一步崩:
    json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig)
start.bat                                                -> 同样崩; 随后
    [X] Server did not start (reason above). Not opening the browser

根因 (5 条, 前 3 条是同一个坑的不同后果)

# 位置 问题
1 install.ps1 第 4 步 Set-Content configs\serve.json -Encoding UTF8 Windows PowerShell 5.1 的 -Encoding UTF8 写出带 BOM 的 UTF-8; guanlan.py 用裸 utf-8 读 -> JSONDecodeError。启动器在解析配置时就死了, 所以 check/serve 全跑不起来。
2 同一行的 Get-Content configs\serve.json -Raw PS 5.1 对无 BOM 的 UTF-8 按 ANSI 代码页 (中文机 = GBK) 解码, 于是把文件里的中文注释读成乱码, 再 Set-Content 写回 -> _note / _note_raw 两个字段永久损坏 (部分字节被替换成 ?, 不可逆)。
3 机器全局 PYTHONPATH=D:\Program Files\Python\Lib\site-packages; ① pip 认为系统那套包 "已满足", 没把传递依赖装进 .venv -> venv 缺 urllib3/polars/pyyaml/jinja2/python-dotenv 等, 换个没设 PYTHONPATH 的 shell 直接 ModuleNotFoundError; ② PYTHONPATH 排在 sys.path 前面, 把 pin 住的版本顶掉 (实测 requests 2.33.0 顶掉 2.34.2)。
4 wheels\win_amd64\ 缺 colorama 轮子 (tqdm 在 Windows 上的依赖)。之前被根因 3 掩盖: pip 看到系统已有 colorama 就不下载, 于是离线轮子集不全; 一旦 PYTHONPATH 摘干净, pip install --no-index 直接 ERROR: No matching distribution found for colorama。
5 release/sim_sys_server.py 第 36 行 from scrub_rules import scrub, residual scrub_rules.py 根本没随包发出 (全盘搜不到, zip 里也没有) -> /sim/sys/ 仿真·四系统合页 502, logs/sim_sys.log 里是 ModuleNotFoundError; 网关因此 degraded, guanlan.py serve 退出码 1。

改了什么

文件 改动
guanlan.py 新增 jload() = utf-8-sig 读 JSON (兼容有/无 BOM), 5 处配置读取全部改走它; 配置坏了只打印提示并退回内置默认端口, 不再抛栈。新增 _hermetic(): 启动时把 PYTHONPATH 从 sys.path 与环境里摘掉 (子进程继承干净环境)。
install.ps1 第 4 步不再用 PS 的 cmdlet 碰配置, 改由 .venv 里的 Python 读写 (读 utf-8-sig / 写无 BOM 的 UTF-8), 并加注释说明为什么不能改回去; 顶部加 Remove-Item Env:PYTHONPATH, 让 pip 老老实实把包装进 .venv。文件仍保持 UTF-8 带 BOM + CRLF (PS 5.1 解码中文的前提)。
install.sh 同样加 unset PYTHONPATH (保持 LF/无 BOM)。
configs/serve.json 去掉 BOM; 还原 _note / _note_raw 两段中文注释 (依据残留可逆部分 + 说明书 §7 的措辞)。
wheels\win_amd64\colorama-0.4.6-py2.py3-none-any.whl 补上缺失的轮子, 离线安装才完整。
release/scrub_rules.py 恢复缺失文件: 不新写任何脱敏规则, 只把包内唯一那份规则表 src/windscada/deid_public.py 转出 scrub / residual(= audit) / scrub_or_die。若拿到交付方原版, 直接覆盖。
start.bat guanlan.py serve 的退出码 1 有两种含义 (网关没起来 / 网关起来了但有模块降级)。现在先探一次 /healthz 区分: 真没起来才报 [X] 并停下; 起来了但有降级则打 [!] 说明, 浏览器照常打开。

备份与回滚

原文件都留了副本, 直接改名覆盖即可回到改前状态:

guanlan.py.bak-bomfix      install.ps1.bak-bomfix     configs\serve.json.bak-bomfix
guanlan.py.bak-pyfix       install.ps1.bak-pyfix      install.sh.bak-pyfix
start.bat.bak-healthz

重放 / 验证

.venv\Scripts\python.exe fix_guanlan_bom.py          # 1 2 (BOM + 乱码)
.venv\Scripts\python.exe fix_guanlan_pythonpath.py   # 3
.venv\Scripts\python.exe fix_guanlan_startbat.py     # start.bat 判定
.venv\Scripts\python.exe verify_guanlan_simsys.py    # 5 个仿真页 200 + 脱敏回扫干净

三个 fix_* 都是幂等的 (已改过会打印 "已修过")。wheels\colorama*.whl 与 release\scrub_rules.py 属于新增文件, 没有对应的 fix_* 脚本。

bug-2 · #documents 页「如东液压系统治理清单 · 网页版」报 {"err":"not found"}

现象: 门户 http://127.0.0.1:28084/#documents 里那个 iframe 显示 {"err": "not found", "path": "/如东/如东治理清单_交付_20260901/02_治理清单/如东_液压系统治理清单_v1.2_2026-09-01.html"}。

根因: v0.2.0 发行包里没有 release/如东/ —— 0.2.0 的 zip 里 release/ 下只有 viewer/, 而门户那个 iframe 指的就是 release/如东/.../_液压系统治理清单_v1.2_2026-09-01.html; 网关 guanlan_gateway.py::_static() 在 release/ 里找不到文件就回那段 404 JSON (见 _static() 里 return self._json(dict(err="not found", path=path), 404))。 顺带核过: 门户里指向 release/如东/ 的引用只有这一条, 所以不是路径写错, 是交付件没随 v0.2.0 发出来。

修复: 从本机的 v0.1.0 全量包 F:\temp\guanlan-rudong-v2_0.1.0_all.zip(这份交付件在里面是齐的) 把整个 release/如东/ 恢复到 v0.2.0 安装目录 —— 75 个文件 / 51.0 MB (如东治理清单_交付_20260901/ 33 件、如东取数单_2026-08-21/ 18 件、观澜离线版.app/ 10 件、根下散件 14 件)。 release/如东/ 按 .gitignore 的既有约定不入库(客户交付物只在受保护工作区), 所以 git 里看不到这次恢复。

验收: iframe URL 走网关取 → [200] text/html, 103760 字节, 标题 如东液压系统治理清单, 正文 0 个外部相对引用(自包含); 同页另外两个引用 (127.0.0.1:18020 CMS、127.0.0.1:18033/v2) 均 200; 全门户扫一遍含「如东 / release」的静态引用 → 坏链 0。

脚本: restore_release_rudong.py (恢复) · verify_bug2.py (验收) · scan_portal_links.py (同类坏链全量扫描)

路径跨平台化(①+②, 2026-09-11)

用户令: 后续部署到其它电脑; 兼容 Windows / Linux; 系统内目录路径必须使用相对路径。

三条口径(写进 docs\数据目录结构与落位约定_v0.2.md §7, 并由门禁自动检查): ① 只写相对路径(相对安装根), 不写机器绝对路径;② 相对基准是安装根、不是 cwd, 一律经 src\paths.py 解析; ③ 写进产物/清单/页面的用 POSIX 相对形式(P.rel()), 显示给人看时才用本机分隔符(P.disp_dir())。

新增

  • src\paths.py —— 唯一路径真源(ROOT/RAW_ROOT + store()/ont()/cms()/m5()/sop()/guanlan()/pitch()/tcm_replay()
    • RELEASE/PORTAL/VIEWER/SIM_DIR/LOGS/RUN + rel()/disp()/disp_dir()/resolve()/venv_python())。 ROOT = env WINDSCADA_ROOT → 否则按本文件位置回溯 ⇒ 整个安装目录可整拷到别的电脑/别的盘。
  • scripts\check_portability.py —— 静态门禁: 机器绝对路径 / cwd 相对字面量 / os.sep 拼库存字符串 = ERROR; 例外必须具名登记理由。当前通过。
  • scripts\page_fingerprint.py —— 10 个端点的回归指纹(status + 归一化 sha256, 自动剔除 checked/ms/repo_head 这类设计上会变的字段、并把安装根前缀归一成 <ROOT>)。换机验收也用它: 基准机 --save, 新机 --diff。

清理的机器绝对路径(换机必失配, 且是静默故障): windcms MAIN_ROOT 默认 mac 检出、windcms/ontology 写死的 mac venv 解释器、kb_ingest 的 mac 技术资料路径、ingest_ops_2025 的 /Volumes/BIG/…、place_raw_data 的 F:\temp\…、界面占位符里的 /Users/…。清理的 cwd 相对路径: 20+ 处 Path('outputs/rudong/…')(ontology 14 个 模块、windscada 4 个、windcms、serve/gateway、sop 8 个)全部归到 paths; sop 里写死的 rudong 改为跟场走。

配置/安装器: configs\serve.json 的 python 改为相对(.venv/Scripts/python.exe); 启动器 py() 支持 "相对→按安装根解析 / 绝对 / 留空自动探测 venv"; install.ps1|sh 写配置时写相对路径。

验证

  • 门禁: 运行期 0 处机器路径、0 处 cwd 相对, os.sep 只留在显示函数里 ✔
  • 回归指纹: 改造前后 10/10 端点逐字节一致 ⇒ 只换了路径解析, 页面与取数面零变化 ✔
  • 换 cwd 实测: 从 C:\ 且不设 PYTHONPATH 跑 scan_stations.py / farm() → 场站辨识与 store/src_10min 均正确 ✔
  • 过程中我自己引入并修掉两处: 服务端 from src import paths 插在 sys.path 引导之前(导致 detail 起不来), 以及 subsys 子包导入深度写成两点(应为三点)。这两处都是靠"重启+指纹比对"逮到的 —— 印证了那把尺子的价值。

场站扫描辨识 + 两个验收场景 (2026-09-11)

用户令: 离线数据统一放 data\raw\, 下一级目录约定为「场站名称」, 观澜系统扫描辨识。

  • src/windscada/config.py: 新增 scan_stations() / station_scan() / station_report(); _expand() 按扫描结果派生 src_*(外场配置若显式给了 src_* 则不覆盖)。辨识规则: ①raw_station 全等 → ②与 src_farm_names 互为子串 → ③只有一个场站目录(单站部署)→ ④都不中 = 不猜, 报"未识别"。
  • scripts/scan_stations.py: 打印扫到的场站、辨识依据(how)、五个约定子目录的件数与存量。
  • 维护页新增一行「场站数据目录 (扫描辨识)」: 扫到 → 覆盖=如东/存在=True; 扫不到 → 存在=False/覆盖=—。
  • scripts/windscada_serve.py: adhoc_query 在原始件缺失时不再抛给 HTTP 处理器(前端只看到 500), 改为 HTTP 200 + 结构化无数据(err=no_source + "本机应在 …\scada_10min\WTG01.csv" 的提示)。

场景① 清空 data\raw + 挪开产物 → 扫描"0 个场站目录 / how=none"; 维护页五项全部 条数=0 / 存在=False; 实时接口从 500 变 200+no_source。 ✅

场景② 按约定放回 → 扫描"1 个场站目录 如东(592 件), how=raw_station", 子目录 38/16/134/404; 摄入后 报警 0→39211、工单 0→5876、油样 0→404、loss_monthly 3729(powercurve 31s + availability 88s); 维护页五项覆盖区间全部回来, 实时接口 months=7。 ✅

产物等价(从 raw 重算 vs 随包基线): loss_monthly 3729/3729、powercurve_dev 38/38、 powercurve_bins 912/912、alarms 39211/39211 逐值完全一致; workorders 5876 行仅 1 条记录 纳秒尾差(同一时刻 .0000015 vs .0000010); oil_samples_index 506→404 —— 清空重建会丢 102 行华标 2026-07 批(源件 BG-2026-07-YP013 …pdf 不在现场数据包里, 且一份覆盖多台), 已恢复完整件 并写入文档 §4 作为缺口。

文档: docs\数据目录结构与落位约定_v0.2.md(目录结构、落位约定、消费方式、可重建性边界、两个场景实测)。

数据链补齐 · 让系统真正吃 data/raw/如东 (2026-09-11)

问题: 说明书 §11 写着「不含从原始数据重生成产物的链 (P0)」, 而维护页四条「摄入命令」指向的文件 在包里一个都不存在; 于是现场把新台账放进 data/raw/如东/… 也不会更新, 工单台账一直停在 2024-11-21。

新增 4 个脚本 (都在 scripts/; 维护页四条命令现在全部「命令可用=True」):

脚本 源 → 产物 关键实现
windscada_alarms_ingest.py 故障报警/*.xls → alarms.parquet .xls 实为 SpreadsheetML(XML); 累计快照件自动跳过 (2025年全年 = Q1..Q4 精确并集 20155 行)
windscada_workorder_ingest.py 风机故障记录/** → workorders.parquet 按场名过滤 (集团表里混着民勤/来福/宝力格等十来个场); 归并跨表重复 1085 行; 日期按 src/sop/ledger_dates.py 同一纪律解析, 解析不了的整行保留
windscada_watch_channels_build.py 油样报告/**/*.pdf → oil_samples_index.parquet 文件名解析 (日期/台号/部件/sample_id); 合并语义: 认不出的老行原样保留
rebuild_from_raw.py 总入口 --verify 等价验收; --scada 再跑包内 10 个 SCADA 侧构建器

等价验收 (python scripts/rebuild_from_raw.py --verify, 对 _pre_rebuild_20260911/ 随包基线):

报警事件       39211 行 / 39211 行   逐值完全一致            ✔
油液化验         506 行 /   506 行   逐值完全一致            ✔
检修工单台账    1574 种内容复现 1566 种; 8 种差异逐条定位:
   · 7 种 = 复位运行时间: 随包 NA、本链有值 — 旧链没认源列名「复位时间时间」这个错别字
   · 1 种 = 随包把 Excel 序列号当纳秒解析成 1970-01-01, 本链解析为 2023-05-13
   · 随包 78 行副本重复已归并; 另多出 69 行 = 旧链丢弃的日期不可解析行

效果 (/detail/v2#tab=system 数据层, 已重启服务实测):

行 改前 改后
检修工单台账 2020-01-03 ~ 2024-11-21, 1652 行 2020-01-03 ~ 2026-07-08, 5876 行
四条「摄入命令」命令可用 False True

依赖: requirements.txt 加 xlrd==2.0.2 (2023/2024 年总表是真 BIFF .xls, openpyxl 读不了), 轮子已放进 wheels/win_amd64/ (wheels/ 按 .gitignore 不入库, 随包分发)。

SCADA 侧: python scripts/rebuild_from_raw.py --scada 把 10 个构建器按依赖顺序跑一遍 (powercurve → loss_monthly → curves/control/stop_events/温度/偏航/液压/热链/系统辅助)。 抽验过等价性 (WTG01/WTG02 的 temp_bins 与随包件 1296/1296 行逐值相同、med 差 0.0), 但未整体重跑 —— 要跑建议先备份 outputs/rudong/windscada/ 再逐项比对覆盖区间。

门户拆包 + 闸门退出码 (2026-09-11 收尾)

起因: 有人问 "release/portal.html 20 MB 里到底是什么、能不能不进 git"。查下来 99.3% 是内嵌的 交付文档正文 (28 个 <template>: 治理清单分册、单机/整机报告、仿真台面板), 只有 138 KB 是门户自己的壳。 原先整份文件入库, 每轮重算产物都往 git 塞 20 MB。

做法 (用户选 ②): 拆成"受管外壳 + 不入库内嵌件 + 可验证装配":

入库 说明
release\portal_src\shell.html ✅ 138,544 B, 内嵌件处留 <!--@TEMPLATE:id-->, 资料索引处留 <!--@GOVERNANCE_SOURCES-->
release\portal_src\manifest.json ✅ 各件 sha256 + 期望门户 sha256 + 行尾约定
release\portal_src\README.md ✅ 重建说明与两个坑
release\portal_src\templates\ ❌ 产物 28 件 20.04 MB (交付件正文)
release\portal_src\governance_sources.json ❌ 产物 1,512 B 脱敏资料索引
release\portal.html ❌ 产物 装配结果, 服务/网关照读

新增 scripts\portal_build.py: --extract 拆 / 默认装配 / --verify 逐字节比对 / --check 漂移检查。 实测 --verify 两行同为 sha256 9b6aabeb6ca18d15, 20,226,052 B —— 拆→装回到同一个文件; --check 全件一致。

两个坑:

  1. 行尾: 门户通体 LF (0 处 CRLF)。Python 文本模式在 Windows 上写文件会把 \n 变 \r\n —— 20 MB 整体改写, /api/version 的 portal_sha256 与页面指纹全变。两个就地注入器 (guanlan_portal_fix_anchors.py / guanlan_portal_inject_claims.py) 补 newline="", portal_build.py 全程字节级读写并在装配后自检 CRLF。第一次拆出来的 portal_src/ 就是这么废的。
  2. 闸门退出码: 同一类坑让"校验通过"被报成失败 —— 中文控制台代码页 936, print('… 一致 ✔') 抛 UnicodeEncodeError 使进程以 1 退出。portal_build --verify 与 page_fingerprint --diff 各踩一次 (后者是回归闸门: 10 个端点全部回到基线, 却报失败)。新增 src\console.py 的 soft() 把编码错误降级为 ?, 接入 5 个"以退出码讲话"的脚本; 复跑四个闸门均正确返回 0。

回归: 拆包后 10 个端点全部与基线一致 (portal.home 64b4b81158d09d9c, detail.v2 a0abff29c000f570 …)。 其中一度看到 /cms/ 掉线 —— 原因是最后那次 guanlan.py serve 发生在产物被挪走时, CMS 启动即因 src\windcms\data.py "No objects to concatenate" 退出 (本该如此); 产物还原后重启即恢复 (414,139 B, 与基线一致)。教训: 产物开关与重启顺序有关, 挪/还产物后必须重启一遍。

从零重算 (2026-09-11 下午, 用户令: 不要随包产物, 基于 data/raw 重算)

做法: 产物全部挪走 (products_state.py --off), 只留 data/raw 重算, 一个随包产物都不吃。

.venv\Scripts\python.exe scripts\products_state.py --off          :: 产物挪走
.venv\Scripts\python.exe scripts\rebuild_from_raw.py              :: 三门台账
.venv\Scripts\python.exe scripts\rebuild_from_raw.py --scada      :: SCADA 侧 10 个构建器 (逐台读 14.7 GB CSV)
.venv\Scripts\python.exe scripts\rebuild_from_raw.py --verify     :: 等价验收 (需临时放回随包基线)

结果: 报警 0→39211 行 · 工单 0→5876 行 · 油样 0→404 行; SCADA 侧 10 个构建器全跑通, L0 仓 16 件 1.78 MB (随包 118 件 —— 差额就是"包内没有生成端"那些件, 见 docs §4); 本体层 objects.json 2340 个对象 + 检索索引 + turbine_params.parquet 1706 条。 等价验收: alarms 39211/39211 逐值完全一致; workorders 8 行差异全部可归类(旧链时间解析缺陷)

  • 69 行本链新增覆盖; 唯一人工项是油样 506→404。

用户追问"缺的数据从 F:\temp\如东风场数据 抽" → place_raw_data.py --scope mech 落 355 件 15.5 GB: data\raw\西门子4.0技术资料\(317 件, 本体层的源件) 与 <场站>\scada_1min\(38 件 12.8 GB)。 抽完本体层就从"包内没有源件⛔"变成可重算。仍然缺的: 油样那 102 行的源件 (BG-2026-07-YP013 … .pdf) 不在包里(整个包只有 2025年油样 一批) → 那 102 行找不回来。

这一轮逮到的三个真 bug (都不是参数问题, 是静默失效)

# 症状 根因 影响面
1 python -m src.ontology.kb_ingest 崩: UnicodeEncodeError: 'gbk' codec can't encode '\u200b' src/ontology/store.py 的 read_text()/write_text() 没写 encoding= → Python 文本 I/O 用系统 locale 编码, 中文 Windows = cp936。读会乱码/解码失败, 写遇到 GBK 编不出的字符就崩 本体层在中文 Windows 上根本不可用(开发机是 UTF-8 locale 所以从没暴露)。已修 src/ontology/ 全层 10 处; 存量 12 处已进 check_portability.py 的新 WARN 项 6
2 place_raw_data.py 一跑就 NameError: name 'os' is not defined 模块级 DEFAULT_SRC 用了 os.environ 却从没 import os 该脚本自 d90da05(路径跨平台化) 起一直跑不通; A3 那次落位不是它干的 → 说明"写成表的落位映射"从未被真正执行过
3 A2 规则 …/大部件维修记录.20240619143912557.xlsx 从没落过盘 包内前缀恰好等于条目本身时, 去前缀后 rest 为空, 旧代码把它当"目录条目"continue 掉 单文件形态的规则全都静默丢件: 受害两例 = 上面那个 25.8 MB 工单源 + 本轮新加的对译表。修后按前缀的文件名落位

教训: 这三个都是"跑一次就暴露、但没人跑过"的类型。从零重算本身就是最好的回归测试 —— 它不依赖任何随包产物, 因而能照出所有"靠旧产物遮掩"的失效。

顺带发现: 落位后工单源件 79→80 张, 但总行数不变(5876)——新件的行与既有来源内容重复, 按"累计快照/副本必须归并"的纪律合并, 脚本把这类情况逐条打了出来(不是静默丢)。

要补什么、找谁补: 那次重算照出来的缺口整理成了独立清单 —— docs\重算缺口与补件清单_v0.1.md(按"源件缺失(现场能补)"与"生成端缺失(研发补脚本)"两类, 每项带证据路径、影响面、补齐判据与验证命令)。

另一件同类收尾: 把剩下的 18 处"文本读写缺 encoding="也清了(见下), 门禁 WARN 项 6 归零。

文本 I/O 的 encoding (2026-09-11 收尾)

上面 bug ① 只是冰山一角: 全库搜下来共 28 处文本 I/O 没写 encoding=(本体层 10 + 其余 18)。 已全部补上 encoding='utf-8', 其中写侧把 json.dump(x, open(p,'w')) 这类匿名句柄改成 with 块(正确关文件), src\windcms\plugins.py 里给子进程当 stdout 的那个句柄不 with-close(父进程持引用, 免得提前关掉)。

check_portability.py 的 WARN 项 6 同时扩了扫描面: 除 read_text/write_text 外, 新增 open(..., 'w') 与 open(p)(无 mode = 文本读)两种形态(二进制模式不算), 并与 ERROR 扫描共用 ALLOW 例外表 (门禁自己的规则/文档里必须写出这些形态, 整份豁免 —— 否则会误报 10 处)。实测命中 0。

冒烟: 11 个文件 py_compile 通过 · windcms 五个模块 + fusion/taxonomy/scenario_29 导入成功 · 重启服务 5/7 ok · 页面字节与改前逐项一致(/ 20225828 · /detail/ 229643 · /sim/ 50998 · /sim/sys/ 123455 · /viewer/ 4986) · 维护页数据层四个数字不变(报警 39211 · 工单 5876 · 油样 404 · 月表 3729)。

探活页提速: /healthz 6.5 s → 冷探 1.5 s / 命中缓存 ~10 ms (2026-09-11)

收尾测页面时发现 /healthz 要 6.5 秒才回来(而 / 那个 20 MB 门户只要 0.04 s)。拆开看是三笔叠加:

项 原实现 代价
6 个上游探活 串行 for 循环, 单探针默认 3 s 4.4 s(其中 CMS / Ollama 两个掉线模块各卡满超时)
Ollama 模型查询 探活之后再串一次 +2 s
门户 sha256 每次 PORTAL.read_bytes() 重读 20 MB 再哈希 0.1–数十秒(磁盘忙时)

改法(返回结构一字未改): ① 全部探针并发(ThreadPoolExecutor, 总耗时 = 最慢那个); ② 单探针 1.5 s; ③ 门户 sha 按 (mtime_ns, size) 缓存(值不变就不重读); ④ 整个结果缓存 5 s —— 连续调用近乎零成本。

实测: 冷探 6.5 s → 1.53 s, 命中缓存 15 ms; 重启后首探 8.8 ms(guanlan.py serve 自己探过一遍, 缓存已热)。结构逐键比对无差异(顶层键/模块集合/每模块键/local_ai 键/status/门户 file_sha256 全同), /api/version 的 portal_sha256 与磁盘门户逐字节一致(9b6aabeb…)。

★一台机器上的实测结论(写进代码注释了): 这台机器连任何已关闭的 127.0.0.1 端口都不回 RST, 而是把 SYN 丢掉等超时 —— 对照用的随机闭端口 49996/49997 同样 1.5 s 超时, 不是我们端口或进程的问题, 是系统防火墙/安全软件行为。所以"有模块掉线"时冷探下限就是 PROBE_TIMEOUT; 想更快只能靠缓存, 别为此把超时压到健康模块也可能被误判的程度(实测最慢的健康探针 sim_sys 345 ms, 其余 <25 ms, 1.5 s 留了 4 倍余量)。

非法转义序列 + 自检加一行 (2026-09-11)

全量编译 src/ + scripts/(168 个文件)把 SyntaxWarning 当错误报出来, 逮到最后一处潜伏炸点:

  • src\windcms\serve.py:12,68 —— JS 正则写在普通字符串里: /\*\*([^*]+)\*\*/g 与 /\d+/g。 Python 3.12 只警告, 未来版本会变成 SyntaxError; 而且开发机上永远看不见, 换解释器才炸。
  • 不能简单改成 raw 字符串: 同一串里混着 \\n(有意转义成 JS 的 \n) 与 \d(想写成 JS 的 \d), 改 raw 会把 \\n 变成"两个反斜杠 + n", 语义就变了。正确改法是把非法转义补成合法: \* → \\*。
  • 证明没改语义: 改前/改后各把该文件 237 个字符串常量的求值内容算指纹 —— 都是 70bd628f72f72101, JS 文本一字未变; 改后全库非法转义 0 处, -W error::SyntaxWarning 编译通过。

guanlan.py check 顺带加了一行自检: 源码可编译 (168 个文件, 无非法转义/语法错) —— 换机器/换解释器 之前先跑它, 这类问题不必等运行到那一页才发现。

自检现状(原始重算后): 3 项 FAIL —— outputs/rudong/sop/findings.json 与 outputs/rudong/guanlan/facts_contract_v0.json 不在(缺口清单 A3), 本机 Ollama 无模型(环境项); L0/L1 产物、本体对象库、门户、仿真、viewer、原始件目录都是 OK。

仍然存在 (不是代码问题)

  • /local-ai/ 仍是 DOWN: 本机 Ollama 没运行, 且 %USERPROFILE%\.ollama 下没有任何模型 (manifests 都没有), 所以问答与本地审核页不可用。其余 6 个页面不受影响。 启用: 启动 Ollama, 按 configs\models.json 的 pull_commands 拉模型 (需联网/大流量), 或把有网机器的 %USERPROFILE%\.ollama\models 整个目录拷过来。
  • release\如东 (治理清单交付件) 本来就是可选项, 不影响页面。