feat: 监控页按名称排序 + 按钮改「看屏 / 打开设备端 Agent」;设备名匹配补 USB 设备
用户反馈的三件事:
1) **监控页设备排序**改成按**名称**(A01/A02/…),没名字的排最后按地址。
之前是 adb 返回的顺序,看着像按 IP 排,同型号多台时对不上号。
排序放在 `/api/status`(后端),大屏页一并受益。
2) **操作按钮改名 + 换实现**:
- 「截图」→「**看屏**」(行为不变,就是查看设备实时画面)
- 「定位」→「**打开设备端 Agent**」:改成让设备上的 Agent 弹身份大字页
(名称/IP/平台地址),替代原来"推 HTML 让设备浏览器打开定位页"那套
(那套要设备装浏览器、页面还依赖设备能访问平台,backlog A1/A2 一直没修)
* 新接口 `POST /api/agent/show-info {serial, close?}`;没装 Agent 时明确报出来
* **先点亮屏幕 + 解除锁屏再拉起** —— 设备息屏时 am start 会把大字页开在锁屏后面,
人看到的还是一片黑(实测踩过)
* 平台地址取操作者当前访问地址;若是 localhost 则换成本机局域网地址
(设备上显示 127.0.0.1 毫无意义)
3) **安装应用的选择设备界面仍显示裸序列号**:USB 设备在 adb 里的 serial 就是
`ro.serialno`,等于池里那条网络记录的**指纹** —— 之前只按 serial 匹配,插着 USB
时会冒出一个没名字的 `s8o7nrt8pbzhfadq`。现在按 serial 或指纹任一命中都取名称,
三处(剪贴板注入 / 安装弹窗 / 安装进度表)统一。
顺带修掉几个自己踩出来的坑:
- `device_pool.list_configured()` 返回的是 **serial 字符串列表**,我当成字典用了
(`.get("serial")` 抛异常被 except 吞掉 → 名字全空)→ 改用 `list_devices()`
- `web/device_agent_api.py` 用了 `PERM_DEVICES` 但没导入 → 服务直接起不来(502)
- Agent 自检里的版本号是写死的常量(升到 1.4 还显示 1.0)→ 改成读包信息
- `doc/API.md` 章节编号(插了一节没把后面整体下移,出现两个 §16)
验证:安装弹窗里 USB 那台现在显示「A07」(指纹匹配);监控页按钮为
「看屏 / 打开设备端 Agent」;对装/未装 Agent 的设备分别得到大字页 / 明确报错;
大字页实测显示 A07 + 192.168.20.250:18050 + Agent v1.4。
This commit is contained in:
+16
-6
@@ -65,23 +65,33 @@ def api_apks_install_devices():
|
||||
source: pool=设备池 / usb=本机有线 adb / adb=本机网络 adb。
|
||||
"""
|
||||
devices, seen = [], set()
|
||||
name_by_serial, name_by_fp = {}, {}
|
||||
try:
|
||||
online = set(device_pool.list_online())
|
||||
for d in device_pool.list_configured(enabled_only=False):
|
||||
# list_devices() 返回字典(含 name/fingerprint);list_configured() 只有 serial 字符串
|
||||
for d in device_pool.list_devices():
|
||||
serial = d.get("serial")
|
||||
if serial in online and serial not in seen:
|
||||
devices.append({"serial": serial, "model": "",
|
||||
"name": d.get("name") or "", "source": "pool"})
|
||||
seen.add(serial)
|
||||
except Exception:
|
||||
pass
|
||||
if serial:
|
||||
name_by_serial[serial] = d.get("name") or ""
|
||||
# USB 设备的 adb serial == ro.serialno == 池里那条网络记录的指纹,
|
||||
# 靠指纹才认得出是同一台(否则插着 USB 时会冒出一个没名字的裸序列号)
|
||||
if d.get("fingerprint"):
|
||||
name_by_fp[d["fingerprint"]] = d.get("name") or ""
|
||||
except Exception as e:
|
||||
_log.warning("读取设备名失败(列表将只显示地址): %s", e)
|
||||
out = _adb("devices")
|
||||
for line in (out or "").splitlines()[1:]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[0] and parts[0] not in seen:
|
||||
devices.append({"serial": parts[0], "model": "",
|
||||
"name": "", "source": "usb" if ":" not in parts[0] else "adb"})
|
||||
seen.add(parts[0])
|
||||
s = parts[0]
|
||||
devices.append({"serial": s, "model": "",
|
||||
"name": name_by_serial.get(s) or name_by_fp.get(s) or "",
|
||||
"source": "usb" if ":" not in s else "adb"})
|
||||
seen.add(s)
|
||||
return jsonify({"ok": True, "devices": devices})
|
||||
|
||||
@bp.route("/api/apks/install", methods=["POST"])
|
||||
|
||||
+14
-5
@@ -20,20 +20,29 @@ def _merged_device_list():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[0] and not parts[0].startswith("*"):
|
||||
states[parts[0]] = parts[1]
|
||||
names = {}
|
||||
# 注意用的是 list_devices()(**字典**,含 name/fingerprint);
|
||||
# list_configured() 只返回 serial 字符串列表,别拿它当字典用(实测踩过:.get 抛异常
|
||||
# 被 except 吞掉 → 名字全空)
|
||||
names, fp_names = {}, {}
|
||||
try:
|
||||
for d in device_pool.list_configured(enabled_only=False):
|
||||
for d in device_pool.list_devices():
|
||||
if d.get("serial"):
|
||||
names[d["serial"]] = d.get("name") or ""
|
||||
except Exception:
|
||||
pass
|
||||
# USB 设备在 adb 里的 serial 就是 ro.serialno,等于池里那条网络记录的
|
||||
# **指纹** —— 插着 USB 时会以另一个 serial 出现,靠指纹才认得出是同一台,
|
||||
# 否则界面上会冒出一个没名字的裸序列号(实测踩过)
|
||||
if d.get("fingerprint"):
|
||||
fp_names[d["fingerprint"]] = d.get("name") or ""
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger("web").warning(f"读取设备名失败: {e}")
|
||||
try:
|
||||
for serial in device_pool.list_configured():
|
||||
if serial not in states:
|
||||
states[serial] = "pool"
|
||||
except Exception:
|
||||
pass
|
||||
return [{"serial": s, "name": names.get(s, ""), "state": st}
|
||||
return [{"serial": s, "name": names.get(s) or fp_names.get(s) or "", "state": st}
|
||||
for s, st in sorted(states.items())]
|
||||
|
||||
|
||||
|
||||
+80
-2
@@ -28,7 +28,7 @@ from config import APK_DIR
|
||||
from core import device_pool
|
||||
from core.db_config import meta_get, meta_set
|
||||
from core.logger import get_logger
|
||||
from web.auth import PERM_APKS, perm_required
|
||||
from web.auth import PERM_APKS, PERM_DEVICES, perm_required
|
||||
|
||||
bp = Blueprint("device_agent", __name__)
|
||||
_log = get_logger("web.devagent")
|
||||
@@ -86,7 +86,7 @@ def _identify():
|
||||
if d:
|
||||
return fp, d.get("serial", serial), d.get("name") or "", True
|
||||
if serial:
|
||||
for d in device_pool.list_configured(enabled_only=False):
|
||||
for d in device_pool.list_devices(): # 返回字典(list_configured 只有 serial)
|
||||
if d.get("serial") == serial:
|
||||
return fp, serial, d.get("name") or "", True
|
||||
except Exception as e:
|
||||
@@ -264,6 +264,84 @@ def _prune_logs(keep=_MAX_LOG):
|
||||
|
||||
|
||||
# ================== 管理端接口(登录 + 权限)==================
|
||||
@bp.route("/api/agent/show-info", methods=["POST"])
|
||||
@perm_required(PERM_DEVICES)
|
||||
def api_agent_show_info():
|
||||
"""让设备上的 Agent 打开「身份大字页」(监控页那个按钮调它)。
|
||||
|
||||
替代原来那套「推 HTML 让设备浏览器打开定位页」——那套在 MIUI 上要装浏览器、
|
||||
页面还依赖设备能访问平台;Agent 是常驻 App,一条 `am start` 就出画面。
|
||||
|
||||
请求:{"serial": "…", "close": false}
|
||||
返回:{ok, msg};设备没装 Agent 时明确报出来(不静默失败)
|
||||
"""
|
||||
from core.adb_helper import _adb
|
||||
|
||||
data = request.json or {}
|
||||
serial = (data.get("serial") or "").strip()
|
||||
close = bool(data.get("close"))
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||||
|
||||
if close:
|
||||
args = ["--es", "close", "1"]
|
||||
else:
|
||||
# 设备信息:名称/IP/平台地址 —— Agent 用它们显示大字页
|
||||
name, ip = "", ""
|
||||
ip = serial.split(":")[0] if ":" in serial else ""
|
||||
for d in device_pool.list_devices(): # 字典列表(list_configured 只有 serial)
|
||||
if d.get("serial") == serial:
|
||||
name = d.get("name") or ""
|
||||
break
|
||||
if not name:
|
||||
# USB 设备的 serial 就是指纹:池里那条记录的 serial 是网络地址,
|
||||
# 靠指纹才认得出来(否则大字页标题会显示一串裸序列号)
|
||||
for d in device_pool.list_devices():
|
||||
if d.get("fingerprint") and d["fingerprint"] == serial:
|
||||
name = d.get("name") or ""
|
||||
break
|
||||
if not ip:
|
||||
# USB 设备:平台不知道它的 IP,现问一下设备(只读命令,不影响设备)
|
||||
out = _adb("-s", serial, "shell", "ip", "-4", "addr", "show", "wlan0")
|
||||
import re as _re
|
||||
m = _re.search(r"inet\s+(\d+\.\d+\.\d+\.\d+)", out or "")
|
||||
ip = m.group(1) if m else ""
|
||||
# 平台地址:用操作者当前访问的地址;如果是 localhost,设备根本连不上 →
|
||||
# 换成局域网地址(设备上显示 127.0.0.1 毫无意义)
|
||||
server = request.host_url.rstrip("/")
|
||||
if "127.0.0.1" in server or "localhost" in server:
|
||||
from web.devices_api import _lan_ip
|
||||
server = "http://%s:18050" % _lan_ip()
|
||||
args = ["--es", "name", name or serial.split(":")[0],
|
||||
"--es", "serial", serial,
|
||||
"--es", "server", server]
|
||||
if ip:
|
||||
args += ["--es", "ip", ip]
|
||||
|
||||
# 先点亮屏幕、解除锁屏:设备息屏时 am start 会把大字页开在锁屏**后面**,
|
||||
# 人看到的还是一片黑(实测踩过)——这正是原来"定位"按钮里的"点亮屏幕"那一步
|
||||
if not close:
|
||||
try:
|
||||
_adb("-s", serial, "shell", "input", "keyevent", "KEYCODE_WAKEUP")
|
||||
_adb("-s", serial, "shell", "wm", "dismiss-keyguard")
|
||||
except Exception as e:
|
||||
_log.warning("点亮屏幕失败(不影响显示身份页): %s", e)
|
||||
|
||||
try:
|
||||
out = _adb("-s", serial, "shell", "am", "start", "-n",
|
||||
"com.example.deviceagent/.InfoActivity", *args) or ""
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"adb 执行失败: {e}"}), 500
|
||||
|
||||
low = out.lower()
|
||||
if "unable to resolve intent" in low or "does not exist" in low or "error" in low:
|
||||
return jsonify({"ok": False,
|
||||
"error": "这台设备没装设备 Agent(com.example.deviceagent)——"
|
||||
"可以从「应用管理 → 设备端应用商店」装,或用扫码配置"}), 400
|
||||
_log.info("设备端 Agent %s: %s", "关闭身份页" if close else "打开身份页", serial)
|
||||
return jsonify({"ok": True, "msg": "已关闭" if close else "已让设备显示身份信息"})
|
||||
|
||||
|
||||
@bp.route("/api/agent-store/config", methods=["GET", "POST"])
|
||||
@perm_required(PERM_APKS)
|
||||
def api_agent_store_config():
|
||||
|
||||
@@ -102,6 +102,11 @@ def api_status():
|
||||
devices, err = context.mgr.get_status()
|
||||
if err:
|
||||
return jsonify({"ok": False, "error": err}), 500
|
||||
# 按**名称**排(A01/A02/…)——之前是 adb 返回的顺序,看着像按 IP 排,很别扭。
|
||||
# 没名字的排在最后、彼此按地址排(历史数据里可能有没命名的设备)。
|
||||
devices.sort(key=lambda d: (
|
||||
1 if not (d.get("device_name") or "").strip() else 0,
|
||||
(d.get("device_name") or "").strip() or d.get("serial", "")))
|
||||
return jsonify({
|
||||
"ok": True, "devices": devices, "server_time": time.time(),
|
||||
"fg_scanning": context.mgr._fg_scanner.is_scanning,
|
||||
|
||||
Reference in New Issue
Block a user