首页
/ Claude Code run-skill-generator 浏览器驱动模式:headless 环境下用 chromium-cli 验证 Web 应用的完整运行与截图取证

Claude Code run-skill-generator 浏览器驱动模式:headless 环境下用 chromium-cli 验证 Web 应用的完整运行与截图取证

2026-09-07 16:42:15作者:殷蕙予

本文围绕 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)。

整个流程可以概括为四个环节:

  1. Dev server:找到 dev 命令,后台启动,轮询端口直到真正可服务;
  2. Drive:把脚本通过 stdin 管道喂给 chromium-cli,执行导航、等待、交互、截图;
  3. What to put in the skill:只把项目专属部分(命令、端口、停止方式、登录、代表性操作、踩坑)写进 skill;
  4. Gotchas:React 受控输入、长连接、慢首屏等反复出现的陷阱。

三、第一步:启动并等待 dev server

3.1 找到 dev 命令

文档给出的查找顺序是:package.jsonscripts.devMakefile、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 完整循环与迭代调试

文档把整个驱动循环总结为一句话:navwait-for 你需要的那个元素 → 执行动作(click / fill / type / press)→ screenshotconsole --errors 确认没有抛错。完整命令参考在 chromium-cli 对应的 skill 中,或在 REPL 提示符下输入 help 查看。

对于需要反复试验的场景,文档给出了一条实用技巧:把 chromium-cli 跑在 tmux 里,用 send-keys 一次发一条命令——同一套命令、同一个会话,不必每次重来。这个技巧与 electron.md 中驱动 REPL 的 tmux 包装、以及 template.md 里"在 send-keyscapture-pane 之间轮询就绪标记"的做法是同源的,构成了 Claude Code 无头操作交互式程序的统一方法论。

五、回退方案:chromium-cli 不可用时自研 Chromium 驱动

文档给出了明确的降级路径:如果环境里没有 chromium-cli,就改造 electron.md 里的 REPL 驱动——结构和命令可以平移,但那是 _electron 专用的,需要做三处修改:

  1. 改导入import { chromium } 替代 _electron
  2. 改启动chromium.launch({ args: ['--no-sandbox'] })--no-sandbox 在容器里几乎总是必需的(electron.md 中解释:Electron/Chromium 的 sandbox 需要 CAP_SYS_ADMIN 或 user namespaces,默认容器两者都没有);
  3. 改取页(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 只记录项目专属的东西。具体是四项:

  1. Dev 命令 + 端口 + 停止方式。精确的启动行、它需要的环境变量,以及用来停掉它的 kill 命令(即上面第三节的 lsof -ti:PORT -sTCP:LISTEN | xargs -r kill)。
  2. Auth。任何能让页面处于登录态的手段:一行 set-cookie、一段 fill/click 的登录序列,或一个完成 API 交互并吐出 cookie 的辅助脚本。
  3. 一条代表性操作。不是整站巡检——就一条能证明"应用在跑"的路径,并以截图收尾。
  4. 应用专属的 gotchas。只写你实际踩过的坑,不写泛泛而谈的建议。

这与 run-skill-generator/SKILL.md 的"完成定义"(Definition of done)严格对齐:

  • 必须在本容器中真正启动过应用并与之交互过,GUI 应用要有"你亲手截的"截图文件;
  • 交互 harness 必须与 skill 一起提交(对 Web 应用,SKILL.md 内联的 chromium-cli heredoc 本身就是 harness,不需要单独文件);
  • SKILL.md 要把 harness 作为首要的 Agent 路径来文档化;
  • SKILL.md 里每个代码块都必须是本次会话里实际跑通过的命令——不是从 README 抄的,不是推断的。

主文档还列了一个"红旗清单",其中两条直接对应本文场景:GUI 应用没截图 = 你没运行过;skill 读起来像 README = 你在转述而不是验证。

七、反复出现的 Gotchas(原文档全部继承并展开)

文档最后列举了五类反复出现的陷阱,每一条都值得对照检查:

  1. React 受控输入eval el.value = '...' 直接改 DOM 的 value 不会触发 React 的 onChange。要用 fill / type——它们走的是 Playwright 的输入管线(模拟真实键盘事件 + 触发 input/change),而 eval 赋值绕过了框架的事件系统。
  2. Websocket / 长轮询wait-idle(等待网络空闲)在这种页面永远不会稳定。对策是放弃"整体空闲"语义,直接 wait-for 你实际需要的元素。
  3. 首屏慢。Vite/Next 按需求编译路由,第一次 nav 可能耗时 10 秒以上。wait-for 能处理这种不确定延迟,裸 sleep 不能(sleep 太短会漏、太长会拖)。
  4. screenshot-element <sel>。可裁剪到单个元素截图——当差异出在某个具体组件(而不是整页)时用它,便于定位与对比。
  5. 先查 console --errors 再宣布成功。页面可以渲染出外壳(shell),同时所有数据请求全部 500——视觉上"渲染了",功能上完全失败。这也是 run/SKILL.md 反复强调的原则:"看着那张截图",空白或错误页就是启动失败。

八、端到端串一遍:从干净容器到可复用的 run skill

把本文各节按 run-skill-generator/SKILL.md 规定的流程(发现 → 执行并自建 harness → 写 SKILL.md → 逐行验证)串起来,一个浏览器驱动 Web 应用的完整操作链是:

  1. 发现:查 package.json scripts / Makefile / README 确定 dev 命令与端口;CI 配置往往比 README 更准(主文档原话:CI configs are often more accurate than READMEs)。
  2. 执行
    npm run dev &
    timeout 30 bash -c 'until curl -sf http://localhost:3000 >/dev/null; do sleep 1; done'
    
  3. 驱动(把实际 dev URL 与真实元素代入后执行):
    chromium-cli --session app <<'EOF'
    nav http://localhost:3000
    wait-for text=Dashboard
    screenshot
    console --errors
    EOF
    
  4. 取证:打开 chromium_cli/sessions/app/screenshots/screenshot.png,确认是真实页面而不是空白/错误页;检查 console --errors 输出。
  5. 停止lsof -ti:3000 -sTCP:LISTEN | xargs -r kill
  6. 沉淀:按 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 的匹配依据。
  7. 验证:开一个全新 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 skillelectron 示例server 示例skill 模板 一起阅读,可以看到 Claude Code 对"Agent 如何从干净机器把一个应用跑起来并证明它跑起来了"这一问题的完整答案:dev server + 端口轮询 + chromium-cli 管道脚本 + 截图取证 + 项目专属 skill 沉淀,六类项目形态各有对应配方,而本文覆盖的浏览器驱动形态是其中唯一"驱动工具开箱即用、只需写脚本"的一类。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391