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

103 lines
3.1 KiB
Python

"""审计服务。"""
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,
)