内容
# PhantomHand
PhantomHand 是一个面向 AI 智能体的浏览器控制引擎。它通过 [Chrome DevTools Protocol](https://developer.chrome.com/docs/devtools/) 以 [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) 标准把浏览器的感知与操控能力暴露给大语言模型,让智能体能像人一样视觉理解网页,并真实地点击、输入、采集信息。
> 所有命令默认作用于浏览器的**当前激活标签页**,也可通过显式 `tabId` 参数指定任意标签页。
---
## 核心特性
- **复用已登录的真实浏览器**:直接在你的日常 Chrome 上操作,保留登录态、插件、书签——无需另开隔离 profile。
- **真视觉感知**:Set-of-Mark 标注截图,能识别 canvas、视频、复杂 CSS 渲染的内容(纯无障碍树看不到的)。
- **稳定 ref 引用**:每个可交互元素分配稳定的 `ref`,操控时解析其**当前**坐标,页面轻微重排也不会失效。
- **拟人与反检测**:贝塞尔鼠标轨迹、人类化打字节奏、点击前瞄准停顿;基于 `chrome.debugger` 而非 WebDriver,并自动在页面主世界抹除自动化指纹。
- **信息密度优先**:文本快照、批量结构化提取、页面智能摘要、页面身份 API 请求;大体积产物默认存盘或摘要,不回流上下文。
- **可观测性**:console、网络请求、性能指标、Web Storage 全量回流。
---
## 快速使用
在你的 AI Agent(Claude Desktop、Cursor、Windsurf、Antigravity 等)的 MCP 配置中注册 `phantom-hand`。两种部署方式任选其一。
### A. 从 npm 安装(已发布)
```json
{
"mcpServers": {
"phantom-hand": {
"command": "npx",
"args": ["-y", "phantom-hand-mcp"]
}
}
}
```
### B. 从本地源码运行(开发 / 未发布)
克隆仓库后先构建,再用产物的**绝对路径**注册:
```bash
npm install && npm run build
```
```json
{
"mcpServers": {
"phantom-hand": {
"command": "node",
"args": ["<项目根目录的绝对路径>/dist/index.cjs"]
}
}
}
```
### 各 Agent 的配置文件位置
| Agent | 配置文件 |
|---|---|
| Claude Desktop(Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
| Claude Desktop(macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Cursor | 工作区 `.cursor/mcp.json`,或设置中的 MCP GUI |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
| Antigravity | `~/.gemini/config/mcp_config.json` |
保存并重启 Agent,服务会在后台启动并监听 `ws://localhost:37210`。
---
## 安装浏览器扩展(必需)
PhantomHand 通过一个浏览器扩展与 Chrome 通信,需手动加载一次:
1. 让你的 AI 助手调用 `get_extension_info` 工具,获取本机扩展目录的绝对路径(位于 `~/.npm/_npx/.../extension`)。
2. 打开 `chrome://extensions/`,开启右上角「开发者模式」,点击「加载已解压的扩展程序」,选中上一步的目录。
3. 点击浏览器工具栏的 PhantomHand 图标,URL 保持默认 `ws://localhost:37210`,点击「Connect to Agent Server」,指示灯变绿即连接成功。
---
## 工具清单
| 分类 | 工具 |
|---|---|
| 元信息 | `get_extension_info` |
| 感知 | `browser_scan`、`browser_snapshot`(文本快照,无图)、`browser_screenshot`、`browser_get_page_info`、`browser_query_ref`(按 selector 精确取 ref) |
| 鼠标 | `browser_click`、`browser_click_ref`、`browser_double_click`、`browser_triple_click`、`browser_right_click`、`browser_hover`、`browser_drag` |
| 键盘 / 剪贴板 | `browser_type`、`browser_type_ref`、`browser_press_key`、`browser_keyboard_shortcut`、`browser_select_all`、`browser_clipboard_set`、`browser_clipboard_get` |
| 滚动 | `browser_scroll`、`browser_scroll_to_element` |
| DOM / JS | `browser_eval_js`、`browser_get_html`、`browser_extract_text`、`browser_get_attributes`、`browser_extract_structured`(批量 → JSON)、`browser_read_page`(主内容智能摘要) |
| 表单 | `browser_set_value`、`browser_clear_value`、`browser_select_option`、`browser_check`、`browser_focus`、`browser_upload_file` |
| 标签页 / 导航 | `browser_navigate`、`browser_go_back`、`browser_go_forward`、`browser_reload`、`browser_tab_new`、`browser_tab_close`、`browser_tab_list`、`browser_tab_switch` |
| 可观测性 · debug | `browser_list_console_messages` |
| 可观测性 · network | `browser_list_network_requests`、`browser_get_network_request`、`browser_fetch`(以页面身份请求 API)、`browser_mock_request`、`browser_clear_mocks` |
| 可观测性 · performance | `browser_performance_start_trace`、`browser_performance_stop_trace` |
| 存储 | `browser_storage_get`、`browser_storage_set`、`browser_storage_remove` |
| 捕获 / 模拟 | `browser_print_to_pdf`、`browser_set_viewport`、`browser_clear_viewport`、`browser_screencast_start`、`browser_screencast_stop` |
| 会话 | `browser_session_export`、`browser_session_import` |
| 时序 | `browser_wait`、`browser_wait_for_element`(精确等待元素出现/消失)、`browser_fast_forward`、`browser_wait_for_download` |
---
## 核心范式
### ref 引用(推荐的元素操控方式)
`browser_scan` / `browser_snapshot` 为每个可交互元素分配稳定 ref(如 `[ref: 3]`)。优先用 `browser_click_ref` / `browser_type_ref` 按 ref 操控——它会解析元素**当前**的坐标,即使页面发生轻微重排也能正确命中。若已知目标的选择器,可直接用 `browser_query_ref` 获取 ref,跳过全页扫描。
### 穿透同源 iframe
感知会递归进入 shadow DOM 与**同源** iframe:iframe 内的元素同样分配 ref,坐标已折算到主页面视口,可直接用 ref 操控,无需关心 iframe 边界与坐标偏移。跨域 iframe(如第三方验证码)的 DOM 不可访问,不会出现在感知结果中。
### 信息密度
为避免上下文膨胀,大体积产物默认**存盘或摘要**:
- `browser_print_to_pdf` 把 PDF 写入下载目录,仅返回路径;
- `browser_storage_get` 不传 key 时只返回键名列表;
- `browser_screencast_start` / `browser_screencast_stop` 只统计帧数,不回传画面;
- `browser_scan` 在长页面上提示「视口内 X / 共 Y 个元素」,告知还有多少未展示。
需要采集成片内容时,优先用高密度工具:`browser_extract_structured`(批量结构化)、`browser_read_page`(页面摘要)、`browser_fetch`(直连站点 API)。
### 自动等待
导航与点击/输入默认启用 auto-wait:导航等待页面 `complete`,交互等待网络空闲(`networkIdle`)。对 SPA 中延迟渲染的元素,用 `browser_wait_for_element` 精确等待其出现、附着或消失。
### 反检测
PhantomHand 基于 `chrome.debugger`(CDP)操控浏览器,不携带 WebDriver 指纹。每次连接一个标签页后,会自动向页面的**主世界**注入轻量补丁(经 `Page.addScriptToEvaluateOnNewDocument`,在任何站点脚本之前生效),抹除剩余自动化痕迹:
- `navigator.webdriver` → `undefined`
- 补全 `window.chrome.runtime`
- 规整 `navigator.languages` / `navigator.plugins`
- 修正 `permissions.query` 对 `notifications` 的泄漏
注入逐标签页幂等,标签页关闭时自动清理,默认开启、无需配置。鼠标移动采用贝塞尔曲线加随机抖动,打字按字符类型差异化停顿(句末标点长停顿、逗号中停顿),点击前有短暂瞄准停顿。
---
## 配置
所有配置通过环境变量驱动(详见 `.env.example`):
| 变量 | 默认 | 说明 |
|---|---|---|
| `PORT` | `37210` | WebSocket 端口 |
| `PHANTOM_HAND_TOKEN` | (空) | WebSocket 鉴权 token |
| `PHANTOM_HAND_SCREENSHOT_FORMAT` | `jpeg` | 截图重编码格式(`jpeg`/`webp`/`png`,需安装 `sharp`) |
| `PHANTOM_HAND_SCREENSHOT_QUALITY` | `75` | 截图质量 1-100 |
| `PHANTOM_HAND_SCREENSHOT_MAX_WIDTH` / `_HEIGHT` | `0`(不限) | 截图最大尺寸,超出则等比缩小 |
| `PHANTOM_HAND_AUTO_WAIT_TIMEOUT` | `3000` | auto-wait 等待上限(ms),`0` 关闭 |
| `PHANTOM_HAND_DOMAIN_ALLOWLIST` | (空) | 域名白名单(逗号分隔),设置后仅允许这些域名 |
| `PHANTOM_HAND_DOMAIN_BLOCKLIST` | (空) | 域名黑名单(逗号分隔) |
| `PHANTOM_HAND_CATEGORY_DEBUG` / `_NETWORK` / `_PERFORMANCE` / `_STORAGE` | `true` | 设为 `false` 隐藏整类工具,缩小 agent 选择面 |
**截图压缩(可选)**:默认极轻量。安装 `npm i sharp` 后,截图会按上述参数重编码(webp/jpeg 显著小于 png),降低上下文占用;未安装时返回原图,功能不受影响。
---
## 安全
- 服务默认监听 `ws://localhost`,仅本机可连。**不要将服务暴露到公网或不受信任的远程地址**——`browser_eval_js` 等工具等同于本机浏览器的远程代码执行权限。
- 启用鉴权:设置 `PHANTOM_HAND_TOKEN=<密钥>`,并在扩展弹窗 URL 中追加 `?token=<密钥>`。服务会拒绝未携带正确 token 的连接。
- 除非完全控制远端主机,否则不要在扩展弹窗填写 `wss://远程地址`。
---
## 多 Agent 并发(Proxy Broker)
多个宿主 Agent(如同时运行 Cursor 与 Claude Desktop,各自启动 phantom-hand)可共享同一个浏览器扩展:
1. 第一个进程占用 `37210` 端口,成为 **Master**,承载 WebSocket 服务并与扩展通信。
2. 后续进程发现端口被占用,自动作为 **Proxy** 连接到 Master,转发命令。
每条请求/响应通过唯一 `requestId` 路由,并发调用互不串扰。若希望多个 Agent 各自控制独立标签页,可在工具调用中传入显式 `tabId`(通过 `browser_tab_list` 获取)。
---
## 开发
```bash
npm install # 安装依赖
npm run build # esbuild 打包到 dist/index.cjs
npm run dev # 以 tsx 直接运行 src/index.ts
npm start # 运行编译产物
npm test # 单元测试
npm run lint # tsc --noEmit 类型检查
```
端到端功能验证见 [`test/smoke-test.md`](./test/smoke-test.md)。
---
## 常见问题
1. **发送指令后浏览器无反应?**
Chrome 禁止扩展在系统保护页(`chrome://newtab`、`chrome://settings`、`chrome://extensions`、Web Store 等)执行调试 API。请确保当前聚焦一个常规网页。
2. **端口被占用?**
若 `37210` 被占用,服务不会结束占用进程,而是自动以 Proxy 模式继续工作(见上节)。如需换端口:设置 `PORT=xxxx`,并在扩展弹窗填入对应端口。
3. **浏览器一直显示「正在调试此浏览器」黄条?**
这是 CDP 调试的正常表现。扩展会在标签页关闭时自动 detach 调试器;如需立即清除,关闭对应标签页。
4. **剪贴板读取偶尔失败?**
现代浏览器要求剪贴板读取有用户手势。无手势上下文中 `browser_clipboard_get` 可能失败——这是浏览器安全策略。
5. **按 ref 操作时报「ref not found」?**
ref 不跨扫描保活。重新 `browser_scan` / `browser_snapshot` / `browser_query_ref` 获取新 ref 后再操作。