feat(pool): 号池额度主动探测、封禁自动清除、调度硬优先级与前端重构

- 新增 PoolQuotaProbeScheduler,按 probing_interval_minutes 主动探测静默 Key 额度
- pool_advanced 增加 probing_enabled / auto_remove_banned_keys 配置项
- error_handler 和 quota_service 支持封禁 Key 自动删除及缓存清理
- multi_score 策略从加权混合重构为硬优先级排序,引入 mutex_group 互斥组
- 指纹注入从 handler 层下移至 ClaudeCode envelope 层
- OAuth 批量导入支持 concurrency 并发参数
- 前端号池管理拆分高级设置/账号批量/代理设置为独立组件
- 号池总览接口精简,仅返回已启用调度的 Provider
This commit is contained in:
fawney19
2026-03-05 15:15:26 +08:00
parent fdb50a065b
commit b1be413dc0
29 changed files with 3098 additions and 852 deletions

View File

@@ -0,0 +1,88 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from src.services.provider_keys.pool_quota_probe_scheduler import _select_probe_key_ids
def _key(
key_id: str,
*,
last_used_at: datetime | None = None,
upstream_metadata: dict | None = None,
) -> SimpleNamespace:
return SimpleNamespace(
id=key_id,
last_used_at=last_used_at,
upstream_metadata=upstream_metadata or {},
)
def test_select_probe_key_ids_selects_silent_keys_only() -> None:
now = datetime(2026, 3, 5, 12, 0, 0, tzinfo=timezone.utc)
now_ts = int(now.timestamp())
keys = [
_key("k1"), # never used, should be probed
_key("k2", last_used_at=now - timedelta(minutes=2)), # recently used, skip
_key(
"k3",
upstream_metadata={"codex": {"updated_at": now_ts - (20 * 60)}},
), # long-time no refresh, should be probed
]
selected = _select_probe_key_ids(
keys=keys, # type: ignore[arg-type]
provider_type="codex",
now_ts=now_ts,
interval_seconds=10 * 60,
last_probe_timestamps={},
limit=0,
)
assert selected == ["k1", "k3"]
def test_select_probe_key_ids_resets_probe_window_after_key_usage() -> None:
now = datetime(2026, 3, 5, 12, 0, 0, tzinfo=timezone.utc)
now_ts = int(now.timestamp())
keys = [
_key(
"k1",
last_used_at=now - timedelta(seconds=30),
upstream_metadata={"codex": {"updated_at": now_ts - (40 * 60)}},
)
]
# 上一次主动探测非常早,但 key 刚刚被真实流量使用,应跳过本次探测
selected = _select_probe_key_ids(
keys=keys, # type: ignore[arg-type]
provider_type="codex",
now_ts=now_ts,
interval_seconds=10 * 60,
last_probe_timestamps={"k1": now_ts - (25 * 60)},
limit=0,
)
assert selected == []
def test_select_probe_key_ids_applies_limit_by_oldest_anchor_first() -> None:
now = datetime(2026, 3, 5, 12, 0, 0, tzinfo=timezone.utc)
now_ts = int(now.timestamp())
keys = [
_key("k1", last_used_at=now - timedelta(minutes=60)),
_key("k2", last_used_at=now - timedelta(minutes=50)),
_key("k3", last_used_at=now - timedelta(minutes=40)),
]
selected = _select_probe_key_ids(
keys=keys, # type: ignore[arg-type]
provider_type="codex",
now_ts=now_ts,
interval_seconds=10 * 60,
last_probe_timestamps={},
limit=2,
)
assert selected == ["k1", "k2"]