feat: 新增技能编排引擎和工作流构建器组件
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
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: 添加测试文件验证工作区路径
This commit is contained in:
456
desktop/src/store/workflowBuilderStore.ts
Normal file
456
desktop/src/store/workflowBuilderStore.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
/**
|
||||
* 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'),
|
||||
};
|
||||
Reference in New Issue
Block a user