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
+49
View File
@@ -356,6 +356,53 @@ def _label(k):
return _FIELD_LABELS.get(k, k)
# ================== 设备名解析(让通知显示名字而不是 IP) ==================
# 由装配层注册(web_server.py):`notifier.set_device_name_resolver(device_pool.name_of)`。
# **刻意做成"纯内存回调"**:notify() 的红线是零 DB——不能为了取个名字去查库。
# 快照由 device_pool 维护(启动/池子变动/每 60s 兜底刷新)。
_device_name_fn = None
def set_device_name_resolver(fn):
"""注册 serial → 设备名 的解析器。必须是**纯内存**实现(不得查库/发请求)。"""
global _device_name_fn
_device_name_fn = fn
def _dev_name(serial):
"""尽力把地址解析成设备名;解析不到返回 ""(调用方保留原值)。"""
if not serial or _device_name_fn is None:
return ""
try:
return str(_device_name_fn(str(serial)) or "")
except Exception:
return ""
def fill_device_names(fields):
"""把 fields 里的设备地址补成设备名(**原地改**,返回同一个 dict)。
两类字段:
· `serial` → 补 `device_name`(原来没填才补)——标题主体、字段表、
聚合样本认的都是 `device_name`,补上这一处就全带名字了;
· `serials` / `devices`(多设备事件,如设备断联)→ 逐项换成名字,
通知里不该出现一串 IP。
查不到名字(设备没命名 / 已不在池里)就**保留原值**,绝不把内容弄丢。
"""
if not isinstance(fields, dict):
return fields
s = fields.get("serial")
if s and not fields.get("device_name"):
n = _dev_name(s)
if n:
fields["device_name"] = n
for k in ("serials", "devices"):
v = fields.get(k)
if isinstance(v, (list, tuple)) and v:
fields[k] = [_dev_name(x) or x for x in v]
return fields
def _subject(ev, fields):
"""标题主体:**用户自己写的 title 优先**,其次任务名,再次设备名/serial。"""
if fields.get("title"):
@@ -373,6 +420,7 @@ def _subject(ev, fields):
def build_message(ev, fields, hook=None):
"""把事件渲染成统一消息体(title/summary/fields/level/markdown)。"""
fields = dict(fields or {})
fill_device_names(fields) # 同 notify():预览/测试发送也要显示设备名
merged = fields.pop("_merged", None)
level = _level_of(ev.key, fields)
subj = _subject(ev, fields)
@@ -1007,6 +1055,7 @@ def notify(event, **fields):
for p in h["events"])]
if not hooks:
return
fill_device_names(fields) # 地址 → 设备名(纯内存快照,不查库)
for h in hooks:
keys = tuple(str(fields.get(k, "")) for k in (ev.agg_key or ()))
# 聚合窗口:事件自己声明「立即发」(agg_window=0,低频高危事件,如批次结束 /