fix: 全面审计修复 — P0 功能缺陷 + P1 代码质量
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
P0 功能修复: - stats: Admin V2 仪表盘 API 路径修正 (/stats/dashboard → /admin/dashboard) - mcp: 桌面端 MCP 插件增加 isTauriRuntime() 守卫,避免浏览器模式崩溃 - admin: 侧边栏高亮逻辑修复 (startsWith → 精确匹配+子路径) P1 代码质量: - 删除 workflowBuilderStore.ts 死代码 (456行,零引用) - sqlite.rs 3 处 SQL 静默失败改为 tracing::warn! 日志 - mcp_tool_adapter 2 处 unwrap 改为安全回退 - orchestration_execute 添加 @reserved 标注 - TRUTH.md 测试数字校准 (734→803),Store 数 26→25
This commit is contained in:
@@ -117,7 +117,7 @@ function Sidebar({
|
|||||||
const isActive =
|
const isActive =
|
||||||
item.path === '/'
|
item.path === '/'
|
||||||
? activePath === '/'
|
? activePath === '/'
|
||||||
: activePath.startsWith(item.path)
|
: activePath === item.path || activePath.startsWith(item.path + '/')
|
||||||
|
|
||||||
const btn = (
|
const btn = (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import type { DashboardStats } from '@/types'
|
|||||||
|
|
||||||
export const statsService = {
|
export const statsService = {
|
||||||
dashboard: (signal?: AbortSignal) =>
|
dashboard: (signal?: AbortSignal) =>
|
||||||
request.get<DashboardStats>('/stats/dashboard', withSignal({}, signal)).then((r) => r.data),
|
request.get<DashboardStats>('/admin/dashboard', withSignal({}, signal)).then((r) => r.data),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,11 +218,14 @@ impl SqliteStorage {
|
|||||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
normalized.hash(&mut hasher);
|
normalized.hash(&mut hasher);
|
||||||
let hash = format!("{:016x}", hasher.finish());
|
let hash = format!("{:016x}", hasher.finish());
|
||||||
let _ = sqlx::query("UPDATE memories SET content_hash = ? WHERE uri = ?")
|
if let Err(e) = sqlx::query("UPDATE memories SET content_hash = ? WHERE uri = ?")
|
||||||
.bind(&hash)
|
.bind(&hash)
|
||||||
.bind(uri)
|
.bind(uri)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!("[sqlite] content_hash update failed for {}: {}", uri, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"[SqliteStorage] Backfilled content_hash for {} existing entries",
|
"[SqliteStorage] Backfilled content_hash for {} existing entries",
|
||||||
@@ -256,9 +259,12 @@ impl SqliteStorage {
|
|||||||
if needs_rebuild {
|
if needs_rebuild {
|
||||||
tracing::info!("[SqliteStorage] Rebuilding FTS5 table: unicode61 → trigram for CJK support");
|
tracing::info!("[SqliteStorage] Rebuilding FTS5 table: unicode61 → trigram for CJK support");
|
||||||
// Drop old FTS5 table
|
// Drop old FTS5 table
|
||||||
let _ = sqlx::query("DROP TABLE IF EXISTS memories_fts")
|
if let Err(e) = sqlx::query("DROP TABLE IF EXISTS memories_fts")
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!("[sqlite] FTS5 table drop failed during rebuild: {}", e);
|
||||||
|
}
|
||||||
// Recreate with trigram tokenizer
|
// Recreate with trigram tokenizer
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
@@ -423,14 +429,17 @@ impl SqliteStorage {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Also clean up FTS entries for archived memories
|
// Also clean up FTS entries for archived memories
|
||||||
let _ = sqlx::query(
|
if let Err(e) = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM memories_fts
|
DELETE FROM memories_fts
|
||||||
WHERE uri NOT IN (SELECT uri FROM memories)
|
WHERE uri NOT IN (SELECT uri FROM memories)
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!("[sqlite] FTS cleanup after archive failed: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
let archived = archive_result
|
let archived = archive_result
|
||||||
.map(|r| r.rows_affected())
|
.map(|r| r.rows_affected())
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ impl McpToolAdapter {
|
|||||||
|
|
||||||
match result.len() {
|
match result.len() {
|
||||||
0 => Ok(Value::Null),
|
0 => Ok(Value::Null),
|
||||||
1 => Ok(result.into_iter().next().unwrap()),
|
1 => Ok(result.into_iter().next().unwrap_or(Value::Null)),
|
||||||
_ => Ok(Value::Array(result)),
|
_ => Ok(Value::Array(result)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ impl McpServiceManager {
|
|||||||
let adapters = McpToolAdapter::from_server(name.clone(), client.clone()).await?;
|
let adapters = McpToolAdapter::from_server(name.clone(), client.clone()).await?;
|
||||||
self.clients.insert(name.clone(), client);
|
self.clients.insert(name.clone(), client);
|
||||||
self.adapters.insert(name.clone(), adapters);
|
self.adapters.insert(name.clone(), adapters);
|
||||||
Ok(self.adapters.get(&name).unwrap().iter().collect())
|
Ok(self.adapters.get(&name).map(|v| v.iter().collect()).unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all registered tool adapters from all services
|
/// Get all registered tool adapters from all services
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ impl From<zclaw_skills::orchestration::OrchestrationResult> for OrchestrationRes
|
|||||||
|
|
||||||
/// @reserved — no frontend UI yet
|
/// @reserved — no frontend UI yet
|
||||||
/// Execute a skill orchestration
|
/// Execute a skill orchestration
|
||||||
|
/// @reserved — orchestration engine internal, no direct frontend caller
|
||||||
///
|
///
|
||||||
/// Either auto-composes a graph from skill_ids, or uses a pre-defined graph.
|
/// Either auto-composes a graph from skill_ids, or uses a pre-defined graph.
|
||||||
/// Executes with true parallel execution within each dependency level.
|
/// Executes with true parallel execution within each dependency level.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import './index.css';
|
|||||||
import { ToastProvider } from './components/ui/Toast';
|
import { ToastProvider } from './components/ui/Toast';
|
||||||
import { GlobalErrorBoundary } from './components/ui/ErrorBoundary';
|
import { GlobalErrorBoundary } from './components/ui/ErrorBoundary';
|
||||||
import { initWebMCPTools } from './lib/webmcp-tools';
|
import { initWebMCPTools } from './lib/webmcp-tools';
|
||||||
|
import { isTauriRuntime } from './lib/tauri-gateway';
|
||||||
import { setupPluginListeners } from 'tauri-plugin-mcp';
|
import { setupPluginListeners } from 'tauri-plugin-mcp';
|
||||||
|
|
||||||
// Global error handler for uncaught errors
|
// Global error handler for uncaught errors
|
||||||
@@ -30,11 +31,10 @@ const handleGlobalReset = () => {
|
|||||||
// Initialize WebMCP debugging tools (dev mode only, Chrome 146+)
|
// Initialize WebMCP debugging tools (dev mode only, Chrome 146+)
|
||||||
initWebMCPTools();
|
initWebMCPTools();
|
||||||
|
|
||||||
// Initialize tauri-plugin-mcp event listeners (dev mode only)
|
// Initialize tauri-plugin-mcp event listeners (dev mode + Tauri runtime only)
|
||||||
if (import.meta.env.DEV) {
|
// Only works inside Tauri webview — gracefully skips in browser dev mode
|
||||||
setupPluginListeners().catch((err) => {
|
if (import.meta.env.DEV && isTauriRuntime()) {
|
||||||
console.warn('[MCP] Failed to setup plugin listeners:', err);
|
setupPluginListeners().catch(() => {});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
|||||||
@@ -1,456 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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'),
|
|
||||||
};
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|------|--------|----------|
|
|------|--------|----------|
|
||||||
| Rust Crates | 10 个 (编译通过) | `cargo check --workspace` |
|
| Rust Crates | 10 个 (编译通过) | `cargo check --workspace` |
|
||||||
| Rust 代码行数 | ~77,000 (crates) + ~61,400 (src-tauri) = ~138,400 | wc -l (2026-04-12 V13 验证) |
|
| Rust 代码行数 | ~77,000 (crates) + ~61,400 (src-tauri) = ~138,400 | wc -l (2026-04-12 V13 验证) |
|
||||||
| Rust 单元测试 | 425 个 (#[test]) + 309 个 (#[tokio::test]) = 734 | `grep '#\[test\]' crates/` + `grep '#\[tokio::test\]'` (2026-04-18 验证) |
|
| Rust 单元测试 | 477 个 (#[test]) + 326 个 (#[tokio::test]) = 803 | `grep '#\[test\]' crates/` + `grep '#\[tokio::test\]'` (2026-04-18 审计验证) |
|
||||||
| Cargo Warnings (非 SaaS) | **0 个** (仅 sqlx-postgres 外部依赖 1 个) | `cargo check --workspace --exclude zclaw-saas` (2026-04-15 清零) |
|
| Cargo Warnings (非 SaaS) | **0 个** (仅 sqlx-postgres 外部依赖 1 个) | `cargo check --workspace --exclude zclaw-saas` (2026-04-15 清零) |
|
||||||
| Rust 测试运行通过 | 684 workspace + 138 SaaS = 822 | Hermes 4 Chunk `cargo test --workspace` 2026-04-09 |
|
| Rust 测试运行通过 | 684 workspace + 138 SaaS = 822 | Hermes 4 Chunk `cargo test --workspace` 2026-04-09 |
|
||||||
| Tauri 命令 | 190 个 | `grep '#\[.*tauri::command'` (2026-04-16 验证) |
|
| Tauri 命令 | 190 个 | `grep '#\[.*tauri::command'` (2026-04-16 验证) |
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
| SaaS 数据表 | 34 个(含 saas_schema_version) | CREATE TABLE 全量统计 |
|
| SaaS 数据表 | 34 个(含 saas_schema_version) | CREATE TABLE 全量统计 |
|
||||||
| SaaS Workers | 7 个 | log_operation/cleanup_rate_limit/cleanup_refresh_tokens/record_usage/update_last_used/aggregate_usage/generate_embedding |
|
| SaaS Workers | 7 个 | log_operation/cleanup_rate_limit/cleanup_refresh_tokens/record_usage/update_last_used/aggregate_usage/generate_embedding |
|
||||||
| LLM Provider | 8 个 | Kimi/Qwen/DeepSeek/Zhipu/OpenAI/Anthropic/Gemini/Local |
|
| LLM Provider | 8 个 | Kimi/Qwen/DeepSeek/Zhipu/OpenAI/Anthropic/Gemini/Local |
|
||||||
| Zustand Store | 26 个 | find desktop/src/store/ -name "*.ts" (2026-04-18 验证,含子目录) |
|
| Zustand Store | 25 个 | find desktop/src/store/ -name "*.ts" (2026-04-18 审计,workflowBuilderStore 已删除) |
|
||||||
| React 组件 | 105 个 (.tsx/.ts) | find desktop/src/components/ (2026-04-15 新增 HealthPanel.tsx) |
|
| React 组件 | 105 个 (.tsx/.ts) | find desktop/src/components/ (2026-04-15 新增 HealthPanel.tsx) |
|
||||||
| 前端 TypeScript 测试 | 31 个文件 (6 store + 5 lib + 1 config + 1 stabilization + 18 E2E spec) | Phase 3-4 全量 |
|
| 前端 TypeScript 测试 | 31 个文件 (6 store + 5 lib + 1 config + 1 stabilization + 18 E2E spec) | Phase 3-4 全量 |
|
||||||
| 前端 lib | 76 个 .ts | find desktop/src/lib/ (2026-04-15 删除 intelligence-client/ 9 文件) |
|
| 前端 lib | 76 个 .ts | find desktop/src/lib/ (2026-04-15 删除 intelligence-client/ 9 文件) |
|
||||||
|
|||||||
Reference in New Issue
Block a user