2026-06-08 13:51:28 +08:00
|
|
|
"""助手 CRUD。前端「助手列表 / 创建 / 编辑」对接这里。
|
|
|
|
|
|
2026-06-09 08:31:39 +08:00
|
|
|
模型/KB 以 FK 引用注册表;外部类型(dify/fastgpt/opencode)的 config.apiKey 是私有密钥,
|
|
|
|
|
读时打码、写时哨兵(复用 services/masking)。
|
2026-06-08 13:51:28 +08:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
from db.models import Assistant
|
|
|
|
|
from db.session import get_session
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
2026-06-09 08:31:39 +08:00
|
|
|
from schemas import EXTERNAL_TYPES, AssistantOut, AssistantUpsert
|
|
|
|
|
from services.masking import mask, resolve_incoming_key
|
2026-06-08 13:51:28 +08:00
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/assistants", tags=["assistants"])
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 08:31:39 +08:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 13:51:28 +08:00
|
|
|
def _to_out(a: Assistant) -> AssistantOut:
|
|
|
|
|
return AssistantOut(
|
|
|
|
|
id=a.id,
|
|
|
|
|
name=a.name,
|
2026-06-09 08:31:39 +08:00
|
|
|
type=a.type, # type: ignore[arg-type]
|
2026-06-08 13:51:28 +08:00
|
|
|
runtime_mode=a.runtime_mode, # type: ignore[arg-type]
|
2026-06-09 08:31:39 +08:00
|
|
|
greeting=a.greeting,
|
2026-06-08 13:51:28 +08:00
|
|
|
enable_interrupt=a.enable_interrupt,
|
2026-06-09 08:31:39 +08:00
|
|
|
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 {}),
|
2026-06-08 13:51:28 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 09:48:43 +08:00
|
|
|
@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)
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 13:51:28 +08:00
|
|
|
@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, "助手不存在")
|
2026-06-09 08:31:39 +08:00
|
|
|
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():
|
2026-06-08 13:51:28 +08:00
|
|
|
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}
|