"""模型资源凭证 CRUD。前端 ComponentsModelsPage 对接这里。 字段对齐前端 ModelResource。读:api_key 打码;写:占位符表示不改(写时哨兵)。 设默认时清掉同 type 其它默认。 """ import uuid from db.models import ProviderCredential from db.session import get_session from fastapi import APIRouter, Depends, HTTPException from schemas import CredentialOut, CredentialUpsert from services.masking import mask, resolve_incoming_key from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter(prefix="/api/credentials", tags=["credentials"]) def _to_out(c: ProviderCredential) -> CredentialOut: return CredentialOut( id=c.id, name=c.name, model_id=c.model_id, type=c.type, interface_type=c.interface_type, api_url=c.api_url, api_key=mask(c.api_key), # 永远打码 is_default=c.is_default, ) async def _clear_other_defaults(session: AsyncSession, type_: str, keep_id: str): await session.execute( update(ProviderCredential) .where(ProviderCredential.type == type_, ProviderCredential.id != keep_id) .values(is_default=False) ) @router.get("", response_model=list[CredentialOut]) async def list_credentials(session: AsyncSession = Depends(get_session)): rows = ( await session.execute( select(ProviderCredential).order_by(ProviderCredential.type) ) ).scalars().all() return [_to_out(c) for c in rows] @router.post("", response_model=CredentialOut) async def create_credential( body: CredentialUpsert, session: AsyncSession = Depends(get_session) ): c = ProviderCredential( id=f"model_{uuid.uuid4().hex[:12]}", name=body.name, model_id=body.model_id, type=body.type, interface_type=body.interface_type, api_url=body.api_url, api_key=resolve_incoming_key(body.api_key, ""), is_default=body.is_default, ) session.add(c) if c.is_default: await _clear_other_defaults(session, c.type, c.id) await session.commit() await session.refresh(c) return _to_out(c) @router.post("/{cred_id}/duplicate", response_model=CredentialOut) async def duplicate_credential( cred_id: str, session: AsyncSession = Depends(get_session) ): """服务端整行复制:含真实 api_key,DB→DB,密钥不经浏览器。副本不继承默认标记。""" src = await session.get(ProviderCredential, cred_id) if not src: raise HTTPException(404, "凭证不存在") c = ProviderCredential( id=f"model_{uuid.uuid4().hex[:12]}", name=f"{src.name} 副本", model_id=src.model_id, type=src.type, interface_type=src.interface_type, api_url=src.api_url, api_key=src.api_key, # 真 key,DB→DB is_default=False, # 副本不继承默认,避免抢走源的默认标记 ) session.add(c) await session.commit() await session.refresh(c) return _to_out(c) @router.put("/{cred_id}", response_model=CredentialOut) async def update_credential( cred_id: str, body: CredentialUpsert, session: AsyncSession = Depends(get_session), ): c = await session.get(ProviderCredential, cred_id) if not c: raise HTTPException(404, "凭证不存在") c.name = body.name c.model_id = body.model_id c.type = body.type c.interface_type = body.interface_type c.api_url = body.api_url c.is_default = body.is_default # 写时哨兵:打码占位符 → 保留旧 key c.api_key = resolve_incoming_key(body.api_key, c.api_key) if c.is_default: await _clear_other_defaults(session, c.type, c.id) await session.commit() await session.refresh(c) return _to_out(c) @router.delete("/{cred_id}") async def delete_credential( cred_id: str, session: AsyncSession = Depends(get_session) ): c = await session.get(ProviderCredential, cred_id) if not c: raise HTTPException(404, "凭证不存在") await session.delete(c) await session.commit() return {"ok": True}