rebuild_all.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. r"""一条命令重算全部 (2026-09-12) —— 把散在各脚本里的重算链按依赖顺序串起来。
  4. ## 为什么要有它
  5. "从零重算"原本是六七条命令、顺序还不能错 (产物齐了本体层才跑得动, 本体层跑完才能拷随包件…),
  6. 手工执行容易漏步、也说不清"到底重算了什么"。本脚本把顺序、依赖、验收锚点全写死在这里。
  7. ## 顺序 (为什么是这个顺序)
  8. ① 放数据 place_raw_data.py --scope full (给了 --src 才跑; 同尺寸自动跳过)
  9. ② 三门台账 rebuild_from_raw.py 报警/工单/油样
  10. ③ SCADA 侧 rebuild_from_raw.py --scada 10 个构建器, 逐台读 ~14 GB, 约 15 分钟
  11. ④ 月度派生件 windscada_monthly_build.py 能从 raw 重算的月表(与随包件逐值对齐才落盘)
  12. ⑤a 台账维护 products_restore_missing.py --refresh 账实相符(谁在位、谁有生成端); 只记账不搬件
  13. ⑤ 反向呼应审计 products_reverse_audit.py --check 逐件回溯"输出→生成端→输入"(用户令 2026-09-17 #1: 运行期不从交付包补齐)
  14. ↑ 必须在 ⑥ 之前: 本体层的 populate/chain_ingest 要吃这些 L1 产物
  15. ⑥ 重启服务 guanlan.py stop/serve chain_ingest 要从 /api/fleet 取链盘
  16. ⑦ 本体层 kb_ingest → populate → chain_ingest → trend_ingest → retrieval → refresh_params
  17. ⑧ 验收 ontology.audit + rebuild_from_raw --verify (可选) + 页面对照
  18. ## 用法
  19. python scripts/rebuild_all.py # 全套 (会跑 SCADA 那 15 分钟)
  20. python scripts/rebuild_all.py --skip-scada # 只换台账时的常用档
  21. python scripts/rebuild_all.py --src <现场包目录> # 连"放数据"一起
  22. python scripts/rebuild_all.py --no-restart # 不碰服务(自己管)
  23. python scripts/rebuild_all.py --with-verify # 末尾加等价验收(需随包基线在位)
  24. python scripts/rebuild_all.py --dry-run # 只打印计划
  25. """
  26. from __future__ import annotations
  27. import argparse
  28. import os
  29. import pathlib
  30. import subprocess
  31. import sys
  32. import time
  33. ROOT = pathlib.Path(__file__).resolve().parents[1]
  34. PY = sys.executable
  35. def step_cmd(name, cmd, tolerate=(), note=''):
  36. """tolerate: 允许的退出码 (不算失败); note: 该步非零退出时打给人看的解释(按步, 别用一句通用话糊过去)。"""
  37. return (name, cmd, tuple(tolerate), note)
  38. def build_plan(a) -> list:
  39. plan = []
  40. if a.src:
  41. plan.append(step_cmd('① 放数据', [PY, 'scripts/place_raw_data.py', '--src', a.src, '--scope', 'full']))
  42. plan.append(step_cmd('② 三门台账', [PY, 'scripts/rebuild_from_raw.py']))
  43. if not a.skip_scada:
  44. plan.append(step_cmd('③ SCADA 侧 10 个构建器 (约 15 分钟)', [PY, 'scripts/rebuild_from_raw.py', '--scada']))
  45. # ④ 月度派生件: `windscada_monthly_build.py` 会拿随包基线**逐值比对**, 没有基线时它返回 5
  46. # ("无法比对 —— 这不是通过", 这是它的诚实口径, 不要改它的退出码)。
  47. # ★但"重算链"与"等价验收"是两件事: 基线只是**验收标准答案**, 缺它不该让整条链停在第④步
  48. # (2026-09-16 实逮: 基线目录被清理后, 从页面点"执行重算" 387 s 后 rc=1 断在这里, ⑦⑧ 全不执行)。
  49. # 故这里容忍 5 并写清"跳过了什么"; 要真做验收, 先把随包件恢复成暂存区/基线(见 products_state.py)。
  50. plan.append(step_cmd('④ 月度派生件', [PY, 'scripts/windscada_monthly_build.py'],
  51. tolerate=(5,),
  52. note='rc=5 = 找不到随包基线可比对 ⇒ **跳过等价验收**(不是通过)。'
  53. '要恢复验收能力: 把随包产物放回 _products_off*/<场>/ 或 '
  54. 'outputs/<场>/windscada/_pre_rebuild_20260911/'))
  55. if not a.skip_vib:
  56. # 振动侧 (2026-09-12 用户令"振动数据参与运行、重算"): 原始导出的 Brande TCM JSON
  57. # (data/raw/<场站>/windcms/**/*_decode.json) → 窗索引 + 谱库。消费者 (src/windcms/data.py)
  58. # 自动发现新窗, 所以这一步之后 CMS 的谱图/标量/页面就带上了新数据。
  59. # ★**不跑** `windcms.py report` —— 本包缺六层链的 model_run/fusion 产物, 重生成会用残缺输入
  60. # 覆盖随包快照 (report.md 16,857 B → 467 B, overview.html −15%; 实测见 vib_raw_build.py 文件头)。
  61. # 六层链补齐后应把该步改为带 --with-report。
  62. # 现场没有振动原始件时本步**空跑退出 0** (不是每台机器都有振动导出, 不该报失败)。
  63. plan.append(step_cmd('④b 振动侧摄入 (原始导出 → 窗索引/谱)',
  64. [PY, 'scripts/vib_raw_build.py'] + (['--with-report'] if a.vib_report else []),
  65. note='没有振动原始件时空跑属正常; 若解析出错会以非零退出 (不静默)'))
  66. # ⑤a 台账维护 (用户令 2026-09-18: 页面只认"有来路的件" ⇒ 台账必须跟着重算一起更新)。
  67. # ★实逮: 2026-09-18 用户「清除产物」后从 raw 全量重算, `outputs/<场>/_provenance.json`
  68. # **整本丢了** —— 因为写它的唯一入口 (旧的第⑤步 products_restore_missing.py) 已按用户令
  69. # 从链上拿下, 而新的第⑤步只**读**台账不写。后果: 门户/页面的「逐件来源」、反向呼应审计的
  70. # 族匹配全部退化成"未归类", 而盘上 1,728 件其实件件有生成端。故把 `--refresh`(纯账实相符,
  71. # 不搬任何随包件) 作为**独立一步**放回链上, 位置在审计之前。
  72. plan.append(step_cmd('⑤a 逐件来源台账 (账实相符: 谁在位、谁有生成端)',
  73. [PY, 'scripts/products_restore_missing.py', '--refresh'],
  74. note='只维护台账, **不搬任何随包件**(用户令 2026-09-17: 运行期不从交付包补齐); '
  75. '盘上有而无人登记的件按族表归口, 归不上族的记 unregistered 并打出来'))
  76. # ⑤ 反向呼应审计 (用户令 2026-09-17 #1): **运行链不再从含产物的交付包补齐**。
  77. # 原先这一步跑 products_restore_missing.py —— 那等于"重算链依赖一个包", 与"目标机放数据自己重算"
  78. # 这条主线冲突: 交付包默认不含产物, 于是这一步在真机上要么报"找不到随包件来源"(rc=6), 要么
  79. # 被指到某个 zip 上(而那正是用户明令不要的)。改成**只报账、不搬运**: 逐件回溯"这件产物有没有
  80. # 生成端与输入", 把没有来路的件如实列出来, 供研发补生成端 (口径见 docs §13.6)。
  81. # rc=5 = 有未归类/判据失败(例如产物被清后族表匹配不到) —— 容忍, 别为它打断后面的本体链与审计。
  82. plan.append(step_cmd('⑤ 反向呼应审计 (输出→功能/算法→输入; 不从交付包补齐)',
  83. [PY, 'scripts/products_reverse_audit.py', '--check'],
  84. tolerate=(5,),
  85. note='rc=5 = 有未归类件或判据失败 ⇒ 停下看清单再决定(不打断整条链)。'
  86. '★ 运行期一律不由交付包补齐(用户令 2026-09-17): 缺件要么放原始件重算, '
  87. '要么由研发补生成端; 逐族可逆性见 docs §13.6'))
  88. if not a.no_restart:
  89. # 只重启**组件服务**, 网关留着 —— 两个理由:
  90. # ① 控制台页面是网关提供的, 连它一起停的话, 从页面点的重算会把自己的界面(甚至自己)弄没;
  91. # ② rebuild_all 自己可能就是从控制台/网关起的任务, `guanlan.py stop` 用 taskkill /T,
  92. # 连网关会**沿父子树把正在执行的重算一起杀掉**(2026-09-12 实测: 日志停在 stop 那行, 之后全无)。
  93. # 组件停掉再 serve 就够: chain_ingest 要的是 detail(18033) 重新加载新产物, 网关不缓存产物。
  94. plan.append(step_cmd('⑥ 重启组件服务 (保留网关, stop)', [PY, 'scripts/_ops_stop_keep_gateway.py']))
  95. plan.append(step_cmd('⑥ 重启组件服务 (serve)', [PY, 'guanlan.py', 'serve'], tolerate=(1,),
  96. note='serve 在"有模块 DOWN"时故意返回 1 —— degraded 是正常状态(例如本机模型未启动)'))
  97. plan.append(step_cmd('⑦ 本体: 码表/手册/文档', [PY, '-m', 'src.ontology.kb_ingest']))
  98. plan.append(step_cmd('⑦ 本体: 铺开判级与台账', [PY, '-m', 'src.ontology.populate']))
  99. plan.append(step_cmd('⑦ 本体: 决策链进度', [PY, '-m', 'src.ontology.chain_ingest']))
  100. plan.append(step_cmd('⑦ 本体: 在升/闭环证据', [PY, '-m', 'src.ontology.trend_ingest']))
  101. plan.append(step_cmd('⑦ 本体: 检索索引', [PY, '-c', 'from src.ontology import retrieval as R; R.build(use_vec=False)']))
  102. plan.append(step_cmd('⑦ 本体: 实机参数表', [PY, '-c', 'from src.ontology.maintenance import refresh_params as f; print(f())']))
  103. plan.append(step_cmd('⑧ 本体审计 (期望 0 问题)', [PY, '-m', 'src.ontology.audit']))
  104. # ⑧b 重装门户: 门户正文里的"契约结论段"是**产物派生**的(由 guanlan_portal_inject_claims.py
  105. # 从 outputs/<场>/guanlan/… 渲染), 所以产物变了门户必须重装一遍, 否则页面挂着旧结论。
  106. # 装配是"先写临时文件再原子替换", 失败不会破坏正在被网关读的 portal.html (见 portal_build 注释)。
  107. plan.append(step_cmd('⑧b 重装门户 (把新产物灌进门户结论段)', [PY, 'scripts/portal_build.py'],
  108. tolerate=(1,),
  109. note='rc=1 = 缺契约产物(产物被清除时属预期) ⇒ 门户保留原样, 不是装配坏了'))
  110. # ⑧c 页面归口审计: 重算之后, 那些"把数据烘进页面"的页面件(见 configs/portal_pages.yaml)会不会陈旧。
  111. # rc=7 = 有页面陈旧(数据换了页面没换) —— 不打断整条链, 但必须报出来, 处置见 docs §10。
  112. # ★ rc=5 也容忍 (2026-09-17 实逮): rc=5 = "登记的文件/被引产物不在位", 而**包内没有生成端**的那批
  113. # 随包件(如 windscada/index.html、m5_cms_tcm/tcm_index.parquet、windcms 页面件)在
  114. # "清过产物 + 交付包不含产物"的机器上必然不在位 ⇒ 第 16 步判失败、整条链 rc=1, 而且把最后的
  115. # ⑧台账等价验收也跳过了(实测 2026-09-17: 用户在页面上点重算, 1367 s 后 rc=1 断在这里)。
  116. # 这与 ④/⑤/⑥/⑧b 的口径一致: 缺的是"随包件"这一路, 不是重算本身。缺件仍会由
  117. # `guanlan.py check` 报 FAIL(那是给人看的告警位), 本步只保证"链不白断"。
  118. plan.append(step_cmd('⑧c 页面归口审计 (页面陈旧检测)', [PY, 'scripts/pages_audit.py', '--check'],
  119. tolerate=(5, 7),
  120. note='rc=7 = 有 data-derived 页面陈旧(内嵌快照的 source_sha256 与当前产物不一致): '
  121. '需要对应生成端重跑, 或在登记表里把它改成 frozen-delivery 并接受会过期; '
  122. 'rc=5 = 登记的文件/被引产物不在位(清过产物、且那批件没有生成端时属预期) '
  123. '⇒ 不打断整条链, 补齐办法见 docs §7 与「数据重算」页的缺件提示'))
  124. if a.with_verify:
  125. # 只有 4 = "有需人工看的差异"(本包已知项: 油样 102 行的源件不在现场包里) 才容忍;
  126. # 5 = 找不到基线(等于没验收) 必须当失败 —— 2026-09-12 就是因为放过了它, 出现过"验收没做却报通过"。
  127. plan.append(step_cmd('⑧ 台账等价验收 (rc=4 = 有需人工看的差异, 属已知)',
  128. [PY, 'scripts/rebuild_from_raw.py', '--verify'], tolerate=(4,),
  129. note='rc=5 表示找不到随包基线, 那是"没验收"不是通过。'
  130. '★ 2026-09-18 现场: 基线目录被「清除产物」真删后这一步必 rc=5, 门户上就显示成'
  131. '"重算失败" ⇒ 用 python scripts/set_baseline.py 一条命令重建基线'
  132. '(用当前已核实的产物快照并写明来历), 再重算即可全绿'))
  133. return plan
  134. def main() -> int:
  135. ap = argparse.ArgumentParser()
  136. ap.add_argument('--src', default=None, help='现场数据包目录 (给了就先跑 place_raw_data --scope full)')
  137. ap.add_argument('--skip-scada', action='store_true', help='跳过 SCADA 侧 10 个构建器 (没换 10min 数据时用)')
  138. ap.add_argument('--skip-vib', action='store_true', help='跳过振动侧摄入 (没换 CMS 原始导出时用)')
  139. ap.add_argument('--vib-report', action='store_true',
  140. help='振动侧额外重生成 CMS 报告/知识库 —— ★默认关: 本包缺六层链的 model_run/fusion '
  141. '产物, 重生成会掉内容 (report.md −97%), 详见 scripts/vib_raw_build.py 文件头')
  142. ap.add_argument('--no-restart', action='store_true', help='不自动重启服务')
  143. ap.add_argument('--with-verify', action='store_true', help='末尾加等价验收')
  144. ap.add_argument('--dry-run', action='store_true', help='只打印计划')
  145. a = ap.parse_args()
  146. plan = build_plan(a)
  147. print(f'重算全部 · {len(plan)} 步 · 根目录 {ROOT}')
  148. print(f' 跳过 SCADA: {a.skip_scada} | 重启服务: {not a.no_restart} | 等价验收: {a.with_verify}\n')
  149. if a.dry_run:
  150. for name, cmd, tol, note in plan:
  151. print(f' {name:34s} {" ".join(cmd[1:])[:90]}')
  152. print('\n(dry-run, 未执行)')
  153. return 0
  154. # 子进程环境: 与 guanlan.py 给服务的一致 —— 中文 Windows 控制台是 GBK, 不设的话
  155. # 子脚本 print 里的 ✔/✗ 会抛 UnicodeEncodeError, 明明干成了却以退出码 1 结束。
  156. env = dict(os.environ, PYTHONUTF8='1', PYTHONIOENCODING='utf-8')
  157. results, t_all = [], time.time()
  158. for i, (name, cmd, tol, note) in enumerate(plan, 1):
  159. print(f'\n{"=" * 78}\n[{i}/{len(plan)}] {name}\n$ {" ".join(cmd)}\n{"=" * 78}', flush=True)
  160. t0 = time.time()
  161. rc = subprocess.run(cmd, cwd=str(ROOT), env=env).returncode
  162. dt = time.time() - t0
  163. ok = (rc == 0) or (rc in tol)
  164. results.append((name, rc, dt, ok, tol))
  165. if not ok:
  166. print(f'\n[X] 第 {i} 步失败 (退出码 {rc}): {name} —— 后面的步骤依赖它, 先停下。')
  167. if note:
  168. print(f' 该步说明: {note}')
  169. break
  170. if rc != 0:
  171. print(f'\n (退出码 {rc} 属预期{(": " + note) if note else ""})')
  172. print(f'\n{"=" * 78}\n汇总 (总耗时 {time.time() - t_all:.0f}s)')
  173. for name, rc, dt, ok, tol in results:
  174. tag = 'OK' if rc == 0 else (f'OK(rc={rc})' if ok else 'FAIL')
  175. print(f' [{tag:9s}] {name:34s} {dt:7.1f}s')
  176. n_fail = sum(1 for *_, ok, _ in results if not ok)
  177. if n_fail:
  178. print(f'\n结论: {n_fail} 步失败 —— 按上面的日志处理后再重跑本命令 (各步都是幂等的)')
  179. return 1
  180. print('\n结论: 全部完成。核验: .venv\\Scripts\\python.exe -m src.ontology.maintenance '
  181. '(数据层/机理层的条数与末次更新); 浏览器 http://127.0.0.1:28084/detail/')
  182. return 0
  183. if __name__ == '__main__':
  184. for _s in (sys.stdout, sys.stderr):
  185. try: _s.reconfigure(errors='replace')
  186. except Exception: pass
  187. sys.exit(main())