fix: 应用代码审查三项发现

- frontend: .tab 补 position:relative,修复激活 tab 下划线定位缺失定位上下文
- core/retry.py: 删除全项目零调用的重试模块(避免"看似生效实际未接入"的误判)
- llm/validation.py: KNOWN_AGENT_NAMES 改为从 orchestrator.SPECIALIST_SPECS 派生,单一权威源
This commit is contained in:
shangfangjian
2026-09-15 20:12:47 +08:00
parent c5f92c9a54
commit fa69795d69
3 changed files with 5 additions and 92 deletions
+1 -1
View File
@@ -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;
-89
View File
@@ -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
+4 -2
View File
@@ -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):