fix(awooop): mirror telegram outbound messages
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m4s
CD Pipeline / build-and-deploy (push) Successful in 3m57s
CD Pipeline / post-deploy-checks (push) Successful in 1m27s

This commit is contained in:
Your Name
2026-05-07 00:23:32 +08:00
parent 012cd27b4a
commit 9365bdab93
4 changed files with 139 additions and 0 deletions

View File

@@ -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,

View File

@@ -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 錯誤訊息"""

View File

@@ -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-openDB / 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。

View File

@@ -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 保持輕量,細節回到控制台。
## 後續建議