fix: Telegram 簽核 gate + 執行結果 reply — 打通人工審核閉環
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 14m7s

3 處修復(統帥盤查發現):

1. telegram_gateway.py:4890 — gate 從 execution_triggered 改 approval.status==APPROVED
   - 原 gate 靠樂觀鎖旗標,race 時失效(REST+Telegram 同時簽核)
   - 與 REST API approvals.py:360 路徑對齊
   - 加 Redis lock exec:{approval_id} 60s TTL 防重入

2. telegram_gateway.py:4772 — 拿掉「👀 等待執行」誤導文案
   - 批准後一律顯示「 執行中...」,實際結果由 #3 reply 補上

3. approval_execution.py — 新增 _push_execution_result_to_alert()
   - 成功/失敗兩處 fire-and-forget 呼叫
   - requested_by=="auto_approve" skip(避免與 _push_auto_repair_result 衝突)
   - Redis tg_msg:{incident_id} 查原告警 message_id → reply_to
   - 找不到 message_id 靜默不發,不影響執行主流程

防破壞性檢查:
-  自動執行路徑不受影響(skip via requested_by)
-  Reject 路徑完全不動
-  Redis lock 防重入
-  132 回歸測試全過

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-14 19:03:38 +08:00
parent e7171a4ac8
commit 72dd0c5875
2 changed files with 121 additions and 13 deletions

View File

@@ -250,6 +250,12 @@ class ApprovalExecutionService:
)
)
# 2026-04-14 Claude Sonnet 4.6: reply_to 原告警卡片顯示執行結果
# auto_approve 路徑由 _push_auto_repair_result 處理,此處僅處理人工批准
asyncio.create_task(
self._push_execution_result_to_alert(approval, success=True, error=None)
)
# Phase 7.6: 觸發 Playbook 自動萃取 (fire-and-forget)
asyncio.create_task(
self._trigger_playbook_extraction(approval)
@@ -325,6 +331,13 @@ class ApprovalExecutionService:
)
)
# 2026-04-14 Claude Sonnet 4.6: reply_to 原告警卡片顯示失敗結果
asyncio.create_task(
self._push_execution_result_to_alert(
approval, success=False, error=result.error
)
)
# ADR-030 Phase 5: 觸發學習服務 (失敗案例)
asyncio.create_task(
self._trigger_learning(
@@ -335,6 +348,79 @@ class ApprovalExecutionService:
)
)
async def _push_execution_result_to_alert(
self,
approval: ApprovalRequest,
success: bool,
error: str | None,
) -> None:
"""
執行結果回覆到原告警 Telegram 卡片reply_to_message_id
2026-04-14 Claude Sonnet 4.6 實裝:
- 人工路徑:人類在 Telegram 點批准後,等執行完成,在原告警下 reply 執行結果
- 自動路徑 (requested_by=auto_approve) 由 _push_auto_repair_result 處理,此處 skip
透過 Redis tg_msg:{incident_id} 查原告警 message_id找不到則靜默不發。
"""
try:
# 自動執行路徑 skip避免與 _push_auto_repair_result 重複發訊息)
if (approval.requested_by or "").lower() == "auto_approve":
return
if not approval.incident_id:
return
from src.core.redis_client import get_redis
redis = get_redis()
msg_id_raw = await redis.get(f"tg_msg:{approval.incident_id}")
if not msg_id_raw:
logger.debug(
"push_execution_result_no_msg_id",
incident_id=approval.incident_id,
approval_id=str(approval.id),
)
return
try:
orig_msg_id = int(msg_id_raw)
except (TypeError, ValueError):
return
from src.core.config import get_settings
from src.services.telegram_gateway import get_telegram_gateway
settings = get_settings()
gateway = get_telegram_gateway()
if success:
text = f"✅ <b>執行成功</b>\n<code>{(approval.action or '')[:180]}</code>"
else:
err_short = (error or "未知錯誤")[:150]
text = f"❌ <b>執行失敗</b>\n<code>{(approval.action or '')[:180]}</code>\n原因: {err_short}"
await gateway._http_client.post(
f"https://api.telegram.org/bot{settings.OPENCLAW_TG_BOT_TOKEN}/sendMessage",
json={
"chat_id": settings.OPENCLAW_TG_CHAT_ID,
"text": text,
"parse_mode": "HTML",
"reply_to_message_id": orig_msg_id,
},
)
logger.info(
"push_execution_result_sent",
incident_id=approval.incident_id,
approval_id=str(approval.id),
success=success,
orig_msg_id=orig_msg_id,
)
except Exception as e:
logger.warning(
"push_execution_result_failed",
approval_id=str(approval.id),
error=str(e),
)
async def _get_anomaly_key_from_approval(self, approval: ApprovalRequest) -> str | None:
"""
從 approval → incident → anomaly_key。