docs: 项目架构设计文档(中文)及使用指南
- architecture.md: 完整架构设计(技术栈、API、CLI、生命周期、测试策略) - api.md: REST + WebSocket 接口文档 - cli.md: CLI 命令参考 - contributing.md: 开发规范与 JSON 转义要求 - quickstart.md: 快速开始指南 - llm-integration.md: 智能体集成指南 - examples.md: 8 个典型使用场景
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
# VisionL REST API 接口文档
|
||||
|
||||
> 适用版本:v1
|
||||
|
||||
## 通用约定
|
||||
|
||||
### Base URL
|
||||
|
||||
```
|
||||
http://127.0.0.1:9527
|
||||
```
|
||||
|
||||
端口可通过启动参数 `--port` 修改。
|
||||
|
||||
### 响应格式
|
||||
|
||||
所有响应统一为 JSON:
|
||||
|
||||
```json
|
||||
// 成功
|
||||
{
|
||||
"ok": true,
|
||||
"data": { ... }
|
||||
}
|
||||
|
||||
// 失败
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "PAGE_NOT_FOUND",
|
||||
"message": "页面 p_xyz 不存在"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误码
|
||||
|
||||
| 错误码 | HTTP 状态码 | 说明 |
|
||||
|--------|-----------|------|
|
||||
| `PAGE_NOT_FOUND` | 404 | 指定页面不存在 |
|
||||
| `DAEMON_UNREACHABLE` | 502 | daemon 不可达(CLI 端产生) |
|
||||
| `INVALID_URL` | 400 | URL 格式不合法 |
|
||||
| `ALIAS_EXISTS` | 409 | 别名已被使用 |
|
||||
| `TIMEOUT` | 408 | 操作超时 |
|
||||
| `INTERNAL` | 500 | 内部错误 |
|
||||
|
||||
### JSON 转义
|
||||
|
||||
所有响应通过 `JSON.stringify` 序列化。页面内容(如 `text`、`html` 字段)中的
|
||||
引号、反斜杠、控制字符均被正确转义,外层 JSON 始终合法。
|
||||
|
||||
---
|
||||
|
||||
## 页面管理
|
||||
|
||||
### 打开页面
|
||||
|
||||
```
|
||||
POST /pages
|
||||
```
|
||||
|
||||
**请求体:**
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"alias": "demo" // 可选
|
||||
}
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"id": "p_a1b2c3d4",
|
||||
"url": "https://example.com",
|
||||
"alias": "demo",
|
||||
"title": "Example Domain",
|
||||
"status": "active"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 列出所有页面
|
||||
|
||||
```
|
||||
GET /pages
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "p_a1b2c3d4",
|
||||
"url": "https://example.com",
|
||||
"alias": "demo",
|
||||
"title": "Example Domain",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 页面详情
|
||||
|
||||
```
|
||||
GET /pages/:id
|
||||
```
|
||||
|
||||
**响应:** 同上 `data` 中的单个对象。
|
||||
|
||||
### 杀死页面
|
||||
|
||||
```
|
||||
DELETE /pages/:id
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{ "ok": true, "data": null }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 页面操作
|
||||
|
||||
### 跳转
|
||||
|
||||
```
|
||||
POST /pages/:id/navigate
|
||||
```
|
||||
|
||||
**请求体:**
|
||||
|
||||
```json
|
||||
{ "url": "https://new-url.com" }
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"url": "https://new-url.com",
|
||||
"title": "New Page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 点击元素
|
||||
|
||||
```
|
||||
POST /pages/:id/click
|
||||
```
|
||||
|
||||
**请求体:**
|
||||
|
||||
```json
|
||||
{ "selector": "#login-button" }
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{ "ok": true, "data": { "success": true } }
|
||||
```
|
||||
|
||||
### 输入文本
|
||||
|
||||
```
|
||||
POST /pages/:id/type
|
||||
```
|
||||
|
||||
**请求体:**
|
||||
|
||||
```json
|
||||
{
|
||||
"selector": "#username",
|
||||
"text": "hello world"
|
||||
}
|
||||
```
|
||||
|
||||
**响应:** 同 click。
|
||||
|
||||
### 滚动
|
||||
|
||||
```
|
||||
POST /pages/:id/scroll
|
||||
```
|
||||
|
||||
**请求体(至少提供一个):**
|
||||
|
||||
```json
|
||||
{
|
||||
"deltaY": 500,
|
||||
"toBottom": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应:** 同 click。
|
||||
|
||||
### 执行 JavaScript
|
||||
|
||||
```
|
||||
POST /pages/:id/eval
|
||||
```
|
||||
|
||||
**请求体:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "document.title"
|
||||
}
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"result": "Example Domain"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:`result` 类型取决于 JS 代码返回值,可以是 string、number、boolean、object 或 null。
|
||||
|
||||
### 等待条件
|
||||
|
||||
```
|
||||
POST /pages/:id/wait
|
||||
```
|
||||
|
||||
**请求体(至少提供一个):**
|
||||
|
||||
```json
|
||||
{
|
||||
"selector": ".loaded",
|
||||
"ms": 2000
|
||||
}
|
||||
```
|
||||
|
||||
**响应:** 同 click。
|
||||
|
||||
---
|
||||
|
||||
## 内容获取
|
||||
|
||||
### 截图
|
||||
|
||||
```
|
||||
GET /pages/:id/screenshot
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"base64": "iVBORw0KGgoAAAANS...",
|
||||
"mime": "image/png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 纯文本
|
||||
|
||||
```
|
||||
GET /pages/:id/text
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"text": "页面正文内容..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HTML
|
||||
|
||||
```
|
||||
GET /pages/:id/html
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"html": "<!DOCTYPE html>..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Daemon 管理
|
||||
|
||||
### 健康检查
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSocket 接口
|
||||
|
||||
### 连接
|
||||
|
||||
```
|
||||
ws://127.0.0.1:9527/ws
|
||||
```
|
||||
|
||||
### 事件类型
|
||||
|
||||
所有事件 JSON 格式:`{ "type": "...", "data": { ... } }`
|
||||
|
||||
| 事件 | 方向 | 说明 |
|
||||
|------|------|------|
|
||||
| `page:created` | daemon → 客户端 | 新页面打开 |
|
||||
| `page:closed` | daemon → 客户端 | 页面被杀死 |
|
||||
| `page:navigated` | daemon → 客户端 | 页面 URL 发生变化 |
|
||||
| `page:crashed` | daemon → 客户端 | Playwright 页面崩溃 |
|
||||
| `page:console` | daemon → 客户端 | 页面控制台输出(调试) |
|
||||
@@ -0,0 +1,348 @@
|
||||
# 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
|
||||
- 页面录制与回放
|
||||
@@ -0,0 +1,286 @@
|
||||
# VisionL CLI 命令参考
|
||||
|
||||
> 适用版本:v1
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 全局安装(需要先构建)
|
||||
npm install -g ./packages/cli
|
||||
|
||||
# 或开发模式
|
||||
cd packages/cli && npm link
|
||||
```
|
||||
|
||||
安装后可使用 `visionl` 命令。
|
||||
|
||||
## 全局选项
|
||||
|
||||
| 选项 | 说明 |
|
||||
|------|------|
|
||||
| `--pretty` | 人类可读格式输出(默认 JSON) |
|
||||
| `--port <n>` | daemon 端口(默认 9527) |
|
||||
| `--help` | 查看帮助 |
|
||||
|
||||
---
|
||||
|
||||
## 页面管理
|
||||
|
||||
### `visionl page open`
|
||||
|
||||
打开新页面。
|
||||
|
||||
```bash
|
||||
visionl page open <url> [--alias <name>]
|
||||
```
|
||||
|
||||
**示例:**
|
||||
|
||||
```bash
|
||||
visionl page open https://www.baidu.com --alias baidu
|
||||
# {"ok":true,"data":{"id":"p_a1b2c3d4","url":"https://www.baidu.com","alias":"baidu","title":"百度一下,你就知道","status":"active"}}
|
||||
```
|
||||
|
||||
### `visionl page list`
|
||||
|
||||
列出所有已打开页面。
|
||||
|
||||
```bash
|
||||
visionl page list
|
||||
# {"ok":true,"data":[{"id":"p_a1b2c3d4","url":"...","alias":"baidu",...}]}
|
||||
```
|
||||
|
||||
### `visionl page info`
|
||||
|
||||
查看页面详情。
|
||||
|
||||
```bash
|
||||
visionl page info <id|alias>
|
||||
visionl page info baidu
|
||||
```
|
||||
|
||||
### `visionl page kill`
|
||||
|
||||
关闭指定页面。
|
||||
|
||||
```bash
|
||||
visionl page kill <id|alias>
|
||||
```
|
||||
|
||||
### `visionl page kill-all`
|
||||
|
||||
关闭所有页面。
|
||||
|
||||
```bash
|
||||
visionl page kill-all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 页面操作
|
||||
|
||||
### `visionl click`
|
||||
|
||||
点击指定元素。
|
||||
|
||||
```bash
|
||||
visionl click <id|alias> <selector>
|
||||
|
||||
# 示例
|
||||
visionl click baidu "#su"
|
||||
```
|
||||
|
||||
### `visionl type`
|
||||
|
||||
在输入框中输入文本。
|
||||
|
||||
```bash
|
||||
visionl type <id|alias> <selector> <text>
|
||||
|
||||
# 示例
|
||||
visionl type baidu "#kw" "VisionL浏览器"
|
||||
```
|
||||
|
||||
### `visionl scroll`
|
||||
|
||||
滚动页面。
|
||||
|
||||
```bash
|
||||
visionl scroll <id|alias> [--down <px>] [--bottom]
|
||||
|
||||
# 示例
|
||||
visionl scroll baidu --down 300
|
||||
visionl scroll baidu --bottom
|
||||
```
|
||||
|
||||
### `visionl navigate`
|
||||
|
||||
页面跳转。
|
||||
|
||||
```bash
|
||||
visionl navigate <id|alias> <url>
|
||||
```
|
||||
|
||||
### `visionl eval`
|
||||
|
||||
在页面中执行 JavaScript。
|
||||
|
||||
```bash
|
||||
visionl eval <id|alias> <js-code>
|
||||
|
||||
# 示例
|
||||
visionl eval baidu "document.title"
|
||||
# {"ok":true,"data":{"result":"百度一下,你就知道"}}
|
||||
```
|
||||
|
||||
### `visionl wait`
|
||||
|
||||
等待条件满足。
|
||||
|
||||
```bash
|
||||
visionl wait <id|alias> [--selector <sel>] [--ms <n>]
|
||||
|
||||
# 等待选择器出现
|
||||
visionl wait baidu --selector "#content"
|
||||
# 等待 2 秒
|
||||
visionl wait baidu --ms 2000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 内容获取
|
||||
|
||||
### `visionl screenshot`
|
||||
|
||||
获取页面截图。
|
||||
|
||||
```bash
|
||||
visionl screenshot <id|alias> [-o <file.png>]
|
||||
|
||||
# 输出 base64(默认)
|
||||
visionl screenshot baidu
|
||||
|
||||
# 写入文件
|
||||
visionl screenshot baidu -o screenshot.png
|
||||
```
|
||||
|
||||
### `visionl text`
|
||||
|
||||
获取页面纯文本。
|
||||
|
||||
```bash
|
||||
visionl text <id|alias>
|
||||
# {"ok":true,"data":{"text":"百度一下,你就知道\n..."}}
|
||||
```
|
||||
|
||||
### `visionl html`
|
||||
|
||||
获取页面 HTML 源码。
|
||||
|
||||
```bash
|
||||
visionl html <id|alias>
|
||||
# {"ok":true,"data":{"html":"<!DOCTYPE html>..."}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Daemon 管理
|
||||
|
||||
### `visionl daemon start`
|
||||
|
||||
手动启动 daemon。
|
||||
|
||||
```bash
|
||||
visionl daemon start [--port <n>]
|
||||
|
||||
# 默认端口 9527
|
||||
visionl daemon start
|
||||
# 指定端口
|
||||
visionl daemon start --port 8080
|
||||
```
|
||||
|
||||
### `visionl daemon stop`
|
||||
|
||||
优雅关闭 daemon(不杀死已打开的页面)。
|
||||
|
||||
```bash
|
||||
visionl daemon stop
|
||||
```
|
||||
|
||||
### `visionl daemon status`
|
||||
|
||||
查看 daemon 运行状态。
|
||||
|
||||
```bash
|
||||
visionl daemon status
|
||||
# {"ok":true,"data":{"running":true,"pid":12345,"port":9527}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## REST 直通
|
||||
|
||||
直接向 daemon 发送原始 HTTP 请求。
|
||||
|
||||
```bash
|
||||
visionl raw <METHOD> <path> [body]
|
||||
|
||||
# 示例
|
||||
visionl raw POST /pages '{"url":"https://example.com"}'
|
||||
visionl raw GET /pages
|
||||
visionl raw GET /pages/p_abc123/text
|
||||
visionl raw DELETE /pages/p_abc123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自动拉起
|
||||
|
||||
CLI 在执行页面命令前会自动检测 daemon 是否存活。如果 daemon 未运行,会自动启动。
|
||||
|
||||
自动拉起流程:
|
||||
|
||||
1. 向默认端口(9527)发送 `GET /health`
|
||||
2. 无响应时检查 `~/.visionl/daemon.pid` 中的进程是否存活
|
||||
3. 都不行则 `spawn` 启动 daemon
|
||||
4. 轮询健康检查,最多等待 3 秒
|
||||
5. 超时则报错退出
|
||||
|
||||
**状态文件:**
|
||||
|
||||
- `~/.visionl/daemon.pid` — daemon PID
|
||||
- `~/.visionl/daemon.port` — daemon 端口
|
||||
|
||||
---
|
||||
|
||||
## 输出格式
|
||||
|
||||
### JSON(默认)
|
||||
|
||||
每行一个 JSON 对象,LLM 直接解析:
|
||||
|
||||
```json
|
||||
{"ok":true,"data":{"id":"p_a1b2c3d4","url":"https://example.com","title":"Example"}}
|
||||
```
|
||||
|
||||
### 人类可读(--pretty)
|
||||
|
||||
```bash
|
||||
visionl page open https://example.com --pretty
|
||||
# ✓ 页面已打开
|
||||
# ID: p_a1b2c3d4
|
||||
# URL: https://example.com
|
||||
# 标题: Example Domain
|
||||
# 状态: active
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 退出码
|
||||
|
||||
| 退出码 | 含义 |
|
||||
|--------|------|
|
||||
| 0 | 成功 |
|
||||
| 1 | 一般错误(页面不存在、选择器未找到等) |
|
||||
| 2 | daemon 不可达 |
|
||||
| 3 | 无效参数 |
|
||||
@@ -0,0 +1,136 @@
|
||||
# VisionL 开发规范
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Node.js >= 18
|
||||
- npm >= 9
|
||||
- Git
|
||||
|
||||
## 克隆与安装
|
||||
|
||||
```bash
|
||||
git clone ssh://git@git.yeij.top:2222/AskaEth/VisionL.git
|
||||
cd VisionL
|
||||
git checkout dev
|
||||
|
||||
# 安装所有 workspace 依赖
|
||||
npm install
|
||||
|
||||
# 安装 Playwright 浏览器(Chromium)
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
Monorepo 使用 npm workspaces 管理:
|
||||
|
||||
```
|
||||
packages/
|
||||
├── core/ # 共享类型、HTTP 客户端、工具函数
|
||||
├── daemon/ # 后台守护进程
|
||||
└── cli/ # 命令行工具
|
||||
```
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
# 类型检查(全仓)
|
||||
npm run typecheck
|
||||
|
||||
# 运行所有测试
|
||||
npm test
|
||||
|
||||
# 运行单个包的测试
|
||||
npm test --workspace=packages/core
|
||||
npm test --workspace=packages/daemon
|
||||
npm test --workspace=packages/cli
|
||||
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 本地开发(cli 链接到全局)
|
||||
cd packages/cli && npm link
|
||||
# 然后可以在任何目录使用 visionl 命令
|
||||
```
|
||||
|
||||
## 代码规范
|
||||
|
||||
### 命名
|
||||
|
||||
- 文件名:kebab-case(`browser-manager.ts`)
|
||||
- 变量/函数:camelCase(`pageRegistry`、`getPageInfo`)
|
||||
- 类型/接口:PascalCase(`PageInfo`、`ApiResponse`)
|
||||
- 常量:UPPER_SNAKE_CASE(`DEFAULT_PORT`)
|
||||
|
||||
### TypeScript
|
||||
|
||||
- 严格模式 (`strict: true`)
|
||||
- 禁止 `any`(必须显式类型)
|
||||
- 导出类型使用 `interface` 而非 `type`(可扩展性更好)
|
||||
|
||||
### 注释
|
||||
|
||||
- 文件头简述文件职责(中英双语一行)
|
||||
- 公共 API 必须有 JSDoc
|
||||
- 不写废话注释(不解释显而易见的代码)
|
||||
|
||||
### 日志
|
||||
|
||||
Daemon 和 CLI 使用统一的日志层级:
|
||||
|
||||
- `DEBUG`:调试信息(带上下文)
|
||||
- `INFO`:关键状态变更(页面打开/关闭/崩溃)
|
||||
- `WARN`:可恢复的异常
|
||||
- `ERROR`:需要关注的错误
|
||||
|
||||
生产环境默认 `INFO` 级别,开发模式可设为 `DEBUG`。
|
||||
|
||||
## 测试规范
|
||||
|
||||
- 使用 `vitest` 作为测试框架
|
||||
- 测试文件放在 `src/__tests__/` 目录下
|
||||
- 文件命名:`*.test.ts`
|
||||
- 每个 PR 必须包含相关测试
|
||||
- 集成测试放在 `packages/daemon/src/__tests__/integration/`
|
||||
|
||||
## Git 工作流
|
||||
|
||||
- `main`:稳定发布分支
|
||||
- `dev`:开发分支(日常开发在此)
|
||||
- 功能分支:`feature/xxx`
|
||||
- 修复分支:`fix/xxx`
|
||||
|
||||
### Commit 规范
|
||||
|
||||
遵循 Conventional Commits:
|
||||
|
||||
```
|
||||
feat: 添加页面截图功能
|
||||
fix: 修复 daemon 端口冲突时的错误提示
|
||||
docs: 更新 API 文档
|
||||
test: 添加 CLI 自动拉起单元测试
|
||||
refactor: 重构 page-registry 为 Map 实现
|
||||
```
|
||||
|
||||
## 发布流程
|
||||
|
||||
1. 功能开发在 `dev` 分支
|
||||
2. 测试全过后合并到 `main`
|
||||
3. 打 tag:`v1.0.0`
|
||||
4. 发布到 npm
|
||||
|
||||
---
|
||||
|
||||
## JSON 转义规范
|
||||
|
||||
所有返回给外部的 JSON 必须通过 `JSON.stringify` 序列化,禁止以下写法:
|
||||
|
||||
```typescript
|
||||
// ❌ 禁止
|
||||
`{"ok":true,"text":"${pageText}"}`
|
||||
|
||||
// ✅ 正确
|
||||
JSON.stringify({ ok: true, text: pageText })
|
||||
```
|
||||
|
||||
页面内容(文本、HTML)中可能包含任意字符,手动拼接必然导致 JSON 非法。
|
||||
Reference in New Issue
Block a user