fix(设备名): 选择设备与所有通知都显示名称而不是 IP

用户反馈:① 元素抓取的选择设备列表显示的是 IP;② 所有 webhook 通知里都是 IP;
要都显示设备名。

- 通知链路统一补名字(调用点不用动):`core/notifier.py`
  · 新增 `set_device_name_resolver()` + `fill_device_names()`,在 `notify()` 与
    `build_message()`(预览/测试发送也走它)里把 `serial` 补成 `device_name`、
    把 `serials`/`devices` 列表逐项换成名字。
  · 补一处就全带名字了——标题主体、字段表、聚合样本认的都是 `device_name`。
  · **做成"纯内存回调"是刻意的**:notify() 的硬红线是零 DB,不能为了取个名字去查库
    (那等于在业务线程里加一次阻塞查询)。
  · 降级规则:调用点自己传了 device_name 就用它的;查不到名字(没命名/不在池里)
    保留原地址;解析器缺失或抛异常都只是降级,绝不影响发送。
- `core/device_pool.py`:维护 `serial → 名称` 内存快照——`init_app` 同步刷一次、
  增删改名/迁址后各刷一次(改名立刻生效)、`device-names` 线程每 60s 兜底刷一次
  (覆盖整库恢复这类进程外改动)。`name_of()` 只读内存,可在通知路径上安全调用。
- `web_server.py`:装配层接上 `notifier.set_device_name_resolver(device_pool.name_of)`。
- 元素抓取/测试此步骤的设备列表:`GET /api/uiauto/devices` 的 `name` 改用**平台名**,
  前端 `editor.js` 新增共用的 `_devCard()`——名字做主标题(粗体),
  `型号 · 地址` 作副标题。
  · 池外设备**退回地址而不是 uiautodev 的 name**:实测那份 name 是设备 codename
    (一柜子机器全叫 "earth"),拿它认设备等于没名字,地址至少唯一。
  · 「测试此步骤」的设备列表原来连状态角标都没有,一并统一成同一个卡片。
- 顺带修一个**既有 bug(不是本次需求)**:`/locate` 设备端定位页从 ce47a5b 那次
  web 蓝图拆分起就一直 500——拆分时漏掉了 `render_template_string` 与
  `markupsafe.escape as _esc` 两个 import。后者不只是缺个名字:定位页把 query 里的
  serial 拼进 HTML,而 `render_template_string` 的模板名是 "<template>"、
  **不会自动转义**,所以那还是个反射 XSS。已显式转义(已用 `<img onerror>` 验证)。

自测:device_pool 快照/改名即时生效、fill_device_names 全分支(含列表、
未命名保留地址、不在池保留地址、解析器缺失/抛异常降级)、真消息渲染断言
**通篇不含 IP**;GET 路由冒烟 54 个 0 个 500;前端语法 + 卡片渲染截图核对。
This commit is contained in:
2026-09-24 08:55:10 +08:00
parent 773cfbb763
commit 8fdde978ea
10 changed files with 156 additions and 14 deletions
+51
View File
@@ -39,13 +39,59 @@ def init_app(app):
并后台刷新一次设备型号(首次启动/设备更换后型号可能变化)。"""
global _app
_app = app
refresh_names() # 同步刷一次名称快照,让通知从第一条起就有名字
try:
t = threading.Thread(target=_refresh_models_bg, daemon=True)
t.start()
t2 = threading.Thread(target=_names_loop, name="device-names", daemon=True)
t2.start()
except Exception:
pass
# ================== 名称内存快照(给通知用) ==================
# 为什么单独存一份:webhook 通知里要显示"是哪台设备"(A08)而不是地址(IP),
# 但 `notifier.notify()` 有一条硬红线——**零 DB 访问**(不能为了取个名字去查库,
# 更不能在业务线程里阻塞)。所以由这里维护一份 serial→名称 的内存快照:
# 启动刷一次、池子有变动时刷一次、再兜底每 60s 刷一次(覆盖整库恢复等外部改动)。
_names = {}
_names_lock = threading.Lock()
_NAMES_INTERVAL = 60
def refresh_names():
"""把设备池的 serial→名称刷进内存快照(只查库,不碰 adb)。返回条数。"""
try:
rows = list_devices()
except Exception as e:
_log.debug(f"刷新设备名快照失败: {e}")
return 0
m = {}
for d in rows:
s = d.get("serial") or ""
if s:
m[s] = d.get("name") or ""
with _names_lock:
_names.clear()
_names.update(m)
return len(m)
def name_of(serial):
"""serial 对应的设备名称(**纯内存,可在通知路径上调用**)。没有则返回 ""。"""
if not serial:
return ""
with _names_lock:
return _names.get(str(serial), "")
def _names_loop():
"""兜底定时刷新名称快照(覆盖整库恢复这类进程外改动)。"""
while True:
time.sleep(_NAMES_INTERVAL)
refresh_names()
def _refresh_models_bg():
"""后台批量采集在线设备型号(启动时/手动触发)。失败静默,不影响启动。"""
time.sleep(3) # 等服务起来再查
@@ -183,6 +229,7 @@ def _move_device_row(old, new_serial, fingerprint=""):
if fingerprint:
old.fingerprint = fingerprint
db.session.commit()
refresh_names() # 键变了(serial → 名称的映射也跟着变)
if _move_hook is not None:
try:
_move_hook(old_serial, new_serial)
@@ -255,11 +302,13 @@ def add_device(serial, name="", note="", enabled=True, fingerprint=""):
if fingerprint:
d.fingerprint = fingerprint
db.session.commit()
refresh_names() # 名字可能改了:通知里要立刻用新名字
return False
db.session.add(Device(serial=serial, name=name or "", note=note or "",
enabled=enabled, fingerprint=fingerprint or "",
created_at=time.strftime("%Y-%m-%d %H:%M")))
db.session.commit()
refresh_names()
_log.info(f"设备池新增设备: {serial}")
return True
@@ -274,6 +323,7 @@ def rename_device(serial, name):
old = d.name or ""
d.name = name
db.session.commit()
refresh_names()
_log.info(f"设备池重命名: {old or serial} → {name}({serial})")
return True
@@ -301,6 +351,7 @@ def remove_device(serial):
return False
db.session.delete(d)
db.session.commit()
refresh_names()
_log.info(f"设备池删除设备: {serial}")
return True