Files
zclaw_openfang/desktop/src/store/workflowStore.ts
iven 813b49a986
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
feat: P0 KernelClient功能修复 + P1/P2/P3质量改进
P0 KernelClient 功能断裂修复:
- Skill CUD: registry.rs create/update/delete + serialize_skill_md + kernel proxy
- Workflow CUD: pipeline_commands.rs create/update/delete + serde_yaml依赖
- Agent更新: registry update方法 + AgentConfigUpdated事件 + agent_update命令
- Hand流式事件: HandStart/HandEnd变体替换ToolStart/ToolEnd
- 后端验证: hand_get/hand_run_status/hand_run_list确认实现完整
- Approval闭环: respond_to_approval后台spawn+5分钟超时轮询

P2/P3 质量改进:
- Browser WebDriver: TCP探测ChromeDriver/GeckoDriver/Edge端口替换硬编码true
- api-fallbacks: 移除假技能和16个捏造安全层,替换为真实能力映射
- dead_code清理: 移除5个模块级#![allow(dead_code)],删除3个真正死方法,
  删除未注册的compactor_compact_llm命令,warnings从8降到3
- 所有变更通过cargo check + tsc --noEmit验证
2026-03-30 10:55:08 +08:00

535 lines
16 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 (workflow) => {
try {
const result = await invoke<{ id: string; name: string }>('pipeline_create', {
request: {
name: workflow.name,
description: workflow.description,
steps: workflow.steps.map((s, i) => ({
handName: s.handName,
name: s.name || `Step ${i + 1}`,
params: s.params,
condition: s.condition,
})),
},
});
return result;
} catch {
return null;
}
},
updateWorkflow: async (id, updates) => {
try {
const result = await invoke<{ id: string; name: string }>('pipeline_update', {
pipelineId: id,
request: {
name: updates.name,
description: updates.description,
steps: updates.steps?.map((s, i) => ({
handName: s.handName,
name: s.name || `Step ${i + 1}`,
params: s.params,
condition: s.condition,
})),
},
});
return result;
} catch {
return null;
}
},
deleteWorkflow: async (id) => {
try {
await invoke('pipeline_delete', { pipelineId: id });
return { status: 'deleted' };
} catch {
return { status: 'error' };
}
},
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);
}