"""人物领域逻辑:纯函数与常量,无 IO。 包含角色档案处理、状态转换规则、模型策略提取等不依赖外部系统的领域逻辑。 设计见 ``docs/domain/identity-continuity.md``。 模块导入限制:禁止引入 ``repositories`` / ``db`` / ``integrations`` / ``httpx`` / ``sqlalchemy``。 """ from __future__ import annotations from datetime import date, datetime from typing import Any from app.repositories.json_utils import dumps, loads_dict # ============================================================ # 常量 # ============================================================ # create_character API 构建人物时的兜底模型策略(仅能力槽标识,不映射具体模型)。 # 系统模板的模型策略由 config/character_templates.yaml 定义,不走此常量为源。 MODEL_POLICY_CREATE_DEFAULTS: dict[str, str] = { "memory_extraction_model_hint": "memory-extraction", "personality_analysis_model_hint": "personality-analysis", } DEFAULT_SYSTEM_TEMPLATE_KEY: str = "default" SYSTEM_TEMPLATE_OWNER_SCOPE: str = "system" # 允许的状态转换:{当前状态 → [可转换的目标状态]} VALID_STATUS_TRANSITIONS: dict[str, list[str]] = { "initialized": ["active", "archived"], "active": ["paused", "archived"], "paused": ["active", "archived"], "archived": [], # 终态,不可再转换 } # 可变状态列表:此列表中的状态允许 profile / model_policy 修改 MUTABLE_STATUSES: tuple[str, ...] = ("active", "paused", "initialized") # ============================================================ # 档案处理 # ============================================================ def sanitize_profile(profile: dict[str, Any]) -> None: """原地转换 profile 中不可 JSON 序列化的类型(如 date / datetime)为 ISO 字符串。""" for key, value in profile.items(): if isinstance(value, (date, datetime)): profile[key] = value.isoformat() def build_profile_json(profile: dict[str, Any], **extra: Any) -> str: """构建 profile JSON 字符串:注入额外字段并序列化。""" merged = {**profile, **extra} sanitize_profile(merged) return dumps(merged) def build_model_policy_json( default_chat_model_hint: str | None = None, overrides: dict[str, Any] | None = None, ) -> str: """构建模型策略 JSON 字符串:以默认策略为基底,叠加 overrides。""" policy = dict(MODEL_POLICY_CREATE_DEFAULTS) if default_chat_model_hint: policy["default_chat_model_hint"] = default_chat_model_hint if overrides: policy.update(overrides) return dumps(policy) # ============================================================ # 模型策略提取 # ============================================================ def extract_model_hint(model_policy_json: str, setting_default: str = "") -> str: """从 ``model_policy_json`` 中提取 ``default_chat_model_hint``。 Args: model_policy_json: ORM 字段 ``character.model_policy_json`` 原始 JSON 文本。 setting_default: 全局默认值(来自 ``settings.default_model_hint``)。 Returns: 提取到的模型提示名,未配置时回退 ``setting_default``。 """ policy = loads_dict(model_policy_json) value = policy.get("default_chat_model_hint") return value if isinstance(value, str) and value else setting_default # ============================================================ # 状态转换验证 # ============================================================ def validate_status_transition(current_status: str, target_status: str) -> None: """验证人物状态转换是否合法。 Raises: ValueError: 转换不允许时。 """ allowed = VALID_STATUS_TRANSITIONS.get(current_status, []) if target_status not in allowed: raise ValueError( f"不允许的状态转换:{current_status!r} → {target_status!r}。" f"允许的目标状态:{allowed or '(无,终态不可转换)'}" ) def validate_is_mutable(status: str) -> None: """检查人物当前状态是否允许修改档案/策略。 Raises: ValueError: 不可修改时。 """ if status not in MUTABLE_STATUSES: raise ValueError(f"状态 {status!r} 的人物不可修改。") def require_not_archived(status: str) -> None: """检查人物是否已归档(终态拦截,状态无关)。""" if status == "archived": raise ValueError("已归档人物不可操作,请先恢复。") # ============================================================ # 模板相关 # ============================================================ def ensure_character_mutable(is_system_template: bool) -> None: """系统人物模板不可修改。""" if is_system_template: raise ValueError("系统人物模板不可修改。") __all__ = [ "MODEL_POLICY_CREATE_DEFAULTS", "DEFAULT_SYSTEM_TEMPLATE_KEY", "SYSTEM_TEMPLATE_OWNER_SCOPE", "VALID_STATUS_TRANSITIONS", "MUTABLE_STATUSES", "build_model_policy_json", "build_profile_json", "ensure_character_mutable", "extract_model_hint", "require_not_archived", "sanitize_profile", "validate_is_mutable", "validate_status_transition", ]