Files
DeskClaw/src/stores/providers.ts
Haze ebb6f515a7 feat(providers): implement secure API key storage and provider management
Add complete provider configuration system with the following features:

- Secure API key storage using Electron's safeStorage encryption
- Provider CRUD operations with IPC handlers
- Lazy-loaded electron-store for ESM compatibility
- Provider settings UI component with add/edit/delete functionality
- API key masking for display (shows first/last 4 chars)
- Basic API key format validation per provider type
- Default provider selection
- Provider enable/disable toggle

New files:
- electron/utils/secure-storage.ts: Encrypted key storage and provider config
- src/stores/providers.ts: Zustand store for provider state
- src/components/settings/ProvidersSettings.tsx: Provider management UI
2026-02-05 23:24:31 +08:00

199 lines
5.8 KiB
TypeScript

/**
* Provider State Store
* Manages AI provider configurations
*/
import { create } from 'zustand';
/**
* Provider configuration
*/
export interface ProviderConfig {
id: string;
name: string;
type: 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom';
baseUrl?: string;
model?: string;
enabled: boolean;
createdAt: string;
updatedAt: string;
}
/**
* Provider with key info (for display)
*/
export interface ProviderWithKeyInfo extends ProviderConfig {
hasKey: boolean;
keyMasked: string | null;
}
interface ProviderState {
providers: ProviderWithKeyInfo[];
defaultProviderId: string | null;
loading: boolean;
error: string | null;
// Actions
fetchProviders: () => Promise<void>;
addProvider: (config: Omit<ProviderConfig, 'createdAt' | 'updatedAt'>, apiKey?: string) => Promise<void>;
updateProvider: (providerId: string, updates: Partial<ProviderConfig>, apiKey?: string) => Promise<void>;
deleteProvider: (providerId: string) => Promise<void>;
setApiKey: (providerId: string, apiKey: string) => Promise<void>;
deleteApiKey: (providerId: string) => Promise<void>;
setDefaultProvider: (providerId: string) => Promise<void>;
validateApiKey: (providerId: string, apiKey: string) => Promise<{ valid: boolean; error?: string }>;
getApiKey: (providerId: string) => Promise<string | null>;
}
export const useProviderStore = create<ProviderState>((set, get) => ({
providers: [],
defaultProviderId: null,
loading: false,
error: null,
fetchProviders: async () => {
set({ loading: true, error: null });
try {
const providers = await window.electron.ipcRenderer.invoke('provider:list') as ProviderWithKeyInfo[];
const defaultId = await window.electron.ipcRenderer.invoke('provider:getDefault') as string | null;
set({
providers,
defaultProviderId: defaultId,
loading: false
});
} catch (error) {
set({ error: String(error), loading: false });
}
},
addProvider: async (config, apiKey) => {
try {
const fullConfig: ProviderConfig = {
...config,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const result = await window.electron.ipcRenderer.invoke('provider:save', fullConfig, apiKey) as { success: boolean; error?: string };
if (!result.success) {
throw new Error(result.error || 'Failed to save provider');
}
// Refresh the list
await get().fetchProviders();
} catch (error) {
console.error('Failed to add provider:', error);
throw error;
}
},
updateProvider: async (providerId, updates, apiKey) => {
try {
const existing = get().providers.find((p) => p.id === providerId);
if (!existing) {
throw new Error('Provider not found');
}
const updatedConfig: ProviderConfig = {
...existing,
...updates,
updatedAt: new Date().toISOString(),
};
const result = await window.electron.ipcRenderer.invoke('provider:save', updatedConfig, apiKey) as { success: boolean; error?: string };
if (!result.success) {
throw new Error(result.error || 'Failed to update provider');
}
// Refresh the list
await get().fetchProviders();
} catch (error) {
console.error('Failed to update provider:', error);
throw error;
}
},
deleteProvider: async (providerId) => {
try {
const result = await window.electron.ipcRenderer.invoke('provider:delete', providerId) as { success: boolean; error?: string };
if (!result.success) {
throw new Error(result.error || 'Failed to delete provider');
}
// Refresh the list
await get().fetchProviders();
} catch (error) {
console.error('Failed to delete provider:', error);
throw error;
}
},
setApiKey: async (providerId, apiKey) => {
try {
const result = await window.electron.ipcRenderer.invoke('provider:setApiKey', providerId, apiKey) as { success: boolean; error?: string };
if (!result.success) {
throw new Error(result.error || 'Failed to set API key');
}
// Refresh the list
await get().fetchProviders();
} catch (error) {
console.error('Failed to set API key:', error);
throw error;
}
},
deleteApiKey: async (providerId) => {
try {
const result = await window.electron.ipcRenderer.invoke('provider:deleteApiKey', providerId) as { success: boolean; error?: string };
if (!result.success) {
throw new Error(result.error || 'Failed to delete API key');
}
// Refresh the list
await get().fetchProviders();
} catch (error) {
console.error('Failed to delete API key:', error);
throw error;
}
},
setDefaultProvider: async (providerId) => {
try {
const result = await window.electron.ipcRenderer.invoke('provider:setDefault', providerId) as { success: boolean; error?: string };
if (!result.success) {
throw new Error(result.error || 'Failed to set default provider');
}
set({ defaultProviderId: providerId });
} catch (error) {
console.error('Failed to set default provider:', error);
throw error;
}
},
validateApiKey: async (providerId, apiKey) => {
try {
const result = await window.electron.ipcRenderer.invoke('provider:validateKey', providerId, apiKey) as { valid: boolean; error?: string };
return result;
} catch (error) {
return { valid: false, error: String(error) };
}
},
getApiKey: async (providerId) => {
try {
return await window.electron.ipcRenderer.invoke('provider:getApiKey', providerId) as string | null;
} catch {
return null;
}
},
}));