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:
@@ -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 ""
|
||||
Reference in New Issue
Block a user