LobeHub ux-audit 动态审计(L3):基于 agent-browser 与 CDP 插桩的用户旅程验证与性能量化
导读
本篇文章讲解 LobeHub 仓库中 .agents/skills/ux-audit/references/layer-3-dynamic.md 所定义的第三层动态审计(Layer 3 — Dynamic Audit):它在前两层(读代码、看渲染)的基础上,以自动化方式像真实用户一样驱动产品界面,专门捕获那些只在运行时才存在的状态(执行中、强制的错误/空态),验证"用户旅程"能否一步一步向前推进,并产出只有数字才能回答的指标——CLS、LCP、INP、长任务(long tasks)。读完你将掌握:如何用 agent-browser 通过 CDP 连接运行中的 Electron/Web 应用,如何编写并执行一条带证据采集的用户旅程,如何用 Network.emulateNetworkConditions 强制错误/空态,如何在页面加载前注入 Web Vitals 观察器并读出量化结果,以及如何把 L3 的结论融入 ux-audit 的共享报告。
一、为什么需要第三层:L1/L2 看不见的东西
LobeHub 的 ux-audit 技能把一次界面审计拆成三个互补的分层,全部定义在 ux-audit 技能入口 SKILL.md:
| 分层 | 程序文件 | 做什么 | 能抓到的问题 |
|---|---|---|---|
| L1 静态 | layer-1-static.md | 读代码 | 缺失的分支(空态/错误/重试)、草稿未持久化、结构性问题 |
| L2 视觉 | layer-2-visual.md | 看渲染截图 | 真实视觉层级、间距/对比/对齐、截断/溢出、各状态的真实长相、响应式断点、明暗主题 |
| L3 动态 | layer-3-dynamic.md | 用 acceptance 框架驱动真实用户旅程 + 插桩测量 | 执行中/锁定态、强制的错误/空态、步骤 N 是否通向 N+1、焦点/键盘、量化的 CLS/LCP/INP/长任务 |
一句话概括三者的分工:L1 读代码,L2 看渲染,L3 像用户一样驱动产品并测量它。这是唯一能够触达"只存在于运行期"状态的分层,是唯一验证旅程"步骤到步骤是否缝合"的分层,也是唯一能产出数字(CLS、LCP、INP、长任务)的分层——而截图和读代码都给不了这些数字。
ux-audit 的 SKILL.md 覆盖矩阵 明确规定了"哪个结论必须由哪个分层给出"的核心纪律:结论必须来自能看到它的分层,不能用代码去勾掉一个视觉/运行时的结论。据此,L3 独占或显著高于其他层的判定类型包括:
- 执行中(in-progress)/锁定(locked)状态、强制错误/空态、能力受限(capability-gated)——只有 L3 能确认;
- 旅程缝合(跨步骤的前进动量)——L1/L2 都只能给出"weak"级别的判断;
- 焦点顺序 / 键盘可达性——只有 L3 能确认;
- CLS / LCP / INP / 长任务数字——L1 完全不能,L2 只能定性;
- 两个变体哪个"更好"(A/B 胜负)——本质是行为结果,必须由 L3(+ 数据分析)裁决,L1/L2 若做"机械对比"可以,但禁止宣布胜者。
什么时候需要跑 L3
- L1 永远跑(廉价、离线、全覆盖的基线);
- 当问题集中在布局、层级、渲染状态或响应式时加跑 L2;
- 当需要走一条旅程、强制 L1/L2 到不了的状态、或量化 CLS 等性能指标时,加跑 L3。
每次审计只针对一个 surface,随着产品迭代按页面重复运行,这就是"持续审计"的含义。这也是为什么官方示例报告 home.md 在 L1 阶段会为尚未执行的 L3 单独预留一节,见 references/example/home.md 第 5 节。
二、前置条件:先让 acceptance 框架的"环境 + 认证"变绿
L3 是整个审计体系里成本最高的一层——它需要一个运行中的环境。layer-3 文档明确要求:L3 假定 acceptance 框架的 Step 0(env + auth) 已经通过。
acceptance 框架的运行流程定义在 .agents/acceptance/PROCESS.md,其核心阶段分为环境解析、依赖安装、启动服务与注入登录态:
- 先解析环境:从项目自己的环境解析器读取端口与 base URL,绝不使用硬编码端口表;解析值与正在运行的 dev server 不一致时,先修复环境再继续。
- 处理依赖:根目录安装并不覆盖
apps/desktop、apps/cli等独立应用,运行涉及哪个独立应用就要在哪个应用内单独安装。 - 启动完整环境:包含该功能依赖的每一个服务——请求会被分发到的队列、缓存、对象存储都是硬前置条件,而不是可选优化项。
- 注入认证而非驱动登录流程:通过种子会话、cookie/状态恢复或 CLI 签发的 token 直接注入登录态,绝不驱动交互式登录/OAuth 流程——那会劫持用户的浏览器会话。
- 屏幕录制预检(仅对 OS 级采集):用
.agents/acceptance/scripts/check-screen-recording.sh把关权限与屏幕状态;而 CDP 采集(agent-browser screenshot、cdp-screenshot.sh等)不受影响。
Layer 3 文档提到的每一条 CDP 操作之所以"也能在云端的 headless 环境、xvfb-run 下工作",正是因为整个证据链基于 CDP 渲染而非 OS 级屏幕捕获。相关命令(端口、服务、认证、surface、探针)的统一清单见 .agents/acceptance/PROJECT.md。
三、驱动器:通过 CDP 使用 agent-browser
L3 的驱动器是 agent-browser over CDP(Chrome DevTools Protocol),即连接到正在运行的应用(Electron 或 Web)并对其进行操控与取证。其核心命令与在审计中的用途如下:
| 命令 | 在一次审计中的用途 |
|---|---|
agent-browser --cdp 9222 snapshot -i |
抓取 accessibility/DOM 树 + 可交互元素(查找控件、断言存在性) |
agent-browser --cdp 9222 screenshot |
在每一步采集渲染证据(喂给 L2 检查) |
agent-browser --cdp 9222 eval "<js>" |
插桩——注入 web-vitals 观察器、读取状态、强制条件 |
type / click(见 acceptance 框架的 surfaces/) |
驱动用户旅程 |
scripts/record-gif.sh |
基于时间线的证据(流式输出、布局跳动) |
组合使用时的典型节奏是:snapshot -i 确认控件可交互并断言存在 → click/type 推进旅程 → screenshot 在每个状态点留证 → eval 注入观察器或读取运行期状态。
一条硬约束:响应式扫描不要靠缩放 Electron 窗口
layer-3 文档特别强调:缩放 Electron 窗口会触发一次完整的 SPA 重载。因此凡是涉及"响应式 / 多视口"的扫描,都必须针对 web Chrome over CDP 来做,而不是通过拉伸 Electron 窗口来模拟。这条约束让 L3 在桌面端之外拥有了干净的移动视口取证途径。
时间型证据:record-gif.sh
type/click/screenshot 都是静态帧取证,而当断言对象是"随时间变化的行为"——流式输出、计时器跳动、加载状态、动画——时需要 GIF。仓库中的 .agents/acceptance/scripts/record-gif.sh 负责把 CDP 帧序列合成为 GIF,用于嵌入测试报告:
# 用法:record-gif.sh <output.gif> <duration_seconds> [fps]
./record-gif.sh "$DIR/assets/case2-tray-running.gif" 12 2 &
GIF_PID=$!
# ... 触发流式/动画行为 ...
wait $GIF_PID
它的可配置环境变量包括:AB_TARGET="--cdp 9222"(Electron,默认;遵循 CDP_PORT)、AB_TARGET="--session your-session"(Web agent-browser 会话)以及可选的 GIF_WIDTH(输出宽度,默认保持源分辨率)。脚本依赖 ffmpeg,由于每帧截图本身有约 0.3~0.5 秒延迟,实际可用帧率在 1~2 fps 这个现实区间。值得注意的工程细节:它对每一帧单独生成调色板并关闭抖动(palettegen=stats_mode=single:max_colors=256:reserve_transparent=0 + paletteuse=new=1:dither=none),因为一个全局 256 色调色板会把细微的中性骨架变成彩色噪点。这是"云兼容"的原因所在——帧来自 CDP 渲染,不涉及 OS 级录屏权限。
四、旅程模板(Journey Template):按有序步骤走并逐步取证
审计的单元是 User Journey——把某个 surface 的用户旅程定义成有序步骤,逐一定位每一层并驱动它们,在每个步骤采集证据、对"前进动量"做断言。下面以 home → task 为例说明模板长什么样:
- 着陆在该 surface →
snapshot+screenshot;断言主控件是否可聚焦(focusable)。 - 执行核心动作(输入并发送) → 先捕获执行中(in-progress)状态(是否展示?是否锁定输入?),再捕获完成(done)状态;断言它是否向前引导(步骤 2 有没有暴露进入步骤 3 的入口)。(对应 ux 规范 Act §3.1)
- 强制错误路径(方法见下一节)→ 断言出现"失败 + 重试",且已输入内容被保留。(对应 ux 规范 §4.2 / §2.1)
- 强制空路径 → 断言出现的是精心设计的空态页面,而不是空白页或卡死状态。
每一步都要记录:snapshot、screenshot,以及对照期望状态的 pass/fail。凡是"没有前进路径的步骤、死掉的 spinner(永久加载)、或输入被清空"的步骤,都构成一条审计发现(finding)。
这套模板把抽象规范变成了可判定断言:某一步没有前路、某个 spinner 永不结束、某次发送把输入框抹掉了——这三个就是动态层最典型的三类问题。
五、状态强制手册(State-Forcing Cookbook)
L1 读代码时只能"推断"某个状态的分支是否存在,L2 只能截到"当前已经渲染出来"的状态。而 L3 的价值在于把那些代码里存在、但正常路径走不到的状态真正制造出来:
- 错误 / 离线(error/offline)——用 CDP 的
Network.emulateNetworkConditions {offline:true}模拟离线(或按路由屏蔽特定请求),然后触发那次 fetch,观察失败 + 重试 UI 是否出现。这正是**当面(live)抓出"永久骨架屏(permanent skeleton)"**的手段:一个只在 fetch 成功后才置位的 init 标志,会让失败请求表现为"永远加载"。 - 慢速 / 保持加载(slow / hold-loading)——用 CDP 限速(如 slow 3G)把骨架屏"冻住"足够久,方便截图与目测判断(L2 职责),同时观察它是否最终会超时。
- 空(empty)——用一个全新 / 空账号,或清空相关 store/表,渲染出首次运行的空态。
- 能力受限(capability-gated)——选中一个不具备某能力的模型,断言出现软性警告(对应 ux 规范 §4.2 capability),并且在切回具备能力的模型后警告消失。
把这些状态照进 L1 的检查点就形成了完整闭环:L1 在代码里发现 isInit 只在成功时置位并标记"疑似永久骨架",L3 用离线强制在真实运行中确认它。
六、性能与 CLS 插桩:把"卡"变成数字
截图无法量化抖动(jank),因此 L3 通过 eval 注入观察器:在导航/触发之前注入,等 surface 稳定后再读取累积值。
CLS(Cumulative Layout Shift,累计布局偏移)——L1/L2 都测不了的那一项:
// 1) 在加载/触发之前注入:agent-browser --cdp 9222 eval "<这段代码>"
window.__cls = 0;
new PerformanceObserver((l) => {
for (const e of l.getEntries()) if (!e.hadRecentInput) window.__cls += e.value;
}).observe({ type: 'layout-shift', buffered: true });
// 2) 等 surface 稳定之后读取:agent-browser --cdp 9222 eval "window.__cls"
其余指标采用同样的形态:observe({ type: 'largest-contentful-paint' }) 得到 LCP;observe({ type: 'longtask' }) 观察主线程阻塞;INP 则在页面带有 web-vitals 库时通过它来测。
为什么要精确地在"加载→内容替换"之后读取?因为骨架屏与真实内容的高度不匹配(skeleton-height mismatch,对应 ux 规范 Feedback §4.1)恰恰是在这个时间窗口显形——加载切换的瞬间正是布局偏移最容易爆发的时刻。L2 只能靠前后对比截图"定性"地展示一次跳动,并把症状转交给 L3;L3 负责把数字测出来。这与 layer-2-visual.md 的收尾建议完全对齐:CLS 的量化一定归属 L3。
核心 Web Vitals 判定阈值(报告时同时给出数字与结论):
- CLS:≤ 0.1 为良好(good)· ≤ 0.25 为需改进(needs-work)· 再高为差(poor)
- LCP:≤ 2.5s 为良好
- INP:≤ 200ms 为良好
报告时给出数字 和 判定,并且指出发生偏移的块(point at the block that shifted)——没有定位的量值对修复没有帮助。
七、诚实的边界(Honest Limits)
L3 不是万能层,文档明确列出它的四类限制:
- 依赖运行环境 + 认证通过(acceptance PROCESS Step 2),比 L1/L2 更慢、更容易 flaky——一条失败的步骤要先重跑一次再采信,避免把偶发抖动当成产品缺陷。
- 并非所有状态都容易强制:某些错误路径需要路由级 mock 或 fixtures 才能制造出来。
- 这里截到的截图仍然需要一轮 L2 视觉审查——引用前必须先"读图"核实(这正是 SKILL.md 中"证据而非感觉"这一基本规则的延伸:被引用但你没看过的截图只是 vibe,不是证据)。
- 只做 L3 独有的事——旅程、强制状态与指标;不要在这里重新推导 L1/L2 已能给出的结论。
官方报告 example/home.md 的第 5 节就是这条原则的正面示范:一篇 L1-only 的审计把若干待确认结论显式标注为 "pending L2/L3",并规划出 L3 具体要做什么——"强制简报离线以当面对证永久骨架屏(确认 gap ①)""驱动发送旅程确认 in-progress/locked 状态与前进引导(确认 gap ④)""注入 layout-shift 观察器量化 home 的 CLS 数字并给出结论"。代码里骨架覆盖看起来很全,但只有指标能证明它没有跳。
八、输出贡献:把动态证据汇入共享报告
一次 journey 级别的 L3 输出贡献包含三部分,全部汇入 ux-audit 的共享报告格式(其完整工作示例与报告骨架见 references/example/task-detail.md 与 references/example/home.md):
- 逐步 pass/fail——每条 journey 带
snapshot/screenshot证据; - 强制状态发现(forced-state findings)——错误/空/加载态中,那些 L1 只是"推断"、现在被当面确认的问题(如永久骨架屏、死 spinner);
- 性能表——CLS / LCP / INP / 长任务,逐项给出数值与判定结论。
落入共享报告后,这些结论会汇入 ux-audit 统一的严重性分级与"落地(land)"流程:具体缺陷修复或建单、可泛化缺口回灌进 ux 技能清单(补强规则及 ❌ 示例)、优秀案例作为 ✅ 示例落回清单,审计报告本身则存档为下一次运行的模板。这也是整个 ux-audit 技能"封闭循环"的一环——ux 是被审计的基准,而审计是让 ux 清单不断变得更敏锐的机制。
九、与相邻层协作的要点小结
- L1 ↔ L3:L1 找出"疑似"运行时缺陷(缺错误分支、无重试、草稿未持久化、init 只在成功时置位),L3 负责把它们从代码里"请"到真机上确认。
- L2 ↔ L3:L2 提供"看起来如何"的定性判断(层级、空态是否像真页面、CLS 症状),L3 提供"数值是多少"的量化结论;L3 截到的每一张图仍需 L2 的视觉复核。
- 评测基准:L3 每一步断言对应的规范条目来自 ux 技能(如 Act §3.1、Feedback §4.1/§4.2、Read §1.1、Edit §2.1)与 Tidwell 模式目录 pattern-catalog.md。
对追求可重复、基于证据而非感觉的 UI 审计而言,L3 的独特价值在于它给"用户体验"补上了其他两层永远拿不出的东西:真实旅程的缝合性、被强制出来的异常态、以及可以放进报告的 CLS/LCP/INP 数字。把这些数字与文档中的核心 Web Vitals 阈值对齐并定位到偏移块,一份动态审计就不再是主观点评,而是可以回归、可以对比、可以驱动修复的工程证据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00