Files
WorkBuddy df56c2c137 初始化提交:contentm_agent 工作区全量快照
内容分四块:
1、产品规划产出 —— MCN 短视频整合营销工作台的①段五份(1a 需求/1b 竞品/1c 画像/1d 策略/1e 场景)、②段两份(2a 功能/2b 布局)、③段界面(DESIGN.md 契约与令牌表 + mcn-workbench.html 原型 + 实测/会诊/审查三份 + 23 张闸门截图)。
2、开源竞品调研 —— 5 个内容工作台项目的取证原始件与 1b 系列分析文档。
3、参考资料 —— 竞品视频抽帧 1145 张 + 2 个源视频 + 功能点截图。
4、机制侧 —— 协作脚本与状态台账、工作区记忆日志、抽帧/OCR 脚本。

.gitignore 只排运行时日志、脚本备份副本与一次性探针输出,其余按原样入库。
2026-10-08 08:13:02 +08:00

99 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""无头 Chrome 取证小工具(③段 3b/3d 用)。
--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
⚠️ 本机实测(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。
"""
import argparse, os, subprocess, sys
from urllib.parse import quote
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)
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 "")
def main():
ap = argparse.ArgumentParser()
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("--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)
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)))
else:
# 🔴 截图出来必须**当面对尺寸**:这条通道曾经因为偏移量算错,把 1456×995 当成
# 「1440×900」交付出去(同一份报告里 dump-dom 又按 640 说话 ⇒ 两份证据互相打架)。
# 现在直接从 PNG 的 IHDR 里读真实像素,跟目标视口比,不等就报红。
if not os.path.exists(out):
sys.stderr.write("shot FAILED\n")
else:
with open(out, "rb") as f:
head = f.read(24)
pw = int.from_bytes(head[16:20], "big")
ph = int.from_bytes(head[20:24], "big")
if (pw, ph) == (vw, vh):
sys.stderr.write("shot ok: %s(视口 %dx%d,PNG 实测 %dx%d ✓)\n" % (out, vw, vh, pw, ph))
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():
sys.stderr.write(err[-1500:] + "\n")
if __name__ == "__main__":
main()