"""息壤(记忆域)API(api-contracts §3.5)。""" from __future__ import annotations import json from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, Field from sqlalchemy import select from app.api.v1.deps import get_current_user from app.core.errors import AppError from app.db.session import AsyncSessionLocal from app.domains import xirang from app.models.character import Character from app.models.xirang import FactualRecord, Memory from app.schemas.auth import CurrentUser router = APIRouter(tags=["xirang"]) class MemorizeRequest(BaseModel): label: str = Field(description="fact / insight / opinion / hypothesis") class CorrectRequest(BaseModel): new_content: str = Field(min_length=1, max_length=8000) def _memory_view(m: Memory, with_content: bool = True) -> dict: return { "id": m.id, "character_id": m.character_id, "tier": m.tier, "type": m.type, "status": m.status, "source": m.source, "content": m.content if with_content else "", "source_refs": json.loads(m.source_refs_json or "[]"), "sensitivity": m.sensitivity, "priority": m.priority, "confidence": m.confidence, "vitality": round(m.vitality, 3), "realm": m.realm, "prev_version_id": m.prev_version_id, "recall_count": m.recall_count, "last_recalled_at": m.last_recalled_at.isoformat() if m.last_recalled_at else None, "created_at": m.created_at.isoformat(), } async def _owned_character(character_id: str, owner_scope: str) -> Character: async with AsyncSessionLocal() as session: character = await session.get(Character, character_id) if character is None or character.owner_scope != owner_scope: raise AppError("not_found", "人物不存在。") return character @router.get("/characters/{character_id}/memories") async def list_memories( character_id: str, tier: str | None = Query(default=None), status: str | None = Query(default=None), type_: str | None = Query(default=None, alias="type"), user: CurrentUser = Depends(get_current_user), ) -> list[dict]: await _owned_character(character_id, user.owner_scope) async with AsyncSessionLocal() as session: stmt = select(Memory).where(Memory.character_id == character_id) if tier: stmt = stmt.where(Memory.tier == tier) if status: stmt = stmt.where(Memory.status == status) if type_: stmt = stmt.where(Memory.type == type_) stmt = stmt.order_by(Memory.created_at.desc()).limit(200) rows = (await session.execute(stmt)).scalars().all() # 高敏不展示原文(sensitivity=high 摘要化);墓碑不承载原文 return [ _memory_view( m, with_content=not (m.sensitivity == "high" or m.status == "tombstoned"), ) for m in rows ] @router.get("/memories/{memory_id}") async def get_memory(memory_id: str, user: CurrentUser = Depends(get_current_user)) -> dict: async with AsyncSessionLocal() as session: memory = await session.get(Memory, memory_id) if memory is not None: character = await session.get(Character, memory.character_id) if memory is None or character is None or character.owner_scope != user.owner_scope: raise AppError("not_found", "记忆不存在。") return _memory_view(memory) @router.post("/memories/{memory_id}/confirm") async def confirm_memory_api(memory_id: str, user: CurrentUser = Depends(get_current_user)) -> dict: memory = await xirang.confirm_memory(memory_id, user.owner_scope) return _memory_view(memory) @router.post("/memories/{memory_id}/deny") async def deny_memory_api(memory_id: str, user: CurrentUser = Depends(get_current_user)) -> dict: return await xirang.deny_memory(memory_id, user.owner_scope) @router.post("/memories/{memory_id}/correct") async def correct_memory_api( memory_id: str, payload: CorrectRequest, user: CurrentUser = Depends(get_current_user) ) -> dict: memory = await xirang.correct_memory(memory_id, user.owner_scope, payload.new_content) return _memory_view(memory) @router.delete("/memories/{memory_id}", status_code=204) async def delete_memory_api(memory_id: str, user: CurrentUser = Depends(get_current_user)) -> None: await xirang.delete_memory(memory_id, user.owner_scope) @router.post("/nodes/{node_id}/memorize", status_code=201) async def memorize_node_api( node_id: str, payload: MemorizeRequest, user: CurrentUser = Depends(get_current_user) ) -> dict: memory = await xirang.memorize_node( node_id=node_id, owner_scope=user.owner_scope, label=payload.label ) return _memory_view(memory) @router.get("/characters/{character_id}/factual-records") async def list_factual_records( character_id: str, user: CurrentUser = Depends(get_current_user) ) -> list[dict]: await _owned_character(character_id, user.owner_scope) async with AsyncSessionLocal() as session: rows = ( await session.execute( select(FactualRecord) .where(FactualRecord.character_id == character_id) .order_by(FactualRecord.occurred_at.desc()) .limit(100) ) ).scalars().all() return [ { "id": r.id, "canonization_id": r.canonization_id, "summary": r.summary, "evidence": json.loads(r.evidence_json or "{}"), "occurred_at": r.occurred_at.isoformat(), } for r in rows ]