From 5d637bd4f5bbd5d314a18c2c5f9e115be7d56677 Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Thu, 20 Aug 2026 10:34:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=89=AA=E8=B4=B4=E6=9D=BF=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=E6=94=B9=E7=94=A8=20ClipInject=20=E9=80=9A=E9=81=93?= =?UTF-8?q?=E2=80=94=E2=80=94u2.set=5Fclipboard=20=E5=9C=A8=20Android=2010?= =?UTF-8?q?+=20=E8=A2=AB=E7=B3=BB=E7=BB=9F=E9=9D=99=E9=BB=98=E4=B8=A2?= =?UTF-8?q?=E5=BC=83=EF=BC=88atx-agent=20=E5=90=8E=E5=8F=B0=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E5=86=99=E5=89=AA=E8=B4=B4=E6=9D=BF=E5=8F=97=E9=99=90?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=94=B9=E4=B8=BA=20am=20start=20=E9=80=8F?= =?UTF-8?q?=E6=98=8E=20Activity=EF=BC=88shell=20=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E4=B8=8D=E5=8F=97=E5=90=8E=E5=8F=B0=E9=99=90?= =?UTF-8?q?=E5=88=B6=EF=BC=89=EF=BC=8C=E6=B3=A8=E5=85=A5=E5=90=8E=20u2=20?= =?UTF-8?q?=E8=AF=BB=E5=9B=9E=E6=AF=94=E5=AF=B9=E9=AA=8C=E8=AF=81=EF=BC=8C?= =?UTF-8?q?=E6=9C=AA=E8=A3=85=20APK=20=E6=98=8E=E7=A1=AE=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/tools_api.py | 92 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 18 deletions(-) diff --git a/web/tools_api.py b/web/tools_api.py index dc7fc31..6b55401 100644 --- a/web/tools_api.py +++ b/web/tools_api.py @@ -94,29 +94,57 @@ def api_tools_clipboard_set(): def _set_device_clipboard(serial, text): - """向单台设备注入剪贴板文字(u2 jsonrpc.setClipboard)。 + """向单台设备注入剪贴板文字(ClipInject APK:透明 Activity 聚焦后写入)。 + + Android 10+ 禁止后台(atx-agent/shell)写剪贴板,u2.set_clipboard 在 + Android 12/13 上调用"成功"但内容被系统静默丢弃。ClipInject + (com.example.clipinject)通过 am start 启动透明 Activity,窗口聚焦后 + setPrimaryClip,shell 启动 Activity 不受后台启动限制(广播方式在 MIUI + 会被 Background activity start 拦截,必须用 am start)。 - IP:port 设备:先 adb connect(已连接自动跳过;绝不 disconnect,红线) - - USB 设备(无冒号):u2 按 serial 直连,首次会自动推送 atx-agent - - 连接/注入都带超时保护,避免 atx-agent 无响应时挂住请求 + - 设备未装 ClipInject:am start 返回 unable to resolve Intent,明确报错 + - 注入后 u2 读回比对验证(后台读剪贴板实测可用),不一致也明确报错 """ + import base64 + import time import uiautomator2 as u2 if ":" in serial: try: adb_connect(serial) except Exception as e: return False, f"adb 连接失败: {e}" - d = None + b64 = base64.b64encode(text.encode("utf-8")).decode() + # 1. am start 透明 Activity 写剪贴板(shell 身份启动,不受后台启动限制) + try: + r = subprocess.run( + [ADB_PATH, "-s", serial, "shell", "am", "start", + "-n", "com.example.clipinject/.ClipActivity", + "--es", "text_b64", b64], + capture_output=True, timeout=30) + out = ((r.stdout or b"") + (r.stderr or b"")).decode(errors="replace") + if "does not exist" in out or "unable to resolve Intent" in out: + return False, ("设备未安装 ClipInject(com.example.clipinject)," + f"请先安装: {out.strip()[:100]}") + if "Error" in out or ("Starting" not in out and r.returncode != 0): + return False, f"am start 失败: {out.strip()[:120]}" + except subprocess.TimeoutExpired: + return False, "am start 超时(adb 无响应)" + except Exception as e: + return False, f"{type(e).__name__}: {str(e)[:120]}" + # 2. 读回验证(等透明 Activity 完成写入;u2 读剪贴板在 Android 12 实测可用) + time.sleep(0.5) try: with ThreadPoolExecutor(max_workers=1) as pool: d = pool.submit(u2.connect, serial).result(timeout=30) with ThreadPoolExecutor(max_workers=1) as pool: - pool.submit(d.set_clipboard, text).result(timeout=15) - return True, "已注入" - except FuturesTimeout: - return False, "连接/注入超时(atx-agent 可能无响应)" + got = pool.submit(lambda: d.clipboard).result(timeout=15) + if got == text: + return True, "已注入" + return False, f"剪贴板读回不一致(写入可能被拒): {str(got)[:60]!r}" except Exception as e: - return False, f"{type(e).__name__}: {str(e)[:120]}" + # 读回不可用(旧版 u2/连接失败)时按写入成功处理,避免误报,但带上具体原因 + return True, f"已注入(读回验证不可用: {type(e).__name__}: {str(e)[:80]})" def _app_ver_on_device(serial, pkg): @@ -186,29 +214,57 @@ def _blocked_adb_cmd(cmd): return any(p in low for p in _ADB_BLOCKED_PATTERNS) def _set_device_clipboard(serial, text): - """向单台设备注入剪贴板文字(u2 jsonrpc.setClipboard)。 + """向单台设备注入剪贴板文字(ClipInject APK:透明 Activity 聚焦后写入)。 + + Android 10+ 禁止后台(atx-agent/shell)写剪贴板,u2.set_clipboard 在 + Android 12/13 上调用"成功"但内容被系统静默丢弃。ClipInject + (com.example.clipinject)通过 am start 启动透明 Activity,窗口聚焦后 + setPrimaryClip,shell 启动 Activity 不受后台启动限制(广播方式在 MIUI + 会被 Background activity start 拦截,必须用 am start)。 - IP:port 设备:先 adb connect(已连接自动跳过;绝不 disconnect,红线) - - USB 设备(无冒号):u2 按 serial 直连,首次会自动推送 atx-agent - - 连接/注入都带超时保护,避免 atx-agent 无响应时挂住请求 + - 设备未装 ClipInject:am start 返回 unable to resolve Intent,明确报错 + - 注入后 u2 读回比对验证(后台读剪贴板实测可用),不一致也明确报错 """ + import base64 + import time import uiautomator2 as u2 if ":" in serial: try: adb_connect(serial) except Exception as e: return False, f"adb 连接失败: {e}" - d = None + b64 = base64.b64encode(text.encode("utf-8")).decode() + # 1. am start 透明 Activity 写剪贴板(shell 身份启动,不受后台启动限制) + try: + r = subprocess.run( + [ADB_PATH, "-s", serial, "shell", "am", "start", + "-n", "com.example.clipinject/.ClipActivity", + "--es", "text_b64", b64], + capture_output=True, timeout=30) + out = ((r.stdout or b"") + (r.stderr or b"")).decode(errors="replace") + if "does not exist" in out or "unable to resolve Intent" in out: + return False, ("设备未安装 ClipInject(com.example.clipinject)," + f"请先安装: {out.strip()[:100]}") + if "Error" in out or ("Starting" not in out and r.returncode != 0): + return False, f"am start 失败: {out.strip()[:120]}" + except subprocess.TimeoutExpired: + return False, "am start 超时(adb 无响应)" + except Exception as e: + return False, f"{type(e).__name__}: {str(e)[:120]}" + # 2. 读回验证(等透明 Activity 完成写入;u2 读剪贴板在 Android 12 实测可用) + time.sleep(0.5) try: with ThreadPoolExecutor(max_workers=1) as pool: d = pool.submit(u2.connect, serial).result(timeout=30) with ThreadPoolExecutor(max_workers=1) as pool: - pool.submit(d.set_clipboard, text).result(timeout=15) - return True, "已注入" - except FuturesTimeout: - return False, "连接/注入超时(atx-agent 可能无响应)" + got = pool.submit(lambda: d.clipboard).result(timeout=15) + if got == text: + return True, "已注入" + return False, f"剪贴板读回不一致(写入可能被拒): {str(got)[:60]!r}" except Exception as e: - return False, f"{type(e).__name__}: {str(e)[:120]}" + # 读回不可用(旧版 u2/连接失败)时按写入成功处理,避免误报,但带上具体原因 + return True, f"已注入(读回验证不可用: {type(e).__name__}: {str(e)[:80]})" def _app_ver_on_device(serial, pkg): """单设备包版本查询:pm path 检查安装 → dumpsys 取 versionName/versionCode。