103 lines
3.1 KiB
Python
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,
|
|
)
|