""" Operation Parser Service - Phase 16 R4 瘦身 Router 抽取 ======================================================= 從 approvals.py 抽取操作解析邏輯,支援中英文指令格式。 功能: - 從 action 字串解析 OperationType - 提取資源名稱與 Namespace - 支援 kubectl 指令與中文自然語言 版本: v1.0 建立: 2026-03-25 (台北時區) 建立者: Claude Code (Phase 16 R4) """ import re from dataclasses import dataclass from src.services.executor import OperationType # 2026-04-06 ogt: Phase 26 — 預設 namespace 改為 awoooi-prod # 原本 "default" 導致 203 次執行全失敗(deployment 全在 awoooi-prod) DEFAULT_NAMESPACE = "awoooi-prod" @dataclass class ParsedOperation: """ 解析後的操作資訊 Attributes: operation_type: K8s 操作類型 (RESTART_DEPLOYMENT, DELETE_POD, etc.) resource_name: 目標資源名稱 namespace: K8s Namespace (預設 "awoooi-prod") Note: 支援 tuple 解包以向後兼容: op, resource, ns = parse_operation_from_action(action) """ operation_type: OperationType | None resource_name: str | None namespace: str def __iter__(self): """支援 tuple 解包: op, resource, ns = parsed""" return iter((self.operation_type, self.resource_name, self.namespace)) def parse_operation_from_action(action: str) -> ParsedOperation: """ 從 action 字串解析操作類型與目標資源 支援格式: - kubectl 指令: "kubectl delete pod nginx-xxx -n production" - 英文自然語言: "Restart deployment api-backend" - 中文自然語言: "重新啟動 awoooi-worker 服務" - SSH 指令: "ssh 192.168.0.110 'docker prune ...'" / "ssh wooo@192.168.0.110 ..." Examples: "kubectl delete pod nginx-xxx -n production" → ParsedOperation(DELETE_POD, "nginx-xxx", "production") "ssh 192.168.0.110 'docker image prune -a -f'" → ParsedOperation(SSH_HOST, "192.168.0.110", "host") Args: action: 操作指令字串 Returns: ParsedOperation: 解析後的操作資訊 """ action_lower = action.lower() # 2026-05-02 ogt + Claude Sonnet 4.6: SSH host 操作識別 # 根因:approval_execution 把 SSH action 丟進 kubectl parser → 全部 None → 「Could not parse」 # 修法:第一順位偵測 ssh,回 SSH_HOST,approval_execution 走 SSHProvider # 支援 "ssh host '...'" / "ssh user@host ..." / "ssh -o opt host ..." ssh_match = re.search( r"^\s*ssh\s+(?:-[a-zA-Z]\s*\S+\s+)*(?:[a-zA-Z][\w-]*@)?([a-zA-Z0-9][\w.-]*)", action_lower, ) if ssh_match: host = ssh_match.group(1) # namespace 欄位借用為「host_class」,下游不必改 return ParsedOperation(OperationType.SSH_HOST, host, "host") # 2026-04-24 ogt + Claude Sonnet 4.6: Gate 11 修復 — 唯讀指令識別(INVESTIGATE) # 根因:parse_operation_from_action 完全不認識 kubectl get/top/describe/logs → 回 None → 執行失敗 # 修法:優先匹配唯讀指令,回傳 OperationType.INVESTIGATE(零衝擊,blast_radius score=1) kubectl_ro_match = re.search( r"kubectl\s+(get|top|describe|logs|version)\s*([a-z][\w.-]*)?", action_lower, ) if kubectl_ro_match: ns_match = re.search(r"-n\s+(\S+)", action_lower) namespace = ns_match.group(1) if ns_match else DEFAULT_NAMESPACE resource = kubectl_ro_match.group(2) or "pods" return ParsedOperation(OperationType.INVESTIGATE, resource, namespace) # Pattern: kubectl rollout restart deployment|statefulset|daemonset/ kubectl_restart_match = re.search( r"kubectl\s+rollout\s+restart\s+(deployment|deploy|statefulset|sts|daemonset|ds)/([a-z0-9][\w.-]*)", action_lower, ) if kubectl_restart_match: resource_type = kubectl_restart_match.group(1) resource_name = kubectl_restart_match.group(2) ns_match = re.search(r"-n\s+(\S+)", action_lower) namespace = ns_match.group(1) if ns_match else DEFAULT_NAMESPACE if resource_type in {"statefulset", "sts"}: return ParsedOperation(OperationType.RESTART_STATEFULSET, resource_name, namespace) if resource_type in {"daemonset", "ds"}: return ParsedOperation(OperationType.RESTART_DAEMONSET, resource_name, namespace) return ParsedOperation(OperationType.RESTART_DEPLOYMENT, resource_name, namespace) # Pattern: kubectl delete pod delete_pod_match = re.search( r"delete\s+pod[:\s]+([a-z0-9][\w.-]*)", action_lower ) if delete_pod_match: pod_name = delete_pod_match.group(1) ns_match = re.search(r"-n\s+(\S+)", action_lower) namespace = ns_match.group(1) if ns_match else DEFAULT_NAMESPACE return ParsedOperation(OperationType.DELETE_POD, pod_name, namespace) # Pattern: 刪除 Pod (Chinese delete) chinese_delete_match = re.search(r"刪除\s*[Pp]od\s+([a-z0-9][\w.-]*)", action) if chinese_delete_match: pod_name = chinese_delete_match.group(1) return ParsedOperation(OperationType.DELETE_POD, pod_name, DEFAULT_NAMESPACE) # Pattern: restart deployment (English - with explicit "deployment") restart_deploy_match = re.search( r"restart\s+deployment[:\s]+([a-z0-9][\w.-]*)", action_lower ) if restart_deploy_match: deploy_name = restart_deploy_match.group(1) ns_match = re.search(r"-n\s+(\S+)", action_lower) namespace = ns_match.group(1) if ns_match else DEFAULT_NAMESPACE return ParsedOperation(OperationType.RESTART_DEPLOYMENT, deploy_name, namespace) # Pattern: restart (English - without "deployment" keyword) restart_simple_match = re.search(r"restart\s+([a-z0-9][\w.-]*)", action_lower) if restart_simple_match: deploy_name = restart_simple_match.group(1) # Skip if captured word is "deployment" (handled above) if deploy_name != "deployment": ns_match = re.search(r"-n\s+(\S+)", action_lower) namespace = ns_match.group(1) if ns_match else DEFAULT_NAMESPACE return ParsedOperation( OperationType.RESTART_DEPLOYMENT, deploy_name, namespace ) # Pattern: 重新啟動 deployment (Chinese with "deployment" keyword) chinese_restart_deploy_match = re.search( r"重新啟動\s+deployment\s+([a-z0-9][\w.-]*)", action, re.IGNORECASE ) if chinese_restart_deploy_match: deploy_name = chinese_restart_deploy_match.group(1) return ParsedOperation(OperationType.RESTART_DEPLOYMENT, deploy_name, DEFAULT_NAMESPACE) # Pattern: 重新啟動 服務 (Chinese) chinese_restart_match = re.search(r"重新啟動\s+([a-z0-9][\w.-]*)\s*服務", action) if chinese_restart_match: resource_name = chinese_restart_match.group(1) # StatefulSet Pod 格式: name-N (如 postgres-primary-0) if re.match(r".*-\d+$", resource_name): return ParsedOperation(OperationType.DELETE_POD, resource_name, DEFAULT_NAMESPACE) return ParsedOperation( OperationType.RESTART_DEPLOYMENT, resource_name, DEFAULT_NAMESPACE ) # Pattern: scale deployment scale_match = re.search( r"scale\s+(?:deployment[:\s]+)?([a-z0-9][\w.-]*)", action_lower ) if scale_match: deploy_name = scale_match.group(1) ns_match = re.search(r"-n\s+(\S+)", action_lower) namespace = ns_match.group(1) if ns_match else DEFAULT_NAMESPACE return ParsedOperation(OperationType.SCALE_DEPLOYMENT, deploy_name, namespace) # Pattern: 擴容 (Chinese scale) chinese_scale_match = re.search(r"擴容\s+([a-z0-9][\w.-]*)", action) if chinese_scale_match: deploy_name = chinese_scale_match.group(1) return ParsedOperation(OperationType.SCALE_DEPLOYMENT, deploy_name, DEFAULT_NAMESPACE) # Pattern: 擴展 副本數 (Chinese scale variant) chinese_scale2_match = re.search(r"擴展\s+([a-z0-9][\w.-]*)\s*副本", action) if chinese_scale2_match: deploy_name = chinese_scale2_match.group(1) # 移除常見的後綴如 -deployment deploy_name = re.sub(r"-deployment$", "", deploy_name) return ParsedOperation(OperationType.SCALE_DEPLOYMENT, deploy_name, DEFAULT_NAMESPACE) # Pattern: 重新啟動 (Chinese restart without 服務) chinese_restart2_match = re.search(r"重新啟動\s+([a-z0-9][\w.-]*)", action) if chinese_restart2_match: resource_name = chinese_restart2_match.group(1) # StatefulSet Pod 格式: name-N (如 postgres-primary-0) if re.match(r".*-\d+$", resource_name): return ParsedOperation(OperationType.DELETE_POD, resource_name, DEFAULT_NAMESPACE) # 移除常見的後綴 deploy_name = re.sub(r"-deployment$", "", resource_name) return ParsedOperation( OperationType.RESTART_DEPLOYMENT, deploy_name, DEFAULT_NAMESPACE ) return ParsedOperation(None, None, DEFAULT_NAMESPACE)