一、账号台账(新表 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 会读到上一次的内容 → 误报"写入可能被拒"(实测内容已写入却回失败)
11 KiB
MCP 手机控制手册(MCP.md)
多模态 AI(DeepSeek / Claude 等)通过 MCP(Model Context Protocol) 操作 Android 手机的统一出口。平台自带的「AI 控制台」(mcp_agent/)与外部 MCP 客户端使用的都是这一批工具。
本文 = 使用手册(工具清单 / 用法 / 接入)。设计取舍与演进见 MCP_DESIGN.md;AI 控制台的会话/经验/动作机制见 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 §9)
2. 运行与配置
生产跑在 220 的 python-app 容器内(scripts/start.sh 自动拉起),端点 http://192.168.20.220:8033/mcp。
# 本机手动启动
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) |
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_statede_tap/de_swipe的坐标一律用de_screenshot返回图像的坐标系,Server 按比例换算成原生坐标- 必须先截图再点击:Server 需要最近一次截图来建立坐标空间,否则报
invalid_param「请先对该设备执行 de_screenshot」 de_tap带自动吸附:落点若在某个可点击元素内,实际点该元素中心 → 坐标只需大致对准;返回snapped/label便于核对
4. 工具清单(20 个)
统一返回约定:
{"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 写操作门控(三连)
所有写工具在执行前依次检查:
_check_write()——MCP_ALLOW_WRITE=1?否则write_disabled_check_serial()—— serial 非空 / 在白名单内?否则device_not_allowed_ensure_device_free()—— 设备worker_status不是running/connecting?否则device_busy(AI 不与任务抢设备)
设备状态查询有 5 秒缓存;查询本身异常时放行(不阻塞),由平台侧兜底。
5. 推荐使用模式
de_list_devices确认目标设备在线de_screenshot看图理解当前界面(图像会在下一回合送达模型)- 点击定位优先级:
- 目标有可见文字 →
de_tap_text(一次调用完成"找到并点击",最可靠) - 文字有歧义/多候选 →
de_ui_tree确认后用de_tap_element(可用text_contains模糊匹配) - 纯图形目标 →
de_tap坐标(无需精算,会自动吸附)
- 目标有可见文字 →
- 输入文字:先点中输入框,再
de_type_text - 每步验证:关键操作后再
de_screenshot——界面变了 = 成功;没变 = 未命中 → 换de_tap_text/de_tap_element重新定位,不要重复点同一坐标 - 收尾:用中文总结做了什么、当前状态、注意事项
- 效率:界面未变时不重复截图/点击;连续 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. 客户端接入示例
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。
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(doc 同步红线)。