feat(automation): implement unified automation system with Hands and Workflows
Phase 1 - Core Fixes: - Fix parameter passing in HandsPanel (params now passed to triggerHand) - Migrate HandsPanel from useGatewayStore to useHandStore - Add type adapters and category mapping for 7 Hands - Create useAutomationEvents hook for WebSocket event handling Phase 2 - UI Components: - Create AutomationPanel as unified entry point - Create AutomationCard with grid/list view support - Create AutomationFilters with category tabs and search - Create BatchActionBar for batch operations Phase 3 - Advanced Features: - Create ScheduleEditor with visual scheduling (no cron syntax) - Support frequency: once, daily, weekly, monthly, custom - Add timezone selection and end date options Technical Details: - AutomationItem type unifies Hand and Workflow - CategoryType: research, data, automation, communication, content, productivity - ScheduleInfo interface for scheduling configuration - WebSocket events: hand, workflow, approval Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
318
desktop/src/hooks/useAutomationEvents.ts
Normal file
318
desktop/src/hooks/useAutomationEvents.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* useAutomationEvents - WebSocket Event Hook for Automation System
|
||||
*
|
||||
* Subscribes to hand and workflow events from OpenFang 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';
|
||||
|
||||
// === 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<Array<() => 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;
|
||||
console.log('[useAutomationEvents] 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,
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
console.log('[useAutomationEvents] 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;
|
||||
console.log('[useAutomationEvents] 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;
|
||||
Reference in New Issue
Block a user