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
基于 Tauri MCP 实机排查发现并修复:
1. VikingPanel: viking_ls('/') 返回0 → 改为 viking_ls('') 返回100条记忆
2. 技能列表: loadSkillsCatalog 静默失败 → 添加直接 invoke('skill_list') 回退
3. 审计日志: 面板读Gateway API无数据 → 回退读localStorage双源数据
4. 工作区: 浏览按钮无事件 → 接入prompt选择 + workspace_dir_stats 命令
5. MCP: 空列表无引导 → 添加配置文件路径提示
6. 新增 workspace_dir_stats Tauri 命令 (Rust)
排查确认正常的功能: 安全存储(OS Keyring✅), 心跳引擎(运行中✅),
定时任务(管道连通), Kernel(已初始化✅), SaaS relay模式
851 lines
26 KiB
TypeScript
851 lines
26 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;
|
|
command?: string;
|
|
args?: string[];
|
|
env?: Record<string, string>;
|
|
cwd?: string;
|
|
}>;
|
|
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;
|
|
|
|
// Path A: via injected client (KernelClient or GatewayClient)
|
|
if (client) {
|
|
try {
|
|
const result = await client.listSkills();
|
|
if (result?.skills && result.skills.length > 0) {
|
|
set({ skillsCatalog: result.skills });
|
|
if (result.extraDirs) {
|
|
set((state) => ({
|
|
quickConfig: {
|
|
...state.quickConfig,
|
|
skillsExtraDirs: result.extraDirs,
|
|
},
|
|
}));
|
|
}
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
console.warn('[configStore] listSkills via client failed, trying direct invoke:', err);
|
|
}
|
|
}
|
|
|
|
// Path B: direct Tauri invoke fallback (works even without client injection)
|
|
try {
|
|
const skills = await invoke('skill_list');
|
|
if (Array.isArray(skills) && skills.length > 0) {
|
|
set({ skillsCatalog: skills.map((s: Record<string, unknown>) => ({
|
|
id: s.id as string,
|
|
name: s.name as string,
|
|
description: (s.description as string) || '',
|
|
version: (s.version as string) || '',
|
|
capabilities: (s.capabilities as string[]) || [],
|
|
tags: (s.tags as string[]) || [],
|
|
mode: (s.mode as string) || '',
|
|
triggers: ((s.triggers as string[]) || []).map((t: string) => ({ type: 'keyword' as const, pattern: t })),
|
|
actions: ((s.capabilities as string[]) || []).map((cap: string) => ({ type: cap, params: undefined })),
|
|
enabled: (s.enabled as boolean) ?? true,
|
|
category: s.category as string,
|
|
source: ((s.source as string) || 'builtin') as 'builtin' | 'extra',
|
|
path: s.path as string | undefined,
|
|
})) });
|
|
}
|
|
} catch (err) {
|
|
console.warn('[configStore] skill_list direct invoke also failed:', err);
|
|
}
|
|
},
|
|
|
|
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: string; name: string; description?: string; version: string; capabilities?: string[]; tags?: string[]; mode: string; enabled?: boolean; triggers?: string[]; category?: string; source?: string; path?: string }) => ({
|
|
id: s.id,
|
|
name: s.name,
|
|
description: s.description || '',
|
|
version: s.version,
|
|
capabilities: s.capabilities || [],
|
|
tags: s.tags || [],
|
|
mode: s.mode,
|
|
triggers: (s.triggers || []).map((t: string) => ({
|
|
type: 'keyword',
|
|
pattern: t,
|
|
})),
|
|
actions: (s.capabilities || []).map((cap: string) => ({
|
|
type: cap,
|
|
params: undefined,
|
|
})),
|
|
enabled: s.enabled ?? true,
|
|
category: s.category,
|
|
source: (s.source as 'builtin' | 'extra') || 'builtin',
|
|
path: s.path || undefined,
|
|
})),
|
|
};
|
|
}
|
|
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();
|
|
const defaultModel = status.defaultModel ? [{
|
|
id: status.defaultModel as string,
|
|
name: status.defaultModel as string,
|
|
provider: (status.defaultProvider as string) || 'default',
|
|
}] : [];
|
|
// Load custom models from localStorage
|
|
const customModels: Array<{ id: string; name: string; provider?: string }> = [];
|
|
try {
|
|
const raw = localStorage.getItem('zclaw-custom-models');
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
if (Array.isArray(parsed)) {
|
|
for (const m of parsed) {
|
|
if (m.id && m.name) {
|
|
customModels.push({ id: m.id, name: m.name, provider: m.provider || 'custom' });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch { /* ignore parse errors */ }
|
|
return { models: [...defaultModel, ...customModels] };
|
|
} 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);
|
|
}
|