diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 286300c8..ae8673f1 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -479,9 +479,79 @@ def _format_km_stale_completion_lines(summary: dict[str, object] | None) -> list else: lines.append("此事件: no_related_owner_review") lines.append("下一步: review_awooop_completion_queue") + triage = _km_stale_completion_triage(summary) + if triage: + support = triage.get("supporting_agents") + support_text = ( + " / ".join(str(item) for item in support[:3]) + if isinstance(support, list) + else "--" + ) + lines += [ + ( + "流程: " + f"{html.escape(str(triage.get('flow_stage') or '--'))}" + " / 匹配 " + f"{html.escape(str(triage.get('matching_strategy') or '--'))}" + ), + ( + "主責: " + f"{html.escape(str(triage.get('ai_lead_agent') or '--'))}" + " / 協作 " + f"{html.escape(support_text)}" + ), + ( + "自動化: " + f"{html.escape(str(triage.get('automation_state') or '--'))}" + " / safe-auto " + f"{html.escape(_bool_code(triage.get('safe_to_auto_repair')))}" + ), + ( + "卡點: " + f"{html.escape(str(triage.get('blocking_reason') or '--'))}" + ), + ] return lines +def _km_stale_completion_triage( + summary: dict[str, object], +) -> dict[str, object] | None: + triage = summary.get("triage") + if isinstance(triage, dict): + return triage + work_item = summary.get("work_item") + if isinstance(work_item, dict) and isinstance(work_item.get("triage"), dict): + return work_item["triage"] + return None + + +def _build_km_stale_callback_owner_review_triage( + *, + reason: str, +) -> dict[str, object]: + return { + "schema_version": "km_stale_callback_owner_review_triage_v1", + "flow_stage": "callback_observed_owner_review_link_missing", + "ai_lead_agent": "Hermes", + "supporting_agents": ["OpenClaw", "ElephantAlpha"], + "automation_state": "manual_owner_review_required", + "safe_to_auto_repair": False, + "blocking_reason": reason, + "matching_strategy": "related_incident_id_exact_match", + "already_done": [ + "callback_reply_persisted", + "completion_queue_checked", + "telegram_detail_history_rendered", + ], + "next_actions": [ + "review_awooop_callback_work_item", + "queue_matching_km_stale_candidate", + "complete_owner_review_after_owner_approval", + ], + } + + async def _fetch_km_stale_completion_summary_for_incident( *, incident_id: str, @@ -508,11 +578,33 @@ async def _fetch_km_stale_completion_summary_for_incident( for item in queue.items if item.related_incident_id == incident_id ] + status_value = "matched_owner_review" if items else "no_related_owner_review" + reason = None if items else "no_matching_completion_item" + triage = ( + None + if items + else _build_km_stale_callback_owner_review_triage(reason=reason) + ) + work_item = None + if triage: + work_item = { + "schema_version": "km_stale_callback_owner_review_work_item_v1", + "kind": "km_stale_callback_owner_review", + "status": "open", + "incident_id": incident_id, + "reason": reason, + "next_step": "review_or_queue_km_owner_review", + "triage": triage, + "writes_on_read": False, + "manual_review_required": True, + "batch_writes_allowed": False, + } return { "schema_version": "km_stale_owner_review_completion_telegram_summary_v1", - "status": "ok", + "status": status_value, "project_id": project_id or "awoooi", "incident_id": incident_id, + "missing_reason": reason, "total": queue.total, "returned": queue.returned, "pending_count": queue.pending_count, @@ -524,6 +616,8 @@ async def _fetch_km_stale_completion_summary_for_incident( "batch_writes_allowed": queue.batch_writes_allowed, "manual_review_required": queue.manual_review_required, "items": items[:5], + "triage": triage, + "work_item": work_item, } except Exception as exc: logger.debug( diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py index 53594cb0..72ec78ae 100644 --- a/apps/api/tests/test_telegram_message_templates.py +++ b/apps/api/tests/test_telegram_message_templates.py @@ -206,6 +206,46 @@ def test_km_stale_completion_lines_show_owner_review_queue_state() -> None: assert "preview_stale_km_review_completion" in joined +def test_km_stale_completion_lines_show_callback_triage_state() -> None: + """詳情/歷史要顯示 callback owner-review 缺口的流程、主責與卡點。""" + lines = telegram_gateway_module._format_km_stale_completion_lines({ + "schema_version": "km_stale_owner_review_completion_telegram_summary_v1", + "status": "no_related_owner_review", + "incident_id": "INC-20260524-16109D", + "ready_count": 10, + "blocked_count": 0, + "completed_count": 1, + "failed_count": 0, + "writes_on_read": False, + "batch_writes_allowed": False, + "items": [], + "work_item": { + "schema_version": "km_stale_callback_owner_review_work_item_v1", + "status": "open", + "triage": { + "schema_version": "km_stale_callback_owner_review_triage_v1", + "flow_stage": "callback_observed_owner_review_link_missing", + "ai_lead_agent": "Hermes", + "supporting_agents": ["OpenClaw", "ElephantAlpha"], + "automation_state": "manual_owner_review_required", + "safe_to_auto_repair": False, + "blocking_reason": "no_matching_completion_item", + "matching_strategy": "related_incident_id_exact_match", + }, + }, + }) + + joined = "\n".join(lines) + assert "no_related_owner_review" in joined + assert "callback_observed_owner_review_link_missing" in joined + assert "related_incident_id_exact_match" in joined + assert "Hermes" in joined + assert "OpenClaw / ElephantAlpha" in joined + assert "manual_owner_review_required" in joined + assert "safe-auto no" in joined + assert "no_matching_completion_item" 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")