feat(awooop): show callback reply states in timeline
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2035,6 +2035,10 @@
|
||||
"received": "已接收",
|
||||
"running": "執行中",
|
||||
"sent": "已送出",
|
||||
"callbackReplySent": "Callback 已送出",
|
||||
"callbackReplyFallbackSent": "Callback fallback",
|
||||
"callbackReplyRescueSent": "Callback 救援",
|
||||
"callbackReplyFailed": "Callback 失敗",
|
||||
"shadow": "Shadow",
|
||||
"success": "成功",
|
||||
"timeout": "已超時",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user