- Update Makefile to include new database seed commands for assistants and credentials. - Refactor assistant model to use explicit fields instead of a config dictionary, improving data integrity and clarity. - Implement new seeding SQL script for assistants, ensuring dependencies on credentials are respected. - Modify backend routes and frontend components to accommodate the new assistant structure, including direct field access for prompt, API URL, and keys. - Enhance the AssistantPage component to handle the new data structure and streamline the save process for different assistant types.
132 lines
4.4 KiB
Python
132 lines
4.4 KiB
Python
"""助手 CRUD。前端「助手列表 / 创建 / 编辑」对接这里。
|
|
|
|
模型/KB 以 FK 引用注册表;瘦类型字段直接是真列。外部类型(dify/fastgpt/opencode)的
|
|
api_key 是私有密钥,读时打码、写时哨兵(列级,复用 services/masking,与凭证表一致)。
|
|
"""
|
|
|
|
import uuid
|
|
|
|
from db.models import Assistant
|
|
from db.session import get_session
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from schemas import AssistantOut, AssistantUpsert
|
|
from services.masking import mask, resolve_incoming_key
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
router = APIRouter(prefix="/api/assistants", tags=["assistants"])
|
|
|
|
|
|
def _to_out(a: Assistant) -> AssistantOut:
|
|
return AssistantOut(
|
|
id=a.id,
|
|
name=a.name,
|
|
type=a.type, # type: ignore[arg-type]
|
|
runtime_mode=a.runtime_mode, # type: ignore[arg-type]
|
|
greeting=a.greeting,
|
|
enable_interrupt=a.enable_interrupt,
|
|
llm_credential_id=a.llm_credential_id,
|
|
asr_credential_id=a.asr_credential_id,
|
|
tts_credential_id=a.tts_credential_id,
|
|
realtime_credential_id=a.realtime_credential_id,
|
|
knowledge_base_id=a.knowledge_base_id,
|
|
prompt=a.prompt,
|
|
api_url=a.api_url,
|
|
api_key=mask(a.api_key), # 仅外部类型有值;空串 mask 仍是空串
|
|
app_id=a.app_id,
|
|
graph=a.graph or {},
|
|
updated_at=a.updated_at.isoformat() if a.updated_at else None,
|
|
)
|
|
|
|
|
|
@router.get("", response_model=list[AssistantOut])
|
|
async def list_assistants(session: AsyncSession = Depends(get_session)):
|
|
rows = (
|
|
await session.execute(select(Assistant).order_by(Assistant.updated_at.desc()))
|
|
).scalars().all()
|
|
return [_to_out(a) for a in rows]
|
|
|
|
|
|
@router.post("", response_model=AssistantOut)
|
|
async def create_assistant(
|
|
body: AssistantUpsert, session: AsyncSession = Depends(get_session)
|
|
):
|
|
a = Assistant(id=f"asst_{uuid.uuid4().hex[:12]}", **body.model_dump())
|
|
session.add(a)
|
|
await session.commit()
|
|
await session.refresh(a)
|
|
return _to_out(a)
|
|
|
|
|
|
@router.get("/{assistant_id}", response_model=AssistantOut)
|
|
async def get_assistant(
|
|
assistant_id: str, session: AsyncSession = Depends(get_session)
|
|
):
|
|
a = await session.get(Assistant, assistant_id)
|
|
if not a:
|
|
raise HTTPException(404, "助手不存在")
|
|
return _to_out(a)
|
|
|
|
|
|
@router.post("/{assistant_id}/duplicate", response_model=AssistantOut)
|
|
async def duplicate_assistant(
|
|
assistant_id: str, session: AsyncSession = Depends(get_session)
|
|
):
|
|
"""服务端整行复制:含真实 api_key,DB→DB,密钥不经过浏览器,副本可直接用。"""
|
|
src = await session.get(Assistant, assistant_id)
|
|
if not src:
|
|
raise HTTPException(404, "助手不存在")
|
|
a = Assistant(
|
|
id=f"asst_{uuid.uuid4().hex[:12]}",
|
|
name=f"{src.name} 副本",
|
|
type=src.type,
|
|
runtime_mode=src.runtime_mode,
|
|
greeting=src.greeting,
|
|
enable_interrupt=src.enable_interrupt,
|
|
llm_credential_id=src.llm_credential_id,
|
|
asr_credential_id=src.asr_credential_id,
|
|
tts_credential_id=src.tts_credential_id,
|
|
realtime_credential_id=src.realtime_credential_id,
|
|
knowledge_base_id=src.knowledge_base_id,
|
|
prompt=src.prompt,
|
|
api_url=src.api_url,
|
|
api_key=src.api_key, # 真 key,DB→DB
|
|
app_id=src.app_id,
|
|
graph=dict(src.graph or {}), # 浅拷贝,避免与源行共享同一 dict
|
|
)
|
|
session.add(a)
|
|
await session.commit()
|
|
await session.refresh(a)
|
|
return _to_out(a)
|
|
|
|
|
|
@router.put("/{assistant_id}", response_model=AssistantOut)
|
|
async def update_assistant(
|
|
assistant_id: str,
|
|
body: AssistantUpsert,
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
a = await session.get(Assistant, assistant_id)
|
|
if not a:
|
|
raise HTTPException(404, "助手不存在")
|
|
data = body.model_dump()
|
|
# 写时哨兵(列级):回传打码/空 api_key → 保留旧 key
|
|
data["api_key"] = resolve_incoming_key(data["api_key"], a.api_key)
|
|
for k, v in data.items():
|
|
setattr(a, k, v)
|
|
await session.commit()
|
|
await session.refresh(a)
|
|
return _to_out(a)
|
|
|
|
|
|
@router.delete("/{assistant_id}")
|
|
async def delete_assistant(
|
|
assistant_id: str, session: AsyncSession = Depends(get_session)
|
|
):
|
|
a = await session.get(Assistant, assistant_id)
|
|
if not a:
|
|
raise HTTPException(404, "助手不存在")
|
|
await session.delete(a)
|
|
await session.commit()
|
|
return {"ok": True}
|