"""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;mCurrentFocus 为空时 fallback mFocusedApp)。""" out = _adb(serial, "dumpsys", "window", timeout=15) or "" m = re.search(r"mCurrentFocus=.*?([\w.]+)/", out) if m: return m.group(1) # 部分 MIUI 焦点在 IME/过渡时 mCurrentFocus=null,用 mFocusedApp 兜底 m2 = re.search(r"mFocusedApp=.*?([\w.]+)/", out) return m2.group(1) if m2 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 ""