"""演化候选派生与审核服务。 applier 注册表按 ``type`` 派生到下游(本期 personality→人格版本、procedural→ 程序记忆,其余返回 None 降级)。审核闭环 approve/reject 复用派生能力,自管 session 并记审计(见 [大周天演化候选落地设计] ../docs/domain/core-domains.md §6-§7)。 """ from collections.abc import Awaitable, Callable from sqlalchemy.ext.asyncio import AsyncSession from app.core.errors import AppError from app.core.time import utc_now from app.db.session import AsyncSessionLocal from app.models.character import Character from app.models.evolution import EvolutionCandidate from app.models.memory import Memory from app.repositories import evolution as evolution_repo from app.repositories import personality as personality_repo from app.repositories.json_utils import loads_dict, loads_list from app.schemas.common import Page from app.schemas.evolution import EvolutionCandidateResponse from app.services.audit import record_audit_event from app.services.personality import derive_personality_version Applier = Callable[..., Awaitable[str]] async def apply_evolution_candidate( session: AsyncSession, candidate: EvolutionCandidate, *, actor: str = "system" ) -> str | None: """应用演化候选到下游;返回派生资源 ID,无 applier 时返回 None(降级)。""" applier = _APPLIERS.get(candidate.type) if applier is None: return None return await applier(session, candidate, actor=actor) async def _apply_personality( session: AsyncSession, candidate: EvolutionCandidate, *, actor: str ) -> str: base = await personality_repo.get_latest_personality(session, candidate.character_id) if base is None: raise AppError("invalid_request", "人物尚无人格版本,无法应用演化候选。") overrides = loads_dict(candidate.delta_json) new_version = derive_personality_version( base, overrides, reason=f"evolution:{candidate.id}", created_by="big_cycle" ) session.add(new_version) await session.flush() character = await session.get(Character, candidate.character_id) if character is None: raise AppError("not_found", "人物不存在。") character.current_personality_version_id = new_version.id await _record_apply( session, candidate, f"type=personality version={new_version.id}", actor=actor ) return new_version.id async def _apply_procedural( session: AsyncSession, candidate: EvolutionCandidate, *, actor: str ) -> str: delta = loads_dict(candidate.delta_json) memory = Memory( character_id=candidate.character_id, type="procedural", title=str(delta.get("title") or candidate.title), content=str(delta.get("content") or candidate.proposal), source="big_cycle", importance=candidate.confidence, ) session.add(memory) await session.flush() await _record_apply(session, candidate, f"type=procedural memory={memory.id}", actor=actor) return memory.id async def list_evolution_candidates( *, character_id: str, status: str | None = None, candidate_type: str | None = None, page: int = 1, page_size: int = 20, ) -> Page[EvolutionCandidateResponse]: async with AsyncSessionLocal() as session: items, total = await evolution_repo.list_candidates( session, character_id, status=status, candidate_type=candidate_type, page=page, page_size=page_size, ) return Page( items=[_to_response(item) for item in items], page=page, page_size=page_size, total=total, ) async def approve_evolution_candidate( candidate_id: str, ) -> EvolutionCandidateResponse: async with AsyncSessionLocal() as session: candidate = await evolution_repo.get_candidate(session, candidate_id) if candidate is None: raise AppError("not_found", "演化候选不存在。") if candidate.status != "pending": raise AppError("conflict", f"候选当前状态为 {candidate.status},不可审核。") target_id = await apply_evolution_candidate(session, candidate, actor="user") if target_id is None: raise AppError("not_implemented", "该类型派生待后续阶段。") candidate.status = "applied" candidate.resulting_target_id = target_id candidate.reviewed_at = utc_now() candidate.reviewed_by = "user" await session.commit() await session.refresh(candidate) return _to_response(candidate) async def reject_evolution_candidate( candidate_id: str, ) -> EvolutionCandidateResponse: async with AsyncSessionLocal() as session: candidate = await evolution_repo.get_candidate(session, candidate_id) if candidate is None: raise AppError("not_found", "演化候选不存在。") if candidate.status != "pending": raise AppError("conflict", f"候选当前状态为 {candidate.status},不可审核。") candidate.status = "rejected" candidate.reviewed_at = utc_now() candidate.reviewed_by = "user" await record_audit_event( session, action="evolution.candidate.rejected", actor="user", risk_level=candidate.level, decision="denied", character_id=candidate.character_id, input_summary=candidate.proposal[:200], resource_type="evolution_candidate", resource_id=candidate.id, ) await session.commit() await session.refresh(candidate) return _to_response(candidate) def _to_response(candidate: EvolutionCandidate) -> EvolutionCandidateResponse: return EvolutionCandidateResponse( id=candidate.id, character_id=candidate.character_id, cycle_id=candidate.cycle_id, type=candidate.type, source=candidate.source, level=candidate.level, scope=candidate.scope, title=candidate.title, proposal=candidate.proposal, delta=loads_dict(candidate.delta_json), evidence_ids=loads_list(candidate.evidence_ids_json), confidence=candidate.confidence, benefit_score=candidate.benefit_score, risk_score=candidate.risk_score, drift_score=candidate.drift_score, auto_apply_allowed=candidate.auto_apply_allowed, status=candidate.status, resulting_target_id=candidate.resulting_target_id, error_message=candidate.error_message, reviewed_at=candidate.reviewed_at, reviewed_by=candidate.reviewed_by, created_at=candidate.created_at, ) async def _record_apply( session: AsyncSession, candidate: EvolutionCandidate, output_summary: str, *, actor: str, ) -> None: await record_audit_event( session, action="evolution.candidate.applied", actor=actor, risk_level=candidate.level, decision="auto", character_id=candidate.character_id, input_summary=candidate.proposal[:200], output_summary=output_summary, resource_type="evolution_candidate", resource_id=candidate.id, ) _APPLIERS: dict[str, Applier] = { "personality": _apply_personality, "procedural": _apply_procedural, }