feat(awooop): show callback reply states in timeline
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 1m8s
CD Pipeline / build-and-deploy (push) Successful in 3m24s
CD Pipeline / post-deploy-checks (push) Successful in 1m17s

This commit is contained in:
Your Name
2026-05-18 14:54:49 +08:00
parent ed37000eba
commit 1a16e083e7
5 changed files with 164 additions and 12 deletions

View File

@@ -271,10 +271,26 @@ def _outbound_timeline_title(
channel_type: str,
message_type: str,
content_preview: str | None,
callback_reply: dict[str, Any] | None = None,
) -> str:
"""將 legacy Telegram outbound 分類成 Operator 看得懂的語義標題。"""
channel = channel_type.upper()
preview = content_preview or ""
if callback_reply:
action = str(callback_reply.get("action") or "").strip()
status = str(callback_reply.get("status") or "").strip()
action_label = {
"detail": "詳情",
"history": "歷史",
}.get(action, action or "Callback")
status_label = {
"callback_reply_sent": "已送出",
"callback_reply_fallback_sent": " fallback 已送出",
"callback_reply_rescue_sent": "救援已送出",
"callback_reply_failed": "送出失敗",
}.get(status, status or "已記錄")
return f"{channel}{action_label}回覆{status_label}"
if "RUNBOOK REVIEW" in preview:
return f"{channel}Runbook 待人工審核"
if "[AWOOOI CI/CD]" in preview or "AWOOOI CI/CD" in preview:
@@ -299,6 +315,73 @@ def _outbound_timeline_title(
return f"{channel}{fallback}"
def _outbound_callback_reply(source_envelope: Any) -> dict[str, Any] | None:
"""Extract Telegram callback reply evidence from outbound source envelope."""
if not isinstance(source_envelope, dict):
return None
callback_reply = source_envelope.get("callback_reply")
return callback_reply if isinstance(callback_reply, dict) else None
def _outbound_timeline_status(
send_status: str,
callback_reply: dict[str, Any] | None,
) -> str:
"""Prefer callback delivery status when the outbound row records one."""
if callback_reply:
status = callback_reply.get("status")
if isinstance(status, str) and status:
return status
return send_status
def _outbound_timeline_summary(
*,
content_preview: str | None,
send_error: str | None,
callback_reply: dict[str, Any] | None,
) -> str | None:
"""Summarize callback reply state without forcing operators to inspect raw JSON."""
if not callback_reply:
return content_preview or send_error
parts = [
f"callback={callback_reply.get('action') or '--'}",
f"incident={callback_reply.get('incident_id') or '--'}",
f"status={callback_reply.get('status') or '--'}",
]
parse_mode = callback_reply.get("parse_mode")
if parse_mode:
parts.append(f"parse_mode={parse_mode}")
error = callback_reply.get("error")
if error:
parts.append(f"error={error}")
if content_preview:
parts.append(str(content_preview))
return " · ".join(parts)
def _outbound_timeline_metadata(
row: AwoooPOutboundMessage,
callback_reply: dict[str, Any] | None,
) -> dict[str, Any]:
"""Build compact outbound metadata with callback fields first when present."""
metadata: dict[str, Any] = {}
if callback_reply:
metadata.update({
"callback_status": callback_reply.get("status"),
"callback_action": callback_reply.get("action"),
"callback_incident_id": callback_reply.get("incident_id"),
"callback_parse_mode": callback_reply.get("parse_mode"),
})
metadata.update({
"message_type": row.message_type,
"provider_message_id": row.provider_message_id,
"triggered_by_state": row.triggered_by_state,
})
return metadata
def _mcp_gateway_summary_row(row: AwoooPMcpGatewayAudit) -> dict[str, Any]:
"""Convert SQLAlchemy audit rows into the truth-chain summary shape."""
return {
@@ -971,8 +1054,10 @@ async def get_run_detail(
for row in inbound_events
]
outbound_items = [
{
outbound_items = []
for row in outbound_messages:
callback_reply = _outbound_callback_reply(row.source_envelope)
outbound_items.append({
"message_id": row.message_id,
"channel_type": row.channel_type,
"message_type": row.message_type,
@@ -983,9 +1068,8 @@ async def get_run_detail(
"queued_at": row.queued_at,
"sent_at": row.sent_at,
"triggered_by_state": row.triggered_by_state,
}
for row in outbound_messages
]
"callback_reply": callback_reply,
})
def _mcp_item(row: AwoooPMcpGatewayAudit) -> dict[str, Any]:
gate_result = row.gate_result if isinstance(row.gate_result, dict) else {}
@@ -1136,6 +1220,7 @@ async def get_run_detail(
)
)
for row in outbound_messages:
callback_reply = _outbound_callback_reply(row.source_envelope)
timeline.append(
_timeline_item(
ts=row.sent_at or row.queued_at,
@@ -1144,14 +1229,15 @@ async def get_run_detail(
row.channel_type,
row.message_type,
row.content_preview,
callback_reply,
),
status=row.send_status,
summary=row.content_preview or row.send_error,
metadata={
"message_type": row.message_type,
"provider_message_id": row.provider_message_id,
"triggered_by_state": row.triggered_by_state,
},
status=_outbound_timeline_status(row.send_status, callback_reply),
summary=_outbound_timeline_summary(
content_preview=row.content_preview,
send_error=row.send_error,
callback_reply=callback_reply,
),
metadata=_outbound_timeline_metadata(row, callback_reply),
)
)
if run.completed_at:

View File

@@ -7,6 +7,8 @@ from src.services.platform_operator_service import (
_legacy_mcp_timeline_summary,
_list_filter_context_limit,
_outbound_timeline_title,
_outbound_timeline_status,
_outbound_timeline_summary,
_remediation_summary_matches_incident_id,
_remediation_summary_matches_status,
_remediation_timeline_summary,
@@ -61,6 +63,54 @@ def test_outbound_timeline_title_falls_back_to_human_label() -> None:
assert title == "TELEGRAM漸進式狀態回饋"
def test_outbound_timeline_title_labels_callback_reply_fallback() -> None:
callback_reply = {
"status": "callback_reply_fallback_sent",
"action": "history",
"incident_id": "INC-20260513-79ED5E",
"parse_mode": "plain_text",
}
title = _outbound_timeline_title(
"telegram",
"final",
"事件歷史統計 INC-20260513-79ED5E",
callback_reply,
)
summary = _outbound_timeline_summary(
content_preview="事件歷史統計 INC-20260513-79ED5E",
send_error=None,
callback_reply=callback_reply,
)
assert title == "TELEGRAM歷史回覆 fallback 已送出"
assert _outbound_timeline_status("sent", callback_reply) == (
"callback_reply_fallback_sent"
)
assert "callback=history" in summary
assert "incident=INC-20260513-79ED5E" in summary
assert "parse_mode=plain_text" in summary
def test_outbound_timeline_title_labels_callback_reply_failure() -> None:
callback_reply = {
"status": "callback_reply_failed",
"action": "detail",
"incident_id": "INC-20260513-79ED5E",
"error": "HTTP error: 400",
}
assert _outbound_timeline_title("telegram", "error", None, callback_reply) == (
"TELEGRAM詳情回覆送出失敗"
)
assert _outbound_timeline_status("failed", callback_reply) == "callback_reply_failed"
assert "error=HTTP error: 400" in _outbound_timeline_summary(
content_preview=None,
send_error="HTTP error: 400",
callback_reply=callback_reply,
)
def test_collect_run_incident_ids_reads_source_refs_and_legacy_text() -> None:
run = SimpleNamespace(
trigger_ref="not-an-incident",

View File

@@ -2034,6 +2034,10 @@
"received": "Received",
"running": "Running",
"sent": "Sent",
"callbackReplySent": "Callback sent",
"callbackReplyFallbackSent": "Callback fallback",
"callbackReplyRescueSent": "Callback rescue",
"callbackReplyFailed": "Callback failed",
"shadow": "Shadow",
"success": "Success",
"timeout": "Timed out",

View File

@@ -2035,6 +2035,10 @@
"received": "已接收",
"running": "執行中",
"sent": "已送出",
"callbackReplySent": "Callback 已送出",
"callbackReplyFallbackSent": "Callback fallback",
"callbackReplyRescueSent": "Callback 救援",
"callbackReplyFailed": "Callback 失敗",
"shadow": "Shadow",
"success": "成功",
"timeout": "已超時",

View File

@@ -213,6 +213,10 @@ const STATUS_STYLE: Record<string, string> = {
completed: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
success: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
sent: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
callback_reply_sent: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
callback_reply_fallback_sent: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
callback_reply_rescue_sent: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
callback_reply_failed: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
running: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
received: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]",
waiting_approval: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
@@ -236,6 +240,10 @@ const STATUS_TRANSLATION_KEYS: Record<string, string> = {
received: "statuses.received",
running: "statuses.running",
sent: "statuses.sent",
callback_reply_sent: "statuses.callbackReplySent",
callback_reply_fallback_sent: "statuses.callbackReplyFallbackSent",
callback_reply_rescue_sent: "statuses.callbackReplyRescueSent",
callback_reply_failed: "statuses.callbackReplyFailed",
shadow: "statuses.shadow",
success: "statuses.success",
timeout: "statuses.timeout",