feat(api): Phase 19.4 Terminal Service 真實 API 整合
整合真實後端服務,移除 Mock 數據: _handle_approval_action: - 使用 ApprovalDBService.get_pending_approvals() - 顯示待簽核清單摘要 (最多 5 個) - 渲染第一個待簽核項目的 ApprovalCard _handle_status_query: - 使用 K8s API 查詢 Pod 狀態 - 統計 Running/Ready/Total Pods - 顯示問題 Pods (非 Running 或 NotReady) - 查詢 Deployment 健康狀態 測試覆蓋: - 6 個新增 API 整合測試 - 總計 60 個測試通過 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ from src.models.terminal import (
|
||||
TerminalSession,
|
||||
TerminalSessionStatus,
|
||||
)
|
||||
from src.services.approval_db import get_approval_service
|
||||
from src.services.openclaw import get_openclaw
|
||||
|
||||
logger = get_logger("awoooi.terminal")
|
||||
@@ -489,30 +490,83 @@ class TerminalService:
|
||||
self,
|
||||
publisher,
|
||||
topic: str,
|
||||
session_id: str,
|
||||
_session_id: str, # Reserved for session-specific approval tracking
|
||||
_intent: str, # Reserved for intent-specific approval filtering
|
||||
) -> None:
|
||||
"""處理簽核操作"""
|
||||
"""
|
||||
處理簽核操作
|
||||
|
||||
Phase 19.4: 整合真實 Approval API
|
||||
@author Claude Code
|
||||
@date 2026-03-30 (台北時間)
|
||||
"""
|
||||
await self._publish_thought(publisher, topic, "Executor", "檢查待簽核項目...")
|
||||
await self._publish_tool_call(publisher, topic, "Approval-Query", "executing")
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# TODO: Phase 19.4 - 整合真實 Approval API
|
||||
await self._publish_tool_call(
|
||||
publisher, topic, "Approval-Query", "completed",
|
||||
result={"pending_count": 2}
|
||||
)
|
||||
await asyncio.sleep(0.3)
|
||||
try:
|
||||
approval_service = get_approval_service()
|
||||
pending_approvals = await approval_service.get_pending_approvals()
|
||||
|
||||
# 渲染 ApprovalCard
|
||||
await self._publish_render_ui(
|
||||
publisher, topic, "ApprovalCard",
|
||||
{
|
||||
"approvalId": f"APR-{session_id[:8].upper()}",
|
||||
"riskLevel": "MEDIUM",
|
||||
"kubectl": "kubectl rollout restart deployment/awoooi-api -n awoooi",
|
||||
}
|
||||
)
|
||||
await self._publish_tool_call(
|
||||
publisher, topic, "Approval-Query", "completed",
|
||||
result={"pending_count": len(pending_approvals)}
|
||||
)
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
if not pending_approvals:
|
||||
await self._publish_thought(
|
||||
publisher, topic, "Executor",
|
||||
"目前沒有待簽核的項目。"
|
||||
)
|
||||
return
|
||||
|
||||
# 顯示待簽核清單摘要
|
||||
summary_lines = [f"發現 {len(pending_approvals)} 個待簽核項目:"]
|
||||
for i, approval in enumerate(pending_approvals[:5], 1): # 最多顯示 5 個
|
||||
risk = approval.risk_level.value.upper() if approval.risk_level else "UNKNOWN"
|
||||
summary_lines.append(
|
||||
f" {i}. [{risk}] {approval.action[:50]}"
|
||||
)
|
||||
|
||||
if len(pending_approvals) > 5:
|
||||
summary_lines.append(f" ... 還有 {len(pending_approvals) - 5} 個")
|
||||
|
||||
await self._publish_thought(
|
||||
publisher, topic, "Executor",
|
||||
"\n".join(summary_lines)
|
||||
)
|
||||
|
||||
# 渲染第一個待簽核項目的 ApprovalCard
|
||||
first_approval = pending_approvals[0]
|
||||
risk_level = first_approval.risk_level.value.upper() if first_approval.risk_level else "MEDIUM"
|
||||
|
||||
# 從 metadata 取得 kubectl 指令 (如果有)
|
||||
kubectl_cmd = "kubectl get pods -n awoooi"
|
||||
if first_approval.metadata:
|
||||
kubectl_cmd = first_approval.metadata.get("kubectl_command", kubectl_cmd)
|
||||
|
||||
await self._publish_render_ui(
|
||||
publisher, topic, "ApprovalCard",
|
||||
{
|
||||
"approvalId": str(first_approval.id)[:8].upper(),
|
||||
"riskLevel": risk_level,
|
||||
"kubectl": kubectl_cmd,
|
||||
"description": first_approval.description[:100] if first_approval.description else "",
|
||||
"requiredSignatures": first_approval.required_signatures,
|
||||
"currentSignatures": first_approval.current_signatures,
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("approval_query_failed", error=str(e))
|
||||
await self._publish_tool_call(
|
||||
publisher, topic, "Approval-Query", "failed",
|
||||
result={"error": str(e)[:100]}
|
||||
)
|
||||
await self._publish_thought(
|
||||
publisher, topic, "System",
|
||||
f"查詢簽核項目失敗: {str(e)[:50]}"
|
||||
)
|
||||
|
||||
async def _handle_metrics_query(
|
||||
self,
|
||||
@@ -584,27 +638,110 @@ class TerminalService:
|
||||
_session_id: str, # Reserved for session-specific caching
|
||||
_intent: str, # Reserved for intent-specific status filtering
|
||||
) -> None:
|
||||
"""處理狀態查詢"""
|
||||
"""
|
||||
處理狀態查詢
|
||||
|
||||
Phase 19.4: 整合真實 K8s API
|
||||
@author Claude Code
|
||||
@date 2026-03-30 (台北時間)
|
||||
"""
|
||||
await self._publish_thought(publisher, topic, "Monitor", "檢查系統狀態...")
|
||||
await self._publish_tool_call(publisher, topic, "K8s-Status", "executing")
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# TODO: Phase 19.4 - 整合真實 K8s API
|
||||
await self._publish_tool_call(
|
||||
publisher, topic, "K8s-Status", "completed",
|
||||
result={
|
||||
"pods_running": 11,
|
||||
"pods_total": 12,
|
||||
"deployments_healthy": 5,
|
||||
}
|
||||
)
|
||||
await asyncio.sleep(0.3)
|
||||
try:
|
||||
# 使用 K8s Diagnostics Service 的 client
|
||||
from src.services.k8s_diagnostics import _get_k8s_client
|
||||
|
||||
await self._publish_thought(
|
||||
publisher, topic, "Monitor",
|
||||
"系統狀態: 11/12 Pods 運行中,5 個 Deployment 健康。\n"
|
||||
"1 個 Pod 正在重啟中 (awoooi-worker-xxx)。"
|
||||
)
|
||||
client = await _get_k8s_client()
|
||||
if not client:
|
||||
raise RuntimeError("K8s client initialization failed")
|
||||
|
||||
v1 = client.CoreV1Api()
|
||||
apps_v1 = client.AppsV1Api()
|
||||
|
||||
# 查詢 awoooi namespace 的 Pods
|
||||
namespace = "awoooi"
|
||||
pods = await v1.list_namespaced_pod(namespace=namespace)
|
||||
|
||||
# 統計 Pod 狀態
|
||||
pods_total = len(pods.items)
|
||||
pods_running = sum(
|
||||
1 for p in pods.items
|
||||
if p.status.phase == "Running"
|
||||
)
|
||||
pods_ready = sum(
|
||||
1 for p in pods.items
|
||||
if p.status.phase == "Running"
|
||||
and all(
|
||||
c.ready for c in (p.status.container_statuses or [])
|
||||
)
|
||||
)
|
||||
|
||||
# 找出有問題的 Pods
|
||||
problem_pods = []
|
||||
for pod in pods.items:
|
||||
if pod.status.phase != "Running":
|
||||
problem_pods.append(f"{pod.metadata.name}: {pod.status.phase}")
|
||||
elif pod.status.container_statuses:
|
||||
for cs in pod.status.container_statuses:
|
||||
if not cs.ready:
|
||||
reason = ""
|
||||
if cs.state and cs.state.waiting:
|
||||
reason = cs.state.waiting.reason or ""
|
||||
problem_pods.append(f"{pod.metadata.name}: {reason or 'NotReady'}")
|
||||
break
|
||||
|
||||
# 查詢 Deployments
|
||||
deployments = await apps_v1.list_namespaced_deployment(namespace=namespace)
|
||||
deployments_total = len(deployments.items)
|
||||
deployments_healthy = sum(
|
||||
1 for d in deployments.items
|
||||
if d.status.ready_replicas == d.spec.replicas
|
||||
)
|
||||
|
||||
await self._publish_tool_call(
|
||||
publisher, topic, "K8s-Status", "completed",
|
||||
result={
|
||||
"pods_running": pods_running,
|
||||
"pods_ready": pods_ready,
|
||||
"pods_total": pods_total,
|
||||
"deployments_healthy": deployments_healthy,
|
||||
"deployments_total": deployments_total,
|
||||
}
|
||||
)
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
# 組合狀態訊息
|
||||
status_lines = [
|
||||
f"系統狀態: {pods_ready}/{pods_total} Pods 就緒",
|
||||
f"Deployments: {deployments_healthy}/{deployments_total} 健康",
|
||||
]
|
||||
|
||||
if problem_pods:
|
||||
status_lines.append(f"\n問題 Pods ({len(problem_pods)}):")
|
||||
for pp in problem_pods[:3]: # 最多顯示 3 個
|
||||
status_lines.append(f" - {pp}")
|
||||
if len(problem_pods) > 3:
|
||||
status_lines.append(f" ... 還有 {len(problem_pods) - 3} 個")
|
||||
else:
|
||||
status_lines.append("\n所有 Pods 運行正常。")
|
||||
|
||||
await self._publish_thought(
|
||||
publisher, topic, "Monitor",
|
||||
"\n".join(status_lines)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("k8s_status_query_failed", error=str(e))
|
||||
await self._publish_tool_call(
|
||||
publisher, topic, "K8s-Status", "failed",
|
||||
result={"error": str(e)[:100]}
|
||||
)
|
||||
await self._publish_thought(
|
||||
publisher, topic, "System",
|
||||
f"K8s 狀態查詢失敗: {str(e)[:50]}\n"
|
||||
"請確認 K8s 連線配置。"
|
||||
)
|
||||
|
||||
async def _handle_general_chat(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user