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']})"
)

View File

@@ -5,14 +5,19 @@ Importing this package registers all built-in preset dimensions.
from __future__ import annotations
from . import cache_affinity # noqa: F401
from . import cost_first # noqa: F401
from . import free_first # noqa: F401
from . import free_team_first # noqa: F401
from . import health_first # noqa: F401
from . import latency_first # noqa: F401
from . import load_balance # noqa: F401
from . import plus_first # noqa: F401
from . import priority_first # noqa: F401
from . import quota_balanced # noqa: F401
from . import recent_refresh # noqa: F401
from . import single_account # noqa: F401
from . import team_first # noqa: F401
from .registry import (
PresetDimensionBase,
PresetDimensionMeta,

View File

@@ -219,7 +219,10 @@ def extract_health_score(key_obj: Any) -> float | None:
def plan_priority_score(plan_type: str | None, mode: str | None = None) -> float:
"""Score a key based on plan type and free_team_first mode."""
"""Score a key based on plan type and scheduling mode.
Lower score = higher priority.
"""
effective_mode = (mode or "both").strip().lower()
if effective_mode == "free_only":
@@ -232,6 +235,11 @@ def plan_priority_score(plan_type: str | None, mode: str | None = None) -> float
return 0.0
if plan_type == "free":
return 0.5
elif effective_mode == "plus_only":
if plan_type in {"plus", "pro"}:
return 0.0
if plan_type in {"enterprise", "business"}:
return 0.3
else:
# "both" or unrecognized -> original behavior
if plan_type in {"free", "team"}:

View File

@@ -0,0 +1,45 @@
"""cache_affinity preset dimension."""
from __future__ import annotations
from typing import Any
from ._helpers import rank_descending
from .registry import PresetDimensionBase, register_preset_dimension
class CacheAffinityDimension(PresetDimensionBase):
@property
def name(self) -> str:
return "cache_affinity"
@property
def label(self) -> str:
return "缓存亲和"
@property
def description(self) -> str:
return "优先复用最近使用过的 Key利用 Prompt Caching"
@property
def mutex_group(self) -> str | None:
return "distribution_mode"
@property
def evidence_hint(self) -> str | None:
return "依据 LRU 时间戳(最近使用优先,与 LRU 轮转相反)"
def compute_metric(
self,
*,
key_id: str,
all_key_ids: list[str],
keys_by_id: dict[str, Any],
lru_scores: dict[str, Any],
context: dict[str, Any],
mode: str | None,
) -> float:
return rank_descending(key_id, lru_scores, all_key_ids)
register_preset_dimension(CacheAffinityDimension())

View File

@@ -0,0 +1,51 @@
"""free_first preset dimension."""
from __future__ import annotations
from typing import Any
from ._helpers import extract_plan_type, plan_priority_score, rank_ascending
from .registry import PresetDimensionBase, register_preset_dimension
class FreeFirstDimension(PresetDimensionBase):
@property
def name(self) -> str:
return "free_first"
@property
def label(self) -> str:
return "Free 优先"
@property
def description(self) -> str:
return "优先消耗 Free 账号(依赖 plan_type"
@property
def evidence_hint(self) -> str | None:
return "依据 plan_typeFree 账号优先调度)"
@property
def providers(self) -> tuple[str, ...]:
return ("codex", "kiro")
def compute_metric(
self,
*,
key_id: str,
all_key_ids: list[str],
keys_by_id: dict[str, Any],
lru_scores: dict[str, Any],
context: dict[str, Any],
mode: str | None,
) -> float:
plan_scores = {
kid: plan_priority_score(extract_plan_type(keys_by_id.get(kid)), "free_only")
for kid in all_key_ids
}
if len(set(plan_scores.values())) <= 1:
return rank_ascending(key_id, lru_scores, all_key_ids)
return rank_ascending(key_id, plan_scores, all_key_ids)
register_preset_dimension(FreeFirstDimension())

View File

@@ -37,6 +37,10 @@ class FreeTeamFirstDimension(PresetDimensionBase):
def default_mode(self) -> str | None:
return "both"
@property
def hidden(self) -> bool:
return True
def compute_metric(
self,
*,

View File

@@ -0,0 +1,45 @@
"""load_balance preset dimension."""
from __future__ import annotations
import random
from typing import Any
from .registry import PresetDimensionBase, register_preset_dimension
class LoadBalanceDimension(PresetDimensionBase):
@property
def name(self) -> str:
return "load_balance"
@property
def label(self) -> str:
return "负载均衡"
@property
def description(self) -> str:
return "随机分散 Key 使用,均匀分摊负载"
@property
def mutex_group(self) -> str | None:
return "distribution_mode"
@property
def evidence_hint(self) -> str | None:
return "每次随机分值,实现完全均匀分散"
def compute_metric(
self,
*,
key_id: str,
all_key_ids: list[str],
keys_by_id: dict[str, Any],
lru_scores: dict[str, Any],
context: dict[str, Any],
mode: str | None,
) -> float:
return random.random()
register_preset_dimension(LoadBalanceDimension())

View File

@@ -0,0 +1,51 @@
"""plus_first preset dimension."""
from __future__ import annotations
from typing import Any
from ._helpers import extract_plan_type, plan_priority_score, rank_ascending
from .registry import PresetDimensionBase, register_preset_dimension
class PlusFirstDimension(PresetDimensionBase):
@property
def name(self) -> str:
return "plus_first"
@property
def label(self) -> str:
return "Plus 优先"
@property
def description(self) -> str:
return "优先消耗 Plus/Pro 账号(依赖 plan_type"
@property
def evidence_hint(self) -> str | None:
return "依据 plan_typePlus/Pro 账号优先调度)"
@property
def providers(self) -> tuple[str, ...]:
return ("codex", "kiro")
def compute_metric(
self,
*,
key_id: str,
all_key_ids: list[str],
keys_by_id: dict[str, Any],
lru_scores: dict[str, Any],
context: dict[str, Any],
mode: str | None,
) -> float:
plan_scores = {
kid: plan_priority_score(extract_plan_type(keys_by_id.get(kid)), "plus_only")
for kid in all_key_ids
}
if len(set(plan_scores.values())) <= 1:
return rank_ascending(key_id, lru_scores, all_key_ids)
return rank_ascending(key_id, plan_scores, all_key_ids)
register_preset_dimension(PlusFirstDimension())

View File

@@ -76,6 +76,16 @@ class PresetDimensionBase(ABC):
return None
@property
def hidden(self) -> bool:
"""If True, this dimension is excluded from API metadata listings.
The dimension remains functional for backward compatibility but
will not appear in the scheduling dialog.
"""
return False
@abstractmethod
def compute_metric(
self,
@@ -170,6 +180,10 @@ def register_preset_dimension(dim: PresetDimensionBase) -> None:
raw = str(self._wrapped.evidence_hint or "").strip()
return raw or None
@property
def hidden(self) -> bool:
return self._wrapped.hidden
def compute_metric(
self,
*,
@@ -223,6 +237,8 @@ def get_preset_dimension_metas() -> list[PresetDimensionMeta]:
metas: list[PresetDimensionMeta] = []
for dim in get_all_preset_dimensions():
if dim.hidden:
continue
metas.append(
PresetDimensionMeta(
name=dim.name,

View File

@@ -0,0 +1,51 @@
"""team_first preset dimension."""
from __future__ import annotations
from typing import Any
from ._helpers import extract_plan_type, plan_priority_score, rank_ascending
from .registry import PresetDimensionBase, register_preset_dimension
class TeamFirstDimension(PresetDimensionBase):
@property
def name(self) -> str:
return "team_first"
@property
def label(self) -> str:
return "Team 优先"
@property
def description(self) -> str:
return "优先消耗 Team 账号(依赖 plan_type"
@property
def evidence_hint(self) -> str | None:
return "依据 plan_typeTeam 账号优先调度)"
@property
def providers(self) -> tuple[str, ...]:
return ("codex", "kiro")
def compute_metric(
self,
*,
key_id: str,
all_key_ids: list[str],
keys_by_id: dict[str, Any],
lru_scores: dict[str, Any],
context: dict[str, Any],
mode: str | None,
) -> float:
plan_scores = {
kid: plan_priority_score(extract_plan_type(keys_by_id.get(kid)), "team_only")
for kid in all_key_ids
}
if len(set(plan_scores.values())) <= 1:
return rank_ascending(key_id, lru_scores, all_key_ids)
return rank_ascending(key_id, plan_scores, all_key_ids)
register_preset_dimension(TeamFirstDimension())