Merge branch 'fix/device-name-display'——选择设备与所有通知显示设备名(不再显示 IP)+ 修 /locate 500/XSS

This commit is contained in:
2026-09-24 09:17:15 +08:00
10 changed files with 156 additions and 15 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
+49
View File
@@ -357,6 +357,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"):
@@ -374,6 +421,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)
@@ -1008,6 +1056,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,低频高危事件,如批次结束 /