# 观澜 · 如东样板 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技术资料\`, **不在场站目录下**。 --- ## 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 与随包基线**逐值完全一致**) | **⛔ 包内没有生成端 / 源件不在数据包内**(只读不写, 因此 `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 未附构建脚本 | | `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 月表 | | `objects.json`(本体)· `windcms\*` · `m5_cms_tcm\*` | 图谱/CMS/融合 | 来源是 `data\raw\西门子4.0技术资料` 与振动线, **不属于**上面四类 | --- ## 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`(19 MB)、三维 `viewer\`、`sim_sys_server.py`、交付件 `如东\` | `scripts\guanlan_portal_*.py` 等 | ⛔ | 网关首页、三维页、交付 | | `logs\` `run\` | — | 运行日志、`pids.json` | 启动器/服务 | — | 排障 | > **"能重算"这一列才是关键**:`data\raw` 换数据只会更新 ✅ 那几项;其余要么来自别的源(厂商资料/振动线),要么包内没有生成端(见 §4 的 ⛔ 表)。 ## 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` ```python 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`)—— 换机后由启动器按安装根解析;也支持绝对路径与"留空自动探测"。 **换机部署清单** ```powershell # 目标机: 拷整个安装目录 → 跑安装 → 自检 → 回归比对 .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` 这类设计上会变的字段,并把安装根前缀归一成 ``),因此"换了机器所以前缀不同" 不会误报,而"内容真的不对"一定报。**基准机**导指纹:`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 跨平台约定 |