Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor(loop_guard): 为LoopGuard添加Clone派生 feat(capabilities): 实现CapabilityManager.validate()安全验证 fix(agentStore): 添加token用量追踪 chore: 删除未实现的Predictor/Lead HAND.toml文件 style(Credits): 移除假数据并标注开发中状态 refactor(Skills): 动态加载技能卡片 perf(configStore): 为定时任务添加localStorage降级 docs: 更新功能文档和版本变更记录
497 lines
15 KiB
TypeScript
497 lines
15 KiB
TypeScript
import { create } from 'zustand';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import type { GatewayClient } from '../lib/gateway-client';
|
|
import type { KernelClient } from '../lib/kernel-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 {
|
|
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 WorkflowDetail {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
steps: WorkflowStep[];
|
|
createdAt?: string;
|
|
}
|
|
|
|
export interface WorkflowCreateOptions {
|
|
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>;
|
|
getWorkflow(id: string): Promise<WorkflowDetail | null>;
|
|
createWorkflow(workflow: WorkflowCreateOptions): 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 Slice ===
|
|
|
|
export interface WorkflowStateSlice {
|
|
workflows: Workflow[];
|
|
workflowRuns: Record<string, ExtendedWorkflowRun[]>;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
client: WorkflowClient;
|
|
}
|
|
|
|
// === Store Actions Slice ===
|
|
|
|
export interface WorkflowActionsSlice {
|
|
setWorkflowStoreClient: (client: WorkflowClient) => void;
|
|
loadWorkflows: () => Promise<void>;
|
|
getWorkflow: (id: string) => Workflow | undefined;
|
|
getWorkflowDetail: (id: string) => Promise<WorkflowDetail | undefined>;
|
|
createWorkflow: (workflow: WorkflowCreateOptions) => 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;
|
|
}
|
|
|
|
// === Combined Store Type ===
|
|
|
|
export type WorkflowStore = WorkflowStateSlice & WorkflowActionsSlice;
|
|
|
|
// === Initial State ===
|
|
|
|
const initialState = {
|
|
workflows: [],
|
|
workflowRuns: {},
|
|
isLoading: false,
|
|
error: null,
|
|
client: null as unknown as WorkflowClient,
|
|
};
|
|
|
|
// === Store ===
|
|
|
|
export const useWorkflowStore = create<WorkflowStateSlice & WorkflowActionsSlice>((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);
|
|
},
|
|
|
|
getWorkflowDetail: async (id: string) => {
|
|
try {
|
|
const result = await get().client.getWorkflow(id);
|
|
if (!result) return undefined;
|
|
return {
|
|
id: result.id,
|
|
name: result.name,
|
|
description: result.description,
|
|
steps: Array.isArray(result.steps) ? result.steps : [],
|
|
createdAt: result.createdAt,
|
|
};
|
|
} catch (err: unknown) {
|
|
const message = err instanceof Error ? err.message : 'Failed to load workflow details';
|
|
set({ error: message });
|
|
return undefined;
|
|
}
|
|
},
|
|
|
|
createWorkflow: async (workflow: WorkflowCreateOptions) => {
|
|
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);
|
|
},
|
|
}));
|
|
|
|
// Types are now defined locally in this file (no longer imported from gatewayStore)
|
|
|
|
// === Client Injection ===
|
|
|
|
/**
|
|
* Helper to create a WorkflowClient adapter from a GatewayClient.
|
|
*/
|
|
function createWorkflowClientFromGateway(client: GatewayClient): WorkflowClient {
|
|
return {
|
|
getWorkflow: async (id: string) => {
|
|
const result = await client.getWorkflow(id);
|
|
if (!result) return null;
|
|
return {
|
|
...result,
|
|
steps: result.steps as WorkflowStep[],
|
|
};
|
|
},
|
|
listWorkflows: () => client.listWorkflows(),
|
|
createWorkflow: (workflow) => client.createWorkflow(workflow),
|
|
updateWorkflow: (id, updates) => client.updateWorkflow(id, updates),
|
|
deleteWorkflow: (id) => client.deleteWorkflow(id),
|
|
executeWorkflow: (id, input) => client.executeWorkflow(id, input),
|
|
cancelWorkflow: (workflowId, runId) => client.cancelWorkflow(workflowId, runId),
|
|
listWorkflowRuns: (workflowId, opts) => client.listWorkflowRuns(workflowId, opts),
|
|
};
|
|
}
|
|
|
|
// === Pipeline types (from Tauri backend) ===
|
|
|
|
interface PipelineInfo {
|
|
id: string;
|
|
displayName: string;
|
|
description: string;
|
|
category: string;
|
|
industry: string;
|
|
tags: string[];
|
|
icon: string;
|
|
version: string;
|
|
author: string;
|
|
inputs: Array<{
|
|
name: string;
|
|
inputType: string;
|
|
required: boolean;
|
|
label: string;
|
|
placeholder?: string;
|
|
default?: unknown;
|
|
options: string[];
|
|
}>;
|
|
}
|
|
|
|
interface RunPipelineResponse {
|
|
runId: string;
|
|
pipelineId: string;
|
|
status: string;
|
|
}
|
|
|
|
interface PipelineRunResponse {
|
|
runId: string;
|
|
pipelineId: string;
|
|
status: string;
|
|
currentStep?: string;
|
|
percentage: number;
|
|
message: string;
|
|
outputs?: unknown;
|
|
error?: string;
|
|
startedAt: string;
|
|
endedAt?: string;
|
|
}
|
|
|
|
/**
|
|
* Helper to create a WorkflowClient adapter from a KernelClient.
|
|
* Uses direct Tauri invoke() calls to pipeline_commands since KernelClient
|
|
* does not have workflow methods (workflows in Tauri mode are pipelines).
|
|
*/
|
|
function createWorkflowClientFromKernel(_client: KernelClient): WorkflowClient {
|
|
return {
|
|
listWorkflows: async () => {
|
|
try {
|
|
const pipelines = await invoke<PipelineInfo[]>('pipeline_list', {});
|
|
if (!pipelines) return null;
|
|
return {
|
|
workflows: pipelines.map((p) => ({
|
|
id: p.id,
|
|
name: p.displayName || p.id,
|
|
steps: p.inputs.length,
|
|
description: p.description,
|
|
createdAt: undefined,
|
|
})),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
getWorkflow: async (id: string) => {
|
|
try {
|
|
const pipeline = await invoke<PipelineInfo>('pipeline_get', { pipelineId: id });
|
|
return {
|
|
id: pipeline.id,
|
|
name: pipeline.displayName || pipeline.id,
|
|
description: pipeline.description,
|
|
steps: pipeline.inputs.map((input) => ({
|
|
handName: input.inputType,
|
|
name: input.label,
|
|
params: input.default ? { default: input.default } : undefined,
|
|
})),
|
|
createdAt: undefined,
|
|
} satisfies WorkflowDetail;
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
createWorkflow: async () => {
|
|
throw new Error('Workflow creation not supported in KernelClient mode. Pipelines are file-based YAML definitions.');
|
|
},
|
|
updateWorkflow: async () => {
|
|
throw new Error('Workflow update not supported in KernelClient mode. Pipelines are file-based YAML definitions.');
|
|
},
|
|
deleteWorkflow: async () => {
|
|
throw new Error('Workflow deletion not supported in KernelClient mode. Pipelines are file-based YAML definitions.');
|
|
},
|
|
executeWorkflow: async (id: string, input?: Record<string, unknown>) => {
|
|
try {
|
|
const result = await invoke<RunPipelineResponse>('pipeline_run', {
|
|
request: { pipelineId: id, inputs: input || {} },
|
|
});
|
|
return { runId: result.runId, status: result.status };
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
cancelWorkflow: async (_workflowId: string, runId: string) => {
|
|
try {
|
|
await invoke('pipeline_cancel', { runId });
|
|
return { status: 'cancelled' };
|
|
} catch {
|
|
return { status: 'error' };
|
|
}
|
|
},
|
|
listWorkflowRuns: async (workflowId: string) => {
|
|
try {
|
|
const runs = await invoke<PipelineRunResponse[]>('pipeline_runs', {});
|
|
// Filter runs by pipeline ID and map to RawWorkflowRun shape
|
|
const filteredRuns: RawWorkflowRun[] = runs
|
|
.filter((r) => r.pipelineId === workflowId)
|
|
.map((r) => ({
|
|
run_id: r.runId,
|
|
workflow_id: r.pipelineId,
|
|
status: r.status,
|
|
started_at: r.startedAt,
|
|
completed_at: r.endedAt,
|
|
current_step: r.currentStep ? Math.round(r.percentage) : undefined,
|
|
error: r.error,
|
|
result: r.outputs,
|
|
}));
|
|
return { runs: filteredRuns };
|
|
} catch {
|
|
return { runs: [] };
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sets the client for the workflow store.
|
|
* Called by the coordinator during initialization.
|
|
* Detects whether the client is a KernelClient (Tauri) or GatewayClient (browser).
|
|
*/
|
|
export function setWorkflowStoreClient(client: unknown): void {
|
|
let workflowClient: WorkflowClient;
|
|
|
|
// Check if it's a KernelClient (has listHands method, which KernelClient has but GatewayClient doesn't)
|
|
if (client && typeof client === 'object' && 'listHands' in client) {
|
|
workflowClient = createWorkflowClientFromKernel(client as KernelClient);
|
|
} else if (client && typeof client === 'object') {
|
|
// It's a GatewayClient
|
|
workflowClient = createWorkflowClientFromGateway(client as GatewayClient);
|
|
} else {
|
|
// Fallback: return a stub client that gracefully handles all calls
|
|
workflowClient = {
|
|
listWorkflows: async () => null,
|
|
getWorkflow: async () => null,
|
|
createWorkflow: async () => null,
|
|
updateWorkflow: async () => null,
|
|
deleteWorkflow: async () => ({ status: 'error' }),
|
|
executeWorkflow: async () => null,
|
|
cancelWorkflow: async () => ({ status: 'error' }),
|
|
listWorkflowRuns: async () => null,
|
|
};
|
|
}
|
|
|
|
useWorkflowStore.getState().setWorkflowStoreClient(workflowClient);
|
|
}
|