/** * useAutomationEvents - WebSocket Event Hook for Automation System * * Subscribes to hand and workflow events from ZCLAW WebSocket * and updates the corresponding stores. * * @module hooks/useAutomationEvents */ import { useEffect, useRef } from 'react'; import { useHandStore } from '../store/handStore'; import { useWorkflowStore } from '../store/workflowStore'; import { useChatStore } from '../store/chatStore'; import type { GatewayClient } from '../lib/gateway-client'; import { speechSynth } from '../lib/speech-synth'; import { createLogger } from '../lib/logger'; const log = createLogger('useAutomationEvents'); // === Event Types === interface HandEventData { hand_name: string; hand_status: 'triggered' | 'running' | 'completed' | 'failed' | 'needs_approval'; hand_result?: unknown; hand_error?: string; run_id?: string; timestamp?: number; } interface WorkflowEventData { workflow_id: string; workflow_status: 'started' | 'step_completed' | 'completed' | 'failed' | 'paused'; current_step?: number; total_steps?: number; step_name?: string; result?: unknown; error?: string; run_id?: string; timestamp?: number; } interface ApprovalEventData { approval_id: string; hand_name?: string; workflow_id?: string; run_id?: string; status: 'requested' | 'approved' | 'rejected' | 'expired'; reason?: string; requested_by?: string; timestamp?: number; } // === Hook Options === export interface UseAutomationEventsOptions { /** Whether to inject hand results into chat as messages */ injectResultsToChat?: boolean; /** Whether to auto-refresh hands on status change */ refreshOnStatusChange?: boolean; /** Custom event handlers */ onHandEvent?: (data: HandEventData) => void; onWorkflowEvent?: (data: WorkflowEventData) => void; onApprovalEvent?: (data: ApprovalEventData) => void; } // === Helper Functions === function isHandEvent(data: unknown): data is HandEventData { return typeof data === 'object' && data !== null && 'hand_name' in data && 'hand_status' in data; } function isWorkflowEvent(data: unknown): data is WorkflowEventData { return typeof data === 'object' && data !== null && 'workflow_id' in data && 'workflow_status' in data; } function isApprovalEvent(data: unknown): data is ApprovalEventData { return typeof data === 'object' && data !== null && 'approval_id' in data && 'status' in data; } // === Main Hook === /** * Hook for subscribing to automation-related WebSocket events. * * @param client - The GatewayClient instance (optional, will try to get from store if not provided) * @param options - Configuration options * * @example * ```tsx * function AutomationPanel() { * const client = useConnectionStore(s => s.client); * useAutomationEvents(client, { * injectResultsToChat: true, * refreshOnStatusChange: true, * }); * // ... * } * ``` */ export function useAutomationEvents( client: GatewayClient | null, options: UseAutomationEventsOptions = {} ): void { const { injectResultsToChat = true, refreshOnStatusChange = true, onHandEvent, onWorkflowEvent, onApprovalEvent, } = options; // Store references const loadHands = useHandStore(s => s.loadHands); const loadHandRuns = useHandStore(s => s.loadHandRuns); const loadApprovals = useHandStore(s => s.loadApprovals); const loadWorkflows = useWorkflowStore(s => s.loadWorkflows); const loadWorkflowRuns = useWorkflowStore(s => s.loadWorkflowRuns); const addMessage = useChatStore(s => s.addMessage); // Track subscriptions for cleanup const unsubscribersRef = useRef void>>([]); useEffect(() => { if (!client) { return; } // Clean up any existing subscriptions unsubscribersRef.current.forEach(unsub => unsub()); unsubscribersRef.current = []; // === Hand Event Handler === const handleHandEvent = (data: unknown) => { if (!isHandEvent(data)) return; const eventData = data as HandEventData; log.debug('Hand event:', eventData); // Refresh hands if status changed if (refreshOnStatusChange) { loadHands(); } // Load updated runs for this hand if (eventData.run_id) { loadHandRuns(eventData.hand_name); } // Inject result into chat if (injectResultsToChat && eventData.hand_status === 'completed') { const resultContent = eventData.hand_result ? typeof eventData.hand_result === 'string' ? eventData.hand_result : JSON.stringify(eventData.hand_result, null, 2) : 'Hand completed successfully'; addMessage({ id: `hand-${eventData.run_id || Date.now()}`, role: 'hand', content: `**${eventData.hand_name}** 执行完成\n\n${resultContent}`, timestamp: new Date(), handName: eventData.hand_name, handStatus: eventData.hand_status, handResult: eventData.hand_result, runId: eventData.run_id, }); // Trigger browser TTS for SpeechHand results if (eventData.hand_name === 'speech' && eventData.hand_result && typeof eventData.hand_result === 'object') { const res = eventData.hand_result as Record; if (res.tts_method === 'browser' && typeof res.text === 'string' && res.text) { speechSynth.speak({ text: res.text, voice: typeof res.voice === 'string' ? res.voice : undefined, language: typeof res.language === 'string' ? res.language : undefined, rate: typeof res.rate === 'number' ? res.rate : undefined, pitch: typeof res.pitch === 'number' ? res.pitch : undefined, volume: typeof res.volume === 'number' ? res.volume : undefined, }).catch((err: unknown) => { log.warn('Browser TTS failed:', err); }); } } } // Handle error status if (eventData.hand_status === 'failed' && eventData.hand_error) { addMessage({ id: `hand-error-${eventData.run_id || Date.now()}`, role: 'hand', content: `**${eventData.hand_name}** 执行失败\n\n错误: ${eventData.hand_error}`, timestamp: new Date(), handName: eventData.hand_name, handStatus: eventData.hand_status, error: eventData.hand_error, runId: eventData.run_id, }); } // Handle approval needed if (eventData.hand_status === 'needs_approval') { loadApprovals('pending'); } // Call custom handler onHandEvent?.(eventData); }; // === Workflow Event Handler === const handleWorkflowEvent = (data: unknown) => { if (!isWorkflowEvent(data)) return; const eventData = data as WorkflowEventData; log.debug('Workflow event:', eventData); // Refresh workflows if status changed if (refreshOnStatusChange) { loadWorkflows(); } // Load updated runs for this workflow if (eventData.run_id) { loadWorkflowRuns(eventData.workflow_id); } // Inject result into chat if (injectResultsToChat && eventData.workflow_status === 'completed') { const resultContent = eventData.result ? typeof eventData.result === 'string' ? eventData.result : JSON.stringify(eventData.result, null, 2) : 'Workflow completed successfully'; addMessage({ id: `workflow-${eventData.run_id || Date.now()}`, role: 'workflow', content: `**工作流: ${eventData.workflow_id}** 执行完成\n\n${resultContent}`, timestamp: new Date(), workflowId: eventData.workflow_id, workflowStatus: eventData.workflow_status, workflowResult: eventData.result, runId: eventData.run_id, }); } // Call custom handler onWorkflowEvent?.(eventData); }; // === Approval Event Handler === const handleApprovalEvent = (data: unknown) => { if (!isApprovalEvent(data)) return; const eventData = data as ApprovalEventData; log.debug('Approval event:', eventData); // Refresh approvals list loadApprovals(); // Call custom handler onApprovalEvent?.(eventData); }; // Subscribe to events const unsubHand = client.on('hand', handleHandEvent); const unsubWorkflow = client.on('workflow', handleWorkflowEvent); const unsubApproval = client.on('approval', handleApprovalEvent); unsubscribersRef.current = [unsubHand, unsubWorkflow, unsubApproval]; // Cleanup on unmount or client change return () => { unsubscribersRef.current.forEach(unsub => unsub()); unsubscribersRef.current = []; }; }, [ client, injectResultsToChat, refreshOnStatusChange, loadHands, loadHandRuns, loadApprovals, loadWorkflows, loadWorkflowRuns, addMessage, onHandEvent, onWorkflowEvent, onApprovalEvent, ]); } // === Utility Hooks === /** * Hook for subscribing to a specific hand's events only */ export function useHandEvents( client: GatewayClient | null, handName: string, onEvent?: (data: HandEventData) => void ): void { useEffect(() => { if (!client || !handName) return; const handler = (data: unknown) => { if (isHandEvent(data) && (data as HandEventData).hand_name === handName) { onEvent?.(data as HandEventData); } }; const unsub = client.on('hand', handler); return unsub; }, [client, handName, onEvent]); } /** * Hook for subscribing to a specific workflow's events only */ export function useWorkflowEvents( client: GatewayClient | null, workflowId: string, onEvent?: (data: WorkflowEventData) => void ): void { useEffect(() => { if (!client || !workflowId) return; const handler = (data: unknown) => { if (isWorkflowEvent(data) && (data as WorkflowEventData).workflow_id === workflowId) { onEvent?.(data as WorkflowEventData); } }; const unsub = client.on('workflow', handler); return unsub; }, [client, workflowId, onEvent]); } export default useAutomationEvents;