"""内驱力审核服务(次生目标 approve → 自动创建太一目标)。 审核 approve 时自动派生 Goal(太一枢任务级目标), 延后 / 驳回仅标记不变更目标树。 """ from app.core.errors import AppError from app.core.ids import new_goal_id from app.core.time import utc_now from app.db.session import AsyncSessionLocal from app.models.drive import CuriosityQuestion, ExploratorySubgoal from app.models.governance import Goal from app.repositories import drive as drive_repo from app.repositories.json_utils import loads_list from app.schemas.common import Page from app.schemas.drive import CuriosityResponse, SubgoalResponse from app.services.audit import record_audit_event async def list_curiosities( *, character_id: str, source: str | None = None, page: int = 1, page_size: int = 20, ) -> Page[CuriosityResponse]: async with AsyncSessionLocal() as session: items, total = await drive_repo.list_curiosities( session, character_id, source=source, page=page, page_size=page_size ) return Page( items=[_curiosity_to_response(i) for i in items], page=page, page_size=page_size, total=total, ) async def list_subgoals( *, character_id: str, status: str | None = None, page: int = 1, page_size: int = 20, ) -> Page[SubgoalResponse]: async with AsyncSessionLocal() as session: items, total = await drive_repo.list_subgoals( session, character_id, status=status, page=page, page_size=page_size ) return Page( items=[_subgoal_to_response(i) for i in items], page=page, page_size=page_size, total=total, ) async def approve_subgoal(subgoal_id: str) -> SubgoalResponse: return await _review_subgoal(subgoal_id, "approved", decision="auto", create_goal=True) async def defer_subgoal(subgoal_id: str) -> SubgoalResponse: return await _review_subgoal(subgoal_id, "deferred", decision="auto", create_goal=False) async def reject_subgoal(subgoal_id: str) -> SubgoalResponse: return await _review_subgoal(subgoal_id, "rejected", decision="denied", create_goal=False) async def _review_subgoal( subgoal_id: str, target_status: str, *, decision: str, create_goal: bool ) -> SubgoalResponse: async with AsyncSessionLocal() as session: sub = await drive_repo.get_subgoal(session, subgoal_id) if sub is None: raise AppError("not_found", "探索性次生目标不存在。") if sub.status != "candidate": raise AppError("conflict", f"次生目标当前状态为 {sub.status},不可审核。") sub.status = target_status sub.reviewed_at = utc_now() sub.reviewed_by = "user" # approve 时自动创建太一任务级目标 if create_goal and sub.character_id: goal = Goal( id=new_goal_id(), character_id=sub.character_id, title=sub.title, description=sub.description or "由次生目标自动生成", level="task", type="exploration", status="active", ) session.add(goal) await record_audit_event( session, action=f"drive.subgoal.{target_status}", actor="user", risk_level=sub.risk_level, decision=decision, character_id=sub.character_id, input_summary=sub.title[:200], resource_type="exploratory_subgoal", resource_id=sub.id, ) await session.commit() await session.refresh(sub) return _subgoal_to_response(sub) def _curiosity_to_response(c: CuriosityQuestion) -> CuriosityResponse: return CuriosityResponse( id=c.id, character_id=c.character_id, cycle_id=c.cycle_id, question=c.question, source=c.source, related_entities=loads_list(c.related_entities_json), expected_value=c.expected_value, urgency=c.urgency, risk_level=c.risk_level, created_at=c.created_at, ) def _subgoal_to_response(s: ExploratorySubgoal) -> SubgoalResponse: return SubgoalResponse( id=s.id, character_id=s.character_id, cycle_id=s.cycle_id, parent_goal_id=s.parent_goal_id, curiosity_question_id=s.curiosity_question_id, title=s.title, description=s.description, expected_information_gain=s.expected_information_gain, interruption_cost=s.interruption_cost, risk_level=s.risk_level, status=s.status, reviewed_at=s.reviewed_at, reviewed_by=s.reviewed_by, created_at=s.created_at, )