| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485 |
- #!/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 <path>]"""
- try:
- from app_common.app_common_guanlan.api import install_root as _install_root
- except ImportError: # 理论不可达;包结构异常时回退到按位置上跳
- from pathlib import Path as _P
- def _install_root(_f): return _P(_f).resolve().parents[4]
- import hashlib, datetime as dt, hashlib, json, re, subprocess, sys
- from pathlib import Path
- ROOT = _install_root(__file__); MAN = ROOT / "outputs/rudong/windscada/release-manifest.json"
- UI = ROOT / "app_frontEnd/app_frontEnd_guanlan" / "assets"; SRC_FILES = ["app_frontEnd/app_frontEnd_guanlan/pages/shell.html",
- "app_frontEnd/app_frontEnd_guanlan/assets/app.css",
- "app_frontEnd/web/src/api/contract.ts", "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"<style[^>]*>(.*?)</style>", 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("<script>") + 8: html.rfind("</script>")]
- tabs = re.search(r"const TABS = \[([^\]]*)\]", js).group(1); tabs = re.findall(r"'([a-z_]+)'", tabs)
- pack = json.loads(re.search(r"window\.__L=(\{.*?\});</script>", 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'<html[^>]*data-theme', html)), "lang": re.search(r'<html lang="([a-z]+)"', html).group(1)}
- return {"css_tokens": tokens, "n_root_blocks": len(roots), "tabs": tabs, "nav_zh": nav, "actions": actions, "dom": dom, "sha256": sha(html.encode("utf-8")), "size": len(html.encode("utf-8")),
- "fingerprint": sha(json.dumps({"t": tokens, "n": nav, "d": dom, "a": actions}, sort_keys=True, ensure_ascii=False).encode("utf-8"))[:16]}
- def live_check(url: str, man: dict) -> 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'<style id="([^"]+)"', html)
- if inj: diffs.append(f"live 服务端注入 <style id=…>: {inj} (基线构建物无)")
- return diffs
- def main():
- mode = sys.argv[1] if len(sys.argv) > 1 else "check"; html = render_zh(); s = summarize(html)
- if mode == "check" and "--live" in sys.argv:
- url = sys.argv[sys.argv.index("--live") + 1]; man = json.loads(MAN.read_text(encoding="utf-8"))["detail_style_baseline"]; d = live_check(url, man)
- print(("PASS" if not d else "FAIL") + f" live {url}"); [print(" -", x) for x in d]; return 0 if not d else 2
- git = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "--short", "HEAD"], capture_output=True, text=True).stdout.strip()
- blobs = {f: subprocess.run(["git", "-C", str(ROOT), "ls-tree", "HEAD", "--", f], capture_output=True, text=True).stdout.split()[2][:16] for f in SRC_FILES}
- if mode == "build":
- def portal_sha(prev): # 门户参考 = 桌面固定单文件 (用户裁定; 2026-09-06 契约段就地注入后变更), 不在库内 → 缺失时沿用清单旧值, 禁写死
- rel = (ROOT / "release/portal.html"); pp = rel if rel.exists() else Path.home() / "Desktop/观澜_如东样板_门户_单文件.html" # 发布目录优先 (用户裁 2026-09-06), 回退桌面
- return hashlib.sha256(pp.read_bytes()).hexdigest() if pp.exists() else (prev.get("detail_style_baseline") or {}).get("portal_reference_sha256")
- man = json.loads(MAN.read_text(encoding="utf-8")) if MAN.exists() else {"schema": "release-manifest/v0"}
- man["detail_style_baseline"] = {"confirmed_by": "用户裁决 2026-09-06 (确认 C1; 删末块与门户一致; 补返回门户/重新选择后冻结)", "route": "/v2 (8033) → 目标架构 /detail/", "source": "src/windscada/ui + src/windscada/lang.py", "git_commit": git, "source_blobs_sha16": blobs,
- "build_cmd": "python -m src.windscada.ui.build zh", "built": dt.datetime.now().isoformat(timespec="minutes"), "artifact_sha256": s["sha256"], "artifact_size": s["size"],
- "portal_reference_sha256": portal_sha(man), "portal_tokens": {"--ground": "#F5F7F8", "--ink": "#1B2430", "--accent": "#1F6F8B", "--line": "#DDE3E8"},
- "css_tokens": s["css_tokens"], "n_root_blocks": s["n_root_blocks"], "tabs": s["tabs"], "nav_zh": s["nav_zh"], "actions": s["actions"], "dom": s["dom"], "fingerprint": s["fingerprint"],
- "screenshot_note": "2026-09-06 浏览器实测 computed style: --ground #F5F7F8 / --ink #1B2430 / --accent #1F6F8B / --line #DDE3E8, 10 tab, 返回门户 href=/ (?portal= 可覆盖), 重新选择 存在; 像素级截图指纹未建 (无基线截图机制), fingerprint = 令牌+导航+DOM 摘要 sha",
- "frozen": ["色彩令牌", "字体", "字号层级/留白/卡片/边框 (app.css)", "顶部导航与十模块顺序", "返回门户/重新选择 位置与语义", "板块框架 (总览…系统维护)"],
- "allowed_changes": "仅结构化事实契约驱动的数据/时间窗/结论/图表数据/报告文本; 不得改 DOM 布局/CSS 令牌/导航顺序/按钮层级"}
- MAN.write_text(json.dumps(man, ensure_ascii=False, indent=1), encoding="utf-8"); print("wrote", MAN.relative_to(ROOT), "commit", git, "sha", s["sha256"][:16], "fp", s["fingerprint"]); return 0
- man = json.loads(MAN.read_text(encoding="utf-8"))["detail_style_baseline"]; diffs = []
- for k in ("css_tokens", "tabs", "nav_zh", "actions", "dom", "n_root_blocks"):
- if man[k] != s[k]: diffs.append(f"{k}: 基线 {json.dumps(man[k], ensure_ascii=False)[:120]} ≠ 现 {json.dumps(s[k], ensure_ascii=False)[:120]}")
- for k, v in man["css_tokens"].items():
- if k in man["portal_tokens"] and s["css_tokens"].get(k) != man["portal_tokens"][k]: diffs.append(f"{k} ≠ 门户 {man['portal_tokens'][k]}")
- print(("PASS" if not diffs else "FAIL") + f" detail_style_baseline fp={s['fingerprint']} (基线 {man['fingerprint']}) sha={s['sha256'][:16]} (基线 {man['artifact_sha256'][:16]})"); [print(" -", d) for d in diffs]
- return 0 if not diffs else 2
- if __name__ == "__main__":
- sys.exit(main())
|