fix: 应用代码审查三项发现
- frontend: .tab 补 position:relative,修复激活 tab 下划线定位缺失定位上下文 - core/retry.py: 删除全项目零调用的重试模块(避免"看似生效实际未接入"的误判) - llm/validation.py: KNOWN_AGENT_NAMES 改为从 orchestrator.SPECIALIST_SPECS 派生,单一权威源
This commit is contained in:
@@ -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
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user