mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: Key 删除时自动解除不再匹配的 GlobalModel 关联
- 新增 auto_disassociate_provider_by_key_whitelist 方法检测并解除关联 - 删除 Key 后检查 Provider 剩余 Key 白名单,移除不再匹配的 Model - 前端删除密钥后并行刷新端点列表、模型列表和模型映射 Closes #102 Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -779,7 +779,12 @@ async function confirmDeleteKey() {
|
|||||||
try {
|
try {
|
||||||
await deleteEndpointKey(keyId)
|
await deleteEndpointKey(keyId)
|
||||||
showSuccess('密钥已删除')
|
showSuccess('密钥已删除')
|
||||||
await loadEndpoints()
|
// 并行刷新:端点列表、模型列表、模型映射(删除 Key 触发自动解除模型关联)
|
||||||
|
await Promise.all([
|
||||||
|
loadEndpoints(),
|
||||||
|
modelsTabRef.value?.reload(),
|
||||||
|
modelMappingTabRef.value?.reload()
|
||||||
|
])
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showError(err.response?.data?.detail || '删除密钥失败', '错误')
|
showError(err.response?.data?.detail || '删除密钥失败', '错误')
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ class AdminDeleteEndpointKeyAdapter(AdminApiAdapter):
|
|||||||
raise NotFoundException(f"Key {self.key_id} 不存在")
|
raise NotFoundException(f"Key {self.key_id} 不存在")
|
||||||
|
|
||||||
provider_id = key.provider_id
|
provider_id = key.provider_id
|
||||||
|
deleted_key_allowed_models = key.allowed_models # 保存被删除 Key 的 allowed_models
|
||||||
try:
|
try:
|
||||||
db.delete(key)
|
db.delete(key)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -353,8 +354,20 @@ class AdminDeleteEndpointKeyAdapter(AdminApiAdapter):
|
|||||||
logger.error(f"删除 Key 失败: ID={self.key_id}, Error={exc}")
|
logger.error(f"删除 Key 失败: ID={self.key_id}, Error={exc}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# 清除 /v1/models 列表缓存
|
# 触发缓存失效和自动解除关联检查
|
||||||
await invalidate_models_list_cache()
|
# 注意:只有当被删除的 Key 有具体的 allowed_models 时才触发 disassociate
|
||||||
|
# 如果 allowed_models 为 null(允许所有模型),则不需要检查解除关联
|
||||||
|
if provider_id:
|
||||||
|
from src.services.model.global_model import on_key_allowed_models_changed
|
||||||
|
|
||||||
|
await on_key_allowed_models_changed(
|
||||||
|
db=db,
|
||||||
|
provider_id=provider_id,
|
||||||
|
skip_disassociate=deleted_key_allowed_models is None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 无 provider_id 时仅清除缓存
|
||||||
|
await invalidate_models_list_cache()
|
||||||
|
|
||||||
logger.warning(f"[DELETE] 删除 Key: ID={self.key_id}, Provider={provider_id}")
|
logger.warning(f"[DELETE] 删除 Key: ID={self.key_id}, Provider={provider_id}")
|
||||||
return {"message": f"Key {self.key_id} 已删除"}
|
return {"message": f"Key {self.key_id} 已删除"}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ GlobalModel 服务层
|
|||||||
提供 GlobalModel 的 CRUD 操作、查询和统计功能
|
提供 GlobalModel 的 CRUD 操作、查询和统计功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, List, Optional, Set
|
from typing import Dict, List, Optional, Set, cast
|
||||||
|
|
||||||
from sqlalchemy import and_, func
|
from sqlalchemy import and_, func
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
@@ -18,19 +18,26 @@ from src.models.pydantic_models import GlobalModelUpdate
|
|||||||
async def on_key_allowed_models_changed(
|
async def on_key_allowed_models_changed(
|
||||||
db: Session,
|
db: Session,
|
||||||
provider_id: str,
|
provider_id: str,
|
||||||
allowed_models: List[str],
|
allowed_models: Optional[List[str]] = None,
|
||||||
|
skip_disassociate: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Key 的 allowed_models 变更后的统一处理
|
Key 的 allowed_models 变更后的统一处理
|
||||||
|
|
||||||
包括:
|
包括:
|
||||||
1. 触发缓存失效(包括 /v1/models 列表缓存)
|
1. 触发缓存失效(包括 /v1/models 列表缓存)
|
||||||
2. 检查并自动关联匹配的 GlobalModel
|
2. 检查并自动关联匹配的 GlobalModel(仅当提供 allowed_models 时)
|
||||||
|
3. 检查并自动解除不再匹配的 GlobalModel 关联(可通过 skip_disassociate 跳过)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: 数据库 Session
|
db: 数据库 Session
|
||||||
provider_id: Provider ID
|
provider_id: Provider ID
|
||||||
allowed_models: 更新后的 allowed_models 列表
|
allowed_models: 更新后的 allowed_models 列表
|
||||||
|
- 提供非空列表:触发自动关联和解除关联检查
|
||||||
|
- 提供空列表或 None:仅触发解除关联检查(用于 Key 删除场景)
|
||||||
|
skip_disassociate: 是否跳过解除关联检查
|
||||||
|
- True:跳过(用于删除 allowed_models 为 null 的 Key 时)
|
||||||
|
- False:执行检查(默认)
|
||||||
"""
|
"""
|
||||||
from src.services.cache.invalidation import get_cache_invalidation_service
|
from src.services.cache.invalidation import get_cache_invalidation_service
|
||||||
|
|
||||||
@@ -38,7 +45,7 @@ async def on_key_allowed_models_changed(
|
|||||||
cache_service = get_cache_invalidation_service()
|
cache_service = get_cache_invalidation_service()
|
||||||
await cache_service.on_key_allowed_models_changed(provider_id)
|
await cache_service.on_key_allowed_models_changed(provider_id)
|
||||||
|
|
||||||
# 2. 检查并自动关联 GlobalModel
|
# 2. 检查并自动关联 GlobalModel(仅当提供非空 allowed_models 时)
|
||||||
if allowed_models:
|
if allowed_models:
|
||||||
GlobalModelService.auto_associate_provider_by_key_whitelist(
|
GlobalModelService.auto_associate_provider_by_key_whitelist(
|
||||||
db=db,
|
db=db,
|
||||||
@@ -46,6 +53,13 @@ async def on_key_allowed_models_changed(
|
|||||||
allowed_models=allowed_models,
|
allowed_models=allowed_models,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 3. 检查并自动解除不再匹配的 GlobalModel 关联
|
||||||
|
if not skip_disassociate:
|
||||||
|
GlobalModelService.auto_disassociate_provider_by_key_whitelist(
|
||||||
|
db=db,
|
||||||
|
provider_id=provider_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GlobalModelService:
|
class GlobalModelService:
|
||||||
"""GlobalModel 服务"""
|
"""GlobalModel 服务"""
|
||||||
@@ -411,7 +425,7 @@ class GlobalModelService:
|
|||||||
if not matched:
|
if not matched:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 自动创建关联
|
# 自动创建关联(逐个处理,允许部分成功)
|
||||||
try:
|
try:
|
||||||
new_model = Model(
|
new_model = Model(
|
||||||
provider_id=provider_id,
|
provider_id=provider_id,
|
||||||
@@ -453,3 +467,146 @@ class GlobalModelService:
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def auto_disassociate_provider_by_key_whitelist(
|
||||||
|
db: Session,
|
||||||
|
provider_id: str,
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
根据 Key 白名单自动解除 Provider 与不再匹配的 GlobalModel 的关联
|
||||||
|
|
||||||
|
当 Key 的 allowed_models 更新后调用此方法,检查所有已关联的 GlobalModel,
|
||||||
|
如果其映射规则不再匹配任何 Key 白名单中的模型,则自动删除关联。
|
||||||
|
|
||||||
|
注意:只删除通过映射规则自动关联的 Model(即 GlobalModel 有 model_mappings 配置的)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: 数据库 Session
|
||||||
|
provider_id: Provider ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 包含 success 和 errors 列表
|
||||||
|
"""
|
||||||
|
from src.core.model_permissions import match_model_with_pattern
|
||||||
|
from src.models.database import Provider, ProviderAPIKey
|
||||||
|
|
||||||
|
results: Dict[str, List[Dict]] = {
|
||||||
|
"success": [],
|
||||||
|
"errors": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取 Provider
|
||||||
|
provider = db.query(Provider).filter(Provider.id == provider_id).first()
|
||||||
|
if not provider:
|
||||||
|
logger.warning(f"Provider {provider_id} not found for auto-disassociation")
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 1. 获取 Provider 下所有活跃 Key 的 allowed_models 并集
|
||||||
|
keys = (
|
||||||
|
db.query(ProviderAPIKey)
|
||||||
|
.filter(
|
||||||
|
ProviderAPIKey.provider_id == provider_id,
|
||||||
|
ProviderAPIKey.is_active == True,
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 收集所有 Key 的 allowed_models 并集
|
||||||
|
# 注意:allowed_models 为 null 表示允许所有模型,此时不应解除任何关联
|
||||||
|
all_allowed_models: Set[str] = set()
|
||||||
|
has_unlimited_key = False # 是否存在允许所有模型的 Key
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
if key.allowed_models is None:
|
||||||
|
# null 表示允许所有模型,直接返回不做任何解除
|
||||||
|
has_unlimited_key = True
|
||||||
|
break
|
||||||
|
if key.allowed_models:
|
||||||
|
all_allowed_models.update(key.allowed_models)
|
||||||
|
|
||||||
|
# 如果存在允许所有模型的 Key,不需要解除任何关联
|
||||||
|
if has_unlimited_key:
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 如果 Provider 无活跃 Key,不做任何解除(保留现有关联)
|
||||||
|
if not keys:
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 2. 获取 Provider 当前关联的所有 Model(带 GlobalModel 信息)
|
||||||
|
models = (
|
||||||
|
db.query(Model)
|
||||||
|
.options(joinedload(Model.global_model))
|
||||||
|
.filter(Model.provider_id == provider_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 检查每个 Model 是否还能匹配,收集需要删除的 Model
|
||||||
|
models_to_delete: List[Model] = []
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
# 跳过没有关联 GlobalModel 的
|
||||||
|
if not model.global_model_id or not model.global_model:
|
||||||
|
continue
|
||||||
|
|
||||||
|
global_model = cast(GlobalModel, model.global_model)
|
||||||
|
|
||||||
|
# 提取映射规则
|
||||||
|
model_mappings: List[str] = []
|
||||||
|
config = global_model.config
|
||||||
|
if config and isinstance(config, dict):
|
||||||
|
mappings = config.get("model_mappings")
|
||||||
|
if isinstance(mappings, list):
|
||||||
|
model_mappings = [m for m in mappings if isinstance(m, str)]
|
||||||
|
|
||||||
|
# 如果 GlobalModel 没有 model_mappings,跳过(说明不是通过映射自动关联的)
|
||||||
|
if not model_mappings:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查是否有映射规则匹配到任一 allowed_models
|
||||||
|
matched = False
|
||||||
|
for mapping_pattern in model_mappings:
|
||||||
|
for allowed_model in all_allowed_models:
|
||||||
|
if match_model_with_pattern(mapping_pattern, allowed_model):
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
if matched:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果不再匹配,标记为待删除
|
||||||
|
if not matched:
|
||||||
|
models_to_delete.append(model)
|
||||||
|
|
||||||
|
# 4. 批量删除不再匹配的 Model(全部成功或全部失败)
|
||||||
|
if models_to_delete:
|
||||||
|
try:
|
||||||
|
for model in models_to_delete:
|
||||||
|
global_model = cast(GlobalModel, model.global_model)
|
||||||
|
db.delete(model)
|
||||||
|
results["success"].append(
|
||||||
|
{
|
||||||
|
"model_id": model.id,
|
||||||
|
"global_model_id": global_model.id,
|
||||||
|
"global_model_name": global_model.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Auto-disassociated Provider {provider.name} from GlobalModel {global_model.name} "
|
||||||
|
f"(no matching allowed_models)"
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logger.error(
|
||||||
|
f"Failed to auto-disassociate Provider {provider.name}: {e}"
|
||||||
|
)
|
||||||
|
# 清空 success,记录整体错误
|
||||||
|
results["success"] = []
|
||||||
|
results["errors"].append(
|
||||||
|
{
|
||||||
|
"provider_id": provider_id,
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|||||||
Reference in New Issue
Block a user