Files
awoooi/apps/web/src/lib/api-client.ts
Your Name 6ccdf199ad
All checks were successful
CD Pipeline / tests (push) Successful in 1m23s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 4m18s
CD Pipeline / post-deploy-checks (push) Successful in 1m44s
chore(web): 清理 IwoooS D2 註解語氣
2026-06-05 01:11:44 +08:00

862 lines
24 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環境變數缺失即噴錯
*/
import { CURRENT_USER } from '@/lib/constants/user'
// 絕對純化: 環境變數缺失時直接拋出致命錯誤,嚴禁任何 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, {
status: 'up' | 'down' | 'degraded'
latency_ms?: number | null
error?: string | null
}>
ollama_route_order?: string[]
}>(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 = CURRENT_USER.id, comment?: string, csrfToken?: string | null) {
// Phase 22 P0: 加入 CSRF token + credentials (2026-03-31 Claude Code)
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (csrfToken) headers['X-CSRF-Token'] = csrfToken
const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/sign`, {
method: 'POST',
headers,
credentials: 'include',
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, csrfToken?: string | null) {
// Phase 22 P0: 加入 CSRF token + credentials (2026-03-31 Claude Code)
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (csrfToken) headers['X-CSRF-Token'] = csrfToken
const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/reject`, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify({
rejector_id: CURRENT_USER.id,
rejector_name: CURRENT_USER.name,
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 getIncidentTimeline(incidentId: string) {
const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}/timeline`)
return handleResponse<IncidentTimelineResponse>(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)
},
// =========================================================================
// Phase 19: UX Audit / Session Replay (#126)
// 2026-03-31 Claude Code - Frontend Replay UI Integration
// =========================================================================
async getUXAudit() {
const res = await fetch(`${API_BASE_URL}/errors/ux-audit`)
return handleResponse<UXAuditResponse>(res)
},
async getAgentMarketGovernanceSnapshot() {
const res = await fetch(`${API_BASE_URL}/agents/market-governance-snapshot`)
return handleResponse<AgentMarketGovernanceSnapshot>(res)
},
async getAiAgentAutomationInventorySnapshot() {
const res = await fetch(`${API_BASE_URL}/agents/automation-inventory-snapshot`)
return handleResponse<AiAgentAutomationInventorySnapshot>(res)
},
async getAiAgentAutomationBacklogSnapshot() {
const res = await fetch(`${API_BASE_URL}/agents/automation-backlog-snapshot`)
return handleResponse<AiAgentAutomationBacklogSnapshot>(res)
},
async getBackupDrTargetInventory() {
const res = await fetch(`${API_BASE_URL}/agents/backup-dr-target-inventory`)
return handleResponse<BackupDrTargetInventorySnapshot>(res)
},
async getBackupDrReadinessMatrix() {
const res = await fetch(`${API_BASE_URL}/agents/backup-dr-readiness-matrix`)
return handleResponse<BackupDrReadinessMatrixSnapshot>(res)
},
async getBackupNotificationPolicy() {
const res = await fetch(`${API_BASE_URL}/agents/backup-notification-policy`)
return handleResponse<BackupNotificationPolicySnapshot>(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 IncidentTimelineEvent {
stage: string
status: string
title: string
description: string | null
actor: string | null
timestamp: string | null
source_table: string | null
data: Record<string, unknown>
}
export interface IncidentTimelineStage extends IncidentTimelineEvent {
label: string
events: IncidentTimelineEvent[]
}
export interface IncidentTimelineResponse {
incident_id: string
title: string
status: string
severity: string
started_at: string | null
updated_at: string | null
resolved_at: string | null
affected_services: string[]
approval_ids: string[]
timeline: IncidentTimelineStage[]
events: IncidentTimelineEvent[]
ascii_timeline: string
}
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
}
// =========================================================================
// Phase 19: UX Audit / Session Replay Types (#126)
// 2026-03-31 Claude Code - Frontend Replay UI Integration
// =========================================================================
export interface UXAuditDetail {
type: 'replay_with_errors' | 'ui_error'
replay_id?: string
issue_id?: string
url: string
error_count?: number
title?: string
count?: number
urls?: string[]
}
export interface UXAuditResponse {
replays_with_errors: number
rage_clicks: number
dead_clicks: number
ui_errors: number
health_score: 'good' | 'moderate' | 'poor'
details: UXAuditDetail[]
replay_dashboard_url: string
}
// =========================================================================
// Agent Market Governance Snapshot
// =========================================================================
export interface AgentMarketGovernanceSnapshot {
schema_version: 'agent_market_governance_snapshot_v1'
generated_at: string
current_decision: string
policy: Record<string, boolean>
evaluation_cadence: {
workflow: string
schedule: string
timezone: 'Asia/Taipei'
next_scheduled_run_at: string
trigger_modes: string[]
primary_source_policy: string
operator_review_gate: string
}
market_watch_health: {
status: 'healthy' | 'blocked'
freshness_sla_hours: 168
stale_grace_hours: 6
stale_after: string
source_failures_block_priority_upgrade: boolean
blocked_from_integration: number
operator_blockers: string[]
}
summary: {
candidate_count: number
source_count: number
source_failures: number
changed_candidates: number
integration_queue_count: number
blocked_from_integration: number
watch_only_candidates_reviewed: number
eligible_for_market_scorecard_prescreen: number
recommended_watch_additions_remaining: number
priority_upgrades_approved: number
market_scorecard_updates_approved: number
replay_candidates_approved: number
sdk_installations_approved: number
paid_api_calls_approved: number
production_changes_approved: number
shadow_or_canary_approved: number
replacement_decisions_approved: number
}
candidate_groups: {
production_baseline: string[]
replay_or_integration_blocked: string[]
watch_only_candidates: string[]
watch_only_scorecard_prescreen_ready: string[]
}
candidate_statuses: Array<{
candidate_id: string
display_name: string
role: string
evaluation_priority: string
gate_status:
| 'production_baseline'
| 'integration_blocked'
| 'integration_reviewed'
| 'watch_only_prescreen_ready'
| 'watch_only_blocked'
| 'watch_only_monitoring'
| 'registered_no_review'
current_gate: string
required_next_gate: string
integration_decision: string
score: number | null
evidence: {
latest_replay_summary: string | null
latest_smoke_gate: string | null
latest_smoke_matrix: string | null
latest_smoke_model: string | null
}
approvals: {
replay: false
sdk_install: false
paid_api: false
shadow_or_canary: false
production_routing: false
}
operator_blockers: string[]
}>
operator_decision_queue: Array<{
candidate_id: string
display_name: string
priority: number
queue_status:
| 'baseline_protected'
| 'blocked_needs_evidence'
| 'operator_review_required'
| 'operator_priority_review'
| 'watch_only_blocked'
| 'watch_only_monitoring'
| 'registered_no_review'
recommended_action: string
approval_boundary: {
replacement_adr_required: boolean
priority_upgrade_required: boolean
market_scorecard_update_required: boolean
replay_approval_required: boolean
sdk_install_approval_required: boolean
paid_api_approval_required: boolean
shadow_or_canary_approval_required: boolean
production_routing_approval_required: boolean
}
risk_notes: string[]
evidence_refs: string[]
}>
next_allowed_actions: string[]
forbidden_actions_without_new_approval: string[]
}
// =========================================================================
// AI Agent Automation Inventory Snapshot
// =========================================================================
export interface AiAgentAutomationInventorySnapshot {
schema_version: 'ai_agent_automation_inventory_snapshot_v1'
generated_at: string
program_status: {
overall_completion_percent: number
current_priority: 'P0' | 'P1' | 'P2' | 'P3'
current_task_id: string
next_task_id: string
read_only_mode: true
}
status_taxonomy: {
task_statuses: string[]
gate_statuses: string[]
priorities: Array<'P0' | 'P1' | 'P2' | 'P3'>
}
agent_roles: Array<{
agent_id: string
display_name: string
primary_role: string
allowed_actions: string[]
blocked_actions: string[]
}>
asset_domains: Array<{
domain_id: string
display_name: string
description: string
}>
assets: Array<{
asset_id: string
domain_id: string
display_name: string
asset_type: string
status: string
gate_status: string
owner_agent: string
risk_level: 'low' | 'medium' | 'high' | 'critical'
evidence_refs: string[]
next_action: string
}>
workstreams: Array<{
workstream_id: string
display_name: string
completion_percent: number
status: string
next_task_id: string
}>
tasks: Array<{
task_id: string
priority: 'P0' | 'P1' | 'P2' | 'P3'
status: string
completion_percent: number
owner_agent: string
title: string
output: string
gate_status: string
next_action: string
}>
evidence: Array<{
evidence_id: string
kind: 'schema' | 'test' | 'browser' | 'api' | 'build' | 'doc' | 'runtime'
ref: string
result: string
}>
approval_boundaries: Record<
| 'sdk_installation_allowed'
| 'paid_api_call_allowed'
| 'shadow_or_canary_allowed'
| 'production_routing_allowed'
| 'destructive_operation_allowed',
false
>
}
export interface AiAgentAutomationBacklogSnapshot {
schema_version: 'ai_agent_automation_backlog_v1'
generated_at: string
source_inventory_snapshot_ref: string
program_status: {
overall_completion_percent: number
current_priority: 'P0' | 'P1' | 'P2' | 'P3'
current_task_id: string
next_task_id: string
read_only_mode: true
}
rollups: {
total_items: number
by_priority: Record<string, number>
by_status: Record<string, number>
by_gate_status: Record<string, number>
by_owner_agent: Record<string, number>
}
backlog_items: Array<{
item_id: string
priority: 'P0' | 'P1' | 'P2' | 'P3'
status: string
workstream_id: string
source_asset_id: string
source_signal_kind: string
title: string
owner_agent: string
recommended_action: string
action_class: string
gate_status: string
risk_level: 'low' | 'medium' | 'high' | 'critical'
evidence_refs: string[]
acceptance_criteria: string[]
next_review: string
}>
approval_boundaries: Record<
| 'sdk_installation_allowed'
| 'paid_api_call_allowed'
| 'shadow_or_canary_allowed'
| 'production_routing_allowed'
| 'destructive_operation_allowed',
false
>
}
export interface BackupDrTargetInventorySnapshot {
schema_version: 'backup_dr_target_inventory_v1'
generated_at: string
source_refs: string[]
program_status: {
overall_completion_percent: number
current_priority: 'P0' | 'P1' | 'P2' | 'P3'
current_task_id: string
next_task_id: string
read_only_mode: true
}
rollups: {
total_targets: number
by_status: Record<string, number>
by_target_type: Record<string, number>
by_gate_status: Record<string, number>
blocked_target_ids: string[]
}
backup_targets: Array<{
target_id: string
display_name: string
target_type: string
status: string
risk_level: 'low' | 'medium' | 'high' | 'critical'
owner_host: string
primary_script: string
schedule: string
rpo: string
storage_class: string
storage_ref: string
offsite_policy: string
automation_gate_status: string
restore_gate_status: string
secret_policy: string
evidence_refs: string[]
next_action: string
}>
approval_boundaries: Record<string, false>
operation_boundaries: Record<string, boolean>
}
export interface BackupDrReadinessMatrixSnapshot {
schema_version: 'backup_dr_readiness_matrix_v1'
generated_at: string
source_target_inventory_ref: string
source_refs: string[]
program_status: {
overall_completion_percent: number
current_priority: 'P0' | 'P1' | 'P2' | 'P3'
current_task_id: string
next_task_id: string
read_only_mode: true
}
rollups: {
total_rows: number
by_overall_readiness: Record<string, number>
by_restore_drill_status: Record<string, number>
by_offsite_status: Record<string, number>
blocked_row_ids: string[]
action_required_row_ids: string[]
}
readiness_rows: Array<{
target_id: string
display_name: string
overall_readiness: string
freshness_status: string
integrity_status: string
restore_drill_status: string
offsite_status: string
notification_policy: string
gate_status: string
evidence_level: string
evidence_refs: string[]
blocker_summary: string
next_action: string
}>
approval_boundaries: Record<string, false>
operation_boundaries: Record<string, boolean>
}
export interface BackupNotificationPolicySnapshot {
schema_version: 'backup_notification_policy_v1'
generated_at: string
source_readiness_matrix_ref: string
source_refs: string[]
program_status: {
overall_completion_percent: number
current_priority: 'P0' | 'P1' | 'P2' | 'P3'
current_task_id: string
next_task_id: string
read_only_mode: true
}
rollups: {
total_rules: number
by_decision: Record<string, number>
immediate_escalation_rule_ids: string[]
suppressed_success_rule_ids: string[]
}
notification_channels: Array<{
channel_id: string
purpose: string
immediate_allowed: boolean
success_immediate_allowed: boolean
requires_operator_action: boolean
}>
policy_rules: Array<{
rule_id: string
event_kind: string
backup_state: string
severity: string
decision: string
channels: string[]
owner_agent: string
requires_incident: boolean
requires_approval_record: boolean
message_contract: string
evidence_refs: string[]
}>
daily_summary_contract: Record<string, unknown>
approval_boundaries: Record<string, false>
operation_boundaries: Record<string, boolean>
}