fix: 任务动作编辑器剪贴板注入改用 ClipInject 通道——u2 set_clipboard 在 Android 10+ 静默失效(粘贴模式必抛 UiAutomationError);提取 core/clipboard_helper 公共模块供工具页与任务执行端共用,粘贴模式 atx-agent pasteClipboard + set_text 兜底
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
"""剪贴板注入公共模块(ClipInject APK 通道)。
|
||||||
|
|
||||||
|
背景:Android 10+ 禁止后台(atx-agent/shell)写剪贴板——u2.set_clipboard
|
||||||
|
调用"成功"但内容被系统静默丢弃(Android 12/13 实测)。ClipInject
|
||||||
|
(com.example.clipinject,用户自研 APK)通过 am start 启动透明 Activity,
|
||||||
|
窗口聚焦后 setPrimaryClip;shell 身份启动 Activity 不受后台启动限制
|
||||||
|
(广播方式在 MIUI 会被 Background activity start 拦截,必须用 am start)。
|
||||||
|
|
||||||
|
工具页(web/tools_api.py)与任务执行器(tasks/generic/task.py)共用本模块。
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from config import ADB_PATH
|
||||||
|
from core.adb_helper import adb_connect
|
||||||
|
from core.logger import get_logger
|
||||||
|
|
||||||
|
_log = get_logger("core.clip")
|
||||||
|
|
||||||
|
_CLIP_PACKAGE = "com.example.clipinject"
|
||||||
|
_CLIP_ACTIVITY = "com.example.clipinject/.ClipActivity"
|
||||||
|
|
||||||
|
|
||||||
|
def inject_clipboard(serial, text, d=None):
|
||||||
|
"""向设备剪贴板注入文字,返回 (ok, msg)。
|
||||||
|
|
||||||
|
- d:可选,任务执行器已持有 u2 Device 时传入复用(跳过重复连接),
|
||||||
|
读回验证直接用它;不传则临时 u2.connect(工具页场景,带超时保护)
|
||||||
|
- IP:port 设备先 adb connect(已连接自动跳过;绝不 disconnect,红线)
|
||||||
|
- 未装 ClipInject / 读回不一致都会明确报错(不再静默失败)
|
||||||
|
"""
|
||||||
|
import uiautomator2 as u2
|
||||||
|
if ":" in serial:
|
||||||
|
try:
|
||||||
|
adb_connect(serial)
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"adb 连接失败: {e}"
|
||||||
|
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", _CLIP_ACTIVITY, "--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, f"设备未安装 ClipInject({_CLIP_PACKAGE}),请先安装"
|
||||||
|
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:
|
||||||
|
if d is None:
|
||||||
|
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||||
|
d = pool.submit(u2.connect, serial).result(timeout=30)
|
||||||
|
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||||
|
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:
|
||||||
|
# 读回不可用(旧版 u2/连接失败)时按写入成功处理,避免误报,但带上具体原因
|
||||||
|
return True, f"已注入(读回验证不可用: {type(e).__name__}: {str(e)[:80]})"
|
||||||
+21
-10
@@ -478,26 +478,37 @@ class GenericStepsWorker(BaseWorker):
|
|||||||
return found # 命中结果,"测试此步骤"用
|
return found # 命中结果,"测试此步骤"用
|
||||||
|
|
||||||
def _exec_clipboard(self, d, params, depth=0):
|
def _exec_clipboard(self, d, params, depth=0):
|
||||||
"""剪贴板注入:写入指定内容到剪贴板,可选立即粘贴到当前聚焦的输入框。
|
"""剪贴板注入(ClipInject 通道,Android 10+ 可用),可选立即粘贴。
|
||||||
|
|
||||||
批量发链接场景:先聚焦评论区输入框 → 注入+粘贴链接1 → 换链接 → 注入+粘贴链接2……
|
批量发链接场景:先聚焦评论区输入框 → 注入+粘贴链接1 → 换链接 → 注入+粘贴链接2……
|
||||||
粘贴模式复用 u2 send_keys(内部 setClipboard + pasteClipboard,与"输入文字"同机制)。
|
粘贴模式:ClipInject 写入剪贴板后调 atx-agent pasteClipboard 粘贴到当前
|
||||||
|
焦点输入框;动作失败时兜底 u2 set_text 直接设置聚焦输入框文本(不依赖
|
||||||
|
剪贴板,与"输入文字"兜底同机制)。
|
||||||
"""
|
"""
|
||||||
text = str(params.get("text", "") or "")
|
text = str(params.get("text", "") or "")
|
||||||
if not text:
|
if not text:
|
||||||
_log.warning(f"[{self.serial}] clipboard 内容为空,跳过")
|
_log.warning(f"[{self.serial}] clipboard 内容为空,跳过")
|
||||||
return False
|
return False
|
||||||
|
from core.clipboard_helper import inject_clipboard
|
||||||
|
ok, msg = inject_clipboard(self.serial, text, d=d)
|
||||||
|
if not ok:
|
||||||
|
_log.warning(f"[{self.serial}] 剪贴板注入失败: {msg}")
|
||||||
|
return False
|
||||||
|
_log.info(f"[{self.serial}] 剪贴板注入: {text[:60]}({msg})")
|
||||||
|
if not params.get("paste"):
|
||||||
|
return True
|
||||||
try:
|
try:
|
||||||
if params.get("paste"):
|
d.jsonrpc.pasteClipboard()
|
||||||
d.send_keys(text) # setClipboard + pasteClipboard
|
_log.info(f"[{self.serial}] 已粘贴: {text[:60]}")
|
||||||
_log.info(f"[{self.serial}] 剪贴板注入并粘贴: {text[:60]}")
|
|
||||||
else:
|
|
||||||
d.set_clipboard(text)
|
|
||||||
_log.info(f"[{self.serial}] 剪贴板注入(不粘贴): {text[:60]}")
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_log.warning(f"[{self.serial}] 剪贴板注入异常: {e}")
|
try:
|
||||||
return False
|
d(focused=True).set_text(text)
|
||||||
|
_log.info(f"[{self.serial}] 粘贴兜底 set_text: {text[:60]}")
|
||||||
|
return True
|
||||||
|
except Exception as e2:
|
||||||
|
_log.warning(f"[{self.serial}] 粘贴失败: {e} / {e2}")
|
||||||
|
return True # 剪贴板已写入,粘贴失败不致命
|
||||||
|
|
||||||
def _exec_wait(self, d, params, depth=0):
|
def _exec_wait(self, d, params, depth=0):
|
||||||
min_s = float(params.get("min", 1.0))
|
min_s = float(params.get("min", 1.0))
|
||||||
|
|||||||
+3
-51
@@ -94,57 +94,9 @@ def api_tools_clipboard_set():
|
|||||||
|
|
||||||
|
|
||||||
def _set_device_clipboard(serial, text):
|
def _set_device_clipboard(serial, text):
|
||||||
"""向单台设备注入剪贴板文字(ClipInject APK:透明 Activity 聚焦后写入)。
|
"""向单台设备注入剪贴板文字(ClipInject 通道,见 core.clipboard_helper)。"""
|
||||||
|
from core.clipboard_helper import inject_clipboard
|
||||||
Android 10+ 禁止后台(atx-agent/shell)写剪贴板,u2.set_clipboard 在
|
return inject_clipboard(serial, text)
|
||||||
Android 12/13 上调用"成功"但内容被系统静默丢弃。ClipInject
|
|
||||||
(com.example.clipinject)通过 am start 启动透明 Activity,窗口聚焦后
|
|
||||||
setPrimaryClip,shell 启动 Activity 不受后台启动限制(广播方式在 MIUI
|
|
||||||
会被 Background activity start 拦截,必须用 am start)。
|
|
||||||
|
|
||||||
- IP:port 设备:先 adb connect(已连接自动跳过;绝不 disconnect,红线)
|
|
||||||
- 设备未装 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}"
|
|
||||||
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:
|
|
||||||
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:
|
|
||||||
# 读回不可用(旧版 u2/连接失败)时按写入成功处理,避免误报,但带上具体原因
|
|
||||||
return True, f"已注入(读回验证不可用: {type(e).__name__}: {str(e)[:80]})"
|
|
||||||
|
|
||||||
|
|
||||||
def _app_ver_on_device(serial, pkg):
|
def _app_ver_on_device(serial, pkg):
|
||||||
|
|||||||
Reference in New Issue
Block a user