'use client' /** * HITLSection - 人機協作審批區域 * ================================ * Phase 1: AI 思考 → 動態卡片對接 * * Features: * - OpenClaw AI 思考流視覺化 * - 動態 Approval Card 渲染 (100% 後端資料) * - Slide Up 動畫過渡 * - **Phase 15: SSE 即時更新** (取代輪詢) */ import { useState, useCallback } from 'react' import { useTranslations } from 'next-intl' import { cn } from '@/lib/utils' import { Z_INDEX } from '@/lib/constants/z-index' import { OpenClawPanel, type OpenClawStatus } from './openclaw-panel' import { ApprovalCard, type RiskLevel } from '@/components/approval/approval-card' import { useApprovalStore, usePendingApprovals, toFrontendApproval, } from '@/stores/approval.store' import { useApprovalSSE } from '@/hooks/useApprovalSSE' import { useTimelineStore } from '@/stores/timeline.store' import { ActionTimeline } from '@/components/timeline' import { GlassCard, GlassCardTitle } from '@/components/ui/glass-card' import { ShieldCheck, Plus, Loader2, Lock, ShieldX, AlertTriangle } from 'lucide-react' // ============================================================================= // API Configuration (統帥鐵律: 禁止任何 Fallback IP) // ============================================================================= const getApiBaseUrl = (): string => { if (typeof window === 'undefined') return '' const url = process.env.NEXT_PUBLIC_API_URL if (!url) { console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL') return '' } return url } const API_BASE_URL = getApiBaseUrl() // ============================================================================= // Phase 3: RBAC Permission System (企業護城河) // ============================================================================= type UserRole = 'viewer' | 'developer' | 'devops' | 'admin' | 'cto' | 'ciso' | 'ceo' const REQUIRED_ROLE_FOR_RISK: Record = { low: ['developer', 'devops', 'admin', 'cto', 'ciso', 'ceo'], medium: ['devops', 'admin', 'cto', 'ciso', 'ceo'], high: ['admin', 'cto', 'ciso', 'ceo'], critical: ['cto', 'ciso', 'ceo'], // Critical 需要 CTO/CISO 等級 } function canSignApproval(userRole: UserRole, riskLevel: RiskLevel): boolean { const allowedRoles = REQUIRED_ROLE_FOR_RISK[riskLevel] return allowedRoles.includes(userRole) } function getRequiredRolesDisplay(riskLevel: RiskLevel): string { const roles = REQUIRED_ROLE_FOR_RISK[riskLevel] return roles.map((r) => r.toUpperCase()).join(' / ') } // ============================================================================= // Types // ============================================================================= interface HITLSectionProps { locale: string className?: string } // ============================================================================= // Component // ============================================================================= export function HITLSection({ locale: _locale, className }: HITLSectionProps) { const t = useTranslations('demo') const tApproval = useTranslations('approval') // Store const { fetchPending, signApproval, rejectApproval } = useApprovalStore() const pendingApprovals = usePendingApprovals() const addTimelineEvent = useTimelineStore((state) => state.addEvent) // Phase 15: SSE 即時更新 (取代輪詢) useApprovalSSE({ autoConnect: true }) // AI thinking state const [openclawStatus, setClawbotStatus] = useState('patrolling') const [currentAlertType, setCurrentAlertType] = useState() const [showCards, setShowCards] = useState(true) const [isSimulating, setIsSimulating] = useState(false) // Phase 3: 模擬當前登入者角色 (可切換測試權限) const [currentUserRole, setCurrentUserRole] = useState('devops') const currentUserName = currentUserRole === 'cto' ? 'CTO Admin' : 'Demo DevOps' // Phase 3: Access Denied 模態框狀態 const [accessDeniedModal, setAccessDeniedModal] = useState<{ show: boolean riskLevel: RiskLevel requiredRoles: string } | null>(null) // Simulate webhook alert (triggers AI thinking flow) const simulateAlert = useCallback(async (alertType: string, severity: 'critical' | 'warning') => { setIsSimulating(true) setShowCards(false) setCurrentAlertType(alertType) // Phase 1: Intercepting setClawbotStatus('intercepting') await new Promise((r) => setTimeout(r, 800)) // Phase 2: Analyzing setClawbotStatus('analyzing') await new Promise((r) => setTimeout(r, 1200)) // Phase 3: Generating setClawbotStatus('generating') // Call actual webhook API try { const response = await fetch(`${API_BASE_URL}/api/v1/webhooks/alerts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alert_type: alertType, severity, source: 'demo-ui', target_resource: alertType === 'k8s_pod_crash' ? 'harbor-core-7d4b8c9f5-xk2m3' : 'nginx-ingress-abc123', namespace: 'default', message: `Demo alert: ${alertType}`, metrics: { cpu_percent: 95, sigma_deviation: 3.5 }, }), }) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } const result = await response.json() console.log('[HITL] Webhook response:', result) // Phase 4: Log SYSTEM alert received addTimelineEvent({ type: 'system', status: 'warning', title: `接收到 ${alertType.replace('_', ' ')} 告警`, riskLevel: severity === 'critical' ? 'critical' : 'medium', }) // Phase 4: Log AGENT analysis complete addTimelineEvent({ type: 'agent', status: 'info', title: `OpenClaw 分析完成,提出 ${severity.toUpperCase()} 提案`, actor: 'OpenClaw', actorRole: 'ai', riskLevel: severity === 'critical' ? 'critical' : 'medium', approvalId: result.approval_id, }) // Phase 4: Complete setClawbotStatus('complete') await new Promise((r) => setTimeout(r, 800)) // Refresh approvals list await fetchPending() // Show cards with animation setShowCards(true) } catch (error) { console.error('[HITL] Webhook failed:', error) setClawbotStatus('patrolling') } finally { setIsSimulating(false) setTimeout(() => { setClawbotStatus('patrolling') setCurrentAlertType(undefined) }, 2000) } }, [fetchPending, addTimelineEvent]) // Handle approval sign with RBAC check (Phase 3) + Timeline Logging (Phase 4) const handleApprove = useCallback(async (id: string, riskLevel?: RiskLevel) => { // Phase 3: 權限檢查 const level = riskLevel || 'medium' if (!canSignApproval(currentUserRole, level)) { // 顯示 Access Denied 模態框 setAccessDeniedModal({ show: true, riskLevel: level, requiredRoles: getRequiredRolesDisplay(level), }) // Phase 4: Log SECURITY Access Denied (紅色微光) addTimelineEvent({ type: 'security', status: 'error', title: `${currentUserRole.toUpperCase()} 嘗試簽核,觸發 Access Denied`, description: `需要 ${getRequiredRolesDisplay(level)} 角色`, actor: currentUserName, actorRole: currentUserRole, riskLevel: level, approvalId: id, }) console.warn('[HITL] Access Denied:', { user: currentUserName, role: currentUserRole, riskLevel: level, requiredRoles: getRequiredRolesDisplay(level), }) return } // Phase 4: Log HUMAN signature (綠色微光) addTimelineEvent({ type: 'human', status: 'success', title: `${currentUserRole.toUpperCase()} 成功簽核批准`, actor: currentUserName, actorRole: currentUserRole, riskLevel: level, approvalId: id, }) const result = await signApproval(id, 'demo-user', currentUserName, 'Approved via demo UI') // Check if approval is now complete - Log EXEC if (result?.execution_triggered) { addTimelineEvent({ type: 'exec', status: 'success', title: '多重簽章完成,指令已執行', actor: 'OpenClaw', actorRole: 'ai', riskLevel: level, approvalId: id, }) } await fetchPending() }, [signApproval, fetchPending, currentUserRole, currentUserName, addTimelineEvent]) // Handle rejection const handleReject = useCallback(async (id: string) => { await rejectApproval(id, 'demo-user', currentUserName, 'Rejected via HITL Panel') await fetchPending() }, [rejectApproval, fetchPending, currentUserName]) return (
{/* Header - Lab-White Style */}

{t('hitlRealApi')}

Multi-Sig
{/* Trigger Buttons */}
{/* Main Content Grid */}
{/* OpenClaw Panel (Left) */}
{/* Approval Cards (Center) */}
{pendingApprovals.length === 0 ? ( {tApproval('noApprovals')}

[SYS] All clear. Awaiting alerts...

) : (
{pendingApprovals.map((approval) => { const frontendApproval = toFrontendApproval(approval) const hasPermission = canSignApproval(currentUserRole, frontendApproval.riskLevel) return (
handleApprove(id, frontendApproval.riskLevel)} onReject={handleReject} holdDuration={approval.risk_level === 'critical' ? 2000 : 1000} /> {/* Phase 3: Permission Warning Badge */} {!hasPermission && (
需要 {getRequiredRolesDisplay(frontendApproval.riskLevel)}
)}
) })}
)}
{/* Action Timeline (Right) - Phase 4 */}
{/* Phase 3 + 4: Current User Role Display with Switcher */}
{/* Current User */}
登入身份: {currentUserName} {currentUserRole}
{/* Role Switcher (Demo Only) */}
切換:
{/* Debug Info */}
Pending: {pendingApprovals.length} | Status: {openclawStatus}
{/* Phase 3: Access Denied Modal (Nothing.tech Style) */} {accessDeniedModal?.show && (
{/* Icon */}
{/* Title */}

ACCESS DENIED

{/* Risk Level Badge */}
{accessDeniedModal.riskLevel} RISK
{/* Message */}

此操作需要更高權限簽核

您的角色: {currentUserRole.toUpperCase()}

{/* Required Roles */}

需要以下角色之一

{accessDeniedModal.requiredRoles.split(' / ').map((role) => ( {role} ))}
{/* Action Button */}
)}
) } export default HITLSection