# 观澜 · 如东样板 v2 · 系统设计说明 > 版本 v0.2.0 · 2026-09-16 · 适用 `<安装目录>`(安装目录) > 本文是**系统设计正本**: ① 产物全景(类型/功用/输出路径/生成端/消费端);② 路径与进程的统一约定; > ③ 输入 ↔ 产物呼应关系;④ 重算的两种运行状态与"产物即时进页面"的实现;⑤ 启动与门户。 > 与本文配套、可复跑的机器校验: `python scripts/inventory_products.py`(清点 + 呼应校验,`--check` 出退出码)。 --- ## 1. 系统是什么(一页) ``` 现场数据 data/raw/<场站>/ ──摄入──▶ outputs/<场>/(产物仓) ──现读──▶ 页面/接口 ▲ 7 类约定目录 ▲ 9 个产物仓 ▲ │ scada_10min / scada_1min / │ windscada L0 标准仓 │ 网关 :28084(门户/运维控制台) │ 故障报警 / 风机故障记录 / │ ontology 本体对象库 │ detail :18033(分析工作台) │ 油样报告 / windcms / m5_cms_tcm │ windcms CMS 振动 │ cms :18020(振动诊断) └─ 西门子4.0技术资料(机理层,不在场站目录下)│ m5_cms_tcm 振动线出件/窗 │ sim :18791 · sim_sys :18792 · viewer :64292 │ tcm_compatible_replay │ │ sop / guanlan / pitch / paradigm_r1 ``` 四条设计铁律(贯穿全部代码,改代码时先读这四条): 1. **路径唯一真源** `src/paths.py` —— 代码/配置里只写 ROOT 相对路径,运行期由助手解析成绝对路径(基准是安装根,**不是 cwd**)。 2. **产物只落在 `outputs/<场>/`**;原始件只读 `data/raw/<场站>/`;`release/` 是交付静态层。三者不互相写。 3. **判断在代码、模型只转述**;每个产物都要能说清"谁生成、谁消费、来源是 raw 重算还是随包补齐"(`_provenance.json`)。 4. **窗口/进程统一口径** `src/proc.py` —— 子进程一律 `CREATE_NO_WINDOW`,日志落 `logs/`,不弹命令窗口。 --- ## 2. 产物全景(自动生成,勿手改) > 由 `python scripts/inventory_products.py --write-doc` 生成;数据源是**盘上的实际文件** + > `outputs/<场>/_provenance.json`(逐件来源台账)+ `outputs/<场>/_derived_manifest.json`(构建脚本自登记)。 *自动生成于 2026-09-16 16:26;数据源: `outputs/rudong/_provenance.json` + `_derived_manifest.json` + 实际文件* | 产物仓 | 输出路径 (相对安装根) | 功用 | 生成端 | 消费端 | 件数 | 大小 | 来源(raw 重算/随包) | |---|---|---|---|---|---|---|---| | `windscada` | `outputs/rudong/windscada/` | L0 标准仓: SCADA/台账/派生分析的全部 parquet (页面主取数处) | rebuild_from_raw.py (三门台账) + --scada (10 个构建器) + windscada_monthly_build.py | scripts/windscada_serve.py 各视图 · src/windscada/taxonomy · subsys/fusion | 115 | 4.5 MB | 17 / 98 | | `ontology` | `outputs/rudong/ontology/` | 本体对象库: 码表/手册/工单展开/失效树 + 检索索引 + 实机参数 | python -m src.ontology.kb_ingest → populate → chain_ingest → trend_ingest → retrieval.build | 脚本 windscada_serve.py 本体页/问答 · scripts/guanlan_facts_contract.py | 25 | 82.9 MB | 3 / 22 | | `windcms` | `outputs/rudong/windcms/` | CMS 振动诊断产物: 状态评估报告/逐台页/工作台页/知识库 + 厂家报告转录 | scripts/windcms.py report/kb · scripts/vib_reports_build.py | 自服务 :18020 (每请求现读) · taxonomy.system_matrix (转录设备状态) | 56 | 30.1 MB | 2 / 54 | | `m5_cms_tcm` | `outputs/rudong/m5_cms_tcm/` | 振动线出件与窗级分析: handoff 接口 + 窗索引/谱库 + TCM 兼容件 | scripts/vib_raw_build.py (窗索引/谱) · 振动线出件 (handoff, 随包快照) | src/windscada/subsys/fusion.py · src/windcms/data.py · scripts/windcms.py report | 1808 | 3329.7 MB | 1718 / 90 | | `tcm_compatible_replay` | `outputs/rudong/tcm_compatible_replay/` | TCM 兼容链回放资产 (模型表/掩码阈值/裁决记录) | 随包快照 (无生成端) | src/windcms/report*.py · config.mask_thresholds | 59 | 32.7 MB | 0 / 59 | | `sop` | `outputs/rudong/sop/` | SOP 中间件/评审/台账与事实契约底稿 | 随包快照 (无生成端) | scripts/guanlan_facts_contract.py · 门户结论段 | 210 | 15.8 MB | 0 / 210 | | `guanlan` | `outputs/rudong/guanlan/` | 事实契约与对外派生 (可上云面孔) | scripts/guanlan_facts_contract.py | 门户 #findings · /api/facts | 5 | 0.5 MB | 0 / 5 | | `pitch` | `outputs/rudong/pitch/` | 变桨侧派生件 (零位/日粒度) | 随包快照 + rebuild_from_raw --scada | 脚本 windscada_serve.py 变桨面 | 2 | 0.3 MB | 0 / 2 | | `paradigm_r1` | `outputs/rudong/paradigm_r1/` | 范式实验件 (E3/E5/E8 底稿, 事实契约输入) | 随包快照 (无生成端) | scripts/guanlan_facts_contract.py | 29 | 0.5 MB | 0 / 29 | 合计 2309 件 / 3496.9 MB;其中 raw 重算 1740 件、随包补齐 571 件。 ### 2.1 逐件来源口径(`_provenance.json`) | 来源 | 含义 | 判据 | |---|---|---| | `raw-derived` | 由 `data/raw` 重算出来的 | 该件在 `RAW_DERIVED` 表里,或由**构建脚本自登记**(`src/derived_manifest.py`) | | `shipped` | 包内没有生成端 / 规则未复现,用随包件补齐 | 既不在上表、也无自登记,且件在随包快照里存在 | *自登记(2026-09-16 新增)*: 振动侧的产物名随"窗名/分片序号"变化,写不进精确路径表,而按名字通配会误伤 同名旧件(`报告_CMS振动状态评估报告_*.md` 既有随包/自产的、也有厂家报告转录的)。改成**谁算的谁登记**: `vib_raw_build.py` / `vib_reports_build.py` 落盘后把相对路径写进 `_derived_manifest.json`, `products_restore_missing.py` 生成台账时据此记为 `raw-derived`。漏登记可用 `python scripts/vib_raw_build.py --register-only` 幂等补登记。 --- ## 3. 路径约定(唯一真源 `src/paths.py`) ### 3.1 全部路径助手(写新代码时只用这些) | 助手 | 返回 | 用途 | |---|---|---| | `P.ROOT` | `<安装目录>` | 一切解析的基准(`WINDSCADA_ROOT` 可覆盖,冻结构建时由启动器设) | | `P.RAW_ROOT` | `data/raw` | 现场原始件根(`WINDSCADA_RUDONG_SRC` / `serve.json.raw_dir` 可覆盖) | | `P.station_dir(name)` | `data/raw/<场站名称>` | 兜底约定位置(权威值来自 `config.raw_station_dir()` 的扫描辨识) | | `P.out_root(name)` | `outputs/<场>` | 产物仓根 | | `P.store(name)` | `outputs/<场>/windscada` | L0 标准仓(页面主取数处) | | `P.ont(name)` | `outputs/<场>/ontology` | 本体对象库 | | `P.objects_json(name)` | `…/ontology/objects.json` | 对象库文件 | | `P.cms(name)` | `outputs/<场>/windcms` | CMS 振动诊断产物 | | `P.m5(name)` | `outputs/<场>/m5_cms_tcm` | 振动线出件 / 窗索引 / 谱库 | | `P.tcm_replay(name)` | `outputs/<场>/tcm_compatible_replay` | TCM 兼容链回放资产 | | `P.sop(name)` | `outputs/<场>/sop` | SOP 中间件与评审落盘 | | `P.guanlan(name)` | `outputs/<场>/guanlan` | 事实契约与对外派生 | | `P.pitch(name)` | `outputs/<场>/pitch` | 变桨侧派生件 | | `P.paradigm(name)` | `outputs/<场>/paradigm_r1` | 范式实验件(E3/E5/E8 底稿)**2026-09-16 新增** | | `P.report_dir(name)` | `outputs/<场>/report` | 报告交付件(交接单/现场单)**2026-09-16 新增** | | `P.cloud(name)` | `outputs/<场>/guanlan/cloud` | 可上云面孔(脱敏后的契约/派生/页面)**2026-09-16 新增** | | `P.contract(name)` | `reference/<场>/windscada_contract.yaml` | 机型判据契约(属 reference 侧,不在 outputs) | | `P.rel(p)` / `P.disp(p)` / `P.disp_dir(p)` | 相对 POSIX 串 / 显示串 / 目录显示串 | 写进产物用 `rel()`;给人看(页面「位置」列)用 `disp()` | | `P.resolve(p)` | 绝对路径 | 把"可能是相对"的值按 **ROOT**(不是 cwd)解析 | | `P.venv_python()` / `P.python_exe()` | 解释器路径 | 跨平台探测 `.venv/Scripts/python.exe` 或 `.venv/bin/python` | ### 3.2 2026-09-16 统一掉的路径问题(源码已改) | # | 位置 | 原样 | 改成 | 为什么要改 | |---|---|---|---|---| | 1 | `scripts/products_restore_missing.py` | `P.STORE if hasattr(P,'STORE') else ROOT/'outputs'/'rudong'` | `P.out_root()` | `P.STORE` **不存在** ⇒ 恒落到写死的 `rudong`;多场部署会把 rudong 的随包件补进别的场(静默串场) | | 2 | `scripts/ingest_ops_2025.py` | `pathlib.Path('outputs/rudong/ontology/objects.json')` + 裸 `write_text` | `_P.objects_json()` + `Store(…).save()` | cwd 相对 + 写死场名 + **绕过 store 的全库校闸与原子写**;且该脚本缺 `import os` 一直 NameError,属"上膛但没响"的凶器 | | 3 | `scripts/ontology_p2_verify.py` | `ROOT/'outputs/rudong/ontology/objects.json'`、`…/guanlan/facts_contract_v0.json` | `P.objects_json()`、`P.guanlan()` | 写死场名 | | 4 | `scripts/guanlan_facts_contract.py` | 4 条 `ROOT/"outputs/rudong/…"` | `P.guanlan()/P.sop()/P.paradigm()` | 同上(事实契约的输入/输出全在这里) | | 5 | `src/ontology/maintenance.py` | 自写 `ROOT=parents[2]`、`_RAW_STR`、`disp()` | `P.ROOT` / `P.RAW_ROOT` / `P.disp` | 同一文件里三处"影子真源"(显示规则两个实现) | | 6 | `src/sop/wrapup.py` | `…/cleaned/turbine.parquet`(硬编码) | 读 `clean_gate.json` 的 `section` → 否则取目录内唯一 parquet → 都取不到则**响亮打印** | 写侧落的是 `cleaned/
.parquet`(section 可变)⇒ 读侧硬编码时静默 `None`,柱2 悄悄降级 INSUFFICIENT | | 7 | `src/windscada/subsys/pitch.py` | `P.store()/'alarms.parquet'` | `cfg['store']`(`registry(cfg)` 透传) | `P.store()` 跟环境变量,`cfg['store']` 跟显式选定的场 ⇒ 多场下跨场串数据且不报错 | | 8 | `scripts/check_transferable.py` | 门户期望字节数写死 `20225828` | 运行时从主实例现量 | 门户外壳一改常量即过期,核验会打印永远不成立的"与主包不同" | | 9 | `src/windscada/taxonomy.py`、`subsys/fusion.py`、`scripts/windscada_serve.py` | 读侧硬绑 `P.cms()` | 新增 `windcms.config.cms_out()`,读侧与写侧同一解析口 | `WINDCMS_OUT` 只有写侧认 ⇒ 设了它就"**写到旁路、页面读生产**",表现为"重算完了页面还是旧数"且**不报错**(冻结构建自检正是这个组合) | | 10 | `src/windcms/pipeline.py::ingest` | 任何 `.zip/.rar/.7z` 都调 `rudong_tcm_ingest_raw.py`(**未随包**) | 先判包内容:含 `*_decode.json` → 按"已解码导出"走 `rudong_tcm_index/spectra`(支持 zip 当 root);否则报明确错误并给两条可行路径 | 把 CMS 导出 zip 直接指过来时原本必崩 `FileNotFoundError`,而其实不需要那个脚本 | ### 3.3 仍然存在的不统一(**如实列出,未擅自大改**) | 类别 | 具体 | 影响 | 处置建议 | |---|---|---|---| | 一次性/历史脚本自拼路径 | `scripts/guanlan_cloud_{face,page,qa}.py`、`guanlan_portal_inject_claims.py`、`guanlan_baseline_manifest.py`、`llm_known_answer.py` 里仍有 `outputs/rudong/…` 字面量 | 只影响这些**一次性派生/云端面孔**脚本;多场时需手改 | 换场前把这几处改走 `P.guanlan()/P.cloud()/P.store()`(`P.paradigm()/P.report_dir()/P.cloud()` 已备好) | | 历史随包留档 | `scripts/sim_hub/*`(含 `/Users/yuanying/...` mac 路径)、`outputs/rudong/sop/*.py` | **非运行期**(装配脚本留档) | 已在 `check_portability.py` 具名登记;随包只为"可重放",不参与运行 | | 多场机制未接通 | `src/windscada/config.py::available()` glob `configs/farms/*.json`,而磁盘上 11 个场配置是 `*.yaml` | 现在只有内置 `rudong` 可用;`cfg['store']` 恒等于 `P.store('rudong')` | 二选一:把 `available()` 同时认 `*.yaml`,或把场配置转成 json | | `store` seam 三种写法 | `cfg['store']`(14 处写点)/ `P.store()`(1 处,已修)/ `ROOT/'outputs'/…`(4 处,已修 3 处) | 多场前无实际差异,属"潜在缺陷" | 新代码一律 `cfg['store']` 或 `P.store(name)` | | 配置/依赖指向不存在的件 | `configs/scenario_registry.yaml`、`configs/<场>/value_assumptions.yaml`、`configs/analysis_lock*.yaml`、`scripts/sop_check.py`、`scripts/analysis_lock_check.py`、`docs/审核规则_经验固化_v1.md`、`docs/振动诊断模型_六层_v1.md`、`.claude/skills/…`、`m5_cms_tcm/model_run_l6.parquet`、`m5_cms_tcm/fusion_38.csv` | SOP 场景模块读注册表必抛;锁闸恒空转;`wrapup` 电价只能走"假定锚";CMS 报告**不能重生成**(会掉内容) | 见 §7 缺口表;这批是"随包没发全",要么补件要么显式降级(`wrapup` 已改为响亮说明) | | 被引用但未随包的脚本(10 个) | `rudong_tcm_ingest_raw.py`(已加前置判断,见 §3.2-10)、`rudong_tcm_oem_scan.py`、`rudong_line_energy_share.py`、`rudong_model_run.py`、`rudong_fusion_run.py`、`rudong_build_baseline.py`、`dsh_learn_page.py`、`intake_scan.py`、`guanlan_cloud_sims.py`、`deploy_gate_check.py` | 前五个属振动六层链(见 §7);其余是开发/部署辅助脚本的引用 | 逐个二选一:补件,或在调用处加"缺失即明确报错 + 替代路径"(`pipeline.ingest` 已按此改) | | 有读无写的"孤儿产物"(12 件) | `genbearing_monthly` / `mblub_monthly` / `yaw_dynamic_monthly` / `yaw1min_liveness` / `yaw_err_clean` / `sector_power` / `duty_monthly` / `pc_monthly_bins` / `thermal_monthly` / `control_monthly` / `structure.parquet` / `watch_channels_monthly` | 趋势件、热链、扇区、偏航/润滑/控制面 | 维持随包件;要"自己算"须研发给口径(照 `temp_monthly` 的办法反推 + 逐值验证) | | **有意**重复落盘(不是缺陷,但要知道) | ① `spectra_meta.parquet` 同时写 `<窗>/spectra/` 与 `<窗>/`(约 4.6 MB/窗)—— 为兼容 `data.spectra_meta()` 的两条读取约定;② `报告_CMS振动状态评估报告_<期>.md` 有两个来源(`report_std.py` 自算 vs `vib_reports_build.py` 厂家转录),靠**日期口径**区分(自算=当天、转录=报告期月末) | 磁盘占用翻倍(仅 meta,非谱数据);命名空间共享 | 已在此登记;`data.py` 优先读 `<窗>/spectra_meta.parquet`,两份内容逐字节一致 | | `P.VIEWER` 常量全仓 0 引用 | `src/paths.py` 定义了 `VIEWER`,实际取 `viewer_dir` 走 `configs/serve.json` | 无功能影响 | 下次统一时二选一(用起来或删掉),避免"看着像真源其实没人用" | --- ## 4. 进程与命令窗口(统一口径 `src/proc.py`) **问题**: Windows 上"**无控制台的父进程** + 裸 spawn 一个控制台程序" = 系统给子进程**新建一个可见控制台窗口**。 本系统里无控制台的父进程很多(网关、运维动作进程),于是"点页面按钮"会闪窗、跑重算会挂一个 20 分钟的黑窗、 `/ops` 每 2 s 轮询 `tasklist` 会反复闪窗。 **统一写法**: `src/proc.py` | API | 用途 | |---|---| | `proc.NO_WINDOW` / `proc.flags()` | `CREATE_NO_WINDOW`(Windows)/ 0(POSIX)。★与 `DETACHED_PROCESS` **互斥**,只认这一种 | | `proc.spawn(cmd, log=…, env=…, cwd=…, **popen_kw)` | 后台起无窗口子进程;`log=` 落日志,也可自己传 `stdout=`/`stderr=` 句柄(**透传**,不覆盖) | | `proc.run(cmd, **kw)` / `proc.run_text(cmd)` | 前台等待的无窗口子进程(`tasklist` / `git` / 短命令);`run_text` 默认 `text=True, errors='replace'`(控制台输出是 GBK,严格解码会得到 `None`) | 已收敛的调用点(**凡"父进程无可见控制台"的入口都必须走这里**): `guanlan.py`(组件 spawn、`tasklist`、`taskkill`)、 `scripts/_ops_launch.py`、`scripts/_ops_run.py`(整套重算的宿主进程)、`scripts/guanlan_ops.py`(启动器调用 + 每 2 s 的 `tasklist` 轮询)、 `scripts/guanlan_gateway.py`(`git`,每次 `/api/version`)、`scripts/_ops_start_and_open.py`(起 serve)、 `src/windcms/plugins.py`(CMS 页面「分析」按钮起的全链)、`scripts/windscada_serve.py`(详情工作台云端档问答)。 **判定口径**(新加子进程前先问一句): 我的父进程**有没有可见控制台**?有(用户从终端跑的 CLI)→ 让它继承,用户能看见进度; 没有(服务/网关/运维动作进程)→ **必须**走 `src/proc.py`,否则 Windows 会给子进程新建一个可见窗口。 ### 4.1 三个"静默故障"坑(都实炸过,已写进 `guanlan.py check` 作回归守卫) | # | 坑 | 现场表现 | 处置 | |---|---|---|---| | 1 | `CREATE_NO_WINDOW` 新建的控制台**会把子进程标准句柄吸走** | 从页面点"执行重算":`_ops_run` 自己那行进了日志,`rebuild_all.py` 及之后**一个字都没有**,任务随后挂住(0 CPU / 0 I/O / 1 线程);页面只剩"运行中" | `src/proc.py::_inherit_stdio()` 把父进程当前 `sys.stdout/stderr` **显式**交给子进程(STARTF_USESTDHANDLES) | | 2 | `capture_output=True` 与显式 `stdout=` **互斥**,同给抛 `ValueError` | 异常被 `guanlan_ops.job_running()` 的 `except` 吞成 `alive=False` ⇒ **任何在跑的任务都被立刻改写成"被强杀"**,页面显示完成、重算按钮重新可点(可能并发起两个重算) | `_inherit_stdio` 先让路(有 `capture_output` 就什么都不加);自检加一条"run_text(capture_output) 可用" | | 3 | 内嵌版 `/ops/recalc` 裁掉了「服务」卡,但 JS 仍 `$('#b_start').onclick=…` | 元素为 null → 抛 `TypeError` → **后面所有按钮绑定与 `refresh()` 全不执行** ⇒ 用户看到的正是"清除产物/执行重算点了没反应" | 所有元素访问与绑定走 `set()`/`on()` 容错包装;并加 try/catch 把脚本错误**显示在页面上**(不再静默) | **服务端配套**: `/ops*` 响应现在带 `Cache-Control: no-store, must-revalidate` —— 否则页面 HTML/脚本更新后浏览器仍用旧版, 表现同样是"改了没反应"(`guanlan_gateway.py::_bytes(no_store=True)`)。 ### 4.2 用户怎么启动(不弹命令窗口) | 入口 | 行为 | |---|---| | `start_hidden.vbs`(推荐,可建桌面快捷方式) | `WScript.Shell.Run(..., 0, False)` 以 `pythonw.exe` 跑启动器 —— **完全不出现命令窗口** | | `start.bat`(双击) | 默认走上面的隐藏路径(自己立刻退出);`start.bat console` = 旧的前台模式(排障看实时日志) | | `scripts/guanlan_start_hidden.py` | 隐藏启动器本体: 端口已在 → 只开浏览器(幂等);否则无窗口起 `guanlan.py serve`、等 `/healthz`、开浏览器 | | 运维控制台/门户「数据重算」 | 动作进程同样无窗口;进度与日志尾巴在页面里看 | **日志去哪了**: `logs/serve.log`(隐藏模式下 serve 的输出)、`logs/start_hidden.log`(启动器轨迹:就绪耗时/失败原因/是否已开浏览器)、 各组件 `logs/.log`、运维动作 `logs/ops_<动作>_<时间>.log`。失败时隐藏启动器还会弹一个**消息框**(无窗口模式下唯一能让人看见的通道)。 **实测**(2026-09-16): 全停后可见窗口 0;`start.bat` 2.3 s 返回;启动后可见窗口 **0**,6 个服务端口全开, 7 s 内 `/healthz` 就绪。 --- ## 5. 页面取数与"产物即时进页面" ### 5.1 产品数据的读取口径 | 服务 | 读法 | 产物改了要不要重启 | |---|---|---| | detail `:18033`(`scripts/windscada_serve.py`) | 首次请求把产物读进内存 `_CACHE` | **不需要**:每次取数前比**产物指纹**(`products_stamp()` = 相关产物文件的 `(路径, mtime_ns, size)` 摘要,TTL 2 s),指纹变了自动重载 | | cms `:18020`(`src/windcms/serve.py`) | 报告/页面/文件**每请求现读** | 不需要 | | cms 的谱元数据(`src/windcms/data.py::spectra_meta`) | 进程内缓存,缓存键含**各窗 index/spectra_meta 指纹** | 不需要(新摄入/重算后自动失效) | | 门户(网关 `:28084`) | `portal.html` 按 `(mtime, size)` 缓存改写结果 | 不需要(重建门户即生效) | | 网关的 `/ops` 页面模块 | `guanlan_ops` 模块级缓存 | **需要重启网关**(改了这个文件才需要) | > 实测证据: 只改一个产物文件的 mtime,`logs/detail.log` 立刻出现 > `[reload] 产物指纹变化 b15e2652… → f0b457f7…, 重载`,页面随即用新数(无需重启)。 > 另有显式入口 `GET /detail/api/reload`(清缓存并回显新旧指纹)。 ### 5.2 门户菜单 顶部菜单: `总览 | 系统架构 | 方法 | 经验发现 | 案例·液压 | 振动·CMS | 仿真与回放 | 交付文档 | 系统状态 | 数据重算 | 登录` (2026-09-16 用户令: 「数据重算」落在「交付文档」与「登录」之间)。 - 门外壳正本 `release/portal_src/shell.html` → 装配 `release/portal.html`(`scripts/portal_build.py`)。 改外壳后流程: `--check`(看漂移)→ `--rebaseline`(**有意**改动后重设基准)→ 装配 → `--check` 应"全部一致 ✔"。 - `#recalc` 段落用 iframe **懒加载** `http://127.0.0.1:28084/ops/recalc`(同源;切到本页才挂 src, 否则隐藏时它还会每 2 s 轮询 `/ops/api/state`)。 - `/ops/recalc` 是 `/ops` 的**内嵌版**(只保留 重算 + 产物 + 动作进度;服务卡与页头由门户承担)。 走独立路由而不是 `?embed=`:网关转给 `guanlan_ops.handle()` 的是 `u.path`,查询串会被丢掉 —— 用查询串 会变成"看起来支持、实际不生效"的静默坑。原 `/ops` 整页保留不变。 --- ## 6. 输入 ↔ 产物呼应 与 重算 ### 6.1 呼应关系(机器可校验) `python scripts/inventory_products.py --check` 逐类输入算跨度/条数,与它喂出的产物对拍(退出码 5 = 有问题)。 2026-09-16 实测**呼应正常**: | 输入类 | 输入跨度 | 件数 | 对拍产物 | 产物跨度/条数 | |---|---|---|---|---| | `scada_10min` | 2025-01-01 ~ 2026-07-07 | 38 | `temp_monthly.parquet` / `loss_monthly.parquet` / `powercurve_bins.parquet` | 2025-01 ~ 2026-07 / 19494 · 3729 行 | | `故障报警` | 2025 ~ 2026(文件名年粒度) | 16 | `alarms.parquet` | 2025-01-01 ~ 2026-07-15 / 39211 行 | | `风机故障记录` | 2021 ~ 2026(年粒度) | 135 | `workorders.parquet` | 2020-01-03 ~ 2026-07-08 / 5876 行 | | `油样报告` | 2024-11-19 ~ 2025-08-29 | 404 | `oil_samples_index.parquet` | 同跨度 / 404 行 | | `windcms`(CMS 原始导出) | 2026-03 ~ 2026-04 | 25693 | `m5_cms_tcm/windows/w0316/index.parquet` | 2026-03-16 ~ 2026-04-21 / 2,066,686 行 | ★ 年粒度输入(报警/工单的"年度汇总表")**不参与"产物落后"判定** —— 用文件名年份推出来的跨度天然是年粒度, 拿它当"输入到 2026-12"会造出假缺口(本脚本第一版就这么误报过 2 条)。 ### 6.2 重算的两种运行状态(都实测过) | 状态 | 入口 | 说明 | |---|---|---| | **系统运行中** | 门户「数据重算」或 `/ops` 的「执行重算」按钮;等价命令行 `python scripts/rebuild_all.py` | 动作经 `_ops_launch.py` 二次启动(不挂网关的父子树,避免 `taskkill /T` 把自己杀掉);跑完**不需要重启**就能在页面看到新数(§5.1 指纹重载)。重算期间按钮全灰、并发动作被后端拒(HTTP 409) | | **系统未运行** | 同一套命令行(先 `guanlan.py stop` 或本就关机状态) | 全部构建器都是普通 CLI,不依赖服务;跑完再 `start.bat`/`start_hidden.vbs` 起来,页面直接读新产物。**实测**: 全停后跑 `rebuild_from_raw.py` rc=0、`inventory_products.py --check` rc=0 | 一键顺序(`rebuild_all.py`,14 步): ① 放数据(`--src` 才跑)/② 三门台账/③ SCADA 侧 10 构建器/④ 月度派生件/ **④b 振动侧摄入**/⑤ 补齐随包件(★2026-09-16 用户令"清除产物不留备份"之后, 随包件不再有 `_products_off/` 暂存区 ⇒ 该步固定返回 6 跳过并说明; 要补齐须显式给交付包 `products_restore_missing.py --stash <交付包.zip>`, **不再打断整条链**)/ ⑥ 重启组件服务/ ⑦ 本体六步/⑧ 本体审计(+可选等价验收)。 --- ## 7. 缺口与边界(如实列) | 缺口 | 影响的产物 | 依据/证据 | 补齐判据 | |---|---|---|---| | 振动六层链四步脚本未随包(`rudong_tcm_oem_scan` / `rudong_line_energy_share` / `rudong_model_run` / `rudong_fusion_run`) | `oem_frequency_scan`、`gear_freq_scan`、`blade_1p_*`、`model_run_l6.parquet`、`fusion_38.csv` 等 | `outputs/<场>/m5_cms_tcm/vib_raw_manifest.json` 的 `missing_chain` | 给脚本或口径;源件已在 `data/raw/<场>/windcms/` | | 厂商月度报告 12 份 PDF 是**纯扫描件** | 「CMS 振动评估报告」的厂商侧 | pypdf 实测 12 页 12 图、`extract_text()` 长度 0 | 现场给电子件(docx/xlsx),或上 OCR(本包不装) | | `handoff_vibration_v2.json` / `component_history.json` 是随包快照 | 融合面判级、`/cms/` | 该件含人工裁决/校准更新,不是测量数据的函数 | 现场给正本,放 `data/raw/<场>/m5_cms_tcm/` | | 随包件"暂存区"**按设计不再存在**(2026-09-16 用户令"清除产物不留备份") | 第⑤步"补齐随包件"固定返回 6 跳过 | 旧设计把清掉的产物挪到 `_products_off/` 以便还原;现口径 = 真删除 | 要补回"包内没有生成端"的随包件: `python scripts/products_restore_missing.py --stash <交付包.zip>`(从交付件按需补齐, **不在安装目录里留备份**)。`products_state.py --off` 需显式 `--yes`;`--on` 已移除 | | 台账等价验收基线 `outputs/<场>/windscada/_pre_rebuild_20260911/` 曾缺失 | 第④步与 `rebuild_from_raw.py --verify` 的逐值比对(缺基线时 ④ 返回 5) | 该目录在打包时被"清除产物"挪进了暂存区, 而暂存区随后被清掉 ⇒ 机器上无标准答案 | **2026-09-16 已重建**: 从当天 dist 包里取 4 件(`alarms/workorders/oil_samples_index/temp_monthly`)+ `_来源说明.json` 标明来历。★链已加固: ④ 容忍 rc=5 —— **缺基线只跳过"等价验收", 不再打断整条重算链**。注意: 「清除产物」会把这个目录一并删掉(它在 `windscada/` 里), 届时需按同样办法重建 | | 油样 2026-07 批 102 行(华标合并报告) | 数据层「油液化验」 | 源件 `BG-2026-07-YP013 ….pdf` 不在现场包 | 补那份 PDF 后 `rebuild_from_raw.py --verify` 无人工项 | | 无生成端的组级产物(`pc_monthly_bins` / `duty_monthly` / `thermal_monthly` / `sector_power` / `yaw_*` / `genbearing_monthly` / `mblub_monthly` / `pitch_daily` 等) | 趋势件、热链、扇区、偏航/润滑面 | 全库只有读取方、0 处写入方 | 研发补口径(照 `temp_monthly` 的办法反推 + 逐值验证) | | `configs/farms/*.yaml` 无代码读者;`available()` 只认 `*.json` | 多场部署 | `src/windscada/config.py::available()` | 二选一(见 §3.3) | --- ## 8. 变更记录 | 日期 | 变更 | |---|---| | 2026-09-16 | **本文建立**(用户令 1): 产物全景(自动生成清单)+ 路径唯一真源与本次统一的 8 处 + 未统一项如实列表 + 进程无窗口口径 `src/proc.py` + 产物指纹热重载 + 输入↔产物呼应 + 重算两种状态 + 门户「数据重算」 | | 2026-09-16 | 用户令 2/3/4/5 的落地: `inventory_products.py`(清点+呼应校验,`--check` 出码);⑤步不再打断重算链(`StashMissing` rc=6);`start_hidden.vbs` + `start.bat` 默认无窗口;门户菜单新增「数据重算」+ `/ops/recalc` 内嵌版;`portal_build.py --rebaseline` | | 2026-09-16 | 振动侧接入(详见 `docs/振动数据接入_v0.1.md`): `data/raw/<场>/{windcms,m5_cms_tcm}` 两类源件、`rudong_tcm_index.py`/`rudong_tcm_spectra.py`/`vib_raw_build.py`/`vib_reports_build.py`、窗 `w0316` | | 2026-09-16 | 用户令"清除产物不留备份": `products_state.py --off --yes` 改为**真删除**(不再产生 `_products_off*/`)、`--on` 与门户「恢复产物」按钮移除;随包件的唯一来源改为**交付包 zip**(`products_restore_missing.py --stash <交付包.zip>`);`derived_manifest.prune()` 清掉陈旧自登记(`raw-derived` 台账 3444 → 1740 件,回到真实) | | 2026-09-16 | 用户令"打包不含 输入数据/产物/日志" → 交付包 **v0.4.0**(见 §9): `pack_dist.py` 增 `--no-products`、`VERSION='0.4.0'`、`dist-manifest.json` 记 `no_data/no_products/no_logs` 与逐条排除理由;`guanlan.py check` 读该清单,产物缺失显示 `[--] 待重算` 而非 FAIL | | 2026-09-16 | **现场实炸一次**: 目标机 `D:\产品\app` 双击 `start.bat` 报"无法找到脚本文件 `start_hidden.vbs`" —— 打包器 `INCLUDE_FILES` 是**手工清单**,新加的根目录文件(无窗口启动的 `.vbs`)忘了写进去。修法不是"再补一行",而是**从引用反推**:新增 `src/entry_refs.py`,凡入口脚本(`.bat/.vbs/.ps1/.sh`)引用的、本机真实存在的文件,打包时**自动补入**、打包后**对着包内条目复核**(缺一个就删掉这个包并 rc=1)、开箱验证(`--verify` 第①b 步)与装机自检(`guanlan.py check` 一行)各再查一遍。同时修正 `size_of()` 把 `__pycache__/*.pyc` 也数进"预演件数"的虚报(src 少报 129 件) | --- ## 9. 交付包组成(v0.4.0,2026-09-16 用户令:不含 输入数据 / 产物 / 日志) 打包器:`scripts/pack_dist.py`。默认文件名 `<父目录>/guanlan-v_dist_<日期>.zip`,可用 `--out` 指定。 | 进包 | 内容 | |---|---| | `src/` `scripts/` `guanlan.py` | 程序与全部构建/运维脚本(含 `src/entry_refs.py` 入口引用闭合检查) | | `configs/` `resources/` `reference/` | 配置(含 `serve.json` 端口真源、场站/通道台账) | | `release/` | 门户、仿真页、三维资产 `viewer/`、治理清单交付件 `release/如东/`(客户交付物,勿外传) | | `docs/` `README_先读我.txt` `测试须知.txt` | 交付文档与说明书 | | `wheels/win_amd64/`(42 件)、`vendor/python/`(3 平台便携运行时) | 离线安装件:Windows 完全离线可装;Linux/macOS 走联网安装(要离线就把轮子放进 `wheels/linux_x86_64` / `wheels/macos_arm64`) | | `install.bat/.ps1/.sh`、`check/start/stop.bat`、**`start_hidden.vbs`**、`requirements.txt` | 安装与起停入口(`start.bat` 默认调 `start_hidden.vbs` 无窗口启动,缺它则双击即报错) | | 不进包 | 理由(同时写进包内 `dist-manifest.json` 的 `excluded`) | |---|---| | `data/` | 现场原始输入件;约定"原始件不随包分发"(要带用 `--with-data`) | | `outputs/` | **用户令**:产物不进包(`--no-products`)。目标机放数据后 `rebuild_all.py` 重算;或缺"无生成端"的随包件时用 `products_restore_missing.py --stash <交付包.zip>` | | `logs/` `run/` | **用户令**:日志不进包(`run/pids.json` 里的 PID 到新机器上是无效引用) | | `.venv/` `.git/` `.github/` | venv 换机必失效(安装时重建);版本库不随交付件分发 | | `_products_off*/` | 旧设计的"清除产物"暂存档(2026-09-16 起清除=真删,不再产生;老机器上若有可手工删) | | `__pycache__/` `*.pyc`、顶层 `*.zip` | 解释器缓存、旧的交付压缩包(避免包中包) | 开箱验证(一条命令给出"能不能装、能不能跑"的证据):`python scripts/pack_dist.py --verify ` —— 解压到 临时目录 → ①b **入口引用闭合核对** → 离线安装 → 起服务核验 `/`·`/detail/`·`/cms/`·`/ops`·`/healthz` → 收尾清理, 要求 ≥4 个页面可用。 ★ 核验要求组件端口(18033/18020/18791/18792/64292)空闲,**验证前先 `guanlan.py stop`**,否则整段核验被跳过(打印 `[!]`)。 ### 9.1 入口引用闭合(`src/entry_refs.py`,2026-09-16 实炸后加) 现场反馈:目标机 `D:\产品\app` 双击 `start.bat` → `无法找到脚本文件 "D:\产品\app\start_hidden.vbs"`。 根因是打包器的 `INCLUDE_FILES` 是**手工清单**,新加根目录文件时漏了一项 —— 这类清单必然漏,所以改成**从引用反推**: ``` 凡是入口脚本 (.bat/.vbs/.ps1/.sh) 里出现的一个路径, 且该路径在本机源码树里真实存在 → 它必须在包里 (本机不存在的 `.venv\Scripts\pythonw.exe` / 系统 exe / `%~dp0` 前缀残片, 按"装机后才有的东西"忽略) ``` 四处落地:打包时**自动补入**(`plan()` 打印 `[+] 自动补入 (被入口脚本引用)`)→ 打包后**对着 zip 条目复核**(缺一个就删包 + rc=1,不留"看起来能用"的坏包)→ `--verify` 第①b 步在解压目录再查 → `guanlan.py check` 输出一行 `入口脚本引用闭合 (23 条: start.bat→start_hidden.vbs 等)`,装机后 `check.bat` 也能看出来。