a7e8141497
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
4.9 KiB
Python
141 lines
4.9 KiB
Python
"""比价卡死定位:读 pricebot trace 末段,判某平台是否原地打转(卡死)。
|
|
|
|
同机直读 {WORK_LOG_DIR}/{dir_name}/{platform}/step_*.json,只取头部字段
|
|
(pipeline_step/detected_page),不解析后面的无障碍树(windows,占单帧 99% 体积)。
|
|
判据与降级见 docs/superpowers/specs/2026-08-05-compare-stuck-detection-design.md。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
# pipeline_step / 平台名 → 中文(映射不到原样显示英文,不阻断)。按 pricebot 实际枚举补全。
|
|
PIPELINE_STEP_LABELS: dict[str, str] = {
|
|
"set_address": "定位",
|
|
"enter_store": "进店",
|
|
"add_one_dish": "加菜",
|
|
"match_dish": "找菜",
|
|
"checkout": "结算",
|
|
}
|
|
PLATFORM_LABELS: dict[str, str] = {
|
|
"meituan": "美团",
|
|
"eleme": "饿了么",
|
|
"jd_waimai": "京东外卖",
|
|
}
|
|
|
|
_PIPE_RE = re.compile(r'"pipeline_step":\s*"([^"]*)"')
|
|
_PAGE_RE = re.compile(r'"detected_page":\s*"([^"]*)"')
|
|
_STEP_NUM_RE = re.compile(r"step_(\d+)")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StuckPoint:
|
|
platform: str
|
|
pipeline_step: str
|
|
frames: int # 末段连续困住的帧数(上限 max_tail)
|
|
|
|
def label(self) -> str:
|
|
p = PLATFORM_LABELS.get(self.platform, self.platform)
|
|
s = PIPELINE_STEP_LABELS.get(self.pipeline_step, self.pipeline_step)
|
|
return f"{p}·{s}"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StuckResult:
|
|
readable: bool # trace 是否读到(区分「读不到」与「读到但没卡」)
|
|
points: list[StuckPoint] # 卡死平台列表;readable=True 且空 = 确认没卡死
|
|
|
|
|
|
def dir_name_from_trace_url(trace_url: str | None) -> str | None:
|
|
""".../traces/{dir_name}/ → dir_name;空/异常 → None。"""
|
|
if not trace_url:
|
|
return None
|
|
name = trace_url.rstrip("/").rsplit("/", 1)[-1]
|
|
return name or None
|
|
|
|
|
|
def _step_num(path: Path) -> int:
|
|
m = _STEP_NUM_RE.search(path.name)
|
|
return int(m.group(1)) if m else -1
|
|
|
|
|
|
def _read_head(path: Path, nbytes: int = 4096) -> tuple[str | None, str | None]:
|
|
"""只读文件头部,抠 (pipeline_step, detected_page)。它们在 json 最前面。"""
|
|
try:
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
|
head = f.read(nbytes)
|
|
except OSError:
|
|
return None, None
|
|
ps = _PIPE_RE.search(head)
|
|
pg = _PAGE_RE.search(head)
|
|
return (ps.group(1) if ps else None, pg.group(1) if pg else None)
|
|
|
|
|
|
def _platform_stuck(
|
|
platform: str, step_files: list[Path], threshold: int, max_tail: int
|
|
) -> StuckPoint | None:
|
|
"""末帧往前数连续同 (pipeline_step, detected_page) 的帧数 ≥threshold → 卡死。"""
|
|
tail = step_files[-max_tail:]
|
|
heads = [_read_head(p) for p in tail]
|
|
last_ps, last_pg = heads[-1]
|
|
if last_ps is None:
|
|
return None
|
|
count = 0
|
|
for ps, pg in reversed(heads):
|
|
if ps == last_ps and pg == last_pg:
|
|
count += 1
|
|
else:
|
|
break
|
|
if count >= threshold:
|
|
return StuckPoint(platform, last_ps, count)
|
|
return None
|
|
|
|
|
|
def read_stuck_points(trace_dir: Path, *, threshold: int, max_tail: int) -> StuckResult:
|
|
"""逐平台判卡死。目录不存在/异常/无平台帧 → StuckResult(readable=False, points=[])。"""
|
|
try:
|
|
if not trace_dir.is_dir():
|
|
return StuckResult(readable=False, points=[])
|
|
points: list[StuckPoint] = []
|
|
any_frames = False
|
|
for pdir in sorted(trace_dir.iterdir()):
|
|
if not pdir.is_dir():
|
|
continue
|
|
step_files = sorted(pdir.glob("step_*.json"), key=_step_num)
|
|
if not step_files:
|
|
continue
|
|
any_frames = True
|
|
sp = _platform_stuck(pdir.name, step_files, threshold, max_tail)
|
|
if sp is not None:
|
|
points.append(sp)
|
|
if not any_frames:
|
|
return StuckResult(readable=False, points=[])
|
|
return StuckResult(readable=True, points=points)
|
|
except OSError:
|
|
return StuckResult(readable=False, points=[])
|
|
|
|
|
|
def last_step(trace_dir: Path) -> StuckPoint | None:
|
|
"""failed 用:取帧数最多平台的末帧环节(不要求原地打转)。读不到 → None。"""
|
|
try:
|
|
if not trace_dir.is_dir():
|
|
return None
|
|
best: tuple[int, str, list[Path]] | None = None
|
|
for pdir in sorted(trace_dir.iterdir()):
|
|
if not pdir.is_dir():
|
|
continue
|
|
step_files = sorted(pdir.glob("step_*.json"), key=_step_num)
|
|
# 平局(同帧数)时取字典序第一个平台(sorted 保证稳定)
|
|
if step_files and (best is None or len(step_files) > best[0]):
|
|
best = (len(step_files), pdir.name, step_files)
|
|
if best is None:
|
|
return None
|
|
_, platform, step_files = best
|
|
ps, _pg = _read_head(step_files[-1])
|
|
if ps is None:
|
|
return None
|
|
return StuckPoint(platform, ps, len(step_files))
|
|
except OSError:
|
|
return None
|