架构

设计约束的浓缩摘要。要了解实现层面的细节,请查看仓库根目录的 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_snapshotcrystallize_specrecall / record_fact 记忆工具,以及 test_app prompt。它自身不启动任何 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 的全部原因。