mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(pool,priority): 号池聚合显示、API 格式归一化与优先级管理重构
- 优先级管理对话框按 family 分组显示 API 格式,号池 key 聚合为单条目展示 - 拖拽排序改用 key ID 替代数组索引,号池聚合项禁用拖拽/编辑/开关操作 - 后端 key 分组查询增加 API 格式键归一化,返回 provider_id - 提取 OAuth auth_config 解密逻辑,新增 _derive_oauth_expires_at 从加密配置派生过期时间 - 号池管理移除会话列,调整 OAuth 过期信息与刷新按钮的布局顺序
This commit is contained in:
@@ -414,7 +414,60 @@ def _normalize_oauth_plan_type(plan_type: Any, provider_type: str) -> str | None
|
||||
return text or None
|
||||
|
||||
|
||||
def _derive_oauth_plan_type(key: ProviderAPIKey, provider_type: str) -> str | None:
|
||||
def _extract_oauth_auth_config(key: ProviderAPIKey) -> dict[str, Any] | None:
|
||||
if str(getattr(key, "auth_type", "") or "").strip().lower() != "oauth":
|
||||
return None
|
||||
|
||||
auth_config_raw = getattr(key, "auth_config", None)
|
||||
if not auth_config_raw:
|
||||
return None
|
||||
|
||||
try:
|
||||
decrypted = crypto_service.decrypt(auth_config_raw)
|
||||
parsed = json.loads(decrypted)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_oauth_expires_at(raw: Any) -> int | None:
|
||||
value = _to_float(raw)
|
||||
if value is None or value <= 0:
|
||||
return None
|
||||
# 兼容毫秒时间戳
|
||||
if value > 1_000_000_000_000:
|
||||
value /= 1000
|
||||
return int(value)
|
||||
|
||||
|
||||
def _derive_oauth_expires_at(
|
||||
key: ProviderAPIKey, auth_config: dict[str, Any] | None = None
|
||||
) -> int | None:
|
||||
if str(getattr(key, "auth_type", "") or "").strip().lower() != "oauth":
|
||||
return None
|
||||
|
||||
cfg = auth_config if isinstance(auth_config, dict) else _extract_oauth_auth_config(key)
|
||||
if cfg:
|
||||
for field in ("expires_at", "expiresAt", "expiry", "exp"):
|
||||
expires_at = _normalize_oauth_expires_at(cfg.get(field))
|
||||
if expires_at is not None:
|
||||
return expires_at
|
||||
|
||||
# 兼容历史字段
|
||||
expires_dt = getattr(key, "expires_at", None)
|
||||
if isinstance(expires_dt, datetime):
|
||||
return int(expires_dt.timestamp())
|
||||
return None
|
||||
|
||||
|
||||
def _derive_oauth_plan_type(
|
||||
key: ProviderAPIKey,
|
||||
provider_type: str,
|
||||
auth_config: dict[str, Any] | None = None,
|
||||
) -> str | None:
|
||||
# Prefer persisted normalized field
|
||||
persisted = _normalize_oauth_plan_type(getattr(key, "oauth_plan_type", None), provider_type)
|
||||
if persisted:
|
||||
@@ -424,20 +477,12 @@ def _derive_oauth_plan_type(key: ProviderAPIKey, provider_type: str) -> str | No
|
||||
return None
|
||||
|
||||
# Fallback 1: encrypted auth_config (common for Codex/Antigravity)
|
||||
auth_config_raw = getattr(key, "auth_config", None)
|
||||
if auth_config_raw:
|
||||
try:
|
||||
decrypted = crypto_service.decrypt(auth_config_raw)
|
||||
auth_config = json.loads(decrypted)
|
||||
if isinstance(auth_config, dict):
|
||||
for plan_key in ("plan_type", "tier", "plan", "subscription_plan"):
|
||||
normalized = _normalize_oauth_plan_type(
|
||||
auth_config.get(plan_key), provider_type
|
||||
)
|
||||
if normalized:
|
||||
return normalized
|
||||
except Exception:
|
||||
pass
|
||||
cfg = auth_config if isinstance(auth_config, dict) else _extract_oauth_auth_config(key)
|
||||
if cfg:
|
||||
for plan_key in ("plan_type", "tier", "plan", "subscription_plan"):
|
||||
normalized = _normalize_oauth_plan_type(cfg.get(plan_key), provider_type)
|
||||
if normalized:
|
||||
return normalized
|
||||
|
||||
# Fallback 2: upstream_metadata
|
||||
upstream_metadata = getattr(key, "upstream_metadata", None)
|
||||
@@ -902,6 +947,7 @@ class AdminListPoolKeysAdapter(AdminApiAdapter):
|
||||
key_last_used_at = getattr(k, "last_used_at", None) or key_usage_stats.get(
|
||||
"last_used_at"
|
||||
)
|
||||
oauth_auth_config = _extract_oauth_auth_config(k)
|
||||
|
||||
key_details.append(
|
||||
PoolKeyDetail(
|
||||
@@ -909,10 +955,8 @@ class AdminListPoolKeysAdapter(AdminApiAdapter):
|
||||
key_name=k.name or "",
|
||||
is_active=bool(k.is_active),
|
||||
auth_type=str(getattr(k, "auth_type", "api_key") or "api_key"),
|
||||
oauth_expires_at=(
|
||||
int(k.oauth_expires_at.timestamp())
|
||||
if getattr(k, "oauth_expires_at", None)
|
||||
else None
|
||||
oauth_expires_at=_derive_oauth_expires_at(
|
||||
k, auth_config=oauth_auth_config
|
||||
),
|
||||
oauth_invalid_at=(
|
||||
int(k.oauth_invalid_at.timestamp())
|
||||
@@ -920,7 +964,9 @@ class AdminListPoolKeysAdapter(AdminApiAdapter):
|
||||
else None
|
||||
),
|
||||
oauth_invalid_reason=getattr(k, "oauth_invalid_reason", None),
|
||||
oauth_plan_type=_derive_oauth_plan_type(k, provider_type),
|
||||
oauth_plan_type=_derive_oauth_plan_type(
|
||||
k, provider_type, auth_config=oauth_auth_config
|
||||
),
|
||||
quota_updated_at=_extract_quota_updated_at(
|
||||
provider_type,
|
||||
getattr(k, "upstream_metadata", None),
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.api_format.signature import normalize_signature_key
|
||||
from src.core.crypto import crypto_service
|
||||
from src.core.exceptions import InvalidRequestException, NotFoundException
|
||||
from src.core.key_capabilities import get_capability
|
||||
@@ -19,6 +20,47 @@ from src.models.endpoint_models import EndpointAPIKeyResponse
|
||||
from src.services.provider_keys.auth_type import normalize_auth_type
|
||||
from src.services.provider_keys.response_builder import build_key_response
|
||||
|
||||
_LEGACY_API_FORMAT_MAP: dict[str, str] = {
|
||||
"CLAUDE": "claude:chat",
|
||||
"CLAUDE_CLI": "claude:cli",
|
||||
"OPENAI": "openai:chat",
|
||||
"OPENAI_CLI": "openai:cli",
|
||||
"OPENAI_COMPACT": "openai:compact",
|
||||
"OPENAI_VIDEO": "openai:video",
|
||||
"GEMINI": "gemini:chat",
|
||||
"GEMINI_CLI": "gemini:cli",
|
||||
"GEMINI_VIDEO": "gemini:video",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_api_format_key(raw_format: Any) -> str | None:
|
||||
"""Normalize api_format to canonical signature key; keeps legacy import compatibility."""
|
||||
text = str(raw_format or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
|
||||
try:
|
||||
return normalize_signature_key(text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
legacy = text.upper().replace("-", "_")
|
||||
return _LEGACY_API_FORMAT_MAP.get(legacy)
|
||||
|
||||
|
||||
def _normalize_format_dict(raw_dict: Any) -> dict[str, Any]:
|
||||
"""Normalize dict keys from any format aliases to canonical api_format."""
|
||||
if not isinstance(raw_dict, dict):
|
||||
return {}
|
||||
|
||||
normalized: dict[str, Any] = {}
|
||||
for raw_key, value in raw_dict.items():
|
||||
format_key = _normalize_api_format_key(raw_key)
|
||||
if not format_key or format_key in normalized:
|
||||
continue
|
||||
normalized[format_key] = value
|
||||
return normalized
|
||||
|
||||
|
||||
def get_keys_grouped_by_format(db: Session) -> dict:
|
||||
"""查询所有 Key,并按 API 格式分组返回。"""
|
||||
@@ -49,11 +91,22 @@ def get_keys_grouped_by_format(db: Session) -> dict:
|
||||
endpoint_base_url_map: dict[tuple[str, str], str] = {}
|
||||
for provider_id, api_format, base_url in endpoints:
|
||||
fmt = api_format.value if hasattr(api_format, "value") else str(api_format)
|
||||
endpoint_base_url_map[(str(provider_id), fmt)] = base_url
|
||||
normalized_fmt = _normalize_api_format_key(fmt)
|
||||
if not normalized_fmt:
|
||||
continue
|
||||
endpoint_base_url_map[(str(provider_id), normalized_fmt)] = base_url
|
||||
|
||||
grouped: dict[str, list[dict]] = {}
|
||||
for key, provider in keys:
|
||||
api_formats = key.api_formats or []
|
||||
raw_api_formats = key.api_formats or []
|
||||
api_formats: list[str] = []
|
||||
seen_formats: set[str] = set()
|
||||
for raw_format in raw_api_formats:
|
||||
normalized_format = _normalize_api_format_key(raw_format)
|
||||
if not normalized_format or normalized_format in seen_formats:
|
||||
continue
|
||||
seen_formats.add(normalized_format)
|
||||
api_formats.append(normalized_format)
|
||||
|
||||
if not api_formats:
|
||||
continue # 跳过没有 API 格式的 Key
|
||||
@@ -88,14 +141,17 @@ def get_keys_grouped_by_format(db: Session) -> dict:
|
||||
caps_list.append(cap_def.short_name if cap_def else cap_name)
|
||||
|
||||
# 构建 Key 信息(基础数据)
|
||||
normalized_rate_multipliers = _normalize_format_dict(key.rate_multipliers)
|
||||
normalized_priority_by_format = _normalize_format_dict(key.global_priority_by_format)
|
||||
key_info = {
|
||||
"id": key.id,
|
||||
"provider_id": str(provider.id),
|
||||
"name": key.name,
|
||||
"auth_type": auth_type,
|
||||
"api_key_masked": masked_key,
|
||||
"internal_priority": key.internal_priority,
|
||||
"global_priority_by_format": key.global_priority_by_format,
|
||||
"rate_multipliers": key.rate_multipliers,
|
||||
"global_priority_by_format": normalized_priority_by_format,
|
||||
"rate_multipliers": normalized_rate_multipliers or None,
|
||||
"is_active": key.is_active,
|
||||
"provider_active": provider.is_active,
|
||||
"provider_name": provider.name,
|
||||
@@ -107,9 +163,14 @@ def get_keys_grouped_by_format(db: Session) -> dict:
|
||||
}
|
||||
|
||||
# 将 Key 添加到每个支持的格式分组中,并附加格式特定的数据
|
||||
health_by_format = key.health_by_format or {}
|
||||
circuit_by_format = key.circuit_breaker_by_format or {}
|
||||
priority_by_format = key.global_priority_by_format or {}
|
||||
health_by_format = _normalize_format_dict(key.health_by_format)
|
||||
circuit_by_format = _normalize_format_dict(key.circuit_breaker_by_format)
|
||||
priority_by_format: dict[str, int] = {}
|
||||
for k, v in normalized_priority_by_format.items():
|
||||
try:
|
||||
priority_by_format[k] = int(v)
|
||||
except Exception:
|
||||
continue
|
||||
provider_id = str(provider.id)
|
||||
for api_format in api_formats:
|
||||
if api_format not in grouped:
|
||||
|
||||
Reference in New Issue
Block a user