"""Administrator CRUD for reusable task backends.""" import uuid from db.models import TaskBackendResource from db.session import get_session from fastapi import APIRouter, Depends, HTTPException from services.auth import require_admin from services.masking import merge_secrets from services.task_backend_resources import ( backend_consumers, task_backend_to_out, validate_backend_binding, validate_backend_resource, ) from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from task_backend_schemas import TaskBackendOut, TaskBackendUpsert router = APIRouter( prefix="/api/task-backends", tags=["task-backends"], dependencies=[Depends(require_admin)], ) def _payload(body: TaskBackendUpsert, stored=None) -> dict: stored_secrets = stored.secrets or {} if stored and stored.type == body.type else {} return { "name": body.name, "type": body.type, "description": body.description.strip(), "config": body.config.model_dump(), "enabled": body.enabled, "secrets": merge_secrets(body.secrets, stored_secrets) if body.type == "opencode" else {}, } async def _validate(session, body: TaskBackendUpsert) -> None: if not body.enabled: return try: await validate_backend_resource(session, body) except ValueError as exc: raise HTTPException(400, str(exc)) from exc @router.get("", response_model=list[TaskBackendOut]) async def list_task_backends(session: AsyncSession = Depends(get_session)): rows = (await session.execute(select(TaskBackendResource).order_by(TaskBackendResource.updated_at.desc()))).scalars().all() return [task_backend_to_out(row) for row in rows] @router.post("", response_model=TaskBackendOut) async def create_task_backend(body: TaskBackendUpsert, session: AsyncSession = Depends(get_session)): await _validate(session, body) resource = TaskBackendResource(id=f"backend_{uuid.uuid4().hex[:12]}", **_payload(body)) session.add(resource) await session.commit() await session.refresh(resource) return task_backend_to_out(resource) async def _get(session, backend_id: str) -> TaskBackendResource: resource = await session.get(TaskBackendResource, backend_id) if resource is None: raise HTTPException(404, "任务后端不存在") return resource @router.get("/{backend_id}", response_model=TaskBackendOut) async def get_task_backend(backend_id: str, session: AsyncSession = Depends(get_session)): return task_backend_to_out(await _get(session, backend_id)) @router.put("/{backend_id}", response_model=TaskBackendOut) async def update_task_backend(backend_id: str, body: TaskBackendUpsert, session: AsyncSession = Depends(get_session)): resource = await _get(session, backend_id) await _validate(session, body) payload = _payload(body, resource) proposed = TaskBackendResource(id=resource.id, **payload) consumers = await backend_consumers(session, backend_id) # Disabling remains reversible; valid enabled configurations must work for all users. if proposed.enabled: try: for assistant in consumers: await validate_backend_binding(session, assistant, resource=proposed) except ValueError as exc: raise HTTPException(400, f"助手 {assistant.name} 的任务配置无效: {exc}") from exc for key, value in payload.items(): setattr(resource, key, value) await session.commit() await session.refresh(resource) return task_backend_to_out(resource) @router.post("/{backend_id}/duplicate", response_model=TaskBackendOut) async def duplicate_task_backend(backend_id: str, session: AsyncSession = Depends(get_session)): source = await _get(session, backend_id) body = TaskBackendUpsert(name=f"{source.name[:124]} 副本", type=source.type, description=source.description, config=source.config, secrets=source.secrets or {}, enabled=source.enabled) return await create_task_backend(body, session) @router.delete("/{backend_id}") async def delete_task_backend(backend_id: str, session: AsyncSession = Depends(get_session)): resource = await _get(session, backend_id) if await backend_consumers(session, backend_id): raise HTTPException(409, "任务后端正被助手引用,请先解绑") await session.delete(resource) await session.commit() return {"ok": True}