AI 测试自动化平台:MCP 网关 + 浏览器 Worker 的本地三进程架构拆解
让 AI Agent 写 E2E 测试,最难的从来不是提示词,而是给它一只"手":浏览器控制、录制回放、页面快照、API 探测,背后都得有真实进程在干活。云端编排方案(SaaS job machine + 远程浏览器)延迟高、代码和页面数据还要过第三方;让 Agent 直接驱动本地 Playwright 又缺乏可控性和隔离。OpenEvident/vindicate 的解法是把运行时拆成三个本地进程:VS Code/Cursor 扩展负责 UI 与进程监管,runtime-mcp(MCP Server,端口 9223)是 Agent 唯一可见的工具面,runtime-worker(Fastify daemon,端口 9121)独占真实 Chromium。无云端组件、无 MongoDB、无远程编排,全部进程只监听 127.0.0.1。这套架构的核心矛盾只有一句话:如何让一台机器上的共享常驻进程,安全地为多个编辑器、多个项目的 Agent 并发服务。答案不是"每项目一个进程",而是"身份随请求走,不随进程走"。
三进程职责与拓扑
┌─ Agent / IDE ─────────────────────┐ ┌─ 本机 (127.0.0.1 only) ─────────────────┐
│ AI Agent (Cursor/Claude Code/…) │ │ Vindicate 扩展 ──spawn + supervise────┐ │
│ │ MCP tool calls (HTTP+SSE) │ │ │ 读/写 │ │
│ .cursor/mcp.json ────────────────┼──▶│ ~/.vindicate/worker.key (0600, 32B) │ │
└────────────────────────────────────┘ │ │ │ │
│ runtime-mcp :9223 POST /mcp │ │
│ │ HTTP + x-vindicate-internal-key│ │
│ runtime-worker :9121 (Fastify) │ │
│ │ Playwright CDP │ │
│ Chromium │ │
└───────────────────────────────────────┘ │
| 组件 | 职责 | 关键点 |
|---|---|---|
| Vindicate 扩展 | 引导 UI、仪表盘、进程监管 | 本身不做自动化;WorkerManager/McpManager 继承同一个 BaseProcessManager,由 RuntimeLifecycle 协调启停 |
| runtime-worker | 真实浏览器交互 | Fastify + SSE;会话状态机 active → paused → dead → expired → closed;录制产物落到 <project_root>/.vindicate/recordings/ |
| runtime-mcp | Agent 的工具面 | @modelcontextprotocol/sdk 的 StreamableHTTP 传输;进程无状态,磁盘文件是唯一事实来源(files-as-truth) |
扩展激活即无条件拉起 worker(有工作区再拉起 mcp),切换工作区只重启 mcp、不碰共享 worker——这是"单例"模型的直接后果:worker 和 mcp 是机器级单例,同一台机器上 Cursor 和 VS Code 各开两个项目,四个编辑器窗口共享同一个 9121 和同一个 9223 进程。
关键设计一:单例进程如何做到多项目隔离
正确性完全依赖"project_root 随请求/会话走"。扩展给每个项目写 MCP 配置时,把绝对路径烤进 URL:
{
"mcpServers": {
"Vindicate": {
"url": "http://127.0.0.1:9223/mcp?project_root=/Users/me/work/app-a",
"headers": { "x-vindicate-project-root": "/Users/me/work/app-a" }
}
}
}
runtime-mcp 为每个 MCP 客户端连接新建一个 McpServer + ProjectFs 实例,绑定该会话的 project_root,所有文件读写、codegen、测试执行都限定在这个目录。worker 侧同理:POST /sessions 创建会话时把 project_root 印在会话记录上,之后该会话的一切文件操作(/sessions/:id/files/*)都从会话记录读回路径,而不是读进程级配置。Agent 永远看不到、也设置不了 project_root——它不是工具参数,是扩展写配置时注入的连接级属性。一个 worker 进程同时服务多个项目的会话,靠的就是这条铁律。
关键设计二:内部密钥认证——防的不是人,是浏览器标签页
系统没有任何用户体系:无云账号、无登录。唯一认证机制是 32 字节随机值,一次性生成写入 ~/.vindicate/worker.key(0600 权限 + 原子创建,防止两个编辑器同时首启时竞态生成出两个 key),所有对 worker 的请求带 x-vindicate-internal-key 头。它只证明"请求来自本机自己的 Vindicate 安装",不携带任何用户身份。威胁模型很具体:本地 9121 端口是个能驱动浏览器的自动化 daemon,任意网页 JS 都能向 localhost 发请求——没有这个头,浏览器里开个恶意页面就能 CSRF 你的测试机。注意 MCP 的 /mcp 端点本身不 gate(Agent 要连),gate 的是 worker HTTP API 和 mcp→worker 这条链路。
关键设计三:用工作流图替代巨型 system prompt
vindicate_workflow 工具下发的是内容图而非一坨提示词:content/graphs/*.graph.json 定义节点和边,content/nodes/*.md 是各阶段的指导正文(YAML frontmatter 声明所属 graph、可用模式、要追加哪些 refs),content/refs/*.md 是共享参考资料(conformance contract、POM 约定、配方),多个节点复用同一份 ref,避免复制粘贴导致的事实漂移。主图是一条完整的测试自动化闭环:
understand → ground → design → generate → execute → audit
↑ ↑ ├→ heal → ground/generate
└────────┴────────┴→ escalate
入口按任务路由:write、fix、flaky、refactor、gaps、coverage、smoke 各自指向对应起始节点。ground 阶段还会做 api-ingest:抓真实端点形状,为后续 API 模式 codegen 提供契约。
关键设计四:codegen 直接写盘,两条轨道
vindicate_generate_code 不搞 schema 往返,直接把生成的代码写进项目。UI 轨道从 browser_read 拿到的定位器生成 page object / spec(validate、create、add_test_cases、register_page 四种模式);API 轨道从 ground 阶段的端点形状生成 resource client、builder、expected.json 和 auth fixture(validate_api、create_api、add_api_test_cases、register_client)。mcp 与 worker 之间的共享契约放在 monorepo 的 packages/protocol/,用 Zod schema 定义,两端编译期即对齐——这比"两边各写一份 TypeScript 接口"的常规做法省掉一整类"协议漂移" bug。
一次录制任务的完整数据流
把上面四个设计串起来看一次 record 任务:Agent 调 vindicate_start_recording(工具面在 mcp 进程)→ mcp 用内部密钥向 worker POST /sessions 建会话(状态 active,记录里印上 project_root)→ worker 拉起 Playwright + Chromium(CDP 连接)→ 录制产物经 /sessions/:id/files/recordings/xxx.jsonl 落盘到 <project_root>/.vindicate/recordings/ → Agent 调 vindicate_stop_recording 拿到产物路径 → 调 vindicate_generate_code 让 mcp 读取产物并生成 POM/spec 直接写回项目目录 → 调 vindicate_run_tests 让 worker 在项目里执行 npx playwright test。整个链路里 Agent 从未直接触碰文件系统 API,所有 IO 都收敛在 mcp 的工具定义里,而 mcp 自己又不持有任何状态——状态在 worker 的会话记录里,事实在磁盘上。会话记录同样持久化:worker 重启后,TTL 内的会话(默认 30 分钟)从磁盘恢复,Agent 可以继续 resume 而不是推倒重来;会话状态机里的 dead/expired/closed 三个终止态把"进程崩溃""超时"和"正常关闭"区分开,日志里一眼能看出是哪种收场。
什么时候这套架构是过度设计
单例 worker + 密钥文件这套东西的复杂度,是为"多编辑器、多项目、常驻浏览器"买单的。如果只是自己本地偶尔让 Claude 写两个测试,直接给 Agent 一个 npx playwright test 的工具就够了;如果团队要的是多人共享的测试执行环境,本地单例模型反而不合适——它刻意不做并发队列、不做远程访问,多人协作场景应该上 CI 或专门的 runner。vindicate 的取舍很清晰:它服务的是"个人开发机上,Agent 与浏览器之间最短的那条本地回路",一切设计(单例、密钥、空闲自关闭、files-as-truth)都围绕这个场景优化,不为别的场景预留抽象。照搬架构前先确认自己的场景和它一致。
踩坑记录与工程细节
- per-editor 随机密钥冲突:早期每个编辑器在各自的 secret storage 里生成独立 key,第二个编辑器打开时必然撞上"worker 已被不同 key 占用"。改成共享 key 文件后,多编辑器、多 profile、多应用都能 attach 到已在运行的 worker。升级后需彻底退出编辑器一次(Reload Window 不够)清掉旧进程。
- 进程静默死亡:最常见的线上崩溃原因是 unhandled rejection 直接带走进程。现在两个进程都装了
unhandledRejection/uncaughtExceptionhandler,日志后保活。注意系统没有自动重启——崩溃后靠扩展每 30s 的GET /health探活显示为 down,需要用户重载编辑器窗口触发 respawn。 - 双进程同时首启的竞态:两个编辑器同时 spawn 时,输家检测到自己 spawn 失败后重新 probe 并 attach 到赢家,而不是报错。
- 客户端兼容性坑:Antigravity 的 MCP 配置不能带
headers字段(google-antigravity/antigravity-cli#71,带了会让所有工具调用失败);Claude Desktop 因为没有 workspace 概念、不符合 per-sessionproject_root模型,直接从 onboarding 移除。 - 空闲自关闭:单例 worker 没人替它收尸——最后一个编辑器关闭后,连续 5 分钟(
VINDICATE_IDLE_SHUTDOWN_MS)无 health ping 且无活跃/暂停会话就自行退出。 - 资源治理:worker 采样 CPU/内存,压力大时节流或拒绝新会话;
browser_diagnose(视觉诊断)用VINDICATE_VISUAL_DIAGNOSIS环境变量做全局 kill switch。 - 多 root workspace:只取第一个文件夹,多个时打警告日志。
安全基线与端口协议
| 组件 | 端口 | 协议 | 认证 |
|---|---|---|---|
| runtime-worker | 9121 | HTTP + SSE (Fastify) | x-vindicate-internal-key 头(除 /health、/ready 外全部路由) |
| runtime-mcp | 9223 | MCP Streamable HTTP (POST /mcp) |
/mcp 不 gate;自身调 worker 时带内部 key |
所有端口只绑 127.0.0.1,整个系统不存在任何网络面。worker.key 就是这台机器的信任锚:谁拿到它谁就能驱动本地浏览器,所以权限 0600、原子创建、不入库。
总结与进阶方向
vindicate 给"AI 写 E2E 测试"提供了一个可复用的架构模板:扩展做进程生命周期、MCP 做工具面、Worker 做浏览器执行,三者用固定端口 + 密钥文件解耦;多项目隔离靠请求级身份而非进程级隔离。对自建类似工具的人,三个可直接抄的点:① 工作流指导用 graph/nodes/refs 三层结构,别堆 system prompt;② 共享契约用 Zod schema 放独立 package,两端同源;③ 本地 daemon 必须做 CSRF 防护,哪怕只是给 localhost 端口加个共享密钥头。
进阶方向:Agent skill 标准化(.agents/skills/ 共享路径,Cursor/Copilot/Antigravity 三端复用一份)、把录制回放升级为"录一次、断言自动生成"、以及把单机 worker 抽象成可插拔远程执行器——但按这个项目的取舍,后者大概率会被按需才加,本地优先是它的立身之本。
更多推荐

所有评论(0)