fix(awooop): send controlled apply telegram receipts
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 RESULT|AI Agent 受控執行完成"
|
||||
if success
|
||||
else "CONTROLLED APPLY RESULT|AI 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 回覆
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user