一、账号台账(新表 device_account,schema v8) - core/ledger.py:CRUD、Excel 粘贴解析(Tab 分隔 / 表头乱序缺列 / 是·否布尔 / 逐行留痕)、 按范围取号(本机台账 / 全部 / 按设备分组)、设备端账号块、dry_run 预览 - 新「账号」顶级 Tab(权限 devices):列表 + 搜索 + 设备筛选 + 增删改 + 粘贴导入(默认先预览再写,逐行显示 新增/覆盖/跳过/失败 + 留痕) - 设备池加「账号 N」列(0 个显示"未登记"),点开看该设备账号明细 - 任务「条件判断」新增 cmp_source / cmp_group:候选值可直接来自台账, 与手填值**合并(OR)**;取不到号时回落手填值,并把原因写进步骤明细与日志 (否则"永远走 else 分支"而任务照样显示成功,最难查) - 设备端契约:身份页多推 accounts_b64(base64 JSON;默认不含手机号; 超预算按整条丢且绝不算字节切),见 doc/DEVICE_AGENT.md §5.2.1 - 备份/文档红线:TABLE_LABELS 加「账号台账」;DATA_MODEL/API/ARCHITECTURE/DEPLOY/ TASK_DEV/DEVICE_AGENT/DEVELOPMENT/README 同步;顺手补上 DATA_MODEL 漏列的 done_mark ⚠ 台账里的抖音号是**纯号**,只当"比对用的候选值":绝不能拿去填「去重」的身份元素 (身份是元素原文逐字算 key,格式不同会让去重静默失效)。代码与文档都写明了。 二、剪贴板注入通道(修:平台还在调早期的独立 APK) - 改为按顺序尝试:设备端 Agent(com.example.deviceagent/.ClipActivity)→ 旧版独立 ClipInject 兜底;两个都没有时报"需要设备端 Agent"(不再只说 ClipInject) - 读回验证改成**轮询到 3 秒**:透明 Activity 要等窗口拿到焦点才写, 原来只睡 0.5s 会读到上一次的内容 → 误报"写入可能被拒"(实测内容已写入却回失败)
205 lines
11 KiB
Markdown
205 lines
11 KiB
Markdown
# MCP 手机控制手册(MCP.md)
|
||
|
||
多模态 AI(DeepSeek / Claude 等)通过 [MCP(Model Context Protocol)](https://modelcontextprotocol.io) 操作 Android 手机的统一出口。平台自带的「AI 控制台」(`mcp_agent/`)与外部 MCP 客户端使用的都是这一批工具。
|
||
|
||
> 本文 = **使用手册**(工具清单 / 用法 / 接入)。设计取舍与演进见 [MCP_DESIGN.md](MCP_DESIGN.md);AI 控制台的会话/经验/动作机制见 [AI_CONSOLE.md](AI_CONSOLE.md)。
|
||
|
||
---
|
||
|
||
## 1. 架构与边界
|
||
|
||
```
|
||
AI 控制台 / 外部 MCP 客户端
|
||
│ Streamable HTTP(http://<host>:8033/mcp)
|
||
▼
|
||
MCP Server(mcp_server/mcp_server.py) ← 20 个 de_* 工具
|
||
│ 平台 HTTP API(登录 + CSRF) 或 轻量 adb/u2 直连
|
||
▼
|
||
auto_control 平台(:18050) ← 设备池 / 任务 / 看屏
|
||
│ adb / uiautomator2 / uiautodev / OCR
|
||
▼
|
||
Android 设备(IP:5555)
|
||
```
|
||
|
||
- **轻量通道直连**(adb monkey 开 App、u2 输入、本地 OCR)不绕平台,省时间省 token
|
||
- **坐标换算、UI 吸附、剪贴板注入**等细节全部在 Server 层消化,模型只给"意图"
|
||
- **只暴露设备层能力**:平台级的任务增改/分组/设备池/APK/备份等 REST 只给 Web 前端用,未做成 MCP 工具(补齐规划见 [AI_TASK_GEN.md](AI_TASK_GEN.md) §9)
|
||
|
||
---
|
||
|
||
## 2. 运行与配置
|
||
|
||
生产跑在 220 的 `python-app` 容器内(`scripts/start.sh` 自动拉起),端点 `http://192.168.20.220:8033/mcp`。
|
||
|
||
```bash
|
||
# 本机手动启动
|
||
MCP_ALLOW_WRITE=1 MCP_PLATFORM_USER=admin MCP_PLATFORM_PASS=<平台密码> \
|
||
MCP_AUDIT_FILE=data/mcp_audit.log python -m mcp_server.mcp_server
|
||
```
|
||
|
||
| 环境变量 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `MCP_PLATFORM_URL` | `http://127.0.0.1:18050` | 平台地址 |
|
||
| `MCP_PLATFORM_USER` / `MCP_PLATFORM_PASS` | `admin` / 空(`start.sh` 兜底 `admin123`) | 平台登录凭据。**改过 admin 密码必须同步**,否则登录失败 |
|
||
| `MCP_ALLOW_WRITE` | `0`(`start.sh` 内强制 `1`) | **写门控**:=1 才允许点击/输入/开关 App(只读工具不受限) |
|
||
| `MCP_ALLOWED_SERIALS` | 空 | 设备白名单(逗号分隔)。非空 = 只允许列出的 serial;**为空时只校验 serial 非空,不按平台设备池过滤**(语义缺口见 [backlog](backlog/TODO.md)) |
|
||
| `MCP_HTTP_HOST` / `MCP_HTTP_PORT` | `0.0.0.0` / `8033` | 监听 |
|
||
| `MCP_SCREENSHOT_WIDTH` | `540` | 截图返回宽度上限(模型看到的坐标系) |
|
||
| `MCP_JPEG_QUALITY` | `70` | 截图 JPEG 质量 |
|
||
| `MCP_AUDIT_FILE` | `/var/log/mcp/audit.log`(容器兜底 `/tmp/mcp_audit.log`) | 审计日志路径 |
|
||
| `MCP_PLATFORM_TIMEOUT` | `30` | 平台请求超时(秒) |
|
||
| `MCP_ENABLED` | `1` | **只被 `scripts/start.sh` 消费**:=0 则不后台拉起 MCP |
|
||
|
||
> `mcp_server/config.py` **只读进程环境变量,不读 `.env`**;`scripts/supervise.sh` 不拉起 MCP(容器场景由 `start.sh` 负责)。
|
||
|
||
---
|
||
|
||
## 3. 坐标空间(重要)
|
||
|
||
- `de_screenshot` 返回 **≤540px 宽**的 JPEG(display 空间),同时给出 `native_size`(设备原生分辨率)与 `screen_state`
|
||
- `de_tap` / `de_swipe` 的坐标一律用 **`de_screenshot` 返回图像的坐标系**,Server 按比例换算成原生坐标
|
||
- **必须先截图再点击**:Server 需要最近一次截图来建立坐标空间,否则报 `invalid_param`「请先对该设备执行 de_screenshot」
|
||
- `de_tap` 带**自动吸附**:落点若在某个可点击元素内,实际点该元素中心 → 坐标**只需大致对准**;返回 `snapped` / `label` 便于核对
|
||
|
||
---
|
||
|
||
## 4. 工具清单(20 个)
|
||
|
||
统一返回约定:
|
||
|
||
```json
|
||
{"ok": true, "data": { … }}
|
||
{"ok": false, "error": {"code": "device_busy", "message": "设备正在执行任务…"}}
|
||
```
|
||
|
||
错误码:`invalid_param` · `device_not_allowed` · `write_disabled` · `device_busy` · `platform_unavailable` · `device_offline` · `text_not_found`。
|
||
|
||
### 4.1 设备与状态(只读)
|
||
|
||
| 工具 | 参数 | 返回 / 说明 |
|
||
|------|------|------------|
|
||
| `de_list_devices` | — | `[{serial, name, model, online, task_job, worker_status, foreground_app}]`。**开局第一步**;`name` 是设备在平台里的名称(身份),**向用户汇报时用名称**(同型号多台靠它区分),调工具仍用 `serial` |
|
||
| `de_foreground_app` | `serial` | `{foreground_app}` 当前前台包名(dumpsys,MIUI 焦点为空时兜底) |
|
||
| `de_list_tasks` | — | 平台任务计划 `[{id,name,task_type,enabled,schedule}]`(只读) |
|
||
|
||
### 4.2 观察屏幕(只读)
|
||
|
||
| 工具 | 参数 | 返回 / 说明 |
|
||
|------|------|------------|
|
||
| `de_screenshot` | `serial` | `{image:{type:"image",data:<base64>,mimeType:"image/jpeg"}, width, height, native_size, screen_state}`。多模态模型直接看图 |
|
||
| `de_ui_tree` | `serial`, `limit`(150,1-300) | `{count, elements:[{text,id,desc,class,clickable,bounds}]}`,**可点击元素排前**;`limit` 控 token |
|
||
| `de_snapshot` | `serial`, `limit`(120,1-300) | **截图 + 元素树一次取齐**(推荐用它代替 `de_screenshot`+`de_ui_tree`):平台侧同一个 u2 连接背靠背取 + 双截图校验。返回 `{image, width, height, native_size, screen_state, unstable, cost_ms, count, elements}`;`unstable=true` 表示抓取期间界面在变化(此时元素坐标不可信,让设备静下来再取)。**同样会建立 `de_tap/de_swipe` 的坐标空间** |
|
||
| `de_ocr` | `serial` | `{count, texts:[{text,score}]}`(≤100 条)。UI 树拿不到的图片/WebView 文字用它 |
|
||
| `de_read_clipboard` | `serial` | `{clipboard}` |
|
||
| `de_list_apps` | `serial`, `keyword`("") | `{count, packages}` 第三方已装包名(`pm list packages -3`,≤200) |
|
||
|
||
### 4.3 点击与滑动(**写操作**)
|
||
|
||
| 工具 | 参数 | 说明 |
|
||
|------|------|------|
|
||
| `de_tap_text` | `serial`, `text`(≤100 字符) | **推荐**:按屏幕可见文字点击。原生控件走 UI 树,WebView/图片文字自动 **OCR 兜底**;找不到 → `text_not_found` |
|
||
| `de_tap_element` | `serial`, `by`(`text`/`id`/`desc`/`text_contains`/`desc_contains`), `value`, `index`(1) | 按元素属性点击,无需坐标;多命中用 `index` |
|
||
| `de_tap` | `serial`, `x`, `y` | 坐标点击(截图坐标系 + 自动吸附)。**纯图形目标才用** |
|
||
| `de_swipe` | `serial`, `x1,y1,x2,y2`, `duration`(0.2) | 滑动(长按 = 同点起止 + `duration≥1`) |
|
||
| `de_press_key` | `serial`, `key` | `back` / `home` / `recent` / `menu` / `power` / `volume_up` / `volume_down` / `enter` / `delete` / `search` / `camera` |
|
||
|
||
### 4.4 输入与剪贴板(**写操作**)
|
||
|
||
| 工具 | 参数 | 说明 |
|
||
|------|------|------|
|
||
| `de_type_text` | `serial`, `text` | 向当前输入框输入(支持中文,直设 EditText,不依赖剪贴板) |
|
||
| `de_set_clipboard` | `serial`, `text` | 写入设备剪贴板(设备端 Agent 通道 + 读回验证) |
|
||
|
||
### 4.5 App 管理(**写操作**,走 adb 直连不建 u2 会话)
|
||
|
||
| 工具 | 参数 | 说明 |
|
||
|------|------|------|
|
||
| `de_open_app` | `serial`, `package` | `adb monkey` 直启(最快路径,无需知道 activity) |
|
||
| `de_stop_app` | `serial`, `package` | `am force-stop` |
|
||
|
||
### 4.6 亮屏 / 熄屏(**写操作**)
|
||
|
||
| 工具 | 参数 | 说明 |
|
||
|------|------|------|
|
||
| `de_wake` | `serial` | 亮屏并解锁(熄屏时先调它再截图) |
|
||
| `de_sleep` | `serial` | 熄屏(**会中断正在运行的任务,慎用**) |
|
||
|
||
> 后两组分别经 `direct_ops`(adb)与平台 `POST /api/device/screen_all` 实现,不在 MCP 进程内建 u2 会话。
|
||
|
||
### 4.7 写操作门控(三连)
|
||
|
||
所有写工具在执行前依次检查:
|
||
|
||
1. `_check_write()` —— `MCP_ALLOW_WRITE=1`?否则 `write_disabled`
|
||
2. `_check_serial()` —— serial 非空 / 在白名单内?否则 `device_not_allowed`
|
||
3. `_ensure_device_free()` —— 设备 `worker_status` 不是 `running`/`connecting`?否则 `device_busy`(**AI 不与任务抢设备**)
|
||
|
||
> 设备状态查询有 **5 秒缓存**;查询本身异常时**放行**(不阻塞),由平台侧兜底。
|
||
|
||
---
|
||
|
||
## 5. 推荐使用模式
|
||
|
||
1. **`de_list_devices`** 确认目标设备在线
|
||
2. **`de_screenshot`** 看图理解当前界面(图像会在下一回合送达模型)
|
||
3. **点击定位优先级**:
|
||
- 目标有可见文字 → **`de_tap_text`**(一次调用完成"找到并点击",最可靠)
|
||
- 文字有歧义/多候选 → `de_ui_tree` 确认后用 `de_tap_element`(可用 `text_contains` 模糊匹配)
|
||
- 纯图形目标 → `de_tap` 坐标(**无需精算**,会自动吸附)
|
||
4. **输入文字**:先点中输入框,再 `de_type_text`
|
||
5. **每步验证**:关键操作后再 `de_screenshot`——界面变了 = 成功;没变 = 未命中 → 换 `de_tap_text`/`de_tap_element` 重新定位,**不要重复点同一坐标**
|
||
6. **收尾**:用中文总结做了什么、当前状态、注意事项
|
||
7. **效率**:界面未变时不重复截图/点击;连续 6 步无进展就停止并总结
|
||
|
||
---
|
||
|
||
## 6. 安全与审计
|
||
|
||
| 机制 | 说明 |
|
||
|------|------|
|
||
| **写门控** | `MCP_ALLOW_WRITE=0`(默认)时所有写操作被拒,只读可用 |
|
||
| **任务互斥** | 写操作前检查设备是否在跑任务,忙则 `device_busy` |
|
||
| **平台会话** | `platform_client` 内部登录(`MCP_PLATFORM_USER/PASS`)、会话失效自动重登、POST 自动带 `X-CSRF-Token`;**平台凭据不暴露给客户端** |
|
||
| **白名单** | `MCP_ALLOWED_SERIALS` 非空时限制可操作 serial |
|
||
| **审计** | 每次调用(**含只读**)写一行 JSON 到 `MCP_AUDIT_FILE`:`{ts, tool, serial, args 摘要, result 摘要}` |
|
||
| **红线** | 任何工具都不执行 `adb kill-server` / `adb disconnect` |
|
||
| **端点鉴权** | ⚠️ MCP HTTP 端点自身**没有** token/账号校验,只做出站登录 → **必须靠网络隔离**(同机/内网),不要直接暴露公网 |
|
||
|
||
---
|
||
|
||
## 7. 客户端接入示例
|
||
|
||
```python
|
||
import asyncio
|
||
from fastmcp import Client
|
||
|
||
async def main():
|
||
async with Client("http://192.168.20.220:8033/mcp", timeout=30) as c:
|
||
devs = await c.call_tool("de_list_devices", {})
|
||
serial = devs.data["data"][0]["serial"] # 第一台设备
|
||
shot = await c.call_tool("de_screenshot", {"serial": serial})
|
||
img = shot.data["data"]["image"] # base64 JPEG(给多模态模型看)
|
||
await c.call_tool("de_tap_text", {"serial": serial, "text": "搜索"})
|
||
|
||
asyncio.run(main())
|
||
```
|
||
|
||
> 给外部数字员工的使用约定与红线,另见 [staffdeck/KNOWLEDGE_BASE.md](staffdeck/KNOWLEDGE_BASE.md)。
|
||
|
||
---
|
||
|
||
## 8. AI 控制台(内置 Agent)
|
||
|
||
Web 的「AI 控制台」Tab 内建 Agent(`mcp_agent/`,OpenAI 兼容模型)用的就是本 Server 的同一批工具:
|
||
|
||
- 流式输出 + 每步 MCP 调用实时展示(含截图缩略)
|
||
- 多轮会话(同会话保留上下文)
|
||
- **自进化记忆**:经验库(任务配方)+ 动作库(命名动作),相似任务自动注入参考
|
||
- 模型与 Key 在控制台右上角 ⚙ 配置(存平台 `app_meta`)
|
||
|
||
> **依赖提示**:AI 控制台依赖本 MCP Server(默认 `http://127.0.0.1:8033/mcp`)。未启动时平台会把 SDK 的含糊报错映射成明确文案「**MCP server(8033) 不可达 …**」。
|
||
|
||
---
|
||
|
||
> **维护约定**:新增 / 修改 / 删除任何一个 MCP 工具或相关配置,必须**同步更新本文与 [MCP_DESIGN.md](MCP_DESIGN.md)**(doc 同步红线)。
|