"""审计服务。""" from sqlalchemy.ext.asyncio import AsyncSession from app.core.errors import current_trace_id from app.core.security import redact_text from app.db.session import AsyncSessionLocal from app.models.safety import AuditEvent from app.repositories.audit import add_audit_event, list_audit_events from app.schemas.admin import AuditEntry from app.schemas.common import Page from app.services.serializers import audit_to_response # action 前缀 → 九枢标识(契约字典 §5.9)映射,供起居注按枢过滤与聚合。 _ACTION_PIVOT_PREFIXES: list[tuple[str, str]] = [ ("memory.", "memory"), ("tool.", "tool_runtime"), ("safety.", "safety_governor"), ("personality.", "personality_core"), ("goal.", "goal_orchestrator"), ("world.", "world_model"), ("plan.", "planner"), ("perception.", "perception"), ("agent.", "agent_society"), ] def infer_pivot(action: str) -> str | None: """根据 audit action 前缀推断所属枢;未命中返回 None。""" for prefix, pivot in _ACTION_PIVOT_PREFIXES: if action.startswith(prefix): return pivot return None def _summary(value: str | None) -> str | None: if value is None: return None redacted = redact_text(value) return redacted[:1000] async def record_audit_event( session: AsyncSession, *, action: str, pivot: str | None = None, character_id: str | None = None, run_id: str | None = None, actor: str = "system", risk_level: str = "L1", decision: str = "auto", reason: str | None = None, input_summary: str | None = None, output_summary: str | None = None, resource_type: str | None = None, resource_id: str | None = None, trace_id: str | None = None, ) -> AuditEvent: return await add_audit_event( session, AuditEvent( pivot=pivot or infer_pivot(action), character_id=character_id, run_id=run_id, action=action, actor=actor, risk_level=risk_level, decision=decision, reason=_summary(reason), input_summary=_summary(input_summary), output_summary=_summary(output_summary), # 显式 trace_id 优先(如沙箱事件携带的 trace_id),否则取请求上下文 current_trace_id trace_id=trace_id or current_trace_id(), resource_type=resource_type, resource_id=resource_id, ), ) async def query_audit_events( *, character_id: str | None = None, pivot: str | None = None, page: int = 1, page_size: int = 50, ) -> Page[AuditEntry]: """审计事件时间线(按创建时间倒序),可选按人物/枢过滤。""" async with AsyncSessionLocal() as session: items, total = await list_audit_events( session, pivot=pivot, character_id=character_id, page=page, page_size=page_size, ) return Page( items=[audit_to_response(item) for item in items], page=page, page_size=page_size, total=total, )