feat(tradein/domclick-local): --enum-cache — enumerate один раз, resume detail поверх
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / openapi-codegen-check (pull_request) Has been skipped

enumerate (~6422 id через SERP, ~1.5-2ч) теперь пишет полный список id в файл-кэш
(--enum-cache PATH) с sentinel завершённости; следующий запуск грузит кэш и полностью
пропускает enumerate → сразу detail. detail-resume по done (JSONL) композится поверх.
Кэш пишется только после реально завершённого свода (прерванный enumerate не кэшируется).
Тест: run1 записал 5 id+sentinel, run2 загрузил и пропустил SERP.
This commit is contained in:
bot-backend 2026-06-27 23:37:07 +03:00
parent 0f7f5ea0ab
commit 5f994cc300

View file

@ -755,6 +755,65 @@ def append_record(path: Path, rec: dict[str, Any]) -> None:
fh.flush()
# ── Enumerate cache (полный список id, чтобы НЕ гонять SERP заново) ────────────
def write_enum_cache(path: str, offers: list[dict[str, Any]]) -> None:
"""Перезаписать кэш-файл полным списком offer-id + sentinel завершённости.
Каждый offer пишется как {"id":..., "source_url":...} (только 2 поля),
финальной строкой sentinel {"_enum_complete": true, "count": N}. Sentinel
означает «enumerate завершился полностью» без него load_enum_cache считает
кэш неполным и заставляет re-enumerate.
"""
cache_path = Path(path)
cache_path.parent.mkdir(parents=True, exist_ok=True)
with cache_path.open("w", encoding="utf-8") as fh:
for serp in offers:
row = {"id": serp.get("id"), "source_url": serp.get("source_url")}
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
fh.write(json.dumps({"_enum_complete": True, "count": len(offers)}) + "\n")
fh.flush()
logger.info("enum-cache: wrote %d ids → %s", len(offers), path)
def load_enum_cache(path: str) -> list[dict[str, Any]] | None:
"""Загрузить полный список offer-id из кэша. None если файла нет/кэш неполный.
Кэш валиден ТОЛЬКО если последняя валидная строка sentinel с
``_enum_complete == True`` (enumerate завершился). Битые строки пропускаются;
нет sentinel / пустой / повреждённый None (заставляет re-enumerate).
"""
cache_path = Path(path)
if not cache_path.exists():
return None
records: list[dict[str, Any]] = []
with cache_path.open("r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(rec, dict):
records.append(rec)
if not records:
return None
last = records[-1]
if not (isinstance(last, dict) and last.get("_enum_complete") is True):
return None # кэш неполный (enumerate не завершился) → re-enumerate
offers: list[dict[str, Any]] = []
for rec in records[:-1]:
sid = rec.get("id")
if sid is None:
continue
offers.append({"id": str(sid), "source_url": rec.get("source_url")})
logger.info("enum-cache: loaded %d ids (complete)", len(offers))
return offers
# ── SERP transport (playwright page navigation, чистый браузерный транспорт) ──
@ -1220,9 +1279,22 @@ async def run(args: argparse.Namespace) -> None:
logger.warning("warmup navigation failed (%s) — continuing", exc)
# 1. Enumerate через фронтовый SERP (тот же чистый браузерный транспорт).
cards = await enumerate_cards(
page, done, args.limit_pages, target_new, enum_render, enum_jitter
)
# enum-cache: полный список id живёт в файле → следующий запуск ПРОПУСКАЕТ
# enumerate (~1.5-2ч) и сразу идёт в detail. detail-resume по `done`
# композится поверх: кэш даёт ВСЕ id, `done` (из JSONL) скипает готовые.
cards: list[dict[str, Any]] | None = None
if args.enum_cache:
cards = load_enum_cache(args.enum_cache)
if cards is None:
cards = await enumerate_cards(
page, done, args.limit_pages, target_new, enum_render, enum_jitter
)
# Sentinel пишем ТОЛЬКО после реально завершённого enumerate (offers
# полный) — прерванный/упавший свод кэш не перезаписывает.
if args.enum_cache:
write_enum_cache(args.enum_cache, cards)
else:
logger.info("enum-cache HIT: %d ids — enumerate ПРОПУЩЕН", len(cards))
summary.enumerated = len(cards)
if args.enumerate_only:
@ -1312,6 +1384,12 @@ def build_arg_parser() -> argparse.ArgumentParser:
)
p.add_argument("--limit-pages", type=int, default=100, help="макс. страниц на room-бакет")
p.add_argument("--enumerate-only", action="store_true", help="только перечислить, без detail")
p.add_argument(
"--enum-cache",
type=str,
default=None,
help="файл-кэш полного списка id (enumerate один раз → resume detail поверх)",
)
p.add_argument(
"--with-sold-similar",
action="store_true",