架构
设计约束的浓缩摘要。要了解实现层面的细节,请查看仓库根目录的 CLAUDE.md。
流程
Hover 是 MCP-first 的。授权引擎是一个 MCP server,你把它加到自己的编码 agent 上;agent 提供智能,Hover 则在输出端保证还原度。一次 test_app 运行的完整路径:
your coding agent (Claude Code / Cursor / …)
↕ MCP (stdio)
@hover-dev/mcp ── grounded actuation tools + crystallize_spec
│ (a thin frontend over the engine)
@hover-dev/core ── the engine: launch/connect debug Chrome, grounded
│ locate + actuate, deterministic crystallize, .hover/ memory
↕ CDP
isolated debug Chrome
↕ DOM
your dev server / app
│
└─ crystallize_spec ──▶ __vibe_tests__/<flow>.spec.ts (plain Playwright, no AI)
各角色职责划分清晰:
@hover-dev/mcp—— 轻量的 MCP 前端。注册 grounded 工具(click_control/fill_control/ …)、browser_navigate/browser_snapshot、crystallize_spec、recall/record_fact记忆工具,以及test_appprompt。它自身不启动任何 agent。@hover-dev/core—— 引擎。负责 Playwright CDP 预检、通过 CDP 进行 grounded 定位与操作、确定性的writeSpec结晶器,以及.hover/记忆加载器。- VS Code 扩展 —— 一个不含引擎的审阅座舱:一张业务地图(Business Map)图 + 一个仪表盘(Dashboard)。它只做审阅,不驱动任何 agent。
边界约束
这些约束是承重的 —— 其中好几条并不显而易见:
- agent 从不启动自己的 Chromium。 引擎通过
connectOverCDP连接到 CDP 端口上的 debug Chrome,并选取 URL 与目标 origin 匹配的现有 context / page。 - 引擎被允许启动一个特定的 Chrome: 位于
<tmpdir>/hover-chrome下、按需启动的隔离 debug Chrome。它不是用户的主 Chrome 配置文件。 - grounded actuation 是还原度机制。 agent 通过
click_control/fill_control/select_control/check_control操作,这些工具接收一个从快照中读取的 grounded target(role+name → testId → text),并通过 CDP 用page.getByRole(...)执行它。驱动这次操作的 selector 就是被结晶下来的那一个 —— 因此 record == replay(录制即回放)。 - 结晶是确定性的。
crystallize_spec在生成代码时不使用任何 LLM,把录制下来的 grounded 步骤翻译成 Playwright。agent 从不手写 spec。 - 没有上传通道。 Hover 不打包任何模型、任何密钥(BYO-CLI)。你的 agent 与它自己的服务商通信;引擎没有 LLM SDK 代码,也没有遥测。
- 生成的 Playwright 代码优先使用
page.getByRole/getByText,而非 CSS / XPath selector。 - Cookies / localStorage 从不经过引擎;认证状态留在浏览器内部,由 Playwright 在进程内处理。
为什么用隔离的 debug Chrome,而不是用户的日常浏览器?
Hover 刻意不接入用户的主 Chrome 配置文件。那样做需要用 --remote-debugging-port 重启日常浏览器,并会把每一个标签页、cookie、扩展都暴露给 agent 的任意操作。这个取舍是诚实的:你需要在 debug Chrome 里登录一次应用,但 <tmpdir>/hover-chrome 的配置文件目录会跨多次运行持久保留。
为什么用 grounded 工具,而不是自由形式的"点击那个按钮"?
一段松散的元素描述无法往返(round-trip)成一个可回放的 selector —— 它会结晶成一个臆造的 getByText,下次可能就匹配不上了。grounded 工具接收的是直接从快照读出的 target,因此保存下来的 selector 正是实际驱动了这次运行的那一个。这正是 MCP 暴露自己的 actuation 工具、而不是把一个通用浏览器 MCP 交给 agent 的全部原因。