/** * configStore.ts - Configuration Management Store * * Extracted from gatewayStore.ts for Phase 11 Store Refactoring. * Manages settings, workspace, channels, skills, and models. */ import { create } from 'zustand'; import type { GatewayModelChoice } from '../lib/gateway-config'; import { setStoredGatewayUrl, setStoredGatewayToken } from '../lib/gateway-client'; import type { GatewayClient } from '../lib/gateway-client'; // === Types === export 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; } export interface WorkspaceInfo { path: string; resolvedPath: string; exists: boolean; fileCount: number; totalSize: number; } export interface ChannelInfo { id: string; type: string; label: string; status: 'active' | 'inactive' | 'error'; accounts?: number; error?: string; } export interface ScheduledTask { id: string; name: string; schedule: string; status: 'active' | 'paused' | 'completed' | 'error'; lastRun?: string; nextRun?: string; description?: string; } export 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; } // === Client Interface === export interface ConfigStoreClient { getWorkspaceInfo(): Promise; getQuickConfig(): Promise<{ quickConfig?: QuickConfig } | null>; saveQuickConfig(config: QuickConfig): Promise<{ quickConfig?: QuickConfig } | null>; listSkills(): Promise<{ skills?: SkillInfo[]; extraDirs?: string[] } | null>; getSkill(id: string): Promise<{ skill?: SkillInfo } | null>; createSkill(skill: { name: string; description?: string; triggers: Array<{ type: string; pattern?: string }>; actions: Array<{ type: string; params?: Record }>; enabled?: boolean; }): Promise<{ skill?: SkillInfo } | null>; updateSkill(id: string, updates: { name?: string; description?: string; triggers?: Array<{ type: string; pattern?: string }>; actions?: Array<{ type: string; params?: Record }>; enabled?: boolean; }): Promise<{ skill?: SkillInfo } | null>; deleteSkill(id: string): Promise; listChannels(): Promise<{ channels?: ChannelInfo[] } | null>; getChannel(id: string): Promise<{ channel?: ChannelInfo } | null>; createChannel(channel: { type: string; name: string; config: Record; enabled?: boolean; }): Promise<{ channel?: ChannelInfo } | null>; updateChannel(id: string, updates: { name?: string; config?: Record; enabled?: boolean; }): Promise<{ channel?: ChannelInfo } | null>; deleteChannel(id: string): Promise; listScheduledTasks(): Promise<{ tasks?: ScheduledTask[] } | null>; createScheduledTask(task: { name: string; schedule: string; scheduleType: 'cron' | 'interval' | 'once'; target?: { type: 'agent' | 'hand' | 'workflow'; id: string; }; description?: string; enabled?: boolean; }): Promise; listModels(): Promise<{ models: GatewayModelChoice[] }>; getFeishuStatus(): Promise<{ configured?: boolean; accounts?: number } | null>; } // === Store State Slice === export interface ConfigStateSlice { quickConfig: QuickConfig; workspaceInfo: WorkspaceInfo | null; channels: ChannelInfo[]; scheduledTasks: ScheduledTask[]; skillsCatalog: SkillInfo[]; models: GatewayModelChoice[]; modelsLoading: boolean; modelsError: string | null; error: string | null; client: ConfigStoreClient | null; } // === Store Actions Slice === export interface ConfigActionsSlice { setConfigStoreClient: (client: ConfigStoreClient) => void; loadQuickConfig: () => Promise; saveQuickConfig: (updates: Partial) => Promise; loadWorkspaceInfo: () => 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; loadModels: () => Promise; clearError: () => void; } // === Combined Store Type === export type ConfigStore = ConfigStateSlice & ConfigActionsSlice; export const useConfigStore = create((set, get) => ({ // Initial State quickConfig: {}, workspaceInfo: null, channels: [], scheduledTasks: [], skillsCatalog: [], models: [], modelsLoading: false, modelsError: null, error: null, client: null, // Client Injection setConfigStoreClient: (client: ConfigStoreClient) => { set({ client }); }, // === Quick Config Actions === loadQuickConfig: async () => { const client = get().client; if (!client) return; try { const result = await client.getQuickConfig(); set({ quickConfig: result?.quickConfig || {} }); } catch { // Ignore if quick config not available } }, saveQuickConfig: async (updates: Partial) => { const client = get().client; if (!client) return; try { const nextConfig = { ...get().quickConfig, ...updates }; // Persist gateway URL/token to localStorage for reconnection if (nextConfig.gatewayUrl) { setStoredGatewayUrl(nextConfig.gatewayUrl); } if (Object.prototype.hasOwnProperty.call(updates, 'gatewayToken')) { setStoredGatewayToken(nextConfig.gatewayToken || ''); } const result = await client.saveQuickConfig(nextConfig); set({ quickConfig: result?.quickConfig || nextConfig }); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); } }, // === Workspace Actions === loadWorkspaceInfo: async () => { const client = get().client; if (!client) return; try { const info = await client.getWorkspaceInfo(); set({ workspaceInfo: info }); } catch { // Ignore if workspace info not available } }, // === Channel Actions === loadChannels: async () => { const client = get().client; if (!client) return; const channels: ChannelInfo[] = []; try { // Try listing channels from Gateway const result = await client.listChannels(); if (result?.channels) { set({ channels: result.channels }); return; } } catch { // channels.list may not be available, fallback to probing } // Fallback: probe known channels individually try { const feishu = await 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' }); } set({ channels }); }, getChannel: async (id: string) => { const client = get().client; if (!client) return undefined; try { const result = await 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) => { const client = get().client; if (!client) return undefined; try { const result = await 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) => { const client = get().client; if (!client) return undefined; try { const result = await 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) => { const client = get().client; if (!client) return; try { await 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) }); } }, // === Scheduled Task Actions === loadScheduledTasks: async () => { const client = get().client; if (!client) return; try { const result = await client.listScheduledTasks(); set({ scheduledTasks: result?.tasks || [] }); } catch { // Ignore if heartbeat.tasks not available } }, createScheduledTask: async (task) => { const client = get().client; if (!client) return undefined; try { const result = await client.createScheduledTask(task); const newTask: ScheduledTask = { id: result.id, name: result.name, schedule: result.schedule, status: result.status as 'active' | 'paused' | 'completed' | 'error', lastRun: result.lastRun, nextRun: result.nextRun, description: result.description, }; 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 }); return undefined; } }, // === Skill Actions === loadSkillsCatalog: async () => { const client = get().client; if (!client) return; try { const result = await 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) => { const client = get().client; if (!client) return undefined; try { const result = await client.getSkill(id); return result?.skill as SkillInfo | undefined; } catch { return undefined; } }, createSkill: async (skill) => { const client = get().client; if (!client) return undefined; try { const result = await 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) => { const client = get().client; if (!client) return undefined; try { const result = await 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) => { const client = get().client; if (!client) return; try { await client.deleteSkill(id); set((state) => ({ skillsCatalog: state.skillsCatalog.filter((s) => s.id !== id), })); } catch { // Ignore deletion errors } }, // === Model Actions === loadModels: async () => { const client = get().client; if (!client) return; try { set({ modelsLoading: true, modelsError: null }); const result = await 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 }); } }, // === Utility === clearError: () => { set({ error: null }); }, })); // Re-export types for convenience export type { QuickConfig as QuickConfigType, WorkspaceInfo as WorkspaceInfoType, ChannelInfo as ChannelInfoType, ScheduledTask as ScheduledTaskType, SkillInfo as SkillInfoType, }; // === Client Injection === /** * Helper to create a ConfigStoreClient adapter from a GatewayClient. */ function createConfigClientFromGateway(client: GatewayClient): ConfigStoreClient { return { getWorkspaceInfo: () => client.getWorkspaceInfo(), getQuickConfig: () => client.getQuickConfig(), saveQuickConfig: (config) => client.saveQuickConfig(config), listSkills: () => client.listSkills(), getSkill: (id) => client.getSkill(id), createSkill: (skill) => client.createSkill(skill), updateSkill: (id, updates) => client.updateSkill(id, updates), deleteSkill: (id) => client.deleteSkill(id), listChannels: () => client.listChannels(), getChannel: (id) => client.getChannel(id), createChannel: (channel) => client.createChannel(channel), updateChannel: (id, updates) => client.updateChannel(id, updates), deleteChannel: (id) => client.deleteChannel(id), listScheduledTasks: () => client.listScheduledTasks(), createScheduledTask: async (task) => { const result = await client.createScheduledTask(task); return { id: result.id, name: result.name, schedule: result.schedule, status: result.status as 'active' | 'paused' | 'completed' | 'error', }; }, listModels: () => client.listModels(), getFeishuStatus: () => client.getFeishuStatus(), }; } /** * Sets the client for the config store. * Called by the coordinator during initialization. */ export function setConfigStoreClient(client: unknown): void { const configClient = createConfigClientFromGateway(client as GatewayClient); useConfigStore.getState().setConfigStoreClient(configClient); }