refactor(phase-11): extract specialized stores from gatewayStore

Decompose monolithic gatewayStore.ts (1660 lines) into focused stores:

- connectionStore.ts (444 lines) - WebSocket, auth, local gateway
- agentStore.ts (256 lines) - Clones, usage stats, plugins
- handStore.ts (498 lines) - Hands, triggers, approvals
- workflowStore.ts (255 lines) - Workflows, runs
- configStore.ts (537 lines) - QuickConfig, channels, skills

Each store uses client injection pattern for loose coupling.
Coordinator layer to be added in next commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-15 20:17:17 +08:00
parent 6a66ce159d
commit f22b1a2095
6 changed files with 2006 additions and 1 deletions

View File

@@ -0,0 +1,255 @@
import { create } from 'zustand';
import { Workflow, WorkflowRun } from './gatewayStore';
// === Types ===
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;
result?: unknown;
step?: string;
}
export interface WorkflowStep {
handName: string;
name?: string;
params?: Record<string, unknown>;
condition?: string;
}
export interface CreateWorkflowInput {
name: string;
description?: string;
steps: WorkflowStep[];
}
export interface UpdateWorkflowInput {
name?: string;
description?: string;
steps?: WorkflowStep[];
}
// Extended WorkflowRun with additional fields from API
export interface ExtendedWorkflowRun extends WorkflowRun {
startedAt?: string;
completedAt?: string;
error?: string;
}
// === Client Interface ===
interface WorkflowClient {
listWorkflows(): Promise<{ workflows: { id: string; name: string; steps: number; description?: string; createdAt?: string }[] } | null>;
createWorkflow(workflow: CreateWorkflowInput): Promise<{ id: string; name: string } | null>;
updateWorkflow(id: string, updates: UpdateWorkflowInput): Promise<{ id: string; name: string } | null>;
deleteWorkflow(id: string): Promise<{ status: string }>;
executeWorkflow(id: string, input?: Record<string, unknown>): Promise<{ runId: string; status: string } | null>;
cancelWorkflow(workflowId: string, runId: string): Promise<{ status: string }>;
listWorkflowRuns(workflowId: string, opts?: { limit?: number; offset?: number }): Promise<{ runs: RawWorkflowRun[] } | null>;
}
// === Store State ===
interface WorkflowState {
workflows: Workflow[];
workflowRuns: Record<string, ExtendedWorkflowRun[]>;
isLoading: boolean;
error: string | null;
client: WorkflowClient;
}
// === Store Actions ===
interface WorkflowActions {
setWorkflowStoreClient: (client: WorkflowClient) => void;
loadWorkflows: () => Promise<void>;
getWorkflow: (id: string) => Workflow | undefined;
createWorkflow: (workflow: CreateWorkflowInput) => Promise<Workflow | undefined>;
updateWorkflow: (id: string, updates: UpdateWorkflowInput) => Promise<Workflow | undefined>;
deleteWorkflow: (id: string) => Promise<void>;
triggerWorkflow: (id: string, input?: Record<string, unknown>) => Promise<{ runId: string; status: string } | undefined>;
cancelWorkflow: (id: string, runId: string) => Promise<void>;
loadWorkflowRuns: (workflowId: string, opts?: { limit?: number; offset?: number }) => Promise<ExtendedWorkflowRun[]>;
clearError: () => void;
reset: () => void;
}
// === Initial State ===
const initialState = {
workflows: [],
workflowRuns: {},
isLoading: false,
error: null,
client: null as unknown as WorkflowClient,
};
// === Store ===
export const useWorkflowStore = create<WorkflowState & WorkflowActions>((set, get) => ({
...initialState,
setWorkflowStoreClient: (client: WorkflowClient) => {
set({ client });
},
loadWorkflows: async () => {
set({ isLoading: true, error: null });
try {
const result = await get().client.listWorkflows();
const workflows: Workflow[] = (result?.workflows || []).map(w => ({
id: w.id,
name: w.name,
steps: w.steps,
description: w.description,
createdAt: w.createdAt,
}));
set({ workflows, isLoading: false });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to load workflows';
set({ error: message, isLoading: false });
}
},
getWorkflow: (id: string) => {
return get().workflows.find(w => w.id === id);
},
createWorkflow: async (workflow: CreateWorkflowInput) => {
set({ error: null });
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) {
const message = err instanceof Error ? err.message : 'Failed to create workflow';
set({ error: message });
return undefined;
}
},
updateWorkflow: async (id: string, updates: UpdateWorkflowInput) => {
set({ error: null });
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) {
const message = err instanceof Error ? err.message : 'Failed to update workflow';
set({ error: message });
return undefined;
}
},
deleteWorkflow: async (id: string) => {
set({ error: null });
try {
await get().client.deleteWorkflow(id);
set(state => ({
workflows: state.workflows.filter(w => w.id !== id),
workflowRuns: (() => {
const { [id]: _, ...rest } = state.workflowRuns;
return rest;
})(),
}));
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to delete workflow';
set({ error: message });
throw err;
}
},
triggerWorkflow: async (id: string, input?: Record<string, unknown>) => {
set({ error: null });
try {
const result = await get().client.executeWorkflow(id, input);
return result ? { runId: result.runId, status: result.status } : undefined;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to trigger workflow';
set({ error: message });
return undefined;
}
},
cancelWorkflow: async (id: string, runId: string) => {
set({ error: null });
try {
await get().client.cancelWorkflow(id, runId);
// Refresh workflows to update status
await get().loadWorkflows();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to cancel workflow';
set({ error: message });
throw err;
}
},
loadWorkflowRuns: async (workflowId: string, opts?: { limit?: number; offset?: number }) => {
try {
const result = await get().client.listWorkflowRuns(workflowId, opts);
const runs: ExtendedWorkflowRun[] = (result?.runs || []).map((r: RawWorkflowRun) => ({
runId: r.runId || r.run_id || r.id || '',
status: r.status || 'unknown',
startedAt: r.startedAt || r.started_at,
completedAt: r.completedAt || r.completed_at,
step: r.currentStep?.toString() || r.current_step?.toString() || r.step,
result: r.result,
error: r.error,
}));
// Store runs by workflow ID
set(state => ({
workflowRuns: { ...state.workflowRuns, [workflowId]: runs },
}));
return runs;
} catch {
return [];
}
},
clearError: () => {
set({ error: null });
},
reset: () => {
set(initialState);
},
}));
// Re-export types from gatewayStore for convenience
export type { Workflow, WorkflowRun };