# 观澜 · 如东样板 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 用户怎么启动(不弹命令窗口) ★ **2026-09-16 用户令"把 VBScript 替换掉"**:原先靠 `start_hidden.vbs`(`WScript.Shell.Run(..., 0, False)`) 隐藏启动。VBScript/Windows 脚本宿主(WSH)已被微软列为弃用特性,且服务器上常被组策略整体禁用, 所以现在**包里没有任何 `.vbs`**,无窗口启动由 `pythonw.exe`(无控制台子系统的解释器)+ Python 启动器承担: | 入口 | 行为 | 窗口 | |---|---|---| | `启动观澜.lnk`(**推荐**;`install.ps1` 安装时生成,根目录一份 + 桌面一份) | 快捷方式直接指向 `.venv\Scripts\pythonw.exe` + `scripts\guanlan_start_hidden.py`(工作目录=安装根,窗口样式 7) | **一个窗口都不出现**(不经过 cmd.exe,也不经过 WSH) | | `start.bat`(双击) | 默认 `start "" /min ".venv\Scripts\pythonw.exe" "scripts\guanlan_start_hidden.py"` | 会**闪一下**(.bat 是控制台程序,Windows 必定先开窗口,脚本内无法消除);`start.bat console` = 旧前台模式(排障看实时日志,关窗=停服务) | | `scripts/guanlan_start_hidden.py` | 启动器本体:端口已在 → 只开浏览器(幂等);否则无窗口起 `guanlan.py serve`、等 `/healthz`、开浏览器 | 无窗口(`--selftest` 自证:`GetConsoleWindow()==0`) | | 运维控制台/门户「数据重算」 | 动作进程同样无窗口;进度与日志尾巴在页面里看 | 无窗口 | **日志去哪了**: `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` 就绪。VBScript 移除后复测(2026-09-17,就在这一版源码上): ① 全新副本 `install.ps1` **rc=0**(172 s),安装后目录里生成 `启动观澜.lnk`(1,683 B); ② `pythonw.exe scripts\guanlan_start_hidden.py --selftest` → `{"console_hwnd": 0, "windowless": true}`; ③ 全停后按快捷方式的方式启动(`pythonw.exe scripts\guanlan_start_hidden.py`):命令**立即返回**, `logs/start_hidden.log` 记下 `窗口状态: 无控制台窗口 (= 无窗口启动成立)`,7 s 内 `/healthz` 就绪, `/`·`/detail/`·`/ops`·`/ops/recalc` 全部 200; ④ 自动化核验用 `install.ps1 -NoDesktopShortcut`,避免在桌面留一个指向临时副本的死链。 --- ## 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`)起来,页面直接读新产物。**实测**: 全停后跑 `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) | | 门户/交付件引用的产物树 **不在本包**:`cleaned/`、`gearbox_life/`、`yaw_verify/`、`structured/` | 传动链回放面板、四系统判据面板、控制律仪表台、如东取数单 | `scripts/pages_audit.py` 报 19 条「引用悬空(已知)」;本包在位的是 `outputs/<场>/{windscada,m5_cms_tcm,sop,ontology,paradigm_r1,pitch,guanlan,tcm_compatible_replay,windcms}` | 六层链/结构化产物随包,**或**把页面里的引用改到本包在位路径(需研发确认是同一件东西;不改交付件正文) | | 2 件数据派生页面**没有生成端**:`release/如东/全场状态一览_脱敏_20260825.html`(1.5 MB 场站状态快照)、`release/如东/如东取数单_2026-08-21.html` | 交付文档页里这两个子页 | 全仓搜不到生成端;页面里也没有 `source`/指纹 ⇒ 无法做陈旧检测(§10) | 补生成端并写成带 `source_sha256` 的产物(推荐);或明确降级为 `frozen-delivery` 并接受它会过期(需用户裁定) | | 3 件内嵌快照**缺指纹**:标准面板、覆盖度 ch0、如东取数单 | 门户内嵌页 | 只有「生成 2026-09-05 16:28」这类时间戳;覆盖度页的源 `e1_base.parquet` 不在本包 | 生成时把 `source` + `source_sha256` 写进页面(机制已就绪,见 §10 的 `data-derived` 规则) | --- ## 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/.ps1/.sh`)引用的、本机真实存在的文件,打包时**自动补入**、打包后**对着包内条目复核**(缺一个就删掉这个包并 rc=1)、开箱验证(`--verify` 第①b 步)与装机自检(`guanlan.py check` 一行)各再查一遍。同时修正 `size_of()` 把 `__pycache__/*.pyc` 也数进"预演件数"的虚报(src 少报 129 件) | | 2026-09-16 | 用户令"**把 VBScript 替换掉**"(见 §4.2): 删除 `start_hidden.vbs`;无窗口启动改为 `pythonw.exe` + `scripts/guanlan_start_hidden.py`(不经过 wscript/WSH —— 服务器上 WSH 常被组策略禁用/未安装);`install.ps1` 安装时用 PowerShell 原生 COM 建 `启动观澜.lnk`(根目录 + 桌面,带 `-NoDesktopShortcut` 供自动化核验);`start.bat` 默认改为 `start "" /min pythonw.exe …`(.bat 会闪一次,快捷方式才是零窗口);`guanlan_start_hidden.py --selftest` 用 `GetConsoleWindow()==0` **自证无窗口**;打包器新增 `*.lnk`(安装时才产生、内含绝对路径)与顶层 `*.zip` 排除(后者原先只写在文档里没实现) | | 2026-09-17 | **入口脚本的换行/编码事故两连** —— 这类问题都不会在开发机上暴露,只在目标机双击时炸:
① `install.ps1` 的 UTF-8 BOM 被编辑工具吃掉(我在规范化之后又改了一次文件)⇒ PS 5.1 按 GBK 解码 ⇒ 中文乱码 + 级联 ParserError,目标机 `install` **1 秒即退出**。开箱验证当场逮到(`install 退出码 1, 耗时 1s` + `The '<' operator is reserved for future use`)。
② `install.sh` **从写出来那天起就是 CRLF**(git HEAD 的 blob 就是 CRLF,不是某次编辑造成的)⇒ POSIX 语义下每个词尾粘 `\r`:`set -e` 变非法选项、`RT=""` 变 `RT="\r"` 让后续 `-n "$RT"` 判断翻面 ⇒ **此前所有交付包的 Linux/macOS 安装脚本都是坏的**。
加固:`src/entry_refs.py::encoding_problems()` 成文守则(`.ps1` = UTF-8 BOM + CRLF;`.bat` = CRLF 无 BOM;`.sh` = LF),在**打包、开箱验证第①b 步、装机自检**三处强制执行(违反就不出包 / 直接判 FAIL);`guanlan.py check` 增一行;新增 `.gitattributes` 把 checkout 也钉死(`*.bat/*.ps1 eol=crlf`、`*.sh eol=lf`)。变异测试:去掉 BOM / 改 CRLF / .bat 存成 LF 三种改法都被逐条报出 | | 2026-09-17 | 打包后**对着 zip 条目逐件复核**(3,733 件全部与工作树逐字节相同)+ 开箱验证:解压 3,734 件 → ①b 闭合与编码守则通过 → 离线安装 rc=0(172 s)→ ②b 快捷方式在位 + `console_hwnd=0` 无窗口成立 + 包内无 `.vbs` → 页面 4/5(`/` 与主包同字节;`/cms/` 503 属无产物预期) | | 2026-09-17 | **用户令 1「页面是否属于产物」的落地(见 §10)**:新建登记表 `configs/portal_pages.yaml`(20 个页面/子页 + 五类 kind + 逐条判定依据)与检查器 `scripts/pages_audit.py`(五条机器规则;**陈旧检测** rc=7:内嵌快照的 `source_sha256` 与当前产物不符即报"数据变了页面没变");`guanlan.py check` 增一行;`rebuild_all.py` 增 ⑧b「重装门户(把新产物灌进门户结论段)」与 ⑧c「页面归口审计」;文档增 §10(含用户问的三页逐页判定)。**结论**:`#findings` 静态、非产物;`#sim` 本体是图纸派生的冻结资料包、其中"实际运行回放"面板属产物;`#documents` 里治理清单/报告是冻结交付件,而脱敏状态一览与取数单属产物且缺生成端与溯源(已入 §7 缺口表) | | 2026-09-17 | **用户令 2「统一配置与日志」的落地(见 §11)**:① 配置——`src/paths.py` 增唯一取用口 `P.config()/P.config_dir()/P.farm_config()`,9 个模块不再手拼 configs 路径;新增登记表 `configs/registry.yaml` 与检查器 `scripts/config_audit.py`(R1–R8);修掉三处真问题:**10 个场定义 YAML 因 `available()` 只 glob `*.json` 而"配了看不见"**(并查明它们其实是另一条链的机型/物理约束 profile,逐件登记在册)、坏模板 `_模板.json.example`(GBK 乱码 + 写死 `/Volumes/WINDDATA`)重写为 `_模板.yaml.example`、`sop/wrapup.py` 拼了不存在的 `configs/<场>/` 路径;删除 `serve.json.bak-bomfix`;5 处"代码引用但包里没有"的配置登进 `known_missing` 并写明影响。② 日志——新增 `src/logfile.py`(目录/命名/行格式唯一口径 + `prefix_stdout()` 流包装 + 动作日志保留 20 份/30 天 + `logs/build` 归位口)与 `scripts/log_audit.py`(L1–L5);6 个服务 + 启动器 + 运维动作全部接入统一格式(`viewer`/`sim` 改走新增的 `scripts/static_server.py`);**把 39 个躺在产物目录里的构建日志搬到 `logs/build/` 并同步重算产物台账**(2309→2270 件,`呼应校验` 仍 rc=0)、22 份历史动作日志与 8 个服务日志的旧内容归档 `logs/legacy/`;`guanlan.py check` 增两行 | | 2026-09-17 | **用户令 3「输入数据放置检查 + 指导」的落地(见 `docs/输入数据放置指导_v0.1.md`)**:新增 `scripts/raw_data_check.py`(R1–R8:顶层/子目录约定、各源类后缀与结构、**TCM `*_decode.json` 按摄入脚本真实取法抽样**(`body.body[时间戳]=[{Record:…}]`,机组取 `Record.Location`)、CSV 编码表头抽样、**增量三清单(新增/相同跳过/冲突)**、时间空洞、体量提示、按新增源类给重算命令);`place_raw_data.py` 落位前自动体检并拦下冲突(`--force` 才覆盖,`--no-check` 可跳过);新增指导文档(正确目录树、逐类规则、增量语义、放完跑什么、常见错误对照表,含自动生成的体检结论块);`guanlan.py check` 增一行。**实测**:真实树 26,325 件/177.6 GB 结构合规、TCM 抽样 12/12 通过;合成增量包(同尺寸/改过/新台号各一)三清单齐全、冲突拦下 rc=6 |:① 配置——`src/paths.py` 增唯一取用口 `P.config()/P.config_dir()/P.farm_config()`,9 个模块不再手拼 configs 路径;新增登记表 `configs/registry.yaml` 与检查器 `scripts/config_audit.py`(R1–R8);修掉三处真问题:**10 个场定义 YAML 因 `available()` 只 glob `*.json` 而"配了看不见"**(并查明它们其实是另一条链的机型/物理约束 profile,逐件登记在册)、坏模板 `_模板.json.example`(GBK 乱码 + 写死 `/Volumes/WINDDATA`)重写为 `_模板.yaml.example`、`sop/wrapup.py` 拼了不存在的 `configs/<场>/` 路径;删除 `serve.json.bak-bomfix`;5 处"代码引用但包里没有"的配置登进 `known_missing` 并写明影响。② 日志——新增 `src/logfile.py`(目录/命名/行格式唯一口径 + `prefix_stdout()` 流包装 + 动作日志保留 20 份/30 天 + `logs/build` 归位口)与 `scripts/log_audit.py`(L1–L5);6 个服务 + 启动器 + 运维动作全部接入统一格式(`viewer`/`sim` 改走新增的 `scripts/static_server.py`);**把 39 个躺在产物目录里的构建日志搬到 `logs/build/` 并同步重算产物台账**(2309→2270 件,`呼应校验` 仍 rc=0)、22 份历史动作日志与 8 个服务日志的旧内容归档 `logs/legacy/`;`guanlan.py check` 增两行 | --- ## 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`、`requirements.txt` | 安装与起停入口(**无 `.vbs`**:`start.bat` 默认调 `pythonw.exe` + `scripts/guanlan_start_hidden.py`;`install.ps1` 安装时另建 `启动观澜.lnk` 供零窗口启动) | | 不进包 | 理由(同时写进包内 `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 起清除=真删,不再产生;老机器上若有可手工删) | | `*.lnk` | 安装时建的快捷方式:内含绝对路径,换机必失效,由目标机的 `install.ps1` 重建 | | `__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/.ps1/.sh) 里出现的一个路径, 且该路径在本机源码树里真实存在 → 它必须在包里 (本机不存在的 `.venv\Scripts\pythonw.exe` / 系统 exe / `%~dp0` 前缀残片, 按"装机后才有的东西"忽略) ``` 四处落地:打包时**自动补入**(`plan()` 打印 `[+] 自动补入 (被入口脚本引用)`)→ 打包后**对着 zip 条目复核**(缺一个就删包 + rc=1,不留"看起来能用"的坏包)→ `--verify` 第①b 步在解压目录再查 → `guanlan.py check` 输出一行 `入口脚本引用闭合 (24 条: start.bat→guanlan_start_hidden.py 等)`,装机后 `check.bat` 也能看出来。 ★ 那个 `.vbs` 已按用户令删除(§4.2),但这份守卫保留:它守的是"入口引用的文件必须齐全"这条性质,与用什么语言实现无关 —— 现在它盯的是 `start.bat`/`install.ps1` → `scripts\guanlan_start_hidden.py` 这类引用(这次替换正是它自动发现的:去掉 `.vbs` 后入口列表变为 6 个、引用 24 条,全部在位)。 --- ## 10. 页面归口(哪些页面是产物、该不该随数据变) **用户问的三页,结论(依据见下表与 `configs/portal_pages.yaml`)**: | 页面 | 该不该随输入数据变 | 算不算产物 | 现状与处置 | |---|---|---|---| | `#findings` 经验发现 | **不该** | **不是** | 外壳里的静态叙述(2,322 B,0 iframe/fetch/产物引用),内容是公司级方法论而非本场站数据的函数 ⇒ 留在受管外壳。若将来要放本场站计数,必须改成 `data-derived` 并登记 `source` | | `#sim` 仿真与回放 | 页面本体**不该** | 本体**不是** | 叙述+6 个链接:仿真台是**图纸/机型**派生的冻结资料包(`release/如东SWT40_控制律仿真台_20260906.zip`,由 `release/sim_sys_server.py` 从 zip 现读),三维工作台是图纸派生。**但**其中"实际运行回放"面板(门户内嵌 `tpl-如东传动链实际运行诊断_单文件版`)把 `outputs/<场>/cleaned/turbine_1min.parquet` 连 `source_sha256` 一起烘进了页面 ⇒ **那一件属于产物** | | `#documents` 交付文档 | 交付件**按版本变**,不随数据自动变 | 治理清单/报告**不是**(冻结交付件);**取数单与脱敏状态一览是** | iframe 指向的治理清单分册自带版本号+日期(v1.2_2026-09-01 / v4.5_2026-09-06)⇒ 冻结交付件。但 `全场状态一览_脱敏_20260825.html`(1.5 MB 场站状态快照)与 `如东取数单`(明列产物路径)是**数据派生**的 ⇒ 属产物,且**现在既无生成端也无溯源** | **五种 kind 与机器规则**(登记表头部有完整说明): | kind | 该随数据变? | 机器查什么 | |---|---|---| | `static` | 否 | 正文不得引用产物(引用了说明分类错了) | | `live` | **是**(永远一致) | iframe/链接的端口必须在 `configs/serve.json` 的已知服务里 | | `data-citing` | 否(正文不变) | 被引用的产物路径**必须存在**(悬空引用 = 挂羊头) | | `data-derived` | **是** | 必须有 `source` + (`source_sha256` 或生成时间);有 sha 就跟当前产物**逐字节比对 ⇒ 陈旧检测** | | `frozen-delivery` | 否(按交付版本变) | 文件名或正文必须能读到**版本号 + 日期** | **"纳入产物管理"落到本系统的四件事**:① 登记(`configs/portal_pages.yaml`:kind/来源/生成端/依据); ② 溯源(`source` + `source_sha256` 或生成时间);③ **陈旧检测**(`scripts/pages_audit.py`,rc=7 = 数据换了页面没换); ④ 进链(`rebuild_all.py` ⑧b 重装门户把新产物灌进结论段、⑧c 页面归口审计;`guanlan.py check` 一行)。 实时读产物的页面(`/detail/`、`/cms/`、`/ops`)**不需要**快照,靠 §5.1 的产物指纹热重载即时更新。 **当前如实状态**:19 条**已知缺口**(登记表里逐条写了理由,不装作通过)—— 都是"页面引用了本包没有的产物树" (`cleaned/`、`gearbox_life/`、`yaw_verify/`、`structured/`,属未随包的六层链/结构化产物线,见 §7), 因此这些页面**拿不到当前值做比对**;另有 2 件数据派生页面**没有生成端**(脱敏状态一览、取数单), 以及 3 件内嵌快照**缺指纹只能按生成时间判断**(标准面板、覆盖度 ch0、取数单)。这三类都记在 §7 缺口表里。 ### 10.1 门户页面归口(自动生成,勿手改) 判据与说明见 `configs/portal_pages.yaml` 头部;检查器 `scripts/pages_audit.py`。 | 页面/子页 | kind | 随输入数据变? | 依据(为什么这么判) | |---|---|---|---| | `index` 总览 | `static` | 否 | 外壳 #index 段 2,519 B: 0 iframe / 0 fetch / 0 产物引用; 正文是产品承诺与入口导航 | | `architecture` 系统架构 | `static` | 否 | 外壳 #architecture 段 3,037 B: 无数据引用; 描述分层架构 | | `method` 方法 | `static` | 否 | 外壳 #method 段 2,959 B: 无数据引用; 描述方法论 | | `findings` 经验发现 | `static` | 否 | ★用户问的三页之一。外壳 #findings 段 2,322 B: 0 iframe / 0 fetch / 0 产物引用; 正文是公司级经验叙述(59 项内部检查 → 8 条公开教训, 60+ 个风场), 不是本场站数据的函数 ⇒ 不该随输入数据变, 也不是产物。若将来要放本场站计数, 必须改成 data-derived 并登记 source | | `case_hydraulic` 案例·液压 | `static` | 否 | 外壳 #case_hydraulic 段 6,187 B: 无数据引用; 案例叙述 | | `cms` 振动·CMS | `live` | **是** | 外壳 #cms 段里 1 个 iframe + 2 个链接指向组件 :18020 (src/windcms/serve.py), 组件按需读产物 ⇒ 页面本身不存快照, 数据变了刷新即变 | | `recalc` 数据重算 | `live` | **是** | 外壳 #recalc 段只有 1 个到 /ops 的链接(网关提供, 后端即真实状态) | | `login` 登录 | `live` | **是** | 外壳 #login 段链接到工作台 :18033/v2 | | `admin` 系统状态 | `live` | **是** | 外壳 #admin 段由外壳 JS 拉网关 /healthz 渲染(段内无静态数字) | | `sim` 仿真与回放 | `static` | 否 | ★用户问的三页之一。外壳 #sim 段 50,767 B 是**静态叙述**(机理/图纸说明), 段内 0 iframe / 0 fetch; 6 个链接指向 :18792 仿真台页与 :64292 三维工作台。仿真台的输入是**图纸/机型参数**(configs/machine_packs), 不是 data/raw ⇒ 页面本体不该随输入数据变 | |   └ `控制律仿真台资料包 (5 页)` | `frozen-delivery` | 否 | release/sim_sys_server.py 直接从 zip 里读这 5 页 ⇒ 冻结资料包(日期在包名里); 内容为图纸/机理仿真, 不随输入数据变 | |   └ `三维拆装工作台` | `static` | 否 | release/viewer/** 由图纸/三维模型构建(build-*.py + esbuild), 与 data/raw 无关; rev=hub-review-v1 是评审版本号 | |   └ `控制律仪表台面板 (门户内嵌)` | `data-citing` | 否 | 面板正文是固定的判据/仪表说明, 只把产物当**脚注引用**('判据与证伪条件全量见 outputs/rudong/yaw_verify/findings.json') ⇒ 正文不随数据变, 但被引用的产物必须在位 | |   └ `四系统判据面板 (门户内嵌, 4 份)` | `data-citing` | 否 | 同仪表台: 静态正文 + findings 引用脚注 | | `documents` 交付文档 | `static` | 否 | ★用户问的三页之一。外壳 #documents 段 5,690 B 是导航页(0 fetch), 1 个 iframe 指向网关下发的治理清单 HTML(release/如东/**)。段内不含数据快照 | |   └ `治理清单交付包 (分册 + 全册 + 正式报告)` | `frozen-delivery` | 否 | 文件名自带版本号与日期(如 如东_液压系统治理清单_v1.2_2026-09-01.html / 全册_v4.5_2026-09-06.html) ⇒ 客户交付件, 随交付版本变; 内容虽由分析产出, 但**冻结发布**, 不该随输入数据自动改 | |   └ `全场状态一览 (脱敏)` | `data-derived` | **是** | ★这是问题所在: 1.5 MB 的场站状态快照(脱敏版), 内容是实际运行状态的函数 ⇒ **属于产物**, 但页面里**没有任何指纹/生成时间/来源登记**, 全仓也搜不到生成端 ⇒ 目前游离在产物管理之外(无溯源、无陈旧检测、无重算入口) | |   └ `如东取数单` | `data-derived` | **是** | 取数单是「要哪些数据」的清单页, 明列产物路径 ⇒ 随产物结构变; 现在只有日期没有指纹, 也没有生成端随包 | | `embed_replay_chain` 传动链实际运行回放 (门户内嵌单文件版) | `data-derived` | **是** | 内嵌 window.RUDONG_ACTUAL_REPLAY 带 source=outputs/rudong/cleaned/turbine_1min.parquet 与 source_sha256 ⇒ 明明白白的产物烘入快照; **已可做陈旧检测**(产物在位时) | | `embed_standard_panel` 标准面板 (门户内嵌) | `data-derived` | **是** | 正文带 '生成 2026-09-05 16:28' 与产物引用 ⇒ 某次生成的数据快照; 缺 source_sha ⇒ 只能按生成时间+引用在位判断 | | `embed_coverage_ch0` 覆盖度报告 ch0 (门户内嵌) | `data-derived` | **是** | 正文写 '生成 2026-09-05 16:28 · 源: e1_base.parquet @e3b531b8eddf8197 (10-min, 2,973,638 格, 38 台)' ⇒ 数据派生; 但 e1_base.parquet **不在本包** ⇒ 溯源链断在包外 | | `embed_u6_sim` U6 变桨液压仿真台 (门户内嵌) | `static` | 否 | 图纸派生的物理回路仿真台面板; 无内嵌数据/指纹/产物引用 | | `embed_u6_health` U6 液压公共站健康报告 (客户版, 门户内嵌) | `frozen-delivery` | 否 | 984 KB 客户交付报告; 文件名标 '客户版'; 无数据指纹 ⇒ 按交付件冻结(缺版本号/日期, 见 audit 提示) | | `embed_gearbox_report` 整机综合诊断与风险评估 (主轴冲击深挖 V2.1, 门户内嵌) | `frozen-delivery` | 否 | 3.7 MB 综合诊断报告, 文件名带修订版号 V2.1; 报告类交付件按版本发布 | | `embed_reports_4` 分系统评估报告 (主轴承/齿轮箱/发电机/变桨, 门户内嵌 4 份) | `frozen-delivery` | 否 | 4 份系统评估报告(0.7–3.4 MB), 无内嵌数据/指纹; 属交付报告(缺版本号/日期登记) | | `embed_governance_12` 治理清单分册 (门户内嵌 12 份) | `frozen-delivery` | 否 | 12 份治理分册(0–11), 与 release/如东 下的分册同源; 冻结交付件 | | `embed_sc1_local` SC1 本地占位 (门户内嵌) | `static` | 否 | 0.4 KB 占位面板 | --- ## 11. 配置与日志的统一口径(2026-09-17 用户令 2) ### 11.1 配置目录与配置文件 **唯一取用口**:代码只许用 `src/paths.py` 的 `P.config(...)` / `P.config_dir(...)` / `P.farm_config(...)`, 不许再手拼 `"configs" / …`(统一前实测 9 个模块各拼各的:`llm_gate`、`windscada/terms`、`sop/analysis_lock`、 `sop/scenarios`、`sop/wrapup`、`guanlan.py`、`guanlan_ops`、`page_fingerprint`、`check_transferable`, 另有 `audit_chinese_terms.py` 要**试三个位置**才找得到 terms 库)。 | 位置 | 放什么 | 谁读 | |---|---|---| | `configs/serve.json` | 运行期单件:端口/路径真源(`install` 脚本写) | `guanlan.py`、网关、运维控制台、指纹、移植检查 | | `configs/models.json` | 运行期单件:本机模型档与 pull 命令 | `guanlan.py check`、`src/ontology/llm_gate.py` | | `configs/portal_pages.yaml` | 运行期单件:门户页面归口登记表(§10) | `scripts/pages_audit.py` | | `configs/registry.yaml` | **配置登记表**:每个域是什么、谁读、缺哪些 | `scripts/config_audit.py` | | `configs/canonical/`(71 别名 + 3 决策) | 通道/测点 canonical 字典与别名字典 | `src/sop/contract_gate.py`、`discriminators.py`;别名表由未随包的分析链消费 | | `configs/contracts/`(74) | 机型契约(判据参数)与机型-场站对照表 | `scripts/sim_hub/wake_*.py`;(历史)分析链 | | `configs/farms/`(15) | **场定义**(YAML 为准,历史 `.json` 兼容) | `src/windscada/config.py` | | `configs/terms/`(3) | 显示层术语映射、OEM 术语基准、行话规则 | `src/windscada/terms.py`、`ui/build.py`、`audit_chinese_terms.py` | **本次修掉的三处真问题**: 1. **场定义"配了看不见"**:`configs/farms/` 下 10 个 YAML 里 0 个能被 `available()` 列出(它只 glob `*.json`), `farm('FUSHAN')` 直接报"未知场"。现在 yaml/json 都认、约定 YAML,并且——**更重要的**——查清了那 10 个 YAML 其实**不是场定义**而是另一条链的"机型/场站物理约束 profile"(顶层 `meta` + `physical_constraints`), 于是 `available()` 只列**能加载**的场、`foreign_farm_files()` 把 profile 逐件报出来(登记表里已声明), 并把 `farm()` 的报错写得能看懂("该文件存在但不是场定义…")。 2. **配置模板是坏的**:`configs/farms/_模板.json.example` 是 GBK 乱码("绀轰緥椋庣數鍦?")且写死 `/Volumes/WINDDATA`, 与实物格式(YAML)相反。已重写为 `configs/farms/_模板.yaml.example`(UTF-8、YAML、`<场站名称>` 占位), 并同步 `docs/数据目录结构与落位约定_v0.2.md`。 3. **散件与悬空引用**:删掉 `configs/serve.json.bak-bomfix`(历史备份垃圾);`sop/wrapup.py` 原来拼的是 `configs/<场>/value_assumptions.yaml`(那个目录根本不存在)→ 改为 `configs/value_assumptions/<场>.yaml`。 另有 5 处**代码引用的配置不在包里**(`analysis_lock*`、`scenario_registry`、`discriminator_registry`、 `analysis_modules`、`value_assumptions/<场>`),逐条登进 `registry.yaml::known_missing` 并写明**影响** (例如场景注册表缺失 ⇒ SOP 场景解析不可用,但本包无调用方,故不影响页面)。 **机器规则(`scripts/config_audit.py`)**:R1 顶层只许放登记过的运行期单件配置;R2 无备份垃圾(`*.bak*` 等); R3 命名(空格判错、中文/大写只提示);R4 内容不得含本机绝对路径——**区分字段**:说明性字段/注释里记的 "当时数据在哪台机器"算溯源信息(提示),**被读取的字段**里出现 `C:\`、`/Volumes/…` 才是失配(判错); R5 代码引用的配置必须存在(未登记的按悬空引用判错,登记过的记 `i`);R6 不许手拼 configs 路径; R7 每个域必须声明 consumers 或写明"无人读";R8 `configs/farms/` 每个文件要么是能加载的场定义, 要么在登记表里声明为 profile/CSV 数据表。现状:**171 件配置、5 个域、1 个场定义、10 件机型 profile、 0 不一致**(14 条已知缺口、7 条提示)。 ### 11.2 日志目录、命名与内容格式 **统一前**(实测,不是推测):`logs/` 顶层堆了 34 个文件,其中 22 个是历史动作日志(无保留策略); **39 个构建日志躺在产物目录里**(`outputs/<场>/windscada/*.log` 等),还被 `_provenance.json` 登记成 `shipped` 随包件——产物台账里混着日志;`detail.log` 首行是 `b'windscada serve :18033\r\n'`(CRLF), `cms.log` 首行是一条历史 `SyntaxWarning`;**行里没有时间戳也没有级别**,`import logging` 的文件数 = 0。 | 位置 | 放什么 | 命名 | |---|---|---| | `logs/<组件>.log` | 长驻服务与启动器(组件 = `serve.json` 的键 + `gateway`/`serve`/`start_hidden`) | `gateway.log`、`detail.log`、`cms.log`、`sim.log`、`sim_sys.log`、`viewer.log`、`serve.log`、`start_hidden.log` | | `logs/ops/` | 运维动作(自动保留最近 **20 份 / 30 天**,写前清理) | `<动作>_.log` | | `logs/audit/` | 机器审计流水(一行一条 JSON) | `llm_audit.jsonl`、`cloud_qa.jsonl`、`terms_audit.json` | | `logs/build/<场>/…` | 构建/摄入脚本的日志(按产物子路径归档) | 沿用原相对路径 | | `logs/legacy/` | 统一格式**之前**的日志(启动器与动作的历史留档,可随时删) | 原名 + 时间戳 | **行格式**(每一行都要满足,正则 `src/logfile.py::LINE_RE`): `YYYY-MM-DD HH:MM:SS LEVEL 组件 消息`,级别 ∈ DEBUG/INFO/WARN/ERROR;UTF-8 无 BOM、行尾 LF、无 ANSI 颜色码。 **怎么保证 6 个服务都合规**:不要求各服务改写自己的 print —— 入口处调一次 `src/logfile.py::prefix_stdout('<组件>')`,之后 stdout/stderr 的每一行自动带前缀(含把 stderr 与含 `error/traceback/失败` 字样的行标成 `ERROR`)。`viewer`/`sim` 原先是 `python -m http.server`(Common Log 格式,挂不上前缀),改用新增的 `scripts/static_server.py`(同样带统一格式)。运维动作日志改由 `logfile.action_log()` 生成并带保留策略;本体模型闸与云问答审计改落 `logs/audit/`。 **本次的实际搬迁(都做了台账/留档,不是悄悄删)**: * **39 个构建日志** `outputs/**/*.log` → `logs/build/<场>/<原相对路径>`;同时把 `_provenance.json` 里这 39 条 删掉并**重算计数**(2309 → 2270 件;shipped 569 → 530;raw-derived 1740 不变),台账里写明"因日志归位移出"。 搬迁后 `inventory_products.py --check` 仍 **rc=0 呼应正常**。 * **22 份历史动作日志**(`ops_*.log`,2026-09-12 那批)与 `_proc_reg.log` → `logs/legacy/`; * 8 个服务日志里"统一格式之前"的内容 → `logs/legacy/<名>.<时间戳>.log`,现场文件只保留合规行 (服务以 append 持句柄,原地截断不影响继续写);`logs/llm_audit.jsonl` → `logs/audit/`。 **机器规则(`scripts/log_audit.py`)**:L1 运行日志只在 `logs/` 下(白名单:产物附件 `analyze_stdout.log`、 第三方工具缓存 `.npm-cache/`、`.qa-profile/`、`node_modules/`,各写了理由);L2 命名;L3 **按末尾 200 行**校验 行格式(启用前的旧行不算数);L4 行尾/颜色(历史行只提示);L5 `logs/ops/` 保留策略 + `logs/audit/*.jsonl` 每行必须是合法 JSON。现状:**8 服务日志 + 39 构建日志 + 1 审计流水,0 不一致**(39 条"历史构建日志"提示, 写它们的人不在本包);`guanlan.py check` 各报一行。