Files
zclaw_openfang/desktop/src/store/configStore.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

801 lines
24 KiB
TypeScript

/**
* configStore.ts - Configuration Management Store
*
* Extracted from gatewayStore.ts for Phase 11 Store Refactoring.
* Manages settings, workspace, channels, skills, and models.
*/
import { create } from 'zustand';
import type { GatewayModelChoice } from '../lib/gateway-config';
import { setStoredGatewayUrl, setStoredGatewayToken } from '../lib/gateway-client';
import type { GatewayClient } from '../lib/gateway-client';
import { invoke } from '@tauri-apps/api/core';
// === Types ===
export interface QuickConfig {
agentName?: string;
agentRole?: string;
userName?: string;
userRole?: string;
agentNickname?: string;
scenarios?: string[];
workspaceDir?: string;
gatewayUrl?: string;
gatewayToken?: string;
skillsExtraDirs?: string[];
mcpServices?: Array<{ id: string; name: string; enabled: boolean }>;
theme?: 'light' | 'dark';
autoStart?: boolean;
showToolCalls?: boolean;
restrictFiles?: boolean;
autoSaveContext?: boolean;
fileWatching?: boolean;
privacyOptIn?: boolean;
}
export interface WorkspaceInfo {
path: string;
resolvedPath: string;
exists: boolean;
fileCount: number;
totalSize: number;
}
export interface ChannelInfo {
id: string;
type: string;
name: string;
label: string;
status: 'active' | 'inactive' | 'error';
enabled?: boolean;
accounts?: number;
error?: string;
config?: Record<string, string>;
}
export interface ScheduledTask {
id: string;
name: string;
schedule: string;
status: 'active' | 'paused' | 'completed' | 'error';
lastRun?: string;
nextRun?: string;
description?: string;
}
export interface SkillInfo {
id: string;
name: string;
path?: string;
source?: 'builtin' | 'extra';
description?: string;
version?: string;
capabilities?: string[];
tags?: string[];
mode?: string;
triggers?: Array<{ type: string; pattern?: string }>;
actions?: Array<{ type: string; params?: Record<string, unknown> }>;
enabled?: boolean;
}
// === Client Interface ===
export interface ConfigStoreClient {
getWorkspaceInfo(): Promise<WorkspaceInfo | null>;
getQuickConfig(): Promise<{ quickConfig?: QuickConfig } | null>;
saveQuickConfig(config: QuickConfig): Promise<{ quickConfig?: QuickConfig } | null>;
listSkills(): Promise<{ skills?: SkillInfo[]; extraDirs?: string[] } | null>;
getSkill(id: string): Promise<{ skill?: SkillInfo } | null>;
createSkill(skill: {
name: string;
description?: string;
triggers: Array<{ type: string; pattern?: string }>;
actions: Array<{ type: string; params?: Record<string, unknown> }>;
enabled?: boolean;
}): Promise<{ skill?: SkillInfo } | null>;
updateSkill(id: string, updates: {
name?: string;
description?: string;
triggers?: Array<{ type: string; pattern?: string }>;
actions?: Array<{ type: string; params?: Record<string, unknown> }>;
enabled?: boolean;
}): Promise<{ skill?: SkillInfo } | null>;
deleteSkill(id: string): Promise<void>;
listChannels(): Promise<{ channels?: ChannelInfo[] } | null>;
getChannel(id: string): Promise<{ channel?: ChannelInfo } | null>;
createChannel(channel: {
type: string;
name: string;
config: Record<string, unknown>;
enabled?: boolean;
}): Promise<{ channel?: ChannelInfo } | null>;
updateChannel(id: string, updates: {
name?: string;
config?: Record<string, unknown>;
enabled?: boolean;
}): Promise<{ channel?: ChannelInfo } | null>;
deleteChannel(id: string): Promise<void>;
listScheduledTasks(): Promise<{ tasks?: ScheduledTask[] } | null>;
createScheduledTask(task: {
name: string;
schedule: string;
scheduleType: 'cron' | 'interval' | 'once';
target?: {
type: 'agent' | 'hand' | 'workflow';
id: string;
};
description?: string;
enabled?: boolean;
}): Promise<ScheduledTask>;
listModels(): Promise<{ models: GatewayModelChoice[] }>;
getFeishuStatus(): Promise<{ configured?: boolean; accounts?: number } | null>;
}
// === Store State Slice ===
export interface ConfigStateSlice {
quickConfig: QuickConfig;
workspaceInfo: WorkspaceInfo | null;
channels: ChannelInfo[];
scheduledTasks: ScheduledTask[];
skillsCatalog: SkillInfo[];
models: GatewayModelChoice[];
modelsLoading: boolean;
modelsError: string | null;
error: string | null;
client: ConfigStoreClient | null;
isLoading: boolean;
}
// === Store Actions Slice ===
export interface ConfigActionsSlice {
setConfigStoreClient: (client: ConfigStoreClient) => void;
loadQuickConfig: () => Promise<void>;
saveQuickConfig: (updates: Partial<QuickConfig>) => Promise<void>;
loadWorkspaceInfo: () => Promise<void>;
loadChannels: () => Promise<void>;
getChannel: (id: string) => Promise<ChannelInfo | undefined>;
createChannel: (channel: {
type: string;
name: string;
config: Record<string, unknown>;
enabled?: boolean;
}) => Promise<ChannelInfo | undefined>;
updateChannel: (id: string, updates: {
name?: string;
config?: Record<string, unknown>;
enabled?: boolean;
}) => Promise<ChannelInfo | undefined>;
deleteChannel: (id: string) => Promise<void>;
loadScheduledTasks: () => Promise<void>;
createScheduledTask: (task: {
name: string;
schedule: string;
scheduleType: 'cron' | 'interval' | 'once';
target?: {
type: 'agent' | 'hand' | 'workflow';
id: string;
};
description?: string;
enabled?: boolean;
}) => Promise<ScheduledTask | undefined>;
loadSkillsCatalog: () => Promise<void>;
getSkill: (id: string) => Promise<SkillInfo | undefined>;
createSkill: (skill: {
name: string;
description?: string;
triggers: Array<{ type: string; pattern?: string }>;
actions: Array<{ type: string; params?: Record<string, unknown> }>;
enabled?: boolean;
}) => Promise<SkillInfo | undefined>;
updateSkill: (id: string, updates: {
name?: string;
description?: string;
triggers?: Array<{ type: string; pattern?: string }>;
actions?: Array<{ type: string; params?: Record<string, unknown> }>;
enabled?: boolean;
}) => Promise<SkillInfo | undefined>;
deleteSkill: (id: string) => Promise<void>;
loadModels: () => Promise<void>;
clearError: () => void;
}
// === Combined Store Type ===
export type ConfigStore = ConfigStateSlice & ConfigActionsSlice;
export const useConfigStore = create<ConfigStateSlice & ConfigActionsSlice>((set, get) => ({
// Initial State
quickConfig: {},
workspaceInfo: null,
channels: [],
scheduledTasks: [],
skillsCatalog: [],
models: [],
modelsLoading: false,
modelsError: null,
error: null,
client: null,
isLoading: false,
// Client Injection
setConfigStoreClient: (client: ConfigStoreClient) => {
set({ client });
},
// === Quick Config Actions ===
loadQuickConfig: async () => {
const client = get().client;
if (!client) return;
try {
const result = await client.getQuickConfig();
set({ quickConfig: result?.quickConfig || {} });
} catch {
// Ignore if quick config not available
}
},
saveQuickConfig: async (updates: Partial<QuickConfig>) => {
const client = get().client;
if (!client) return;
try {
const nextConfig = { ...get().quickConfig, ...updates };
// Persist gateway URL/token to localStorage for reconnection
if (nextConfig.gatewayUrl) {
setStoredGatewayUrl(nextConfig.gatewayUrl);
}
if (Object.prototype.hasOwnProperty.call(updates, 'gatewayToken')) {
setStoredGatewayToken(nextConfig.gatewayToken || '');
}
const result = await client.saveQuickConfig(nextConfig);
set({ quickConfig: result?.quickConfig || nextConfig });
} catch (err: unknown) {
set({ error: err instanceof Error ? err.message : String(err) });
}
},
// === Workspace Actions ===
loadWorkspaceInfo: async () => {
const client = get().client;
if (!client) return;
try {
const info = await client.getWorkspaceInfo();
set({ workspaceInfo: info });
} catch {
// Ignore if workspace info not available
}
},
// === Channel Actions ===
loadChannels: async () => {
const client = get().client;
if (!client) return;
const channels: ChannelInfo[] = [];
try {
// Try listing channels from Gateway
const result = await client.listChannels();
if (result?.channels) {
set({ channels: result.channels });
return;
}
} catch {
// channels.list may not be available, fallback to probing
}
// Fallback: probe known channels individually
try {
const feishu = await client.getFeishuStatus();
channels.push({
id: 'feishu',
type: 'feishu',
name: 'feishu',
label: '飞书 (Feishu)',
status: feishu?.configured ? 'active' : 'inactive',
accounts: feishu?.accounts || 0,
});
} catch {
channels.push({ id: 'feishu', type: 'feishu', name: 'feishu', label: '飞书 (Feishu)', status: 'inactive' });
}
set({ channels });
},
getChannel: async (id: string) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.getChannel(id);
if (result?.channel) {
// Update the channel in the local state if it exists
const currentChannels = get().channels;
const existingIndex = currentChannels.findIndex(c => c.id === id);
if (existingIndex >= 0) {
const updatedChannels = [...currentChannels];
updatedChannels[existingIndex] = result.channel;
set({ channels: updatedChannels });
}
return result.channel as ChannelInfo;
}
return undefined;
} catch (err: unknown) {
set({ error: err instanceof Error ? err.message : String(err) });
return undefined;
}
},
createChannel: async (channel) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.createChannel(channel);
if (result?.channel) {
// Add the new channel to local state
const currentChannels = get().channels;
set({ channels: [...currentChannels, result.channel as ChannelInfo] });
return result.channel as ChannelInfo;
}
return undefined;
} catch (err: unknown) {
set({ error: err instanceof Error ? err.message : String(err) });
return undefined;
}
},
updateChannel: async (id, updates) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.updateChannel(id, updates);
if (result?.channel) {
// Update the channel in local state
const currentChannels = get().channels;
const updatedChannels = currentChannels.map(c =>
c.id === id ? (result.channel as ChannelInfo) : c
);
set({ channels: updatedChannels });
return result.channel as ChannelInfo;
}
return undefined;
} catch (err: unknown) {
set({ error: err instanceof Error ? err.message : String(err) });
return undefined;
}
},
deleteChannel: async (id) => {
const client = get().client;
if (!client) return;
try {
await client.deleteChannel(id);
// Remove the channel from local state
const currentChannels = get().channels;
set({ channels: currentChannels.filter(c => c.id !== id) });
} catch (err: unknown) {
set({ error: err instanceof Error ? err.message : String(err) });
}
},
// === Scheduled Task Actions ===
loadScheduledTasks: async () => {
const client = get().client;
if (!client) return;
try {
const result = await client.listScheduledTasks();
const tasks = result?.tasks || [];
set({ scheduledTasks: tasks });
// Persist to localStorage as fallback
try { localStorage.setItem('zclaw-scheduled-tasks', JSON.stringify(tasks)); } catch { /* ignore */ }
} catch {
// Fallback: load from localStorage
try {
const stored = localStorage.getItem('zclaw-scheduled-tasks');
if (stored) {
set({ scheduledTasks: JSON.parse(stored) });
}
} catch { /* ignore */ }
}
},
createScheduledTask: async (task) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.createScheduledTask(task);
const newTask: ScheduledTask = {
id: result.id,
name: result.name,
schedule: result.schedule,
status: result.status as 'active' | 'paused' | 'completed' | 'error',
lastRun: result.lastRun,
nextRun: result.nextRun,
description: result.description,
};
set((state) => {
const tasks = [...state.scheduledTasks, newTask];
try { localStorage.setItem('zclaw-scheduled-tasks', JSON.stringify(tasks)); } catch { /* ignore */ }
return { scheduledTasks: tasks };
});
return newTask;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Failed to create scheduled task';
set({ error: errorMessage });
return undefined;
}
},
// === Skill Actions ===
loadSkillsCatalog: async () => {
const client = get().client;
if (!client) return;
try {
const result = await client.listSkills();
set({ skillsCatalog: result?.skills || [] });
if (result?.extraDirs) {
set((state) => ({
quickConfig: {
...state.quickConfig,
skillsExtraDirs: result.extraDirs,
},
}));
}
} catch {
// Ignore if skills list not available
}
},
getSkill: async (id: string) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.getSkill(id);
return result?.skill as SkillInfo | undefined;
} catch {
return undefined;
}
},
createSkill: async (skill) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.createSkill(skill);
const newSkill = result?.skill as SkillInfo | undefined;
if (newSkill) {
set((state) => ({
skillsCatalog: [...state.skillsCatalog, newSkill],
}));
}
return newSkill;
} catch {
return undefined;
}
},
updateSkill: async (id, updates) => {
const client = get().client;
if (!client) return undefined;
try {
const result = await client.updateSkill(id, updates);
const updatedSkill = result?.skill as SkillInfo | undefined;
if (updatedSkill) {
set((state) => ({
skillsCatalog: state.skillsCatalog.map((s) =>
s.id === id ? updatedSkill : s
),
}));
}
return updatedSkill;
} catch {
return undefined;
}
},
deleteSkill: async (id) => {
const client = get().client;
if (!client) return;
try {
await client.deleteSkill(id);
set((state) => ({
skillsCatalog: state.skillsCatalog.filter((s) => s.id !== id),
}));
} catch {
// Ignore deletion errors
}
},
// === Model Actions ===
loadModels: async () => {
const client = get().client;
if (!client) return;
try {
set({ modelsLoading: true, modelsError: null });
const result = await client.listModels();
const models: GatewayModelChoice[] = result?.models || [];
set({ models, modelsLoading: false });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to load models';
set({ modelsError: message, modelsLoading: false });
}
},
// === Utility ===
clearError: () => {
set({ error: null });
},
}));
// Re-export types for convenience
export type {
QuickConfig as QuickConfigType,
WorkspaceInfo as WorkspaceInfoType,
ChannelInfo as ChannelInfoType,
ScheduledTask as ScheduledTaskType,
SkillInfo as SkillInfoType,
};
// === Client Injection ===
import type { KernelClient } from '../lib/kernel-client';
/**
* Helper to create a ConfigStoreClient adapter from a GatewayClient.
*/
function createConfigClientFromGateway(client: GatewayClient): ConfigStoreClient {
return {
getWorkspaceInfo: () => client.getWorkspaceInfo(),
getQuickConfig: () => client.getQuickConfig(),
saveQuickConfig: (config) => client.saveQuickConfig(config),
listSkills: () => client.listSkills(),
getSkill: (id) => client.getSkill(id),
createSkill: (skill) => client.createSkill(skill),
updateSkill: (id, updates) => client.updateSkill(id, updates),
deleteSkill: (id) => client.deleteSkill(id),
listChannels: () => client.listChannels(),
getChannel: (id) => client.getChannel(id),
createChannel: (channel) => client.createChannel(channel),
updateChannel: (id, updates) => client.updateChannel(id, updates),
deleteChannel: (id) => client.deleteChannel(id),
listScheduledTasks: () => client.listScheduledTasks(),
createScheduledTask: async (task) => {
const result = await client.createScheduledTask(task);
return {
id: result.id,
name: result.name,
schedule: result.schedule,
status: result.status as 'active' | 'paused' | 'completed' | 'error',
};
},
listModels: () => client.listModels(),
getFeishuStatus: () => client.getFeishuStatus(),
};
}
/**
* Helper to create a ConfigStoreClient adapter from a KernelClient.
*/
function createConfigClientFromKernel(client: KernelClient): ConfigStoreClient {
return {
getWorkspaceInfo: async () => {
try {
const status = await client.status();
return {
path: '',
resolvedPath: '',
exists: status.initialized as boolean,
fileCount: 0,
totalSize: 0,
};
} catch {
return null;
}
},
getQuickConfig: async () => {
// Read from localStorage in kernel mode
try {
const stored = localStorage.getItem('zclaw-quick-config');
if (stored) {
return { quickConfig: JSON.parse(stored) };
}
} catch { /* ignore */ }
return { quickConfig: {} };
},
saveQuickConfig: async (config) => {
// Persist to localStorage in kernel mode
try {
localStorage.setItem('zclaw-quick-config', JSON.stringify(config));
} catch { /* ignore */ }
return { quickConfig: config };
},
listSkills: async () => {
try {
const result = await client.listSkills();
if (result?.skills) {
return {
skills: result.skills.map((s) => ({
id: s.id,
name: s.name,
description: s.description || '',
version: s.version,
// Use capabilities directly
capabilities: s.capabilities || [],
tags: s.tags || [],
mode: s.mode,
// Map triggers to the expected format
triggers: (s.triggers || []).map((t: string) => ({
type: 'keyword',
pattern: t,
})),
// Create actions from capabilities for UI display
actions: (s.capabilities || []).map((cap: string) => ({
type: cap,
params: undefined,
})),
enabled: s.enabled ?? true,
category: s.category,
})),
};
}
return { skills: [] };
} catch {
return { skills: [] };
}
},
getSkill: async (id: string) => {
return { skill: { id, name: id, description: '' } };
},
createSkill: async (skill) => {
try {
const result = await client.createSkill(skill);
if (result?.skill) {
return {
skill: {
id: result.skill.id,
name: result.skill.name,
description: result.skill.description,
version: result.skill.version,
capabilities: result.skill.capabilities,
tags: result.skill.tags,
mode: result.skill.mode,
enabled: result.skill.enabled,
triggers: (result.skill.triggers || []).map((t: string) => ({ type: 'keyword', pattern: t })),
category: result.skill.category,
} as SkillInfo,
};
}
return null;
} catch {
return null;
}
},
updateSkill: async (id, updates) => {
try {
const result = await client.updateSkill(id, updates);
if (result?.skill) {
return {
skill: {
id: result.skill.id,
name: result.skill.name,
description: result.skill.description,
version: result.skill.version,
capabilities: result.skill.capabilities,
tags: result.skill.tags,
mode: result.skill.mode,
enabled: result.skill.enabled,
triggers: (result.skill.triggers || []).map((t: string) => ({ type: 'keyword', pattern: t })),
category: result.skill.category,
} as SkillInfo,
};
}
return null;
} catch {
return null;
}
},
deleteSkill: async (id) => {
try {
await client.deleteSkill(id);
} catch {
// Ignore deletion errors
}
},
listChannels: async () => ({ channels: [] }),
getChannel: async () => null,
createChannel: async () => null,
updateChannel: async () => null,
deleteChannel: async () => {},
listScheduledTasks: async () => {
try {
const tasks = await invoke<ScheduledTask[]>('scheduled_task_list');
return { tasks };
} catch {
return { tasks: [] };
}
},
createScheduledTask: async (task) => {
const result = await invoke<{ id: string; name: string; schedule: string; status: string }>(
'scheduled_task_create',
{ request: task },
);
return { ...result, status: result.status as 'active' | 'paused' | 'completed' | 'error' };
},
listModels: async () => {
try {
const status = await client.status();
return {
models: status.defaultModel ? [{
id: status.defaultModel as string,
name: status.defaultModel as string,
provider: (status.defaultProvider as string) || 'default',
}] : [],
};
} catch {
return { models: [] };
}
},
getFeishuStatus: async () => null,
};
}
/**
* Sets the client for the config store.
* Called by the coordinator during initialization.
*/
export function setConfigStoreClient(client: unknown): void {
let configClient: ConfigStoreClient;
// Check if it's a KernelClient (has listHands method)
if (client && typeof client === 'object' && 'listHands' in client) {
configClient = createConfigClientFromKernel(client as KernelClient);
} else if (client && typeof client === 'object') {
// It's GatewayClient
configClient = createConfigClientFromGateway(client as GatewayClient);
} else {
// Fallback stub client
configClient = {
getWorkspaceInfo: async () => null,
getQuickConfig: async () => null,
saveQuickConfig: async () => null,
listSkills: async () => ({ skills: [] }),
getSkill: async () => null,
createSkill: async () => null,
updateSkill: async () => null,
deleteSkill: async () => {},
listChannels: async () => ({ channels: [] }),
getChannel: async () => null,
createChannel: async () => null,
updateChannel: async () => null,
deleteChannel: async () => {},
listScheduledTasks: async () => ({ tasks: [] }),
createScheduledTask: async () => { throw new Error('Not implemented'); },
listModels: async () => ({ models: [] }),
getFeishuStatus: async () => null,
};
}
useConfigStore.getState().setConfigStoreClient(configClient);
}