feat(pool,admin): 拆分调度维度、重构调度 UI 与增强配置导入导出

调度维度:
- 新增 cache_affinity/free_first/team_first/plus_first/load_balance 五个独立维度
- 将 free_team_first 标记为 hidden,保留后向兼容但不再显示
- Registry 新增 hidden 属性,_helpers 新增 plus_only 优先级评分

前端调度对话框:
- 分配模式(互斥组)独立为按钮组选择,策略调度保留拖拽排序
- 默认调度从 LRU 轮转改为缓存亲和
- 提取 buildPresetListItem/insertMissingByPreferredOrder 消除重复代码

配置导入导出:
- 导出时 api_formats 支持规范化、去重与 None 回退到 Provider 端点
- 导入时兼容 supported_endpoints 别名与历史 None 语义
- 新增 test_admin_system_key_formats 单元测试
This commit is contained in:
fawney19
2026-03-05 23:22:25 +08:00
parent da915208a8
commit 228cbc8f87
12 changed files with 667 additions and 229 deletions

View File

@@ -910,6 +910,67 @@ class AdminExportConfigAdapter(AdminApiAdapter):
"cookie",
}
@staticmethod
def _normalize_api_formats(raw_formats: Any) -> list[str]:
"""规范化 api_formats 为 endpoint signature 列表。"""
from src.core.api_format.signature import normalize_signature_key
if not isinstance(raw_formats, list):
return []
normalized: list[str] = []
seen: set[str] = set()
for raw in raw_formats:
if not isinstance(raw, str):
continue
value = raw.strip()
if not value:
continue
try:
fmt = normalize_signature_key(value)
except Exception:
continue
if fmt in seen:
continue
seen.add(fmt)
normalized.append(fmt)
return normalized
def _resolve_export_key_api_formats(
self, raw_formats: Any, provider_endpoint_formats: list[str]
) -> list[str]:
"""导出 Key 时解析支持端点:
- 优先使用 Key 自身 api_formats规范化后
- 当 api_formats 为 None历史语义全支持时回退为 Provider 端点列表
- 当 api_formats 显式为空列表时保留空列表
"""
normalized = self._normalize_api_formats(raw_formats)
if normalized:
return normalized
if raw_formats is None:
return list(provider_endpoint_formats)
return []
def _collect_provider_endpoint_formats(self, endpoints: list[Any]) -> list[str]:
"""收集 Provider 下所有 endpoint signature去重后排序"""
normalized: list[str] = []
seen: set[str] = set()
for ep in endpoints:
raw = getattr(ep, "api_format", None)
if hasattr(raw, "value"):
raw = raw.value
fmt_list = self._normalize_api_formats([raw])
if not fmt_list:
continue
fmt = fmt_list[0]
if fmt in seen:
continue
seen.add(fmt)
normalized.append(fmt)
return sorted(normalized)
def _decrypt_provider_config(self, config: dict, crypto_service: Any) -> dict:
"""解密 Provider config 中的 provider_ops credentials"""
if not config:
@@ -959,6 +1020,7 @@ class AdminExportConfigAdapter(AdminApiAdapter):
db.query(ProviderEndpoint).filter(ProviderEndpoint.provider_id == provider.id).all()
)
endpoints_data = [ep.to_export_dict() for ep in endpoints]
provider_endpoint_formats = self._collect_provider_endpoint_formats(endpoints)
# 导出 Provider Keys按 provider_id 归属,包含 api_formats
keys = (
@@ -970,6 +1032,13 @@ class AdminExportConfigAdapter(AdminApiAdapter):
keys_data = []
for key in keys:
key_data = key.to_export_dict()
key_formats = self._resolve_export_key_api_formats(
key_data.get("api_formats"),
provider_endpoint_formats,
)
# 保持现有字段名 api_formats并补充可读别名 supported_endpoints。
key_data["api_formats"] = key_formats
key_data["supported_endpoints"] = list(key_formats)
# 解密 API Key
try:
key_data["api_key"] = crypto_service.decrypt(key.api_key)
@@ -1122,6 +1191,29 @@ class AdminImportConfigAdapter(AdminApiAdapter):
"cookie",
}
@staticmethod
def _extract_import_key_api_formats(
key_data: dict[str, Any], endpoint_formats: set[str]
) -> list[str]:
"""导入 Key 时提取 api_formats兼容历史字段与旧语义"""
raw_formats = key_data.get("api_formats")
if isinstance(raw_formats, list):
if raw_formats:
return raw_formats
legacy_formats = key_data.get("supported_endpoints")
if isinstance(legacy_formats, list) and legacy_formats:
return legacy_formats
return []
legacy_formats = key_data.get("supported_endpoints")
if isinstance(legacy_formats, list) and legacy_formats:
return legacy_formats
# 兼容历史数据api_formats=None 代表支持 Provider 的全部端点。
if raw_formats is None and endpoint_formats:
return sorted(endpoint_formats)
return []
def _encrypt_provider_config(self, config: dict, crypto_service: Any) -> dict:
"""加密 Provider config 中的 provider_ops credentials"""
if not config:
@@ -1436,8 +1528,8 @@ class AdminImportConfigAdapter(AdminApiAdapter):
stats["keys"]["skipped"] += 1
continue
raw_formats = key_data.get("api_formats") or []
if not isinstance(raw_formats, list) or len(raw_formats) == 0:
raw_formats = self._extract_import_key_api_formats(key_data, endpoint_formats)
if len(raw_formats) == 0:
stats["errors"].append(
f"跳过无 api_formats 的 Key (Provider: {prov_data['name']})"
)