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:
iven
2026-03-18 16:32:18 +08:00
parent dfeb286591
commit 3a7631e035
11 changed files with 2321 additions and 40 deletions

View 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;