fix(web): pending-approvals-card 加防重複點擊 + loading 狀態

linter 自動強化: actioningId state 防止同一張卡重複操作
- disabled + opacity 0.6 + cursor not-allowed
- loading 時按鈕顯示 '...'
- finally() 確保 actioningId 清除

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-09 18:38:08 +08:00
parent 890e2a9568
commit 896bef94ee
7 changed files with 89 additions and 48 deletions

View File

@@ -71,6 +71,53 @@ CONFIDENCE_THRESHOLD_COLLAB = 0.70 # 低於此閾值自動標記為 COLLAB
LLMAnalysisResult = OpenClawDecision
# =============================================================================
# kubectl_command 回填 helper
# 2026-04-09 Claude Sonnet 4.6: I2 架構Review修復 — 補齊所有 tool 類型,消除兩處重複邏輯 (M3)
# =============================================================================
def _backfill_kubectl_command(proposal: dict, tools: list) -> None:
"""將 AI tool call 結果回填為可執行的 kubectl_command。
proposal["kubectl_command"] 若已有值則不覆蓋LLM 直接填的優先)。
"""
if not tools or proposal.get("kubectl_command"):
return
_t = tools[0]
_tool_name = _t.get("tool", "")
_args = _t.get("args", {})
_ns = _args.get("namespace", proposal.get("namespace", "awoooi-prod"))
if _tool_name == "restart_deployment":
_deploy = _args.get("deployment_name", proposal.get("target_resource", ""))
if _deploy:
proposal["kubectl_command"] = f"kubectl rollout restart deployment/{_deploy} -n {_ns}"
elif _tool_name == "delete_pod":
_pod = _args.get("pod_name", "")
if _pod:
proposal["kubectl_command"] = f"kubectl delete pod {_pod} -n {_ns}"
elif _tool_name == "scale_deployment":
_deploy = _args.get("deployment_name", "")
_replicas = _args.get("replicas", 2)
if _deploy:
proposal["kubectl_command"] = f"kubectl scale deployment/{_deploy} --replicas={_replicas} -n {_ns}"
elif _tool_name == "delete_deployment":
_deploy = _args.get("deployment_name", "")
if _deploy:
proposal["kubectl_command"] = f"kubectl delete deployment/{_deploy} -n {_ns}"
elif _tool_name == "drain_node":
_node = _args.get("node_name", "")
if _node:
proposal["kubectl_command"] = f"kubectl drain {_node} --ignore-daemonsets --delete-emptydir-data"
elif _tool_name == "cordon_node":
_node = _args.get("node_name", "")
if _node:
proposal["kubectl_command"] = f"kubectl cordon {_node}"
elif _tool_name == "delete_service":
_svc = _args.get("service_name", "")
if _svc:
proposal["kubectl_command"] = f"kubectl delete service/{_svc} -n {_ns}"
# =============================================================================
# OpenClaw Service
# =============================================================================
@@ -1559,25 +1606,7 @@ Focus on:
# 2026-04-09 Claude Sonnet 4.6: 將 Nemotron tool call 回填為 kubectl_command
# 根本問題修復approval_records.action 需要可執行指令才能被 parse_operation_from_action 解析
_tools = proposal["nemotron_tools"]
if _tools:
_t = _tools[0]
_tool_name = _t.get("tool", "")
_args = _t.get("args", {})
_ns = _args.get("namespace", proposal.get("namespace", "awoooi-prod"))
if _tool_name == "restart_deployment":
_deploy = _args.get("deployment_name", proposal.get("target_resource", ""))
if _deploy:
proposal["kubectl_command"] = f"kubectl rollout restart deployment/{_deploy} -n {_ns}"
elif _tool_name == "delete_pod":
_pod = _args.get("pod_name", "")
if _pod:
proposal["kubectl_command"] = f"kubectl delete pod {_pod} -n {_ns}"
elif _tool_name == "scale_deployment":
_deploy = _args.get("deployment_name", "")
_replicas = _args.get("replicas", 2)
if _deploy:
proposal["kubectl_command"] = f"kubectl scale deployment/{_deploy} --replicas={_replicas} -n {_ns}"
_backfill_kubectl_command(proposal, proposal["nemotron_tools"])
logger.info(
"nemotron_collaboration_complete",
@@ -1628,25 +1657,7 @@ Focus on:
proposal["nemotron_tool_backend"] = "Gemini 雲端"
# 2026-04-09 Claude Sonnet 4.6: Gemini fallback 同樣回填 kubectl_command
_fb_tools = proposal["nemotron_tools"]
if _fb_tools:
_t = _fb_tools[0]
_tool_name = _t.get("tool", "")
_args = _t.get("args", {})
_ns = _args.get("namespace", proposal.get("namespace", "awoooi-prod"))
if _tool_name == "restart_deployment":
_deploy = _args.get("deployment_name", proposal.get("target_resource", ""))
if _deploy:
proposal["kubectl_command"] = f"kubectl rollout restart deployment/{_deploy} -n {_ns}"
elif _tool_name == "delete_pod":
_pod = _args.get("pod_name", "")
if _pod:
proposal["kubectl_command"] = f"kubectl delete pod {_pod} -n {_ns}"
elif _tool_name == "scale_deployment":
_deploy = _args.get("deployment_name", "")
_replicas = _args.get("replicas", 2)
if _deploy:
proposal["kubectl_command"] = f"kubectl scale deployment/{_deploy} --replicas={_replicas} -n {_ns}"
_backfill_kubectl_command(proposal, proposal["nemotron_tools"])
return proposal, provider, True