fix(awooop): mirror telegram outbound messages
This commit is contained in:
@@ -29,6 +29,7 @@ import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from uuid import NAMESPACE_URL, UUID, uuid5
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
@@ -79,6 +80,26 @@ def _is_noisy_failure_update(status_line: str) -> bool:
|
||||
or "AI 診斷工具失敗" in status_line
|
||||
)
|
||||
|
||||
|
||||
def _legacy_outbound_run_id(chat_id: str, provider_message_id: str) -> UUID:
|
||||
"""Legacy Telegram 發送尚未有 run_id 時,產生穩定 soft run_id 供 Channel Hub 串接。"""
|
||||
return uuid5(NAMESPACE_URL, f"awoooi:legacy-telegram:{chat_id}:{provider_message_id}")
|
||||
|
||||
|
||||
def _infer_outbound_message_type(text: str, payload: dict) -> str:
|
||||
"""將既有 Telegram 訊息映射成 AwoooP outbound_message 的有限分類。"""
|
||||
if "reply_to_message_id" in payload:
|
||||
if "失敗" in text or "錯誤" in text or "FAILED" in text:
|
||||
return "error"
|
||||
return "final"
|
||||
if payload.get("reply_markup"):
|
||||
return "approval_request"
|
||||
if "ACTION REQUIRED" in text or "待審" in text or "審批" in text:
|
||||
return "approval_request"
|
||||
if "失敗" in text or "錯誤" in text or "FAILED" in text:
|
||||
return "error"
|
||||
return "final"
|
||||
|
||||
# 2026-04-27 Claude Sonnet 4.6: B3 — LLM 動態 Telegram 按鈕 Feature Flag
|
||||
# true → 優先使用 ActionPlan.recommended_actions 動態生成按鈕
|
||||
# false → 維持現有 callback_action_spec.yaml 路徑(預設,向下相容)
|
||||
@@ -1511,6 +1532,11 @@ class TelegramGateway:
|
||||
result_val = result.get("result")
|
||||
if isinstance(result_val, dict) and "message_id" in result_val:
|
||||
span.set_attribute("telegram.message_id", result_val["message_id"])
|
||||
await self._mirror_outbound_message(
|
||||
method=method,
|
||||
payload=payload,
|
||||
provider_message_id=str(result_val["message_id"]),
|
||||
)
|
||||
|
||||
span.set_status(trace.Status(trace.StatusCode.OK))
|
||||
return result
|
||||
@@ -1541,6 +1567,52 @@ class TelegramGateway:
|
||||
)
|
||||
raise TelegramGatewayError(safe_error) from None
|
||||
|
||||
async def _mirror_outbound_message(
|
||||
self,
|
||||
*,
|
||||
method: str,
|
||||
payload: dict,
|
||||
provider_message_id: str,
|
||||
) -> None:
|
||||
"""將 legacy Telegram 出站訊息鏡像到 AwoooP,不改變實際發送行為。"""
|
||||
if method != "sendMessage":
|
||||
return
|
||||
|
||||
chat_id = str(payload.get("chat_id") or "")
|
||||
text = str(payload.get("text") or payload.get("caption") or "")
|
||||
if not chat_id or not text:
|
||||
return
|
||||
|
||||
try:
|
||||
from src.core.context import get_current_project_id
|
||||
from src.db.base import get_db_context
|
||||
from src.services.channel_hub import record_outbound_message
|
||||
|
||||
project_id = get_current_project_id() or "awoooi"
|
||||
run_id = _legacy_outbound_run_id(chat_id, provider_message_id)
|
||||
async with get_db_context(project_id) as db:
|
||||
await record_outbound_message(
|
||||
db,
|
||||
project_id=project_id,
|
||||
run_id=run_id,
|
||||
channel_type="telegram",
|
||||
channel_chat_id=chat_id,
|
||||
message_type=_infer_outbound_message_type(text, payload),
|
||||
content=text,
|
||||
provider_message_id=provider_message_id,
|
||||
send_status="sent",
|
||||
triggered_by_state="legacy_gateway",
|
||||
is_shadow=False,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"telegram_outbound_mirror_failed",
|
||||
method=method,
|
||||
chat_id=chat_id,
|
||||
provider_message_id=provider_message_id,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
async def _build_inline_keyboard(
|
||||
self,
|
||||
approval_id: str,
|
||||
|
||||
@@ -231,6 +231,43 @@ async def test_append_incident_update_suppresses_duplicate_failure_across_incide
|
||||
]
|
||||
|
||||
|
||||
def test_outbound_message_type_inference():
|
||||
"""Legacy Telegram 訊息 mirror 到 Channel Hub 時,必須映射成有限分類。"""
|
||||
|
||||
assert (
|
||||
telegram_gateway_module._infer_outbound_message_type(
|
||||
"ℹ️ ACTION REQUIRED | 低風險",
|
||||
{"reply_markup": {"inline_keyboard": []}},
|
||||
)
|
||||
== "approval_request"
|
||||
)
|
||||
assert (
|
||||
telegram_gateway_module._infer_outbound_message_type(
|
||||
"🤖❌ [AUTO] AI 自動修復失敗",
|
||||
{"reply_to_message_id": 123},
|
||||
)
|
||||
== "error"
|
||||
)
|
||||
assert (
|
||||
telegram_gateway_module._infer_outbound_message_type(
|
||||
"✅ 執行成功",
|
||||
{"reply_to_message_id": 123},
|
||||
)
|
||||
== "final"
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_outbound_run_id_is_stable():
|
||||
"""沒有正式 run_id 的 legacy Telegram 發送,要有穩定 soft run_id 供查詢串接。"""
|
||||
|
||||
first = telegram_gateway_module._legacy_outbound_run_id("-1001", "42")
|
||||
second = telegram_gateway_module._legacy_outbound_run_id("-1001", "42")
|
||||
other = telegram_gateway_module._legacy_outbound_run_id("-1001", "43")
|
||||
|
||||
assert first == second
|
||||
assert first != other
|
||||
|
||||
|
||||
class TestSentryErrorMessage:
|
||||
"""測試 Sentry 錯誤訊息"""
|
||||
|
||||
|
||||
@@ -4388,3 +4388,32 @@ HTTP:
|
||||
```
|
||||
|
||||
判讀:Telegram 失敗摘要跨 incident 去重已進正式 image;後續若仍看到洗版,下一步要查的是 firing alert fingerprint 聚合與 `conversation_event` / `outbound_message` 是否仍有 caller 繞過 `append_incident_update()`。
|
||||
|
||||
## 2026-05-07(台北)— Telegram 出站訊息鏡像到 AwoooP Outbound
|
||||
|
||||
**觸發**:統帥指出 Telegram 訊息仍然太雜,難以分辨哪些是 AI 自動化修復、哪些是 AI 無法修復需人工接手;AwoooP 需要成為治理與操作層,而不是與 Telegram 分裂。
|
||||
|
||||
### 改動
|
||||
|
||||
- 在 legacy `TelegramGateway._send_request()` 成功執行 `sendMessage` 後,將出站訊息鏡像到 `awooop_outbound_message`。
|
||||
- 沒有正式 `run_id` 的 legacy 發送使用穩定 soft run id:`awoooi:legacy-telegram:{chat_id}:{provider_message_id}` 的 UUIDv5。
|
||||
- legacy 訊息先映射成有限分類:`approval_request`、`final`、`error`,供 AwoooP Run / Timeline 後續彙整。
|
||||
- 鏡像採 fail-open:DB / RLS / schema 或 Channel Hub 失敗時只寫 `telegram_outbound_mirror_failed`,不得影響 Telegram 原本送達。
|
||||
|
||||
### 驗證
|
||||
|
||||
```text
|
||||
/Users/ogt/awoooi/apps/api/.venv/bin/python -m py_compile \
|
||||
apps/api/src/services/telegram_gateway.py \
|
||||
apps/api/tests/test_telegram_message_templates.py
|
||||
# passed
|
||||
|
||||
DATABASE_URL='postgresql+asyncpg://test:test@localhost:5432/test' \
|
||||
/Users/ogt/awoooi/apps/api/.venv/bin/python -m pytest \
|
||||
apps/api/tests/test_telegram_message_templates.py -q
|
||||
# 22 passed
|
||||
```
|
||||
|
||||
### 判讀
|
||||
|
||||
這一步是 mirror-first,不改 Telegram 實際發送、不切 Channel Hub 主路徑。下一步要補的是 direct Bot API caller 收斂與 firing alert fingerprint 聚合,讓戰情室只接收需要人注意的摘要,完整時間線回到 AwoooP。
|
||||
|
||||
@@ -38,6 +38,7 @@ Telegram 不應是完整執行日誌,也不應承載所有 AI 推理細節。T
|
||||
- `TelegramMessage` 主卡新增「處置狀態」。
|
||||
- `append_incident_update()` 對同一 incident 的相同狀態做 5 分鐘 Redis 去重。
|
||||
- `append_incident_update()` 對相同的「AI 自動修復失敗 / AI 診斷工具失敗」摘要增加 10 分鐘跨 incident 去重;每個 incident 仍會移除原卡危險按鈕,但 Telegram 不再重複 reply 同一個失敗摘要。
|
||||
- `TelegramGateway._send_request()` 對成功送出的 legacy `sendMessage` 增加 AwoooP `awooop_outbound_message` 鏡像。鏡像失敗只記錄 `telegram_outbound_mirror_failed`,不能影響 Telegram 正常送達。
|
||||
- 既有 `詳情 / 重診 / 歷史` 按鈕保留,讓 Telegram 保持輕量,細節回到控制台。
|
||||
|
||||
## 後續建議
|
||||
|
||||
Reference in New Issue
Block a user