diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index 79a0dcc52..332debbfd 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -1824,10 +1824,13 @@ async def run_failed_apply_check_mode_replay_once( "check_mode": True, "diff": True, "apply_enabled": False, - "approval_required_before_apply": True, - "controlled_apply_allowed": False, - "controlled_apply_blocker": ( - "retry_requires_repair_and_new_apply_gate" + "approval_required_before_apply": False, + "owner_review_required": False, + "controlled_apply_allowed": True, + "controlled_apply_blocker": None, + "bounded_execution_scope": "retry_check_mode_only", + "next_controlled_action": ( + "queue_ai_playbook_or_transport_repair_candidate" ), } claimed = await db.execute( @@ -1898,13 +1901,20 @@ async def run_failed_apply_check_mode_replay_once( ) status, output, dry_run_result, error = _build_result_payload( replay_result, - controlled_apply_allowed=False, - controlled_apply_blocker="retry_requires_repair_and_new_apply_gate", + controlled_apply_allowed=True, + controlled_apply_blocker=None, ) output.update({ "execution_mode": "controlled_retry_check_mode_replay", "retry_of_apply_op_id": apply_op_id, + "apply_enabled": False, + "approval_required_before_apply": False, + "owner_review_required": False, "runtime_apply_executed": False, + "bounded_execution_scope": "retry_check_mode_only", + "next_required_step": ( + "queue_ai_playbook_or_transport_repair_candidate" + ), "terminal_disposition": ( "no_write_replay_passed_waiting_repair_candidate" if replay_result.returncode == 0 @@ -1914,7 +1924,14 @@ async def run_failed_apply_check_mode_replay_once( dry_run_result.update({ "execution_mode": "controlled_retry_check_mode_replay", "retry_of_apply_op_id": apply_op_id, + "apply_enabled": False, + "approval_required_before_apply": False, + "owner_review_required": False, "runtime_apply_executed": False, + "bounded_execution_scope": "retry_check_mode_only", + "next_controlled_action": ( + "queue_ai_playbook_or_transport_repair_candidate" + ), }) async with get_db_context(project_id) as db: await db.execute( diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 8d0122e73..944d95a91 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -2118,8 +2118,19 @@ def _legacy_outbound_run_id(chat_id: str, provider_message_id: str) -> UUID: return uuid5(NAMESPACE_URL, f"awoooi:legacy-telegram:{chat_id}:{provider_message_id}") -def _infer_outbound_message_type(text: str, payload: dict) -> str: +def _infer_outbound_message_type( + text: str, + payload: dict, + source_envelope_extra: dict[str, object] | None = None, +) -> str: """將既有 Telegram 訊息映射成 AwoooP outbound_message 的有限分類。""" + explicit_type = ( + source_envelope_extra.get("outbound_message_type") + if isinstance(source_envelope_extra, dict) + else None + ) + if explicit_type in {"interim", "final", "error", "approval_request"}: + return str(explicit_type) if "RUNBOOK REVIEW" in text or "待審核" in text: return "approval_request" if _has_reply_context(payload): @@ -5090,7 +5101,11 @@ class TelegramGateway: run_id=run_id, channel_type="telegram", channel_chat_id=chat_id, - message_type=_infer_outbound_message_type(text, payload), + message_type=_infer_outbound_message_type( + text, + payload, + source_envelope_extra, + ), content=text, source_envelope=_merge_outbound_source_envelope_extra( _outbound_source_envelope(method, payload), @@ -9457,6 +9472,7 @@ class TelegramGateway: awooop_status_chain=status_snapshot, ) if source_extra is not None: + source_extra["outbound_message_type"] = "final" if success else "error" source_extra["automation_run_id"] = automation_run_id callback_reply = source_extra.get("callback_reply") if isinstance(callback_reply, dict): diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index ede54cd0f..9d40a8506 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -1935,9 +1935,12 @@ def test_failed_apply_retry_replay_is_no_write_and_idempotent() -> None: assert "FOR UPDATE SKIP LOCKED" in source assert "controlled_retry_check_mode_replay" in source assert "build_ansible_check_mode_command" in source - assert "controlled_apply_allowed=False" in source + assert "controlled_apply_allowed=True" in source + assert '"approval_required_before_apply": False' in source + assert '"owner_review_required": False' in source assert '"runtime_apply_executed": False' in source - assert "retry_requires_repair_and_new_apply_gate" in source + assert "queue_ai_playbook_or_transport_repair_candidate" in source + assert "retry_requires_repair_and_new_apply_gate" not in source assert "_record_retry_runtime_stage_receipt" in source assert "NOT EXISTS" in source assert "run_controlled_apply_for_claim" not in source diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py index 1a345bb9e..a7b2289d4 100644 --- a/apps/api/tests/test_telegram_message_templates.py +++ b/apps/api/tests/test_telegram_message_templates.py @@ -1235,6 +1235,15 @@ async def test_controlled_apply_result_receipt_marks_callback_reply_evidence(mon assert source_extra["source_refs"]["automation_run_ids"] == [ "00000000-0000-0000-0000-000000000001" ] + assert source_extra["outbound_message_type"] == "final" + assert ( + telegram_gateway_module._infer_outbound_message_type( + payload["text"], + payload, + source_extra, + ) + == "final" + ) km_snapshot = source_extra["km_stale_completion_summary"] assert ( km_snapshot["source_schema_version"] @@ -2393,6 +2402,14 @@ def test_outbound_message_type_inference(): ) == "approval_request" ) + assert ( + telegram_gateway_module._infer_outbound_message_type( + "CONTROLLED APPLY RESULT|AI Agent 受控執行待修復", + {"reply_markup": {"inline_keyboard": []}}, + {"outbound_message_type": "error"}, + ) + == "error" + ) assert ( telegram_gateway_module._infer_outbound_message_type( "🤖❌ [AUTO] AI 自動修復失敗",