"""大周天抽取候选审核服务(记忆候选 + 世界实体候选)。 记忆候选 approve 派生为 ``memories``(source=big_cycle);世界实体候选 approve → confirmed 进入世界模型。审核仅 pending 可操作(见 [大周天记忆/世界抽取落地设计] ../docs/domain/core-domains.md §5)。 """ from app.core.errors import AppError from app.core.time import utc_now from app.db.session import AsyncSessionLocal from app.models.memory import Memory from app.repositories import candidates as candidates_repo from app.repositories import world as world_repo from app.schemas.candidates import MemoryCandidateResponse from app.schemas.common import Page from app.schemas.world import WorldEntityResponse from app.services.audit import record_audit_event from app.services.serializers import ( character_world_entity_to_response, memory_candidate_to_response, ) async def list_memory_proposals( *, character_id: str, source: str | None = None, status: str | None = None, page: int = 1, page_size: int = 20, ) -> Page[MemoryCandidateResponse]: async with AsyncSessionLocal() as session: all_items = await candidates_repo.list_memory_candidates( session, character_id, source=source, status=status ) start = max(page - 1, 0) * page_size items = all_items[start : start + page_size] return Page( items=[memory_candidate_to_response(i) for i in items], page=page, page_size=page_size, total=len(all_items), ) async def approve_memory_proposal( candidate_id: str, ) -> MemoryCandidateResponse: async with AsyncSessionLocal() as session: candidate = await candidates_repo.get_memory_candidate(session, candidate_id) if candidate is None: raise AppError("not_found", "记忆候选不存在。") if candidate.status != "pending": raise AppError("conflict", f"候选当前状态为 {candidate.status},不可审核。") session.add( Memory( character_id=candidate.character_id, type=candidate.type, title=candidate.title, content=candidate.content, source="big_cycle", confidence=candidate.confidence, importance=candidate.importance, sensitivity=candidate.sensitivity, evidence_ids_json=candidate.evidence_ids_json, status="active", ) ) candidate.status = "applied" candidate.reviewed_at = utc_now() await record_audit_event( session, action="extraction.memory.applied", actor="user", character_id=candidate.character_id, input_summary=candidate.title[:200], resource_type="memory_candidate", resource_id=candidate.id, ) await session.commit() await session.refresh(candidate) return memory_candidate_to_response(candidate) async def reject_memory_proposal( candidate_id: str, ) -> MemoryCandidateResponse: return await _reject_memory(candidate_id) async def list_world_entities_pending( *, character_id: str, status: str | None = None, page: int = 1, page_size: int = 20, ) -> Page[WorldEntityResponse]: async with AsyncSessionLocal() as session: items, total = await world_repo.list_entities( session, character_id, status=status, page=page, page_size=page_size ) return Page( items=[character_world_entity_to_response(i) for i in items], page=page, page_size=page_size, total=total, ) async def approve_world_entity(entity_id: str) -> WorldEntityResponse: return await _review_world_entity(entity_id, "confirmed", decision="auto") async def reject_world_entity(entity_id: str) -> WorldEntityResponse: return await _review_world_entity(entity_id, "rejected", decision="denied") async def _reject_memory(candidate_id: str) -> MemoryCandidateResponse: async with AsyncSessionLocal() as session: candidate = await candidates_repo.get_memory_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() await record_audit_event( session, action="extraction.memory.rejected", actor="user", decision="denied", character_id=candidate.character_id, input_summary=candidate.title[:200], resource_type="memory_candidate", resource_id=candidate.id, ) await session.commit() await session.refresh(candidate) return memory_candidate_to_response(candidate) async def _review_world_entity( entity_id: str, target_status: str, *, decision: str ) -> WorldEntityResponse: async with AsyncSessionLocal() as session: entity = await world_repo.get_entity(session, entity_id) if entity is None: raise AppError("not_found", "世界实体不存在。") if entity.status != "pending": raise AppError("conflict", f"实体当前状态为 {entity.status},不可审核。") entity.status = target_status await record_audit_event( session, action=f"extraction.world.{target_status}", actor="user", decision=decision, character_id=entity.character_id, input_summary=entity.name[:200], resource_type="character_world_entity", resource_id=entity.id, ) await session.commit() await session.refresh(entity) return character_world_entity_to_response(entity)