1实现原理 · 为什么它能做到
整个 skill 的骨架是「渲染证据分级制」:A=真实可见浏览器/原生窗口驱动,B=同态 DevTools 或项目自有 E2E,C=自带脚本的全新无头机械扫描,D=源码/lint/build/单测;并显式禁止把低层级证据升格成强结论。自带 Playwright 扫描被明确定位为 Level C,不是最终裁决。
| C | Fresh headless mechanical sweep from the bundled script | Overflow, wrapping, images, basic layout heuristics, screenshot candidates |
自带脚本刻意不带任何 npm 依赖:Playwright 从**被审计项目**的 node_modules 解析,先用 createRequire(被审计项目/package.json),再回落到 skill 自身目录;项目里没有 Playwright 时不是去安装,而是降级并如实上报。
createRequire(`${process.cwd()}/package.json`), // the audited project
扫描不是静态分析:整个 auditPage 检测函数被 page.evaluate 注入真实 Chromium 页面内执行,用 getBoundingClientRect / scrollWidth-clientWidth / getComputedStyle / checkVisibility 在真实排版引擎上取值后回传结构化 issues。
const result = await page.evaluate(auditPage, {
「文字有没有真的渲染出来」是逐字符判定的:visibleTextFromNode 用 Range 把每个字符单独取框,再逐层减去祖先 overflow:hidden/clip 的裁剪矩形,被裁掉的字符替换为空格;因此 --forbid/--require 检查的是真实可见文本,不是 textContent。
range.setStart(node, index);
溢出与裁切是数值计算而不是「列出可疑元素」:元素级 overflowX = el.scrollWidth - el.clientWidth,页面级用 documentElement.scrollWidth - clientWidth,区块级另算 scrollWidth-clientWidth 与 rect.width - documentElement.clientWidth 两个不同口径,超阈值才产出带 px 数值的条目。
const overflowX = el.scrollWidth - el.clientWidth;
媒体缺陷按状态拆分而不是通过/失败:selected / rendered / notRendered / loaded / broken / stillLoading / deferredLazy / missingSource,并把「首屏外 + lazy + 尚未请求」单独归入 deferredLazy,避免把没请求当成坏图。
const isDeferred = !mediaWalkRequested
找「静默降级」用可证伪的测量而不是信任 API 自述:font 模式渲染含拉丁字符的探针串,与一个故意不存在的字体族宽度做差;document.fonts.check() 只作对照输出(它在完全没有 @font-face 时也返回 true)。
const sentinel = measure('"NoSuchFamily__PROBE__"');
font 模式给五种互斥结论(platformGeneric / healthy / hostProvidedOnly / declaredButUnusable / absentStackEntries),只把 hostProvidedOnly 与 broken asset 判为缺陷——因为把「内置且正常」与「从未发布」混为一谈,会导致用户删掉本来可用的 fallback。
const hostOnly = custom.filter((f) => f.actuallyRenders && !f.shippedByPage);
class 模式检测「组件库大版本改名后整组 CSS 规则永不匹配」这类不报错的降级:从项目 CSS 抽出被引用的 .ant-* 类名,逐个到安装的库 CSS 里查是否存在,零命中即 dead。
Rules targeting them match nothing and fail silently — the component keeps the library's stock look.
证据纪律被写进代码而不只写在提示词里:报告中的 URL 一律结构化脱敏(只留 origin + /<redacted-path> + 查询键名 + #<redacted-fragment>),并把目标相关的敏感 token 从渲染文本中打码;header 只能经环境变量间接传入,原样 --header 在启动浏览器之前就被拒绝。
process.stderr.write('--header is disabled; use --header-env for every header value\n');
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| package | Playwright(playwright-core / playwright / @playwright/test 三选一,从被审计项目解析) |
| cli | 本机 Chrome / Chromium / Edge 可执行文件(findChrome 的候选路径,可用 CHROME_PATH 覆盖) |
| network | 被审计目标 URL 及其子资源(唯一网络目的地,由调用者经 --url 指定) |
| cli | Node.js 运行时与内置模块(node:crypto/fs/os/path/url/module);自带回归用 node --test |
4风险提醒 风险提醒:橙色 · 评估后使用
- 凭证与登录态:--header-env 会把环境变量里的值作为请求头发给目标 origin,--storage 会读入 Playwright 登录态文件(cookie/localStorage)。 — 默认不触发,需调用者显式写出 flag;但如果把生产 token 放进环境变量或复用真实账号的 storage-state 去审生产站,凭证就会随审计请求离开本机。用前确认目标站与账号范围。
- TLS 降级开关存在:probe 的 --ignore-https-errors 会关闭该上下文的证书校验。 — SKILL.md 只承诺『TLS stays enabled except for an explicitly trusted self-signed origin』,而代码里同一开关可用于任意 URL;自签本地环境以外不要用,且要清楚这只在自建 context 内降级。
- 模型侧提示注入面没有防护。 — 脚本不吃页面内容当代码,但审计 agent 会把渲染文本、标题、DOM 摘要读进上下文;被审计页面(尤其是第三方或用户提供的 HTML)可以埋针对 agent 的指令文本。审陌生人给的页面时该项风险实际由宿主与 agent 的通用防护承担,本 skill 未加过滤。
- 正则入口无回溯上限。 — --forbid/--require 由调用者提供,validatePattern 只校验能否编译;灾难性回溯的模式会让 page.evaluate 长时间挂住。属自伤面(输入来自自己),但在 CI 里可能表现为超时而非报错。
- --out 未被限制在临时区。 — 脚本只要求该目录不存在或为空,因此把 --out 写进被审计项目就真的会写进去;『不要为了跑扫描去改被审计项目』目前只是提示词层纪律。
- 证据为本地敏感物且默认不脱敏图片像素。 — 报告里的 URL 与渲染文本做了脱敏,但截图 PNG 是原样像素(可能含客户邮箱、订单号等);SKILL.md 明确要求 raw 截图只当临时本地证据、对外交付须做最小化裁剪衍生件,这一步依赖使用者自觉。
5第二遍独立确认
- [ok] external_deps 逐条反查调用点是否真实存在 — grep 工具全量扫 scripts/:Playwright 候选数组(playwright-core / playwright / @playwright/test)、findChrome 的 process.env.CHROME_PATH、page.goto(url, { waitUntil, timeout: 30_000 })、SKILL.md 的 node --test 四处均逐字定位到文件行;网络 token 扫描只命中两处——probe 的 route.fetch({ headers: requestHeaders, maxRedirects: 0 }) 与 visual_layout_audit.mjs 用法字符串里的 http://localhost:port/ 示例,无第三方端点,无 axios / XMLHttpRequest / WebSocket / child_process / new Function。
- [discrepancy] 安全结论反例搜索:是否存在未披露的外发/降级/隐藏脚本 — 主体结论(无第三方外发、无遥测、默认只读)成立,但发现两处 SKILL.md 未在安全语境点明的代码级能力:① probe 经 route.fetch 把 --header-env 提供的凭证以请求头形式发到目标 origin(值取自 process.env,不进 argv),文档只强调『argv 只含名字不含值』,未说『值会被转发给目标站』;② probe 有 --ignore-https-errors(ignoreHTTPSErrors: has('ignore-https-errors')),会关闭该上下文的 TLS 校验——SKILL.md 只在 sweep 段落自述『TLS stays enabled except for an explicitly trusted self-signed origin』,未提 probe 侧同一开关。两者都是显式 opt-in、需调用者自己写出 flag;未发现混淆内容、后门或隐藏脚本。
- [ok] 凭证读取与文件写入面逐条复核 — credential_reads 三条、file_writes 三条均落到代码行:process.env[envName](probe)、storageState: flag('storage') || undefined、findChrome 的 CHROME_PATH/PROGRAMFILES/LOCALAPPDATA;写入侧为 outDir(默认 os.tmpdir()/frontend-visual-qa-<ISO>-<pid>)、frontend-visual-qa-report.json、截图 PNG。脚本不读 cookie 数据库、不读 keychain、不读 .env,也无删除行为。
- [ok] 文件清单 glob 全量核对(含隐藏文件) — find -type f 得 24 个文件:SKILL.md 1 + scripts 3 + tests 2 + references 7 + evals/evals.json + evals/trigger-evals.json + evals/files 13(12 HTML + static-review.png);find -name ".*" 在 skill 目录内为空(无隐藏文件、无 .env、无嵌套仓库)。与 SKILL.md『Bundled Resources』清单一一对应,没有未列出的可执行文件。
- [ok] 『实现原理』是否夸大:功能声明 vs 实际代码能力 — SKILL.md 称自带脚本为 'Playwright-powered mechanical viewport, layout, and media-state sweep with screenshots and JSON evidence',代码确为 chromium.launch + 三档视口 + 截图 + JSON 报告;report.checked 列的七项(navigation status and final URL、effective viewport and meta viewport、page and section overflow、rendered text wrapping and clipping candidates、custom interactive-role focusability candidates、rendered image request/load state and aspect candidates、page-type contract heuristics)在 auditPage 内逐条可定位到具体计算与 push。反向也不夸大:脚本在 report.notChecked 里主动列出做不到的项(真实浏览器 chrome 与打印预览、下载/剪贴板/弹窗/原生对话框、键盘旅程与焦点外观、完整 WCAG、GIS 交互、主观 parity),自述边界严于对外宣称。
- [discrepancy] SKILL.md 关于『报告只写进唯一临时运行目录』的措辞与代码是否一致 — 默认路径确实唯一(os.tmpdir()/frontend-visual-qa-<ISO 时间戳>-<pid>,且拒绝非空 outDir 以防陈旧证据),但 --out 允许任意路径,脚本层不限制它落在被审计项目内;SKILL.md 的『Do not mutate the audited project merely to run the sweep』只是提示词层约束,代码不强制。属措辞与实现的轻微落差,非缺陷。
- [ok] 注入面复核:被审计页面内容是否会进入可执行路径 — 四个注入浏览器的函数体全部来自 skill 自带文件(auditPage / collectAttentionInventory / primeVisibleMedia / collectScreenshotSections),页面内容只作为数据被 Range 测量、正则匹配、结构描述,不参与代码执行;无 eval、无 new Function、无 child_process,也没有把页面文本拼进 argv/shell 的路径(probe 的 --select 只交给 page.locator)。残余风险在模型侧(页面可埋针对审计 agent 的指令文本,本 skill 无过滤)与用户输入侧(--forbid/--require 正则无回溯上限)。
- [ok] evals/ 的定位:是否确为自带评测夹具,能否在本地复跑出分数 — evals/evals.json 是 21 条 prompt + expectations 的行为回归集(12 条带 fixture 文件),evals/trigger-evals.json 是 24 条 should_trigger 路由集(14 true / 10 false),schema 与仓库内 daymade-skill/skill-creator/references/eval_pipeline_schemas.md 一致,且 skill-creator 的打包逻辑默认排除 root evals/(自述 'excluded by default as a development asset'),与 SKILL.md 的『excluded from packaged runtime content』吻合。但 skill 目录内没有 runner/评分器,仓库 .github/workflows/ci.yml 与 benchmark.yml 也未引用本 skill 的 tests 或 evals(grep 无命中),因此这些用例目前只在手动执行时生效。
6结论
48ff3a56b28549a2…d5c4678cb5