"""默认人物模板加载器:从 ``character_templates.yaml`` 读取系统模板定义。 按项目约定,此文件放在应用服务层(``app/services/``),而非 ``core/config/``, 因为人物模板定义属于业务数据(出厂产品内容),而非部署环境配置。 """ from __future__ import annotations from typing import Any import yaml from pydantic import BaseModel, ConfigDict, Field from app.core.config import settings from app.core.errors import AppError class CharacterTemplateConfig(BaseModel): """一条系统模板的配置定义。""" model_config = ConfigDict(extra="forbid") key: str = Field(description="模板唯一标识,对应 ORM 的 template_key。") name: str = Field(description="模板人物内部名称。") display_name: str | None = Field(default=None, description="模板人物显示昵称。") language: str = Field(default="zh-CN", description="默认语言。") privacy_level: str = Field(default="normal", description="隐私等级。") is_default: bool = Field( default=False, description="注册新用户时是否以此模板创建默认角色。至多一个模板为 true,多个则取第一个。", ) profile: dict[str, Any] = Field( default_factory=dict, description="人物档案,可含 background / speaking_style 等(不含 birthday——由创建时兜底)。", ) model_policy: dict[str, Any] = Field( default_factory=dict, description="模型策略(不含 default_chat_model_hint——启动时从 settings 补充)。", ) class CharacterTemplatesFile(BaseModel): """配置文件顶层结构。""" model_config = ConfigDict(extra="forbid") version: int = Field(description="配置文件版本号,当前仅支持 1。") templates: list[CharacterTemplateConfig] = Field(description="系统模板定义列表。") def load_character_templates() -> dict[str, CharacterTemplateConfig]: """读 ``character_templates.yaml`` → parse → validate → ``{key → config}`` 字典。 Returns: 空字典:文件不存在或内容为空时返回空。 Raises: AppError: 文件存在但 YAML 解析失败或 version 不匹配。 """ path = settings.resolved_character_templates_path if not path.exists(): return {} try: raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {} config = CharacterTemplatesFile.model_validate(raw) except AppError: raise except Exception as exc: raise AppError( "config_error", "人物模板配置文件解析失败。", details={"path": str(path)}, ) from exc if config.version != 1: raise AppError( "config_error", f"仅支持 character_templates.yaml version: 1,当前为 {config.version}。", details={"path": str(path)}, ) return {t.key: t for t in config.templates} def get_default_template() -> CharacterTemplateConfig: """便利函数:取 ``key='default'`` 的系统模板配置。 Raises: AppError: 加载失败或缺少 default 模板。 """ templates = load_character_templates() template = templates.get("default") if template is None: raise AppError( "config_error", "人物模板配置中缺少 key='default' 的模板。", ) return template