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: 统一Hands系统常量到单个源文件 refactor: 更新Hands中文名称和描述 fix: 修复技能市场在连接状态变化时重新加载 fix: 修复身份变更提案的错误处理逻辑 docs: 更新多个功能文档的验证状态和实现位置 docs: 更新Hands系统文档 test: 添加测试文件验证工作区路径
457 lines
13 KiB
TypeScript
457 lines
13 KiB
TypeScript
/**
|
|
* Workflow Builder Store
|
|
*
|
|
* Zustand store for managing workflow builder state.
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import type {
|
|
WorkflowCanvas,
|
|
WorkflowNode,
|
|
WorkflowEdge,
|
|
WorkflowNodeData,
|
|
WorkflowTemplate,
|
|
ValidationResult,
|
|
NodePaletteItem,
|
|
WorkflowNodeType,
|
|
NodeCategory,
|
|
} from '../lib/workflow-builder/types';
|
|
import { validateCanvas } from '../lib/workflow-builder/yaml-converter';
|
|
|
|
// =============================================================================
|
|
// Store State
|
|
// =============================================================================
|
|
|
|
interface WorkflowBuilderState {
|
|
// Canvas state
|
|
canvas: WorkflowCanvas | null;
|
|
workflows: WorkflowCanvas[];
|
|
|
|
// Selection
|
|
selectedNodeId: string | null;
|
|
selectedEdgeId: string | null;
|
|
|
|
// UI state
|
|
isDragging: boolean;
|
|
isDirty: boolean;
|
|
isPreviewOpen: boolean;
|
|
validation: ValidationResult | null;
|
|
|
|
// Templates
|
|
templates: WorkflowTemplate[];
|
|
|
|
// Available items for palette
|
|
availableSkills: Array<{ id: string; name: string; description: string }>;
|
|
availableHands: Array<{ id: string; name: string; actions: string[] }>;
|
|
|
|
// Actions
|
|
createNewWorkflow: (name: string, description?: string) => void;
|
|
loadWorkflow: (id: string) => void;
|
|
saveWorkflow: () => void;
|
|
deleteWorkflow: (id: string) => void;
|
|
|
|
// Node actions
|
|
addNode: (type: WorkflowNodeType, position: { x: number; y: number }) => void;
|
|
updateNode: (nodeId: string, data: Partial<WorkflowNodeData>) => void;
|
|
deleteNode: (nodeId: string) => void;
|
|
duplicateNode: (nodeId: string) => void;
|
|
|
|
// Edge actions
|
|
addEdge: (source: string, target: string) => void;
|
|
deleteEdge: (edgeId: string) => void;
|
|
|
|
// Selection actions
|
|
selectNode: (nodeId: string | null) => void;
|
|
selectEdge: (edgeId: string | null) => void;
|
|
|
|
// UI actions
|
|
setDragging: (isDragging: boolean) => void;
|
|
setPreviewOpen: (isOpen: boolean) => void;
|
|
validate: () => ValidationResult;
|
|
|
|
// Data loading
|
|
setAvailableSkills: (skills: Array<{ id: string; name: string; description: string }>) => void;
|
|
setAvailableHands: (hands: Array<{ id: string; name: string; actions: string[] }>) => void;
|
|
|
|
// Canvas metadata
|
|
updateCanvasMetadata: (updates: Partial<Pick<WorkflowCanvas, 'name' | 'description' | 'category'>>) => void;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Default Node Data
|
|
// =============================================================================
|
|
|
|
function getDefaultNodeData(type: WorkflowNodeType, _id: string): WorkflowNodeData {
|
|
const base = { label: type.charAt(0).toUpperCase() + type.slice(1) };
|
|
|
|
switch (type) {
|
|
case 'input':
|
|
return { type: 'input', ...base, variableName: 'input', schema: undefined };
|
|
case 'llm':
|
|
return { type: 'llm', ...base, template: '', isTemplateFile: false, jsonMode: false };
|
|
case 'skill':
|
|
return { type: 'skill', ...base, skillId: '', inputMappings: {} };
|
|
case 'hand':
|
|
return { type: 'hand', ...base, handId: '', action: '', params: {} };
|
|
case 'orchestration':
|
|
return { type: 'orchestration', ...base, inputMappings: {} };
|
|
case 'condition':
|
|
return { type: 'condition', ...base, condition: '', branches: [{ when: '', label: 'Branch 1' }], hasDefault: true };
|
|
case 'parallel':
|
|
return { type: 'parallel', ...base, each: '${inputs.items}', maxWorkers: 4 };
|
|
case 'loop':
|
|
return { type: 'loop', ...base, each: '${inputs.items}', itemVar: 'item', indexVar: 'index' };
|
|
case 'export':
|
|
return { type: 'export', ...base, formats: ['json'] };
|
|
case 'http':
|
|
return { type: 'http', ...base, url: '', method: 'GET', headers: {} };
|
|
case 'setVar':
|
|
return { type: 'setVar', ...base, variableName: 'result', value: '' };
|
|
case 'delay':
|
|
return { type: 'delay', ...base, ms: 1000 };
|
|
default:
|
|
throw new Error(`Unknown node type: ${type}`);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Store Implementation
|
|
// =============================================================================
|
|
|
|
export const useWorkflowBuilderStore = create<WorkflowBuilderState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
// Initial state
|
|
canvas: null,
|
|
workflows: [],
|
|
selectedNodeId: null,
|
|
selectedEdgeId: null,
|
|
isDragging: false,
|
|
isDirty: false,
|
|
isPreviewOpen: false,
|
|
validation: null,
|
|
templates: [],
|
|
availableSkills: [],
|
|
availableHands: [],
|
|
|
|
// Workflow actions
|
|
createNewWorkflow: (name, description) => {
|
|
const canvas: WorkflowCanvas = {
|
|
id: `workflow_${Date.now()}`,
|
|
name,
|
|
description,
|
|
category: 'custom',
|
|
nodes: [],
|
|
edges: [],
|
|
viewport: { x: 0, y: 0, zoom: 1 },
|
|
metadata: {
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
tags: [],
|
|
version: '1.0.0',
|
|
},
|
|
};
|
|
set({ canvas, isDirty: false, selectedNodeId: null, selectedEdgeId: null, validation: null });
|
|
},
|
|
|
|
loadWorkflow: (id) => {
|
|
const workflow = get().workflows.find(w => w.id === id);
|
|
if (workflow) {
|
|
set({ canvas: workflow, isDirty: false, selectedNodeId: null, selectedEdgeId: null });
|
|
}
|
|
},
|
|
|
|
saveWorkflow: () => {
|
|
const { canvas, workflows } = get();
|
|
if (!canvas) return;
|
|
|
|
const updatedCanvas: WorkflowCanvas = {
|
|
...canvas,
|
|
metadata: {
|
|
...canvas.metadata,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
};
|
|
|
|
const existingIndex = workflows.findIndex(w => w.id === canvas.id);
|
|
let updatedWorkflows: WorkflowCanvas[];
|
|
|
|
if (existingIndex >= 0) {
|
|
updatedWorkflows = [...workflows];
|
|
updatedWorkflows[existingIndex] = updatedCanvas;
|
|
} else {
|
|
updatedWorkflows = [...workflows, updatedCanvas];
|
|
}
|
|
|
|
set({ workflows: updatedWorkflows, canvas: updatedCanvas, isDirty: false });
|
|
},
|
|
|
|
deleteWorkflow: (id) => {
|
|
set(state => ({
|
|
workflows: state.workflows.filter(w => w.id !== id),
|
|
canvas: state.canvas?.id === id ? null : state.canvas,
|
|
}));
|
|
},
|
|
|
|
// Node actions
|
|
addNode: (type, position) => {
|
|
const { canvas } = get();
|
|
if (!canvas) return;
|
|
|
|
const id = `${type}_${Date.now()}`;
|
|
const node: WorkflowNode = {
|
|
id,
|
|
type,
|
|
position,
|
|
data: getDefaultNodeData(type, id),
|
|
};
|
|
|
|
set({
|
|
canvas: { ...canvas, nodes: [...canvas.nodes, node] },
|
|
isDirty: true,
|
|
selectedNodeId: id,
|
|
});
|
|
},
|
|
|
|
updateNode: (nodeId, data) => {
|
|
const { canvas } = get();
|
|
if (!canvas) return;
|
|
|
|
const updatedNodes = canvas.nodes.map(node =>
|
|
node.id === nodeId
|
|
? { ...node, data: { ...node.data, ...data } as WorkflowNodeData }
|
|
: node
|
|
);
|
|
|
|
set({ canvas: { ...canvas, nodes: updatedNodes }, isDirty: true });
|
|
},
|
|
|
|
deleteNode: (nodeId) => {
|
|
const { canvas } = get();
|
|
if (!canvas) return;
|
|
|
|
const updatedNodes = canvas.nodes.filter(n => n.id !== nodeId);
|
|
const updatedEdges = canvas.edges.filter(e => e.source !== nodeId && e.target !== nodeId);
|
|
|
|
set({
|
|
canvas: { ...canvas, nodes: updatedNodes, edges: updatedEdges },
|
|
isDirty: true,
|
|
selectedNodeId: null,
|
|
});
|
|
},
|
|
|
|
duplicateNode: (nodeId) => {
|
|
const { canvas } = get();
|
|
if (!canvas) return;
|
|
|
|
const node = canvas.nodes.find(n => n.id === nodeId);
|
|
if (!node) return;
|
|
|
|
const newId = `${node.type}_${Date.now()}`;
|
|
const newNode: WorkflowNode = {
|
|
...node,
|
|
id: newId,
|
|
position: {
|
|
x: node.position.x + 50,
|
|
y: node.position.y + 50,
|
|
},
|
|
data: { ...node.data, label: `${node.data.label} (copy)` } as WorkflowNodeData,
|
|
};
|
|
|
|
set({
|
|
canvas: { ...canvas, nodes: [...canvas.nodes, newNode] },
|
|
isDirty: true,
|
|
selectedNodeId: newId,
|
|
});
|
|
},
|
|
|
|
// Edge actions
|
|
addEdge: (source, target) => {
|
|
const { canvas } = get();
|
|
if (!canvas) return;
|
|
|
|
// Check if edge already exists
|
|
const exists = canvas.edges.some(e => e.source === source && e.target === target);
|
|
if (exists) return;
|
|
|
|
const edge: WorkflowEdge = {
|
|
id: `edge_${source}_${target}`,
|
|
source,
|
|
target,
|
|
type: 'default',
|
|
};
|
|
|
|
set({ canvas: { ...canvas, edges: [...canvas.edges, edge] }, isDirty: true });
|
|
},
|
|
|
|
deleteEdge: (edgeId) => {
|
|
const { canvas } = get();
|
|
if (!canvas) return;
|
|
|
|
set({
|
|
canvas: { ...canvas, edges: canvas.edges.filter(e => e.id !== edgeId) },
|
|
isDirty: true,
|
|
});
|
|
},
|
|
|
|
// Selection actions
|
|
selectNode: (nodeId) => set({ selectedNodeId: nodeId, selectedEdgeId: null }),
|
|
selectEdge: (edgeId) => set({ selectedEdgeId: edgeId, selectedNodeId: null }),
|
|
|
|
// UI actions
|
|
setDragging: (isDragging) => set({ isDragging }),
|
|
setPreviewOpen: (isOpen) => set({ isPreviewOpen: isOpen }),
|
|
|
|
validate: () => {
|
|
const { canvas } = get();
|
|
if (!canvas) {
|
|
return { valid: false, errors: [{ nodeId: 'canvas', message: 'No workflow loaded', severity: 'error' as const }], warnings: [] };
|
|
}
|
|
const result = validateCanvas(canvas);
|
|
set({ validation: result });
|
|
return result;
|
|
},
|
|
|
|
// Data loading
|
|
setAvailableSkills: (skills) => set({ availableSkills: skills }),
|
|
setAvailableHands: (hands) => set({ availableHands: hands }),
|
|
|
|
// Canvas metadata
|
|
updateCanvasMetadata: (updates) => {
|
|
const { canvas } = get();
|
|
if (!canvas) return;
|
|
set({ canvas: { ...canvas, ...updates }, isDirty: true });
|
|
},
|
|
}),
|
|
{
|
|
name: 'workflow-builder-storage',
|
|
partialize: (state) => ({
|
|
workflows: state.workflows,
|
|
templates: state.templates,
|
|
}),
|
|
}
|
|
)
|
|
);
|
|
|
|
// =============================================================================
|
|
// Node Palette Items
|
|
// =============================================================================
|
|
|
|
export const nodePaletteItems: NodePaletteItem[] = [
|
|
// Input category
|
|
{
|
|
type: 'input',
|
|
label: 'Input',
|
|
description: 'Define workflow input variables',
|
|
icon: '📥',
|
|
category: 'input',
|
|
defaultData: { variableName: 'input' },
|
|
},
|
|
|
|
// AI category
|
|
{
|
|
type: 'llm',
|
|
label: 'LLM Generate',
|
|
description: 'Generate text using LLM',
|
|
icon: '🤖',
|
|
category: 'ai',
|
|
defaultData: { template: '', jsonMode: false },
|
|
},
|
|
{
|
|
type: 'skill',
|
|
label: 'Skill',
|
|
description: 'Execute a skill',
|
|
icon: '⚡',
|
|
category: 'ai',
|
|
defaultData: { skillId: '', inputMappings: {} },
|
|
},
|
|
{
|
|
type: 'orchestration',
|
|
label: 'Skill Orchestration',
|
|
description: 'Execute multiple skills in a DAG',
|
|
icon: '🔀',
|
|
category: 'ai',
|
|
defaultData: { inputMappings: {} },
|
|
},
|
|
|
|
// Action category
|
|
{
|
|
type: 'hand',
|
|
label: 'Hand',
|
|
description: 'Execute a hand action',
|
|
icon: '✋',
|
|
category: 'action',
|
|
defaultData: { handId: '', action: '', params: {} },
|
|
},
|
|
{
|
|
type: 'http',
|
|
label: 'HTTP Request',
|
|
description: 'Make an HTTP request',
|
|
icon: '🌐',
|
|
category: 'action',
|
|
defaultData: { url: '', method: 'GET', headers: {} },
|
|
},
|
|
{
|
|
type: 'setVar',
|
|
label: 'Set Variable',
|
|
description: 'Set a variable value',
|
|
icon: '📝',
|
|
category: 'action',
|
|
defaultData: { variableName: '', value: '' },
|
|
},
|
|
{
|
|
type: 'delay',
|
|
label: 'Delay',
|
|
description: 'Pause execution',
|
|
icon: '⏱️',
|
|
category: 'action',
|
|
defaultData: { ms: 1000 },
|
|
},
|
|
|
|
// Control category
|
|
{
|
|
type: 'condition',
|
|
label: 'Condition',
|
|
description: 'Branch based on condition',
|
|
icon: '🔀',
|
|
category: 'control',
|
|
defaultData: { condition: '', branches: [{ when: '', label: 'Branch' }] },
|
|
},
|
|
{
|
|
type: 'parallel',
|
|
label: 'Parallel',
|
|
description: 'Execute in parallel',
|
|
icon: '⚡',
|
|
category: 'control',
|
|
defaultData: { each: '${inputs.items}', maxWorkers: 4 },
|
|
},
|
|
{
|
|
type: 'loop',
|
|
label: 'Loop',
|
|
description: 'Iterate over items',
|
|
icon: '🔄',
|
|
category: 'control',
|
|
defaultData: { each: '${inputs.items}', itemVar: 'item', indexVar: 'index' },
|
|
},
|
|
|
|
// Output category
|
|
{
|
|
type: 'export',
|
|
label: 'Export',
|
|
description: 'Export to file formats',
|
|
icon: '📤',
|
|
category: 'output',
|
|
defaultData: { formats: ['json'] },
|
|
},
|
|
];
|
|
|
|
// Group palette items by category
|
|
export const paletteCategories: Record<NodeCategory, NodePaletteItem[]> = {
|
|
input: nodePaletteItems.filter(i => i.category === 'input'),
|
|
ai: nodePaletteItems.filter(i => i.category === 'ai'),
|
|
action: nodePaletteItems.filter(i => i.category === 'action'),
|
|
control: nodePaletteItems.filter(i => i.category === 'control'),
|
|
output: nodePaletteItems.filter(i => i.category === 'output'),
|
|
};
|