③段第八轮收口:功能入口卡按参照物对齐 + 导航按 tiaoyue 源站重建 + 密度 A 方案

原型(执行会话/目标-…-d85687/ui/mcn-workbench.html)
- 功能入口卡改三段纵向(图标/标题/描述各一行),68 → 111.19px,对齐 mcn-work-shop
  参考卡(同视口 1249×1277 实测 133px),补 --sh-L1 投影;主因是排布不是字号
- 左侧导航重建:由「纯图标 + 悬停提示」改为 64px 药丸浮栏,图标 24 在上、名字常驻在下
  (名字一律 2 字,说明走 data-tip);真值取自 tiaoyue 源站实测
  (栏 64/内衬 3/圆角 100/项 56×78/字 12px-500-行高 16)
- 密度 A 方案:≤1000 卡最小宽 200→160、≤820 统计条最小宽 132→96
  (两处 auto-fit 只在真装不下时才缩)⇒ 768/700/560 三处转绿,其余 10 个视口未动
- 移动档隐藏导航条横向滚动轨(16px 轨道曾吃掉 52px 条高的三成,还白吃 16px 主区)

取证通道
- _tools/cap.py 改为「只连不起」:只连 9223 常驻实例,视口走 Emulation.setDeviceMetricsOverride,
  不再自起 headless Chrome(顺带解除 500px 视口下限,390×844 可测)
- _tools/check_js.py 新增第 5 道门禁(node --check + 模板内注释反引号扫描)

文档
- 复盘-为什么第一版做得很差.md(新建):13 轮时间轴/四类反作用/三重归因/已修与仍欠
- 3d-审查报告.md:更正 §九 那条假 ✓(把「§5.5 五条逐条过完」记成机核,实际零覆盖,
  正是「五道闸门全绿却难看」的开关),补 §十二 功能入口卡高差、§十三 tiaoyue 导航逐条对照
- 3b-实测记录.md/DESIGN.md/3c-GPT 参考版与会诊/布局排版参考 ×2

机制与记忆
- .workbuddy/collab/collabctl.py:off --ws 不再全机扫描(原先一次杀掉他区+看板共 5 个进程)
- .workbuddy/memory/:本轮纪律与实测真值
- .gitignore:补排运行时噪音(ui/_scratch/__pycache__/浏览器取证临时目录/*.bak-*/一次性脚本)

闸门:语法全过/骨架八页 39 板块/变异对照 15-15/逐视口 14 绿(500×844 唯一红,缺口待拍板)/命名 2060-0-27
This commit is contained in:
WorkBuddy committed 2026-10-08 22:21:42 +08:00
1 parent df56c2c137
commit eee042c855
48 files changed
+8222 -1752

No files matched your search

@@ -1,41 +1,74 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""无头 Chrome 取证小工具(③段 3b/3d 用)。
"""取证小工具(③段 3b/3d 用)。
--mode dom : 取渲染后的 DOM(自检输出、数值取证都从这里读)
--mode shot : 截图(视口帧或整页长图)
--mode dom : 取渲染后的 DOM(自检输出、数值取证都从这里读)
--mode shot : 截图(视口帧)
用法示例:
python _tools/cap.py --mode dom --route selftest --out _scratch/selftest.html
python _tools/cap.py --mode shot --route p/p1 --viewport 1440x900 --out _gate_shots/P1-1440.png
python _tools/cap.py --mode shot --route p/p1 --viewport 1440x900 --out _gate_shots/P1-1440.png
⚠️ 本机实测(Chrome 154 · headless=new)——**两条通道对 --window-size 的解释不一样**,
同一台机器、同一个窗口尺寸,拿到的是两个不同视口(探针页实测记录见 3b §五):
--window-size=656,995 ─┬─ --dump-dom ⇒ 视口 640 × 900 (mq<=640 true)
└─ --screenshot ⇒ 视口 656 × 995 (mq<=640 false)
即:dump-dom 通道会从窗口里扣掉滚动条与标题栏(16 / 95),截图通道不扣。
⇒ 想拿到「视口 W × H」,dom 模式要传 (W+16, H+95),shot 模式**原样传 (W, H)**。
⚠️ 视口宽度下限 500(窗口最小 516):`--window-size=390,844` 拿到的仍是 500×844。
要 390px 视口,本机 CLI 做不到(需要 CDP 的 setDeviceMetricsOverride),
所以窄视口只测到 500×844 —— 报告里⛔ 不许写成 390。
🔴 2026-10-08 改:**连已有浏览器实例,不再自己起 headless Chrome**。
旧写法是 `chrome.exe --headless=new --dump-dom/--screenshot`,两个后果:
① 违反本机明令(见 `skills/browser-harness/SKILL.md` 开头「先探测、能复用就复用」);
② 只能靠 `--window-size` 猜视口 —— 而且**两条通道对同一个 window-size 的解释不同**
(dom 通道会扣掉 16 / 95,截图通道不扣),还得在调用侧补偏移量;
更糟的是它有**视口宽度下限 500**(Chrome 窗口最小 516),390px 根本量不了。
现在改用 CDP 的 `Emulation.setDeviceMetricsOverride`:视口是**声明式的,要多少是多少**,
上下限与偏移量的坑一并消失(390px 现在能量了)。
实例地址默认 `http://127.0.0.1:9223`(可用环境变量 `BU_CDP_URL` 覆盖)。
⚠️ 实例没起来时**直接报错退出**,⛔ 不自动拉起浏览器 —— 拉起谁、拉几个,那是人的决定。
"""
import argparse, os, subprocess, sys
from urllib.parse import quote
import argparse, os, socket, subprocess, sys, tempfile
from urllib.parse import quote, urlparse
CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
HERE = os.path.dirname(os.path.abspath(__file__))
PROTO = os.path.abspath(os.path.join(HERE, "..", "mcn-workbench.html"))
FRAME_W, FRAME_H = 16, 95 # window-size 与视口的实测差值
MIN_VP_W = 500 # 视口宽度下限(Chrome 窗口最小 516)
HARNESS = os.path.expanduser("~/.local/bin/browser-harness.exe")
BU_URL = os.environ.get("BU_CDP_URL", "http://127.0.0.1:9223")
SCRIPT = r"""
import io, json
BU = {bu!r}
url = {url!r}
out = {out!r}
mode = {mode!r}
W, H = {w}, {h}
t = new_tab(url)
cdp("Emulation.setDeviceMetricsOverride", width=W, height=H, deviceScaleFactor=1, mobile=False)
wait_for_load()
wait(0.8)
if mode == 'dom':
vw = js("innerWidth"); vh = js("innerHeight")
assert (vw, vh) == (W, H), "视口不符:要 %dx%d,实到 %dx%d" % (W, H, vw, vh)
html = js("document.documentElement.outerHTML")
io.open(out, 'w', encoding='utf-8').write("<!DOCTYPE html>\n" + html)
print("VIEWPORT %dx%d" % (vw, vh))
else:
vw = js("innerWidth"); vh = js("innerHeight")
assert (vw, vh) == (W, H), "视口不符:要 %dx%d,实到 %dx%d" % (W, H, vw, vh)
capture_screenshot(path=out, full=False)
print("VIEWPORT %dx%d" % (vw, vh))
cdp("Emulation.clearDeviceMetricsOverride")
close_tab()
"""
def _probe(host="127.0.0.1", port=9223):
s = socket.socket()
s.settimeout(1.5)
try:
s.connect((host, port))
return True
except OSError:
return False
finally:
s.close()
def url(route, proto=None):
# Windows 盘符路径 -> file:///E:/...
p = (proto or PROTO).replace("\\", "/")
# ⚠️ 路由不带前导 #/ :本机 bash 是 Git Bash(MSYS),会把 "#/xxx" 当成路径改写成
# "#C:/.../PortableGit/xxx"(实测踩过)⇒ 这里由脚本自己拼 "#/"。
h = route.strip().lstrip("#").lstrip("/")
return "file:///" + quote(p, safe="/:") + ("#/" + h if h else "")
@@ -45,34 +78,44 @@ def main():
ap.add_argument("--mode", choices=["dom", "shot"], required=True)
ap.add_argument("--route", default="p/p1/ok", help="例:selftest / p/p4/empty(⛔ 不带 #/)")
ap.add_argument("--hash", default=None, help="兼容旧参数(不带 #/)")
ap.add_argument("--viewport", default="1440x900", help="目标 CSS 视口,例 1440x900 / 500x844")
ap.add_argument("--viewport", default="1440x900", help="目标 CSS 视口,例 1440x900 / 500x844 / 390x844")
ap.add_argument("--proto", default=None, help="要渲染的文件(默认本目录的 mcn-workbench.html;变异对照用)")
ap.add_argument("--out", required=True)
ap.add_argument("--budget", default="6000")
a = ap.parse_args()
route = a.hash or a.route
vw, vh = [int(x) for x in a.viewport.lower().replace(",", "x").split("x")]
vw = max(vw, MIN_VP_W)
# 🔴 dom 与 shot 两条通道对 --window-size 的解释不同(见文件头实测):截图通道原样等于视口,
# dump-dom 通道要补回被扣掉的 16 / 95。这里分开算,⛔ 别再用同一个偏移量。
win = ("%d,%d" % (vw + FRAME_W, vh + FRAME_H)) if a.mode == "dom" else ("%d,%d" % (vw, vh))
out = os.path.abspath(a.out)
os.makedirs(os.path.dirname(out), exist_ok=True)
cmd = [CHROME, "--headless=new", "--no-sandbox", "--disable-gpu", "--hide-scrollbars",
"--window-size=" + win, "--virtual-time-budget=" + a.budget,
"--run-all-compositor-stages-before-draw"]
if a.mode == "shot":
cmd += ["--screenshot=" + out]
else:
cmd += ["--dump-dom"]
cmd += [url(route, a.proto)]
r = subprocess.run(cmd, capture_output=True)
host = urlparse(BU_URL).hostname or "127.0.0.1"
port = urlparse(BU_URL).port or 9223
if not _probe(host, port):
sys.stderr.write("⛔ %s 上没有浏览器实例在跑。\n"
" 本工具**只连不起**(本机明令)。先把常驻实例起来,或者\n"
" 用 browser-harness 自带的 daemon 流程,再重跑本命令。\n" % BU_URL)
return 2
script = SCRIPT.format(bu=BU_URL, url=url(route, a.proto), out=out, mode=a.mode, w=vw, h=vh)
tf = tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8")
tf.write(script)
tf.close()
env = dict(os.environ)
for k in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
env.pop(k, None)
env["NO_PROXY"] = "127.0.0.1,localhost"
env["BU_CDP_URL"] = BU_URL
r = subprocess.run([HARNESS], stdin=None, capture_output=True, env=env,
input=open(tf.name, "rb").read())
os.unlink(tf.name)
err = r.stderr.decode("utf-8", "replace")
ok = r.stdout.decode("utf-8", "replace")
if a.mode == "dom":
with open(out, "wb") as f:
f.write(r.stdout)
sys.stderr.write("viewport %dx%d → window %s | dom bytes=%d\n" % (vw, vh, win, len(r.stdout)))
sys.stderr.write("dom → %s | %s | bytes=%d\n"
% (out, ok.strip() or "(没说视口)", os.path.getsize(out) if os.path.exists(out) else -1))
else:
# 🔴 截图出来必须**当面对尺寸**:这条通道曾经因为偏移量算错,把 1456×995 当成
# 「1440×900」交付出去(同一份报告里 dump-dom 又按 640 说话 ⇒ 两份证据互相打架)。
@@ -89,10 +132,11 @@ def main():
else:
sys.stderr.write("shot ⛔ 尺寸不符:目标视口 %dx%d,PNG 实际 %dx%d ⇒ 别拿它当 %dx%d 的证据\n"
% (vw, vh, pw, ph, vw, vh))
err = r.stderr.decode("utf-8", "replace")
if "ERROR" in err.upper():
if "Traceback (most recent call last)" in err:
sys.stderr.write(err[-1500:] + "\n")
return 1
return 0
if __name__ == "__main__":
main()
sys.exit(main())