diff --git a/frontend/src/index.css b/frontend/src/index.css index eac7833..21a4454 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -80,7 +80,7 @@ /* ── 版面切换文字标签(联赛/状态/模式) ── */ .tab { - @apply whitespace-nowrap px-0.5 py-1 text-sm text-ink-500 transition-colors hover:text-ink-900; + @apply relative whitespace-nowrap px-0.5 py-1 text-sm text-ink-500 transition-colors hover:text-ink-900; } .tab-on { @apply font-medium text-press; diff --git a/src/core/retry.py b/src/core/retry.py deleted file mode 100644 index 564c93d..0000000 --- a/src/core/retry.py +++ /dev/null @@ -1,89 +0,0 @@ -"""重试工具:带指数退避的瞬态错误重试。 - -NOTE(审查报告 P3):当前全项目**无调用点** —— bzzoiro 在 `_fetch_json_sync` -里自带了一套重试逻辑,understat/injuries 各自也有。这里保留是作为后续统一 -重试策略的落点,但请勿误以为它已在生效。 - -如果决定不引入统一重试,建议删除本文件以避免"看起来有重试、实际没有"的误判。 -""" -from __future__ import annotations - -import asyncio -import functools -import logging -import random -import time -from typing import Callable, Iterable, TypeVar - -logger = logging.getLogger(__name__) - -T = TypeVar("T") - - -def with_retry( - *, - max_retries: int = 3, - base_delay: float = 1.0, - max_delay: float = 30.0, - retryable_exceptions: Iterable[type[BaseException]] = (Exception,), - on_retry: Callable[[Exception, int], None] | None = None, -) -> Callable: - """重试装饰器(同步/异步通用,指数退避 + 抖动)。 - - Args: - max_retries: 最大重试次数 - base_delay: 基础延迟(秒) - max_delay: 最大延迟(秒) - retryable_exceptions: 触发重试的异常类型 - on_retry: 重试回调(exception, attempt) - """ - retryable = tuple(retryable_exceptions) - - def decorator(func: Callable) -> Callable: - @functools.wraps(func) - async def async_wrapper(*args, **kwargs): - last_exc: Exception | None = None - for attempt in range(max_retries + 1): - try: - return await func(*args, **kwargs) - except retryable as e: - last_exc = e - if attempt == max_retries: - break - delay = min(base_delay * (2 ** attempt), max_delay) - delay += random.uniform(0, delay * 0.1) # 抖动 - logger.warning( - "%s failed (attempt %d/%d), retry in %.1fs: %s", - func.__name__, attempt + 1, max_retries, delay, e, - ) - if on_retry: - on_retry(e, attempt + 1) - await asyncio.sleep(delay) - raise last_exc # type: ignore[misc] - - @functools.wraps(func) - def sync_wrapper(*args, **kwargs): - last_exc: Exception | None = None - for attempt in range(max_retries + 1): - try: - return func(*args, **kwargs) - except retryable as e: - last_exc = e - if attempt == max_retries: - break - delay = min(base_delay * (2 ** attempt), max_delay) - delay += random.uniform(0, delay * 0.1) - logger.warning( - "%s failed (attempt %d/%d), retry in %.1fs: %s", - func.__name__, attempt + 1, max_retries, delay, e, - ) - if on_retry: - on_retry(e, attempt + 1) - time.sleep(delay) - raise last_exc # type: ignore[misc] - - if asyncio.iscoroutinefunction(func): - return async_wrapper - return sync_wrapper - - return decorator diff --git a/src/llm/validation.py b/src/llm/validation.py index 4be2a90..d1b9cd8 100644 --- a/src/llm/validation.py +++ b/src/llm/validation.py @@ -10,8 +10,10 @@ from pydantic import BaseModel, Field, field_validator, model_validator logger = logging.getLogger(__name__) -# 已知的 5 个专家 agent 名(与 orchestrator.SPECIALIST_SPECS 保持一致) -KNOWN_AGENT_NAMES: tuple[str, ...] = ("form", "stats", "home_away", "injuries", "h2h") +# 单一权威源:从 orchestrator.SPECIALIST_SPECS 派生,避免两端独立定义导致静默偏离 +from src.llm.agents.orchestrator import SPECIALIST_SPECS + +KNOWN_AGENT_NAMES: tuple[str, ...] = tuple(spec.name for spec in SPECIALIST_SPECS) class AgentReportSchema(BaseModel):