Files
2026-08-21 16:00:13 +08:00

144 lines
4.7 KiB
Python

"""内驱力审核服务(次生目标 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,
)