fix(awooop): send controlled apply telegram receipts
Some checks failed
CD Pipeline / tests (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
Code Review / ai-code-review (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-27 20:49:32 +08:00
parent de609a79ae
commit 4e4c56cae5
5 changed files with 310 additions and 1 deletions

View File

@@ -217,6 +217,18 @@ def _int_or_none(value: Any) -> int | None:
return None
def _is_controlled_apply_record(row: dict[str, Any]) -> bool:
if not row:
return False
if row.get("approval_source"):
return True
if str(row.get("execution_mode") or "").strip().lower() == "controlled_apply":
return True
if "controlled_apply" in _tags(row):
return True
return str(row.get("actor") or "").strip().lower() == "ansible_controlled_apply_worker"
def _is_ansible_operation(row: dict[str, Any]) -> bool:
operation_type = str(_get(row, "operation_type") or "").lower()
if operation_type in ANSIBLE_OPERATION_TYPES:
@@ -341,7 +353,7 @@ def summarize_ansible_execution(records: list[dict[str, Any]]) -> dict[str, Any]
"pending_check_mode_total": pending_check_mode_total,
"applied_success_total": applied_success_total,
"applied": applied_success_total > 0,
"controlled_apply": bool(latest_apply) and bool(approval_source),
"controlled_apply": bool(latest_apply) and _is_controlled_apply_record(latest_apply),
"latest_operation_type": focused.get("operation_type"),
"latest_status": focused.get("status"),
"latest_catalog_id": focused.get("catalog_id"),

View File

@@ -805,6 +805,41 @@ async def _record_post_apply_verifier_and_learning(
return status
async def _send_controlled_apply_telegram_receipt(
claim: AnsibleCheckModeClaim,
result: AnsibleRunResult,
*,
apply_op_id: str,
writeback: dict[str, bool],
project_id: str,
) -> bool:
try:
from src.services.telegram_gateway import get_telegram_gateway
response = await get_telegram_gateway().send_controlled_apply_result_receipt(
incident_id=claim.incident_id,
catalog_id=claim.catalog_id,
apply_op_id=apply_op_id,
playbook_path=claim.apply_playbook_path,
verification_result=_post_apply_verification_result(result),
returncode=result.returncode,
duration_ms=result.duration_ms,
verifier_written=bool(writeback.get("verification")),
learning_written=bool(writeback.get("learning")),
project_id=project_id,
)
return bool(response)
except Exception as exc:
logger.warning(
"ansible_controlled_apply_telegram_receipt_failed",
incident_id=claim.incident_id,
catalog_id=claim.catalog_id,
apply_op_id=apply_op_id,
error=str(exc),
)
return False
async def backfill_missing_auto_repair_execution_receipts_once(
*,
project_id: str = "awoooi",
@@ -1322,6 +1357,13 @@ async def run_controlled_apply_for_claim(
apply_op_id=apply_op_id,
project_id=project_id,
)
telegram_receipt_sent = await _send_controlled_apply_telegram_receipt(
claim,
result,
apply_op_id=apply_op_id,
writeback=writeback,
project_id=project_id,
)
logger.info(
"ansible_controlled_apply_completed",
@@ -1335,6 +1377,7 @@ async def run_controlled_apply_for_claim(
auto_repair_receipt_written=receipt_written,
post_apply_verification_written=writeback.get("verification"),
post_apply_learning_written=writeback.get("learning"),
telegram_receipt_sent=telegram_receipt_sent,
)
return result

View File

@@ -9182,6 +9182,107 @@ class TelegramGateway:
}
return await self._send_request("sendMessage", payload)
async def send_controlled_apply_result_receipt(
self,
*,
incident_id: str,
catalog_id: str,
apply_op_id: str,
playbook_path: str,
verification_result: str,
returncode: int,
duration_ms: int,
verifier_written: bool,
learning_written: bool,
project_id: str = "awoooi",
) -> dict:
"""Send and mirror the AI Agent controlled-apply result receipt."""
if not self.alert_chat_id:
logger.warning(
"controlled_apply_result_receipt_skipped_no_chat",
incident_id=incident_id,
apply_op_id=apply_op_id,
)
return {}
success = verification_result == "success" and returncode == 0
next_step = (
"monitor_for_regression"
if success
else "queue_ai_rollback_or_playbook_repair"
)
title = (
"CONTROLLED APPLY RESULTAI Agent 受控執行完成"
if success
else "CONTROLLED APPLY RESULTAI Agent 受控執行待修復"
)
truth_chain: dict[str, object] | None = None
try:
from src.services.awooop_truth_chain_service import fetch_truth_chain
truth_chain = await fetch_truth_chain(
source_id=incident_id,
project_id=project_id or "awoooi",
)
except Exception as exc:
logger.warning(
"controlled_apply_result_truth_chain_snapshot_failed",
incident_id=incident_id,
apply_op_id=apply_op_id,
error=str(exc),
)
status_snapshot = _callback_reply_awooop_status_chain_snapshot(
incident_id=incident_id,
truth_chain=truth_chain,
)
source_extra = _callback_reply_source_envelope_extra(
incident_id=incident_id,
failure_context="controlled_apply_result",
status="callback_reply_sent",
chunk_index=0,
chunk_count=1,
callback_action="controlled_apply_result",
parse_mode="HTML",
awooop_status_chain=status_snapshot,
)
lines = [
f"<b>{html.escape(title)}</b>",
f"Incident: <code>{html.escape(str(incident_id))}</code>",
f"Catalog: <code>{html.escape(str(catalog_id or '--'))}</code>",
f"Apply op: <code>{html.escape(str(apply_op_id or '')[:8])}</code>",
f"PlayBook: <code>{html.escape(str(playbook_path or '--'))}</code>",
(
"Result: "
f"<code>{html.escape(str(verification_result or 'missing'))}</code> "
f"/ rc <code>{html.escape(str(returncode))}</code> "
f"/ {html.escape(str(duration_ms))}ms"
),
(
"Receipts: verifier "
f"<code>{html.escape(_bool_code(verifier_written))}</code> / KM "
f"<code>{html.escape(_bool_code(learning_written))}</code>"
),
f"Next: <code>{html.escape(next_step)}</code>",
f"Runs: {html.escape(incident_runs_url(incident_id, project_id=project_id or 'awoooi'))}",
]
payload: dict = {
"chat_id": self.alert_chat_id,
"text": "\n".join(lines)[:4096],
"parse_mode": "HTML",
"disable_web_page_preview": True,
}
reply_markup = incident_truth_chain_reply_markup(
incident_id,
project_id=project_id or "awoooi",
)
if reply_markup:
payload["reply_markup"] = reply_markup
if source_extra:
payload[_AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY] = source_extra
return await self._send_request("sendMessage", payload)
# =========================================================================
# 2026-04-24 Claude Sonnet 4.6 (ADR-095 WS4): Hermes NL 回覆
# =========================================================================