feat: Provider 异步删除、可配置密码策略、Hub 超时优化及多项改进

- 新增 Provider 异步删除任务系统,后台分阶段删除子资源并清理残留引用
- 新增可配置密码策略等级(weak/medium/strong),支持系统设置面板调整
- aether-hub 升级至 0.1.4,idle timeout 支持禁用(设为 0),worker 默认超时调整为 120s
- OAuth 手动续期增加 Redis 分布式锁,防止并发刷新冲突
- ProxyNode 心跳检测改为 asyncio.to_thread,避免阻塞事件循环
- 删除 ModelMultiSelect 和 useInvalidModels,MultiSelect 组件通用化
- 明确 allowed_providers/allowed_api_formats 的 NULL 与空数组语义
- 前端 StandaloneKeyFormDialog、UserFormDialog 等多处 UI 优化
- 新增 Alembic 迁移脚本清理 Provider 删除后的残留引用
- 补充相关测试用例
This commit is contained in:
fawney19
2026-03-12 01:11:35 +08:00
parent 0d770d1c4d
commit 6e51a3f45d
55 changed files with 3219 additions and 862 deletions

View File

@@ -0,0 +1,109 @@
from __future__ import annotations
import sys
import types
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any
import pytest
from src.services.provider import auth as module
class _FakeQuery:
def __init__(self, row: Any | None) -> None:
self._row = row
def filter(self, *_args: Any, **_kwargs: Any) -> "_FakeQuery":
return self
def first(self) -> Any | None:
return self._row
class _FakeDB:
def __init__(self, row: Any | None) -> None:
self.row = row
self.committed = False
def query(self, _model: Any) -> _FakeQuery:
return _FakeQuery(self.row)
def commit(self) -> None:
self.committed = True
class _FakeSessionCtx:
def __init__(self, db: _FakeDB) -> None:
self.db = db
def __enter__(self) -> _FakeDB:
return self.db
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
_ = exc_type, exc, tb
return False
def _install_module(monkeypatch: pytest.MonkeyPatch, name: str, attrs: dict[str, Any]) -> None:
fake_module = types.ModuleType(name)
for key, value in attrs.items():
setattr(fake_module, key, value)
monkeypatch.setitem(sys.modules, name, fake_module)
def test_persist_refreshed_token_detached_key_does_not_raise(
monkeypatch: pytest.MonkeyPatch,
) -> None:
key = SimpleNamespace(
id="key-1",
api_key="old-api",
auth_config="old-config",
oauth_invalid_at=datetime.now(timezone.utc),
oauth_invalid_reason="[REFRESH_FAILED] stale",
)
monkeypatch.setattr(
module, "object_session", lambda _key: (_ for _ in ()).throw(RuntimeError())
)
monkeypatch.setattr(module.crypto_service, "encrypt", lambda value: f"enc:{value}")
module._persist_refreshed_token(key, "new-token", {"refresh_token": "rt-2"})
assert key.api_key == "enc:new-token"
assert key.auth_config == 'enc:{"refresh_token": "rt-2"}'
assert key.oauth_invalid_at is None
assert key.oauth_invalid_reason is None
def test_mark_refresh_token_invalid_persists_detached_key(
monkeypatch: pytest.MonkeyPatch,
) -> None:
key = SimpleNamespace(id="key-1")
row = SimpleNamespace(id="key-1", oauth_invalid_at=None, oauth_invalid_reason=None)
fake_db = _FakeDB(row)
monkeypatch.setattr(
module, "object_session", lambda _key: (_ for _ in ()).throw(RuntimeError())
)
_install_module(
monkeypatch, "src.database", {"create_session": lambda: _FakeSessionCtx(fake_db)}
)
_install_module(
monkeypatch,
"src.models.database",
{"ProviderAPIKey": type("ProviderAPIKey", (), {"id": "id"})},
)
module._mark_refresh_token_invalid(
key,
401,
'{"error": {"code": "refresh_token_reused", "message": "used"}}',
)
assert fake_db.committed is True
assert key.oauth_invalid_at is not None
assert row.oauth_invalid_at is not None
assert str(key.oauth_invalid_reason).startswith("[REFRESH_FAILED] Token 续期失败 (401)")
assert "refresh_token_reused" in str(row.oauth_invalid_reason)