Files
butubb 7ce4ae9016 feat(账号): 账号台账(web 页 + 任务取号 + 设备端身份页);fix(剪贴板): 注入通道改走设备端 Agent
一、账号台账(新表 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 会读到上一次的内容 → 误报"写入可能被拒"(实测内容已写入却回失败)
2026-09-24 16:12:58 +08:00

276 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MCP 手机控制(Mobile Control MCP Server)设计文档
> 状态:**设计稿 / 演进记录**——凡与实现不符处,以 [MCP.md](MCP.md)(使用手册,含 20 个工具的权威清单)与 `mcp_server/` 代码为准。
> 本文保留设计取舍与里程碑,便于回溯"为什么这么做";文中的"演进备选"均**未落地**。
> 最后核对:2026-09-10(对照 dev 现状)。
## 1. 背景与目标
平台(auto_control,生产 220:18050)已具备完整的手机远程控制与自动化能力:设备池管理、u2 控制(tap/swipe/key/输入/剪贴板)、minicap 看屏(截图/流)、uiautodev 元素树、RapidOCR、任务调度与动作编辑器。
本设计的目标:**新增一个 MCP Server,把平台能力以 MCP 协议暴露给多模态 AI(Claude 等),使 AI 能像人一样"看到手机屏幕 → 理解 → 操作 → 再看到"地闭环控制手机**。
核心判断:平台能力全部已有,MCP Server 是**薄封装层**——主要工作在:工具规格设计、多模态图像链路、认证与安全、部署。
## 2. 术语
| 术语 | 说明 |
|---|---|
| MCP | Model Context Protocol,模型上下文协议(客户端-服务器,工具调用) |
| Tools | MCP 暴露的可调用能力(本设计按 L1/L2/L3 分层) |
| serial | 设备标识,如 `192.168.20.66:5555`(设备池唯一主键) |
| 平台 | 指 auto_control web 服务(18050)及其 core 层能力 |
| 宿主 | MCP Server 运行进程/容器 |
## 3. 总体架构
```
┌──────────────┐ MCP (Streamable HTTP / stdio) ┌──────────────────┐
│ 多模态 AI │ ────────────────────────────────▶ │ MCP Server │
│ Claude Desktop│ tools + image content block │ (与平台同容器) │
│ Claude Code │ ◀──────────────────────────────── │ mcp_server/ 包 │
└──────────────┘ 截图图像 / JSON 结果 └────────┬─────────┘
│ 内部调用
▼
┌──────────────────┐
│ auto_control │
│ 18050 REST API │
│ (登录会话 token)│
├──────────────────┤
│ core 层能力: │
│ u2 / minicap / │
│ uiautodev / OCR /│
│ 任务调度 / 设备池 │
└──────────────────┘
```
- **图像方向**:MCP 协议支持 `image` content block(base64),截图以此返回,多模态模型原生可读——这是"AI 看到手机"的关键链路。
- **控制方向**:AI 返回结构化工具调用(tap/swipe/type…),MCP Server 转发平台执行。
- **循环**:截图 → 模型视觉推理 → 工具调用 → 平台执行 → 截图验证。
## 4. 平台现有能力复用清单
| 平台能力 | 现有入口 | MCP 复用方式 |
|---|---|---|
| 设备列表/状态 | `GET /api/status`、`/api/devices/pool` | MCP server 内部 HTTP 调用(带会话) |
| 截图 | `GET /api/screen/thumb?serial=`(单帧 JPEG) | 直接转发为 image block(亦可直连 core u2 screenshot) |
| 看屏流 | `/api/screen/stream`(MJPEG) | M0-M2 不需要;M3 可选(视频理解场景) |
| 点击 | `POST /api/screen/tap {serial,x,y}` | 封装 tool `tap` |
| 滑动 | `POST /api/screen/swipe` | 封装 `swipe` |
| 按键 | `POST /api/screen/key` | 封装 `press_key` |
| 输入文字 | 设备端 Agent 通道(core.clipboard_helper)+ u2 input | 封装 `type_text` |
| 剪贴板 | `POST /api/tools/clipboard/set`(设备端 Agent 通道) | 封装 `set_clipboard` |
| 元素树 | `GET /api/uiauto/...`(uiautodev) | 封装 `get_ui_tree` |
| OCR | core.ocr(RapidOCR) | 封装 `ocr_screen` |
| 打开 App | u2 `app_start`(经任务层/直连) | 封装 `open_app`(走 core device 直连) |
| 设备在线状态/亮熄屏 | `/api/device/screen_all`、dumpsys | 封装 `screen_state`/`wake` |
> 决策:MCP Server 优先走 **HTTP API**(薄封装,认证简单、与平台解耦、平台安全逻辑全复用)。对延迟敏感且 HTTP 无入口的能力(u2 直连截图/输入),经平台 core 模块进程内调用或新增少量只读端点,不绕过平台安全层。
## 5. MCP Server 设计
### 5.1 技术栈
- 语言/运行时:Python 3.11(与平台一致)
- 框架:**FastMCP**(`fastmcp`,官方 SDK 之上,装饰器式 tools,自带 Streamable HTTP/stdio 双传输)
- 依赖:`fastmcp`(requirements.txt 已含,>=2.0)、`httpx`、`Pillow`(图像处理)、`mcp[cli]`(**设计依赖,未落地**——当前 requirements 未含,仅 fastmcp)
- 日志:logging → 平台同款格式(时间/级别/模块)
### 5.2 进程与容器
> **实现现状**:当前 MCP 与 web_server **同容器**,由 `scripts/start.sh` 后台拉起(`MCP_ENABLED=1` 默认,=0 可关),监听 8033。下方独立容器方案为**演进备选**。
- 独立容器 `mcp-server`(220 docker-compose 追加),image `python:3.11-slim`(演进备选)
- 挂载:无数据挂载(无状态,配置走环境变量);网络 host 或独立端口(**候选:8033**,避免与 18050/18051 冲突)
- 独立于 auto_control 重启,互不阻塞;MCP Server 崩溃不影响平台,平台不可用时 MCP tools 返回明确错误
### 5.3 配置(环境变量)
| 变量 | 默认 | 说明 |
|---|---|---|
| `MCP_PLATFORM_URL` | `http://127.0.0.1:18050` | 平台地址 |
| `MCP_PLATFORM_USER` / `MCP_PLATFORM_PASS` | 空 | 平台登录账号(admin) |
| `MCP_ALLOW_WRITE` | `0` | 写操作总开关(0=只读感知,1=可操作) |
| `MCP_ALLOWED_SERIALS` | 空=全部 | 设备白名单(逗号分隔;为空时自动=设备池内设备)——**设计目标,代码未实现**(实现仅校验 serial 非空,见 §8.3 注) |
| `MCP_HTTP_HOST` | `0.0.0.0` | HTTP 监听地址 |
| `MCP_HTTP_PORT` | `8033` | HTTP 传输端口 |
| `MCP_PLATFORM_TIMEOUT` | `30` | 平台请求超时(秒) |
| `MCP_SCREENSHOT_WIDTH` | `540` | 截图宽度(等比缩放,控图像 token 成本) |
| `MCP_JPEG_QUALITY` | `70` | 截图 JPEG 质量 |
| `MCP_AUDIT_FILE` | `/var/log/mcp/audit.log` | 审计日志路径 |
> `MCP_ENABLED` 由 `scripts/start.sh` 消费(默认 1),非 MCP 进程内配置;`MCP_PLATFORM_PASS` 在 start.sh 兜底 `admin123`,改 admin 密码须同步(见 doc/MCP.md)。
## 6. Tools 规格
命名空间 `de_`(device)前缀避免与常见工具冲突。全部工具对**未配置白名单/离线设备**返回明确错误,不做静默跳过。
> **实现现状对照**(本节以下是设计规格;实际实现以 `mcp_server/` 与 [doc/MCP.md](MCP.md) 为准,主要差异):
> - `de_screen_state` **未独立实现**:并入 `de_screenshot` 返回的 `screen_state` 字段
> - 实际另有(设计稿未列):`de_tap_text` / `de_tap_element` / `de_stop_app` / `de_read_clipboard` / `de_foreground_app` / `de_list_apps` / `de_list_tasks`
> - `de_type_text` 实际签名 `(serial, text)`,走 u2 `EditText.set_text`(非设计稿的 ClipInject+paste 优先)
> - `de_tap` 坐标语义已是**截图坐标系、server 按比例换算原生**(非设计稿「原生像素、调用方自换算」)
> - `de_open_app` 是 **adb monkey 直启**(非 u2 `app_start`)
> - `de_find_and_tap` / `de_wait_until` / `de_get_task_state` **未实现**;语义点击由 `de_tap_text` / `de_tap_element` 承担
> - **平台级工具层未实现**:任务 CRUD/提交、分组/设备池/自定义动作/APK/备份等 REST 只给前端用,未 MCP 化;补齐规划见 [doc/AI_TASK_GEN.md](AI_TASK_GEN.md) §9——P0 只补 `list_task_types` / `list_groups` / `list_pool` + `submit_task`(校验不落库),**不暴露写 CRUD**,维持「AI 提案 → 人工确认」
### L1 感知层
#### `de_list_devices`
- 描述:列出可控制设备及其状态(在线/任务/型号/前台 App)
- 返回:`[{serial, model, online, task_job, worker_status, foreground_app, screen_state}]`
- 错误:平台不可达 → `platform_unavailable`
#### `de_screenshot(serial: str) -> image`
- 描述:截取设备当前屏幕(JPEG),返回 image content block;同时返回 `width/height/serial` 元信息
- 实现:平台 `/api/screen/thumb`(X-Screen-State 头复用判断亮熄屏);失败(离线/超时)→ 明确错误
- 图像规格:宽 ≤ `MCP_SCREENSHOT_WIDTH`(默认 540),质量 `MCP_JPEG_QUALITY`;**需保证图像方向正确**(设备可能横竖屏,含 EXIF 或由调用方按截图尺寸推断)
- 限制:截图频率 ≥ 1s/次(防 AI 疯狂截图),可配置(**未落地**:现状无频率限制)
#### `de_ui_tree(serial: str) -> str`
- 描述:获取当前界面元素树(扁平 JSON:resource-id/text/content-desc/class/bounds),供定位
- 返回:文本 JSON(大树截断至 `MCP_UI_TREE_MAX` 字符,默认 20000,防 context 爆炸)
- 空界面/无元素 → 明确错误
#### `de_ocr(serial: str) -> str`
- 描述:OCR 识别当前屏幕文字,返回 `[{text, score, box}]`
- 场景:图片/画布/WebView 渲染文字(UI 树里没有的)
#### `de_screen_state(serial: str) -> str`
- 描述:亮屏/熄屏/未知
### L2 操作层(受 `MCP_ALLOW_WRITE=1` 门控)
#### `de_tap(serial, x: int, y: int)`
- 坐标:设备**原生分辨率**像素(与截图 1:1 换算——调用方用截图尺寸按比例换算,server 不做猜测)
#### `de_swipe(serial, x1,y1,x2,y2, duration: float=0.2)`
#### `de_press_key(serial, key: str)`
- key ∈ back/home/recent/menu/power/enter/delete…
#### `de_type_text(serial, text: str, clear_first: bool=True)`
- 实现:优先设备端 Agent 通道(`core/clipboard_helper.inject_clipboard` 语义:写入剪贴板 + paste 到焦点输入框),失败回退 u2 input
- 中文/emoji 全支持(Agent 通道实测通过)
#### `de_set_clipboard(serial, text: str)`
- 写入设备剪贴板(设备端 Agent 的 am start 通道),读回验证
#### `de_open_app(serial, package: str)`
- 打开指定 App(u2 app_start);`package` 需在已知清单或完全限定名
#### `de_wake(serial)` / `de_sleep(serial)`
### L3 语义层(M2 里程碑)
#### `de_find_and_tap(serial, target: str, by: "text"|"id"|"ocr"|"desc"=...)`
- UI 树/OCR 定位含 target 的元素 → 自动点击其中心;多命中返回候选列表让 AI 选择
- 复用平台元素选择器策略(属性唯一/重复序号消歧)
#### `de_wait_until(serial, condition: str, timeout: int=15)`
- 轮询 OCR/UI 树直到出现条件文本(或消失),返回命中的坐标/文本
#### `de_get_task_state(serial)`(若二期接任务系统)
- 设备当前任务状态;可选 `de_run_task(task_id)` 需单独授权
### 工具返回约定
- 全部工具返回结构化 JSON:`{ok: true, data: ...}` 或 `{ok: false, error: {code, message}}`
- 错误码:`platform_unavailable` / `device_offline` / `device_not_allowed` / `invalid_param` / `write_disabled` / `device_busy`(设备被任务占用,running/connecting 拒写) / `text_not_found`(de_tap_text 屏幕上无该文字)
- image 返回:`{ok:true, image: <content block>, width, height}`
## 7. 图像链路细节
1. 平台截图(`/api/screen/thumb`)→ JPEG 字节
2. MCP Server 校验尺寸 → 等比缩放到 `MCP_SCREENSHOT_WIDTH`(用 Pillow,不重压缩已够质量则跳过)
3. 组装 MCP `image` content block(`{"type":"image","data":<base64>,"mimeType":"image/jpeg"}`)
4. 客户端(Claude)原生把 image 传入视觉上下文
性能预算:540px JPEG ~40-80KB/张;模型一次多步操作 5-15 张 ≈ 1MB 以内,可接受。
## 8. 认证与安全
1. **平台会话**:MCP Server 启动时用 `MCP_PLATFORM_USER/PASS` 登录平台拿会话(Cookie),会话失效自动重登;不向客户端暴露平台凭据
2. **写操作门控**:`MCP_ALLOW_WRITE=0`(默认)时 L2/L3 操作全部拒绝(`write_disabled`)——先部署只读,验证感知链路后再开写
3. **设备白名单**:`MCP_ALLOWED_SERIALS` 指定;为空时自动限制为**设备池 enabled 设备**(平台口径)——**此为设计目标,当前代码未实现**:实现只校验 serial 非空,空名单不按设备池过滤(如需收紧请配置白名单)
4. **占用互斥**:写操作前查设备 `worker_status`——running/connecting 中拒绝(`device_busy`),避免 MCP 与任务打架(已实现,只读工具不受限)
5. **审计**:每次调用(含只读)写审计日志:`ts|tool|serial|args摘要|result`;落盘 `MCP_AUDIT_FILE`
6. **传输安全**:内网部署(host 网络 8033)默认无 TLS;若外网暴露需前置 TLS/网络隔离(平台红线:设备控制接口不进公网)
7. **无状态**:MCP Server 不存设备数据,全量透传平台
## 9. 部署(220 docker-compose 追加)
> **演进备选**:下方「独立 mcp-server 容器」为演进方案。**当前实现**=与 web_server 同容器,由 `scripts/start.sh` 后台拉起(`MCP_ENABLED=1` 默认、`MCP_ALLOW_WRITE=1`、`MCP_PLATFORM_PASS` 兜底 admin123),见 [doc/MCP.md](MCP.md) 与 DEPLOY §2.4。
```yaml
mcp-server:
container_name: mcp-server
image: dockerproxy.net/library/python:3.11-slim
restart: unless-stopped
working_dir: /app
volumes:
- "./mcp_server:/app"
command: sh -c "pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt && python -m mcp_server.mcp_server"
environment:
- MCP_PLATFORM_URL=http://127.0.0.1:18050
- MCP_PLATFORM_USER=admin
- MCP_PLATFORM_PASS=${MCP_PLATFORM_PASS} # .env 提供
- MCP_ALLOW_WRITE=1
- MCP_ALLOWED_SERIALS=
network_mode: host
```
客户端接入(Claude Code / Desktop):
```json
// .mcp.json / claude_desktop_config.json
{ "mcpServers": { "mobile": {
"url": "http://192.168.20.220:8033/mcp",
"transport": "streamable-http"
}}}
```
## 10. 里程碑与验收
| 里程碑 | 内容 | 验收标准 |
|---|---|---|
| M0 | Server 骨架 + `de_list_devices` + `de_screenshot` + `de_tap`/`de_swipe`(HTTP 传输、登录会话、审计骨架) | Claude Code 连上后:能列设备、截图(AI 能描述屏幕内容)、点击指定坐标生效 |
| M1 | L1 完备(ui_tree/ocr/screen_state)+ L2 全量(key/type/clipboard/open_app/wake) | AI 完成「打开抖音搜索『奚学东』」多步操作 |
| M2 | L3 语义层(find_and_tap/wait_until)+ 图像尺寸/性能调优 | AI 用自然语言指令(含中文输入)完成跨 3 步以上操作且自适应页面变化 |
| M3 | 生产化:安全加固复核、审计看板、部署脚本、接入文档、任务系统互斥回归 | 日常稳定使用 1 周无异常 |
> **实现状态备注**:M0/M1 已实现;M2 部分实现(`de_tap_text` / `de_tap_element` 已实现,`de_find_and_tap` / `de_wait_until` 未实现);M3 未验收。
## 11. 风险与开放问题
1. **AI 误操作**:模型点击错位/误触关键按钮(如删除)——缓解:L3 定位优先于裸坐标、写门控先行、审计可追溯;**不做**二次确认(破坏自动化体验),靠白名单+占用互斥兜底
2. **截图成本**:长会话多截图 → token/延迟成本——缓解:尺寸/质量可调、截图上限、`de_ui_tree` 作为低成本替代(文本树比图像便宜)
3. **横竖屏/分辨率异构**:设备多样 → 坐标换算依赖截图 1:1;`de_find_and_tap`(语义层)不受分辨率影响
4. **图像 content block 兼容性**:Claude 系原生支持;其他多模态客户端需验证(M0 用 Claude Code 验证)
5. **是否接任务系统**:二期决策——`de_run_task` 语义与"单设备操作"不同(任务面向多设备调度),倾向二期用独立命名空间
6. **clipboard/input 通道差异**:个别 MIUI 需授权写入剪贴板的透明 Activity——M0 阶段在受控设备集验证,必要时 fallback 链已在 core 层(设备端 Agent → 旧版 ClipInject)
## 12. 附录:平台 API 映射(MCP → 平台)
| MCP tool | 实际平台调用(现状) |
|---|---|
| de_list_devices | GET /api/status |
| de_screenshot | GET /api/screen/thumb?serial=(X-Screen-State 头 → screen_state;另 GET /api/screen/size 取原生分辨率) |
| de_ui_tree | GET /api/uiauto/elements(uiautodev) |
| de_ocr | direct_ops.ocr:u2 截图 + core.ocr RapidOCR |
| de_tap | POST /api/screen/tap(snap=1 自动吸附) |
| de_swipe | POST /api/screen/swipe |
| de_press_key | POST /api/screen/key |
| de_tap_text | POST /api/screen/tap_text(UI 树子串匹配 → OCR 兜底) |
| de_tap_element | u2 元素直连(uiautomator2 `d(by=value).click()`,无平台端点) |
| de_type_text | u2 `EditText.set_text`(direct_ops.type_text,不走剪贴板通道) |
| de_set_clipboard | core.clipboard_helper 设备端 Agent 通道(inject_clipboard,读回验证) |
| de_read_clipboard | u2 `d.clipboard` |
| de_open_app | adb monkey 直启(direct_ops.open_app) |
| de_stop_app | adb `am force-stop`(direct_ops.stop_app) |
| de_foreground_app | adb `dumpsys window`(direct_ops.foreground_app,mCurrentFocus/mFocusedApp 兜底) |
| de_list_apps | adb `pm list packages -3`(direct_ops.list_apps) |
| de_wake / de_sleep | POST /api/device/screen_all(mode=on/off) |
| de_list_tasks | GET /api/jobs |
> `de_screen_state` 无独立工具,已并入 `de_screenshot` 的 `screen_state` 字段;设计稿中 `de_find_and_tap`/`de_wait_until`/`de_get_task_state` 未实现,不在此表。现状中 MCP 直连面(u2/adb)不经平台 REST,但安全(白名单/写门控/busy/审计)仍在 MCP 工具层统一把关,绝不越权触碰平台红线。