import { create } from 'zustand'; import { DEFAULT_GATEWAY_URL, FALLBACK_GATEWAY_URLS, GatewayClient, ConnectionState, getGatewayClient, getLocalDeviceIdentity, getStoredGatewayToken, getStoredGatewayUrl, setStoredGatewayToken, setStoredGatewayUrl } from '../lib/gateway-client'; import type { GatewayModelChoice } from '../lib/gateway-config'; import { approveLocalGatewayDevicePairing, getLocalGatewayAuth, getLocalGatewayStatus, getUnsupportedLocalGatewayStatus, isTauriRuntime, prepareLocalGatewayForTauri, restartLocalGateway as restartLocalGatewayCommand, startLocalGateway as startLocalGatewayCommand, stopLocalGateway as stopLocalGatewayCommand, type LocalGatewayStatus } from '../lib/tauri-gateway'; import { useChatStore } from './chatStore'; interface GatewayLog { timestamp: number; level: string; message: string; } interface Clone { id: string; name: string; role?: string; nickname?: string; scenarios?: string[]; model?: string; workspaceDir?: string; workspaceResolvedPath?: string; restrictFiles?: boolean; privacyOptIn?: boolean; userName?: string; userRole?: string; createdAt: string; bootstrapReady?: boolean; bootstrapFiles?: Array<{ name: string; path: string; exists: boolean }>; updatedAt?: string; } interface UsageStats { totalSessions: number; totalMessages: number; totalTokens: number; byModel: Record; } interface ChannelInfo { id: string; type: string; label: string; status: 'active' | 'inactive' | 'error'; accounts?: number; error?: string; } export interface PluginStatus { id: string; name?: string; status: 'active' | 'inactive' | 'error' | 'loading'; version?: string; description?: string; } interface ScheduledTask { id: string; name: string; schedule: string; status: 'active' | 'paused' | 'completed' | 'error'; lastRun?: string; nextRun?: string; description?: string; } interface SkillInfo { id: string; name: string; path: string; source: 'builtin' | 'extra'; description?: string; triggers?: Array<{ type: string; pattern?: string }>; actions?: Array<{ type: string; params?: Record }>; enabled?: boolean; } interface QuickConfig { agentName?: string; agentRole?: string; userName?: string; userRole?: string; agentNickname?: string; scenarios?: string[]; workspaceDir?: string; gatewayUrl?: string; gatewayToken?: string; skillsExtraDirs?: string[]; mcpServices?: Array<{ id: string; name: string; enabled: boolean }>; theme?: 'light' | 'dark'; autoStart?: boolean; showToolCalls?: boolean; restrictFiles?: boolean; autoSaveContext?: boolean; fileWatching?: boolean; privacyOptIn?: boolean; } interface WorkspaceInfo { path: string; resolvedPath: string; exists: boolean; fileCount: number; totalSize: number; } // === Raw API Response Types (for mapping) === interface RawHandRequirement { description?: string; name?: string; met?: boolean; satisfied?: boolean; details?: string; hint?: string; } interface RawHandRun { runId?: string; run_id?: string; id?: string; status?: string; startedAt?: string; started_at?: string; created_at?: string; completedAt?: string; completed_at?: string; finished_at?: string; result?: unknown; output?: unknown; error?: string; message?: string; } interface RawApproval { id?: string; approvalId?: string; approval_id?: string; type?: string; request_type?: string; handId?: string; hand_id?: string; requester?: string; requested_by?: string; status?: string; createdAt?: string; created_at?: string; details?: Record; metadata?: Record; } interface RawSession { id?: string; sessionId?: string; session_id?: string; agentId?: string; agent_id?: string; model?: string; status?: string; createdAt?: string; created_at?: string; updatedAt?: string; updated_at?: string; messageCount?: number; message_count?: number; } interface RawSessionMessage { id?: string; messageId?: string; message_id?: string; role?: string; content?: string; createdAt?: string; created_at?: string; metadata?: Record; } interface RawWorkflowRun { runId?: string; run_id?: string; id?: string; workflowId?: string; workflow_id?: string; status?: string; startedAt?: string; started_at?: string; completedAt?: string; completed_at?: string; currentStep?: number; current_step?: number; totalSteps?: number; total_steps?: number; error?: string; } // === OpenFang Types === export interface HandRequirement { description: string; met: boolean; details?: string; } export interface Hand { id: string; // Hand ID used for API calls name: string; // Display name description: string; status: 'idle' | 'running' | 'needs_approval' | 'error' | 'unavailable' | 'setup_needed'; currentRunId?: string; requirements_met?: boolean; category?: string; // productivity, data, content, communication icon?: string; // Extended fields from details API provider?: string; model?: string; requirements?: HandRequirement[]; tools?: string[]; metrics?: string[]; toolCount?: number; metricCount?: number; } export interface HandRun { runId: string; status: string; startedAt: string; completedAt?: string; result?: unknown; error?: string; } export interface HandRunStore { runs: HandRun[]; isLoading: boolean; error?: string; } export interface Workflow { id: string; name: string; steps: number; description?: string; createdAt?: string; } export interface WorkflowRun { runId: string; status: string; step?: string; result?: unknown; } // === Session Types === export interface SessionMessage { id: string; role: 'user' | 'assistant' | 'system'; content: string; createdAt: string; tokens?: { input?: number; output?: number }; } export interface Session { id: string; agentId: string; createdAt: string; updatedAt?: string; messageCount?: number; status?: 'active' | 'archived' | 'expired'; metadata?: Record; } export interface Trigger { id: string; type: string; enabled: boolean; } // === Scheduler Types === export interface ScheduledJob { id: string; name: string; cron: string; enabled: boolean; handName?: string; workflowId?: string; lastRun?: string; nextRun?: string; } export interface EventTrigger { id: string; name: string; eventType: string; enabled: boolean; handName?: string; workflowId?: string; } export interface RunHistoryEntry { id: string; type: 'scheduled_job' | 'event_trigger'; sourceId: string; sourceName: string; status: 'success' | 'failure' | 'running'; startedAt: string; completedAt?: string; duration?: number; error?: string; } // === Approval Types === export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired'; export interface Approval { id: string; handName: string; runId?: string; status: ApprovalStatus; requestedAt: string; requestedBy?: string; reason?: string; action?: string; params?: Record; respondedAt?: string; respondedBy?: string; responseReason?: string; } export interface AuditLogEntry { id: string; timestamp: string; action: string; actor?: string; result?: 'success' | 'failure'; details?: Record; // Merkle hash chain fields (OpenFang) hash?: string; previousHash?: string; } // === Security Types === export interface SecurityLayer { name: string; enabled: boolean; description?: string; } export interface SecurityStatus { layers: SecurityLayer[]; enabledCount: number; totalCount: number; securityLevel: 'critical' | 'high' | 'medium' | 'low'; } function shouldRetryGatewayCandidate(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error || ''); return ( message === 'WebSocket connection failed' || message.startsWith('Gateway handshake timed out') || message.startsWith('WebSocket closed before handshake completed') ); } function requiresLocalDevicePairing(error: unknown): boolean { const message = error instanceof Error ? error.message : String(error || ''); return message.includes('pairing required'); } function calculateSecurityLevel(enabledCount: number, totalCount: number): 'critical' | 'high' | 'medium' | 'low' { if (totalCount === 0) return 'low'; const ratio = enabledCount / totalCount; if (ratio >= 0.875) return 'critical'; // 14-16 layers if (ratio >= 0.625) return 'high'; // 10-13 layers if (ratio >= 0.375) return 'medium'; // 6-9 layers return 'low'; // 0-5 layers } function isLoopbackGatewayUrl(url: string): boolean { return /^wss?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/i.test(url.trim()); } async function approveCurrentLocalDevicePairing(url: string): Promise { if (!isTauriRuntime() || !isLoopbackGatewayUrl(url)) { return false; } const identity = await getLocalDeviceIdentity(); const result = await approveLocalGatewayDevicePairing(identity.deviceId, identity.publicKeyBase64, url); return result.approved; } interface GatewayStore { // Connection state connectionState: ConnectionState; gatewayVersion: string | null; error: string | null; logs: GatewayLog[]; localGateway: LocalGatewayStatus; localGatewayBusy: boolean; isLoading: boolean; // Data clones: Clone[]; usageStats: UsageStats | null; pluginStatus: PluginStatus[]; channels: ChannelInfo[]; scheduledTasks: ScheduledTask[]; skillsCatalog: SkillInfo[]; quickConfig: QuickConfig; workspaceInfo: WorkspaceInfo | null; // Models Data models: GatewayModelChoice[]; modelsLoading: boolean; modelsError: string | null; // OpenFang Data hands: Hand[]; handRuns: Record; // handName -> runs workflows: Workflow[]; triggers: Trigger[]; auditLogs: AuditLogEntry[]; securityStatus: SecurityStatus | null; securityStatusLoading: boolean; securityStatusError: string | null; approvals: Approval[]; // Session Data sessions: Session[]; sessionMessages: Record; // sessionId -> messages // Workflow Runs Data workflowRuns: Record; // workflowId -> runs // Client reference client: GatewayClient; // Actions connect: (url?: string, token?: string) => Promise; disconnect: () => void; sendMessage: (message: string, sessionKey?: string) => Promise<{ runId: string }>; loadClones: () => Promise; createClone: (opts: { name: string; role?: string; nickname?: string; scenarios?: string[]; model?: string; workspaceDir?: string; restrictFiles?: boolean; privacyOptIn?: boolean; userName?: string; userRole?: string; }) => Promise; updateClone: (id: string, updates: Partial) => Promise; deleteClone: (id: string) => Promise; loadUsageStats: () => Promise; loadPluginStatus: () => Promise; loadChannels: () => Promise; getChannel: (id: string) => Promise; createChannel: (channel: { type: string; name: string; config: Record; enabled?: boolean }) => Promise; updateChannel: (id: string, updates: { name?: string; config?: Record; enabled?: boolean }) => Promise; deleteChannel: (id: string) => Promise; loadScheduledTasks: () => Promise; createScheduledTask: (task: { name: string; schedule: string; scheduleType: 'cron' | 'interval' | 'once'; target?: { type: 'agent' | 'hand' | 'workflow'; id: string; }; description?: string; enabled?: boolean; }) => Promise; loadSkillsCatalog: () => Promise; getSkill: (id: string) => Promise; createSkill: (skill: { name: string; description?: string; triggers: Array<{ type: string; pattern?: string }>; actions: Array<{ type: string; params?: Record }>; enabled?: boolean }) => Promise; updateSkill: (id: string, updates: { name?: string; description?: string; triggers?: Array<{ type: string; pattern?: string }>; actions?: Array<{ type: string; params?: Record }>; enabled?: boolean }) => Promise; deleteSkill: (id: string) => Promise; loadQuickConfig: () => Promise; saveQuickConfig: (updates: Partial) => Promise; loadWorkspaceInfo: () => Promise; refreshLocalGateway: () => Promise; startLocalGateway: () => Promise; stopLocalGateway: () => Promise; restartLocalGateway: () => Promise; clearLogs: () => void; // Models Actions loadModels: () => Promise; // OpenFang Actions loadHands: () => Promise; getHandDetails: (name: string) => Promise; loadHandRuns: (name: string, opts?: { limit?: number; offset?: number }) => Promise; triggerHand: (name: string, params?: Record) => Promise; approveHand: (name: string, runId: string, approved: boolean, reason?: string) => Promise; cancelHand: (name: string, runId: string) => Promise; loadWorkflows: () => Promise; createWorkflow: (workflow: { name: string; description?: string; steps: Array<{ handName: string; name?: string; params?: Record; condition?: string; }>; }) => Promise; updateWorkflow: (id: string, updates: { name?: string; description?: string; steps?: Array<{ handName: string; name?: string; params?: Record; condition?: string; }>; }) => Promise; deleteWorkflow: (id: string) => Promise; executeWorkflow: (id: string, input?: Record) => Promise; cancelWorkflow: (id: string, runId: string) => Promise; loadTriggers: () => Promise; // Workflow Run Actions loadWorkflowRuns: (workflowId: string, opts?: { limit?: number; offset?: number }) => Promise; // Trigger Actions getTrigger: (id: string) => Promise; createTrigger: (trigger: { type: string; name?: string; enabled?: boolean; config?: Record; handName?: string; workflowId?: string }) => Promise; updateTrigger: (id: string, updates: { name?: string; enabled?: boolean; config?: Record; handName?: string; workflowId?: string }) => Promise; deleteTrigger: (id: string) => Promise; loadAuditLogs: (opts?: { limit?: number; offset?: number }) => Promise; loadSecurityStatus: () => Promise; loadApprovals: (status?: ApprovalStatus) => Promise; respondToApproval: (approvalId: string, approved: boolean, reason?: string) => Promise; // Session Actions loadSessions: (opts?: { limit?: number; offset?: number }) => Promise; getSession: (sessionId: string) => Promise; createSession: (agentId: string, metadata?: Record) => Promise; deleteSession: (sessionId: string) => Promise; loadSessionMessages: (sessionId: string, opts?: { limit?: number; offset?: number }) => Promise; } function normalizeGatewayUrlCandidate(url: string): string { return url.trim().replace(/\/+$/, ''); } function getLocalGatewayConnectUrl(status: LocalGatewayStatus): string | null { if (status.probeUrl && status.probeUrl.trim()) { return normalizeGatewayUrlCandidate(status.probeUrl); } if (status.port) { return `ws://127.0.0.1:${status.port}`; } return null; } export const useGatewayStore = create((set, get) => { const client = getGatewayClient(); // Wire up state change callback client.onStateChange = (state) => { set({ connectionState: state }); }; client.onLog = (level, message) => { set((s) => ({ logs: [...s.logs.slice(-99), { timestamp: Date.now(), level, message }], })); }; return { connectionState: 'disconnected', gatewayVersion: null, error: null, logs: [], localGateway: getUnsupportedLocalGatewayStatus(), localGatewayBusy: false, isLoading: false, clones: [], usageStats: null, pluginStatus: [], channels: [], scheduledTasks: [], skillsCatalog: [], quickConfig: {}, workspaceInfo: null, // Models state models: [], modelsLoading: false, modelsError: null, // OpenFang state hands: [], handRuns: {}, // handName -> runs workflows: [], triggers: [], auditLogs: [], securityStatus: null, securityStatusLoading: false, securityStatusError: null, approvals: [], // Session state sessions: [], sessionMessages: {}, // Workflow Runs state workflowRuns: {}, client, connect: async (url?: string, token?: string) => { const c = get().client; const resolveCandidates = async (): Promise => { const explicitUrl = url?.trim(); if (explicitUrl) { return [normalizeGatewayUrlCandidate(explicitUrl)]; } const candidates: string[] = []; if (isTauriRuntime()) { try { const localStatus = await getLocalGatewayStatus(); const localUrl = getLocalGatewayConnectUrl(localStatus); if (localUrl) { candidates.push(localUrl); } } catch { /* ignore local gateway lookup failures during candidate selection */ } } const quickConfigGatewayUrl = get().quickConfig.gatewayUrl?.trim(); if (quickConfigGatewayUrl) { candidates.push(quickConfigGatewayUrl); } candidates.push(getStoredGatewayUrl(), DEFAULT_GATEWAY_URL, ...FALLBACK_GATEWAY_URLS); return Array.from( new Set( candidates .filter(Boolean) .map(normalizeGatewayUrlCandidate) ) ); }; try { set({ error: null }); if (isTauriRuntime()) { try { await prepareLocalGatewayForTauri(); } catch { /* ignore local gateway preparation failures during connection bootstrap */ } } // Use the first non-empty token from: param > quickConfig > localStorage let effectiveToken = token || get().quickConfig.gatewayToken || getStoredGatewayToken(); if (!effectiveToken && isTauriRuntime()) { try { const localAuth = await getLocalGatewayAuth(); if (localAuth.gatewayToken) { effectiveToken = localAuth.gatewayToken; setStoredGatewayToken(localAuth.gatewayToken); } } catch { /* ignore local auth lookup failures during connection bootstrap */ } } console.log('[GatewayStore] Connecting with token:', effectiveToken ? `${effectiveToken.substring(0, 8)}...` : '(empty)'); const candidateUrls = await resolveCandidates(); let lastError: unknown = null; let connectedUrl: string | null = null; for (const candidateUrl of candidateUrls) { try { c.updateOptions({ url: candidateUrl, token: effectiveToken, }); await c.connect(); connectedUrl = candidateUrl; break; } catch (err) { lastError = err; if (requiresLocalDevicePairing(err)) { const approved = await approveCurrentLocalDevicePairing(candidateUrl); if (approved) { c.updateOptions({ url: candidateUrl, token: effectiveToken, }); await c.connect(); connectedUrl = candidateUrl; break; } } if (!shouldRetryGatewayCandidate(err)) { throw err; } } } if (!connectedUrl) { throw (lastError instanceof Error ? lastError : new Error('无法连接到任何可用 Gateway')); } setStoredGatewayUrl(connectedUrl); // Fetch initial data after connection try { const health = await c.health(); set({ gatewayVersion: health?.version }); } catch { /* health may not return version */ } await Promise.allSettled([ get().loadQuickConfig(), get().loadWorkspaceInfo(), get().loadClones(), get().loadUsageStats(), get().loadPluginStatus(), get().loadScheduledTasks(), get().loadSkillsCatalog(), // OpenFang data loading get().loadHands(), get().loadWorkflows(), get().loadTriggers(), get().loadSecurityStatus(), ]); await get().loadChannels(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, disconnect: () => { get().client.disconnect(); }, sendMessage: async (message: string, sessionKey?: string) => { const c = get().client; return c.chat(message, { sessionKey }); }, loadClones: async () => { try { const result = await get().client.listClones(); const clones = result?.clones || result?.agents || []; set({ clones }); useChatStore.getState().syncAgents(clones); // Set default agent ID if we have agents and none is set if (clones.length > 0 && clones[0].id) { const client = get().client; const currentDefault = client.getDefaultAgentId(); // Only set if the default doesn't exist in the list const defaultExists = clones.some((c) => c.id === currentDefault); if (!defaultExists) { client.setDefaultAgentId(clones[0].id); } } } catch { /* ignore if method not available */ } }, createClone: async (opts) => { try { const result = await get().client.createClone(opts); await get().loadClones(); return result?.clone; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, updateClone: async (id, updates) => { try { const result = await get().client.updateClone(id, updates); await get().loadClones(); return result?.clone; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, deleteClone: async (id: string) => { try { await get().client.deleteClone(id); await get().loadClones(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); } }, loadUsageStats: async () => { try { const stats = await get().client.getUsageStats(); set({ usageStats: stats }); } catch { /* ignore */ } }, loadPluginStatus: async () => { try { const result = await get().client.getPluginStatus(); set({ pluginStatus: result?.plugins || [] }); } catch { /* ignore */ } }, loadChannels: async () => { const channels: { id: string; type: string; label: string; status: 'active' | 'inactive' | 'error'; accounts?: number; error?: string }[] = []; try { // Try listing channels from Gateway const result = await get().client.listChannels(); if (result?.channels) { set({ channels: result.channels }); return; } } catch { /* channels.list may not be available */ } // Fallback: probe known channels individually try { const feishu = await get().client.getFeishuStatus(); channels.push({ id: 'feishu', type: 'feishu', label: '飞书 (Feishu)', status: feishu?.configured ? 'active' : 'inactive', accounts: feishu?.accounts || 0, }); } catch { channels.push({ id: 'feishu', type: 'feishu', label: '飞书 (Feishu)', status: 'inactive' }); } // QQ channel (check if qqbot plugin is loaded) const plugins = get().pluginStatus; const qqPlugin = plugins.find((p) => (p.name || p.id || '').toLowerCase().includes('qqbot')); if (qqPlugin) { channels.push({ id: 'qqbot', type: 'qqbot', label: 'QQ 机器人', status: qqPlugin.status === 'active' ? 'active' : 'inactive', }); } set({ channels }); }, getChannel: async (id: string) => { try { const result = await get().client.getChannel(id); if (result?.channel) { // Update the channel in the local state if it exists const currentChannels = get().channels; const existingIndex = currentChannels.findIndex(c => c.id === id); if (existingIndex >= 0) { const updatedChannels = [...currentChannels]; updatedChannels[existingIndex] = result.channel; set({ channels: updatedChannels }); } return result.channel as ChannelInfo; } return undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, createChannel: async (channel) => { try { const result = await get().client.createChannel(channel); if (result?.channel) { // Add the new channel to local state const currentChannels = get().channels; set({ channels: [...currentChannels, result.channel as ChannelInfo] }); return result.channel as ChannelInfo; } return undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, updateChannel: async (id, updates) => { try { const result = await get().client.updateChannel(id, updates); if (result?.channel) { // Update the channel in local state const currentChannels = get().channels; const updatedChannels = currentChannels.map(c => c.id === id ? (result.channel as ChannelInfo) : c ); set({ channels: updatedChannels }); return result.channel as ChannelInfo; } return undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, deleteChannel: async (id) => { try { await get().client.deleteChannel(id); // Remove the channel from local state const currentChannels = get().channels; set({ channels: currentChannels.filter(c => c.id !== id) }); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); } }, loadScheduledTasks: async () => { try { const result = await get().client.listScheduledTasks(); set({ scheduledTasks: result?.tasks || [] }); } catch { /* ignore if heartbeat.tasks not available */ } }, createScheduledTask: async (task) => { try { const result = await get().client.createScheduledTask(task); const newTask = { id: result.id, name: result.name, schedule: result.schedule, status: result.status as 'active' | 'paused' | 'completed' | 'error', }; set((state) => ({ scheduledTasks: [...state.scheduledTasks, newTask], })); return newTask; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Failed to create scheduled task'; set({ error: errorMessage }); throw err; } }, loadSkillsCatalog: async () => { try { const result = await get().client.listSkills(); set({ skillsCatalog: result?.skills || [] }); if (result?.extraDirs) { set((state) => ({ quickConfig: { ...state.quickConfig, skillsExtraDirs: result.extraDirs, }, })); } } catch { /* ignore if skills list not available */ } }, getSkill: async (id: string) => { try { const result = await get().client.getSkill(id); return result?.skill as SkillInfo | undefined; } catch { return undefined; } }, createSkill: async (skill) => { try { const result = await get().client.createSkill(skill); const newSkill = result?.skill as SkillInfo | undefined; if (newSkill) { set((state) => ({ skillsCatalog: [...state.skillsCatalog, newSkill], })); } return newSkill; } catch { return undefined; } }, updateSkill: async (id, updates) => { try { const result = await get().client.updateSkill(id, updates); const updatedSkill = result?.skill as SkillInfo | undefined; if (updatedSkill) { set((state) => ({ skillsCatalog: state.skillsCatalog.map((s) => s.id === id ? updatedSkill : s ), })); } return updatedSkill; } catch { return undefined; } }, deleteSkill: async (id) => { try { await get().client.deleteSkill(id); set((state) => ({ skillsCatalog: state.skillsCatalog.filter((s) => s.id !== id), })); } catch { /* ignore deletion errors */ } }, loadQuickConfig: async () => { try { const result = await get().client.getQuickConfig(); set({ quickConfig: result?.quickConfig || {} }); } catch { /* ignore if quick config not available */ } }, saveQuickConfig: async (updates) => { try { const nextConfig = { ...get().quickConfig, ...updates }; if (nextConfig.gatewayUrl) { setStoredGatewayUrl(nextConfig.gatewayUrl); } if (Object.prototype.hasOwnProperty.call(updates, 'gatewayToken')) { setStoredGatewayToken(nextConfig.gatewayToken || ''); } const result = await get().client.saveQuickConfig(nextConfig); set({ quickConfig: result?.quickConfig || nextConfig }); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); } }, loadWorkspaceInfo: async () => { try { const info = await get().client.getWorkspaceInfo(); set({ workspaceInfo: info }); } catch { /* ignore if workspace info not available */ } }, refreshLocalGateway: async () => { if (!isTauriRuntime()) { const unsupported = getUnsupportedLocalGatewayStatus(); set({ localGateway: unsupported, localGatewayBusy: false }); return unsupported; } set({ localGatewayBusy: true }); try { const status = await getLocalGatewayStatus(); set({ localGateway: status, localGatewayBusy: false }); return status; } catch (err: unknown) { const message = err?.message || '读取本地 Gateway 状态失败'; const nextStatus = { ...get().localGateway, supported: true, error: message, }; set({ localGateway: nextStatus, localGatewayBusy: false, error: message }); return nextStatus; } }, startLocalGateway: async () => { if (!isTauriRuntime()) { const unsupported = getUnsupportedLocalGatewayStatus(); set({ localGateway: unsupported, localGatewayBusy: false }); return unsupported; } set({ localGatewayBusy: true, error: null }); try { const status = await startLocalGatewayCommand(); set({ localGateway: status, localGatewayBusy: false }); return status; } catch (err: unknown) { const message = err?.message || '启动本地 Gateway 失败'; const nextStatus = { ...get().localGateway, supported: true, error: message, }; set({ localGateway: nextStatus, localGatewayBusy: false, error: message }); return undefined; } }, stopLocalGateway: async () => { if (!isTauriRuntime()) { const unsupported = getUnsupportedLocalGatewayStatus(); set({ localGateway: unsupported, localGatewayBusy: false }); return unsupported; } set({ localGatewayBusy: true, error: null }); try { const status = await stopLocalGatewayCommand(); set({ localGateway: status, localGatewayBusy: false }); return status; } catch (err: unknown) { const message = err?.message || '停止本地 Gateway 失败'; const nextStatus = { ...get().localGateway, supported: true, error: message, }; set({ localGateway: nextStatus, localGatewayBusy: false, error: message }); return undefined; } }, restartLocalGateway: async () => { if (!isTauriRuntime()) { const unsupported = getUnsupportedLocalGatewayStatus(); set({ localGateway: unsupported, localGatewayBusy: false }); return unsupported; } set({ localGatewayBusy: true, error: null }); try { const status = await restartLocalGatewayCommand(); set({ localGateway: status, localGatewayBusy: false }); return status; } catch (err: unknown) { const message = err?.message || '重启本地 Gateway 失败'; const nextStatus = { ...get().localGateway, supported: true, error: message, }; set({ localGateway: nextStatus, localGatewayBusy: false, error: message }); return undefined; } }, // === OpenFang Actions === loadHands: async () => { set({ isLoading: true }); try { const result = await get().client.listHands(); // Map API response to Hand interface const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const; const hands: Hand[] = (result?.hands || []).map(h => { const status = validStatuses.includes(h.status as any) ? h.status as Hand['status'] : (h.requirements_met ? 'idle' : 'setup_needed'); return { id: h.id || h.name, name: h.name, description: h.description || '', status, requirements_met: h.requirements_met, category: h.category, icon: h.icon, toolCount: h.tool_count || h.tools?.length, metricCount: h.metric_count || h.metrics?.length, }; }); set({ hands, isLoading: false }); } catch { set({ isLoading: false }); /* ignore if hands API not available */ } }, getHandDetails: async (name: string) => { try { const result = await get().client.getHand(name); if (!result) return undefined; // Helper to extract string from unknown config const getStringFromConfig = (key: string): string | undefined => { const val = result.config?.[key]; return typeof val === 'string' ? val : undefined; }; const getArrayFromConfig = (key: string): string[] | undefined => { const val = result.config?.[key]; return Array.isArray(val) ? val : undefined; }; const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'] as const; const status = validStatuses.includes(result.status as any) ? result.status as Hand['status'] : (result.requirements_met ? 'idle' : 'setup_needed'); // Map API response to extended Hand interface const hand: Hand = { id: result.id || result.name || name, name: result.name || name, description: result.description || '', status, requirements_met: result.requirements_met, category: result.category, icon: result.icon, provider: result.provider || getStringFromConfig('provider'), model: result.model || getStringFromConfig('model'), requirements: result.requirements?.map((r: RawHandRequirement) => ({ description: r.description || r.name || String(r), met: r.met ?? r.satisfied ?? true, details: r.details || r.hint, })), tools: result.tools || getArrayFromConfig('tools'), metrics: result.metrics || getArrayFromConfig('metrics'), toolCount: result.tool_count || result.tools?.length || 0, metricCount: result.metric_count || result.metrics?.length || 0, }; // Update hands list with detailed info set(state => ({ hands: state.hands.map(h => h.name === name ? { ...h, ...hand } : h), })); return hand; } catch { return undefined; } }, loadHandRuns: async (name: string, opts?: { limit?: number; offset?: number }) => { try { const result = await get().client.listHandRuns(name, opts); const runs: HandRun[] = (result?.runs || []).map((r: RawHandRun) => ({ runId: r.runId || r.run_id || r.id, status: r.status || 'unknown', startedAt: r.startedAt || r.started_at || r.created_at || new Date().toISOString(), completedAt: r.completedAt || r.completed_at || r.finished_at, result: r.result || r.output, error: r.error || r.message, })); // Store runs by hand name set(state => ({ handRuns: { ...state.handRuns, [name]: runs }, })); return runs; } catch { return []; } }, triggerHand: async (name: string, params?: Record) => { try { const result = await get().client.triggerHand(name, params); return result ? { runId: result.runId, status: result.status, startedAt: new Date().toISOString() } : undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, approveHand: async (name: string, runId: string, approved: boolean, reason?: string) => { try { await get().client.approveHand(name, runId, approved, reason); // Refresh hands to update status await get().loadHands(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, cancelHand: async (name: string, runId: string) => { try { await get().client.cancelHand(name, runId); // Refresh hands to update status await get().loadHands(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, loadWorkflows: async () => { set({ isLoading: true }); try { const result = await get().client.listWorkflows(); set({ workflows: result?.workflows || [], isLoading: false }); } catch { set({ isLoading: false }); /* ignore if workflows API not available */ } }, createWorkflow: async (workflow: { name: string; description?: string; steps: Array<{ handName: string; name?: string; params?: Record; condition?: string; }>; }) => { try { const result = await get().client.createWorkflow(workflow); if (result) { const newWorkflow: Workflow = { id: result.id, name: result.name, steps: workflow.steps.length, description: workflow.description, }; set(state => ({ workflows: [...state.workflows, newWorkflow] })); return newWorkflow; } return undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, updateWorkflow: async (id: string, updates: { name?: string; description?: string; steps?: Array<{ handName: string; name?: string; params?: Record; condition?: string; }>; }) => { try { const result = await get().client.updateWorkflow(id, updates); if (result) { set(state => ({ workflows: state.workflows.map(w => w.id === id ? { ...w, name: updates.name || w.name, description: updates.description ?? w.description, steps: updates.steps?.length ?? w.steps, } : w ), })); return get().workflows.find(w => w.id === id); } return undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, deleteWorkflow: async (id: string) => { try { await get().client.deleteWorkflow(id); set(state => ({ workflows: state.workflows.filter(w => w.id !== id), })); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, executeWorkflow: async (id: string, input?: Record) => { try { const result = await get().client.executeWorkflow(id, input); return result ? { runId: result.runId, status: result.status } : undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, cancelWorkflow: async (id: string, runId: string) => { try { await get().client.cancelWorkflow(id, runId); // Refresh workflows to update status await get().loadWorkflows(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, loadTriggers: async () => { try { const result = await get().client.listTriggers(); set({ triggers: result?.triggers || [] }); } catch { /* ignore if triggers API not available */ } }, getTrigger: async (id: string) => { try { const result = await get().client.getTrigger(id); if (!result) return undefined; return { id: result.id, type: result.type, enabled: result.enabled, } as Trigger; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, createTrigger: async (trigger) => { try { const result = await get().client.createTrigger(trigger); if (!result?.id) return undefined; // Refresh triggers list after creation await get().loadTriggers(); return get().triggers.find(t => t.id === result.id); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, updateTrigger: async (id: string, updates) => { try { await get().client.updateTrigger(id, updates); // Update local state set(state => ({ triggers: state.triggers.map(t => t.id === id ? { ...t, ...updates } : t ), })); return get().triggers.find(t => t.id === id); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, deleteTrigger: async (id: string) => { try { await get().client.deleteTrigger(id); set(state => ({ triggers: state.triggers.filter(t => t.id !== id), })); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, loadAuditLogs: async (opts?: { limit?: number; offset?: number }) => { try { const result = await get().client.getAuditLogs(opts); set({ auditLogs: (result?.logs || []) as AuditLogEntry[] }); } catch { /* ignore if audit API not available */ } }, loadSecurityStatus: async () => { set({ securityStatusLoading: true, securityStatusError: null }); try { const result = await get().client.getSecurityStatus(); if (result?.layers) { const layers = result.layers as SecurityLayer[]; const enabledCount = layers.filter(l => l.enabled).length; const totalCount = layers.length; const securityLevel = calculateSecurityLevel(enabledCount, totalCount); set({ securityStatus: { layers, enabledCount, totalCount, securityLevel, }, securityStatusLoading: false, securityStatusError: null, }); } else { set({ securityStatusLoading: false, securityStatusError: 'API returned no data', }); } } catch (err: unknown) { set({ securityStatusLoading: false, securityStatusError: (err instanceof Error ? err.message : String(err)) || 'Security API not available', }); } }, loadApprovals: async (status?: ApprovalStatus) => { try { const result = await get().client.listApprovals(status); const approvals: Approval[] = (result?.approvals || []).map((a: RawApproval) => ({ id: a.id || a.approval_id, handName: a.hand_name || a.handName, runId: a.run_id || a.runId, status: a.status || 'pending', requestedAt: a.requested_at || a.requestedAt || new Date().toISOString(), requestedBy: a.requested_by || a.requestedBy, reason: a.reason || a.description, action: a.action || 'execute', params: a.params, respondedAt: a.responded_at || a.respondedAt, respondedBy: a.responded_by || a.respondedBy, responseReason: a.response_reason || a.responseReason, })); set({ approvals }); } catch { /* ignore if approvals API not available */ } }, respondToApproval: async (approvalId: string, approved: boolean, reason?: string) => { try { await get().client.respondToApproval(approvalId, approved, reason); // Refresh approvals after response await get().loadApprovals(); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, // === Session Actions === loadSessions: async (opts?: { limit?: number; offset?: number }) => { try { const result = await get().client.listSessions(opts); const sessions: Session[] = (result?.sessions || []).map((s: RawSession) => ({ id: s.id, agentId: s.agent_id, createdAt: s.created_at, updatedAt: s.updated_at, messageCount: s.message_count, status: s.status, metadata: s.metadata, })); set({ sessions }); } catch { /* ignore if sessions API not available */ } }, getSession: async (sessionId: string) => { try { const result = await get().client.getSession(sessionId); if (!result) return undefined; const session: Session = { id: result.id, agentId: result.agent_id, createdAt: result.created_at, updatedAt: result.updated_at, messageCount: result.message_count, status: result.status, metadata: result.metadata, }; // Update in list if exists set(state => ({ sessions: state.sessions.some(s => s.id === sessionId) ? state.sessions.map(s => s.id === sessionId ? session : s) : [...state.sessions, session], })); return session; } catch { return undefined; } }, createSession: async (agentId: string, metadata?: Record) => { try { const result = await get().client.createSession({ agent_id: agentId, metadata }); if (!result) return undefined; const session: Session = { id: result.id, agentId: result.agent_id, createdAt: result.created_at, status: 'active', metadata: metadata, }; set(state => ({ sessions: [...state.sessions, session] })); return session; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, deleteSession: async (sessionId: string) => { try { await get().client.deleteSession(sessionId); set(state => ({ sessions: state.sessions.filter(s => s.id !== sessionId), sessionMessages: Object.fromEntries( Object.entries(state.sessionMessages).filter(([id]) => id !== sessionId) ), })); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); throw err; } }, loadSessionMessages: async (sessionId: string, opts?: { limit?: number; offset?: number }) => { try { const result = await get().client.getSessionMessages(sessionId, opts); const messages: SessionMessage[] = (result?.messages || []).map((m: RawSessionMessage) => ({ id: m.id, role: m.role, content: m.content, createdAt: m.created_at, tokens: m.tokens, })); set(state => ({ sessionMessages: { ...state.sessionMessages, [sessionId]: messages }, })); return messages; } catch { return []; } }, clearLogs: () => set({ logs: [] }), // === Models Actions === loadModels: async () => { try { set({ modelsLoading: true, modelsError: null }); const result = await get().client.listModels(); const models: GatewayModelChoice[] = result?.models || []; set({ models, modelsLoading: false }); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to load models'; set({ modelsError: message, modelsLoading: false }); } }, // === Workflow Run Actions === loadWorkflowRuns: async (workflowId: string, opts?: { limit?: number; offset?: number }) => { try { const result = await get().client.listWorkflowRuns(workflowId, opts); const runs: WorkflowRun[] = (result?.runs || []).map((r: RawWorkflowRun) => ({ runId: r.runId || r.run_id, status: r.status, startedAt: r.startedAt || r.started_at, completedAt: r.completedAt || r.completed_at, step: r.step, result: r.result, error: r.error, })); // Store runs by workflow ID set(state => ({ workflowRuns: { ...state.workflowRuns, [workflowId]: runs }, })); return runs; } catch { return []; } }, }; });