Files
VisionL/docs/development/architecture.md
T
AskaEth 0c437a96f1 docs: 项目架构设计文档(中文)及使用指南
- architecture.md: 完整架构设计(技术栈、API、CLI、生命周期、测试策略)
- api.md: REST + WebSocket 接口文档
- cli.md: CLI 命令参考
- contributing.md: 开发规范与 JSON 转义要求
- quickstart.md: 快速开始指南
- llm-integration.md: 智能体集成指南
- examples.md: 8 个典型使用场景
2026-08-12 20:14:54 +08:00

349 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <url> [--alias <name>]
visionl page list
visionl page info <id|alias>
visionl page kill <id|alias>
visionl page kill-all
```
### 5.2 页面操作
```bash
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 内容获取
```bash
visionl screenshot <id|alias> [-o <file.png>]
visionl text <id|alias>
visionl html <id|alias>
```
### 5.4 Daemon 管理
```bash
visionl daemon start [--port <n>]
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<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
- 页面录制与回放