Implement server-side duplication for both assistants and credentials, allowing users to create copies with unique IDs and modified names. Update the respective API routes and frontend components to handle duplication requests, ensuring sensitive information is securely managed. Enhance the AssistantPage and ComponentsModelsPage to support this new feature, including loading and error handling for the duplication process.
143 lines
4.8 KiB
Python
143 lines
4.8 KiB
Python
"""助手 CRUD。前端「助手列表 / 创建 / 编辑」对接这里。
|
|
|
|
模型/KB 以 FK 引用注册表;外部类型(dify/fastgpt/opencode)的 config.apiKey 是私有密钥,
|
|
读时打码、写时哨兵(复用 services/masking)。
|
|
"""
|
|
|
|
import uuid
|
|
|
|
from db.models import Assistant
|
|
from db.session import get_session
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from schemas import EXTERNAL_TYPES, 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 _mask_config(type_: str, config: dict) -> dict:
|
|
"""读取返回前:外部类型的 apiKey 打码,其余原样。"""
|
|
if type_ in EXTERNAL_TYPES and config.get("apiKey"):
|
|
return {**config, "apiKey": mask(config["apiKey"])}
|
|
return config
|
|
|
|
|
|
def _merge_config(type_: str, incoming: dict, stored: dict) -> dict:
|
|
"""写入时:外部类型若回传打码占位符/空 apiKey → 保留旧 key。"""
|
|
if type_ in EXTERNAL_TYPES and "apiKey" in incoming:
|
|
incoming = {
|
|
**incoming,
|
|
"apiKey": resolve_incoming_key(
|
|
incoming.get("apiKey"), stored.get("apiKey", "")
|
|
),
|
|
}
|
|
return incoming
|
|
|
|
|
|
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,
|
|
config=_mask_config(a.type, a.config 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)
|
|
):
|
|
"""服务端整行复制:含真实 config(真 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,
|
|
config=dict(src.config 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()
|
|
# 外部类型 apiKey 写时哨兵:打码占位符 → 保留旧 key(在改 a.config 前用旧值)
|
|
data["config"] = _merge_config(body.type, data["config"], a.config or {})
|
|
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}
|