diff --git a/apps/api/src/api/v1/platform/operator_runs.py b/apps/api/src/api/v1/platform/operator_runs.py
index 28eaf318c..75884b85f 100644
--- a/apps/api/src/api/v1/platform/operator_runs.py
+++ b/apps/api/src/api/v1/platform/operator_runs.py
@@ -116,9 +116,11 @@ class CallbackReplyAuditSummary(BaseModel):
outbound_total: int
outbound_source_envelope_total: int
outbound_source_refs_total: int
+ outbound_trace_ref_total: int = 0
outbound_incident_ref_total: int
outbound_reply_markup_total: int = 0
outbound_reply_markup_missing_incident_ref_total: int = 0
+ outbound_reply_markup_missing_trace_ref_total: int = 0
outbound_reply_markup_missing_incident_ref_top_prefixes: list[
OutboundReplyMarkupGapPrefix
] = Field(default_factory=list)
diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py
index 6dc24775b..194b437f6 100644
--- a/apps/api/src/services/platform_operator_service.py
+++ b/apps/api/src/services/platform_operator_service.py
@@ -473,6 +473,33 @@ async def _fetch_callback_reply_audit_summary(
"""Summarize Telegram outbound mirror and callback evidence capture coverage."""
result = await db.execute(
text("""
+ WITH outbound AS (
+ SELECT
+ m.*,
+ EXISTS (
+ SELECT 1
+ FROM jsonb_each(
+ CASE
+ WHEN jsonb_typeof(
+ COALESCE(
+ m.source_envelope -> 'source_refs',
+ '{}'::jsonb
+ )
+ ) = 'object'
+ THEN COALESCE(
+ m.source_envelope -> 'source_refs',
+ '{}'::jsonb
+ )
+ ELSE '{}'::jsonb
+ END
+ ) AS refs(key, value)
+ WHERE jsonb_typeof(refs.value) = 'array'
+ AND refs.value <> '[]'::jsonb
+ ) AS has_trace_ref
+ FROM awooop_outbound_message m
+ WHERE m.project_id = :project_id
+ AND m.channel_type = 'telegram'
+ )
SELECT
COUNT(*) AS outbound_total,
COUNT(*) FILTER (
@@ -481,6 +508,9 @@ async def _fetch_callback_reply_audit_summary(
COUNT(*) FILTER (
WHERE source_envelope ? 'source_refs'
) AS outbound_source_refs_total,
+ COUNT(*) FILTER (
+ WHERE has_trace_ref
+ ) AS outbound_trace_ref_total,
COUNT(*) FILTER (
WHERE COALESCE(
source_envelope #> '{source_refs,incident_ids}',
@@ -497,6 +527,10 @@ async def _fetch_callback_reply_audit_summary(
'[]'::jsonb
) = '[]'::jsonb
) AS outbound_reply_markup_missing_incident_ref_total,
+ COUNT(*) FILTER (
+ WHERE source_envelope #>> '{reply_markup,present}' = 'true'
+ AND NOT has_trace_ref
+ ) AS outbound_reply_markup_missing_trace_ref_total,
COALESCE((
SELECT jsonb_agg(
jsonb_build_object(
@@ -525,10 +559,8 @@ async def _fetch_callback_reply_audit_summary(
) AS recent_24h_total,
MIN(COALESCE(sent_at, queued_at)) AS first_sent_at,
MAX(COALESCE(sent_at, queued_at)) AS last_sent_at
- FROM awooop_outbound_message
- WHERE project_id = :project_id
- AND channel_type = 'telegram'
- AND source_envelope #>> '{reply_markup,present}' = 'true'
+ FROM outbound
+ WHERE source_envelope #>> '{reply_markup,present}' = 'true'
AND COALESCE(
source_envelope #> '{source_refs,incident_ids}',
'[]'::jsonb
@@ -604,9 +636,7 @@ async def _fetch_callback_reply_audit_summary(
MAX(COALESCE(sent_at, queued_at)) FILTER (
WHERE source_envelope ? 'callback_reply'
) AS latest_callback_at
- FROM awooop_outbound_message
- WHERE project_id = :project_id
- AND channel_type = 'telegram'
+ FROM outbound
"""),
{"project_id": project_id},
)
@@ -661,6 +691,7 @@ def _callback_reply_audit_summary_from_row(
"outbound_source_refs_total": _safe_int(
row.get("outbound_source_refs_total")
),
+ "outbound_trace_ref_total": _safe_int(row.get("outbound_trace_ref_total")),
"outbound_incident_ref_total": outbound_incident_refs,
"outbound_reply_markup_total": _safe_int(
row.get("outbound_reply_markup_total")
@@ -668,6 +699,9 @@ def _callback_reply_audit_summary_from_row(
"outbound_reply_markup_missing_incident_ref_total": _safe_int(
row.get("outbound_reply_markup_missing_incident_ref_total")
),
+ "outbound_reply_markup_missing_trace_ref_total": _safe_int(
+ row.get("outbound_reply_markup_missing_trace_ref_total")
+ ),
"outbound_reply_markup_missing_incident_ref_top_prefixes": (
top_missing_prefixes
),
diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py
index ffcab8684..89f0d3d9e 100644
--- a/apps/api/src/services/telegram_gateway.py
+++ b/apps/api/src/services/telegram_gateway.py
@@ -70,6 +70,10 @@ 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")
+_META_EVENT_ID_RE = re.compile(r"\bMETA-\d{14}\b")
+_AI_ADVISORY_CALLBACK_RE = re.compile(
+ r"^ai_advisory_(?:handled|snooze|view|produce_cmd):([^:]+):(.+)$"
+)
_CODE_REF_RE = re.compile(r"([0-9a-f]{7,12})", re.IGNORECASE)
_TELEGRAM_HTML_CHUNK_LIMIT = 3600
_AWOOOP_WEB_BASE_URL = "https://awoooi.wooo.work"
@@ -969,6 +973,47 @@ def _reply_markup_incident_ids(payload: dict) -> list[str]:
return sorted(incident_ids)
+def _append_source_ref(refs: dict[str, list[str]], key: str, value: object) -> None:
+ text = str(value or "").strip()
+ if not text:
+ return
+
+ values = refs.setdefault(key, [])
+ if text not in values:
+ values.append(text)
+
+
+def _reply_markup_domain_source_refs(payload: dict) -> dict[str, list[str]]:
+ """Extract non-secret domain refs from known button schemas."""
+ reply_markup = payload.get("reply_markup")
+ if not isinstance(reply_markup, dict):
+ return {}
+
+ refs: dict[str, list[str]] = {}
+ for row in reply_markup.get("inline_keyboard") or []:
+ if not isinstance(row, list):
+ continue
+ for button in row:
+ if not isinstance(button, dict):
+ continue
+ callback_data = str(button.get("callback_data") or "")
+ match = _AI_ADVISORY_CALLBACK_RE.match(callback_data)
+ if not match:
+ continue
+ advisory_type = match.group(1).strip()
+ advisory_id = match.group(2).strip()
+ if not advisory_type or not advisory_id:
+ continue
+ advisory_ref = f"{advisory_type}:{advisory_id}"
+ advisory_event = f"ai_advisory:{advisory_ref}"
+ _append_source_ref(refs, "event_ids", advisory_event)
+ _append_source_ref(refs, "advisory_ids", advisory_ref)
+ _append_source_ref(refs, "alert_ids", advisory_type)
+ _append_source_ref(refs, "fingerprints", advisory_event)
+
+ return {key: values[:20] for key, values in refs.items() if values}
+
+
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 "")
@@ -976,7 +1021,22 @@ def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]:
set(_INCIDENT_ID_RE.findall(text))
| set(_reply_markup_incident_ids(payload))
)
+ domain_refs = _reply_markup_domain_source_refs(payload)
+ event_ids = sorted(
+ set(_META_EVENT_ID_RE.findall(text))
+ | set(domain_refs.get("event_ids", []))
+ )
code_refs = sorted(set(match.group(1) for match in _CODE_REF_RE.finditer(text)))
+ source_refs: dict[str, list[str]] = {
+ "incident_ids": incident_ids[:20],
+ "event_ids": event_ids[:20],
+ "code_refs": code_refs[:20],
+ }
+ for key, values in domain_refs.items():
+ if key == "event_ids":
+ continue
+ source_refs[key] = values[:20]
+
return {
"adapter": "legacy_telegram_gateway",
"method": method,
@@ -986,10 +1046,7 @@ 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],
- },
+ "source_refs": source_refs,
}
@@ -4463,6 +4520,16 @@ class TelegramGateway:
]
}
+ source_refs: dict[str, list[str]] = {
+ "approval_ids": [approval_id],
+ "alert_ids": [alertname],
+ "fingerprints": [f"meta:{alert_category}:{alertname}"],
+ }
+ if _INCIDENT_ID_RE.fullmatch(incident_id):
+ source_refs["incident_ids"] = [incident_id]
+ else:
+ source_refs["event_ids"] = [incident_id]
+
return await self._send_request(
"sendMessage",
{
@@ -4470,6 +4537,9 @@ class TelegramGateway:
"text": text,
"parse_mode": "HTML",
"reply_markup": keyboard,
+ _AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY: {
+ "source_refs": source_refs,
+ },
},
)
diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py
index d919d835e..2e2462380 100644
--- a/apps/api/tests/test_awooop_operator_timeline_labels.py
+++ b/apps/api/tests/test_awooop_operator_timeline_labels.py
@@ -666,9 +666,11 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
"outbound_total": 120,
"outbound_source_envelope_total": 118,
"outbound_source_refs_total": 100,
+ "outbound_trace_ref_total": 92,
"outbound_incident_ref_total": 80,
"outbound_reply_markup_total": 30,
"outbound_reply_markup_missing_incident_ref_total": 4,
+ "outbound_reply_markup_missing_trace_ref_total": 2,
"outbound_reply_markup_missing_incident_ref_top_prefixes": [
{
"prefix": "silence",
@@ -719,8 +721,10 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
assert dumped["items"][0]["evidence_capture_status"]["status"] == "captured"
assert dumped["items"][0]["run_detail_href"].endswith("project_id=awoooi")
assert dumped["summary"]["outbound_total"] == 120
+ assert dumped["summary"]["outbound_trace_ref_total"] == 92
assert dumped["summary"]["outbound_reply_markup_total"] == 30
assert dumped["summary"]["outbound_reply_markup_missing_incident_ref_total"] == 4
+ assert dumped["summary"]["outbound_reply_markup_missing_trace_ref_total"] == 2
assert dumped["summary"][
"outbound_reply_markup_missing_incident_ref_top_prefixes"
][0] == {
@@ -749,9 +753,11 @@ def test_callback_reply_audit_summary_marks_missing_snapshots() -> None:
"outbound_total": 5256,
"outbound_source_envelope_total": 5256,
"outbound_source_refs_total": 5000,
+ "outbound_trace_ref_total": 4300,
"outbound_incident_ref_total": 3200,
"outbound_reply_markup_total": 100,
"outbound_reply_markup_missing_incident_ref_total": 12,
+ "outbound_reply_markup_missing_trace_ref_total": 5,
"outbound_reply_markup_missing_incident_ref_top_prefixes": [
{
"prefix": "silence",
@@ -786,6 +792,8 @@ def test_callback_reply_audit_summary_marks_missing_snapshots() -> None:
assert summary["schema_version"] == "telegram_callback_reply_audit_summary_v1"
assert summary["outbound_total"] == 5256
+ assert summary["outbound_trace_ref_total"] == 4300
+ assert summary["outbound_reply_markup_missing_trace_ref_total"] == 5
assert summary["callback_total"] == 2
assert summary["callback_snapshot_missing_total"] == 2
assert summary["snapshot_status"] == "not_captured"
@@ -804,9 +812,11 @@ def test_callback_reply_audit_summary_marks_mixed_legacy_snapshots_partial() ->
"outbound_total": 5221,
"outbound_source_envelope_total": 4905,
"outbound_source_refs_total": 4676,
+ "outbound_trace_ref_total": 4230,
"outbound_incident_ref_total": 920,
"outbound_reply_markup_total": 1322,
"outbound_reply_markup_missing_incident_ref_total": 684,
+ "outbound_reply_markup_missing_trace_ref_total": 154,
"outbound_reply_markup_missing_incident_ref_top_prefixes": [
{
"prefix": "silence",
@@ -840,6 +850,8 @@ def test_callback_reply_audit_summary_marks_mixed_legacy_snapshots_partial() ->
)
assert summary["callback_snapshot_captured_total"] == 1
+ assert summary["outbound_trace_ref_total"] == 4230
+ assert summary["outbound_reply_markup_missing_trace_ref_total"] == 154
assert summary["callback_snapshot_missing_total"] == 2
assert summary["snapshot_status"] == "partial"
assert summary["next_action"] == "review_legacy_callback_snapshot_gap"
diff --git a/apps/api/tests/test_telegram_gateway_error_sanitizer.py b/apps/api/tests/test_telegram_gateway_error_sanitizer.py
index 5c7da5b8e..555836561 100644
--- a/apps/api/tests/test_telegram_gateway_error_sanitizer.py
+++ b/apps/api/tests/test_telegram_gateway_error_sanitizer.py
@@ -76,3 +76,66 @@ def test_outbound_source_envelope_reads_incident_refs_from_buttons() -> None:
]
assert envelope["reply_markup"]["buttons"][0]["callback_prefix"] == "detail"
assert "detail:INC-20260525-ABC123" not in str(envelope)
+
+
+def test_outbound_source_envelope_reads_meta_event_ref_from_text() -> None:
+ payload = {
+ "chat_id": "-100123",
+ "text": (
+ "⚙️ META SYSTEM\n"
+ "📋 META-20260525201300\n"
+ "異常元件:AI 自健診異常"
+ ),
+ "parse_mode": "HTML",
+ "reply_markup": {
+ "inline_keyboard": [
+ [{"text": "靜默", "callback_data": "silence:opaque-secret"}]
+ ],
+ },
+ }
+
+ envelope = _outbound_source_envelope("sendMessage", payload)
+
+ assert envelope["source_refs"]["event_ids"] == ["META-20260525201300"]
+ assert envelope["source_refs"]["incident_ids"] == []
+ assert envelope["reply_markup"]["buttons"][0]["callback_prefix"] == "silence"
+ assert "opaque-secret" not in str(envelope)
+
+
+def test_outbound_source_envelope_reads_ai_advisory_refs_without_raw_callback() -> None:
+ payload = {
+ "chat_id": "-100123",
+ "text": "Coverage 缺口分析",
+ "reply_markup": {
+ "inline_keyboard": [
+ [
+ {
+ "text": "已處理",
+ "callback_data": (
+ "ai_advisory_handled:coverage_gap:auto_rule_creation"
+ ),
+ },
+ {
+ "text": "忽略",
+ "callback_data": (
+ "ai_advisory_snooze:coverage_gap:auto_rule_creation"
+ ),
+ },
+ ]
+ ],
+ },
+ }
+
+ envelope = _outbound_source_envelope("sendMessage", payload)
+
+ assert envelope["source_refs"]["event_ids"] == [
+ "ai_advisory:coverage_gap:auto_rule_creation"
+ ]
+ assert envelope["source_refs"]["advisory_ids"] == [
+ "coverage_gap:auto_rule_creation"
+ ]
+ assert envelope["source_refs"]["alert_ids"] == ["coverage_gap"]
+ assert envelope["source_refs"]["fingerprints"] == [
+ "ai_advisory:coverage_gap:auto_rule_creation"
+ ]
+ assert "ai_advisory_handled:coverage_gap:auto_rule_creation" not in str(envelope)
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 4a6694e71..5b115ced0 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -2833,8 +2833,8 @@
"error": "Callback evidence failed to load: {error}",
"summary": {
"outbound": "Outbound mirror",
- "outboundDetail": "source_refs {sourceRefs}; incident refs {incidentRefs}; coverage {coverage}",
- "outboundReplyMarkupDetail": "reply_markup {replyMarkup}; button missing incident refs {missingIncidentRefs}",
+ "outboundDetail": "source_refs {sourceRefs}; trace refs {traceRefs}; incident refs {incidentRefs}; coverage {coverage}",
+ "outboundReplyMarkupDetail": "reply_markup {replyMarkup}; missing trace refs {missingTraceRefs}; missing incident refs {missingIncidentRefs}",
"outboundReplyMarkupTopPrefixes": "Gap top prefixes: {prefixes}",
"outboundReplyMarkupTopPrefixItem": "{prefix} {total} (24h {recent}, last {last})",
"callbacks": "Callback replies",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 3cf9956ab..245d0f487 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -2834,8 +2834,8 @@
"error": "Callback evidence 載入失敗:{error}",
"summary": {
"outbound": "出站鏡像",
- "outboundDetail": "source_refs {sourceRefs};incident refs {incidentRefs};覆蓋 {coverage}",
- "outboundReplyMarkupDetail": "reply_markup {replyMarkup};按鈕缺 incident refs {missingIncidentRefs}",
+ "outboundDetail": "source_refs {sourceRefs};trace refs {traceRefs};incident refs {incidentRefs};覆蓋 {coverage}",
+ "outboundReplyMarkupDetail": "reply_markup {replyMarkup};缺 trace refs {missingTraceRefs};缺 incident refs {missingIncidentRefs}",
"outboundReplyMarkupTopPrefixes": "缺口 top prefixes:{prefixes}",
"outboundReplyMarkupTopPrefixItem": "{prefix} {total}(24h {recent},最後 {last})",
"callbacks": "Callback replies",
diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx
index b208a51b8..e3e66d4e4 100644
--- a/apps/web/src/app/[locale]/awooop/runs/page.tsx
+++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx
@@ -144,9 +144,11 @@ interface CallbackReplyAuditSummary {
outbound_total?: number;
outbound_source_envelope_total?: number;
outbound_source_refs_total?: number;
+ outbound_trace_ref_total?: number;
outbound_incident_ref_total?: number;
outbound_reply_markup_total?: number;
outbound_reply_markup_missing_incident_ref_total?: number;
+ outbound_reply_markup_missing_trace_ref_total?: number;
outbound_reply_markup_missing_incident_ref_top_prefixes?: Array<{
prefix?: string | null;
total?: number | null;
@@ -1980,7 +1982,7 @@ function CallbackReplyAuditSummaryPanel({
nextActionRaw === "review_outbound_source_refs"
) ? nextActionRaw : "observed";
const sourceRefCoverage = formatCoveragePercent(
- summary.outbound_incident_ref_total ?? 0,
+ summary.outbound_trace_ref_total ?? summary.outbound_incident_ref_total ?? 0,
outboundTotal
);
const topMissingPrefixes = (
@@ -2034,6 +2036,10 @@ function CallbackReplyAuditSummaryPanel({
{t("outboundDetail", { sourceRefs: summary.outbound_source_refs_total ?? 0, + traceRefs: + summary.outbound_trace_ref_total ?? + summary.outbound_incident_ref_total ?? + 0, incidentRefs: summary.outbound_incident_ref_total ?? 0, coverage: sourceRefCoverage, })} @@ -2043,6 +2049,8 @@ function CallbackReplyAuditSummaryPanel({ replyMarkup: summary.outbound_reply_markup_total ?? 0, missingIncidentRefs: summary.outbound_reply_markup_missing_incident_ref_total ?? 0, + missingTraceRefs: + summary.outbound_reply_markup_missing_trace_ref_total ?? 0, })}
diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 6b4f7ee31..7c6406aef 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -20913,3 +20913,70 @@ GET /api/v1/health: - KM governance:約 84.6%。 - AI Provider lane visibility:約 92.2%。 - 完整 AI 自動化管理產品化:約 97.65%。 + +### 2026-05-25 — T190 Telegram action cards domain source refs(pre-deploy) + +**背景**:T189 production recency 顯示最近 24h 仍在新增的 `silence` 與 +`ai_advisory_handled` 缺口不是一般 Incident 卡,而是 `META-*` AI 自健診卡 +與 Coverage 缺口 AI advisory 卡。這兩類卡本來就不該硬塞 `INC-*`,但仍必須 +有可追溯的 domain source refs,否則前端看起來像「按鈕斷鏈」。 + +**完成變更**: + +- `telegram_gateway._outbound_source_envelope()` 會從 `META-YYYYMMDDHHMMSS` + 文字萃取 `source_refs.event_ids`。 +- `telegram_gateway._outbound_source_envelope()` 會從 + `ai_advisory_{handled|snooze|view|produce_cmd}:{type}:{id}` 按鈕 schema + 萃取 `event_ids / advisory_ids / alert_ids / fingerprints`,但不保存 raw + callback payload。 +- `send_meta_alert()` 額外鏡像 `approval_ids / alert_ids / fingerprints`; + 若卡片 id 是 `INC-*` 寫入 `incident_ids`,否則寫入 `event_ids`。 +- `/api/v1/platform/runs/callback-replies` summary 新增: + `outbound_trace_ref_total` 與 + `outbound_reply_markup_missing_trace_ref_total`。 +- AwoooP Runs 前端 summary 同步顯示 trace refs / 缺 trace refs,避免把 + 非 Incident governance/action card 誤判為完全缺來源。 + +**local validation(完成)**: + +```text +python3 -m py_compile apps/api/src/services/telegram_gateway.py apps/api/src/services/platform_operator_service.py apps/api/src/api/v1/platform/operator_runs.py +jq empty apps/web/messages/zh-TW.json apps/web/messages/en.json +PYTHONPATH=. DATABASE_URL='postgresql+asyncpg://test:test@localhost/test' /Users/ogt/.pyenv/shims/pytest tests/test_telegram_gateway_error_sanitizer.py tests/test_awooop_operator_timeline_labels.py -q + 58 passed in 0.84s +pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-t190-tsconfig.tsbuildinfo +pnpm --dir apps/web lint -- --file 'src/app/[locale]/awooop/runs/page.tsx' + pass with pre-existing i18next/no-literal-string warnings in the same file +NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web run build + pass; Sentry global-error / instrumentation-client warnings are pre-existing +NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm turbo build --filter=@awoooi/web --concurrency=1 + pass; lewooogo-core package.json condition warnings are pre-existing +git diff --check + pass +``` + +**production DB dry-run(完成,read-only SQL)**: + +```text +outbound_total = 5299 +outbound_trace_ref_total = 2346 +outbound_reply_markup_total = 1329 +outbound_reply_markup_missing_trace_ref_total = 417 +outbound_reply_markup_missing_incident_ref_total = 684 +``` + +**目前整體進度**: + +- AwoooP 告警可觀測鏈:約 99.7%。 +- 低風險自動修復閉環:約 95.8%。 +- 前端 AI 自動化管理介面同步:約 99.45%。 +- 首頁 KPI / 小龍蝦流程 truth alignment:約 96.5%。 +- Telegram 詳情 / 歷史可追溯:約 99.0%。 +- Telegram outbound / callback DB coverage 可視化:約 99.55%。 +- callback / DB replayability:約 98.6%。 +- MCP / 自建 MCP 可視化:約 95.1%。 +- Sentry / SigNoz source correlation:約 94.5%。 +- Ansible / PlayBook 可視化:約 92.6%。 +- KM governance:約 84.6%。 +- AI Provider lane visibility:約 92.2%。 +- 完整 AI 自動化管理產品化:約 97.7%。