fix(通知): 多设备批次不再逐台报成功;批次汇总瘦身并列出失败设备;设备只显示名字

用户反馈:一条任务覆盖 13 台设备,每台成功都推一条,群里被刷屏;批次汇总里
「任务:抖音养号」与标题重复、「样本:抖音养号」毫无信息量、「stopped」还是英文。

- task_manager:
  · **多设备批次不发 `task.device.success`**(批次汇总里已有成功台数;单台任务照发);
    失败仍逐台发——那是少数,且要知道是哪台;
  · `_BatchTracker` 收集失败设备(`名字(型号):原因`,最多 5 条)并加进批次汇总;
  · 设备级事件带 `model`:型号取自**设备池快照**(批次开始时一次查好带下去),
    不是 worker 每次连接现采的(那次 `d.info()` 可能超时 → 空),也不查库;
- notifier:
  · 聚合窗口口径修正:事件声明 `agg_window=0`(批次结束/服务启停/备份恢复等低频
    高危事件)**不再被 hook 的窗口拖住**——原先事件级设置完全失效;
  · 「样本」行只在真的合并了多条(>1)时出现,且按**设备**维度写(合并多台时写
    任务名每条都一样);与标题重复的字段行(「任务:x」)不再重复渲染;
  · 设备名与地址同时存在时**只显示名字**(IP 是给日志看的);
  · 补 `stopped`/`failed_devices` 中文标签(原先直接显示英文 key);
- notify_events:批次事件补 `skipped`/`failed_devices` 字段,设备事件补 `model`。

实测(dev 真机 + 假接收端):
  批次 2 台 → 只收到 batch.started/finished,无 device.success,汇总 总数2/成功1/跳过1;
  单台任务 → 收到 device.success,显示「设备:cs1 / 型号:M2010J19SC」不含 IP。
文档:doc/NOTIFY.md §3(多设备批次怎么发 + 批次消息样例)、§6(窗口口径与注意事项)。
This commit is contained in:
2026-09-16 09:47:25 +08:00
parent 49544b1985
commit 16e3e162b2
4 changed files with 150 additions and 31 deletions
+31 -5
View File
@@ -331,6 +331,7 @@ _FIELD_LABELS = {
"timeout_s": "超时(秒)", "task_job": "原任务", "model": "型号",
"apk_name": "应用", "apk_id": "应用ID", "package_name": "包名",
"success": "成功", "failed": "失败", "skipped": "跳过", "total": "总数",
"stopped": "停止", "failed_devices": "失败设备",
"failed_items": "失败明细", "filename": "文件", "size": "大小(字节)",
"tables": "表数", "include_apk": "含APK", "user": "操作人", "operator": "操作人",
"env": "环境", "db_target": "数据库", "device_count": "设备数",
@@ -379,13 +380,25 @@ def build_message(ev, fields, hook=None):
v = fields.get(k)
if v in (None, "", [], {}):
continue
# 标题里已经写出来的主体(任务名/设备名)不再重复占一行:
# "任务批次结束:抖音养号" 下面再来一行 "任务:抖音养号" 是纯噪音
if k in ("job_name", "device_name", "apk_name", "hook_name") \
and str(v) == str(subj):
continue
# 设备名优先:两个都带时只显示名字——IP 是给日志看的,通知里没人想读地址
if k == "serial" and fields.get("device_name"):
continue
if isinstance(v, (list, tuple)):
v = "、".join(str(x) for x in list(v)[:6]) + ("…" if len(v) > 6 else "")
elif isinstance(v, dict):
v = json.dumps(v, ensure_ascii=False)[:200]
rows.append((_label(k), str(v)[:400]))
if merged and merged.get("samples"):
rows.append(("样本", ";".join(merged["samples"][:3])))
# 样本只在**真的合并了多条**时才有意义:合并 1 条时样本就是标题主体本身
if merged and merged.get("merged_count", 0) > 1:
samples = [s for s in (merged.get("samples") or [])[:3]
if s and str(s) != str(subj)]
if samples:
rows.append(("样本", ";".join(samples)))
if merged and merged.get("folded"):
rows.append(("限流折叠", f"另有 {merged['folded']} 条被限流折叠"))
@@ -913,7 +926,16 @@ def _dispatcher_loop():
def _sample(ev, fields):
subj = _subject(ev, fields)
"""聚合样本:一句话说明"合并进来的是哪几条"。
**优先设备维度**(与标题相反):聚合键是 job_id 时,合并的是"同一任务的多台
设备",样本再写任务名就每条都一样(标题里已经有了),写设备名才看得出是哪几台。
"""
subj = ""
for k in ("device_name", "serial", "job_name", "apk_name", "hook_name"):
if fields.get(k):
subj = str(fields[k])
break
reason = fields.get("cause") or fields.get("error") or fields.get("error_msg") or ""
return f"{subj}{' · ' + str(reason)[:40] if reason else ''}"
@@ -976,9 +998,13 @@ def notify(event, **fields):
return
for h in hooks:
keys = tuple(str(fields.get(k, "")) for k in (ev.agg_key or ()))
# 聚合窗口:事件自己声明「立即发」(agg_window=0,低频高危事件,如批次结束 /
# 服务启停 / 备份恢复)时**不受 hook 窗口影响**——否则白等一个窗口,还会
# 挂上一行毫无信息量的「样本」(单条事件的样本=标题主体,纯重复)。
# 其余事件听 hook 的:那个值表达的是"这个群最多等多久合并"。
window = 0 if ev.agg_window == 0 else int(h.get("agg_window") or 0)
try:
_event_q.put_nowait((h["id"], event, dict(fields), keys,
int(h.get("agg_window") or 0)))
_event_q.put_nowait((h["id"], event, dict(fields), keys, window))
except queue.Full:
_dropped["event_q"] += 1
except Exception as e: # 通知出问题绝不能影响业务