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

refactor: 统一Hands系统常量到单个源文件
refactor: 更新Hands中文名称和描述

fix: 修复技能市场在连接状态变化时重新加载
fix: 修复身份变更提案的错误处理逻辑

docs: 更新多个功能文档的验证状态和实现位置
docs: 更新Hands系统文档

test: 添加测试文件验证工作区路径
This commit is contained in:
iven
2026-03-25 08:27:25 +08:00
parent 9c781f5f2a
commit aa6a9cbd84
110 changed files with 12384 additions and 1337 deletions

View 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'),
};