Files
shaguabijia-app-server/app/services/compare_alert.py
T
guke 347c4c7de4 feat(compare-alert): 卡点独立成列(AlertHit.stuck_point + 卡片第5列),reason 去重简化
- AlertHit 加 stuck_point: str | None = None 字段(格式化好的「平台·环节 帧/s」)
- classify_cancelled_fallback reason 简化为「深度放弃」(耗时/步数已在「用时」列,不重复)
- build_hits 加 _fmt_stuck helper;cancelled 卡死 stuck_point=「美团·加菜 110帧/32s」;
  failed stuck_point=环节标签、reason 不再附「卡在 X」
- format_alert_card 列序改为 时间/手机号/用时/失败原因/卡点/版本/trace (7列)
- 同步更新 test_compare_alert_stuck_worker / _fallback / _format / _rules 断言

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 16:47:23 +08:00

99 lines
3.4 KiB
Python

"""比价失败报警规则:一条记录 → 命中的 AlertHit(或 None)。
纯函数、不碰 DB(阈值/关键词由调用方从 config 传入),便于单测与调阈值。判定顺序保证四类互斥:
failed → fail_reason 空(且 information 非业务)=T1 / 含未识别=T6 / 含超时词=T2 / 其余业务不报;
cancelled 且耗时或步数超阈值=T5(深度放弃);success/running 不报。
口径依据见 docs/superpowers/specs/2026-08-04-compare-fail-alert-design.md 第 3 节。
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Any
ALERT_TYPE_LABELS: dict[str, str] = {
"T1": "系统技术失败",
"T6": "商品识别失败",
"T2": "启动/超时失败",
"T5": "深度放弃(cancelled)",
}
@dataclass(frozen=True)
class AlertHit:
trace_id: str
alert_type: str
reason: str
app_version: str | None
created_at: datetime | None
trace_url: str | None
user_id: int | None
total_ms: int | None = None
step_count: int | None = None
stuck_point: str | None = None
def make_hit(rec: Any, alert_type: str, reason: str) -> AlertHit:
return AlertHit(
trace_id=rec.trace_id,
alert_type=alert_type,
reason=reason,
app_version=getattr(rec, "app_version", None),
created_at=getattr(rec, "created_at", None),
trace_url=getattr(rec, "trace_url", None),
user_id=getattr(rec, "user_id", None),
total_ms=getattr(rec, "total_ms", None),
step_count=getattr(rec, "step_count", None),
)
def classify_cancelled_fallback(
rec: Any,
*,
cancelled_ms_threshold: int,
cancelled_step_threshold: int,
) -> AlertHit | None:
"""cancelled 保底判定(读不到 trace 时用):超耗时或步数阈值 → T5 深度放弃,否则 None。纯函数。"""
ms = rec.total_ms
step = rec.step_count
deep = (ms is not None and ms > cancelled_ms_threshold) or (
step is not None and step > cancelled_step_threshold
)
if deep:
return make_hit(rec, "T5", "深度放弃")
return None
def classify_record(
rec: Any,
*,
cancelled_ms_threshold: int,
cancelled_step_threshold: int,
timeout_keywords: tuple[str, ...],
unrecognized_keywords: tuple[str, ...],
biz_exclude_keywords: tuple[str, ...],
) -> AlertHit | None:
"""判定单条记录是否触发报警。rec 需有 status/fail_reason/information/total_ms/step_count/
trace_id/app_version 属性(ComparisonRecord 或等价对象)。"""
status = rec.status
if status == "failed":
fail_reason = rec.fail_reason
if fail_reason is None:
info = (rec.information or "").strip()
if info and any(w in info for w in biz_exclude_keywords):
return None
return make_hit(rec, "T1", f"技术失败·{info[:80] or '比价过程出错'}")
if any(w in fail_reason for w in unrecognized_keywords):
return make_hit(rec, "T6", f"识别失败·{fail_reason[:80]}")
if any(w in fail_reason for w in timeout_keywords):
return make_hit(rec, "T2", fail_reason[:80])
return None
if status == "cancelled":
return classify_cancelled_fallback(
rec,
cancelled_ms_threshold=cancelled_ms_threshold,
cancelled_step_threshold=cancelled_step_threshold,
)
return None