/** * AWOOOI API Client * ADR-005: 所有請求經過 BFF * * 統帥鐵律: 禁止任何 Fallback IP,環境變數缺失即噴錯 */ // 絕對純化: 環境變數缺失時直接拋出致命錯誤,嚴禁任何 Fallback const getApiBaseUrl = (): string => { const url = process.env.NEXT_PUBLIC_API_URL if (!url) { const fatalMsg = '[AWOOOI FATAL] Missing NEXT_PUBLIC_API_URL configuration.' console.error(fatalMsg) if (typeof window !== 'undefined') { console.error('%c' + fatalMsg, 'color: #ef4444; font-weight: bold; font-size: 16px;') } throw new Error(fatalMsg) } return url.endsWith('/api/v1') ? url : `${url}/api/v1` } const API_BASE_URL = getApiBaseUrl() export class ApiError extends Error { constructor( public status: number, public code: string, message: string ) { super(message) this.name = 'ApiError' } } async function handleResponse(response: Response): Promise { if (!response.ok) { const error = await response.json().catch(() => ({})) throw new ApiError( response.status, error.code || 'UNKNOWN_ERROR', error.message || response.statusText ) } return response.json() } export const apiClient = { // Health async getHealth() { const res = await fetch(`${API_BASE_URL}/health`) return handleResponse<{ status: 'healthy' | 'degraded' | 'unhealthy' version: string timestamp: string components: Record }>(res) }, // Agent async getAgentStatus() { const res = await fetch(`${API_BASE_URL}/agent/status`) return handleResponse<{ status: 'idle' | 'thinking' | 'executing' | 'waiting_approval' active_conversations: number current_task: string | null last_activity: string | null }>(res) }, async chat(message: string, conversationId?: string) { const res = await fetch(`${API_BASE_URL}/agent/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, conversation_id: conversationId }), }) return handleResponse<{ message: string conversation_id: string requires_approval: boolean approval_id?: string }>(res) }, // Plugins async listPlugins(category?: string) { const params = category ? `?category=${category}` : '' const res = await fetch(`${API_BASE_URL}/plugins${params}`) return handleResponse>(res) }, // Approvals async listApprovals(status?: string) { const params = status ? `?status=${status}` : '' const res = await fetch(`${API_BASE_URL}/approvals${params}`) return handleResponse<{ items: Array<{ id: string type: string status: string action: { plugin_id: string operation: string risk_level: string } requested_at: string }> }>(res) }, async signApproval(approvalId: string, signer: string = 'commander', comment?: string) { const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/sign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ signer_id: signer, signer_name: signer, comment: comment, }), }) // 🔧 Fix: 回傳型別與後端實際結構對齊 return handleResponse<{ success: boolean message: string approval: ApprovalResponse execution_triggered: boolean // 向下相容舊欄位 (deprecated) approval_id?: string status?: string current_signatures?: number required_signatures?: number }>(res) }, async rejectApproval(approvalId: string, reason?: string) { const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rejector_id: 'commander', rejector_name: 'Commander', reason: reason || 'Rejected via WarRoom', }), }) return handleResponse<{ id: string; status: string }>(res) }, // ========================================================================= // Phase 7: Incidents API (真實血脈) // ========================================================================= async listIncidents() { const res = await fetch(`${API_BASE_URL}/incidents`) return handleResponse(res) }, async getIncident(incidentId: string) { const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}`) return handleResponse(res) }, async generateProposal(incidentId: string) { const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}/proposal`, { method: 'POST', }) return handleResponse(res) }, // ========================================================================= // Phase 7: Pending Approvals API (真實血脈) // ========================================================================= async getPendingApprovals() { const res = await fetch(`${API_BASE_URL}/approvals/pending`) return handleResponse(res) }, // ========================================================================= // Phase 10: Sentry Errors API (#40 BFF) // ========================================================================= async getErrorStats() { const res = await fetch(`${API_BASE_URL}/errors/stats`) return handleResponse(res) }, async listErrors(params?: { status?: string; level?: string; limit?: number }) { const searchParams = new URLSearchParams() if (params?.status) searchParams.set('status', params.status) if (params?.level) searchParams.set('level', params.level) if (params?.limit) searchParams.set('limit', params.limit.toString()) const query = searchParams.toString() ? `?${searchParams.toString()}` : '' const res = await fetch(`${API_BASE_URL}/errors/issues${query}`) return handleResponse(res) }, async getErrorDetail(issueId: string) { const res = await fetch(`${API_BASE_URL}/errors/issues/${issueId}`) return handleResponse(res) }, async getErrorTrends(period: '24h' | '7d' | '30d' = '24h') { const res = await fetch(`${API_BASE_URL}/errors/trends?period=${period}`) return handleResponse(res) }, async analyzeError(issueId: string) { const res = await fetch(`${API_BASE_URL}/errors/issues/${issueId}/analyze`, { method: 'POST', }) return handleResponse(res) }, } // ========================================================================= // Type Definitions (Phase 7) // ========================================================================= /** * Phase 6.5: 決策令牌資訊 * 確保 UI 永遠有決策可操作 */ export interface DecisionInfo { token: string state: 'init' | 'analyzing' | 'ready' | 'executing' | 'completed' | 'error' proposal_data: { action: string description: string reasoning: string risk_level: 'low' | 'medium' | 'critical' kubectl_command: string source: string confidence: number } | null proposal_id: string | null } export interface IncidentResponse { incident_id: string status: 'investigating' | 'mitigating' | 'resolved' | 'closed' severity: 'P0' | 'P1' | 'P2' | 'P3' signal_count: number affected_services: string[] proposal_count: number created_at: string updated_at: string /** Phase 6.5: 決策令牌 (確保 UI 永不鎖死) */ decision: DecisionInfo | null } export interface IncidentListResponse { count: number incidents: IncidentResponse[] } export interface BlastRadius { affected_pods: number estimated_downtime: string related_services: string[] data_impact: 'none' | 'read_only' | 'write' | 'destructive' } export interface DryRunCheck { name: string passed: boolean message: string } export interface ApprovalResponse { id: string action: string description: string status: 'pending' | 'approved' | 'rejected' | 'expired' risk_level: 'low' | 'medium' | 'high' | 'critical' blast_radius: BlastRadius dry_run_checks: DryRunCheck[] required_signatures: number current_signatures: number signatures: Array<{ signer: string; signed_at: string }> requested_by: string created_at: string expires_at: string | null } export interface PendingApprovalsResponse { count: number approvals: ApprovalResponse[] } export interface ProposalGenerateResponse { success: boolean message: string incident_id: string proposal: ApprovalResponse | null incident_status: string | null } // ========================================================================= // Phase 10: Sentry Error Types (#40 BFF) // ========================================================================= export interface SentryIssue { id: string short_id: string title: string culprit: string | null level: 'error' | 'warning' | 'info' | 'fatal' status: 'unresolved' | 'resolved' | 'ignored' count: number user_count: number first_seen: string last_seen: string permalink: string | null } export interface ErrorStatsResponse { total_issues: number unresolved_issues: number error_count_24h: number critical_count: number projects: string[] } export interface ErrorListResponse { issues: SentryIssue[] total: number has_more: boolean } export interface ErrorDetailResponse { issue: Record latest_event: Record | null sentry_url: string } export interface ErrorTrendPoint { timestamp: string count: number } export interface ErrorTrendResponse { period: '24h' | '7d' | '30d' data: ErrorTrendPoint[] total_count: number change_percent: number } export interface FixRecommendation { summary: string steps: string[] code_suggestion: string | null } export interface PreventionMeasure { type: string description: string } export interface ErrorAnalysis { root_cause: string category: string severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' impact_assessment: string fix_recommendation: FixRecommendation prevention: PreventionMeasure[] related_files: string[] confidence: number reasoning: string } export interface ErrorAnalysisResponse { status: 'completed' | 'failed' issue_id: string provider: string analysis?: ErrorAnalysis analyzed_at?: string sentry_url: string message?: string }