feat: 新增工具Tab - 剪贴板注入(多选本地adb/STF设备批量注入文字,u2 setClipboard 实现,实测支持中文/引号/换行)

This commit is contained in:
2026-08-11 09:53:32 +08:00
parent cff0fc6a79
commit 2f1003bf1f
3 changed files with 162 additions and 4 deletions
+56 -1
View File
@@ -23,6 +23,7 @@ import shlex
import re
import subprocess
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
from flask import (Flask, jsonify, request, redirect, url_for, render_template,
Response, make_response, session)
@@ -35,7 +36,8 @@ from core.device_worker import clear_worker_error, clear_all_worker_errors
from core.logger import get_logger, _LOG_DIR, _MODULE_FILES
from core.models import db, init_db, User, DeviceGroup, TaskJob, CustomAction
from core.apk_manager import ApkManager
from core.adb_helper import screenshot, list_installed_apps, adb_connect_light, _ADB_LOCK, _adb
from core.adb_helper import (screenshot, list_installed_apps, adb_connect_light,
adb_connect, _ADB_LOCK, _adb)
from core import uiauto_helper, tailscale_client
from core.tailscale_client import TailscaleError
from config import ADB_PATH, STF_SSH_TARGET, STF_DOCKER_CONTAINER
@@ -937,6 +939,59 @@ def api_adb_cmd():
return jsonify({"ok": False, "error": f"执行失败: {e}"}), 500
@app.route("/api/tools/clipboard/set", methods=["POST"])
@admin_required
def api_tools_clipboard_set():
"""工具-剪贴板注入:把指定文字写入一台或多台设备的剪贴板。
请求: {"serials": ["100.100.10.11:5555", ...], "text": "要注入的文字"}
设备来源与维护终端一致:本地 adb 已连接设备(含 USB)+ STF 在线池。
实现:u2 jsonrpc.setClipboard(实测 cmd clipboard 在 MIUI 上不存在)。
"""
data = request.json or {}
serials = data.get("serials") or []
text = (data.get("text") or "").strip()
if not isinstance(serials, list) or not serials:
return jsonify({"ok": False, "error": "未选择设备"}), 400
if not text:
return jsonify({"ok": False, "error": "注入内容不能为空"}), 400
results = {}
for serial in serials:
ok, msg = _set_device_clipboard(serial, text)
results[serial] = {"ok": ok, "msg": msg}
ok_count = sum(1 for v in results.values() if v["ok"])
fail_count = len(results) - ok_count
return jsonify({"ok": True, "results": results,
"ok_count": ok_count, "fail_count": fail_count,
"error": None if ok_count == len(results) else f"{fail_count} 台设备注入失败"})
def _set_device_clipboard(serial, text):
"""向单台设备注入剪贴板文字(u2 jsonrpc.setClipboard)。
- IP:port 设备:先 adb connect(已连接自动跳过;绝不 disconnect,红线)
- USB 设备(无冒号):u2 按 serial 直连,首次会自动推送 atx-agent
- 连接/注入都带超时保护,避免 atx-agent 无响应时挂住请求
"""
import uiautomator2 as u2
if ":" in serial:
try:
adb_connect(serial)
except Exception as e:
return False, f"adb 连接失败: {e}"
d = None
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 可能无响应)"
except Exception as e:
return False, f"{type(e).__name__}: {str(e)[:120]}"
@app.route("/api/stf/restart", methods=["POST"])
@admin_required
def api_stf_restart():