feat: /api/v1/auto-repair/history endpoint + neural-command 接真實 API (Sprint 3)
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 8m50s

- 新增 RepairHistoryItem/RepairHistoryResponse Pydantic models
- GET /api/v1/auto-repair/history?limit=N 從 incidents working memory 推導修復歷史
- 前端 fetchData() 同時拉 history + approvals/pending,移除硬編碼 pendingApprovals=0
- try/except 包覆確保任何錯誤都回傳空列表不中斷前端

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-06 14:28:55 +08:00
parent 4561f141bb
commit 02510d3d93
2 changed files with 107 additions and 3 deletions

View File

@@ -13,7 +13,7 @@ Phase 8.2: API Router 實作
- 業務邏輯委託給 Service 層
"""
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from src.services.auto_repair_service import (
@@ -197,3 +197,97 @@ async def get_auto_repair_stats() -> dict:
"overall_success_rate": total_success / total_executions if total_executions > 0 else 0.0,
"auto_repair_eligible": high_quality_count > 0,
}
# =============================================================================
# History Models & Endpoint
# 2026-04-06 Claude Code: Sprint 3 T_frontend — 修復歷史記錄 API
# =============================================================================
class RepairHistoryItem(BaseModel):
"""修復歷史記錄項目"""
id: str
incident_id: str
playbook_id: str
playbook_name: str
action_type: str # "kubectl" | "ssh_command" | "manual"
uri_scheme: str # "kubectl://" | "openclaw://" | "ansible://"
command: str
status: str # "success" | "failed" | "pending_approval" | "running"
executed_at: str
duration_ms: int | None = None
error: str | None = None
rag_confidence: float | None = None
class RepairHistoryResponse(BaseModel):
"""修復歷史記錄回應"""
count: int
items: list[RepairHistoryItem]
@router.get("/history", response_model=RepairHistoryResponse)
async def get_repair_history(
limit: int = Query(20, ge=1, le=100),
) -> RepairHistoryResponse:
"""
取得修復歷史記錄。
從 incidents (working memory) 推導,回傳有 auto_repair 活動的事件。
2026-04-06 Claude Code: Sprint 3 T_frontend
"""
try:
incident_service = get_incident_service()
all_incidents = await incident_service.get_active_incidents()
items: list[RepairHistoryItem] = []
for incident in all_incidents:
fs = incident.frequency_stats
if fs is None or fs.auto_repair_count == 0:
continue
# 從 frequency_stats 推導修復狀態
if fs.last_repair_success is True:
status = "success"
elif fs.last_repair_success is False:
status = "failed"
else:
status = "running"
action = fs.last_repair_action or "kubectl rollout restart"
# 推導 action_type 和 uri_scheme
if action.startswith("kubectl"):
action_type = "kubectl"
uri_scheme = "kubectl://"
elif action.startswith("ssh") or action.startswith("ansible"):
action_type = "ssh_command"
uri_scheme = "ansible://"
else:
action_type = "manual"
uri_scheme = "openclaw://"
items.append(RepairHistoryItem(
id=f"hist-{incident.incident_id}",
incident_id=incident.incident_id,
playbook_id="unknown",
playbook_name=action,
action_type=action_type,
uri_scheme=uri_scheme,
command=action,
status=status,
executed_at=incident.updated_at.isoformat(),
duration_ms=None,
error=None,
rag_confidence=None,
))
# 最多回傳 limit 筆newest first (updated_at 已是活躍事件,先按 ID 截斷)
items = items[:limit]
return RepairHistoryResponse(count=len(items), items=items)
except Exception:
# 任何錯誤都回傳空列表,不中斷前端
return RepairHistoryResponse(count=0, items=[])

View File

@@ -61,12 +61,15 @@ export default function NeuralCommandPage({ params }: { params: { locale: string
const [history, setHistory] = useState<RepairHistoryItem[]>([])
const [loading, setLoading] = useState(true)
const [lastRefresh, setLastRefresh] = useState<Date>(new Date())
const [pendingApprovals, setPendingApprovals] = useState(0)
const fetchData = useCallback(async () => {
try {
const [statsRes, pbRes] = await Promise.all([
const [statsRes, pbRes, histRes, approvalsRes] = await Promise.all([
fetch('/api/v1/auto-repair/stats'),
fetch('/api/v1/playbooks/'),
fetch('/api/v1/auto-repair/history?limit=20'),
fetch('/api/v1/approvals/pending'),
])
if (statsRes.ok) {
@@ -77,6 +80,14 @@ export default function NeuralCommandPage({ params }: { params: { locale: string
const data = await pbRes.json()
setPlaybooks(data.items?.map((i: { playbook: PlaybookItem }) => i.playbook) ?? [])
}
if (histRes.ok) {
const data = await histRes.json()
setHistory(data.items ?? [])
}
if (approvalsRes.ok) {
const data = await approvalsRes.json()
setPendingApprovals(data.count ?? 0)
}
setLastRefresh(new Date())
} catch {
@@ -93,7 +104,6 @@ export default function NeuralCommandPage({ params }: { params: { locale: string
}, [fetchData])
const approvedPlaybooks = playbooks.filter(p => p.status === 'approved')
const pendingApprovals = 0 // TODO: fetch from /api/v1/approvals
return (
<AppLayout locale={params.locale}>