"""灵核情绪状态服务(设计 §11):拉模式计算 + 一炁订阅 time.* 处理器。 降级原则(设计 §12 / 辰正 §12):任何失败 → emotion_state=None,不阻塞对话/事件流。 本模块是一炁订阅基础设施(计划 5 ``Dispatcher``)的第一个真实消费者。 """ from __future__ import annotations from datetime import UTC, datetime from loguru import logger from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.db.session import AsyncSessionLocal from app.models.character import Character from app.models.chat import ChatMessage from app.models.personality_emotion import CharacterEmotionState from app.modules.personality.emotion import ( EmotionState, apply_signal, decay, parse_baseline, to_prompt_text, ) from app.modules.runtime.events import Event from app.repositories.personality import get_latest_personality, get_personality from app.repositories.personality_emotion import get_state, upsert_state async def _resolve_baseline(session: AsyncSession, character_id: str) -> EmotionState: """从当前人格版本 ``emotion_baseline`` 解析 baseline。 character 不存在 / 无人格版本 / baseline 损坏 → 默认 neutral 0.2(parse_baseline 容错)。 """ character = await session.get(Character, character_id) if character is None: return EmotionState() pv = None if character.current_personality_version_id: pv = await get_personality(session, character.current_personality_version_id) if pv is None: pv = await get_latest_personality(session, character_id) if pv is None: return EmotionState() return parse_baseline(pv.emotion_baseline) async def _count_user_turns_since( session: AsyncSession, character_id: str, since: datetime ) -> float: """自 since 以来该角色未删除的 user 消息数(近似对话回合)。""" result = await session.execute( select(func.count()) .select_from(ChatMessage) .where( ChatMessage.character_id == character_id, ChatMessage.role == "user", ChatMessage.created_at > since, ChatMessage.deleted_at.is_(None), ) ) return float(result.scalar_one()) async def compute_current_state(session: AsyncSession, character_id: str) -> EmotionState | None: """拉模式:读最新 state + baseline,按自上次更新以来的 user 回合 decay。 - 无 state row:返回 baseline(不 decay)。 - 有 state row:``decay(state, baseline, elapsed_turns)``。 - 任何异常降级返回 None。 """ try: baseline = await _resolve_baseline(session, character_id) row = await get_state(session, character_id) if row is None: return baseline current = EmotionState(mood=row.mood, intensity=row.intensity) # SQLite DateTime(timezone=True) 读回可能 naive,与 aware 比较前补 tz(同 time_sense 防御)。 updated = row.updated_at if updated.tzinfo is None: updated = updated.replace(tzinfo=UTC) elapsed = await _count_user_turns_since(session, character_id, updated) return decay(current, baseline, elapsed) except Exception: logger.exception("灵核 compute_current_state 降级 character_id={}", character_id) return None async def compute_emotion_prompt(session: AsyncSession, character_id: str) -> str | None: """拉模式 → system prompt 文本;失败返回 None(不阻塞对话)。""" state = await compute_current_state(session, character_id) if state is None: return None return to_prompt_text(state) async def apply_time_event(event: Event, session: AsyncSession) -> None: """订阅处理器核心:辰正 time.* 事件 → 更新情绪状态。 - 读当前 state(无则用 baseline) - ``apply_signal`` 计算新状态 - ``upsert_state`` + 同步 ``Character.current_mood_id`` 无 character_id 的事件直接跳过;DB 异常由 ``handle_time_event`` 兜底。 """ character_id = event.character_id if character_id is None: return baseline = await _resolve_baseline(session, character_id) row = await get_state(session, character_id) current = EmotionState(mood=row.mood, intensity=row.intensity) if row else baseline new_state = apply_signal(current, event.type, event.payload, baseline) await upsert_state( session, CharacterEmotionState( character_id=character_id, mood=new_state.mood, intensity=new_state.intensity, baseline_mood=baseline.mood, baseline_intensity=baseline.intensity, last_trigger_event_id=event.id, ), ) character = await session.get(Character, character_id) if character is not None: character.current_mood_id = new_state.mood async def handle_time_event(event: Event) -> None: """Dispatcher ``time.*`` 处理器:自管 session + 提交,降级不抛。 一炁订阅基础设施(计划 5)的第一个真实消费者,注册在 ``main.py`` lifespan。 """ try: async with AsyncSessionLocal() as session: await apply_time_event(event, session) await session.commit() except Exception: logger.exception("灵核 handle_time_event 降级 event_id={} type={}", event.id, event.type)