refactor(store): split gatewayStore into specialized domain stores

Major restructuring:
- Split monolithic gatewayStore into 5 focused stores:
  - connectionStore: WebSocket connection and gateway lifecycle
  - configStore: quickConfig, workspaceInfo, MCP services
  - agentStore: clones, usage stats, agent management
  - handStore: hands, approvals, triggers, hand runs
  - workflowStore: workflows, workflow runs, execution

- Update all components to use new stores with selector pattern
- Remove
This commit is contained in:
iven
2026-03-20 22:14:13 +08:00
parent 6f72442531
commit 1cf3f585d3
43 changed files with 2826 additions and 3103 deletions

View File

@@ -6,6 +6,7 @@
*/
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 ===
@@ -233,6 +234,13 @@ export const useConfigStore = create<ConfigStateSlice & ConfigActionsSlice>((set
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) {
@@ -278,12 +286,12 @@ export const useConfigStore = create<ConfigStateSlice & ConfigActionsSlice>((set
channels.push({
id: 'feishu',
type: 'feishu',
label: 'Feishu',
label: '飞书 (Feishu)',
status: feishu?.configured ? 'active' : 'inactive',
accounts: feishu?.accounts || 0,
});
} catch {
channels.push({ id: 'feishu', type: 'feishu', label: 'Feishu', status: 'inactive' });
channels.push({ id: 'feishu', type: 'feishu', label: '飞书 (Feishu)', status: 'inactive' });
}
set({ channels });

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,7 @@
*
* The coordinator:
* 1. Injects the shared client into all stores
* 2. Provides a composite hook that combines all store slices
* 3. Re-exports all individual stores for direct access
* 2. Re-exports all individual stores for direct access
*/
// === Re-export Individual Stores ===
@@ -26,6 +25,12 @@ export type { WorkflowStore, WorkflowStateSlice, WorkflowActionsSlice, Workflow,
export { useConfigStore, setConfigStoreClient } from './configStore';
export type { ConfigStore, ConfigStateSlice, ConfigActionsSlice, QuickConfig, WorkspaceInfo, ChannelInfo, ScheduledTask, SkillInfo } from './configStore';
export { useSecurityStore, setSecurityStoreClient } from './securityStore';
export type { SecurityStore, SecurityStateSlice, SecurityActionsSlice, SecurityLayer, SecurityStatus, AuditLogEntry } from './securityStore';
export { useSessionStore, setSessionStoreClient } from './sessionStore';
export type { SessionStore, SessionStateSlice, SessionActionsSlice, Session, SessionMessage } from './sessionStore';
// === New Stores ===
export { useMemoryGraphStore } from './memoryGraphStore';
export type { MemoryGraphStore, GraphNode, GraphEdge, GraphFilter, GraphLayout } from './memoryGraphStore';
@@ -49,14 +54,15 @@ export type {
SessionOptions,
} from '../components/BrowserHand/templates/types';
// === Composite Store Hook ===
// === Store Initialization ===
import { useMemo } from 'react';
import { useConnectionStore, getClient } from './connectionStore';
import { useAgentStore, setAgentStoreClient } from './agentStore';
import { useHandStore, setHandStoreClient } from './handStore';
import { useWorkflowStore, setWorkflowStoreClient } from './workflowStore';
import { useConfigStore, setConfigStoreClient } from './configStore';
import { getClient } from './connectionStore';
import { setAgentStoreClient } from './agentStore';
import { setHandStoreClient } from './handStore';
import { setWorkflowStoreClient } from './workflowStore';
import { setConfigStoreClient } from './configStore';
import { setSecurityStoreClient } from './securityStore';
import { setSessionStoreClient } from './sessionStore';
/**
* Initialize all stores with the shared client.
@@ -70,207 +76,8 @@ export function initializeStores(): void {
setHandStoreClient(client);
setWorkflowStoreClient(client);
setConfigStoreClient(client);
}
/**
* Hook that provides a composite view of all stores.
* Use this for components that need access to multiple store slices.
*
* For components that only need specific slices, import the individual
* store hooks directly (e.g., useConnectionStore, useAgentStore).
*/
export function useCompositeStore() {
// Subscribe to all stores
const connectionState = useConnectionStore((s) => s.connectionState);
const gatewayVersion = useConnectionStore((s) => s.gatewayVersion);
const connectionError = useConnectionStore((s) => s.error);
const logs = useConnectionStore((s) => s.logs);
const localGateway = useConnectionStore((s) => s.localGateway);
const localGatewayBusy = useConnectionStore((s) => s.localGatewayBusy);
const isLoading = useConnectionStore((s) => s.isLoading);
const client = useConnectionStore((s) => s.client);
const clones = useAgentStore((s) => s.clones);
const usageStats = useAgentStore((s) => s.usageStats);
const pluginStatus = useAgentStore((s) => s.pluginStatus);
const hands = useHandStore((s) => s.hands);
const handRuns = useHandStore((s) => s.handRuns);
const triggers = useHandStore((s) => s.triggers);
const approvals = useHandStore((s) => s.approvals);
const workflows = useWorkflowStore((s) => s.workflows);
const workflowRuns = useWorkflowStore((s) => s.workflowRuns);
const quickConfig = useConfigStore((s) => s.quickConfig);
const workspaceInfo = useConfigStore((s) => s.workspaceInfo);
const channels = useConfigStore((s) => s.channels);
const scheduledTasks = useConfigStore((s) => s.scheduledTasks);
const skillsCatalog = useConfigStore((s) => s.skillsCatalog);
const models = useConfigStore((s) => s.models);
const modelsLoading = useConfigStore((s) => s.modelsLoading);
const modelsError = useConfigStore((s) => s.modelsError);
// Get all actions
const connect = useConnectionStore((s) => s.connect);
const disconnect = useConnectionStore((s) => s.disconnect);
const clearLogs = useConnectionStore((s) => s.clearLogs);
const refreshLocalGateway = useConnectionStore((s) => s.refreshLocalGateway);
const startLocalGateway = useConnectionStore((s) => s.startLocalGateway);
const stopLocalGateway = useConnectionStore((s) => s.stopLocalGateway);
const restartLocalGateway = useConnectionStore((s) => s.restartLocalGateway);
const loadClones = useAgentStore((s) => s.loadClones);
const createClone = useAgentStore((s) => s.createClone);
const updateClone = useAgentStore((s) => s.updateClone);
const deleteClone = useAgentStore((s) => s.deleteClone);
const loadUsageStats = useAgentStore((s) => s.loadUsageStats);
const loadPluginStatus = useAgentStore((s) => s.loadPluginStatus);
const loadHands = useHandStore((s) => s.loadHands);
const getHandDetails = useHandStore((s) => s.getHandDetails);
const triggerHand = useHandStore((s) => s.triggerHand);
const loadHandRuns = useHandStore((s) => s.loadHandRuns);
const loadTriggers = useHandStore((s) => s.loadTriggers);
const createTrigger = useHandStore((s) => s.createTrigger);
const deleteTrigger = useHandStore((s) => s.deleteTrigger);
const loadApprovals = useHandStore((s) => s.loadApprovals);
const respondToApproval = useHandStore((s) => s.respondToApproval);
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
const getWorkflow = useWorkflowStore((s) => s.getWorkflow);
const createWorkflow = useWorkflowStore((s) => s.createWorkflow);
const updateWorkflow = useWorkflowStore((s) => s.updateWorkflow);
const deleteWorkflow = useWorkflowStore((s) => s.deleteWorkflow);
const triggerWorkflow = useWorkflowStore((s) => s.triggerWorkflow);
const loadWorkflowRuns = useWorkflowStore((s) => s.loadWorkflowRuns);
const loadQuickConfig = useConfigStore((s) => s.loadQuickConfig);
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
const loadWorkspaceInfo = useConfigStore((s) => s.loadWorkspaceInfo);
const loadChannels = useConfigStore((s) => s.loadChannels);
const getChannel = useConfigStore((s) => s.getChannel);
const createChannel = useConfigStore((s) => s.createChannel);
const updateChannel = useConfigStore((s) => s.updateChannel);
const deleteChannel = useConfigStore((s) => s.deleteChannel);
const loadScheduledTasks = useConfigStore((s) => s.loadScheduledTasks);
const createScheduledTask = useConfigStore((s) => s.createScheduledTask);
const loadSkillsCatalog = useConfigStore((s) => s.loadSkillsCatalog);
const getSkill = useConfigStore((s) => s.getSkill);
const createSkill = useConfigStore((s) => s.createSkill);
const updateSkill = useConfigStore((s) => s.updateSkill);
const deleteSkill = useConfigStore((s) => s.deleteSkill);
const loadModels = useConfigStore((s) => s.loadModels);
// Memoize the composite store to prevent unnecessary re-renders
return useMemo(() => ({
// Connection state
connectionState,
gatewayVersion,
error: connectionError,
logs,
localGateway,
localGatewayBusy,
isLoading,
client,
// Agent state
clones,
usageStats,
pluginStatus,
// Hand state
hands,
handRuns,
triggers,
approvals,
// Workflow state
workflows,
workflowRuns,
// Config state
quickConfig,
workspaceInfo,
channels,
scheduledTasks,
skillsCatalog,
models,
modelsLoading,
modelsError,
// Connection actions
connect,
disconnect,
clearLogs,
refreshLocalGateway,
startLocalGateway,
stopLocalGateway,
restartLocalGateway,
// Agent actions
loadClones,
createClone,
updateClone,
deleteClone,
loadUsageStats,
loadPluginStatus,
// Hand actions
loadHands,
getHandDetails,
triggerHand,
loadHandRuns,
loadTriggers,
createTrigger,
deleteTrigger,
loadApprovals,
respondToApproval,
// Workflow actions
loadWorkflows,
getWorkflow,
createWorkflow,
updateWorkflow,
deleteWorkflow,
triggerWorkflow,
loadWorkflowRuns,
// Config actions
loadQuickConfig,
saveQuickConfig,
loadWorkspaceInfo,
loadChannels,
getChannel,
createChannel,
updateChannel,
deleteChannel,
loadScheduledTasks,
createScheduledTask,
loadSkillsCatalog,
getSkill,
createSkill,
updateSkill,
deleteSkill,
loadModels,
// Legacy sendMessage (delegates to client)
sendMessage: async (message: string, sessionKey?: string) => {
return client.chat(message, { sessionKey });
},
}), [
connectionState, gatewayVersion, connectionError, logs, localGateway, localGatewayBusy, isLoading, client,
clones, usageStats, pluginStatus,
hands, handRuns, triggers, approvals,
workflows, workflowRuns,
quickConfig, workspaceInfo, channels, scheduledTasks, skillsCatalog, models, modelsLoading, modelsError,
connect, disconnect, clearLogs, refreshLocalGateway, startLocalGateway, stopLocalGateway, restartLocalGateway,
loadClones, createClone, updateClone, deleteClone, loadUsageStats, loadPluginStatus,
loadHands, getHandDetails, triggerHand, loadHandRuns, loadTriggers, createTrigger, deleteTrigger, loadApprovals, respondToApproval,
loadWorkflows, getWorkflow, createWorkflow, updateWorkflow, deleteWorkflow, triggerWorkflow, loadWorkflowRuns,
loadQuickConfig, saveQuickConfig, loadWorkspaceInfo, loadChannels, getChannel, createChannel, updateChannel, deleteChannel,
loadScheduledTasks, createScheduledTask, loadSkillsCatalog, getSkill, createSkill, updateSkill, deleteSkill, loadModels,
]);
setSecurityStoreClient(client);
setSessionStoreClient(client);
}
/**

View File

@@ -0,0 +1,141 @@
/**
* securityStore.ts - Security Status and Audit Log Management
*
* Extracted from gatewayStore.ts for Store Refactoring.
* Manages OpenFang security layers, security status, and audit logs.
*/
import { create } from 'zustand';
import type { GatewayClient } from '../lib/gateway-client';
// === Types ===
export interface SecurityLayer {
name: string;
enabled: boolean;
description?: string;
}
export interface SecurityStatus {
layers: SecurityLayer[];
enabledCount: number;
totalCount: number;
securityLevel: 'critical' | 'high' | 'medium' | 'low';
}
export interface AuditLogEntry {
id: string;
timestamp: string;
action: string;
actor?: string;
result?: 'success' | 'failure';
details?: Record<string, unknown>;
// Merkle hash chain fields (OpenFang)
hash?: string;
previousHash?: string;
}
// === Helpers ===
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
}
// === Client Interface ===
interface SecurityClient {
getSecurityStatus(): Promise<{ layers?: SecurityLayer[] } | null>;
getAuditLogs(opts?: { limit?: number; offset?: number }): Promise<{ logs?: AuditLogEntry[] } | null>;
}
// === Store Interface ===
export interface SecurityStateSlice {
securityStatus: SecurityStatus | null;
securityStatusLoading: boolean;
securityStatusError: string | null;
auditLogs: AuditLogEntry[];
auditLogsLoading: boolean;
}
export interface SecurityActionsSlice {
loadSecurityStatus: () => Promise<void>;
loadAuditLogs: (opts?: { limit?: number; offset?: number }) => Promise<void>;
}
export type SecurityStore = SecurityStateSlice & SecurityActionsSlice & { client: SecurityClient | null };
// === Store Implementation ===
export const useSecurityStore = create<SecurityStore>((set, get) => ({
// Initial state
securityStatus: null,
securityStatusLoading: false,
securityStatusError: null,
auditLogs: [],
auditLogsLoading: false,
client: null,
loadSecurityStatus: async () => {
const client = get().client;
if (!client) return;
set({ securityStatusLoading: true, securityStatusError: null });
try {
const result = await 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',
});
}
},
loadAuditLogs: async (opts?: { limit?: number; offset?: number }) => {
const client = get().client;
if (!client) return;
set({ auditLogsLoading: true });
try {
const result = await client.getAuditLogs(opts);
set({ auditLogs: (result?.logs || []) as AuditLogEntry[], auditLogsLoading: false });
} catch {
set({ auditLogsLoading: false });
/* ignore if audit API not available */
}
},
}));
// === Client Injection ===
function createSecurityClientFromGateway(client: GatewayClient): SecurityClient {
return {
getSecurityStatus: () => client.getSecurityStatus() as Promise<{ layers?: SecurityLayer[] } | null>,
getAuditLogs: (opts) => client.getAuditLogs(opts) as Promise<{ logs?: AuditLogEntry[] } | null>,
};
}
export function setSecurityStoreClient(client: unknown): void {
const securityClient = createSecurityClientFromGateway(client as GatewayClient);
useSecurityStore.setState({ client: securityClient });
}

View File

@@ -0,0 +1,228 @@
/**
* sessionStore.ts - Session Management Store
*
* Extracted from gatewayStore.ts for Store Refactoring.
* Manages Gateway sessions and session messages.
*/
import { create } from 'zustand';
import type { GatewayClient } from '../lib/gateway-client';
// === Types ===
export interface Session {
id: string;
agentId: string;
createdAt: string;
updatedAt?: string;
messageCount?: number;
status?: 'active' | 'archived' | 'expired';
metadata?: Record<string, unknown>;
}
export interface SessionMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
createdAt: string;
tokens?: { input?: number; output?: number };
}
// === Raw API Response Types ===
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;
metadata?: Record<string, unknown>;
}
interface RawSessionMessage {
id?: string;
messageId?: string;
message_id?: string;
role?: string;
content?: string;
createdAt?: string;
created_at?: string;
metadata?: Record<string, unknown>;
tokens?: { input?: number; output?: number };
}
// === Client Interface ===
interface SessionClient {
listSessions(opts?: { limit?: number; offset?: number }): Promise<{ sessions?: RawSession[] } | null>;
getSession(sessionId: string): Promise<Record<string, unknown> | null>;
createSession(params: { agent_id: string; metadata?: Record<string, unknown> }): Promise<Record<string, unknown> | null>;
deleteSession(sessionId: string): Promise<void>;
getSessionMessages(sessionId: string, opts?: { limit?: number; offset?: number }): Promise<{ messages?: RawSessionMessage[] } | null>;
}
// === Store Interface ===
export interface SessionStateSlice {
sessions: Session[];
sessionMessages: Record<string, SessionMessage[]>;
isLoading: boolean;
error: string | null;
}
export interface SessionActionsSlice {
loadSessions: (opts?: { limit?: number; offset?: number }) => Promise<void>;
getSession: (sessionId: string) => Promise<Session | undefined>;
createSession: (agentId: string, metadata?: Record<string, unknown>) => Promise<Session | undefined>;
deleteSession: (sessionId: string) => Promise<void>;
loadSessionMessages: (sessionId: string, opts?: { limit?: number; offset?: number }) => Promise<SessionMessage[]>;
}
export type SessionStore = SessionStateSlice & SessionActionsSlice & { client: SessionClient | null };
// === Store Implementation ===
export const useSessionStore = create<SessionStore>((set, get) => ({
// Initial state
sessions: [],
sessionMessages: {},
isLoading: false,
error: null,
client: null,
loadSessions: async (opts?: { limit?: number; offset?: number }) => {
const client = get().client;
if (!client) return;
try {
const result = await client.listSessions(opts);
const sessions: Session[] = (result?.sessions || [])
.filter((s: RawSession) => s.id || s.session_id)
.map((s: RawSession) => ({
id: s.id || s.session_id || '',
agentId: s.agent_id || s.agentId || '',
createdAt: s.created_at || s.createdAt || new Date().toISOString(),
updatedAt: s.updated_at || s.updatedAt,
messageCount: s.message_count || s.messageCount,
status: s.status as Session['status'],
metadata: s.metadata,
}));
set({ sessions });
} catch {
/* ignore if sessions API not available */
}
},
getSession: async (sessionId: string) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.getSession(sessionId);
if (!result) return undefined;
const session: Session = {
id: result.id as string,
agentId: result.agent_id as string,
createdAt: result.created_at as string,
updatedAt: result.updated_at as string | undefined,
messageCount: result.message_count as number | undefined,
status: result.status as Session['status'],
metadata: result.metadata as Record<string, unknown> | undefined,
};
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<string, unknown>) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.createSession({ agent_id: agentId, metadata });
if (!result) return undefined;
const session: Session = {
id: result.id as string,
agentId: result.agent_id as string,
createdAt: result.created_at as string,
status: 'active',
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) => {
const client = get().client;
if (!client) return;
try {
await 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 }) => {
const client = get().client;
if (!client) return [];
try {
const result = await client.getSessionMessages(sessionId, opts);
const messages: SessionMessage[] = (result?.messages || []).map((m: RawSessionMessage) => ({
id: m.id || m.message_id || '',
role: (m.role || 'user') as 'user' | 'assistant' | 'system',
content: m.content || '',
createdAt: m.created_at || m.createdAt || new Date().toISOString(),
tokens: m.tokens,
}));
set(state => ({
sessionMessages: { ...state.sessionMessages, [sessionId]: messages },
}));
return messages;
} catch {
return [];
}
},
}));
// === Client Injection ===
function createSessionClientFromGateway(client: GatewayClient): SessionClient {
return {
listSessions: (opts) => client.listSessions(opts),
getSession: (sessionId) => client.getSession(sessionId),
createSession: (params) => client.createSession(params),
deleteSession: async (sessionId) => { await client.deleteSession(sessionId); },
getSessionMessages: (sessionId, opts) => client.getSessionMessages(sessionId, opts),
};
}
export function setSessionStoreClient(client: unknown): void {
const sessionClient = createSessionClientFromGateway(client as GatewayClient);
useSessionStore.setState({ client: sessionClient });
}

View File

@@ -24,8 +24,6 @@ import type {
ReviewFeedback,
TaskDeliverable,
} from '../types/team';
import { parseJsonOrDefault } from '../lib/json-utils';
// === Store State ===
interface TeamStoreState {

View File

@@ -1,7 +1,23 @@
import { create } from 'zustand';
import { Workflow, WorkflowRun } from './gatewayStore';
import type { GatewayClient } from '../lib/gateway-client';
// === Core Types (previously imported from gatewayStore) ===
export interface Workflow {
id: string;
name: string;
steps: number;
description?: string;
createdAt?: string;
}
export interface WorkflowRun {
runId: string;
status: string;
step?: string;
result?: unknown;
}
// === Types ===
interface RawWorkflowRun {
@@ -256,8 +272,7 @@ export const useWorkflowStore = create<WorkflowStateSlice & WorkflowActionsSlice
},
}));
// Re-export types from gatewayStore for convenience
export type { Workflow, WorkflowRun };
// Types are now defined locally in this file (no longer imported from gatewayStore)
// === Client Injection ===