From 7fa9f743ddaaa16c9f9225ecff8816a014b8bc25 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 13 May 2026 12:04:26 +0800 Subject: [PATCH] fix(awooop): strengthen outbound truth references --- .../services/awooop_truth_chain_service.py | 2 + apps/api/src/services/channel_hub.py | 3 +- apps/api/src/services/telegram_gateway.py | 8 +++ .../test_channel_hub_grouped_alert_events.py | 29 ++++++++++ .../test_telegram_gateway_error_sanitizer.py | 8 ++- docs/LOGBOOK.md | 54 +++++++++++++++++++ 6 files changed, 102 insertions(+), 2 deletions(-) diff --git a/apps/api/src/services/awooop_truth_chain_service.py b/apps/api/src/services/awooop_truth_chain_service.py index 413dce68..53cb947d 100644 --- a/apps/api/src/services/awooop_truth_chain_service.py +++ b/apps/api/src/services/awooop_truth_chain_service.py @@ -785,6 +785,8 @@ async def fetch_truth_chain(source_id: str, project_id: str = "awoooi") -> dict[ AND ( run_id::text = :source_id OR content_preview ILIKE :needle + OR coalesce(source_envelope #> '{source_refs,incident_ids}', '[]'::jsonb) ? :source_id + OR coalesce(source_envelope #> '{source_refs,code_refs}', '[]'::jsonb) ? :source_id ) ORDER BY queued_at DESC LIMIT :limit diff --git a/apps/api/src/services/channel_hub.py b/apps/api/src/services/channel_hub.py index ff60ea5e..4d61830c 100644 --- a/apps/api/src/services/channel_hub.py +++ b/apps/api/src/services/channel_hub.py @@ -514,7 +514,7 @@ async def record_outbound_message( content_hash, content_preview, content_redacted, redaction_version, source_envelope, provider_message_id, - send_status, queued_at, + send_status, queued_at, sent_at, triggered_by_state, waiting_since ) VALUES ( :project_id, :run_id, :conversation_event_id, @@ -523,6 +523,7 @@ async def record_outbound_message( :redaction_version, CAST(:source_envelope AS jsonb), :provider_message_id, :send_status, NOW(), + CASE WHEN :send_status = 'sent' THEN NOW() ELSE NULL END, :triggered_by_state, :waiting_since ) RETURNING message_id diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 87546067..f00d3ef1 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -69,6 +69,7 @@ POLLING_LEADER_WATCH = 30 # seconds - 非 Leader Pod 每 30s 嘗試接管 logger = structlog.get_logger(__name__) _TELEGRAM_BOT_URL_RE = re.compile(r"(api\.telegram\.org/bot)[^/\s]+") _INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b") +_CODE_REF_RE = re.compile(r"([0-9a-f]{7,12})", re.IGNORECASE) def _top_gateway_bucket( @@ -213,6 +214,9 @@ def _reply_markup_summary(payload: dict) -> dict[str, object]: def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]: """Build a redaction-friendly source envelope for Channel Hub replay.""" + text = str(payload.get("text") or payload.get("caption") or "") + incident_ids = sorted(set(_INCIDENT_ID_RE.findall(text))) + code_refs = sorted(set(match.group(1) for match in _CODE_REF_RE.finditer(text))) return { "adapter": "legacy_telegram_gateway", "method": method, @@ -222,6 +226,10 @@ def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]: "disable_web_page_preview": payload.get("disable_web_page_preview"), "has_reply_context": _has_reply_context(payload), "reply_markup": _reply_markup_summary(payload), + "source_refs": { + "incident_ids": incident_ids[:20], + "code_refs": code_refs[:20], + }, } # 2026-04-27 Claude Sonnet 4.6: B3 — LLM 動態 Telegram 按鈕 Feature Flag diff --git a/apps/api/tests/test_channel_hub_grouped_alert_events.py b/apps/api/tests/test_channel_hub_grouped_alert_events.py index eb22386c..230b3e75 100644 --- a/apps/api/tests/test_channel_hub_grouped_alert_events.py +++ b/apps/api/tests/test_channel_hub_grouped_alert_events.py @@ -6,6 +6,7 @@ from src.services.channel_hub import ( ensure_completed_shadow_run, format_grouped_alert_digest_text, format_grouped_alert_event_content, + record_outbound_message, ) @@ -79,10 +80,14 @@ class _FakeSession: def __init__(self) -> None: self.statement = "" self.params = {} + self.statements = [] + self.param_sets = [] async def execute(self, statement, params): # noqa: ANN001 self.statement = str(statement) self.params = params + self.statements.append(str(statement)) + self.param_sets.append(params) return _FakeResult() @@ -105,3 +110,27 @@ async def test_completed_shadow_run_sets_run_state_not_null_defaults() -> None: assert "0, 3, 0.0000, 0" in session.statement assert session.params["project_id"] == "awoooi" assert session.params["run_id"] == run_id + + +async def test_record_outbound_message_sets_sent_at_for_sent_messages() -> None: + session = _FakeSession() + run_id = build_grouped_alert_run_id("awoooi", "telegram-message-13152") + + await record_outbound_message( + session, # type: ignore[arg-type] + project_id="awoooi", + run_id=run_id, + channel_type="telegram", + channel_chat_id="-100123", + message_type="approval_request", + content="ACTION REQUIRED INC-20260513-9B082D", + provider_message_id="13152", + send_status="sent", + triggered_by_state="legacy_gateway", + is_shadow=False, + ) + + insert_statement = session.statements[-1] + assert "sent_at" in insert_statement + assert "CASE WHEN :send_status = 'sent' THEN NOW() ELSE NULL END" in insert_statement + assert session.param_sets[-1]["send_status"] == "sent" diff --git a/apps/api/tests/test_telegram_gateway_error_sanitizer.py b/apps/api/tests/test_telegram_gateway_error_sanitizer.py index 913a0848..5edffa5a 100644 --- a/apps/api/tests/test_telegram_gateway_error_sanitizer.py +++ b/apps/api/tests/test_telegram_gateway_error_sanitizer.py @@ -18,7 +18,11 @@ def test_telegram_gateway_sanitizes_bot_token_url() -> None: def test_outbound_source_envelope_keeps_replay_context_without_raw_payload() -> None: payload = { "chat_id": "-100123", - "text": "ACTION REQUIRED token 1234567890:abcdefghijklmnopqrstuvwxyzABCDEFGH", + "text": ( + "ACTION REQUIRED INC-20260513-9B082D " + "7f858956 token " + "1234567890:abcdefghijklmnopqrstuvwxyzABCDEFGH" + ), "parse_mode": "HTML", "reply_markup": { "inline_keyboard": [ @@ -40,6 +44,8 @@ def test_outbound_source_envelope_keeps_replay_context_without_raw_payload() -> assert envelope["reply_markup"]["button_count"] == 2 assert envelope["reply_markup"]["buttons"][0]["callback_prefix"] == "approve" assert envelope["reply_markup"]["buttons"][1]["callback_prefix"] == "details" + assert envelope["source_refs"]["incident_ids"] == ["INC-20260513-9B082D"] + assert envelope["source_refs"]["code_refs"] == ["7f858956"] assert "approval-id-secret" not in str(envelope) assert "1234567890:" not in str(envelope) assert "ACTION REQUIRED" not in str(envelope) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 53c6b323..aceee09d 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -7251,3 +7251,57 @@ scope=write - T11 已部署:Operator Run Detail 與 Telegram detail formatter 都已能呈現 MCP Gateway 摘要。 - 這次沒有發送真實 Telegram 測試訊息,避免洗版;改以 production pod 直接呼叫 detail formatter 驗證。 - 目前整體進度更新:約 68%。 + +### 2026-05-13 — AwoooP truth-chain T12a:Telegram outbound 可回放關聯強化(local green) + +**production live audit 摘要**: + +```text +24h: +incidents=150 +approval_records=102 +alert_operation_log=682 +timeline_events=154 +incident_evidence=130 +auto_repair_executions=10 +knowledge_entries=42 +legacy_mcp_audit_log=1265 +awooop_mcp_gateway_audit=1365 +awooop_outbound_message=420 + +outbound_quality: +total=420 redacted=226 envelope=420 envelope_schema=226 with_run=420 sent_at=0 + +incident_join_quality: +total=150 with_aol=100 with_evidence=102 with_legacy_mcp=102 with_timeline=102 with_approval=102 with_auto_repair=10 + +Sentry / SignOz durable event tables: +none found by information_schema table_name ILIKE '%sentry%' OR '%signoz%' +``` + +**判讀**: + +- MCP / SignOz 能力已有實際使用,且 legacy MCP bridge 已寫入 AwoooP Gateway audit。 +- 仍不能宣稱完整 AI 自動修復:24h incident 150 筆中只有 10 筆有 `auto_repair_executions`。 +- Telegram outbound mirror 有資料,但 `sent_at=0` 是真缺口;source envelope 也缺少 structured source refs,truth-chain 只能靠 `content_preview ILIKE` 猜關聯。 + +**變更**: + +- `record_outbound_message()` 在 `send_status='sent'` 時寫入 `sent_at=NOW()`。 +- Telegram outbound `source_envelope` 新增 `source_refs.incident_ids` 與 `source_refs.code_refs`,保留 redaction-friendly 關聯錨點。 +- truth-chain outbound 查詢支援 structured `source_refs`,不只靠 preview 文字搜尋。 + +**local verification**: + +```text +DATABASE_URL=postgresql+asyncpg://u:p@localhost:5432/db python -m pytest tests/test_telegram_gateway_error_sanitizer.py tests/test_channel_hub_grouped_alert_events.py tests/test_awooop_truth_chain_service.py tests/test_telegram_adr050.py -q +51 passed + +python -m ruff check --select F821 src/services/channel_hub.py src/services/telegram_gateway.py src/services/awooop_truth_chain_service.py tests/test_telegram_gateway_error_sanitizer.py tests/test_channel_hub_grouped_alert_events.py tests/test_telegram_adr050.py +All checks passed + +python -m py_compile src/services/channel_hub.py src/services/telegram_gateway.py src/services/awooop_truth_chain_service.py tests/test_telegram_gateway_error_sanitizer.py tests/test_channel_hub_grouped_alert_events.py tests/test_telegram_adr050.py +OK +``` + +**目前整體進度**:約 69%。