Files
awoooi/apps/web/src/lib/api-client.ts
OG T 5172c4c925 feat(web): Error Dashboard + GenUI + OmniTerminal 組件
新增功能:
- errors/: Sentry 錯誤儀表板 (overview-card, trend-chart, issues-list)
- genui/ApprovalCard: GenUI 風格簽核卡片
- terminal/OmniTerminal: AI 終端機組件
- useErrors.ts: 錯誤數據 hooks
- terminal.store.ts: 終端機狀態管理

更新:
- conversational-view.tsx: 改進對話式 UI
- providers.tsx: 新增 provider
- sidebar.tsx: 新增 Errors 導航
- api-client.ts: 錯誤 API 整合
- i18n messages: 新增錯誤相關翻譯

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-26 19:09:36 +08:00

386 lines
10 KiB
TypeScript
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.
/**
* 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<T>(response: Response): Promise<T> {
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<string, 'up' | 'down'>
}>(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<Array<{
id: string
name: string
version: string
category: string
enabled: boolean
description?: string
}>>(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<IncidentListResponse>(res)
},
async getIncident(incidentId: string) {
const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}`)
return handleResponse<IncidentResponse>(res)
},
async generateProposal(incidentId: string) {
const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}/proposal`, {
method: 'POST',
})
return handleResponse<ProposalGenerateResponse>(res)
},
// =========================================================================
// Phase 7: Pending Approvals API (真實血脈)
// =========================================================================
async getPendingApprovals() {
const res = await fetch(`${API_BASE_URL}/approvals/pending`)
return handleResponse<PendingApprovalsResponse>(res)
},
// =========================================================================
// Phase 10: Sentry Errors API (#40 BFF)
// =========================================================================
async getErrorStats() {
const res = await fetch(`${API_BASE_URL}/errors/stats`)
return handleResponse<ErrorStatsResponse>(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<ErrorListResponse>(res)
},
async getErrorDetail(issueId: string) {
const res = await fetch(`${API_BASE_URL}/errors/issues/${issueId}`)
return handleResponse<ErrorDetailResponse>(res)
},
async getErrorTrends(period: '24h' | '7d' | '30d' = '24h') {
const res = await fetch(`${API_BASE_URL}/errors/trends?period=${period}`)
return handleResponse<ErrorTrendResponse>(res)
},
async analyzeError(issueId: string) {
const res = await fetch(`${API_BASE_URL}/errors/issues/${issueId}/analyze`, {
method: 'POST',
})
return handleResponse<ErrorAnalysisResponse>(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<string, unknown>
latest_event: Record<string, unknown> | 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
}