110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""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}
|