/** * Browser Hand State Management * * Zustand store for managing browser automation state, sessions, and execution. */ import { create } from 'zustand'; import { v4 as uuidv4 } from 'uuid'; import Browser, { createSession, closeSession, listSessions, } from '../lib/browser-client'; import { BUILTIN_TEMPLATES, validateTemplateParams, mergeParamsWithDefaults, type TaskTemplate, type ExecutionState, type BrowserSession, type BrowserLog, type RecentTask, type SessionOptions, type LogLevel, type SessionStatus, } from '../components/BrowserHand/templates'; // ============================================================================ // Store State Interface // ============================================================================ interface BrowserHandState { // Sessions sessions: BrowserSession[]; activeSessionId: string | null; // Execution execution: ExecutionState; // Logs logs: BrowserLog[]; maxLogs: number; // Templates templates: TaskTemplate[]; recentTasks: RecentTask[]; maxRecentTasks: number; // UI State isTemplateModalOpen: boolean; isLoading: boolean; error: string | null; } interface BrowserHandActions { // Session Management createSession: (options?: SessionOptions) => Promise; closeSession: (sessionId: string) => Promise; listSessions: () => Promise; setActiveSession: (sessionId: string | null) => void; // Template Execution executeTemplate: (templateId: string, params: Record) => Promise; executeScript: (script: string, args?: unknown[]) => Promise; // State Updates updateExecutionState: (state: Partial) => void; addLog: (log: Omit) => void; clearLogs: () => void; // Screenshot takeScreenshot: () => Promise; // UI Control openTemplateModal: () => void; closeTemplateModal: () => void; setLoading: (loading: boolean) => void; setError: (error: string | null) => void; clearError: () => void; // Recent Tasks addRecentTask: (task: Omit) => void; clearRecentTasks: () => void; } // ============================================================================ // Initial State // ============================================================================ const initialExecutionState: ExecutionState = { isRunning: false, currentAction: null, currentUrl: null, lastScreenshot: null, progress: 0, startTime: null, status: 'idle', error: null, }; const initialState: BrowserHandState = { sessions: [], activeSessionId: null, execution: initialExecutionState, logs: [], maxLogs: 100, templates: BUILTIN_TEMPLATES, recentTasks: [], maxRecentTasks: 50, isTemplateModalOpen: false, isLoading: false, error: null, }; // ============================================================================ // Store Implementation // ============================================================================ export const useBrowserHandStore = create((set, get) => ({ // State ...initialState, // Session Management createSession: async (options?: SessionOptions) => { const store = get(); store.setLoading(true); store.clearError(); try { store.addLog({ level: 'info', message: '正在创建浏览器会话...' }); const result = await createSession({ webdriverUrl: options?.webdriverUrl, headless: options?.headless ?? true, browserType: options?.browserType ?? 'chrome', windowWidth: options?.windowWidth, windowHeight: options?.windowHeight, }); const sessionId = result.session_id; // Fetch session info const sessions = await listSessions(); const sessionInfo = sessions.find(s => s.id === sessionId); const newSession: BrowserSession = { id: sessionId, name: `Browser ${sessionId.substring(0, 8)}`, currentUrl: sessionInfo?.current_url ?? null, title: sessionInfo?.title ?? null, status: (sessionInfo?.status as SessionStatus) ?? 'connected', createdAt: sessionInfo?.created_at ?? new Date().toISOString(), lastActivity: sessionInfo?.last_activity ?? new Date().toISOString(), }; set((state) => ({ sessions: [...state.sessions, newSession], activeSessionId: sessionId, isLoading: false, })); store.addLog({ level: 'info', message: `会话已创建: ${sessionId}` }); return sessionId; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); store.addLog({ level: 'error', message: `创建会话失败: ${errorMsg}` }); set({ isLoading: false, error: errorMsg }); throw error; } }, closeSession: async (sessionId: string) => { const store = get(); store.setLoading(true); try { await closeSession(sessionId); set((state) => ({ sessions: state.sessions.filter(s => s.id !== sessionId), activeSessionId: state.activeSessionId === sessionId ? null : state.activeSessionId, isLoading: false, })); store.addLog({ level: 'info', message: `会话已关闭: ${sessionId}` }); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); store.addLog({ level: 'error', message: `关闭会话失败: ${errorMsg}` }); set({ isLoading: false, error: errorMsg }); throw error; } }, listSessions: async () => { try { const sessions = await listSessions(); const mappedSessions: BrowserSession[] = sessions.map(s => ({ id: s.id, name: `Browser ${s.id.substring(0, 8)}`, currentUrl: s.current_url, title: s.title, status: s.status as SessionStatus, createdAt: s.created_at, lastActivity: s.last_activity, })); set({ sessions: mappedSessions }); } catch (error) { console.error('[BrowserHand] Failed to list sessions:', error); } }, setActiveSession: (sessionId: string | null) => { set({ activeSessionId: sessionId }); }, // Template Execution executeTemplate: async (templateId: string, params: Record) => { const store = get(); // Find template const template = store.templates.find(t => t.id === templateId); if (!template) { throw new Error(`Template not found: ${templateId}`); } // Validate params const validation = validateTemplateParams(template.params, params); if (!validation.valid) { const errorMessages = validation.errors.map(e => e.message).join(', '); throw new Error(`Invalid parameters: ${errorMessages}`); } // Merge with defaults const mergedParams = mergeParamsWithDefaults(template.params, params); // Initialize execution state const startTime = new Date().toISOString(); set({ execution: { ...initialExecutionState, isRunning: true, startTime, status: 'running', }, }); // Create browser instance const browser = new Browser(); try { store.addLog({ level: 'info', message: `开始执行模板: ${template.name}` }); // Start browser session await browser.start({ headless: true }); // Create execution context const context = { browser, onProgress: (action: string, progress: number) => { store.updateExecutionState({ currentAction: action, progress }); store.addLog({ level: 'action', message: action }); }, onLog: (level: LogLevel, message: string, details?: Record) => { store.addLog({ level, message, details }); }, }; // Execute template const result = await template.execute(mergedParams, context); // Update state on success set((state) => ({ execution: { ...state.execution, isRunning: false, progress: 100, status: 'success', }, })); // Add to recent tasks const duration = Date.now() - new Date(startTime).getTime(); store.addRecentTask({ templateId: template.id, templateName: template.name, params: mergedParams, status: 'success', duration, result, }); store.addLog({ level: 'info', message: `模板执行完成: ${template.name}` }); return result; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); set((state) => ({ execution: { ...state.execution, isRunning: false, status: 'error', error: errorMsg, }, })); // Add failed task const duration = Date.now() - new Date(startTime).getTime(); store.addRecentTask({ templateId: template.id, templateName: template.name, params: mergedParams, status: 'failed', duration, error: errorMsg, }); store.addLog({ level: 'error', message: `模板执行失败: ${errorMsg}` }); throw error; } finally { await browser.close(); } }, executeScript: async (script: string, args?: unknown[]) => { const store = get(); if (!store.activeSessionId) { throw new Error('No active browser session'); } store.updateExecutionState({ isRunning: true, currentAction: 'Executing script...', status: 'running', }); try { const browser = new Browser(); await browser.start(); const result = await browser.eval(script, args); store.updateExecutionState({ isRunning: false, status: 'success', }); return result; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); store.updateExecutionState({ isRunning: false, status: 'error', error: errorMsg, }); throw error; } }, // State Updates updateExecutionState: (state: Partial) => { set((prev) => ({ execution: { ...prev.execution, ...state }, })); }, addLog: (log: Omit) => { const newLog: BrowserLog = { ...log, id: uuidv4(), timestamp: new Date().toISOString(), }; set((state) => { const logs = [...state.logs, newLog]; // Trim logs if exceeding max if (logs.length > state.maxLogs) { return { logs: logs.slice(-state.maxLogs) }; } return { logs }; }); }, clearLogs: () => { set({ logs: [] }); }, // Screenshot takeScreenshot: async () => { const store = get(); if (!store.activeSessionId) { throw new Error('No active browser session'); } try { const browser = new Browser(); await browser.start(); const result = await browser.screenshot(); set((state) => ({ execution: { ...state.execution, lastScreenshot: result.base64, }, })); store.addLog({ level: 'info', message: 'Screenshot captured' }); return result.base64; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); store.addLog({ level: 'error', message: `Screenshot failed: ${errorMsg}` }); throw error; } }, // UI Control openTemplateModal: () => { set({ isTemplateModalOpen: true }); }, closeTemplateModal: () => { set({ isTemplateModalOpen: false }); }, setLoading: (loading: boolean) => { set({ isLoading: loading }); }, setError: (error: string | null) => { set({ error }); }, clearError: () => { set({ error: null }); }, // Recent Tasks addRecentTask: (task: Omit) => { const newTask: RecentTask = { ...task, id: uuidv4(), executedAt: new Date().toISOString(), }; set((state) => { const recentTasks = [newTask, ...state.recentTasks]; // Trim if exceeding max if (recentTasks.length > state.maxRecentTasks) { return { recentTasks: recentTasks.slice(0, state.maxRecentTasks) }; } return { recentTasks }; }); }, clearRecentTasks: () => { set({ recentTasks: [] }); }, })); // ============================================================================ // Selector Hooks // ============================================================================ export const useActiveSession = () => useBrowserHandStore((state) => { if (!state.activeSessionId) return null; return state.sessions.find(s => s.id === state.activeSessionId) ?? null; }); export const useExecutionState = () => useBrowserHandStore((state) => state.execution); export const useIsRunning = () => useBrowserHandStore((state) => state.execution.isRunning); export const useTemplates = () => useBrowserHandStore((state) => state.templates); export const useTemplatesByCategory = (category: string) => useBrowserHandStore((state) => state.templates.filter(t => t.category === category) ); export const useRecentTasks = () => useBrowserHandStore((state) => state.recentTasks); export const useLogs = () => useBrowserHandStore((state) => state.logs);