From 0c437a96f16c7198c7feb82484d6d0c7886ef35b Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 12 Aug 2026 20:14:54 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=A1=B9=E7=9B=AE=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3=EF=BC=88=E4=B8=AD=E6=96=87?= =?UTF-8?q?=EF=BC=89=E5=8F=8A=E4=BD=BF=E7=94=A8=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - architecture.md: 完整架构设计(技术栈、API、CLI、生命周期、测试策略) - api.md: REST + WebSocket 接口文档 - cli.md: CLI 命令参考 - contributing.md: 开发规范与 JSON 转义要求 - quickstart.md: 快速开始指南 - llm-integration.md: 智能体集成指南 - examples.md: 8 个典型使用场景 --- .gitignore | 31 +++ README.md | 32 +++ docs/development/api.md | 345 ++++++++++++++++++++++++++++++ docs/development/architecture.md | 348 +++++++++++++++++++++++++++++++ docs/development/cli.md | 286 +++++++++++++++++++++++++ docs/development/contributing.md | 136 ++++++++++++ docs/usage/examples.md | 143 +++++++++++++ docs/usage/llm-integration.md | 138 ++++++++++++ docs/usage/quickstart.md | 89 ++++++++ 9 files changed, 1548 insertions(+) create mode 100644 .gitignore create mode 100644 docs/development/api.md create mode 100644 docs/development/architecture.md create mode 100644 docs/development/cli.md create mode 100644 docs/development/contributing.md create mode 100644 docs/usage/examples.md create mode 100644 docs/usage/llm-integration.md create mode 100644 docs/usage/quickstart.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e44b6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +*.tsbuildinfo + +# Runtime state +.visionl/ + +# Environment +.env +.env.local + +# Logs +*.log +npm-debug.log* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Test artifacts +coverage/ +test-results/ diff --git a/README.md b/README.md index e38c2ed..cea0d81 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,34 @@ # VisionL +面向 AI 智能体的开源可持久化浏览器。 + +LLM 通过 `VisionL-cli` 工具调用操控网页——打开页面、点击、输入、截图、提取内容。 +页面独立持久化,关闭任何窗口都不会杀死后台页面。 + +## 特性 + +- **智能体优先**:CLI 子命令专为 LLM 工具调用设计,输出结构化 JSON +- **页面持久化**:页面存活不受客户端断连影响,只有显式 `kill` 才终止 +- **全功能自动化**:点击、输入、滚动、截图、JS 执行、网络拦截 +- **多页面并行**:同时管理多个页面,ID + 别名双轨引用 +- **自动拉起**:CLI 自动检测并启动后台 daemon + +## 文档 + +| 文档 | 说明 | +|------|------| +| [快速开始](docs/usage/quickstart.md) | 安装和基本使用 | +| [LLM 集成](docs/usage/llm-integration.md) | 在智能体中集成 VisionL | +| [使用示例](docs/usage/examples.md) | 典型场景 | +| [架构设计](docs/development/architecture.md) | 整体架构 | +| [API 文档](docs/development/api.md) | REST + WebSocket 接口 | +| [CLI 命令](docs/development/cli.md) | 命令参考 | +| [开发规范](docs/development/contributing.md) | 贡献指南 | + +## 技术栈 + +TypeScript + Node.js + Playwright + Chromium + +## License + +待定 diff --git a/docs/development/api.md b/docs/development/api.md new file mode 100644 index 0000000..89ed377 --- /dev/null +++ b/docs/development/api.md @@ -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": "..." + } +} +``` + +--- + +## 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 → 客户端 | 页面控制台输出(调试) | diff --git a/docs/development/architecture.md b/docs/development/architecture.md new file mode 100644 index 0000000..6f292af --- /dev/null +++ b/docs/development/architecture.md @@ -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 [--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 +- 页面录制与回放 diff --git a/docs/development/cli.md b/docs/development/cli.md new file mode 100644 index 0000000..fe1e27c --- /dev/null +++ b/docs/development/cli.md @@ -0,0 +1,286 @@ +# VisionL CLI 命令参考 + +> 适用版本:v1 + +## 安装 + +```bash +# 全局安装(需要先构建) +npm install -g ./packages/cli + +# 或开发模式 +cd packages/cli && npm link +``` + +安装后可使用 `visionl` 命令。 + +## 全局选项 + +| 选项 | 说明 | +|------|------| +| `--pretty` | 人类可读格式输出(默认 JSON) | +| `--port ` | daemon 端口(默认 9527) | +| `--help` | 查看帮助 | + +--- + +## 页面管理 + +### `visionl page open` + +打开新页面。 + +```bash +visionl page open [--alias ] +``` + +**示例:** + +```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 +visionl page info baidu +``` + +### `visionl page kill` + +关闭指定页面。 + +```bash +visionl page kill +``` + +### `visionl page kill-all` + +关闭所有页面。 + +```bash +visionl page kill-all +``` + +--- + +## 页面操作 + +### `visionl click` + +点击指定元素。 + +```bash +visionl click + +# 示例 +visionl click baidu "#su" +``` + +### `visionl type` + +在输入框中输入文本。 + +```bash +visionl type + +# 示例 +visionl type baidu "#kw" "VisionL浏览器" +``` + +### `visionl scroll` + +滚动页面。 + +```bash +visionl scroll [--down ] [--bottom] + +# 示例 +visionl scroll baidu --down 300 +visionl scroll baidu --bottom +``` + +### `visionl navigate` + +页面跳转。 + +```bash +visionl navigate +``` + +### `visionl eval` + +在页面中执行 JavaScript。 + +```bash +visionl eval + +# 示例 +visionl eval baidu "document.title" +# {"ok":true,"data":{"result":"百度一下,你就知道"}} +``` + +### `visionl wait` + +等待条件满足。 + +```bash +visionl wait [--selector ] [--ms ] + +# 等待选择器出现 +visionl wait baidu --selector "#content" +# 等待 2 秒 +visionl wait baidu --ms 2000 +``` + +--- + +## 内容获取 + +### `visionl screenshot` + +获取页面截图。 + +```bash +visionl screenshot [-o ] + +# 输出 base64(默认) +visionl screenshot baidu + +# 写入文件 +visionl screenshot baidu -o screenshot.png +``` + +### `visionl text` + +获取页面纯文本。 + +```bash +visionl text +# {"ok":true,"data":{"text":"百度一下,你就知道\n..."}} +``` + +### `visionl html` + +获取页面 HTML 源码。 + +```bash +visionl html +# {"ok":true,"data":{"html":"..."}} +``` + +--- + +## Daemon 管理 + +### `visionl daemon start` + +手动启动 daemon。 + +```bash +visionl daemon start [--port ] + +# 默认端口 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 [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 | 无效参数 | diff --git a/docs/development/contributing.md b/docs/development/contributing.md new file mode 100644 index 0000000..8041c8e --- /dev/null +++ b/docs/development/contributing.md @@ -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 非法。 diff --git a/docs/usage/examples.md b/docs/usage/examples.md new file mode 100644 index 0000000..db82367 --- /dev/null +++ b/docs/usage/examples.md @@ -0,0 +1,143 @@ +# VisionL 使用示例 + +> 面向智能体和人工用户的典型场景。 + +## 场景一:搜索引擎查询 + +```bash +# 1. 打开百度 +visionl page open https://www.baidu.com --alias search +# → {"ok":true,"data":{"id":"p_1234","alias":"search",...}} + +# 2. 输入搜索词 +visionl type search "#kw" "VisionL 浏览器" + +# 3. 点击搜索 +visionl click search "#su" + +# 4. 等待结果加载 +visionl wait search --selector "#content_left" + +# 5. 获取页面文本 +visionl text search +# → {"ok":true,"data":{"text":"搜索结果..."}} + +# 6. 用完关闭 +visionl page kill search +``` + +## 场景二:多页面信息收集 + +```bash +# 同时打开多个信息源 +visionl page open https://news.ycombinator.com --alias hn +visionl page open https://www.reddit.com/r/programming --alias reddit +visionl page open https://github.com/trending --alias gh + +# 分别提取内容 +visionl text hn +visionl text reddit +visionl text gh + +# 用完批量关闭 +visionl page kill-all +``` + +## 场景三:表单填写 + +```bash +# 1. 打开登录页 +visionl page open https://example.com/login --alias login + +# 2. 填写表单 +visionl type login "#email" "user@example.com" +visionl type login "#password" "s3cret" + +# 3. 提交 +visionl click login "button[type=submit]" + +# 4. 截图验证 +visionl screenshot login -o logged-in.png +``` + +## 场景四:页面截图 + +```bash +# 打开并截图 +visionl page open https://www.example.com --alias page +visionl wait page --ms 2000 # 等待渲染 +visionl screenshot page -o page.png + +# 滚动后截图 +visionl scroll page --down 600 +visionl screenshot page -o page-scrolled.png +``` + +## 场景五:JS 数据提取 + +```bash +# 1. 打开页面 +visionl page open https://api.example.com/data --alias data + +# 2. 执行 JS 提取 JSON 数据 +visionl eval data "JSON.parse(document.body.innerText)" +# → {"ok":true,"data":{"result":{"items":[...]}}} + +# 3. 获取页面标题 +visionl eval data "document.title" +# → {"ok":true,"data":{"result":"API Data Page"}} +``` + +## 场景六:REST 直通(高级) + +```bash +# 不使用子命令,直接调用 HTTP API +visionl raw POST /pages '{"url":"https://example.com","alias":"test"}' +# → {"ok":true,"data":{"id":"p_5678",...}} + +visionl raw GET /pages/p_5678/text +# → {"ok":true,"data":{"text":"..."}} + +visionl raw DELETE /pages/p_5678 +# → {"ok":true,"data":null} +``` + +## 场景七:智能体自动化工作流 + +LLM 通过 VisionL 完成机票比价: + +``` +1. visionl page open https://flights.example.com --alias flights +2. visionl type flights "#from" "北京" +3. visionl type flights "#to" "上海" +4. visionl type flights "#date" "2026-08-20" +5. visionl click flights "#search" +6. visionl wait flights --selector ".results" +7. visionl text flights + → 提取票价信息 +8. visionl eval flights "document.querySelectorAll('.price').length" + → 统计结果数量 +9. visionl page kill flights +``` + +## 场景八:长时间运行的任务 + +```bash +# 打开监控面板 +visionl page open https://monitor.example.com --alias monitor --pretty +# ✓ 页面已打开 +# ID: p_mon_001 +# URL: https://monitor.example.com +# 别名: monitor + +# ... 过了一段时间 ... + +# 还是同一个页面 +visionl page list --pretty +# ✓ 1 个页面 +# p_mon_001 monitor https://monitor.example.com active + +# 刷新重新截图 +visionl navigate monitor https://monitor.example.com +visionl screenshot monitor -o latest.png +``` diff --git a/docs/usage/llm-integration.md b/docs/usage/llm-integration.md new file mode 100644 index 0000000..eada769 --- /dev/null +++ b/docs/usage/llm-integration.md @@ -0,0 +1,138 @@ +# 在 LLM 智能体中集成 VisionL + +> 本文档介绍如何让 LLM 通过工具调用使用 VisionL-CLI 操控浏览器。 + +## 原理 + +LLM 将 `visionl` 注册为一个系统命令/工具,在需要浏览网页时调用。 +所有命令输出结构化的 JSON,LLM 直接解析结果并决定下一步操作。 + +## 集成方式 + +### 方式一:Function Calling(推荐) + +在 LLM 的 function/tool 定义中注册 VisionL 命令。大多数 LLM 平台(OpenAI、Claude、本地模型)都支持。 + +**工具定义示例(OpenAI 格式):** + +```json +{ + "type": "function", + "function": { + "name": "visionl", + "description": "通过 VisionL 浏览器操控网页。子命令: page open|list|info|kill|kill-all, click, type, scroll, navigate, eval, wait, screenshot, text, html, daemon start|stop|status, raw", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "完整 visionl 命令,例如 'page open https://example.com'" + } + }, + "required": ["command"] + } + } +} +``` + +**使用流程:** + +1. LLM 决策需要访问网页 +2. LLM 生成 `visionl page open ` 调用 +3. 宿主程序在终端执行该命令,将 JSON 输出返回给 LLM +4. LLM 解析结果,继续决策(截图、点击、提取文本等) + +### 方式二:MCP Server + +可以封装一个 MCP(Model Context Protocol)Server,将 VisionL-CLI 包装为 MCP 工具: + +```typescript +// 伪代码示意 +server.tool( + "visionl", + "通过 VisionL 浏览器操控网页", + { command: z.string() }, + async ({ command }) => { + const { stdout } = await exec(`visionl ${command}`); + return JSON.parse(stdout); + } +); +``` + +### 方式三:Agent 框架集成 + +与 LangChain、AutoGPT、CrewAI 等框架集成,注册为自定义工具。 + +**LangChain 示例:** + +```python +from langchain.tools import Tool +import subprocess, json + +def visionl_tool(command: str) -> str: + result = subprocess.run( + ["visionl", *command.split()], + capture_output=True, text=True + ) + return result.stdout + +visionl = Tool( + name="visionl", + description="浏览器操控工具。命令示例:page open , click , text ", + func=visionl_tool, +) +``` + +## LLM Prompt 建议 + +在系统 prompt 中添加以下指引: + +``` +你可以使用 visionl 命令操控浏览器: + +1. visionl page open [--alias ] — 打开页面 +2. visionl page list — 列出所有页面 +3. visionl text — 获取页面文本 +4. visionl screenshot — 截图(返回 base64) +5. visionl click — 点击元素 +6. visionl type — 输入文本 +7. visionl page kill — 关闭页面 + +所有命令返回 JSON。解析 ok 字段判断成功/失败。 +使用 --alias 给页面起别名方便后续引用。 +``` + +## 多页面管理 + +LLM 可以同时打开多个页面,通过别名区分: + +``` +LLM: visionl page open https://docs.python.org --alias py +LLM: visionl page open https://developer.mozilla.org --alias mdn +LLM: visionl text py # 读 Python 文档 +LLM: visionl text mdn # 读 MDN 文档 +``` + +## 错误处理 + +LLM 应检查返回的 `ok` 字段: + +```json +// 失败示例 +{"ok":false,"error":{"code":"PAGE_NOT_FOUND","message":"页面 py 不存在"}} +``` + +常见错误及处理: + +| 错误码 | 处理建议 | +|--------|---------| +| `PAGE_NOT_FOUND` | 页面可能已被关闭,重新打开 | +| `DAEMON_UNREACHABLE` | 等待几秒重试(自动拉起正在启动) | +| `TIMEOUT` | 页面加载慢,重试或增加等待时间 | +| `ALIAS_EXISTS` | 换一个别名或直接用 page ID | + +## 安全注意事项 + +- VisionL daemon 仅监听 127.0.0.1,外部不可访问 +- 执行的 JS 代码在页面沙箱内运行,无法逃逸到宿主机 +- LLM 应避免在不可信页面执行敏感操作(自动填写密码等) diff --git a/docs/usage/quickstart.md b/docs/usage/quickstart.md new file mode 100644 index 0000000..4ab56e2 --- /dev/null +++ b/docs/usage/quickstart.md @@ -0,0 +1,89 @@ +# VisionL 快速开始 + +## 安装 + +```bash +# 1. 克隆仓库 +git clone ssh://git@git.yeij.top:2222/AskaEth/VisionL.git +cd VisionL + +# 2. 安装依赖 +npm install + +# 3. 安装 Chromium 浏览器 +npx playwright install chromium + +# 4. 构建 +npm run build + +# 5. 全局安装 CLI(可选) +cd packages/cli && npm link +``` + +## 基本使用 + +### 启动 daemon + +```bash +visionl daemon start +# ✓ Daemon 已启动 (端口 9527, PID 12345) +``` + +### 打开一个页面 + +```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"}} +``` + +### 获取页面文本 + +```bash +visionl text baidu +# {"ok":true,"data":{"text":"百度一下,你就知道\n..."}} +``` + +### 搜索 + +```bash +visionl type baidu "#kw" "VisionL" +visionl click baidu "#su" +``` + +### 截图 + +```bash +visionl screenshot baidu -o result.png +``` + +### 关闭页面 + +```bash +visionl page kill baidu +# {"ok":true,"data":null} +``` + +### 停止 daemon + +```bash +visionl daemon stop +``` + +## 无需手动启动 daemon + +CLI 会自动检测 daemon 是否运行,未运行则自动启动: + +```bash +# 直接使用,CLI 自动拉起 daemon +visionl page open https://example.com + +# 关闭所有页面(daemon 继续运行) +visionl page kill-all +``` + +## 查看所有页面 + +```bash +visionl page list +# {"ok":true,"data":[{"id":"p_xxx","url":"...","alias":"...","title":"...","status":"active"}]} +```