feat: 强制安装APK(推送+pm install 绕过厂商弹窗)+ 定位设备(亮屏+大字定位页,结束定位关闭浏览器含后台)+ 一键亮屏/息屏(并发全部设备)
This commit is contained in:
+181
-1
@@ -22,11 +22,14 @@ import functools
|
||||
import shlex
|
||||
import re
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError as FuturesTimeout
|
||||
|
||||
from flask import (Flask, jsonify, request, redirect, url_for, render_template,
|
||||
Response, make_response, session)
|
||||
render_template_string, Response, make_response, session)
|
||||
from markupsafe import escape as _esc
|
||||
from flask_login import (LoginManager, login_user, logout_user,
|
||||
login_required, current_user)
|
||||
|
||||
@@ -547,6 +550,183 @@ def api_stop_device():
|
||||
return jsonify({"ok": False, "error": f"{serial} 没有运行中的任务"}), 400
|
||||
|
||||
|
||||
@app.route("/locate")
|
||||
def locate_page():
|
||||
"""设备端定位页(免登录):点亮后浏览器打开,大字显示设备 IP/序列号。
|
||||
|
||||
只展示 serial 文本,无任何平台数据,免登录可接受。
|
||||
"""
|
||||
serial = request.args.get("serial", "")
|
||||
return render_template_string(
|
||||
"""<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>设备定位</title><style>
|
||||
body{margin:0;background:#000;color:#fff;display:flex;flex-direction:column;
|
||||
align-items:center;justify-content:center;height:100vh;font-family:monospace}
|
||||
.serial{font-size:min(9vw,72px);font-weight:700;color:#ffd700;word-break:break-all;padding:0 20px;text-align:center}
|
||||
.ip{font-size:min(5vw,36px);color:#7dd3fc;margin-top:24px;word-break:break-all;padding:0 20px;text-align:center}
|
||||
.hint{font-size:min(3vw,16px);color:#64748b;margin-top:40px}
|
||||
</style></head><body>
|
||||
<div class="serial">{{ serial }}</div>
|
||||
<div class="ip">{{ ip }}</div>
|
||||
<div class="hint">定位完成 · 按返回键退出</div>
|
||||
</body></html>""",
|
||||
serial=_esc(serial), ip=_esc(serial.split(":")[0]))
|
||||
|
||||
|
||||
# 常见浏览器包名(结束定位时 force-stop 用)
|
||||
_BROWSER_PKGS = {"com.android.browser", "com.miui.browser", "com.android.chrome",
|
||||
"com.brave.browser", "com.opera.browser", "org.mozilla.firefox",
|
||||
"com.UCMobile", "com.tencent.mtt", "com.baidu.browser.apps"}
|
||||
# 定位时启动的浏览器:serial -> 包名(结束定位时无论前后台都关闭它)
|
||||
_locate_browsers = {}
|
||||
_locate_lock = threading.Lock()
|
||||
|
||||
|
||||
def _current_focus_pkg(serial):
|
||||
"""取设备当前前台包名(dumpsys window 解析,失败返回空串)。"""
|
||||
try:
|
||||
r = subprocess.run([ADB_PATH, "-s", serial, "shell", "dumpsys", "window"],
|
||||
capture_output=True, timeout=20)
|
||||
out = (r.stdout or b"").decode("utf-8", errors="replace")
|
||||
m = re.search(r"mCurrentFocus=.*?([\w.]+)/", out)
|
||||
return m.group(1) if m else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
@app.route("/api/device/locate/stop", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_device_locate_stop():
|
||||
"""结束定位:关闭定位启动的浏览器(无论当前是否前台)。
|
||||
|
||||
优先 force-stop 定位时记录下的浏览器包名(切到后台也能关掉);
|
||||
无记录时退回:前台是浏览器 → force-stop;否则按返回键(不误杀任务应用)。
|
||||
"""
|
||||
serial = str((request.json or {}).get("serial", "")).strip()
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少设备 serial"}), 400
|
||||
try:
|
||||
if ":" in serial:
|
||||
adb_connect(serial)
|
||||
# 1. 优先关闭定位时启动的浏览器(无论前后台)
|
||||
with _locate_lock:
|
||||
started = _locate_browsers.pop(serial, None)
|
||||
if started:
|
||||
subprocess.run([ADB_PATH, "-s", serial, "shell", "am", "force-stop", started],
|
||||
capture_output=True, timeout=20)
|
||||
msg = f"已关闭定位浏览器 {started}"
|
||||
else:
|
||||
# 2. 退回:前台是浏览器则关闭,否则返回键轻量退出
|
||||
pkg = _current_focus_pkg(serial)
|
||||
if pkg in _BROWSER_PKGS:
|
||||
subprocess.run([ADB_PATH, "-s", serial, "shell", "am", "force-stop", pkg],
|
||||
capture_output=True, timeout=20)
|
||||
msg = f"已关闭浏览器 {pkg}"
|
||||
else:
|
||||
subprocess.run([ADB_PATH, "-s", serial, "shell", "input", "keyevent", "4"],
|
||||
capture_output=True, timeout=15) # BACK 轻量退出
|
||||
msg = "已按返回键退出" + (f"(前台 {pkg})" if pkg else "")
|
||||
_log.info(f"结束定位 {serial}: {msg}")
|
||||
return jsonify({"ok": True, "msg": msg})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"结束定位失败: {e}"}), 502
|
||||
|
||||
|
||||
@app.route("/api/device/screen_all", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_device_screen_all():
|
||||
"""批量亮屏/息屏所有在线设备(并发)。mode: on=亮屏解锁 / off=息屏。
|
||||
|
||||
息屏会中断运行中的任务(u2 无法操作),前端已有确认提示。
|
||||
"""
|
||||
data = request.json or {}
|
||||
mode = data.get("mode", "on")
|
||||
try:
|
||||
serials = [d["serial"] for d in stf.list_all_devices() if d.get("present")]
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"获取设备列表失败: {e}"}), 502
|
||||
if not serials:
|
||||
return jsonify({"ok": False, "error": "无在线设备"}), 400
|
||||
|
||||
def _do(serial):
|
||||
try:
|
||||
if ":" in serial:
|
||||
adb_connect(serial)
|
||||
if mode == "off":
|
||||
subprocess.run([ADB_PATH, "-s", serial, "shell", "input", "keyevent", "26"],
|
||||
capture_output=True, timeout=15) # KEYCODE_POWER 息屏
|
||||
else:
|
||||
subprocess.run([ADB_PATH, "-s", serial, "shell", "input", "keyevent", "224"],
|
||||
capture_output=True, timeout=15) # WAKEUP
|
||||
subprocess.run([ADB_PATH, "-s", serial, "shell", "wm", "dismiss-keyguard"],
|
||||
capture_output=True, timeout=15)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
with ThreadPoolExecutor(max_workers=min(10, len(serials))) as pool:
|
||||
results = list(pool.map(_do, serials))
|
||||
ok_n = sum(1 for x in results if x)
|
||||
_log.info(f"批量{'息屏' if mode == 'off' else '亮屏'}: 成功 {ok_n}/{len(serials)}")
|
||||
return jsonify({"ok": True, "success": ok_n, "total": len(serials)})
|
||||
|
||||
|
||||
@app.route("/api/device/locate", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_device_locate():
|
||||
"""定位设备:点亮屏幕并解除锁屏,可选打开大字定位页(查找设备用)。
|
||||
|
||||
请求 {"serial": "...", "show": true}:show=true 时额外用浏览器打开
|
||||
平台 /locate 定位页,全屏大字显示设备 IP(更醒目,但会切换前台,
|
||||
任务运行中慎用)。点亮组合:WAKEUP → dismiss-keyguard → MENU 兜底。
|
||||
绝不 disconnect,红线。
|
||||
"""
|
||||
data = request.json or {}
|
||||
serial = str(data.get("serial", "")).strip()
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少设备 serial"}), 400
|
||||
try:
|
||||
if ":" in serial:
|
||||
adb_connect(serial)
|
||||
for cmd in (["shell", "input", "keyevent", "224"],
|
||||
["shell", "wm", "dismiss-keyguard"],
|
||||
["shell", "input", "keyevent", "82"]):
|
||||
try:
|
||||
subprocess.run([ADB_PATH, "-s", serial, *cmd],
|
||||
capture_output=True, timeout=15)
|
||||
except Exception:
|
||||
pass # 单步失败不阻塞,继续下一步
|
||||
msgs = ["屏幕已点亮"]
|
||||
if data.get("show"):
|
||||
# 用设备浏览器打开定位页(设备走 tailnet 访问本机 100.100.10.2:18050)
|
||||
locate_url = f"http://100.100.10.2:18050/locate?serial={urllib.parse.quote(serial)}"
|
||||
r = subprocess.run([ADB_PATH, "-s", serial, "shell", "am", "start",
|
||||
"-a", "android.intent.action.VIEW", "-d", locate_url],
|
||||
capture_output=True, timeout=20)
|
||||
out = ((r.stdout or b"") + (r.stderr or b"")).decode("utf-8", errors="replace")
|
||||
if "error" in out.lower() or "exception" in out.lower():
|
||||
msgs.append(f"打开定位页失败: {out.strip()[:80]}")
|
||||
else:
|
||||
msgs.append("已打开大字定位页(按返回键退出)")
|
||||
# 记录启动的浏览器包名:结束定位时无论前后台都关闭它。
|
||||
# 优先从 am start 输出解析 pkg=(浏览器可能尚未到前台,前台检测会扑空)
|
||||
pkg = ""
|
||||
m2 = re.search(r"pkg=([\w.]+)", out)
|
||||
if m2 and m2.group(1) in _BROWSER_PKGS:
|
||||
pkg = m2.group(1)
|
||||
else:
|
||||
time.sleep(1.5) # 等浏览器切到前台再查
|
||||
pkg = _current_focus_pkg(serial)
|
||||
if pkg in _BROWSER_PKGS:
|
||||
with _locate_lock:
|
||||
_locate_browsers[serial] = pkg
|
||||
_log.info(f"定位设备 {serial}: {'; '.join(msgs)}")
|
||||
return jsonify({"ok": True, "msg": f"{serial} {';'.join(msgs)}"})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"定位失败: {e}"}), 502
|
||||
|
||||
|
||||
@app.route("/api/stop_all", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_stop_all():
|
||||
|
||||
Reference in New Issue
Block a user