#!/usr/bin/env python3 # -*- coding: utf-8 -*- r"""一条命令重算全部 (2026-09-12) —— 把散在各脚本里的重算链按依赖顺序串起来。 ## 为什么要有它 "从零重算"原本是六七条命令、顺序还不能错 (产物齐了本体层才跑得动, 本体层跑完才能拷随包件…), 手工执行容易漏步、也说不清"到底重算了什么"。本脚本把顺序、依赖、验收锚点全写死在这里。 ## 顺序 (为什么是这个顺序) ① 放数据 place_raw_data.py --scope full (给了 --src 才跑; 同尺寸自动跳过) ② 三门台账 rebuild_from_raw.py 报警/工单/油样 ③ SCADA 侧 rebuild_from_raw.py --scada 10 个构建器, 逐台读 ~14 GB, 约 15 分钟 ④ 月度派生件 windscada_monthly_build.py 能从 raw 重算的月表(与随包件逐值对齐才落盘) ⑤ 补齐缺件 products_restore_missing.py 包内没有生成端的那批(只补缺, 不覆盖重算件) ↑ 必须在 ⑥ 之前: 本体层的 populate/chain_ingest 要吃这些 L1 产物 ⑥ 重启服务 guanlan.py stop/serve chain_ingest 要从 /api/fleet 取链盘 ⑦ 本体层 kb_ingest → populate → chain_ingest → trend_ingest → retrieval → refresh_params ⑧ 验收 ontology.audit + rebuild_from_raw --verify (可选) + 页面对照 ## 用法 python scripts/rebuild_all.py # 全套 (会跑 SCADA 那 15 分钟) python scripts/rebuild_all.py --skip-scada # 只换台账时的常用档 python scripts/rebuild_all.py --src <现场包目录> # 连"放数据"一起 python scripts/rebuild_all.py --no-restart # 不碰服务(自己管) python scripts/rebuild_all.py --with-verify # 末尾加等价验收(需随包基线在位) python scripts/rebuild_all.py --dry-run # 只打印计划 """ from __future__ import annotations import argparse import os import pathlib import subprocess import sys import time ROOT = pathlib.Path(__file__).resolve().parents[1] PY = sys.executable def step_cmd(name, cmd, tolerate=(), note=''): """tolerate: 允许的退出码 (不算失败); note: 该步非零退出时打给人看的解释(按步, 别用一句通用话糊过去)。""" return (name, cmd, tuple(tolerate), note) def build_plan(a) -> list: plan = [] if a.src: plan.append(step_cmd('① 放数据', [PY, 'scripts/place_raw_data.py', '--src', a.src, '--scope', 'full'])) plan.append(step_cmd('② 三门台账', [PY, 'scripts/rebuild_from_raw.py'])) if not a.skip_scada: plan.append(step_cmd('③ SCADA 侧 10 个构建器 (约 15 分钟)', [PY, 'scripts/rebuild_from_raw.py', '--scada'])) # ④ 月度派生件: `windscada_monthly_build.py` 会拿随包基线**逐值比对**, 没有基线时它返回 5 # ("无法比对 —— 这不是通过", 这是它的诚实口径, 不要改它的退出码)。 # ★但"重算链"与"等价验收"是两件事: 基线只是**验收标准答案**, 缺它不该让整条链停在第④步 # (2026-09-16 实逮: 基线目录被清理后, 从页面点"执行重算" 387 s 后 rc=1 断在这里, ⑦⑧ 全不执行)。 # 故这里容忍 5 并写清"跳过了什么"; 要真做验收, 先把随包件恢复成暂存区/基线(见 products_state.py)。 plan.append(step_cmd('④ 月度派生件', [PY, 'scripts/windscada_monthly_build.py'], tolerate=(5,), note='rc=5 = 找不到随包基线可比对 ⇒ **跳过等价验收**(不是通过)。' '要恢复验收能力: 把随包产物放回 _products_off*/<场>/ 或 ' 'outputs/<场>/windscada/_pre_rebuild_20260911/')) if not a.skip_vib: # 振动侧 (2026-09-12 用户令"振动数据参与运行、重算"): 原始导出的 Brande TCM JSON # (data/raw/<场站>/windcms/**/*_decode.json) → 窗索引 + 谱库。消费者 (src/windcms/data.py) # 自动发现新窗, 所以这一步之后 CMS 的谱图/标量/页面就带上了新数据。 # ★**不跑** `windcms.py report` —— 本包缺六层链的 model_run/fusion 产物, 重生成会用残缺输入 # 覆盖随包快照 (report.md 16,857 B → 467 B, overview.html −15%; 实测见 vib_raw_build.py 文件头)。 # 六层链补齐后应把该步改为带 --with-report。 # 现场没有振动原始件时本步**空跑退出 0** (不是每台机器都有振动导出, 不该报失败)。 plan.append(step_cmd('④b 振动侧摄入 (原始导出 → 窗索引/谱)', [PY, 'scripts/vib_raw_build.py'] + (['--with-report'] if a.vib_report else []), note='没有振动原始件时空跑属正常; 若解析出错会以非零退出 (不静默)')) # ⑤ 补齐"包内没有生成端"的产物: 源件是**暂存区**(清除产物时挪出来的那份随包件)。 # 暂存区不存在时该步返回 6 (不是崩溃) —— 首次全量重算/清完又还原过的机器上都没有暂存区, # 若不容忍就会把后面的 ⑦本体链 与 ⑧审计 一起打断 ("从零重算"整条链跑不完; 2026-09-16 实逮)。 plan.append(step_cmd('⑤ 补齐"包内没有生成端"的产物', [PY, 'scripts/products_restore_missing.py'], tolerate=(6,), note='rc=6 = 没有随包件来源, 该步跳过。★2026-09-16 用户令"清除产物不留备份"之后, ' '随包件不再保留 _products_off/ 暂存区; 要补齐请显式给交付包: ' 'scripts/products_restore_missing.py --stash <交付包.zip> (或在 --src 的现场包场景下先补齐)')) if not a.no_restart: # 只重启**组件服务**, 网关留着 —— 两个理由: # ① 控制台页面是网关提供的, 连它一起停的话, 从页面点的重算会把自己的界面(甚至自己)弄没; # ② rebuild_all 自己可能就是从控制台/网关起的任务, `guanlan.py stop` 用 taskkill /T, # 连网关会**沿父子树把正在执行的重算一起杀掉**(2026-09-12 实测: 日志停在 stop 那行, 之后全无)。 # 组件停掉再 serve 就够: chain_ingest 要的是 detail(18033) 重新加载新产物, 网关不缓存产物。 plan.append(step_cmd('⑥ 重启组件服务 (保留网关, stop)', [PY, 'scripts/_ops_stop_keep_gateway.py'])) plan.append(step_cmd('⑥ 重启组件服务 (serve)', [PY, 'guanlan.py', 'serve'], tolerate=(1,), note='serve 在"有模块 DOWN"时故意返回 1 —— degraded 是正常状态(例如本机模型未启动)')) plan.append(step_cmd('⑦ 本体: 码表/手册/文档', [PY, '-m', 'src.ontology.kb_ingest'])) plan.append(step_cmd('⑦ 本体: 铺开判级与台账', [PY, '-m', 'src.ontology.populate'])) plan.append(step_cmd('⑦ 本体: 决策链进度', [PY, '-m', 'src.ontology.chain_ingest'])) plan.append(step_cmd('⑦ 本体: 在升/闭环证据', [PY, '-m', 'src.ontology.trend_ingest'])) plan.append(step_cmd('⑦ 本体: 检索索引', [PY, '-c', 'from src.ontology import retrieval as R; R.build(use_vec=False)'])) plan.append(step_cmd('⑦ 本体: 实机参数表', [PY, '-c', 'from src.ontology.maintenance import refresh_params as f; print(f())'])) plan.append(step_cmd('⑧ 本体审计 (期望 0 问题)', [PY, '-m', 'src.ontology.audit'])) if a.with_verify: # 只有 4 = "有需人工看的差异"(本包已知项: 油样 102 行的源件不在现场包里) 才容忍; # 5 = 找不到基线(等于没验收) 必须当失败 —— 2026-09-12 就是因为放过了它, 出现过"验收没做却报通过"。 plan.append(step_cmd('⑧ 台账等价验收 (rc=4 = 有需人工看的差异, 属已知)', [PY, 'scripts/rebuild_from_raw.py', '--verify'], tolerate=(4,), note='rc=5 表示找不到随包基线, 那是"没验收"不是通过')) return plan def main() -> int: ap = argparse.ArgumentParser() ap.add_argument('--src', default=None, help='现场数据包目录 (给了就先跑 place_raw_data --scope full)') ap.add_argument('--skip-scada', action='store_true', help='跳过 SCADA 侧 10 个构建器 (没换 10min 数据时用)') ap.add_argument('--skip-vib', action='store_true', help='跳过振动侧摄入 (没换 CMS 原始导出时用)') ap.add_argument('--vib-report', action='store_true', help='振动侧额外重生成 CMS 报告/知识库 —— ★默认关: 本包缺六层链的 model_run/fusion ' '产物, 重生成会掉内容 (report.md −97%), 详见 scripts/vib_raw_build.py 文件头') ap.add_argument('--no-restart', action='store_true', help='不自动重启服务') ap.add_argument('--with-verify', action='store_true', help='末尾加等价验收') ap.add_argument('--dry-run', action='store_true', help='只打印计划') a = ap.parse_args() plan = build_plan(a) print(f'重算全部 · {len(plan)} 步 · 根目录 {ROOT}') print(f' 跳过 SCADA: {a.skip_scada} | 重启服务: {not a.no_restart} | 等价验收: {a.with_verify}\n') if a.dry_run: for name, cmd, tol, note in plan: print(f' {name:34s} {" ".join(cmd[1:])[:90]}') print('\n(dry-run, 未执行)') return 0 # 子进程环境: 与 guanlan.py 给服务的一致 —— 中文 Windows 控制台是 GBK, 不设的话 # 子脚本 print 里的 ✔/✗ 会抛 UnicodeEncodeError, 明明干成了却以退出码 1 结束。 env = dict(os.environ, PYTHONUTF8='1', PYTHONIOENCODING='utf-8') results, t_all = [], time.time() for i, (name, cmd, tol, note) in enumerate(plan, 1): print(f'\n{"=" * 78}\n[{i}/{len(plan)}] {name}\n$ {" ".join(cmd)}\n{"=" * 78}', flush=True) t0 = time.time() rc = subprocess.run(cmd, cwd=str(ROOT), env=env).returncode dt = time.time() - t0 ok = (rc == 0) or (rc in tol) results.append((name, rc, dt, ok, tol)) if not ok: print(f'\n[X] 第 {i} 步失败 (退出码 {rc}): {name} —— 后面的步骤依赖它, 先停下。') if note: print(f' 该步说明: {note}') break if rc != 0: print(f'\n (退出码 {rc} 属预期{(": " + note) if note else ""})') print(f'\n{"=" * 78}\n汇总 (总耗时 {time.time() - t_all:.0f}s)') for name, rc, dt, ok, tol in results: tag = 'OK' if rc == 0 else (f'OK(rc={rc})' if ok else 'FAIL') print(f' [{tag:9s}] {name:34s} {dt:7.1f}s') n_fail = sum(1 for *_, ok, _ in results if not ok) if n_fail: print(f'\n结论: {n_fail} 步失败 —— 按上面的日志处理后再重跑本命令 (各步都是幂等的)') return 1 print('\n结论: 全部完成。核验: .venv\\Scripts\\python.exe -m src.ontology.maintenance ' '(数据层/机理层的条数与末次更新); 浏览器 http://127.0.0.1:28084/detail/') return 0 if __name__ == '__main__': for _s in (sys.stdout, sys.stderr): try: _s.reconfigure(errors='replace') except Exception: pass sys.exit(main())