Files
awoooi/apps/api/src/services/agent99_telegram_lifecycle.py
ogt a0a5e850e1
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m26s
CD Pipeline / build-and-deploy (push) Successful in 14m46s
CD Pipeline / post-deploy-checks (push) Successful in 2m59s
fix(agent99): reconcile telegram lifecycle ack
2026-07-15 22:18:34 +08:00

204 lines
7.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Canonical, durable Telegram lifecycle delivery for Windows Agent99."""
from __future__ import annotations
import asyncio
import html
from typing import Any
from src.models.agent99_completion import Agent99TelegramLifecycleRequest
from src.services.telegram_gateway import get_telegram_gateway
_LIFECYCLE_LABELS = {
"detected": "已偵測 / DETECTED",
"investigating": "調查中 / INVESTIGATING",
"remediating": "修復中 / REMEDIATING",
"verifying": "驗證中 / VERIFYING",
"recovered": "已恢復 / RECOVERED",
"blocked": "需介入 / ACTION REQUIRED",
"unknown": "狀態未知 / UNKNOWN",
}
_SEVERITY_LABELS = {
"critical": "重大 / CRITICAL",
"warning": "警告 / WARNING",
"info": "資訊 / INFO",
}
_DURABLE_RECONCILE_DELAYS_SECONDS = (0.25, 0.5, 1.0)
def _format_lifecycle_card(payload: Agent99TelegramLifecycleRequest) -> str:
def esc(value: object) -> str:
return html.escape(str(value or ""))
return "\n".join(
[
f"<b>Agent99 事件|{esc(payload.title)}</b>",
(
f"狀態:<b>{esc(_LIFECYCLE_LABELS[payload.lifecycle])}</b>"
f" 等級:<b>{esc(_SEVERITY_LABELS[payload.severity])}</b>"
),
"────────────────────",
f"<b>資產</b> {esc(payload.target)}",
f"<b>影響</b> {esc(payload.impact)}",
f"<b>AI 處置</b> {esc(payload.action)}",
f"<b>驗證</b> {esc(payload.verification)}",
"────────────────────",
(
f"事件:<code>{esc(payload.incident_id)}</code> "
f"發生:{payload.occurrences} 次 耗時:{esc(payload.duration_text)}"
),
f"下一次更新:{esc(payload.next_update)}",
f"時間:{esc(payload.occurred_at.isoformat())}",
]
)[:4096]
def _format_lifecycle_caption(payload: Agent99TelegramLifecycleRequest) -> str:
"""Keep the visual card caption scannable on mobile Telegram clients."""
def esc(value: object, limit: int) -> str:
rendered: list[str] = []
rendered_length = 0
truncated = False
for character in str(value or ""):
encoded = html.escape(character)
if rendered_length + len(encoded) > limit - 3:
truncated = True
break
rendered.append(encoded)
rendered_length += len(encoded)
return "".join(rendered) + ("..." if truncated else "")
return "\n".join(
[
(
f"<b>{esc(_LIFECYCLE_LABELS[payload.lifecycle], 48)}</b>"
f"{esc(_SEVERITY_LABELS[payload.severity], 32)}"
),
f"<b>{esc(payload.title, 100)}</b>",
f"資產:{esc(payload.target, 80)}",
f"Agent99{esc(payload.action, 220)}",
f"驗證:{esc(payload.verification, 180)}",
(
f"事件 <code>{esc(payload.incident_id, 180)}</code>"
f"發生 {payload.occurrences} 次|{esc(payload.duration_text, 60)}"
),
]
)
async def deliver_agent99_telegram_lifecycle(
payload: Agent99TelegramLifecycleRequest,
) -> dict[str, Any]:
"""Send once through the registered AWOOOI SRE route and verify its ack."""
visual_png = payload.visual.png_bytes() if payload.visual is not None else None
gateway = get_telegram_gateway()
send_kwargs = {
"delivery_id": payload.delivery_id,
"incident_id": payload.incident_id,
"state_key": payload.state_key,
"text": (
_format_lifecycle_caption(payload)
if visual_png is not None
else _format_lifecycle_card(payload)
),
"priority": "P0" if payload.severity == "critical" else "P1",
"project_id": payload.project_id,
"reply_to_message_id": payload.reply_to_message_id,
"visual_png": visual_png,
"visual_sha256": (
payload.visual.sha256 if payload.visual is not None else None
),
}
response = await gateway.send_agent99_lifecycle_receipt(**send_kwargs)
provider_send_performed = bool(
isinstance(response, dict)
and response.get("_awooop_provider_send_performed") is True
)
reconciliation_attempts = 0
# A provider send can finish before the durable outbox finalizer becomes
# readable. Re-entering with the same delivery identity only re-reads the
# send-once reservation; it cannot send a second provider message.
for delay_seconds in _DURABLE_RECONCILE_DELAYS_SECONDS:
delivery_status = (
str(response.get("_awooop_delivery_status") or "")
if isinstance(response, dict)
else ""
)
if delivery_status != "pending_unknown":
break
await asyncio.sleep(delay_seconds)
reconciliation_attempts += 1
response = await gateway.send_agent99_lifecycle_receipt(**send_kwargs)
provider_send_performed = bool(
provider_send_performed
or (
isinstance(response, dict)
and response.get("_awooop_provider_send_performed") is True
)
)
result = response.get("result") if isinstance(response, dict) else None
message_id = str(result.get("message_id") or "") if isinstance(result, dict) else ""
delivery_context = (
response.get("_awooop_delivery_context")
if isinstance(response, dict)
and isinstance(response.get("_awooop_delivery_context"), dict)
else {}
)
delivery_status = (
str(response.get("_awooop_delivery_status") or "")
if isinstance(response, dict)
else ""
)
durable_ack = bool(
isinstance(response, dict)
and response.get("_awooop_outbound_mirror_acknowledged") is True
)
destination_verified = bool(
delivery_status == "sent_reused"
or delivery_context.get("destination_binding_verified") is True
)
ok = bool(
durable_ack
and destination_verified
and message_id
and delivery_status in {"sent", "sent_reused"}
)
visual_requested = payload.visual is not None
visual_sent = bool(
ok
and visual_requested
and isinstance(response, dict)
and response.get("_awooop_visual_delivery_verified") is True
)
return {
"schema_version": "agent99_telegram_lifecycle_receipt_v1",
"ok": ok,
"delivery_id": payload.delivery_id,
"incident_id": payload.incident_id,
"event_type": payload.event_type,
"lifecycle": payload.lifecycle,
"provider_message_id": message_id or None,
"delivery_status": delivery_status or "unknown",
"durable_outbound_acknowledged": durable_ack,
"destination_binding_verified": destination_verified,
"provider_send_performed": provider_send_performed,
"durable_reconciliation_attempts": reconciliation_attempts,
"visual_requested": visual_requested,
"visual_sent": visual_sent,
"visual_delivery_status": (
delivery_status if visual_requested else "not_requested"
),
"visual_sha256": (
payload.visual.sha256 if payload.visual is not None else None
),
"stores_secret": False,
"raw_response_stored": False,
"raw_visual_stored": False,
}