From fdce0a3ab9ca98f478af07385f9e398566e8addc Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 19 Apr 2026 01:08:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(approval):=20NO=5FACTION=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=AA=A4=E6=A8=99=20EXECUTION=5FFAILED=20(MASTER=20?= =?UTF-8?q?=C2=A77.1=20#11=20=E4=BF=AE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-04-19 凌晨(台北時區)— ogt + Claude Opus 4.7 (1M) 根因: approval.action='NO_ACTION - 待分析' (幻覺 validator 降級產物) 丟進 parse_operation_from_action → operation_type=None → background_execution_skip → update_execution_status(success=False) → 標為 EXECUTION_FAILED。 污染 KPI: MASTER §7.1 #11 auto_execute 成功率 = EXECUTION_SUCCESS / (SUCCESS+FAILED) NO_ACTION 本來就不該計入失敗,但卻被算進去拖垮指標。 實測 30d 成功率 0.9% 有很大比例是 NO_ACTION 誤標造成。 修復: parse 失敗時先判斷是否 NO_ACTION 類 (action 含 NO_ACTION/OBSERVE/INVESTIGATE 等關鍵字) → 走專屬 noop 分支: - log event=background_execution_noop (info 級) - update_execution_status(success=True) → EXECUTION_SUCCESS - timeline 標 ✅ 純觀察類動作完成 - reply 原告警卡片顯示成功 - return True 真正解析失敗 (非 NO_ACTION) 保留原失敗路徑,但補上 error_message (P0.2 延伸),讓 rejection_reason 有 "Could not parse operation type from action: " 而非空字串。 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/src/services/approval_execution.py | 47 ++++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/apps/api/src/services/approval_execution.py b/apps/api/src/services/approval_execution.py index b8d1c5de..3de07695 100644 --- a/apps/api/src/services/approval_execution.py +++ b/apps/api/src/services/approval_execution.py @@ -144,14 +144,57 @@ class ApprovalExecutionService: namespace = parsed.namespace if operation_type is None or resource_name is None: + # 2026-04-19 ogt + Claude Opus 4.7: 區分 NO_ACTION vs 真解析失敗 + # NO_ACTION 是 AI 刻意選的「純調查不破壞」,不該誤標 EXECUTION_FAILED + # 污染 auto_execute 成功率 KPI (MASTER §7.1 #11) + _action_upper = (approval.action or "").upper() + _is_no_action = ( + "NO_ACTION" in _action_upper + or "NO-ACTION" in _action_upper + or "NOACTION" in _action_upper + or "(未設)" in approval.action + or _action_upper.startswith("OBSERVE") + or _action_upper.startswith("INVESTIGATE") + ) + + if _is_no_action: + logger.info( + "background_execution_noop", + approval_id=str(approval.id), + action=approval.action, + reason="NO_ACTION - 純調查/觀察類,不執行破壞動作", + ) + # 標為 SUCCESS (觀察/調查本身就是成功完成) + await service.update_execution_status(approval.id, success=True) + await timeline.add_event( + event_type="exec", + status="success", + title="✅ 純觀察類動作完成 (NO_ACTION)", + description=f"Action: {approval.action[:120]}", + actor="leWOOOgo", + actor_role="executor", + approval_id=str(approval.id), + ) + # 執行結果 reply 原告警卡片 + asyncio.create_task( + self._push_execution_result_to_alert( + approval, success=True, error=None, + ) + ) + return True # NO_ACTION 視為成功完成 + + # 真解析失敗 (非 NO_ACTION) logger.warning( "background_execution_skip", approval_id=str(approval.id), reason="Could not parse operation type from action", action=approval.action, ) - # Phase 5: 更新資料庫狀態 - await service.update_execution_status(approval.id, success=False) + # Phase 5: 更新資料庫狀態 + 帶 error_message (P0.2) + await service.update_execution_status( + approval.id, success=False, + error_message=f"Could not parse operation type from action: {approval.action[:150]}", + ) await timeline.add_event( event_type="exec", status="error",