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

@@ -50,6 +50,7 @@ class _FakeDB:
self._provider = provider
self._keys = keys
self.added: list[object] = []
self.deleted: list[object] = []
self.commit_count = 0
def query(self, model: Any) -> _FakeQuery:
@@ -63,6 +64,9 @@ class _FakeDB:
def add(self, obj: object) -> None:
self.added.append(obj)
def delete(self, obj: object) -> None:
self.deleted.append(obj)
def commit(self) -> None:
self.commit_count += 1
@@ -148,6 +152,7 @@ async def test_refresh_provider_quota_no_active_keys_returns_empty() -> None:
"total": 0,
"results": [],
"message": "没有可刷新的 Key",
"auto_removed": 0,
}
@@ -174,6 +179,7 @@ async def test_refresh_provider_quota_empty_key_ids_returns_empty() -> None:
"total": 0,
"results": [],
"message": "未提供可刷新的 Key",
"auto_removed": 0,
}
@@ -324,3 +330,84 @@ async def test_refresh_provider_quota_handler_exception_returns_error(
assert result["total"] == 1
assert result["results"][0]["status"] == "error"
assert "unit-test boom" in result["results"][0]["message"]
@pytest.mark.asyncio
async def test_refresh_provider_quota_auto_removes_banned_keys_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
provider = SimpleNamespace(
id="p1",
provider_type=ProviderType.CODEX,
endpoints=[SimpleNamespace(api_format="openai:cli", is_active=True)],
config={"pool_advanced": {"auto_remove_banned_keys": True}},
)
key1 = SimpleNamespace(
id="k1",
name="K1",
provider_id="p1",
allowed_models=["gpt-4o"],
upstream_metadata={},
is_active=True,
oauth_invalid_reason=None,
)
key2 = SimpleNamespace(
id="k2",
name="K2",
provider_id="p1",
allowed_models=None,
upstream_metadata={},
is_active=True,
oauth_invalid_reason=None,
)
db = _FakeDB(provider=provider, keys=[key1, key2])
deleted_side_effect_calls: list[tuple[str | None, list[str] | None]] = []
redis_cleared: list[tuple[str, str]] = []
async def _fake_handler(**kwargs: Any) -> dict[str, Any]:
key = kwargs["key"]
state_updates = kwargs["state_updates"]
if key.id == "k1":
state_updates[key.id] = {"is_active": False, "oauth_invalid_reason": "账户已封禁: test"}
return {"key_id": key.id, "key_name": key.name, "status": "banned"}
return {"key_id": key.id, "key_name": key.name, "status": "success"}
async def _fake_run_delete_side_effects(
*,
db: Any,
provider_id: str | None,
deleted_key_allowed_models: list[str] | None,
) -> None:
_ = db
deleted_side_effect_calls.append((provider_id, deleted_key_allowed_models))
async def _fake_clear(provider_id: str, key_id: str) -> None:
redis_cleared.append((provider_id, key_id))
monkeypatch.setattr(
quota_service_module,
"_select_refresh_endpoint",
lambda provider, provider_type: provider.endpoints[0],
)
monkeypatch.setattr(
quota_service_module, "_resolve_quota_refresh_handler", lambda _: _fake_handler
)
monkeypatch.setattr(
quota_service_module, "run_delete_key_side_effects", _fake_run_delete_side_effects
)
monkeypatch.setattr(quota_service_module.pool_redis, "clear_cooldown", _fake_clear)
monkeypatch.setattr(quota_service_module.pool_redis, "clear_cost", _fake_clear)
result = await refresh_provider_quota_for_provider(
db=cast(Any, db),
provider_id="p1",
codex_wham_usage_url="https://example.test/wham/usage",
)
assert result["auto_removed"] == 1
assert db.commit_count == 1
assert db.deleted == [key1]
assert key2 not in db.deleted
assert result["results"][0]["auto_removed"] is True
assert deleted_side_effect_calls == [("p1", ["gpt-4o"])]
assert redis_cleared == [("p1", "k1"), ("p1", "k1")]