# 观澜 · 如东样板 v2 目录结构设计与落位约定 > 版本: v0.2.0 现场版 · 2026-09-11 > 适用: `F:\temp\guanlan-rudong-v2_0.2.0`(安装目录, 下称 `<安装目录>`) > 本文回答三件事: ①离线数据往哪放、系统怎么认;②每个目录干什么用;③放/不放数据时系统分别表现什么。 --- ## 1. 一页速览 ``` <安装目录>\ ├─ data\raw\ ★离线数据唯一入口 (现场数据只放这里) │ └─ <场站名称>\ ← 扫描辨识的抓手: 下一级目录就是场站名 │ ├─ scada_10min\ SCADA 10min 导出 (逐台 WTG01.csv…WTG38.csv) │ ├─ scada_1min\ (可选) 1min 导出 │ ├─ 故障报警\ 报警事件导出 (SpreadsheetML *.xls) │ ├─ 风机故障记录\ 检修工单台账 (*.xls/xlsx, 内按 {年}年故障记录\ 分年) │ └─ 油样报告\ 油液化验报告 (*.pdf) ├─ 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\ 端口/模型/场配置/机型契约 │ ├─ serve.json 端口、发布目录、Python 路径 (install 脚本写) │ ├─ models.json Ollama 档位与 pull 命令 │ ├─ farms\<场名>.json 外场配置 (内置 rudong 可不配); _模板.json.example 是模板 │ ├─ 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\ 说明书与本文 ├─ logs\ run\ 运行日志 / pids.json (不入 git) └─ _修复记录_20260911\ 现场修复记录与可重放脚本 ``` --- ## 2. `data\raw\` 落位约定与扫描辨识 **一句话**: `data\raw\` 的**下一级目录就是场站名称**, 系统在取数时**扫描辨识**, 不写死目录名。 辨识规则(逐条命中即止, 依据会打印出来, 见 `scripts\scan_stations.py` 与维护页那一行「场站数据目录 (扫描辨识)」): | # | 规则 | 例 | |---|---|---| | ① | 目录名与场配置 `raw_station` **完全相同** | 配置 `"raw_station": "如东"` ↔ 目录 `data\raw\如东\` | | ② | 目录名与场名写法 `src_farm_names` **互为子串** | 配置 `["如海","如东"]` ↔ 目录 `如东海上风电场\` 也能认 | | ③ | `data\raw\` 下**只有这一个**场站目录(单站部署) | 采用它, 但报告里标"凭单站唯一性", 不假装精确匹配 | | ④ | 多目录且都不匹配 | **不猜**: 报"未识别", 页面显示无数据并列出扫到的目录 | 约定子目录(这五个名字是摄入接口, 改名等于换接口): | 子目录 | 放什么 | 文件形态要求 | |---|---|---| | `scada_10min\` | SCADA 10min 导出 | **逐台平铺** `WTG01.csv…WTG38.csv`(放文件本身, 不是压缩包) | | `scada_1min\` | 1min 导出(可选) | 同上, 台号须在场配置的 38 台内 | | `故障报警\` | 报警事件导出 | `.xls` 实为 SpreadsheetML(XML), `` 需带 `TimeOn` + `Alarmcode`;「全年/年至今」累计快照件会自动跳过 | | `风机故障记录\` | 检修工单台账 | 模板表(表头含 `机组编号`+`故障名称`+`故障代码`);表里 `风场名称` 必须属本场(集团导出件混着十来个场);按年分组不限层级 | | `油样报告\` | 油液化验报告 | `.pdf`, 文件名须含 `日期_台号_部件`(如 `17072025_10303681_…_1#_主轴后.pdf`) | 机理层(厂商资料)按 A2 仍放 `data\raw\西门子4.0技术资料\`, **不在场站目录下**。 **怎么把现场包落成这个样子**: `scripts\place_raw_data.py` 把"包里的哪个目录 → 落到哪"写成一张表, 可复核、可重跑(`--dry-run` 先看计划; 同尺寸文件自动跳过, 重跑不会把十几 GB 重抄一遍)。 ```bat .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 full` | 两组一起 | 从零重算要用的全集 | > 为什么 `scada_1min` 也属"该落的": 它写在 `config.STATION_SUBDIRS` 里, `scan_stations.py` 会把它报成 > "缺"。但**包内没有消费者** —— 落它是补数据层完整性, 不改任何页面数值。同理, 现场包里的 > `scada数据(如东)\`(19 个月原始通道导出)、`fastlog数据\`、`(8)…\振动分析报告\` 三种**故意不落**: > 前者是 `scada_10min` 的上游且无人读, 中间那种全库只有 2 处注释提到, 后一种是振动线的成品牌报告 > 而非可再加工的测量数据 —— 理由都逐条写在脚本的 `SKIPPED` 表里。 --- ## 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` | 数据层「油液化验」、油液时效胶囊、融合面油样轴 | 命令(在 `<安装目录>` 下跑): ```powershell .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 # 现场压缩包 → 约定子目录 (映射表) ``` --- ## 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()"` | > 本体层这三件的前提是**技术资料在盘上**(`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` + 挪开产物 → 系统无数据呈现** ✅ 通过 ```powershell Move-Item .\data\raw\如东 F:\temp\_raw_off # 数据挪走 # 把「这四类源产出的产物」也挪走 (只清源不动产物, 页面照旧显示旧数 —— 设计如此, 见 §3) Move-Item .\outputs\rudong\windscada\{alarms,workorders,oil_samples_index,loss_monthly,powercurve_bins,powercurve_dev}.parquet F:\temp\_prod_off\ .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` → 系统有数据呈现** ✅ 通过 ```powershell Move-Item F:\temp\_raw_off .\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\` | 2 件 0.3 MB | 变桨零位月表 | 变桨线 | ⛔ | 变桨面 | | `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 个 `