#!/usr/bin/env python3 # -*- coding: utf-8 -*- """观澜 详细分析工作台 视觉基线 (detail_style_baseline) 清单构建/校验 (用户裁决 2026-09-06: 确认 C1 = /v2, 删末块与门户一致, 补返回门户后冻结). build: 从 src/windscada/ui 源重建 zh 页 → 摘要 {css_tokens, nav, dom, sha256, source blobs} → outputs/rudong/windscada/release-manifest.json (detail_style_baseline 段) check: 重建后摘要 vs 清单; 任一差异 = 样式/框架变化 = FAIL (除非用户批准新版本并重写清单). 用法: python scripts/guanlan_baseline_manifest.py build|check [--html ]""" import hashlib, datetime as dt, hashlib, json, re, subprocess, sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1]; MAN = ROOT / "outputs/rudong/windscada/release-manifest.json" UI = ROOT / "src/windscada/ui"; SRC_FILES = ["src/windscada/ui/shell.html", "src/windscada/ui/app.css", "src/windscada/ui/app.js", "src/windscada/ui/charts.js", "src/windscada/ui/build.py", "src/windscada/lang.py"] TOKENS = ("--ground", "--panel", "--ink", "--muted", "--line", "--hair", "--accent", "--accent-deep", "--accent-soft", "--wash", "--critical", "--warning", "--good", "--excellent", "--sans", "--mono") sha = lambda b: hashlib.sha256(b).hexdigest() def render_zh() -> str: sys.path.insert(0, str(ROOT)); from src.windscada.ui import build; return build.render("zh") def summarize(html: str) -> dict: css = "\n".join(re.findall(r"]*>(.*?)", html, flags=re.S)) # 全部 style 块按出现顺序 (服务端注入在后 → 覆盖) roots = [b for sel, b in re.findall(r"(?:^|[}\s;])(:root[^{]*)\{([^}]*)\}", css) if ":root" in sel]; eff = {} # 任何含 :root 的选择器块 (含 ':root,:root[data-theme]' 这种注入写法) for blk in roots: # 后块覆盖前块 = 浏览器生效值 (末块已删, 中段已删; 若有人再加覆盖, 摘要就变) for k, v in re.findall(r"(--[a-z-]+)\s*:\s*([^;]+)", blk): eff[k] = v.strip() tokens = {k: eff.get(k) for k in TOKENS} js = html[html.rfind("")] tabs = re.search(r"const TABS = \[([^\]]*)\]", js).group(1); tabs = re.findall(r"'([a-z_]+)'", tabs) pack = json.loads(re.search(r"window\.__L=(\{.*?\});", html, flags=re.S).group(1)) nav = [pack.get("tab." + t) for t in tabs]; actions = {k: pack.get(k) for k in ("ui.back_portal", "ui.reselect", "ui.back_to", "win.label")} dom = {"shell_ids": re.findall(r'id="(top|tabs|root|foot)"', html), "header_controls": ["backPortal" if 'id="backPortal"' in html else None, "reselect" if 'id="reselect"' in html else None, "win" if 'id="win"' in html else None], "theme_attr": bool(re.search(r']*data-theme', html)), "lang": re.search(r' list: """线上 /v2 实际响应 (含服务端注入的样式) 的摘要必须等于基线 —— 服务端注入 (如 guanlan-unified-review) 改令牌 = 红.""" import urllib.request html = urllib.request.urlopen(url, timeout=10).read().decode("utf-8"); s = summarize(html); diffs = [] for k in ("css_tokens", "tabs", "nav_zh", "actions", "dom"): if man[k] != s[k]: diffs.append(f"live {k}: 基线 {json.dumps(man[k], ensure_ascii=False)[:100]} ≠ 线上 {json.dumps(s[k], ensure_ascii=False)[:100]}") inj = re.findall(r'