feat: MCP 工具补全(9→18)——de_open_app(adb monkey 直启,省 token)/de_stop_app/de_foreground_app/de_type_text(中文直接输入)/de_set_clipboard/de_sleep/de_ocr(屏幕文字识别)/de_list_apps/de_list_tasks;direct_ops 直连封装(adb/u2/OCR/剪贴板轻量通道)

This commit is contained in:
2026-09-04 13:48:55 +08:00
parent 1560a13d86
commit fab7e2b11a
3 changed files with 298 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
"""MCP 直连操作封装:adb/u2/OCR/剪贴板——轻量通道优先。
为什么直连而不是全部走平台 HTTP:
- adb monkey 打开 App / am force-stop:一条 adb 命令,不建 u2 连接(省时省 token)
- 输入文字:u2 EditText.set_text 直接设文本(比剪贴板+粘贴少依赖)
- OCR:平台 RapidOCR(本地模型,截图即识别)
平台 REST 无法表达的操作(无独立端点)在此封装;安全(白名单/写门控)
仍在 MCP 工具层统一把关。绝不 kill-server / 绝不 disconnect(项目红线)。
"""
import re
import subprocess
from config import ADB_PATH
_log = None # mcp 层 logging 由调用方配置
def _adb(serial, *args, timeout=25):
"""对指定设备执行 adb shell 命令,返回输出文本(超时返回空)。"""
try:
r = subprocess.run([ADB_PATH, "-s", serial, "shell", *args],
capture_output=True, timeout=timeout)
return (r.stdout or b"").decode("utf-8", errors="replace")
except subprocess.TimeoutExpired:
return ""
except Exception:
return ""
def open_app(serial, package):
"""adb monkey 打开 App(无需知道 activity,最轻量)。"""
out = _adb(serial, "monkey", "-p", package,
"-c", "android.intent.category.LAUNCHER", "1")
return out or ""
def stop_app(serial, package):
"""强制停止 App(am force-stop)。"""
return _adb(serial, "am", "force-stop", package)
def foreground_app(serial):
"""当前前台 App 包名(dumpsys window 解析;失败返回空)。"""
out = _adb(serial, "dumpsys", "window", timeout=15)
m = re.search(r"mCurrentFocus=.*?([\w.]+)/", out or "")
return m.group(1) if m else ""
def list_apps(serial, keyword=""):
"""第三方已装应用包名列表(pm list packages -3,可关键词过滤)。"""
out = _adb(serial, "pm", "list", "packages", "-3")
pkgs = []
for line in (out or "").splitlines():
p = line.replace("package:", "").strip()
if p and (not keyword or keyword.lower() in p.lower()):
pkgs.append(p)
return pkgs
def type_text(serial, text):
"""向当前界面输入框输入文字(u2 定位 EditText set_text,支持中文)。
返回 (ok, msg)。
"""
import uiautomator2 as u2
d = u2.connect(serial)
# 优先聚焦输入框;找不到则第一个 EditText(与任务 input_text 兜底一致)
try:
el = d(focused=True)
if el.exists:
el.set_text(text)
return True, "已输入到聚焦输入框"
except Exception:
pass
try:
edit = d(className="android.widget.EditText")
if edit.exists:
edit.set_text(text)
return True, "已输入到输入框"
except Exception as e:
return False, f"输入失败: {type(e).__name__}: {str(e)[:100]}"
return False, "未找到输入框(请先点击输入框或提供界面信息)"
def set_clipboard(serial, text):
"""剪贴板注入(ClipInject 通道,读回验证)。"""
from core.clipboard_helper import inject_clipboard
return inject_clipboard(serial, text)
def ocr(serial):
"""截屏 + RapidOCR 识别,返回 [{text, score, box}]。"""
import uiautomator2 as u2
from core.ocr import recognize
d = u2.connect(serial)
img = d.screenshot()
if img is None:
return []
return recognize(img)
def read_clipboard(serial):
"""读设备剪贴板(u2)。"""
import uiautomator2 as u2
d = u2.connect(serial)
return d.clipboard or ""