Files
awoooi/apps/api/src/services/operation_parser.py
OG T 658337ec18
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 1m29s
Type Sync Check / check-type-sync (push) Failing after 52s
fix(phase26): 打通 Incident→DB→KM 完整鏈路 + namespace 修正
問題根因:
1. create_incident_for_approval 只存 Redis,不存 PostgreSQL
   → TTL 7天後消失,Playbook 萃取永遠找不到 Incident
2. ApprovalRecord 無 incident_id 欄位
   → _trigger_playbook_extraction 靠 regex 掃中文文字找 INC-,永遠失敗
3. operation_parser namespace fallback 是 "default"
   → 所有 deployment 在 awoooi-prod,203 次執行全失敗

修復:
- Incident 同時寫入 Redis + PostgreSQL (save_to_episodic_memory)
- ApprovalRecord 加入 incident_id 欄位 (model + ORM + migration)
- alertmanager_webhook 建立 Approval 後回寫 incident_id
- _trigger_playbook_extraction 直接用 approval.incident_id
- operation_parser DEFAULT_NAMESPACE = "awoooi-prod"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 11:46:05 +08:00

186 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 服務"
Examples:
"kubectl delete pod nginx-xxx -n production"
→ ParsedOperation(DELETE_POD, "nginx-xxx", "production")
"Restart deployment api-backend"
→ ParsedOperation(RESTART_DEPLOYMENT, "api-backend", "default")
"Scale deployment web-frontend to 5 replicas"
→ ParsedOperation(SCALE_DEPLOYMENT, "web-frontend", "default")
"重新啟動 awoooi-worker 服務"
→ ParsedOperation(RESTART_DEPLOYMENT, "awoooi-worker", "default")
Args:
action: 操作指令字串
Returns:
ParsedOperation: 解析後的操作資訊
"""
action_lower = action.lower()
# Pattern: kubectl rollout restart deployment/<name>
kubectl_restart_match = re.search(
r"kubectl\s+rollout\s+restart\s+deployment/([a-z0-9][\w.-]*)", action_lower
)
if kubectl_restart_match:
deploy_name = kubectl_restart_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: kubectl delete pod <name>
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 <name> (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 <name> (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 <name> (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 <name> (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: 重新啟動 <name> 服務 (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 <name>
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: 擴容 <name> (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: 擴展 <name> 副本數 (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: 重新啟動 <name> (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)