数据目录结构与落位约定_v0.2.md 35 KB

观澜 · 如东样板 v2 目录结构设计与落位约定

版本: v0.2.0 现场版 · 2026-09-11 适用: <安装目录>(安装目录, 下称 <安装目录>) 本文回答三件事: ①离线数据往哪放、系统怎么认;②每个目录干什么用;③放/不放数据时系统分别表现什么。


1. 一页速览

<安装目录>\
├─ data\raw\                         ★离线数据唯一入口 (现场数据只放这里)
│    └─ <场站名称>\                    ← 扫描辨识的抓手: 下一级目录就是场站名
│         ├─ scada_10min\             SCADA 10min 导出 (逐台 WTG01.csv…WTG38.csv)
│         ├─ scada_1min\              (可选) 1min 导出
│         ├─ 故障报警\                 报警事件导出 (SpreadsheetML *.xls)
│         ├─ 风机故障记录\             检修工单台账 (*.xls/xlsx, 内按 {年}年故障记录\ 分年)
│         ├─ 油样报告\                 油液化验报告 (*.pdf)
│         ├─ windcms\                 ★振动侧: CMS 原始测量导出 + 厂商月度评估报告 (2026-09-12 新增)
│         └─ m5_cms_tcm\              ★振动侧: TCM 侧深度分析报告 + 现场给的 handoff 正本
├─ outputs\rudong\                   ★系统取数用的**产物仓** (页面 99% 读这里, 不直接读 raw)
│    ├─ windscada\                   L0 标准仓: 37 个 parquet + 索引/日志
│    ├─ ontology\                    本体对象库 objects.json + 检索索引 + release_r1/r2
│    ├─ windcms\                     CMS 振动诊断产物 (报告/缓存/单机页)
│    ├─ m5_cms_tcm\                  振动线 handoff 与 TCM 兼容件
│    ├─ guanlan\facts_contract_v0.json  事实契约 (问答引文的依据)
│    └─ sop\                         SOP 中间件/评审/台账 (含少量历史装配脚本)
├─ configs\                          端口/模型/场配置/机型契约 (目录约定见 系统设计说明 §11)
│    ├─ serve.json                   端口、发布目录、Python 路径 (install 脚本写)
│    ├─ models.json                  Ollama 档位与 pull 命令
│    ├─ portal_pages.yaml            门户页面归口登记表 (哪些页是产物/该不该随数据变, §10)
│    ├─ registry.yaml                配置登记表 (每个域是什么、谁读它、缺哪些)
│    ├─ farms\<场名>.yaml             外场配置 (**YAML**, 内置 rudong 可不配); _模板.yaml.example 是模板
│    │                               ★ 同目录还有一类 schema 不同的"机型/物理约束 profile"(meta+physical_constraints),
│    │                                 不是场定义, 不参与场加载 (见 §11)
│    ├─ contracts\                   机型契约 (变桨/偏航/发电机…判据参数)
│    ├─ canonical\ terms\            术语与词典 (canonical 决策/字典/手册; terms 显示映射)
├─ src\                              库源码: windscada(分析) / windcms(振动) / ontology(本体) / sop(方法)
├─ scripts\                          服务与工具: 网关/工作台/CMS + 摄入链 + 扫描/落位
├─ release\                          交付层: portal.html(门户) viewer(三维) sim_sys_server.py
│    └─ 如东\                         客户交付件 (治理清单/取数单), 按约定不入 git
├─ resources\oem_envision_sc1_rudong2014\   仿真回放资产
├─ reference\rudong\                 契约与语言资产 (windscada_contract.yaml / 英文语言库 / 报警码表 …)
├─ wheels\win_amd64\                 离线轮子 (随包分发, 不入 git)
├─ vendor\                           便携 Python / Ollama 离线包 (不入 git)
├─ docs\                             说明书、落位约定、振动接入与本文
└─ (不随包: `_修复记录_*`/临时目录 —— 本包已纳入 git, 变更历史由版本库承载)

2. data\raw\ 落位约定与扫描辨识

一句话: data\raw\ 的下一级目录就是场站名称, 系统在取数时扫描辨识, 不写死目录名。

辨识规则(逐条命中即止, 依据会打印出来, 见 scripts\scan_stations.py 与维护页那一行「场站数据目录 (扫描辨识)」):

# 规则 例
① 目录名与场配置 raw_station 完全相同 配置 "raw_station": "如东" ↔ 目录 data\raw\如东\
② 目录名与场名写法 src_farm_names 互为子串 配置 ["如海","如东"] ↔ 目录 如东海上风电场\ 也能认
③ data\raw\ 下只有这一个场站目录(单站部署) 采用它, 但报告里标"凭单站唯一性", 不假装精确匹配
④ 多目录且都不匹配 不猜: 报"未识别", 页面显示无数据并列出扫到的目录

约定子目录(这些名字是摄入接口, 改名等于换接口; 前五个是 SCADA/台账侧, 后两个是振动侧, 见 §2b):

子目录 放什么 文件形态要求
scada_10min\ SCADA 10min 导出 逐台平铺 WTG01.csv…WTG38.csv(放文件本身, 不是压缩包)
scada_1min\ 1min 导出(可选) 同上, 台号须在场配置的 38 台内
故障报警\ 报警事件导出 .xls 实为 SpreadsheetML(XML), <row> 需带 TimeOn + Alarmcode;「全年/年至今」累计快照件会自动跳过
风机故障记录\ 检修工单台账 模板表(表头含 机组编号+故障名称+故障代码);表里 风场名称 必须属本场(集团导出件混着十来个场);按年分组不限层级
油样报告\ 油液化验报告 .pdf, 文件名须含 日期_台号_部件(如 17072025_10303681_…_1#_主轴后.pdf)
windcms\ CMS 原始测量导出 + 厂商月度评估报告 导出为 测量根\<年>\<月>\<WTGxx>\*_decode.json(Brande TCM 导出, 目录层级不限); 报告为 .docx(可解析) 或 .pdf(扫描件只归档)
m5_cms_tcm\ TCM 侧深度分析报告 + handoff 正本 .docx 报告; 若现场给出 handoff_vibration_v2.json / component_history.json 正本, 放这里即被优先采用

机理层(厂商资料)按 A2 仍放 data\raw\西门子4.0技术资料\, 不在场站目录下。

怎么把现场包落成这个样子: scripts\place_raw_data.py 把"包里的哪个目录 → 落到哪"写成一张表, 可复核、可重跑(--dry-run 先看计划; 同尺寸文件自动跳过, 重跑不会把十几 GB 重抄一遍)。

.venv\Scripts\python.exe scripts\place_raw_data.py --src <现场包目录> --scope a2 --dry-run
.venv\Scripts\python.exe scripts\place_raw_data.py --src <现场包目录> --scope full
范围 落什么 依据
--scope a2(默认) 上表四类(scada_10min/故障报警/风机故障记录/油样报告) A2 约定的四项数据层
--scope mech data\raw\西门子4.0技术资料\(317 件 2.7 GB,含那份对译表)+ <场站>\scada_1min\(38 件 12.8 GB) 本体层构建器 kb_ingest.py 的 TECH 与 rd() 四个文件名;config.STATION_SUBDIRS 声明的 scada_1min
--scope vib <场站>\windcms\(CMS 原始测量导出 25,679 件 150 GB + 上海电气月度报告)+ <场站>\m5_cms_tcm\(大生科技 TCM 报告) §2b
--scope full 三组一起 从零重算要用的全集

为什么 scada_1min 也属"该落的": 它写在 config.STATION_SUBDIRS 里, scan_stations.py 会把它报成 "缺"。但包内没有消费者 —— 落它是补数据层完整性, 不改任何页面数值。同理, 现场包里的 scada数据(如东)\(19 个月原始通道导出)、fastlog数据\ 两种故意不落: 前者是 scada_10min 的上游且无人读, 后者全库只有 2 处注释提到 —— 理由都逐条写在脚本的 SKIPPED 表里。

★ 关于 (8)…\振动分析报告\(12 份月度用印版 PDF): v0.2.0 初版把它列在"故意不落"里, 理由是 "成品牌报告而非可再加工的测量数据, 且 windcms 要的测点索引包里没有"。那条判断在 2026-09-12 被推翻: 用户把 CMS 原始测量导出 (CMS_RuDong_CGN_202603-04.zip) 补进了现场包, 索引源件已具备, 于是 报告作为数据层证据一并落位(见 §2b)。脚本的 SKIPPED 表里保留了这条记录并标注了推翻原因。


2b. 振动侧落位与摄入链(2026-09-12 新增)

用户令: 「数据层里的「CMS 振动评估报告」应遵循 <安装目录>\data\raw\如东\windcms、 「振动线 handoff」应遵循 <安装目录>\data\raw\如东\m5_cms_tcm 存放; 修改系统支持振动数据参与运行、重算; 自现场包提取相应振动数据存放至上述目录」。

落位后的实际形态(--scope vib):

data\raw\如东\
├─ windcms\
│    ├─ CMS_RuDong_CGN_202603-04\          ← 原始测量导出原样保留包内层级 (可追溯"哪来的")
│    │    └─ measurement\2026\{03,04}\WTGxx\WTGxx_<uuid>_decode.json     25,679 件 / 150 GB
│    └─ 厂家报告\上海电气_月度\
│         ├─ 中广核如东海上风电场2025年06月…2026年05月振动分析报告用印版.pdf   12 件 (扫描件, 只归档)
│         ├─ 中广核如东海上风电场2026年07月振动分析报告_上海电气.docx          (有文本层, 摄入)
│         └─ _扫描件清单.json                                                (登记: sha256/大小/无文本层)
└─ m5_cms_tcm\厂家报告\
     └─ 中广核如东海上风电场传动链振动分析报告_大生科技_20260311(1).docx      (TCM M-system, 摄入)

摄入链(两条, 分工不同, 别混):

源 摄入命令 产出 谁消费
CMS 原始测量导出(*_decode.json) python scripts\vib_raw_build.py(= rudong_tcm_index.py → rudong_tcm_spectra.py;★不跑 windcms.py report —— 缺六层链产物时重生成会掉内容, 实测 report.md −97%, 见 docs\振动数据接入_v0.1.md §3b) outputs\rudong\m5_cms_tcm\windows\<窗>\index.parquet(54 列)+ …\<窗>\spectra\*.npz + spectra_meta.parquet src\windcms\data.py(窗/标量/谱图自动收录)、scripts\windcms.py report/serve、src\windscada\taxonomy.py(设备状态转录)
厂商 docx 评估报告 python scripts\vib_reports_build.py outputs\rudong\windcms\厂家报告提取_<报告期>.json + 报告_CMS振动状态评估报告_<报告期>.md;outputs\rudong\m5_cms_tcm\报告_TCM传动链振动分析_<报告期>.md 同上(报告 md 与自产报告同构, 消费端不必改代码)

窗名口径: wMMDD = 数据起始日(与既有 w0127/w0707/w0811 同口径, pipeline.window_year_gate 会拦"窗名无年份"的历史误用)。同一批数据重复摄入时, 已存在的窗会改名为 <窗>_reimport_<时分>, 而 data.EXCLUDE_DEFAULT 会把带 _reimport 的窗排除在生产集外 —— 重复摄入不会污染分析集(2026-08-24 的教训)。

这一步之后"振动数据就真的参与了"的证据链: data.windows() 自动发现新窗 → load_scalars() 把 新窗标量并进 CMS 报告 → data.spectrum() 能取到谱(此前 m5\spectra 整个目录缺失, 谱图是死的)→ fusion.windcms_grades() / taxonomy.system_matrix() 读最新报告 md 做设备状态转录。

仍然缺的(如实标注, 不冒充): 六层链的四步脚本 rudong_tcm_oem_scan.py / rudong_line_energy_share.py / rudong_model_run.py / rudong_fusion_run.py 未随包(原在振动线分支 claude/vibration-data-diagnosis-32b69e)。 vib_raw_build.py 产出的 outputs\rudong\m5_cms_tcm\vib_raw_manifest.json 里 missing_chain 字段列明这四项。 因此扫描线/能量占比/模型层/融合层那几类 parquet 仍走"包内 shipped 快照", 只有索引/谱/报告/知识库是可重算的。


3. 各目录的功用:谁在读、什么时候读

四类源的消费方式不同 —— 这决定了"放进去"和"用上"之间差哪一步:

源 运行期是否实时读 要跑什么才进系统 影响的页面/面板
scada_10min\ 是(windscada_serve.py 的 adhoc_query 请求时现读 WTGnn.csv) 派生产物需 rebuild_from_raw.py --scada 工作台「台内即时查询」等原始窗立即生效;损失/可用率/温度/偏航等派生面需重建
故障报警\ 否(只有摄入脚本读) scripts\windscada_alarms_ingest.py 数据层「报警事件」、故障分析、停机事件、限电绑定
风机故障记录\ 否 scripts\windscada_workorder_ingest.py 数据层「检修工单台账」、检修面、闭环验证
油样报告\ 否 scripts\windscada_watch_channels_build.py 数据层「油液化验」、油液时效胶囊、融合面油样轴
windcms\(CMS 原始导出) 否(窗是摄入时落盘的) scripts\vib_raw_build.py(索引→谱→报告/知识库) CMS 系统的谱图/标量与报告(windcms.py serve);taxonomy 的设备状态转录
windcms\/m5_cms_tcm\(厂商报告) 否 scripts\vib_reports_build.py 数据层的厂商评估报告转录(与自产报告同构的 md)

命令(在 <安装目录> 下跑):

.venv\Scripts\python.exe scripts\scan_stations.py            # 看扫到了什么、辨识依据
.venv\Scripts\python.exe scripts\rebuild_from_raw.py         # 跑三类台账摄入 (秒级~分钟级)
.venv\Scripts\python.exe scripts\rebuild_from_raw.py --scada # 再加 SCADA 侧 10 个构建器 (慢)
.venv\Scripts\python.exe scripts\rebuild_from_raw.py --verify# 与随包基线逐值等价验收
.venv\Scripts\python.exe scripts\place_raw_data.py           # 现场压缩包 → 约定子目录 (映射表)
.venv\Scripts\python.exe scripts\vib_raw_build.py            # 振动侧: CMS 原始导出 → 窗索引/谱/报告

4. data\raw 能重建什么、不能重建什么(实测, 2026-09-11)

outputs\rudong\windscada\ 是页面取数的产物仓。其中哪些能由 data\raw 重算出来, 是"数据驱动程度"的边界:

✅ 能从 data\raw 重算(构建器都在包内, 已实测等价)

产物 来源 入口
alarms.parquet 故障报警 windscada_alarms_ingest.py(39211/39211 行逐值一致)
workorders.parquet 风机故障记录 windscada_workorder_ingest.py(1574 种内容复现 1566 种, 8 种差异已逐条定位)
oil_samples_index.parquet 油样报告 windscada_watch_channels_build.py——部分可重建: 404/506 行(SGS 批, 文件名可解析)逐值一致; 见下表缺口
powercurve_dev/bins · loss_monthly · curve_lenses/liveness · control_profile/schedule · stop_events · temp_bins · yaw_daily · hydraulic_accum · thermal_chain · system_aux scada_10min + alarms rebuild_from_raw.py --scada(实测: loss_monthly 3729/3729、powercurve_dev 38/38、powercurve_bins 912/912 与随包基线逐值完全一致)
objects.json(本体 2340 个对象)· retrieval_index.json 厂商技术资料 data\raw\西门子4.0技术资料(不在场站目录下) python -m src.ontology.kb_ingest;检索索引 python -c "from src.ontology import retrieval as R; R.build(use_vec=False)"(BM25 词法索引; 向量那半要 Ollama 的 bge-m3, 本机没装模型时会跳过并保持纯词法可用)
turbine_params.parquet(1706 条整定值) 同上, Turbine+Parameters.*.xlsx python -c "from src.ontology.maintenance import refresh_params as f; f()"
m5_cms_tcm\windows\<窗>\index.parquet(54 列标量索引) CMS 原始测量导出 data\raw\如东\windcms\**\*_decode.json scripts\rudong_tcm_index.py(旧包缺这个脚本 → 2026-09-12 补齐; 列名列序 dtype 与包内 tcm_index.parquet 逐列对齐, 实测 FFT 行取值逐格一致)
m5_cms_tcm\windows\<窗>\spectra\*.npz + spectra_meta.parquet 同上 scripts\rudong_tcm_spectra.py(DataSets.DataSet.Values 是空格分隔字符串: Size=Lines+1, X-axisDelta=带宽/Lines)
windcms\报告_CMS振动状态评估报告_<报告期>.md(厂商报告转录) 厂商 docx 评估报告 scripts\vib_reports_build.py(合并单元格还原 + 厂家总览句作校验和: 实测 34优秀/3良好/1无数据逐项一致)

本体层这三件的前提是技术资料在盘上(place_raw_data.py --scope mech 会落, 见 §2);技术资料不在时 kb_ingest 会打印 ⚠ 源缺失 并降级 —— 不静默。

⛔ 包内没有生成端 / 源件不在数据包内(只读不写, 因此 data\raw 换数据不会更新它们)

产物 谁在读它 说明
oil_samples_index 里的 102 行华标 2026-07 批 数据层「油液化验」、油液时效胶囊 源件是一份合并报告 BG-2026-07-YP013 中广核新能源如东海上风电场.pdf(一份覆盖多台, 台号/部件只在 PDF 表格里), 且该文件不在现场数据包里 → 清空重建会丢这 102 行。摄入脚本因此采取合并语义: 认不出的老行原样保留、绝不删(场景② 实测就出现了 506→404 的落差, 需现场补那份 PDF 才能从 raw 全量重建)
pc_monthly_bins · duty_monthly · thermal_monthly · sector_power 趋势件/热链/扇区 组级产物, v0.2.0 未附构建脚本(全库只有读取方, 0 处写入方)
yaw_dynamic_monthly · yaw_press_monthly · yaw1min_liveness · yaw_err_clean · genbearing_monthly · mblub_monthly 偏航/温度/润滑面 同上
structure.parquet · watch_channels_monthly.parquet 结构面/温度联动/融合 后者连包内注释都写着"须重建 scripts\windscada_watch_channels_build.py" —— 本版该脚本只产油样索引, 未含 watch_channels 月表
windcms\* · m5_cms_tcm\* · tcm_compatible_replay\* CMS/融合/TCM 兼容链 来源是振动线 handoff(CMS 测点索引); 现场包里只有成品牌报告 PDF, 没有测点数据 → 仍 ⛔(/cms/ 因此 503)

本体层的可重算性取决于技术资料在不在: kb_ingest.py 的 TECH = P.RAW_ROOT/'西门子4.0技术资料', 且 rd() 要读 故障处理/故障处理手册.xlsx、广核如海风电场西门子风机故障代码中英文对译表.xlsx、 如海故障代码表.xlsx、维护相关/维护作业指导书.xlsx 四个文件 —— 缺任一个都会打印 ⚠ 源缺失 并降级, 所以 place_raw_data.py --scope mech 连那份不在技术资料目录里的对译表都单独补了一条规则。 本体层的其它入口 (populate / chain_ingest / trend_ingest / scenario_29) 要从 L1 产物转录 (判级矩阵 system_matrix/fusion_table、振动 handoff、链盘 /api/fleet), 而 L1 那几件本身 ⛔, 所以从零重算时它们要么报错退役、要么无变化 —— populate 实测卡在 pitch\pitch_daily.parquet。

要补什么、找谁补: 见 docs\重算缺口与补件清单_v0.1.md —— 按"源件缺失(现场能补)"与 "生成端缺失(研发补脚本)"两类逐项列了证据路径、影响面与补齐判据。 手工重算怎么敲: 见 docs\重算操作手册_v0.1.md(逐步命令 + 期望输出 + 会踩的坑)。

从零重算实测(2026-09-11,只用 data\raw,不吃随包产物)

环节 结果
三门台账 报警 0→39211 行 · 工单 0→5876 行 · 油样 0→404 行(唯一源件数 80, 其中 2 张非台账表跳过)
SCADA 侧 10 个构建器 全跑通(loss_monthly 3729 · temp_bins 24624 · yaw_daily/hydraulic_accum/thermal_chain/system_aux 各 38 台 · curve_lenses · control_profile/schedule · stop_events · powercurve_dev/bins)→ L0 仓 16 件 1.78 MB(随包是 118 件:差额就是 §4 第二张表那些 ⛔ 件)
本体层 objects.json 1.43 MB + retrieval_index.json 0.62 MB + turbine_params.parquet
等价验收 --verify alarms 39211/39211 逐值完全一致; workorders 8 行差异全部可归类(旧链时间解析缺陷)+ 69 行本链新增覆盖; 1 处人工项 = 油样 506→404(那 102 行的源件不在包内)

5. 两个验收场景(已实测, 2026-09-11 14:2x)

场景① 清空 data\raw + 挪开产物 → 系统无数据呈现 ✅ 通过

Move-Item .\data\raw\如东 <暂存目录>            # 数据挪走
# 把「这四类源产出的产物」也挪走 (只清源不动产物, 页面照旧显示旧数 —— 设计如此, 见 §3)
Move-Item .\outputs\rudong\windscada\{alarms,workorders,oil_samples_index,loss_monthly,powercurve_bins,powercurve_dev}.parquet <暂存目录>\
.venv\Scripts\python.exe guanlan.py stop; .venv\Scripts\python.exe guanlan.py serve

实测结果:

检查项 结果
scan_stations.py 扫描到 0 个场站目录;辨识结论 how=none;依据: “data\raw 下没有任何场站目录 → 系统无数据可呈现”
维护页「数据层」 「场站数据目录 (扫描辨识)」存在=False / 覆盖=—;四项 条数=0 / 覆盖=— / 存在=False
工作台实时原始窗 /api/query 首次暴露 HTTP 500(异常直接抛给前端,人分不清"没放数据"还是"程序坏了")→ 已修: 改为 HTTP 200 + 结构化无数据 err=no_source,并给出"本机应在 <…>\scada_10min\WTG01.csv、放入后重跑 rebuild_from_raw.py --scada"

场景② 按约定结构放回 data\raw → 系统有数据呈现 ✅ 通过

Move-Item <暂存目录> .\data\raw\如东
.venv\Scripts\python.exe scripts\rebuild_from_raw.py        # 三类台账 (实测: 报警 0→39211, 工单 0→5876, 油样 0→404)
# loss_monthly 另跑 SCADA 两步 (powercurve 31s + availability 88s)
.venv\Scripts\python.exe guanlan.py serve

实测结果:

检查项 结果
scan_stations.py 扫描到 1 个场站目录: 如东 (592 件);how=raw_station(依据: 目录名与场配置 raw_station 完全相同);四个子目录件数 38 / 16 / 134 / 404
维护页「数据层」 扫描行 覆盖=如东 / 存在=True;10min 2025-01 ~ 2026-07 / 3729;报警 39211;工单 5876;油样 506(恢复完整件后)
工作台实时原始窗 err=None, months=7, series 7 点 —— 数据回来了
产物等价 loss_monthly 3729/3729、powercurve_dev 38/38、powercurve_bins 912/912、alarms 39211/39211 逐值完全一致;workorders 5876 行仅 1 条记录有纳秒尾差(同一时刻 …16:23:00.0000015 vs …0000010);oil_samples_index 需恢复那 102 行(见 §4)

注意: 清 data\raw 不会自动清产物 —— 这是刻意的(产物是"上一次算好的结果", 现场拔盘不该让页面变空)。要验证"无数据呈现", 必须同时挪开产物, 如上。


6. 产物地图(谁生成、谁能重算、谁在读)

产物统一在 outputs\<场名>\ 下(当前只有 rudong),按"线"分仓;门户与交付件在 release\:

仓 件数/体积 功用 生成端 能否由 data\raw 重算 谁在读
outputs\<场>\windscada\ 118 件 5.6 MB L0 标准仓(34 个 parquet + index.html + build 日志 + turbines\ review\) scripts\rebuild_from_raw.py(三类摄入 + --scada 十个构建器) ✅ 大部分(见 §4) 工作台、分台页、网关 /release/、所有分析面
outputs\<场>\ontology\ 24 件 100 MB 本体对象库 objects.json + 检索索引 retrieval_vec.npy + release_r1/r2 python -m src.ontology.kb_ingest / retrieval / chain_ingest …(读 data\raw\西门子4.0技术资料 与仓) ⛔(源是厂商资料,不在这四类里) 图谱检索、问答、台账对象
outputs\<场>\m5_cms_tcm\ 90 件 53.5 MB 振动线 handoff、TCM 兼容件、窗/谱/模型产物 振动线(windcms 会话)出件 ⛔ 融合面、台账判级、验收件
outputs\<场>\windcms\ 53 件 27.7 MB CMS 报告与单机页(HTML×41) windcms analyze ⛔ 数据层「CMS 振动评估报告」、温度/振动互证
outputs\<场>\tcm_compatible_replay\ 59 件 32.7 MB TCM 兼容链模型表(mask_thresholds.tsv 等) 振动线 ⛔ windcms 配置引用
outputs\<场>\sop\ 210 件 15.8 MB SOP 中间件/评审/台账 + 22 个历史装配脚本 src\sop\farm_pipeline.py 等 ⛔(历史脚本含 mac 死路径,仅留档) 判据链、事实契约的输入
outputs\<场>\paradigm_r1\ 29 件 0.5 MB 范式实验底稿(E3/E5/E8…) 实验脚本 ⛔ 事实契约的 source_refs
outputs\<场>\guanlan\ 5 件 0.5 MB 事实契约 facts_contract_v0.json + derived\ + cloud\(可上云面孔) scripts\guanlan_facts_contract.py 等 ⛔ 问答引文、门户契约段
outputs\<场>\pitch\ 3 件 0.4 MB 变桨面日粒度(液压/润滑/柱塞/运行段)+ 零位三口径月表 + 口径说明 scripts\pitch_face_build.py(重算链 ④c;读 data\raw\<场>\scada_10min + scada_1min) ✅(2026-09-19 起有生成端) 变桨面判级(src\windscada\subsys\pitch.py)
release\ — 门户 portal.html(20.2 MB 装配产物)、三维 viewer\、sim_sys_server.py、交付件 如东\、受管外壳 portal_src\ scripts\portal_build.py(拆 / 装) ⛔ 网关首页、三维页、交付
logs\ run\ — 运行日志、pids.json 启动器/服务 — 排障

"能重算"这一列才是关键:data\raw 换数据只会更新 ✅ 那几项;其余要么来自别的源(厂商资料/振动线),要么包内没有生成端(见 §4 的 ⛔ 表)。

门户的"壳/件分离"(2026-09-11):release\portal.html 20.23 MB 里 99.3% 是内嵌的交付文档正文 (28 个 <template>:治理清单分册、单机与整机报告、仿真台面板),只有 138 KB 是门户自己的壳。因此:

  • release\portal_src\shell.html(138 KB,含 <!--@TEMPLATE:id--> 与 <!--@GOVERNANCE_SOURCES--> 标记)、 manifest.json(各件 sha256 + 期望门户 sha256)、README.md —— 入库;
  • release\portal_src\templates\(20.04 MB 交付件正文)、governance_sources.json、release\portal.html —— 按产物处理(.gitignore,磁盘文件随包分发、服务照读);
  • scripts\portal_build.py --extract / (默认装) / --verify / --check;--verify 实测"拆→装"回到 逐字节相同的文件(sha256 9b6aabeb6ca18d15…,20,226,052 B)。行尾必须是 LF —— 三个相关脚本 都用字节级写文件(newline=""),装配后另有 CRLF 自检。
  • 契约结论段 <section id="contract-claims"> 是产物(由 guanlan_portal_inject_claims.py 在装配最后注入, 内容来自 outputs\<场>\guanlan\derived\portal_claims.json)—— 产物挪走时装出的门户会缺这一段,属预期。

7. 跨平台部署与相对路径约定(Windows / Linux)

三条硬约定(用户令 2026-09-11;由 scripts\check_portability.py 自动门禁):

  1. 只写相对路径 —— 相对安装根,不写机器相关绝对路径(/Users/…、/Volumes/…、C:\……)。 整包拷到别的电脑/别的盘、或换 Linux,都不需要改任何路径。
  2. 相对基准是安装根,不是 cwd —— 一切路径经 src\paths.py 解析;Path('outputs/…') 这种 cwd 相对写法 会让"从别处调用同一个脚本"指到别的地方(本仓实测曾有 20+ 处,已全部归到 paths)。
  3. 写进产物/清单/页面的路径用 POSIX 相对形式(outputs/rudong/…,见 P.rel());电脑间传输不受 分隔符影响。显示给人看时才用本机分隔符(P.disp() / P.disp_dir())。

唯一真源:src\paths.py

from src import paths as P
P.ROOT            # 安装根 (env WINDSCADA_ROOT → 否则按 src/paths.py 位置回溯)
P.RAW_ROOT        # data/raw (离线数据入口)
P.store()         # outputs/<场>/windscada      P.ont() / P.cms() / P.m5() / P.sop() / P.guanlan() / P.pitch()
P.guanlan()/…     P.tcm_replay()                P.RELEASE / P.PORTAL / P.VIEWER / P.SIM_DIR / P.LOGS / P.RUN
P.rel(p)          # → 'outputs/rudong/windscada/…' (POSIX 相对, 用于清单/契约)
P.disp(p)         # → '<安装目录>/…' (显示)
P.venv_python()   # 跨平台探测 .venv/Scripts/python.exe 或 .venv/bin/python

configs\serve.json 的 python 现在写相对路径(.venv/Scripts/python.exe;Linux 为 .venv/bin/python)—— 换机后由启动器按安装根解析;也支持绝对路径与"留空自动探测"。

换机部署清单

# 目标机: 拷整个安装目录 → 跑安装 → 自检 → 回归比对
.venv\Scripts\python.exe scripts\check_portability.py   # 1) 静态门禁: 无机器路径/无 cwd 相对
.venv\Scripts\python.exe guanlan.py check               # 2) 依赖/产物/端口自检
.venv\Scripts\python.exe scripts\scan_stations.py       # 3) 场站目录扫描辨识 (数据认到了没)
.venv\Scripts\python.exe scripts\page_fingerprint.py --diff <原机导出的指纹.json>   # 4) 与基准机逐端点比对

第 4 步是换机验收的硬证据:指纹对每组页面/接口取 status + 归一化 sha256(自动剔掉 checked/ms/repo_head 这类设计上会变的字段,并把安装根前缀归一成 <ROOT>),因此"换了机器所以前缀不同" 不会误报,而"内容真的不对"一定报。基准机导指纹:page_fingerprint.py --save <文件名>。

已知例外(check_portability.py 的 ALLOW 里逐项写了理由):src\sop\farm_paths.py(它就是盘符映射表本体)、 scripts\sim_hub\*(开发期生成器)、outputs\rudong\sop\*.py(历史装配脚本留档)、若干文档字符串/证据文本 里的路径引用,以及交付侧离线工具(scripts\guanlan_cloud_* 等,ROOT 相对但尚未收敛为 paths,属 WARN 级)。

8. 变更记录

日期 变更
2026-09-11 A2: 四项数据源改为 data\raw\<场站名称>\…
2026-09-11 场站目录改为扫描辨识(config.scan_stations() / station_scan(),不再写死目录名),维护页新增「场站数据目录 (扫描辨识)」一行
2026-09-11 补三类摄入(报警/工单/油样)+ 总入口 rebuild_from_raw.py + 等价验收 --verify;新增 scan_stations.py、place_raw_data.py
2026-09-11 无数据时实时接口不再抛 500,改为结构化"无数据"回答(场景① 逮到)
2026-09-11 两个验收场景实测通过(见 §5);记录油样 102 行华标批的源件缺口(见 §4)
2026-09-11 路径跨平台化:新增 src\paths.py 唯一真源;清理机器绝对路径(mac 检出/TCM 主仓/写死解释器)与 20+ 处 cwd 相对路径;serve.json 的 python 改相对;新增静态门禁 check_portability.py 与回归尺子 page_fingerprint.py;改造前后 10 个端点指纹逐字节一致;补本文件 §6 产物地图与 §7 跨平台约定
2026-09-11 门户拆包:release\portal.html 拆成受管外壳 release\portal_src\shell.html(138 KB) + 不入库内嵌件 templates\(28 件 20.04 MB)与 governance_sources.json;新增 scripts\portal_build.py(--extract/--verify/--check),实测拆→装逐字节一致(见 §6);配套 git rm --cached release\portal.html
2026-09-11 验证闸门退出码修复:控制台 GBK 编不出 ✔ 时降级为 ?(新增 src\console.py),修 page_fingerprint / check_portability / rebuild_from_raw / scan_stations / portal_build —— 此前"全部一致"会因 print 抛异常而退出码 1(通过被报成失败)
2026-09-11 从零重算实测 + 落位扩范围:place_raw_data.py 新增 --scope a2\|mech\|full(机理层技术资料 + 对译表 + scada_1min)、修"单文件规则恒不落件"与缺 import os(自 d90da05 起脚本跑不通)、同尺寸文件跳过;src\ontology\ 全层 10 处文本 I/O 补 encoding='utf-8'(中文 Windows 上本体层原本根本不可用:默认 cp936 写 objects.json 崩在 \u200b);check_portability.py 新增 WARN 项 6「文本读写缺 encoding=」;实测数字见 §4 末
2026-09-11 收尾这 18 处同类文本 I/O(scripts\ontology_p2_verify.py、scripts\windscada_serve.py、src\ontology\scenario_29.py、src\windcms\{knowledge,pipeline,plugins}.py、src\windscada\subsys\fusion.py、src\windscada\taxonomy.py),门禁 WARN 项 6 扫描面同时扩到 open('w') 与 open(p)(无 mode = 文本读);WARN 归零
2026-09-12 振动侧落位与摄入(本文 §2b):data\raw\如东\ 新增 windcms(CMS 原始测量导出 25,679 件 150 GB + 上海电气月度报告) / m5_cms_tcm(大生科技 TCM 报告) 两个约定子目录(config.STATION_SUBDIRS 同步);补齐振动线分支缺失的摄入端 scripts\rudong_tcm_index.py(54 列窗索引, 与包内 tcm_index.parquet 同构)与 scripts\rudong_tcm_spectra.py(npz 谱库 + spectra_meta.parquet);新增一键 scripts\vib_raw_build.py(索引→谱→报告/知识库, 并行+窗体清单)与厂商报告摄入 scripts\vib_reports_build.py;place_raw_data.py 新增 --scope vib(含散装件规则与每次开包一次的写法修正: 旧写法对 2.5 万条目是平方复杂度);rebuild_all.py 新增 ④b 步;check_transferable.py 文本扫描排除 data\raw\(现场原始件不随包, 扫它只会把秒级闸门拖成小时级)。细节见 docs\振动数据接入_v0.1.md
2026-09-12 不再随包"修复记录/临时"类目录(用户令: 本包已纳入 git 管理): pack_dist.py 的 INCLUDE_DIRS 去掉 _修复记录_20260911(其内容并入 docs\振动数据接入_v0.1.md 与本文 §2b), 相关文档/README 的引用一并改指正式文档
2026-09-12 振动侧实测复核(见 docs\振动数据接入_v0.1.md): 落位 25,693 件/150.2 GB; 摄入出窗 w0316(2,066,686 行 × 54 列 + 420,742 条谱, 全链 875 s); data.windows/load_scalars/spectrum 三处消费实测通过(谱图此前是死的)。★ 同时逮到一条坑: 在本包跑 windcms.py report 会用残缺输入覆盖完整快照(report.md −97%)—— 因为六层链的 model_run/fusion 产物缺失, 故该步改为 --with-report 显式开启, outputs\rudong\windcms 已逐字节还原为随包快照(56 件/0 差异)。新增 src\derived_manifest.py(产物来源自登记)让台账认出新造件