diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index d82ffbec..286300c8 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -433,6 +433,114 @@ def _format_awooop_status_chain_lines( return lines +def _format_km_stale_completion_lines(summary: dict[str, object] | None) -> list[str]: + """Render KM owner-review completion state for Telegram detail/history replies.""" + if not summary: + return [] + status = str(summary.get("status") or "ok") + if status == "fetch_failed": + return [ + "", + "📚 KM Owner Review", + "狀態: fetch_failed", + "下一步: open_awooop_work_items", + ] + + items = summary.get("items") if isinstance(summary.get("items"), list) else [] + first_item = items[0] if items and isinstance(items[0], dict) else None + lines = [ + "", + "📚 KM Owner Review", + ( + "Completion queue: " + f"ready {_safe_int(summary.get('ready_count'))} / " + f"blocked {_safe_int(summary.get('blocked_count'))} / " + f"completed {_safe_int(summary.get('completed_count'))} / " + f"failed {_safe_int(summary.get('failed_count'))}" + ), + ( + "Guardrail: " + f"read-only {html.escape(_bool_code(not bool(summary.get('writes_on_read'))))} / " + f"batch-write {html.escape(_bool_code(summary.get('batch_writes_allowed')))}" + ), + ] + if first_item: + lines += [ + ( + "此事件: " + f"{html.escape(str(first_item.get('entry_id') or '--'))} / " + f"{html.escape(str(first_item.get('readiness') or '--'))}" + ), + ( + "下一步: " + f"{html.escape(str(first_item.get('next_action') or '--'))}" + ), + ] + else: + lines.append("此事件: no_related_owner_review") + lines.append("下一步: review_awooop_completion_queue") + return lines + + +async def _fetch_km_stale_completion_summary_for_incident( + *, + incident_id: str, + project_id: str, +) -> dict[str, object] | None: + """Fetch read-only completion queue state related to one incident.""" + if not incident_id: + return None + try: + from src.services.governance_km_stale_review_service import ( + query_km_stale_owner_review_completion_queue, + ) + + queue = await asyncio.wait_for( + query_km_stale_owner_review_completion_queue( + project_id=project_id or "awoooi", + status_bucket="all", + limit=100, + ), + timeout=2.5, + ) + items = [ + item.model_dump(mode="json") + for item in queue.items + if item.related_incident_id == incident_id + ] + return { + "schema_version": "km_stale_owner_review_completion_telegram_summary_v1", + "status": "ok", + "project_id": project_id or "awoooi", + "incident_id": incident_id, + "total": queue.total, + "returned": queue.returned, + "pending_count": queue.pending_count, + "ready_count": queue.ready_count, + "blocked_count": queue.blocked_count, + "completed_count": queue.completed_count, + "failed_count": queue.failed_count, + "writes_on_read": queue.writes_on_read, + "batch_writes_allowed": queue.batch_writes_allowed, + "manual_review_required": queue.manual_review_required, + "items": items[:5], + } + except Exception as exc: + logger.debug( + "telegram_km_stale_completion_summary_fetch_failed", + incident_id=incident_id, + project_id=project_id, + error=str(exc), + ) + return { + "schema_version": "km_stale_owner_review_completion_telegram_summary_v1", + "status": "fetch_failed", + "project_id": project_id or "awoooi", + "incident_id": incident_id, + "items": [], + } + + async def _fetch_remediation_summary_for_card( *, approval_id: str, @@ -5855,17 +5963,23 @@ class TelegramGateway: error=str(remediation_exc), ) + project_id = getattr(incident, "project_id", None) or "awoooi" + km_completion_summary = await _fetch_km_stale_completion_summary_for_incident( + incident_id=incident_id, + project_id=project_id, + ) try: from src.services.awooop_truth_chain_service import fetch_truth_chain truth_chain = await fetch_truth_chain( source_id=incident_id, - project_id=getattr(incident, "project_id", None) or "awoooi", + project_id=project_id, ) lines += _format_awooop_status_chain_lines( truth_chain=truth_chain, remediation_history=remediation_history, ) + lines += _format_km_stale_completion_lines(km_completion_summary) lines += _format_remediation_history_lines(remediation_history) gateway_summary = ( (truth_chain.get("mcp") or {}) @@ -5884,6 +5998,7 @@ class TelegramGateway: lines += _format_awooop_status_chain_lines( remediation_history=remediation_history, ) + lines += _format_km_stale_completion_lines(km_completion_summary) lines += _format_remediation_history_lines(remediation_history) await self._send_html_line_message( @@ -6005,17 +6120,23 @@ class TelegramGateway: error=str(remediation_exc), ) + project_id = getattr(incident, "project_id", None) or "awoooi" + km_completion_summary = await _fetch_km_stale_completion_summary_for_incident( + incident_id=incident_id, + project_id=project_id, + ) try: from src.services.awooop_truth_chain_service import fetch_truth_chain truth_chain = await fetch_truth_chain( source_id=incident_id, - project_id=getattr(incident, "project_id", None) or "awoooi", + project_id=project_id, ) lines += _format_awooop_status_chain_lines( truth_chain=truth_chain, remediation_history=remediation_history, ) + lines += _format_km_stale_completion_lines(km_completion_summary) lines += _format_remediation_history_lines(remediation_history) lines += _format_automation_quality_lines( truth_chain.get("automation_quality") @@ -6029,6 +6150,7 @@ class TelegramGateway: lines += _format_awooop_status_chain_lines( remediation_history=remediation_history, ) + lines += _format_km_stale_completion_lines(km_completion_summary) lines += _format_remediation_history_lines(remediation_history) await self._send_html_line_message( diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py index 63e094d0..53594cb0 100644 --- a/apps/api/tests/test_telegram_message_templates.py +++ b/apps/api/tests/test_telegram_message_templates.py @@ -177,6 +177,35 @@ def test_awooop_status_chain_lines_show_read_only_manual_gate() -> None: assert "pending_human_approval" in joined +def test_km_stale_completion_lines_show_owner_review_queue_state() -> None: + """詳情/歷史要顯示 KM owner-review completion queue 是否卡住或可處理。""" + lines = telegram_gateway_module._format_km_stale_completion_lines({ + "schema_version": "km_stale_owner_review_completion_telegram_summary_v1", + "status": "ok", + "incident_id": "INC-20260513-205814", + "ready_count": 10, + "blocked_count": 0, + "completed_count": 1, + "failed_count": 0, + "writes_on_read": False, + "batch_writes_allowed": False, + "items": [ + { + "entry_id": "bf81a30d-6abe-4c0c-b4ba-9c0ba0d761bf", + "readiness": "ready", + "next_action": "preview_stale_km_review_completion", + } + ], + }) + + joined = "\n".join(lines) + assert "KM Owner Review" in joined + assert "ready 10" in joined + assert "batch-write no" in joined + assert "bf81a30d-6abe-4c0c-b4ba-9c0ba0d761bf" in joined + assert "preview_stale_km_review_completion" in joined + + def test_awooop_runs_url_for_incident_uses_public_incident_filter() -> None: """Telegram URL button 必須導到公開 AwoooP Run list,並帶 incident filter。""" url = telegram_gateway_module._awooop_runs_url_for_incident("INC-20260514-F85F21") diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 7a829fe9..385f307f 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -2022,6 +2022,8 @@ "knowledgeNext": "Next action: {action}", "knowledgeDrafts": "KM review drafts: {drafts}; duplicate drafts: {duplicates}", "knowledgeStaleCandidates": "Stale KM priority queue: {total}; top {top} / {tier}", + "knowledgeCompletionQueue": "Completion queue: ready {ready}; blocked {blocked}; completed {completed}; failed {failed}", + "knowledgeCompletionLatest": "Latest completion: {entry} / {readiness}; next {next}", "knowledgeEmpty": "No recent knowledge_degradation dispatch trail", "frontendConsole": "This page now reads production APIs instead of a static list", "mcpReady": "MCP Gateway gate is not currently a top gap", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 39c50654..24a15356 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -2023,6 +2023,8 @@ "knowledgeNext": "下一步:{action}", "knowledgeDrafts": "KM 審核草稿:{drafts};重複草稿:{duplicates}", "knowledgeStaleCandidates": "陳舊 KM 優先清單:{total} 筆;最高 {top} / {tier}", + "knowledgeCompletionQueue": "Completion queue:可處理 {ready};卡住 {blocked};完成 {completed};失敗 {failed}", + "knowledgeCompletionLatest": "最新 completion:{entry} / {readiness};下一步 {next}", "knowledgeEmpty": "近期沒有 knowledge_degradation dispatch trail", "frontendConsole": "本頁已改讀 production API,而非靜態清單", "mcpReady": "MCP Gateway gate 目前未列為主要缺口", diff --git a/apps/web/src/app/[locale]/awooop/work-items/page.tsx b/apps/web/src/app/[locale]/awooop/work-items/page.tsx index 48f4e339..2db266a8 100644 --- a/apps/web/src/app/[locale]/awooop/work-items/page.tsx +++ b/apps/web/src/app/[locale]/awooop/work-items/page.tsx @@ -1513,6 +1513,8 @@ function buildWorkItems( telemetry.knowledgeReviewDedupe?.duplicate_draft_total ?? knowledgeDuplicateDrafts; const knowledgeStaleTotal = telemetry.knowledgeStaleCandidates?.total_stale ?? 0; const topKnowledgeStaleCandidate = telemetry.knowledgeStaleCandidates?.items?.[0] ?? null; + const knowledgeCompletionQueue = telemetry.knowledgeStaleOwnerReviewCompletionQueue; + const latestKnowledgeCompletion = knowledgeCompletionQueue?.items?.[0] ?? null; const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue; const remediationTotal = remediationQueue?.total ?? 0; const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0; @@ -1766,6 +1768,21 @@ function buildWorkItems( top: topKnowledgeStaleCandidate?.entry_id ?? "--", tier: topKnowledgeStaleCandidate?.priority_tier ?? "--", }), + t("evidence.knowledgeCompletionQueue", { + ready: knowledgeCompletionQueue?.ready_count ?? 0, + blocked: knowledgeCompletionQueue?.blocked_count ?? 0, + completed: knowledgeCompletionQueue?.completed_count ?? 0, + failed: knowledgeCompletionQueue?.failed_count ?? 0, + }), + t("evidence.knowledgeCompletionLatest", { + entry: latestKnowledgeCompletion?.entry_id ?? "--", + readiness: latestKnowledgeCompletion + ? t( + `knowledgeGovernance.staleCandidates.completionQueue.readiness.${kmStaleCompletionReadinessKey(latestKnowledgeCompletion.readiness)}` as never + ) + : "--", + next: latestKnowledgeCompletion?.next_action ?? "--", + }), ] : [ t("evidence.knowledgeEmpty"), @@ -1774,6 +1791,12 @@ function buildWorkItems( top: topKnowledgeStaleCandidate?.entry_id ?? "--", tier: topKnowledgeStaleCandidate?.priority_tier ?? "--", }), + t("evidence.knowledgeCompletionQueue", { + ready: knowledgeCompletionQueue?.ready_count ?? 0, + blocked: knowledgeCompletionQueue?.blocked_count ?? 0, + completed: knowledgeCompletionQueue?.completed_count ?? 0, + failed: knowledgeCompletionQueue?.failed_count ?? 0, + }), ], href: "/awooop/work-items", },