"""模型资源凭证 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, CredentialTestRequest, CredentialTestResult, CredentialUpsert, ) from services.credential_tester import test_openai_credential, test_xfyun_credential 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), # 永远打码 voice=c.voice, speed=c.speed, language=c.language, 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, ""), voice=body.voice, speed=body.speed, language=body.language, 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("/test", response_model=CredentialTestResult) async def test_new_credential(body: CredentialTestRequest): if body.interface_type == "xfyun": return test_xfyun_credential(body) if body.interface_type != "openai": return CredentialTestResult( ok=False, message="暂不支持该接口类型", detail="当前仅支持 OpenAI 兼容接口测试", ) if not body.api_key: return CredentialTestResult( ok=False, message="缺少 API Key", detail="测试新配置时需要输入 API Key", ) return await test_openai_credential(body) @router.post("/{cred_id}/test", response_model=CredentialTestResult) async def test_saved_credential( cred_id: str, body: CredentialTestRequest, session: AsyncSession = Depends(get_session), ): c = await session.get(ProviderCredential, cred_id) if not c: raise HTTPException(404, "凭证不存在") config = body.model_copy( update={"api_key": resolve_incoming_key(body.api_key, c.api_key)} ) if config.interface_type == "xfyun": return test_xfyun_credential(config) if config.interface_type != "openai": return CredentialTestResult( ok=False, message="暂不支持该接口类型", detail="当前仅支持 OpenAI 兼容接口测试", ) return await test_openai_credential(config) @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 voice=src.voice, speed=src.speed, language=src.language, 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.voice = body.voice c.speed = body.speed c.language = body.language 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}