feat(awooop): surface gateway summary in details
All checks were successful
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / tests (push) Successful in 1m4s
CD Pipeline / build-and-deploy (push) Successful in 3m47s
CD Pipeline / post-deploy-checks (push) Successful in 1m16s

This commit is contained in:
Your Name
2026-05-13 11:49:37 +08:00
parent 51528b2cf9
commit c486087294
7 changed files with 299 additions and 5 deletions

View File

@@ -28,6 +28,7 @@ from src.db.awooop_models import (
)
from src.db.base import get_db_context
from src.services.audit_sink import write_audit
from src.services.awooop_truth_chain_service import _summarize_gateway_mcp
from src.services.awooop_approval_token import issue_approval_token, record_approval
from src.services.run_state_machine import transition
@@ -240,6 +241,17 @@ def _outbound_timeline_title(
return f"{channel}{fallback}"
def _mcp_gateway_summary_row(row: AwoooPMcpGatewayAudit) -> dict[str, Any]:
"""Convert SQLAlchemy audit rows into the truth-chain summary shape."""
return {
"agent_id": row.agent_id,
"tool_name": row.tool_name,
"result_status": row.result_status,
"block_gate": row.block_gate,
"gate_result": row.gate_result or {},
}
async def get_run_detail(
run_id: str,
project_id: str | None = None,
@@ -373,18 +385,27 @@ async def get_run_detail(
for row in outbound_messages
]
mcp_items = [
{
def _mcp_item(row: AwoooPMcpGatewayAudit) -> dict[str, Any]:
gate_result = row.gate_result if isinstance(row.gate_result, dict) else {}
return {
"call_id": row.call_id,
"agent_id": row.agent_id,
"tool_name": row.tool_name,
"result_status": row.result_status,
"block_gate": row.block_gate,
"block_reason": row.block_reason,
"latency_ms": row.latency_ms,
"created_at": row.created_at,
"required_scope": gate_result.get("required_scope"),
"policy_enforced": gate_result.get("policy_enforced"),
"is_shadow": gate_result.get("is_shadow"),
"gate_result": gate_result,
}
for row in mcp_calls
]
mcp_items = [_mcp_item(row) for row in mcp_calls]
mcp_gateway_summary = _summarize_gateway_mcp([
_mcp_gateway_summary_row(row) for row in mcp_calls
])
timeline: list[dict[str, Any]] = [
_timeline_item(
@@ -433,15 +454,28 @@ async def get_run_detail(
)
)
for row in mcp_calls:
gate_result = row.gate_result if isinstance(row.gate_result, dict) else {}
scope = gate_result.get("required_scope")
policy_enforced = gate_result.get("policy_enforced")
summary = row.block_reason
if summary is None:
summary = (
f"agent={row.agent_id or 'unknown'}"
f" scope={scope or 'unknown'}"
f" policy_enforced={policy_enforced}"
)
timeline.append(
_timeline_item(
ts=row.created_at,
kind="mcp",
title=f"MCP: {row.tool_name}",
status=row.result_status,
summary=row.block_reason,
summary=summary,
metadata={
"agent_id": row.agent_id,
"block_gate": row.block_gate,
"required_scope": scope,
"policy_enforced": policy_enforced,
"latency_ms": row.latency_ms,
},
)
@@ -487,6 +521,7 @@ async def get_run_detail(
"inbound_events": inbound_items,
"outbound_messages": outbound_items,
"mcp_calls": mcp_items,
"mcp_gateway": mcp_gateway_summary,
"timeline": timeline,
"counts": {
"steps": len(step_items),

View File

@@ -71,6 +71,62 @@ _TELEGRAM_BOT_URL_RE = re.compile(r"(api\.telegram\.org/bot)[^/\s]+")
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b")
def _top_gateway_bucket(
buckets: list[dict[str, object]],
field: str,
) -> str | None:
if not buckets:
return None
top = max(buckets, key=lambda row: int(row.get("total") or 0))
value = top.get(field)
if value is None:
return None
return f"{value} ({top.get('total', 0)})"
def _format_gateway_summary_lines(summary: dict[str, object] | None) -> list[str]:
if not summary or int(summary.get("total") or 0) <= 0:
return []
by_agent = summary.get("by_agent") if isinstance(summary.get("by_agent"), list) else []
by_tool = summary.get("by_tool") if isinstance(summary.get("by_tool"), list) else []
by_scope = summary.get("by_scope") if isinstance(summary.get("by_scope"), list) else []
blockers = summary.get("blockers") if isinstance(summary.get("blockers"), list) else []
lines = [
"",
"🛡️ <b>MCP Gateway</b>",
(
"階段: "
f"<code>{html.escape(str(summary.get('stage') or 'unknown'))}</code>"
" / "
f"<code>{html.escape(str(summary.get('stage_status') or 'unknown'))}</code>"
),
(
"治理: "
f"first-class <code>{int(summary.get('first_class_total') or 0)}</code> / "
f"policy <code>{int(summary.get('policy_enforced_total') or 0)}</code> / "
f"legacy <code>{int(summary.get('legacy_bridge_total') or 0)}</code>"
),
]
agent = _top_gateway_bucket(by_agent, "agent_id")
tool = _top_gateway_bucket(by_tool, "tool_name")
scope = _top_gateway_bucket(by_scope, "required_scope")
if agent:
lines.append(f"Agent: <code>{html.escape(agent)}</code>")
if tool:
lines.append(f"Tool: <code>{html.escape(tool)}</code>")
if scope:
lines.append(f"Scope: <code>{html.escape(scope)}</code>")
if blockers:
lines.append(
"卡點: "
+ html.escape(", ".join(str(item) for item in blockers[:3]))
)
return lines
def _sanitize_telegram_error(text: str) -> str:
"""遮蔽 Telegram Bot URL 中的 token避免例外字串污染 log / trace。"""
return _TELEGRAM_BOT_URL_RE.sub(r"\1<redacted>", text)
@@ -5064,6 +5120,25 @@ class TelegramGateway:
+ html.escape(", ".join(mismatch_codes[:4]))
)
try:
from src.services.awooop_truth_chain_service import fetch_truth_chain
truth_chain = await fetch_truth_chain(
source_id=incident_id,
project_id=getattr(incident, "project_id", None) or "awoooi",
)
gateway_summary = (
(truth_chain.get("mcp") or {})
.get("awooop_gateway")
)
lines += _format_gateway_summary_lines(gateway_summary)
except Exception as truth_exc:
logger.warning(
"incident_detail_truth_chain_summary_failed",
incident_id=incident_id,
error=str(truth_exc),
)
await self.send_notification("\n".join(lines))
except Exception as e: