# VisionL 架构设计 > 面向 AI 智能体的开源可持久化浏览器 — 架构设计文档 ## 1. 概述 VisionL 是一款专为具备本地读写执行能力的 AI 智能体设计的开源浏览器。 LLM 通过 `VisionL-cli` 工具调用来操控网页。页面独立持久化——关闭 GUI 窗口不会杀死页面后台, 每个页面只能通过 CLI 参数或未来 GUI 的右键菜单按钮来关闭。 ### 核心原则 - **页面持久化**:页面在客户端断连后依然存活,只有显式 `kill` 才能终止 - **智能体优先**:CLI 子命令为 LLM 工具调用而设计,输出结构化 JSON - **本地优先**:Daemon 仅监听 localhost,不对外开放 - **GUI 就绪**:HTTP/WS API 同时服务于 CLI 和未来的 GUI,避免重复实现 --- ## 2. 技术栈 | 层级 | 选择 | 理由 | |------|------|------| | 浏览器引擎 | Playwright + Chromium | AI 浏览器自动化的事实标准 | | 语言 | TypeScript + Node.js | Playwright 原生语言,生态最优 | | 通信协议 | HTTP REST + WebSocket (localhost) | 职责分离,GUI 可复用同一套 API | | 包管理 | npm workspaces (monorepo) | 共享类型,单仓库管理 | | CLI 框架 | commander + chalk | 轻量、社区熟知 | | HTTP 服务 | 原生 `http` + `ws` 库 | 本地 daemon 无需重型框架 | ### JSON 转义处理 所有返回给智能体的 JSON 必须正确处理特殊字符转义。页面内容(文本、HTML)中可能包含 引号、反斜杠、控制字符、Unicode 代理对等,统一通过 `JSON.stringify` 序列化整个响应对象, 禁止手动拼接 JSON 字符串。 --- ## 3. 项目结构 ``` VisionL/ ├── docs/ │ ├── development/ │ │ ├── architecture.md # 本文档 │ │ ├── api.md # REST + WS 接口文档 │ │ ├── cli.md # CLI 命令参考 │ │ └── contributing.md # 开发规范 │ └── usage/ │ ├── quickstart.md # 快速开始 │ ├── llm-integration.md # 智能体集成指南 │ └── examples.md # 典型场景示例 ├── packages/ │ ├── core/ # 共享库:类型定义、HTTP 客户端、工具函数 │ │ ├── src/ │ │ │ ├── types/ │ │ │ │ ├── page.ts # Page 结构 │ │ │ │ ├── api.ts # 请求/响应类型 │ │ │ │ └── ws.ts # WebSocket 事件类型 │ │ │ ├── client.ts # Daemon HTTP + WS 客户端(CLI 和 GUI 共用) │ │ │ ├── escape.ts # JSON 安全转义 │ │ │ └── index.ts │ │ └── package.json │ ├── daemon/ # 后台进程:Playwright 管理 + HTTP/WS 服务 │ │ ├── src/ │ │ │ ├── server.ts # HTTP + WS 入口 │ │ │ ├── routes/ │ │ │ │ ├── pages.ts # 页面 CRUD │ │ │ │ ├── actions.ts # click/type/scroll/eval/navigate │ │ │ │ ├── content.ts # screenshot/text/html │ │ │ │ └── health.ts # 健康检查 │ │ │ ├── browser-manager.ts # Playwright 浏览器/页面生命周期 │ │ │ ├── page-registry.ts # 内存页面表(id → Playwright Page) │ │ │ ├── ws-relay.ts # WebSocket 事件广播 │ │ │ └── pidfile.ts # Daemon 进程管理 │ │ └── package.json │ └── cli/ # CLI 工具:用户 + LLM 交互入口 │ ├── src/ │ │ ├── index.ts # CLI 入口(commander) │ │ ├── commands/ │ │ │ ├── page.ts # visionl page open/close/list/info │ │ │ ├── action.ts # visionl click/type/scroll/eval/wait/navigate │ │ │ ├── view.ts # visionl screenshot/text/html │ │ │ └── daemon.ts # visionl daemon start/stop/status │ │ ├── format.ts # 输出格式化(JSON/pretty) │ │ └── auto-daemon.ts # 自动拉起 daemon 逻辑 │ └── package.json ├── package.json # monorepo root ├── tsconfig.json ├── .gitignore ├── README.md └── LICENSE ``` --- ## 4. Daemon 设计 ### 4.1 REST API | 方法 | 路径 | 说明 | 请求体 | 响应 | |------|------|------|--------|------| | POST | `/pages` | 打开新页面 | `{ url, alias? }` | `{ id, url, alias?, title, status }` | | GET | `/pages` | 列出所有页面 | — | `[{ id, url, alias?, title, status }]` | | GET | `/pages/:id` | 页面详情 | — | `{ id, url, alias?, title, status }` | | DELETE | `/pages/:id` | 杀死页面 | — | `{ ok: true }` | | POST | `/pages/:id/navigate` | 跳转 URL | `{ url }` | `{ url, title }` | | POST | `/pages/:id/click` | 点击元素 | `{ selector }` | `{ success }` | | POST | `/pages/:id/type` | 输入文本 | `{ selector, text }` | `{ success }` | | POST | `/pages/:id/scroll` | 滚动页面 | `{ deltaY?, toBottom? }` | `{ success }` | | POST | `/pages/:id/eval` | 执行 JS | `{ code }` | `{ result }` | | POST | `/pages/:id/wait` | 等待条件 | `{ selector?, timeout?, ms? }` | `{ success }` | | GET | `/pages/:id/screenshot` | 页面截图 | — | `{ base64, mime }` | | GET | `/pages/:id/text` | 页面纯文本 | — | `{ text }` | | GET | `/pages/:id/html` | 页面 HTML | — | `{ html }` | | GET | `/health` | 健康检查 | — | `{ status: "ok" }` | ### 4.2 WebSocket 事件 客户端通过 `ws://127.0.0.1:{port}/ws` 建立连接,daemon 主动推送: | 事件 | 数据 | 触发时机 | |------|------|---------| | `page:created` | `{ id, url, alias?, title }` | 新页面打开 | | `page:closed` | `{ id }` | 页面被杀死 | | `page:navigated` | `{ id, url, title }` | 页面 URL 变化 | | `page:crashed` | `{ id, error }` | 页面崩溃 | | `page:console` | `{ id, level, text }` | 控制台输出(调试用) | ### 4.3 页面生命周期 ``` visionl page open https://example.com --alias demo → POST /pages { url, alias: "demo" } → daemon 创建 Playwright Context + Page → 注册到 PageRegistry { id: "p_abc123", page, alias: "demo" } → 返回 { id: "p_abc123", url, alias: "demo", title, status: "active" } visionl page screenshot p_abc123 → GET /pages/p_abc123/screenshot → daemon 调用 page.screenshot(),返回 base64 visionl page kill p_abc123 → DELETE /pages/p_abc123 → daemon 调用 page.close()、context.close() → 从 PageRegistry 移除 ``` **规则:** - 页面在被显式 `kill` 之前永远存活,无超时、无闲置清理 - Daemon 重启不会恢复之前的页面(v1 不包含此功能) - `visionl daemon stop` 优雅退出 daemon,但不杀死页面(Playwright 浏览器进程独立管理) --- ## 5. CLI 设计 ### 5.1 页面管理 ```bash visionl page open [--alias ] visionl page list visionl page info visionl page kill visionl page kill-all ``` ### 5.2 页面操作 ```bash visionl click visionl type visionl scroll [--down ] [--bottom] visionl navigate visionl eval visionl wait [--selector ] [--ms ] ``` ### 5.3 内容获取 ```bash visionl screenshot [-o ] visionl text visionl html ``` ### 5.4 Daemon 管理 ```bash visionl daemon start [--port ] visionl daemon stop visionl daemon status ``` ### 5.5 REST 直通 ```bash visionl raw POST /pages '{"url":"https://example.com"}' visionl raw GET /pages visionl raw GET /pages/p_abc123/text ``` ### 5.6 输出格式 默认输出 JSON(供 LLM 解析)。使用 `--pretty` 切换为人类可读格式。 ```json // 成功 { "ok": true, "data": { ... } } // 失败 { "ok": false, "error": { "code": "PAGE_NOT_FOUND", "message": "页面 p_xyz 不存在" } } ``` **JSON 转义规则**:所有响应通过 `JSON.stringify` 序列化完整对象。页面内容字段(`text`、`html`) 作为不透明字符串处理,由 `JSON.stringify` 负责转义,不手动拼接。 ### 5.7 自动拉起 CLI 在任何页面/操作命令前执行 daemon 存活检测: ``` 1. 向配置端口(默认 9527)发送 GET /health 2. 有响应 → 继续执行 3. 超时/拒绝 → 检查 pidfile(~/.visionl/daemon.pid) - pid 存活且端口可达 → 更新端口,继续 - pid 已死或端口不可达 → 清理残留 pidfile,启动 daemon 4. 启动:child_process.spawn('visionl-daemon', ['--port', port]) 5. 轮询 GET /health(最多 3 秒,间隔 100ms) 6. 就绪 → 继续;超时 → 报错退出 ``` **状态文件:** - `~/.visionl/daemon.pid` — daemon 进程 PID - `~/.visionl/daemon.port` — daemon 监听端口 --- ## 6. 类型定义(core 包) ```typescript // packages/core/src/types/page.ts interface PageInfo { id: string; // 自动生成,如 "p_a1b2c3d4" url: string; alias?: string; // 用户指定,可选 title: string; status: 'active' | 'crashed'; } // packages/core/src/types/api.ts interface ApiResponse { ok: boolean; data?: T; error?: { code: string; message: string; }; } // 错误码 enum ErrorCode { PAGE_NOT_FOUND = 'PAGE_NOT_FOUND', DAEMON_UNREACHABLE = 'DAEMON_UNREACHABLE', INVALID_URL = 'INVALID_URL', ALIAS_EXISTS = 'ALIAS_EXISTS', TIMEOUT = 'TIMEOUT', INTERNAL = 'INTERNAL', } // packages/core/src/types/ws.ts type WsEvent = | { type: 'page:created'; data: PageInfo } | { type: 'page:closed'; data: { id: string } } | { type: 'page:navigated'; data: { id: string; url: string; title: string } } | { type: 'page:crashed'; data: { id: string; error: string } } | { type: 'page:console'; data: { id: string; level: string; text: string } }; ``` --- ## 7. 关键设计决策 ### v1 不包含会话恢复 页面在客户端断连后存活,但 daemon 重启后不会恢复。理由: - Playwright 浏览器进程状态复杂,难以序列化/恢复 - v1 聚焦 CLI 智能体工作流,daemon 在任务期间保持运行即可 - 会话恢复(跨 daemon 重启保存/恢复页面状态)可在 v2 添加 ### 无身份认证 Daemon 仅绑定 `127.0.0.1`,任何本地进程均可连接。这是刻意的设计:智能体与 daemon 运行在同一台机器。v2 可添加基于 token 的认证以支持多用户场景。 ### Page ID 与别名解析 - Page ID(`p_xxxxxxxx`)始终唯一,自动生成 - 别名由用户指定,必须唯一。重复使用已有别名会报错 - 命令同时接受两种形式:`visionl text myalias` 或 `visionl text p_abc123` - 解析顺序:先尝试别名匹配,再尝试 ID 匹配 ### 并发模型 - 每个 daemon 只有一个 Playwright `Browser` 实例 - 每次 `POST /pages` 创建一个新的 `BrowserContext`(隔离 cookie/存储)+ `Page` - v1 不设并发上限;由操作系统自然限制 - 未来:可配置最大页面数、排队、优先级 --- ## 8. 错误处理策略 | 场景 | Daemon 行为 | CLI 行为 | |------|------------|---------| | 页面不存在 | 404 + 错误码 | 退出码 1,输出 JSON 错误 | | Daemon 不可达 | — | 自动拉起或退出码 1 | | 无效 URL | 400 + 错误码 | 退出码 1,输出 JSON 错误 | | 选择器未找到 | 操作响应内 404 | 退出码 1,`{ ok: false, error: ... }` | | 页面崩溃 | `page:crashed` WS 事件,status → crashed | 后续操作返回 500 | | Daemon 端口被占用 | 启动失败,日志记录 | 退出码 1,建议 `--port` 覆盖 | --- ## 9. 测试策略 | 层级 | 工具 | 范围 | |------|------|------| | `core` 类型 | ts 类型检查 | 编译期正确性 | | `core/client.ts` | vitest + msw | HTTP 客户端行为、错误处理 | | `daemon/routes` | vitest + playwright-test | 路由逻辑 + 真实 Chromium | | `daemon/browser-manager` | vitest + playwright-test | 页面生命周期、崩溃恢复 | | `cli/commands` | vitest + mock 客户端 | 命令解析、输出格式化 | | `cli/auto-daemon` | vitest + fs mock | Pidfile 逻辑、spawn 行为 | | 集成测试 | vitest + 真实 daemon | CLI → daemon → Playwright 端到端 | | JSON 转义 | vitest | 验证特殊字符、Unicode、XSS 向量安全 | --- ## 10. 未来规划(v1 范围外) - `visionl-gui`:基于 Electron/Tauri 的桌面 GUI,用于查看和管理页面 - 会话持久化:跨 daemon 重启保存/恢复页面状态 - 多用户支持:基于 token 的认证 - 插件系统:自定义页面处理器 - 远程 daemon 支持(突破 localhost 限制) - 网络拦截和 Mock API - Cookie/存储管理 CLI - 页面录制与回放