0c437a96f1
- architecture.md: 完整架构设计(技术栈、API、CLI、生命周期、测试策略) - api.md: REST + WebSocket 接口文档 - cli.md: CLI 命令参考 - contributing.md: 开发规范与 JSON 转义要求 - quickstart.md: 快速开始指南 - llm-integration.md: 智能体集成指南 - examples.md: 8 个典型使用场景
13 KiB
13 KiB
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 页面管理
visionl page open <url> [--alias <name>]
visionl page list
visionl page info <id|alias>
visionl page kill <id|alias>
visionl page kill-all
5.2 页面操作
visionl click <id|alias> <selector>
visionl type <id|alias> <selector> <text>
visionl scroll <id|alias> [--down <px>] [--bottom]
visionl navigate <id|alias> <url>
visionl eval <id|alias> <js-code>
visionl wait <id|alias> [--selector <sel>] [--ms <n>]
5.3 内容获取
visionl screenshot <id|alias> [-o <file.png>]
visionl text <id|alias>
visionl html <id|alias>
5.4 Daemon 管理
visionl daemon start [--port <n>]
visionl daemon stop
visionl daemon status
5.5 REST 直通
visionl raw POST /pages '{"url":"https://example.com"}'
visionl raw GET /pages
visionl raw GET /pages/p_abc123/text
5.6 输出格式
默认输出 JSON(供 LLM 解析)。使用 --pretty 切换为人类可读格式。
// 成功
{ "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 包)
// 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<T = unknown> {
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
- 页面录制与回放