#!/usr/bin/env python3 # -*- coding: utf-8 -*- """门户内嵌文档 (iframe srcdoc) 的章节锚点修复 — 幂等注入器 (xzy 测试报告 2026-09-08 P1-3). 病灶: 报告经 `ovf.srcdoc = 模板` 挂进 iframe。srcdoc 文档的**基地址继承父页**, 所以报告里的 `` 解析成 `http://127.0.0.1:28084/#section-3` —— 与 iframe 当前文档 (about:srcdoc) 不同源同址, 浏览器判为**导航**, 于是 iframe 整个跳去门户首页 (门户路由 route() 认不出 section-3 → 回落 index)。 修法: iframe 载入后 (srcdoc 同源, 父页可访问其 document) 拦截 `a[href^="#"]` 点击, 改为在报告文档内 scrollIntoView。 不改 srcdoc 挂载方式 (换 blob: 会让相对资源失效; 加 又会把锚点解析回父页)。 用法: python scripts/guanlan_portal_fix_anchors.py [--portal ] [--check] --check 只报是否已注入 (exit 0 已注入 / 1 未注入); 不带则注入 (已注入时不重复写)。""" from __future__ import annotations import argparse, hashlib, os, shutil, sys, time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] MARK = "/*guanlan-anchor-fix-v2*/" MARK_ANY = "/*guanlan-anchor-fix-v" # 旧版本先剥再注 (升级用) ANCHOR = "document.getElementById('ovf').srcdoc=t.content.textContent;" ANCHOR_ALT = "document.getElementById('ovf').srcdoc = t.content.textContent;" # v2: 用**即时**跳 (原生锚点行为), 不用 behavior:'smooth' —— 平滑滚动在隐藏/零尺寸视口里不执行 # (2026-09-08 浏览器实测: smooth 后 scrollTop 仍 0, 即时跳到位 14173), 且长文档平滑滚动本身也不是文档阅读的习惯。 PATCH = (MARK + "(function(){var f=document.getElementById('ovf');if(!f||f._afix)return;f._afix=1;" "f.addEventListener('load',function(){try{var d=f.contentDocument;if(!d||d._afix)return;d._afix=1;" "d.addEventListener('click',function(ev){var a=ev.target&&ev.target.closest?ev.target.closest('a[href^=\"#\"]'):null;if(!a)return;" "ev.preventDefault();var id=decodeURIComponent((a.getAttribute('href')||'#').slice(1));" "if(!id){d.defaultView.scrollTo(0,0);return}" "var el=d.getElementById(id)||(d.getElementsByName(id)||[])[0];" "if(el&&el.scrollIntoView)el.scrollIntoView({block:'start'})},true)}catch(e){}})})();") def portal_path(a=None) -> Path: if a: return Path(a) return Path(os.environ.get("GUANLAN_PORTAL") or (ROOT / "release/portal.html")) def _strip_old(s: str) -> tuple[str, int]: """剥掉旧版本注入块 (从旧标记到该块结尾 `})();`), 用于升级; 返回 (新串, 剥掉几块).""" n = 0 while True: i = s.find(MARK_ANY) if i < 0 or s[i:i + len(MARK)] == MARK: break j = s.find("})();", i) if j < 0: break s = s[:i] + s[j + len("})();"):]; n += 1 return s, n def inject(p: Path) -> tuple[bool, str]: s = p.read_text(encoding="utf-8", errors="strict") if MARK in s: return False, "已注入 (幂等, 不重复写)" s, n_old = _strip_old(s) for a in (ANCHOR, ANCHOR_ALT): if a in s: bak = p.with_suffix(p.suffix + ".bak_anchor_" + time.strftime("%Y%m%d%H%M%S")); shutil.copy2(p, bak) s2 = s.replace(a, a + PATCH, 1) assert MARK in s2 and len(s2) > len(s), "注入后自检失败" p.write_text(s2, encoding="utf-8", newline="") # 保持 LF: Windows 上默认会把整份门户改成 CRLF return True, f"已注入 ({p.name}, 备份 {bak.name}, 剥旧版 {n_old} 块, sha16 {hashlib.sha256(s2.encode()).hexdigest()[:16]})" return False, "未找到挂载点 (门户结构变了: 找 ovf.srcdoc 赋值处) → 未改动" def main(): ap = argparse.ArgumentParser(); ap.add_argument("--portal"); ap.add_argument("--check", action="store_true"); a = ap.parse_args() p = portal_path(a.portal) if not p.exists(): print(f"门户不存在: {p}"); return 2 if a.check: ok = MARK in p.read_text(encoding="utf-8", errors="replace"); print(("已注入" if ok else "未注入") + f": {p}"); return 0 if ok else 1 changed, msg = inject(p); print(msg); return 0 if (changed or "已注入" in msg) else 2 if __name__ == "__main__": sys.exit(main())