"""面向前端的请求/响应 DTO。与 DB 模型解耦,**响应里的 key 一律打码**。 凭证 DTO 字段对齐前端 ComponentsModelsPage 的 ModelResource: JSON 用 camelCase(modelId/interfaceType/apiUrl/apiKey),Python 内部用 snake_case, 靠 Pydantic alias 自动互转。FastAPI 响应默认 by_alias=True,所以出参也是 camelCase。 """ from __future__ import annotations from typing import Any, Literal from pydantic import BaseModel, ConfigDict, model_validator from pydantic.alias_generators import to_camel RuntimeMode = Literal["pipeline", "realtime"] ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding"] InterfaceType = Literal["openai", "xfyun", "dashscope", "gemini"] AssistantType = Literal["prompt", "workflow", "dify", "fastgpt", "opencode"] # 外部应用类型:其 config.apiKey 是该助手私有密钥,读时打码 / 写时哨兵 EXTERNAL_TYPES = {"dify", "fastgpt", "opencode"} class CamelModel(BaseModel): """JSON camelCase ↔ Python snake_case。protected_namespaces 关掉以允许 model_id。""" model_config = ConfigDict( alias_generator=to_camel, populate_by_name=True, protected_namespaces=(), ) # 各 type 允许的瘦字段(其余字段写入时清零,防止跨类型脏数据) ALLOWED_FIELDS: dict[str, set[str]] = { "prompt": {"prompt"}, "workflow": {"graph"}, "dify": {"api_url", "api_key"}, "fastgpt": {"app_id", "api_url", "api_key"}, "opencode": {"prompt", "api_url", "api_key"}, } # ---------- 助手(单表 STI:瘦类型真列 + workflow 图 JSON 列) ---------- class AssistantUpsert(CamelModel): name: str type: AssistantType = "prompt" runtime_mode: RuntimeMode = "pipeline" greeting: str = "" enable_interrupt: bool = True # 引用注册资源(FK id;None=未选) llm_credential_id: str | None = None asr_credential_id: str | None = None tts_credential_id: str | None = None realtime_credential_id: str | None = None knowledge_base_id: str | None = None # 瘦类型专属(真列);按 type 取用,无关字段写入时清零 prompt: str = "" api_url: str = "" api_key: str = "" # 写时:占位符/空 → 保留旧(哨兵) app_id: str = "" # workflow 专属:图 graph: dict[str, Any] = {} @model_validator(mode="after") def _strip_irrelevant_fields(self): allowed = ALLOWED_FIELDS[self.type] for field in ("prompt", "api_url", "api_key", "app_id"): if field not in allowed: setattr(self, field, "") if "graph" not in allowed: self.graph = {} return self class AssistantOut(AssistantUpsert): id: str updated_at: str | None = None # ---------- 知识库 ---------- class KnowledgeBaseUpsert(CamelModel): name: str description: str = "" embedding_credential_id: str | None = None class KnowledgeBaseOut(KnowledgeBaseUpsert): id: str status: str = "active" updated_at: str | None = None # ---------- 模型凭证(对齐前端 ModelResource) ---------- class CredentialUpsert(CamelModel): name: str = "" # 资源名称 model_id: str = "" # 模型ID type: ModelType # LLM/ASR/TTS/Realtime/Embedding interface_type: InterfaceType = "openai" # openai/xfyun/dashscope/gemini api_url: str = "" api_key: str = "" # 写时:占位符/空表示不改 is_default: bool = False class CredentialOut(CamelModel): id: str name: str model_id: str type: str interface_type: str api_url: str api_key: str # 读时:打码后的值 is_default: bool