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

@@ -8,11 +8,8 @@
*/
import { useState, useEffect, useCallback } from 'react';
import {
useGatewayStore,
type Approval,
type ApprovalStatus,
} from '../store/gatewayStore';
import { useHandStore } from '../store/handStore';
import type { Approval, ApprovalStatus } from '../store/handStore';
import {
CheckCircle,
XCircle,
@@ -297,8 +294,10 @@ function EmptyState({ filter }: { filter: FilterStatus }) {
// === Main ApprovalsPanel Component ===
export function ApprovalsPanel() {
const { approvals, loadApprovals, respondToApproval, isLoading } =
useGatewayStore();
const approvals = useHandStore((s) => s.approvals);
const loadApprovals = useHandStore((s) => s.loadApprovals);
const respondToApproval = useHandStore((s) => s.respondToApproval);
const isLoading = useHandStore((s) => s.isLoading);
const [filter, setFilter] = useState<FilterStatus>('all');
const [processingId, setProcessingId] = useState<string | null>(null);

View File

@@ -1,12 +1,18 @@
import { useState, useEffect } from 'react';
import { useGatewayStore } from '../store/gatewayStore';
import { useAgentStore } from '../store/agentStore';
import { useConnectionStore } from '../store/connectionStore';
import { useConfigStore } from '../store/configStore';
import { toChatAgent, useChatStore } from '../store/chatStore';
import { Bot, Plus, X, Globe, Cat, Search, BarChart2, Sparkles } from 'lucide-react';
import { AgentOnboardingWizard } from './AgentOnboardingWizard';
import type { Clone } from '../store/agentStore';
export function CloneManager() {
const { clones, loadClones, deleteClone, connectionState, quickConfig } = useGatewayStore();
const clones = useAgentStore((s) => s.clones);
const loadClones = useAgentStore((s) => s.loadClones);
const deleteClone = useAgentStore((s) => s.deleteClone);
const connectionState = useConnectionStore((s) => s.connectionState);
const quickConfig = useConfigStore((s) => s.quickConfig);
const { agents, currentAgent, setCurrentAgent } = useChatStore();
const [showWizard, setShowWizard] = useState(false);

View File

@@ -6,7 +6,7 @@
*/
import { useEffect } from 'react';
import { useGatewayStore, type Hand } from '../store/gatewayStore';
import { useHandStore, type Hand } from '../store/handStore';
import { Zap, Loader2, RefreshCw, CheckCircle, XCircle, AlertTriangle } from 'lucide-react';
interface HandListProps {
@@ -42,7 +42,9 @@ const STATUS_LABELS: Record<Hand['status'], string> = {
};
export function HandList({ selectedHandId, onSelectHand }: HandListProps) {
const { hands, loadHands, isLoading } = useGatewayStore();
const hands = useHandStore((s) => s.hands);
const loadHands = useHandStore((s) => s.loadHands);
const isLoading = useHandStore((s) => s.isLoading);
useEffect(() => {
loadHands();

View File

@@ -6,7 +6,7 @@
*/
import { useState, useEffect, useCallback } from 'react';
import { useGatewayStore, type Hand, type HandRun } from '../store/gatewayStore';
import { useHandStore, type Hand, type HandRun } from '../store/handStore';
import {
Zap,
Loader2,
@@ -39,7 +39,12 @@ const RUN_STATUS_CONFIG: Record<string, { label: string; className: string; icon
};
export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) {
const { hands, handRuns, loadHands, loadHandRuns, triggerHand, isLoading } = useGatewayStore();
const hands = useHandStore((s) => s.hands);
const handRuns = useHandStore((s) => s.handRuns);
const loadHands = useHandStore((s) => s.loadHands);
const loadHandRuns = useHandStore((s) => s.loadHandRuns);
const triggerHand = useHandStore((s) => s.triggerHand);
const isLoading = useHandStore((s) => s.isLoading);
const { toast } = useToast();
const [selectedHand, setSelectedHand] = useState<Hand | null>(null);
const [isActivating, setIsActivating] = useState(false);
@@ -103,7 +108,7 @@ export function HandTaskPanel({ handId, onBack }: HandTaskPanelProps) {
]);
} else {
// Check for specific error in store
const storeError = useGatewayStore.getState().error;
const storeError = useHandStore.getState().error;
if (storeError?.includes('already active')) {
toast(`Hand "${selectedHand.name}" 已在运行中`, 'warning');
} else {

View File

@@ -1,9 +1,10 @@
import { FileText, Globe } from 'lucide-react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useConfigStore } from '../../store/configStore';
import { silentErrorHandler } from '../../lib/error-utils';
export function MCPServices() {
const { quickConfig, saveQuickConfig } = useGatewayStore();
const quickConfig = useConfigStore((s) => s.quickConfig);
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
const services = quickConfig.mcpServices || [];

View File

@@ -1,10 +1,13 @@
import { useEffect } from 'react';
import { ExternalLink } from 'lucide-react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useConfigStore } from '../../store/configStore';
import { silentErrorHandler } from '../../lib/error-utils';
export function Privacy() {
const { quickConfig, workspaceInfo, loadWorkspaceInfo, saveQuickConfig } = useGatewayStore();
const quickConfig = useConfigStore((s) => s.quickConfig);
const workspaceInfo = useConfigStore((s) => s.workspaceInfo);
const loadWorkspaceInfo = useConfigStore((s) => s.loadWorkspaceInfo);
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
useEffect(() => {
loadWorkspaceInfo().catch(silentErrorHandler('Privacy'));

View File

@@ -1,9 +1,12 @@
import { useEffect, useState } from 'react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useAgentStore } from '../../store/agentStore';
import { useConnectionStore } from '../../store/connectionStore';
import { BarChart3, TrendingUp, Clock, Zap } from 'lucide-react';
export function UsageStats() {
const { usageStats, loadUsageStats, connectionState } = useGatewayStore();
const usageStats = useAgentStore((s) => s.usageStats);
const loadUsageStats = useAgentStore((s) => s.loadUsageStats);
const connectionState = useConnectionStore((s) => s.connectionState);
const [timeRange, setTimeRange] = useState<'7d' | '30d' | 'all'>('7d');
useEffect(() => {

View File

@@ -1,14 +1,12 @@
import { useEffect, useState } from 'react';
import { useGatewayStore } from '../../store/gatewayStore';
import { useConfigStore } from '../../store/configStore';
import { silentErrorHandler } from '../../lib/error-utils';
export function Workspace() {
const {
quickConfig,
workspaceInfo,
loadWorkspaceInfo,
saveQuickConfig,
} = useGatewayStore();
const quickConfig = useConfigStore((s) => s.quickConfig);
const workspaceInfo = useConfigStore((s) => s.workspaceInfo);
const loadWorkspaceInfo = useConfigStore((s) => s.loadWorkspaceInfo);
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
const [projectDir, setProjectDir] = useState('~/.openfang/zclaw-workspace');
useEffect(() => {

View File

@@ -6,7 +6,7 @@ import {
} from 'lucide-react';
import { CloneManager } from './CloneManager';
import { TeamList } from './TeamList';
import { useGatewayStore } from '../store/gatewayStore';
import { useConfigStore } from '../store/configStore';
import { containerVariants, defaultTransition } from '../lib/animations';
export type MainViewType = 'chat' | 'automation' | 'team' | 'swarm' | 'skills';
@@ -44,7 +44,7 @@ export function Sidebar({
}: SidebarProps) {
const [activeTab, setActiveTab] = useState<Tab>('clones');
const [searchQuery, setSearchQuery] = useState('');
const userName = useGatewayStore((state) => state.quickConfig.userName) || '用户7141';
const userName = useConfigStore((state) => state.quickConfig?.userName) || '用户7141';
const handleNavClick = (key: Tab, mainView?: MainViewType) => {
setActiveTab(key);

View File

@@ -5,8 +5,8 @@
*/
import { useState, useEffect, useCallback } from 'react';
import { useGatewayStore } from '../store/gatewayStore';
import type { Trigger } from '../store/gatewayStore';
import { useHandStore } from '../store/handStore';
import type { Trigger } from '../store/handStore';
import { CreateTriggerModal } from './CreateTriggerModal';
import {
Zap,
@@ -105,7 +105,11 @@ function TriggerCard({ trigger, onToggle, onDelete, isToggling, isDeleting }: Tr
}
export function TriggersPanel() {
const { triggers, loadTriggers, isLoading, client, deleteTrigger } = useGatewayStore();
const triggers = useHandStore((s) => s.triggers);
const loadTriggers = useHandStore((s) => s.loadTriggers);
const deleteTrigger = useHandStore((s) => s.deleteTrigger);
const isLoading = useHandStore((s) => s.isLoading);
const client = useHandStore((s) => s.client);
const [togglingTrigger, setTogglingTrigger] = useState<string | null>(null);
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);

View File

@@ -8,7 +8,7 @@
*/
import { useState, useEffect, useCallback } from 'react';
import { useGatewayStore, type Workflow, type WorkflowRun } from '../store/gatewayStore';
import { useWorkflowStore, type Workflow, type WorkflowRun } from '../store/workflowStore';
import {
ArrowLeft,
Clock,
@@ -113,7 +113,9 @@ function RunCard({ run, index }: RunCardProps) {
}
export function WorkflowHistory({ workflow, onBack }: WorkflowHistoryProps) {
const { loadWorkflowRuns, cancelWorkflow, isLoading } = useGatewayStore();
const loadWorkflowRuns = useWorkflowStore((s) => s.loadWorkflowRuns);
const cancelWorkflow = useWorkflowStore((s) => s.cancelWorkflow);
const isLoading = useWorkflowStore((s) => s.isLoading);
const [runs, setRuns] = useState<WorkflowRun[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
const [cancellingRunId, setCancellingRunId] = useState<string | null>(null);

View File

@@ -7,8 +7,7 @@
*/
import { useState, useEffect, useCallback } from 'react';
import { useGatewayStore } from '../store/gatewayStore';
import type { Workflow } from '../store/gatewayStore';
import { useWorkflowStore, type Workflow } from '../store/workflowStore';
import { WorkflowEditor } from './WorkflowEditor';
import { WorkflowHistory } from './WorkflowHistory';
import {
@@ -236,7 +235,13 @@ function WorkflowRow({ workflow, onExecute, onEdit, onDelete, onHistory, isExecu
// === Main WorkflowList Component ===
export function WorkflowList() {
const { workflows, loadWorkflows, executeWorkflow, deleteWorkflow, createWorkflow, updateWorkflow, isLoading } = useGatewayStore();
const workflows = useWorkflowStore((s) => s.workflows);
const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows);
const triggerWorkflow = useWorkflowStore((s) => s.triggerWorkflow);
const deleteWorkflow = useWorkflowStore((s) => s.deleteWorkflow);
const createWorkflow = useWorkflowStore((s) => s.createWorkflow);
const updateWorkflow = useWorkflowStore((s) => s.updateWorkflow);
const isLoading = useWorkflowStore((s) => s.isLoading);
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [executingWorkflowId, setExecutingWorkflowId] = useState<string | null>(null);
const [deletingWorkflowId, setDeletingWorkflowId] = useState<string | null>(null);
@@ -254,11 +259,11 @@ export function WorkflowList() {
const handleExecute = useCallback(async (id: string, input?: Record<string, unknown>) => {
setExecutingWorkflowId(id);
try {
await executeWorkflow(id, input);
await triggerWorkflow(id, input);
} finally {
setExecutingWorkflowId(null);
}
}, [executeWorkflow]);
}, [triggerWorkflow]);
const handleExecuteClick = useCallback((workflow: Workflow) => {
setSelectedWorkflow(workflow);