Claude Code run-skill-generator 浏览器驱动模式:headless 环境下用 chromium-cli 验证 Web 应用的完整运行与截图取证
本文围绕 run-skill-generator 的 playwright 示例 展开,讲解在无图形界面的 Linux 容器中"运行一个浏览器驱动的 Web 应用"的完整工程方案:后台拉起 dev server 并轮询端口、用 chromium-cli 管道脚本驱动 headless Chromium 完成导航/点击/填表/截图,以及何时如何回退到自研 Playwright 驱动。读完后你将掌握一套可复用的"启动-驱动-截图-查错"闭环,并能按 run-skill-generator 主文档 的完成标准把这套流程沉淀为项目级 run skill。
一、这个示例在整个 run-skill-generator 体系中的位置
playwright.md 不是孤立文档,而是 Claude Code 内置的 run-skill-generator skill 下的六类项目模式之一。该 skill 的目标是产出位于 <unit>/.claude/skills/run-<unit-name>/ 的项目级技能,让未来的 Agent 能从干净环境构建、启动并驱动本项目的应用。主文档 SKILL.md 给出了一张项目类型到驱动形态的映射表:
| 项目类型 | 驱动形态 | 示例文档 |
|---|---|---|
| Web server / API | 后台启动 + curl 冒烟脚本 |
examples/server.md |
| CLI 工具 | 代表性参数冒烟脚本,检查退出码与输出 | examples/cli.md |
| TUI / 交互式终端 | tmux 包装:send-keys / capture-pane |
examples/tui.md |
| Electron / 桌面 GUI | xvfb 下的 Playwright _electron REPL 驱动,tmux 包装 |
examples/electron.md |
| 浏览器驱动(Browser-driven) | dev server + chromium-cli 脚本 |
examples/playwright.md |
| 库 / SDK | import 后调用的冒烟脚本 | examples/library.md |
playwright 示例对应的正是"Browser-driven"一行。它与 run skill(/run 命令的 fallback 模式库)共享同一套按项目类型的模式——/run 在找不到项目级 run skill 时,直接按这张表回退到对应示例。也就是说,这份文档既是"生成 run skill 时的起手模板",也是"运行时没有项目 skill 时的兜底配方",双重身份决定了它必须写成可直接复制执行的命令级流程,而不是概念描述。
二、核心问题定义:headless 容器里"运行应用"意味着什么
示例开篇就把问题定义得非常明确:
你有一个 dev server 向浏览器提供 HTML。无头容器里的 Agent 打不开浏览器窗口——所以"运行应用"意味着:启动 dev server、用它打一个 headless Chromium,并产出一张证明页面渲染成功的截图。
紧接着是一条关键约束:"不要自己写浏览器驱动。用 chromium-cli。"
这与主文档 SKILL.md 中"交付物是代码加文档"的原则一致:如果一个交互式应用没有程序化的"抓手",skill 就只是一扇没人碰得到的窗口的描述。但对于 Web 应用,"点按钮的东西"已经现成存在——就是 chromium-cli,所以 skill 作者要做的不是造轮子,而是编写驱动它的脚本(SKILL.md 里的 heredoc 本身就是 harness)。
整个流程可以概括为四个环节:
- Dev server:找到 dev 命令,后台启动,轮询端口直到真正可服务;
- Drive:把脚本通过 stdin 管道喂给
chromium-cli,执行导航、等待、交互、截图; - What to put in the skill:只把项目专属部分(命令、端口、停止方式、登录、代表性操作、踩坑)写进 skill;
- Gotchas:React 受控输入、长连接、慢首屏等反复出现的陷阱。
三、第一步:启动并等待 dev server
3.1 找到 dev 命令
文档给出的查找顺序是:package.json 的 scripts.dev、Makefile、README。然后用合适的包管理器后台启动它:
npm run dev & # 或 yarn dev、pnpm dev、make serve、./dev.sh
timeout 30 bash -c 'until curl -sf http://localhost:3000 >/dev/null; do sleep 1; done'
注意第二个命令的细节:不要 sleep 5,要轮询端口。timeout 30 给轮询加了上限,避免 dev server 起不来时永久挂死;curl -sf 的 -f 让非 2xx 响应返回非零退出码,因此轮询条件等价于"服务真的能响应"。
3.2 停止服务:杀端口监听者,而不是杀 $!
文档明确指出,重新启动前必须先停掉旧进程,否则下次运行会撞上 EADDRINUSE:
lsof -ti:3000 -sTCP:LISTEN | xargs -r kill
这里有一段很容易踩坑的说明,值得逐句理解:
npm run dev &之后的$!只是 npm 包装进程的 PID。npm 不会把 SIGTERM 转发给它拉起的服务进程,所以kill $!杀不掉真正占着端口的 server——真正释放端口的是"杀端口监听者"这一招。server.md 示例里也重复了这一结论,说明这是跨项目类型的通用教训。- 避免用宽泛模式的
pkill -f。宽模式可能匹配到 Agent 自己的命令行,把当前会话一起杀掉。用lsof -ti:PORT -sTCP:LISTEN精确锁定"该端口上处于 LISTEN 状态的进程"是最安全的做法。
四、第二步:用 chromium-cli 驱动页面
4.1 管道脚本驱动
chromium-cli 是一个 headless-Chromium 的 REPL,直接把脚本从 stdin 管道喂进去即可执行。文档给出的完整示例:
chromium-cli --session app <<'EOF'
nav http://localhost:3000
wait-for text=Dashboard
screenshot
click button:has-text("New item")
fill input[name="title"] Smoke test
press Enter
wait-for text=Smoke test
screenshot
console --errors
EOF
逐行解读这条脚本(也是示例中最值得复制的"最小完整闭环"):
| 命令 | 作用 |
|---|---|
nav http://localhost:3000 |
导航到 dev server |
wait-for text=Dashboard |
等待目标文本出现,确认页面真正就绪 |
screenshot |
首屏截图,证明页面渲染了 |
click button:has-text("New item") |
点击含指定文本的按钮(Playwright 风格的 CSS 扩展选择器) |
fill input[name="title"] Smoke test |
向指定输入框填充文本 |
press Enter |
提交表单 |
wait-for text=Smoke test |
等待操作结果在页面上出现 |
screenshot |
操作后截图,留下第二份证据 |
console --errors |
导出控制台报错,确认没有 JS 异常 |
文档点明了截图的落盘位置:chromium_cli/sessions/app/screenshots/(由 --session app 决定),最新一张以 screenshot.png 软链。
4.2 完整循环与迭代调试
文档把整个驱动循环总结为一句话:nav → wait-for 你需要的那个元素 → 执行动作(click / fill / type / press)→ screenshot → console --errors 确认没有抛错。完整命令参考在 chromium-cli 对应的 skill 中,或在 REPL 提示符下输入 help 查看。
对于需要反复试验的场景,文档给出了一条实用技巧:把 chromium-cli 跑在 tmux 里,用 send-keys 一次发一条命令——同一套命令、同一个会话,不必每次重来。这个技巧与 electron.md 中驱动 REPL 的 tmux 包装、以及 template.md 里"在 send-keys 与 capture-pane 之间轮询就绪标记"的做法是同源的,构成了 Claude Code 无头操作交互式程序的统一方法论。
五、回退方案:chromium-cli 不可用时自研 Chromium 驱动
文档给出了明确的降级路径:如果环境里没有 chromium-cli,就改造 electron.md 里的 REPL 驱动——结构和命令可以平移,但那是 _electron 专用的,需要做三处修改:
- 改导入:
import { chromium }替代_electron; - 改启动:
chromium.launch({ args: ['--no-sandbox'] })。--no-sandbox在容器里几乎总是必需的(electron.md 中解释:Electron/Chromium 的 sandbox 需要 CAP_SYS_ADMIN 或 user namespaces,默认容器两者都没有); - 改取页:
(await app.newContext()).newPage()再goto()到 dev URL,并删掉 Electron 专属的窗口内省(.windows()/.firstWindow()/windows命令)——那些命令在 electron.md 的 REPL 骨架里专门用来排查"真实 UI 在哪个 window/BrowserView",对普通 Chromium 没有意义。
这个回退方案的设计意图与 electron.md 的 REPL 形态一致:驱动是一个读 stdin 命令、执行 Playwright 动作的脚本,跑在 tmux 里,Agent 用 send-keys 逐条迭代,避免每次交互都重新启动(缓慢的)应用。
六、skill 里应该写什么:只写项目专属部分
文档对"skill 内容边界"的划法非常干脆:chromium-cli 负责机制,skill 只记录项目专属的东西。具体是四项:
- Dev 命令 + 端口 + 停止方式。精确的启动行、它需要的环境变量,以及用来停掉它的
kill命令(即上面第三节的lsof -ti:PORT -sTCP:LISTEN | xargs -r kill)。 - Auth。任何能让页面处于登录态的手段:一行
set-cookie、一段fill/click的登录序列,或一个完成 API 交互并吐出 cookie 的辅助脚本。 - 一条代表性操作。不是整站巡检——就一条能证明"应用在跑"的路径,并以截图收尾。
- 应用专属的 gotchas。只写你实际踩过的坑,不写泛泛而谈的建议。
这与 run-skill-generator/SKILL.md 的"完成定义"(Definition of done)严格对齐:
- 必须在本容器中真正启动过应用并与之交互过,GUI 应用要有"你亲手截的"截图文件;
- 交互 harness 必须与 skill 一起提交(对 Web 应用,
SKILL.md内联的chromium-cliheredoc 本身就是 harness,不需要单独文件); SKILL.md要把 harness 作为首要的 Agent 路径来文档化;SKILL.md里每个代码块都必须是本次会话里实际跑通过的命令——不是从 README 抄的,不是推断的。
主文档还列了一个"红旗清单",其中两条直接对应本文场景:GUI 应用没截图 = 你没运行过;skill 读起来像 README = 你在转述而不是验证。
七、反复出现的 Gotchas(原文档全部继承并展开)
文档最后列举了五类反复出现的陷阱,每一条都值得对照检查:
- React 受控输入。
eval el.value = '...'直接改 DOM 的 value 不会触发 React 的 onChange。要用fill/type——它们走的是 Playwright 的输入管线(模拟真实键盘事件 + 触发 input/change),而eval赋值绕过了框架的事件系统。 - Websocket / 长轮询。
wait-idle(等待网络空闲)在这种页面永远不会稳定。对策是放弃"整体空闲"语义,直接wait-for你实际需要的元素。 - 首屏慢。Vite/Next 按需求编译路由,第一次
nav可能耗时 10 秒以上。wait-for能处理这种不确定延迟,裸sleep不能(sleep 太短会漏、太长会拖)。 screenshot-element <sel>。可裁剪到单个元素截图——当差异出在某个具体组件(而不是整页)时用它,便于定位与对比。- 先查
console --errors再宣布成功。页面可以渲染出外壳(shell),同时所有数据请求全部 500——视觉上"渲染了",功能上完全失败。这也是 run/SKILL.md 反复强调的原则:"看着那张截图",空白或错误页就是启动失败。
八、端到端串一遍:从干净容器到可复用的 run skill
把本文各节按 run-skill-generator/SKILL.md 规定的流程(发现 → 执行并自建 harness → 写 SKILL.md → 逐行验证)串起来,一个浏览器驱动 Web 应用的完整操作链是:
- 发现:查
package.jsonscripts /Makefile/ README 确定 dev 命令与端口;CI 配置往往比 README 更准(主文档原话:CI configs are often more accurate than READMEs)。 - 执行:
npm run dev & timeout 30 bash -c 'until curl -sf http://localhost:3000 >/dev/null; do sleep 1; done' - 驱动(把实际 dev URL 与真实元素代入后执行):
chromium-cli --session app <<'EOF' nav http://localhost:3000 wait-for text=Dashboard screenshot console --errors EOF - 取证:打开
chromium_cli/sessions/app/screenshots/screenshot.png,确认是真实页面而不是空白/错误页;检查console --errors输出。 - 停止:
lsof -ti:3000 -sTCP:LISTEN | xargs -r kill。 - 沉淀:按 template.md 的结构(frontmatter、Prerequisites、Setup、Build、Run (agent path)、Run (human path)、Test、Gotchas、Troubleshooting)写入
SKILL.md,其中 frontmatter 的name:成为斜杠命令(如/run-billing),description:要包含 Agent 真正会打的动词——"run"、"start"、"build"、"test"、"screenshot"——因为它是 Claude 判断是否自动加载该 skill 的匹配依据。 - 验证:开一个全新 shell,逐行照
SKILL.md执行,任何一处需要"临场发挥"都说明文档有缺口,回去修掉。
九、小结
这份 playwright 示例的价值不在命令本身,而在它体现的一套无头验证方法论:
- 等待用轮询,不用猜:端口用
curl轮询,页面用wait-for轮询,tmux 用就绪标记轮询——全链路消灭sleep N; - 停止用端口,不用进程名:
lsof -ti:PORT -sTCP:LISTEN精确释放端口,规避$!只拿到包装进程、pkill -f误杀会话两个坑; - 驱动用现成的,不造轮子:
chromium-cli存在时脚本即 harness;不存在时降级改造 Playwright REPL,而非从零发明; - 成功要双重证据:截图证明渲染,
console --errors证明没炸,两者缺一不可。
结合仓库内 run skill、electron 示例、server 示例 与 skill 模板 一起阅读,可以看到 Claude Code 对"Agent 如何从干净机器把一个应用跑起来并证明它跑起来了"这一问题的完整答案:dev server + 端口轮询 + chromium-cli 管道脚本 + 截图取证 + 项目专属 skill 沉淀,六类项目形态各有对应配方,而本文覆盖的浏览器驱动形态是其中唯一"驱动工具开箱即用、只需写脚本"的一类。
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