refactor: 拆分调度器为独立子模块,增强 Sub2API 多认证方式支持

调度器重构:
- 将 CacheAwareScheduler 拆分为 candidate_builder、candidate_sorter、
  concurrency_checker、restriction_checker、scheduling_config、schemas、utils 等独立模块
- 删除旧的 _candidate_builder.py 和 _candidate_sorter.py
- 新增调度并发拒绝 Prometheus 指标

Sub2API 架构增强:
- 支持账号密码登录和 Refresh Token 两种认证方式
- 实现 JWT 自动刷新和 Token Rotation 持久化
- 前端 ProviderAuthDialog 支持多认证方式切换和 credentials_schema 动态渲染
- 验证接口返回 updated_credentials 以同步轮换后的 token

其他改进:
- 并发管理器增加 RPM guard 和动态预留逻辑
- RequestCandidate 支持 mark_skipped 附加 extra_data
- TaskService 增强健壮性
- 补充相关单元测试和契约测试
This commit is contained in:
fawney19
2026-02-15 16:32:23 +08:00
parent 8a670f5524
commit 1c16b77a92
27 changed files with 2159 additions and 645 deletions

View File

@@ -247,7 +247,13 @@ class RequestCandidateService:
@staticmethod
def mark_candidate_skipped(
db: Session, candidate_id: str, skip_reason: str | None = None
db: Session,
candidate_id: str,
skip_reason: str | None = None,
*,
status_code: int | None = None,
concurrent_requests: int | None = None,
extra_data: dict | None = None,
) -> None:
"""
标记候选为已跳过
@@ -256,12 +262,25 @@ class RequestCandidateService:
db: 数据库会话
candidate_id: 候选ID
skip_reason: 跳过原因
status_code: HTTP 状态码(可选)
concurrent_requests: 并发请求数(这里实际记录 RPM 计数)
extra_data: 额外数据(合并写入)
"""
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
if candidate:
candidate.status = "skipped"
candidate.skip_reason = skip_reason
candidate.finished_at = datetime.now(timezone.utc)
if status_code is not None:
candidate.status_code = int(status_code)
if concurrent_requests is not None:
candidate.concurrent_requests = int(concurrent_requests)
if extra_data:
base = candidate.extra_data if isinstance(candidate.extra_data, dict) else {}
candidate.extra_data = {**base, **extra_data}
db.flush() # 只 flush不立即 commit
get_batch_committer().mark_dirty(db)
@@ -364,5 +383,5 @@ class RequestCandidateService:
first_byte_epoch_ms = request_start_epoch_ms + global_first_byte_time_ms
return max(0, int(first_byte_epoch_ms - started_at_epoch_ms))
except Exception as e:
logger.debug(f"计算候选 TTFB 失败: {e}")
logger.debug("计算候选 TTFB 失败: {}", e)
return global_first_byte_time_ms

View File

@@ -4,6 +4,7 @@
from __future__ import annotations
import math
import time
from collections.abc import Callable
from dataclasses import dataclass
@@ -34,6 +35,13 @@ class ExecutionContext:
start_time: float | None = None
elapsed_ms: int | None = None
concurrent_requests: int | None = None
rpm_current: int | None = None
rpm_limit: int | None = None
rpm_available_for_new: int | None = None
reservation_ratio: float | None = None
reservation_phase: str | None = None
reservation_confidence: float | None = None
reservation_load_factor: float | None = None
@dataclass
@@ -100,9 +108,13 @@ class RequestExecutor:
key_id=key.id,
)
except Exception as e:
logger.debug(f"获取 RPM 计数失败(用于预留计算): {e}")
logger.debug("获取 RPM 计数失败(用于预留计算): {}", e)
current_key_rpm = 0
# 在获取 guard 之前记录当前 RPM 计数,便于并发拒绝场景落库
context.concurrent_requests = current_key_rpm
context.rpm_current = current_key_rpm
# 获取有效的 RPM 限制(自适应或固定)
effective_key_limit = get_adaptive_rpm_manager().get_effective_limit(key)
@@ -113,10 +125,23 @@ class RequestExecutor:
)
dynamic_reservation_ratio = reservation_result.ratio
context.rpm_limit = effective_key_limit
context.reservation_ratio = dynamic_reservation_ratio
context.reservation_phase = reservation_result.phase
context.reservation_confidence = reservation_result.confidence
context.reservation_load_factor = reservation_result.load_factor
if effective_key_limit is not None and not is_cached_user:
context.rpm_available_for_new = max(
1, math.floor(effective_key_limit * (1 - dynamic_reservation_ratio))
)
logger.debug(
f"[Executor] 动态预留: key={key.id[:8]}..., "
f"ratio={dynamic_reservation_ratio:.0%}, phase={reservation_result.phase}, "
f"confidence={reservation_result.confidence:.0%}"
"[Executor] 动态预留: key={}..., ratio={:.0%}, phase={}, confidence={:.0%}",
key.id[:8],
dynamic_reservation_ratio,
reservation_result.phase,
reservation_result.confidence,
)
async with self.concurrency_manager.rpm_guard(
@@ -131,10 +156,11 @@ class RequestExecutor:
key_id=key.id,
)
except Exception as e:
logger.debug(f"获取 RPM 计数失败guard 内): {e}")
logger.debug("获取 RPM 计数失败guard 内): {}", e)
key_rpm_count = None
context.concurrent_requests = key_rpm_count # 用于记录,实际是 RPM 计数
if key_rpm_count is not None:
context.concurrent_requests = key_rpm_count # 用于记录,实际是 RPM 计数
context.start_time = time.time()
response = await request_func(provider, endpoint, key, candidate)