feat(awooop): show source ref gap prefixes
This commit is contained in:
@@ -102,6 +102,11 @@ class CallbackReplyItem(BaseModel):
|
|||||||
run_detail_href: str | None = None
|
run_detail_href: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class OutboundReplyMarkupGapPrefix(BaseModel):
|
||||||
|
prefix: str
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
class CallbackReplyAuditSummary(BaseModel):
|
class CallbackReplyAuditSummary(BaseModel):
|
||||||
schema_version: str
|
schema_version: str
|
||||||
project_id: str
|
project_id: str
|
||||||
@@ -111,6 +116,9 @@ class CallbackReplyAuditSummary(BaseModel):
|
|||||||
outbound_incident_ref_total: int
|
outbound_incident_ref_total: int
|
||||||
outbound_reply_markup_total: int = 0
|
outbound_reply_markup_total: int = 0
|
||||||
outbound_reply_markup_missing_incident_ref_total: int = 0
|
outbound_reply_markup_missing_incident_ref_total: int = 0
|
||||||
|
outbound_reply_markup_missing_incident_ref_top_prefixes: list[
|
||||||
|
OutboundReplyMarkupGapPrefix
|
||||||
|
] = Field(default_factory=list)
|
||||||
outbound_failed_total: int
|
outbound_failed_total: int
|
||||||
callback_total: int
|
callback_total: int
|
||||||
callback_sent_total: int
|
callback_sent_total: int
|
||||||
|
|||||||
@@ -497,6 +497,39 @@ async def _fetch_callback_reply_audit_summary(
|
|||||||
'[]'::jsonb
|
'[]'::jsonb
|
||||||
) = '[]'::jsonb
|
) = '[]'::jsonb
|
||||||
) AS outbound_reply_markup_missing_incident_ref_total,
|
) AS outbound_reply_markup_missing_incident_ref_total,
|
||||||
|
COALESCE((
|
||||||
|
SELECT jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'prefix', prefix,
|
||||||
|
'total', total
|
||||||
|
)
|
||||||
|
ORDER BY total DESC, prefix ASC
|
||||||
|
)
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(
|
||||||
|
source_envelope #>>
|
||||||
|
'{reply_markup,buttons,0,callback_prefix}',
|
||||||
|
''
|
||||||
|
),
|
||||||
|
'unknown'
|
||||||
|
) AS prefix,
|
||||||
|
COUNT(*) AS total
|
||||||
|
FROM awooop_outbound_message
|
||||||
|
WHERE project_id = :project_id
|
||||||
|
AND channel_type = 'telegram'
|
||||||
|
AND source_envelope #>> '{reply_markup,present}' = 'true'
|
||||||
|
AND COALESCE(
|
||||||
|
source_envelope #> '{source_refs,incident_ids}',
|
||||||
|
'[]'::jsonb
|
||||||
|
) = '[]'::jsonb
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY total DESC, prefix ASC
|
||||||
|
LIMIT 5
|
||||||
|
) missing_prefixes
|
||||||
|
), '[]'::jsonb)
|
||||||
|
AS outbound_reply_markup_missing_incident_ref_top_prefixes,
|
||||||
COUNT(*) FILTER (
|
COUNT(*) FILTER (
|
||||||
WHERE send_status = 'failed'
|
WHERE send_status = 'failed'
|
||||||
) AS outbound_failed_total,
|
) AS outbound_failed_total,
|
||||||
@@ -586,6 +619,9 @@ def _callback_reply_audit_summary_from_row(
|
|||||||
partial = _safe_int(row.get("callback_snapshot_partial_total"))
|
partial = _safe_int(row.get("callback_snapshot_partial_total"))
|
||||||
missing = _safe_int(row.get("callback_snapshot_missing_total"))
|
missing = _safe_int(row.get("callback_snapshot_missing_total"))
|
||||||
outbound_incident_refs = _safe_int(row.get("outbound_incident_ref_total"))
|
outbound_incident_refs = _safe_int(row.get("outbound_incident_ref_total"))
|
||||||
|
top_missing_prefixes = _reply_markup_gap_prefixes_from_value(
|
||||||
|
row.get("outbound_reply_markup_missing_incident_ref_top_prefixes")
|
||||||
|
)
|
||||||
|
|
||||||
if callback_total <= 0:
|
if callback_total <= 0:
|
||||||
snapshot_status = "no_callback"
|
snapshot_status = "no_callback"
|
||||||
@@ -623,6 +659,9 @@ def _callback_reply_audit_summary_from_row(
|
|||||||
"outbound_reply_markup_missing_incident_ref_total": _safe_int(
|
"outbound_reply_markup_missing_incident_ref_total": _safe_int(
|
||||||
row.get("outbound_reply_markup_missing_incident_ref_total")
|
row.get("outbound_reply_markup_missing_incident_ref_total")
|
||||||
),
|
),
|
||||||
|
"outbound_reply_markup_missing_incident_ref_top_prefixes": (
|
||||||
|
top_missing_prefixes
|
||||||
|
),
|
||||||
"outbound_failed_total": _safe_int(row.get("outbound_failed_total")),
|
"outbound_failed_total": _safe_int(row.get("outbound_failed_total")),
|
||||||
"callback_total": callback_total,
|
"callback_total": callback_total,
|
||||||
"callback_sent_total": _safe_int(row.get("callback_sent_total")),
|
"callback_sent_total": _safe_int(row.get("callback_sent_total")),
|
||||||
@@ -642,6 +681,24 @@ def _callback_reply_audit_summary_from_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _reply_markup_gap_prefixes_from_value(value: Any) -> list[dict[str, Any]]:
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
prefixes: list[dict[str, Any]] = []
|
||||||
|
for item in value:
|
||||||
|
if not isinstance(item, Mapping):
|
||||||
|
continue
|
||||||
|
prefix = str(item.get("prefix") or "unknown").strip() or "unknown"
|
||||||
|
prefixes.append({
|
||||||
|
"prefix": prefix[:80],
|
||||||
|
"total": _safe_int(item.get("total")),
|
||||||
|
})
|
||||||
|
if len(prefixes) >= 5:
|
||||||
|
break
|
||||||
|
return prefixes
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_km_stale_completion_summary_for_incident(
|
async def _fetch_km_stale_completion_summary_for_incident(
|
||||||
*,
|
*,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
|
|||||||
@@ -669,6 +669,10 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
|||||||
"outbound_incident_ref_total": 80,
|
"outbound_incident_ref_total": 80,
|
||||||
"outbound_reply_markup_total": 30,
|
"outbound_reply_markup_total": 30,
|
||||||
"outbound_reply_markup_missing_incident_ref_total": 4,
|
"outbound_reply_markup_missing_incident_ref_total": 4,
|
||||||
|
"outbound_reply_markup_missing_incident_ref_top_prefixes": [
|
||||||
|
{"prefix": "silence", "total": 3},
|
||||||
|
{"prefix": "drift_view", "total": 1},
|
||||||
|
],
|
||||||
"outbound_failed_total": 1,
|
"outbound_failed_total": 1,
|
||||||
"callback_total": 3,
|
"callback_total": 3,
|
||||||
"callback_sent_total": 1,
|
"callback_sent_total": 1,
|
||||||
@@ -707,6 +711,12 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
|
|||||||
assert dumped["summary"]["outbound_total"] == 120
|
assert dumped["summary"]["outbound_total"] == 120
|
||||||
assert dumped["summary"]["outbound_reply_markup_total"] == 30
|
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_incident_ref_total"] == 4
|
||||||
|
assert dumped["summary"][
|
||||||
|
"outbound_reply_markup_missing_incident_ref_top_prefixes"
|
||||||
|
][0] == {
|
||||||
|
"prefix": "silence",
|
||||||
|
"total": 3,
|
||||||
|
}
|
||||||
assert dumped["summary"]["callback_snapshot_missing_total"] == 1
|
assert dumped["summary"]["callback_snapshot_missing_total"] == 1
|
||||||
assert dumped["summary"]["snapshot_status"] == "partial"
|
assert dumped["summary"]["snapshot_status"] == "partial"
|
||||||
|
|
||||||
@@ -729,6 +739,10 @@ def test_callback_reply_audit_summary_marks_missing_snapshots() -> None:
|
|||||||
"outbound_incident_ref_total": 3200,
|
"outbound_incident_ref_total": 3200,
|
||||||
"outbound_reply_markup_total": 100,
|
"outbound_reply_markup_total": 100,
|
||||||
"outbound_reply_markup_missing_incident_ref_total": 12,
|
"outbound_reply_markup_missing_incident_ref_total": 12,
|
||||||
|
"outbound_reply_markup_missing_incident_ref_top_prefixes": [
|
||||||
|
{"prefix": "silence", "total": 8},
|
||||||
|
{"prefix": "drift_view", "total": 4},
|
||||||
|
],
|
||||||
"outbound_failed_total": 0,
|
"outbound_failed_total": 0,
|
||||||
"callback_total": 2,
|
"callback_total": 2,
|
||||||
"callback_sent_total": 2,
|
"callback_sent_total": 2,
|
||||||
@@ -764,6 +778,10 @@ def test_callback_reply_audit_summary_marks_mixed_legacy_snapshots_partial() ->
|
|||||||
"outbound_incident_ref_total": 920,
|
"outbound_incident_ref_total": 920,
|
||||||
"outbound_reply_markup_total": 1322,
|
"outbound_reply_markup_total": 1322,
|
||||||
"outbound_reply_markup_missing_incident_ref_total": 684,
|
"outbound_reply_markup_missing_incident_ref_total": 684,
|
||||||
|
"outbound_reply_markup_missing_incident_ref_top_prefixes": [
|
||||||
|
{"prefix": "silence", "total": 275},
|
||||||
|
{"prefix": "drift_view", "total": 144},
|
||||||
|
],
|
||||||
"outbound_failed_total": 0,
|
"outbound_failed_total": 0,
|
||||||
"callback_total": 3,
|
"callback_total": 3,
|
||||||
"callback_sent_total": 3,
|
"callback_sent_total": 3,
|
||||||
|
|||||||
@@ -2835,6 +2835,7 @@
|
|||||||
"outbound": "Outbound mirror",
|
"outbound": "Outbound mirror",
|
||||||
"outboundDetail": "source_refs {sourceRefs}; incident refs {incidentRefs}; coverage {coverage}",
|
"outboundDetail": "source_refs {sourceRefs}; incident refs {incidentRefs}; coverage {coverage}",
|
||||||
"outboundReplyMarkupDetail": "reply_markup {replyMarkup}; button missing incident refs {missingIncidentRefs}",
|
"outboundReplyMarkupDetail": "reply_markup {replyMarkup}; button missing incident refs {missingIncidentRefs}",
|
||||||
|
"outboundReplyMarkupTopPrefixes": "Gap top prefixes: {prefixes}",
|
||||||
"callbacks": "Callback replies",
|
"callbacks": "Callback replies",
|
||||||
"callbackDetail": "detail {detail} / history {history}; incidents {incidents}",
|
"callbackDetail": "detail {detail} / history {history}; incidents {incidents}",
|
||||||
"snapshots": "Evidence snapshots",
|
"snapshots": "Evidence snapshots",
|
||||||
|
|||||||
@@ -2836,6 +2836,7 @@
|
|||||||
"outbound": "出站鏡像",
|
"outbound": "出站鏡像",
|
||||||
"outboundDetail": "source_refs {sourceRefs};incident refs {incidentRefs};覆蓋 {coverage}",
|
"outboundDetail": "source_refs {sourceRefs};incident refs {incidentRefs};覆蓋 {coverage}",
|
||||||
"outboundReplyMarkupDetail": "reply_markup {replyMarkup};按鈕缺 incident refs {missingIncidentRefs}",
|
"outboundReplyMarkupDetail": "reply_markup {replyMarkup};按鈕缺 incident refs {missingIncidentRefs}",
|
||||||
|
"outboundReplyMarkupTopPrefixes": "缺口 top prefixes:{prefixes}",
|
||||||
"callbacks": "Callback replies",
|
"callbacks": "Callback replies",
|
||||||
"callbackDetail": "detail {detail} / history {history};Incident {incidents}",
|
"callbackDetail": "detail {detail} / history {history};Incident {incidents}",
|
||||||
"snapshots": "Evidence snapshots",
|
"snapshots": "Evidence snapshots",
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ interface CallbackReplyAuditSummary {
|
|||||||
outbound_incident_ref_total?: number;
|
outbound_incident_ref_total?: number;
|
||||||
outbound_reply_markup_total?: number;
|
outbound_reply_markup_total?: number;
|
||||||
outbound_reply_markup_missing_incident_ref_total?: number;
|
outbound_reply_markup_missing_incident_ref_total?: number;
|
||||||
|
outbound_reply_markup_missing_incident_ref_top_prefixes?: Array<{
|
||||||
|
prefix?: string | null;
|
||||||
|
total?: number | null;
|
||||||
|
}>;
|
||||||
outbound_failed_total?: number;
|
outbound_failed_total?: number;
|
||||||
callback_total?: number;
|
callback_total?: number;
|
||||||
callback_sent_total?: number;
|
callback_sent_total?: number;
|
||||||
@@ -1976,6 +1980,12 @@ function CallbackReplyAuditSummaryPanel({
|
|||||||
summary.outbound_incident_ref_total ?? 0,
|
summary.outbound_incident_ref_total ?? 0,
|
||||||
outboundTotal
|
outboundTotal
|
||||||
);
|
);
|
||||||
|
const topMissingPrefixes = (
|
||||||
|
summary.outbound_reply_markup_missing_incident_ref_top_prefixes ?? []
|
||||||
|
)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((item) => `${item.prefix || "--"} ${item.total ?? 0}`)
|
||||||
|
.join(" / ") || "--";
|
||||||
const snapshotCoverage = formatCoveragePercent(
|
const snapshotCoverage = formatCoveragePercent(
|
||||||
summary.callback_snapshot_captured_total ?? 0,
|
summary.callback_snapshot_captured_total ?? 0,
|
||||||
callbackTotal
|
callbackTotal
|
||||||
@@ -2017,6 +2027,11 @@ function CallbackReplyAuditSummaryPanel({
|
|||||||
summary.outbound_reply_markup_missing_incident_ref_total ?? 0,
|
summary.outbound_reply_markup_missing_incident_ref_total ?? 0,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">
|
||||||
|
{t("outboundReplyMarkupTopPrefixes", {
|
||||||
|
prefixes: topMissingPrefixes,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white px-4 py-3">
|
<div className="bg-white px-4 py-3">
|
||||||
<p className="text-xs font-semibold text-[#77736a]">{t("callbacks")}</p>
|
<p className="text-xs font-semibold text-[#77736a]">{t("callbacks")}</p>
|
||||||
|
|||||||
@@ -20704,3 +20704,63 @@ GET /api/v1/health:
|
|||||||
- KM governance:約 84.6%。
|
- KM governance:約 84.6%。
|
||||||
- AI Provider lane visibility:約 92.2%。
|
- AI Provider lane visibility:約 92.2%。
|
||||||
- 完整 AI 自動化管理產品化:約 97.45%。
|
- 完整 AI 自動化管理產品化:約 97.45%。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-25 T188 — Source Refs Gap Top Prefix Breakdown
|
||||||
|
|
||||||
|
**背景**:
|
||||||
|
|
||||||
|
- T187 已把 Telegram outbound button gap 顯示為 `按鈕缺 incident refs 682`。
|
||||||
|
- 但 operator 仍不知道缺口集中在哪一類 Telegram 卡片或按鈕族群,無法判斷下一步應修哪個 template / gateway。
|
||||||
|
|
||||||
|
**本輪修正**:
|
||||||
|
|
||||||
|
- `/api/v1/platform/runs/callback-replies` summary 新增:
|
||||||
|
- `outbound_reply_markup_missing_incident_ref_top_prefixes`
|
||||||
|
- Run 監控 `TG Callback Evidence` 的 Outbound mirror 卡新增:
|
||||||
|
- `缺口 top prefixes:silence 276 / unknown 154 / drift_view 144`
|
||||||
|
- 這讓 source matching 的下一輪修補可以直接對準 `silence`、`drift_view`、`approve` 等 button prefix,而不是只看總數猜原因。
|
||||||
|
|
||||||
|
**local validation(完成)**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 -m py_compile apps/api/src/services/platform_operator_service.py apps/api/src/api/v1/platform/operator_runs.py apps/api/tests/test_awooop_operator_timeline_labels.py
|
||||||
|
jq empty apps/web/messages/zh-TW.json apps/web/messages/en.json
|
||||||
|
git diff --check
|
||||||
|
PYTHONPATH=. DATABASE_URL='postgresql+asyncpg://test:test@localhost/test' /Users/ogt/.pyenv/shims/pytest tests/test_awooop_operator_timeline_labels.py -q
|
||||||
|
53 passed in 1.02s
|
||||||
|
pnpm --dir apps/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-t188-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
|
||||||
|
```
|
||||||
|
|
||||||
|
**production read-only preflight(完成)**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
kubectl exec -n awoooi-prod deploy/awoooi-api -- production DB read-only SQL
|
||||||
|
top prefixes:
|
||||||
|
silence = 276
|
||||||
|
unknown = 154
|
||||||
|
drift_view = 144
|
||||||
|
ai_advisory_handled = 51
|
||||||
|
approve = 50
|
||||||
|
```
|
||||||
|
|
||||||
|
**目前整體進度**:
|
||||||
|
|
||||||
|
- AwoooP 告警可觀測鏈:約 99.62%。
|
||||||
|
- 低風險自動修復閉環:約 95.8%。
|
||||||
|
- 前端 AI 自動化管理介面同步:約 99.25%。
|
||||||
|
- 首頁 KPI / 小龍蝦流程 truth alignment:約 96.5%。
|
||||||
|
- Telegram 詳情 / 歷史可追溯:約 99.0%。
|
||||||
|
- Telegram outbound / callback DB coverage 可視化:約 99.25%。
|
||||||
|
- callback / DB replayability:約 98.5%。
|
||||||
|
- MCP / 自建 MCP 可視化:約 95.1%。
|
||||||
|
- Sentry / SigNoz source correlation:約 94.2%。
|
||||||
|
- Ansible / PlayBook 可視化:約 92.6%。
|
||||||
|
- KM governance:約 84.6%。
|
||||||
|
- AI Provider lane visibility:約 92.2%。
|
||||||
|
- 完整 AI 自動化管理產品化:約 97.5%。
|
||||||
|
|||||||
Reference in New Issue
Block a user