Metadata-Version: 2.5
Name: agent-gavel
Version: 0.2.3
Summary: 验证驱动的多端操作框架：动作发出去，程序断言判成败，省掉一次模型往返
Project-URL: Homepage, https://github.com/Rottenwooood/agent-gavel
Project-URL: Repository, https://github.com/Rottenwooood/agent-gavel
Author: Rottenwooood
License: MIT
License-File: LICENSE
Keywords: agent,atspi,automation,browser,cdp,dom,mcp,web
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.13
Requires-Dist: mcp[cli]>=2.1.1
Requires-Dist: psutil>=6.0
Requires-Dist: pypdf>=5.0
Requires-Dist: python-docx>=1.1
Requires-Dist: websockets>=17.1
Description-Content-Type: text/markdown

<div align="center">

# agent-gavel

**AI 操作浏览器/桌面，动作发出后由程序断言判成败，省掉一次"把页面喂回模型判断"的往返。**

![License](https://img.shields.io/badge/license-MIT-blue) ![Python](https://img.shields.io/badge/python-3.13-brightgreen)

---

</div>

## 这是什么

agent-gavel 是一个给 AI 用的 MCP server，提供"操作网页 / 桌面 + 程序化验证"。

典型的浏览器 agent 循环是：动作 → 页面快照喂回模型 → 模型判断成败 → 下一步。agent-gavel 改成：

```
AI 声明动作 + "做完后页面应该长什么样" → 执行 → 程序断言 → 返回 pass/fail/ambiguous + 实际值
```

成败由程序判定，模型不用回头"看"一遍页面。省掉每次动作后的一次模型往返（慢 + 费 token + grep容易漏,全量看token花费大）。

结果**三态**：`pass`（断言满足）/ `fail`（断言未满足且页面无变化，明确失败）/ `ambiguous`（拿不准——断言没过但页面确实变了、或无断言但检测到变化，交回模型仲裁）。多数场景是明确的 pass/fail，ambiguous 只留给少数真拿不准的情况。

**两个通道，可独立安装：**

| 通道 | 干什么 | 需要什么 |
|---|---|---|
| **DOM**（网页，主通道） | 通过 Chrome CDP 操作真实浏览器：填表 / 点击 / 搜索 / 登录，全部 `isTrusted=true` 真实输入 | 本机 Chrome + Python 3.13，纯 Python 依赖 |
| **AT-SPI**（桌面，实验性·停止开发） | 通过无障碍树操作桌面应用 | 额外 `npm i -g computer-use-linux` |

不装 computer-use-linux 也能用 DOM；装了才解锁桌面工具（不装时桌面工具返回 `atspi_unavailable` 的明确提示）。

## Install

**兼容性**：Linux + Python 3.13 验证过，需本机有 Chrome/Chromium。**Windows 上 DOM 通道已适配**（Chrome 自管/进程清理/置顶已跨平台，Windows + Chrome 实测跑通；见文末 TODO）；macOS 与桌面(AT-SPI)通道仍仅限 Linux。

### 环境准备（Ubuntu / Windows）

两者都需要 **Python 3.13**（`uv` 会自动装，`pip` 方式请自备）和一个 **Chrome/Chromium**；DOM 通道默认**在桌面会话里跑**（调试 Chrome 是可见窗口）。无桌面会话（服务器 / CI / SSH）可切无头模式，见下文"怎么接到 opencode"的 `AGENT_GAVEL_HEADLESS`。

**Ubuntu**

```sh
# 1. 装 Chrome（二选一）
sudo apt install -y ./google-chrome-stable_current_amd64.deb   # 先从 google.com/chrome 下载 deb
sudo apt install -y chromium-browser                            # 或装 Chromium

# 2. 置顶工具（可选，装了才能把调试 Chrome 窗口自动置顶）
sudo apt install -y xdotool

# 3. 确认在桌面会话（有 DISPLAY 或 WAYLAND_DISPLAY）
echo "$DISPLAY $WAYLAND_DISPLAY"
```

Chrome 需能在 PATH 里找到（`google-chrome` / `google-chrome-stable` / `chromium` / `chromium-browser` 任一）。

**Windows**

```powershell
# 1. 装 Chrome
#    自动探测顺序：Program Files / Program Files(x86) / LOCALAPPDATA 下的 chrome.exe、msedge.exe，再查 PATH

# 2. 确认在正常桌面会话即可
```

两种安装方式，按场景选：

```sh
# ① 全局安装 ——命令装进 ~/.local/bin，全局 PATH 可用
uv tool install agent-gavel

# ② 装进当前 Python 环境（项目 venv / conda env）
pip install agent-gavel

# 桌面操作（可选，仅 Linux）：不装则 DOM 照常可用，只是桌面工具返回 atspi_unavailable
npm install -g computer-use-linux
```

装好后在 MCP 客户端（见下）里配置`["agent-gavel"]`（方式①②，命令已在 PATH），重启后 `doctor` 工具会报告 DOM 通道状态（Linux 上还含 AT-SPI）——这就是安装成功的信号。

### 从源码安装

```sh
git clone https://github.com/Rottenwooood/agent-gavel.git
cd agent-gavel
uv sync                    # 装依赖（Python 3.13）
uv run python -m agent_gavel.main    # 起 MCP server
uv run python tests/browser_manager_smoke.py   # Chrome 生命周期冒烟测试
uv build                   # 本地构建 wheel/sdist
```

`uv run python ...` 两端通用。Linux 上另有 `./run-mcp.sh`——带优化版 computer-use-linux 的启动器（仅源码运行用）；Windows 直接用上面的 `uv run python -m agent_gavel.main`。

源码跑通后，想贡献你验证过的模板 → 见文末"贡献你验证过的模板"。

## 安装后到底发生了什么（流程说明）

### 两种安装方式的区别

**`uv tool install agent-gavel`** —— 全局安装（推荐正式用）：
- uv 把 agent-gavel 装进 uv 的 tool 目录（`uv tool dir` 可查；Linux 默认 `~/.local/share/uv/tools/`）
- 可执行命令 `agent-gavel` 链接到 uv 的 bin 目录（Linux `~/.local/bin/`，Windows `%USERPROFILE%\.local\bin`），已在 PATH 里
- 任何目录都能直接 `agent-gavel` 起 server，类似 `npm i -g`

**`pip install agent-gavel`** —— 装进当前 Python 环境（项目 venv / conda env）：
- `agent_gavel/` 包落到 site-packages（Linux `<venv>/lib/python3.13/site-packages/`，Windows `<venv>\Lib\site-packages\`）
- 同时生成可执行命令 `agent-gavel`（指向 `agent_gavel.main:main`），在所在环境 PATH 里

三种方式最终效果一致：**启动一个在 stdio 上说话的 MCP server 进程**，等 MCP 客户端连它。区别只在命令装在哪、是否全局可用。

### 怎么接到 opencode

opencode 通过 `command` 数组拉起这个进程，两者用 stdio 通信（配置在 opencode 的配置文件里，Linux/macOS 通常是 `~/.config/opencode/opencode.json`；Windows 路径见 opencode 文档）：

```json
{
  "mcp": {
    "agent-gavel": {
      "type": "local",
      "command": ["agent-gavel"],
      "enabled": true
    }
  }
}
```

重启 opencode，工具列表出现 `agent-gavel_dom_*` 系列。**无需手动开 Chrome**——首次 DOM 调用时 server 自动用独立 profile 拉起调试 Chrome，崩溃自动重拉，退出自动清理。

**可选无头**：默认拉起**可见窗口**的调试 Chrome（能看到操作过程）。无桌面会话（服务器 / CI / SSH）时，在 MCP 客户端给这个 server 传环境变量切无头：

```json
"agent-gavel": {
  "type": "local",
  "command": ["agent-gavel"],
  "environment": { "AGENT_GAVEL_HEADLESS": "1" },
  "enabled": true
}
```

无头用 `--headless=new`（Chrome 112+），**不要求 `DISPLAY`**；`doctor` 的 `cdp_mode` 会报告当前模式（`headed` / `headless`）。切换模式后**重启 opencode**（旧 Chrome 随 server 退出被清理）。注意：无头下无法人工扫码/验证码，登录类流程需依赖已登录 profile。

### 模板存在哪

- **随包模板**：安装自带的模板在 `site-packages/agent_gavel/channels/{dom,desktop}/templates/`（只读默认，如百度/必应/豆瓣等 8 个验证过的）
- **你的模板**：`dom_save_template` / `desktop_save_template` 保存到你自己的用户目录——Linux `~/.config/agent-gavel/`，Windows `%APPDATA%\agent-gavel\`（下分 `templates/`、`desktop_templates/`）——跨缓存、跨安装版本持久存在，不会被升级覆盖
- 读取时**用户目录优先**：你保存的同名模板覆盖自带模板；失效检测记录（stats）也存用户目录

## 实测耗时

### DOM 通道（当前各步骤耗时，测试环境：Linux + 本机 Chrome）

| 阶段 | 耗时 | 说明 |
|---|---|---|
| Chrome 冷启动（首次连接） | ~5s | 只在进程没起时一次；热连接 ~30ms |
| `navigate`（跳转等加载） | 150–300ms | 热站，readyState complete 即返，无固定 sleep |
| `set_value`（填输入框+断言） | 20–50ms | 真实输入 |
| `click`（点击+断言） | ~10ms | 单点 |
| `press_key`（按键） | ~5ms | 单键/组合键 |
| `press_enter`（提交+等结果页） | 150–400ms | 含页面真实响应 |
| 单模板（3 步：导航+填+提交） | ~0.5–1s | 视站点加载速度 |

数据来自 `tests/dom_smoke.py` 分段计时与 `tests/dom_smoke_loop.py` 多模板循环压测（5 模板 × 3 轮全过，平均单次 ~0.6s）。慢的是页面真实加载，工具本身动作步都在毫秒级。

**验证判定耗时**（`tests/verify_branches.py`，10 个分支全过）：

| 分支 | 结果 | 耗时 |
|---|---|---|
| 有断言 + 满足 | pass | ~3ms |
| 有断言 + 不满足 + 页面有变化 | ambiguous | ~5ms（不等超时，立即判定）|
| 有断言 + 不满足 + 页面无变化 | fail | ~wait_s（等满才判失败）|
| 无断言 + 有变化 | pass | ~4ms |
| 无断言 + 无变化 | ambiguous | ~5ms |
| 有断言 + 延迟 500ms 满足 | pass | ~511ms |
| 降级全失败（strict=False，3 策略）| fail | ~1.8s |

DOM diff 兜底让"无断言"从原来固定 sleep 0.5s → **~4ms**（~100x）；early ambiguous 让"断言失败但页面有变"从等满 wait_s → **~5ms**。只有真 fail 和降级才耗超时。

### AT-SPI 桌面通道：微信发消息模板

微信发消息模板（4 步：激活窗口→点会话→输入→发送）的耗时构成，实测微信窗口开着时（优化前基准）：

| 步骤 | 动作 | 总耗时 | 其中读树(before+稳定) | 动作本身 |
|---|---|---|---|---|
| step0 | activate_window | 1472ms | 1332ms | 131ms |
| step1 | click 文件传输助手 | 1508ms | 1145ms | 362ms |
| step2 | type(剪贴板) | 1507ms | 1161ms | 346ms |
| step3 | click 发送+断言 | 2533ms | 576ms+稳定 | 354ms |
| **合计** | | **~6.8s** | | |

**耗时大头在读树**：每步做 2 次 `get_app_state`（before + 稳定确认），每次 ~580ms（微信 ~600 节点 × ~10 次 DBus 读）。step3 多一次等消息。

**优化**：给 computer-use-linux 打补丁（`third_party/` 保存）——AT-SPI 连接复用 + doctor 缓存 + `fast_app_filter` 跳过全桌面 pid 反查 + 坐标操作不再触发 portal 截屏；server 层读树次数减半（before 复用 + 确定性动作跳过稳定确认）。桌面 70 节点应用实测单次读树：

| 阶段 | 原版 | 优化后(fast=true) |
|---|---|---|
| doctor_report | ~150ms | ~0ms（缓存） |
| app_filter | ~280ms | ~0ms（按 pid 直选） |
| snapshot_tree | ~145ms | ~140ms |
| **合计/次读树** | **~630ms** | **~220ms**（2.9x） |

端到端对比：`read_state` 620→235ms（2.6x）；`act_and_verify` 单次闭环 ~1.5s→0.65s（2.3x）；**微信发消息模板整条流程 6.8s→4.8s**。树内容与动作结果和原版一致，无回归。

## 功能一览

### DOM（网页）

- **Chrome 自管**：首次调用自动起独立 profile 调试 Chrome（默认可见窗口；`AGENT_GAVEL_HEADLESS=1` 切无头），崩溃自愈，退出清理
- **`dom_step`**：通用单步闭环（动作 + 选择器 + 断言一次调用），动作空间按**输入通道参数化**——键盘 `press_key`（特殊键：Enter/Tab/F5/ArrowDown…；组合键 `Ctrl+A` 有解析 bug，见 TODO）+ 文本 `type_text`（含中文）+ 鼠标 `click`（左/右/中、单击/双击）+ `hover`/`drag`/`scroll` + 导航 `navigate`；`set_value/press_enter/clear/focus` 作便捷动作保留。点击/按键可能跳转时用 `wait_navigation=True`，等跳转完成再返回最终 url
- **`dom_read`**：只读取值——给一组"名字 → 取值规则"直接返回实际值，不判定 / 不重试 / 不 diff（读标题、输入框内容、正文、PDF 文字层）。取值规则支持纯表达式，或函数形式 `"() => { ...; return ...; }"`（多语句 / 赋值）
- **`dom_text`**：取页面/元素文本（`text` 可见文字 / `content` 原始文字 / `html` 结构），正文提取；浏览器内置 PDF 阅读界面不是页面元素，取不到
- **`dom_explore`**：枚举页面可交互元素，给验证过唯一的锚点（`#id` / `input[name=q]` / `__text__:登录` / 同名按钮用 `__text_nth__:N::`）
- **模板复用**：跑通的流程存 JSON（`templates/`，按网站组织：`site` 纯站名，文件名 `site_功能.json`），换参数直接跑
- **失败诊断**：环境错误（Chrome 没起 / Linux 缺 DISPLAY / CDP 断）返回 `reason + hint`，hint 里会提示可切无头（`AGENT_GAVEL_HEADLESS=1`），不会让 agent 对着一个 "Error executing tool" 猜
- **三态验证 + DOM diff 兜底**：无断言时不再盲 sleep 放行——抓动作作用域前后 DOM 结构签名做 diff，变化明显判 `pass`、无变化判 `ambiguous`；断言失败但页面确实变化 → `ambiguous`（可能点错/断言写窄），断言失败且无变化 → `fail`
- **断言降级重试**：fail 自动换策略（trusted 翻转 → 重新 explore 换锚点 → 滚动 → 切换等待模式）；`strict` 参数关掉降级暴露真实 fail（写模板/排查时用）
- **模板失效检测**：连续失败 ≥3 次标 suspected，再跑返回 warning 建议重新探索；任何一次 pass 清零

#### 工具选型（该用哪个）

| 我要做的事 | 用哪个 |
|---|---|
| 打开一个网址 | `dom_navigate`（**唯一导航入口**；自动验证到没到，返回最终 url / 是否跳转 / content_type）|
| 读页面上的值（标题 / 输入框 / 某字段 / 一段文本） | `dom_read`（**通用读**：给"名字 → 取值规则"，直接返回值，不判定不重试）|
| 读整页或某块的正文、或页面所有链接 | `dom_text`（`text` / `content` / `html` / `links`；dom_read 的专用视图）|
| 做一个动作并验证结果 | `dom_step`（动作 + 断言；**不含导航**——导航用 dom_navigate）|
| 看页面有哪些可点 / 可填元素 | `dom_explore`（dom_read 的专用视图，锚点带唯一性校验；可传 url 一次性探索）|
| 读 .pdf / .docx 直链文件内容 | `dom_document`（dom_read 读不到的"文件"视图）|
| 重复跑同一套流程 | `dom_save_template` 存模板 → `dom_run_template` |
| 以"读"为主、几乎不点 | `dom_navigate` + `dom_text` + `dom_read` 三件套足够 |

一句话边界：**改页面用 `dom_step`（导航除外），纯读用 `dom_read`（或它的三个专用视图 `dom_text` / `dom_explore` / `dom_document`），导航只用 `dom_navigate`。**

### AT-SPI（桌面，实验性·停止开发）

> 曾用于操作桌面应用（`act_and_verify` / `run_operation` / 桌面模板，经 computer-use-linux 无障碍树）。Linux 上性能不佳、微信等闭源应用 A11y 树不完整，已停止开发。工具保留可用但不投入；纯 DOM 用法无需安装 computer-use-linux。

### 验证机制（核心，DOM 在用）

- **三态判定**：`pass` / `fail` / `ambiguous`——程序能明确判的就判，判不了的交回模型
- **断言**：`eq / neq / exists / not_exists / contains`（特征用 JS 表达式提取）
- **DOM diff 兜底**：无断言、或断言失败时，抓动作作用域的前后 DOM 结构签名做 diff——URL/TITLE 变化=导航级，局部作用域任意 1 处变化即算，body 作用域需足够增删（过滤广告位等噪音），据此程序判 `pass`/`ambiguous`
- **等待**：`poll`（轮询断言，间隔 0.1s）/ `event`（MutationObserver，DOM 一变即醒，适合异步长等待）
- **降级重试**：fail 自动换策略（trusted 翻转 → 换锚点 → 滚动 → 切等待模式）；`strict=True` 关降级暴露真实 fail
- 每动作返回 `cost.elapsed_ms`（端到端毫秒）；各分支结果与计时见 `tests/verify_branches.py`

## TODO

### 通道状态

- **DOM（网页）**：主通道，维护中。
- **AT-SPI（桌面）**：**实验性，Linux 上性能不佳（读树慢、微信等闭源应用 A11y 树不完整），暂时停止开发**。不装 computer-use-linux 也可用 DOM；桌面工具保留但不再投入。

### 修复（动作空间）

- [x] **三态验证 + DOM diff 兜底**（本次）：无断言/断言失败时抓动作作用域前后 DOM 结构签名做 diff，程序判定 `pass`/`fail`/`ambiguous`；修复 early ambiguous 被 `all_pass=False → fail` 覆盖的 bug；`tests/verify_branches.py` 10 分支全过
- [ ] **press_key 组合键 bug**：三种拼写行为不一致——`Ctrl+A`/`Control+A` 能识别修饰键但主键 `A` 走 raw_char 分支（`_KEYS` 只收小写字母）；`ctrl+a` 的修饰键完全丢失（`_MODIFIERS` 只认 `Control` 不认 `Ctrl`/小写）。需：修饰键名规范化（大小写/全称 `Control`↔`Ctrl`），主键字母统一转小写查 `_KEYS`，组合按下带修饰位。当前单键特殊键 OK（Enter/Tab/F5 等），普通字符键走 type_text。
- [x] **单键特殊键已确认**：Home 光标归零、双击选词（click count=2）、及其他原子动作（type_text 中文/右键/hover/drag/scroll）实测正常（`tests/dom_atomic_coverage.py` 17 步全过）。

### 平台 / 能力

- [x] **适配 Windows（DOM）**：Chrome 自管跨平台化（psutil 进程管理、`taskkill /T` 清理、user32 置顶、Windows 版 Chrome 路径探测），去掉 DISPLAY 硬检查；Windows + Chrome 153 实测跑通 `dom_step`/导航全链路（Python 3.12 验证，声明 3.13）。
- [ ] **适配其他平台**：macOS 尚未适配（需补 Chrome 路径/进程管理/置顶）；桌面(AT-SPI)通道依赖 Linux 无障碍树，不随 DOM 跨平台。
- [ ] **完善模板共享机制**：当前模板存本地用户目录，缺少"模板共享/导入"通道（按站点从远端拉模板、版本化、社区模板源）。
- [ ] **登录态模板**：Chrome profile 登录态保留，覆盖真实登录类流程。
- [ ] **多步骤 / 分页 / 滚动模板**：现有模板多是"导航+填+提交"，缺连续点进详情、无限滚动、多 tab。
- [ ] **断言语言增强**：`eq/neq/exists/contains` 之外，补数值比较 / 正则 / 列表断言（结果条数等）。

### 产品化

- [ ] **权限 / 确认机制**：高危操作（提交表单 / 发送）前人工确认。
- [ ] **可观测性**：debug 日志之上，补任务级 trace / 会话重放。

## 贡献你验证过的模板（PR template）

agent-gavel 的模板按网站组织（`site` 纯站名，文件名 `site_功能.json`），随包带 8 个验证过的。你在一台机器上跑通了新网站的流程，**PR 给我**，合并后所有用户随包可用。

仓库内的验证过的模板：百度 / 百度百科 / 必应 / 博客园 / 豆瓣电影 / Google / 菜鸟教程 / 知乎登录填表。
设计说明见 [docs/internal-beta.md](docs/internal-beta.md)，路线见 [docs/roadmap.md](docs/roadmap.md)。

### 模板去哪、怎么存

模板默认读**用户目录**优先（Linux `~/.config/agent-gavel/`，Windows `%APPDATA%\agent-gavel\`；下分 `templates/`、`desktop_templates/`），随包模板在 `agent_gavel/channels/{dom,desktop}/templates/`。

贡献模板的路径：源码 clone → 用 `dom_explore` + `dom_step` 现场探索跑通 → `dom_save_template` 存 JSON → 确认稳定后放进仓库对应目录提 PR。

### 模板 PR 检查清单

模板 PR 要能直接合并，请过一遍：

- [ ] 模板 JSON 在 `agent_gavel/channels/dom/templates/<site>_<func>.json`（或 desktop 对应目录）
- [ ] `site`（DOM）/ `app`（desktop）字段是纯站名，不含功能名；文件名 `站名_功能`
- [ ] 真实跑通过（`dom_run_template` / `desktop_run_template` 全 pass），PR 描述里贴结果
- [ ] 步骤含断言（`page_features` + `expected_feature`），不是只发动作不验证
- [ ] 用了稳定锚点（`#id` / `input[name=x]` / `__text__:`），没有写死易变的 CSS 路径
- [ ] 用 `$VAR` 占位符替代可变输入（关键词/账号/密码），**不要写死真实值**——`params` 字段自动从 `$VAR` 提取（`dom_list_templates` 会显示模板需要哪些参数）
- [ ] **密码/token 等敏感参数在模板里显式声明**：`dom_save_template(..., sensitive_params=["PASSWORD"])` → params 存 `{"PASSWORD": {"sensitive": true}}`。声明后运行时传的真实值不落日志、不进返回；不声明则默认不敏感（仅参数名命中 PASSWORD/TOKEN 等关键词时兜底脱敏）
- [ ] 涉及登录/个人数据时：模板文件里用**假凭据占位**（如 `$USERNAME`/`$PASSWORD`，跑的时候才传真实值）；若流程依赖"已登录浏览器"（如删订单），PR 里说明依赖的登录态，别假装模板能独立登录
- [ ] 描述里写清：站点 URL、功能、测过的关键词/参数

模板存疑或失效会走失效检测（连续失败 ≥3 次标 suspected），无需担心一次不完美。

## License

MIT
