97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""默认人物模板加载器:从 ``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
|