refactor provider 1
This commit is contained in:
35
electron/services/providers/provider-migration.ts
Normal file
35
electron/services/providers/provider-migration.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { ProviderConfig } from '../../shared/providers/types';
|
||||
import {
|
||||
getDefaultProviderAccountId,
|
||||
providerConfigToAccount,
|
||||
saveProviderAccount,
|
||||
} from './provider-store';
|
||||
import { getClawXProviderStore } from './store-instance';
|
||||
|
||||
const PROVIDER_STORE_SCHEMA_VERSION = 1;
|
||||
|
||||
export async function ensureProviderStoreMigrated(): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const schemaVersion = Number(store.get('schemaVersion') ?? 0);
|
||||
|
||||
if (schemaVersion >= PROVIDER_STORE_SCHEMA_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
const legacyProviders = (store.get('providers') ?? {}) as Record<string, ProviderConfig>;
|
||||
const defaultProviderId = (store.get('defaultProvider') ?? null) as string | null;
|
||||
const existingDefaultAccountId = await getDefaultProviderAccountId();
|
||||
|
||||
for (const provider of Object.values(legacyProviders)) {
|
||||
const account = providerConfigToAccount(provider, {
|
||||
isDefault: provider.id === defaultProviderId,
|
||||
});
|
||||
await saveProviderAccount(account);
|
||||
}
|
||||
|
||||
if (!existingDefaultAccountId && defaultProviderId) {
|
||||
store.set('defaultProviderAccountId', defaultProviderId);
|
||||
}
|
||||
|
||||
store.set('schemaVersion', PROVIDER_STORE_SCHEMA_VERSION);
|
||||
}
|
||||
61
electron/services/providers/provider-service.ts
Normal file
61
electron/services/providers/provider-service.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
PROVIDER_DEFINITIONS,
|
||||
getProviderDefinition,
|
||||
} from '../../shared/providers/registry';
|
||||
import type {
|
||||
ProviderAccount,
|
||||
ProviderConfig,
|
||||
ProviderDefinition,
|
||||
} from '../../shared/providers/types';
|
||||
import { ensureProviderStoreMigrated } from './provider-migration';
|
||||
import {
|
||||
getDefaultProviderAccountId,
|
||||
getProviderAccount,
|
||||
listProviderAccounts,
|
||||
providerConfigToAccount,
|
||||
saveProviderAccount,
|
||||
setDefaultProviderAccount,
|
||||
} from './provider-store';
|
||||
|
||||
export class ProviderService {
|
||||
async listVendors(): Promise<ProviderDefinition[]> {
|
||||
return PROVIDER_DEFINITIONS;
|
||||
}
|
||||
|
||||
async listAccounts(): Promise<ProviderAccount[]> {
|
||||
await ensureProviderStoreMigrated();
|
||||
return listProviderAccounts();
|
||||
}
|
||||
|
||||
async getAccount(accountId: string): Promise<ProviderAccount | null> {
|
||||
await ensureProviderStoreMigrated();
|
||||
return getProviderAccount(accountId);
|
||||
}
|
||||
|
||||
async getDefaultAccountId(): Promise<string | undefined> {
|
||||
await ensureProviderStoreMigrated();
|
||||
return getDefaultProviderAccountId();
|
||||
}
|
||||
|
||||
async syncLegacyProvider(config: ProviderConfig, options?: { isDefault?: boolean }): Promise<ProviderAccount> {
|
||||
await ensureProviderStoreMigrated();
|
||||
const account = providerConfigToAccount(config, options);
|
||||
await saveProviderAccount(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
async setDefaultAccount(accountId: string): Promise<void> {
|
||||
await ensureProviderStoreMigrated();
|
||||
await setDefaultProviderAccount(accountId);
|
||||
}
|
||||
|
||||
getVendorDefinition(vendorId: string): ProviderDefinition | undefined {
|
||||
return getProviderDefinition(vendorId);
|
||||
}
|
||||
}
|
||||
|
||||
const providerService = new ProviderService();
|
||||
|
||||
export function getProviderService(): ProviderService {
|
||||
return providerService;
|
||||
}
|
||||
103
electron/services/providers/provider-store.ts
Normal file
103
electron/services/providers/provider-store.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { ProviderAccount, ProviderConfig, ProviderType } from '../../shared/providers/types';
|
||||
import { getProviderDefinition } from '../../shared/providers/registry';
|
||||
import { getClawXProviderStore } from './store-instance';
|
||||
|
||||
const PROVIDER_STORE_SCHEMA_VERSION = 1;
|
||||
|
||||
function inferAuthMode(type: ProviderType): ProviderAccount['authMode'] {
|
||||
if (type === 'ollama') {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
const definition = getProviderDefinition(type);
|
||||
if (definition?.defaultAuthMode) {
|
||||
return definition.defaultAuthMode;
|
||||
}
|
||||
|
||||
return 'api_key';
|
||||
}
|
||||
|
||||
export function providerConfigToAccount(
|
||||
config: ProviderConfig,
|
||||
options?: { isDefault?: boolean },
|
||||
): ProviderAccount {
|
||||
return {
|
||||
id: config.id,
|
||||
vendorId: config.type,
|
||||
label: config.name,
|
||||
authMode: inferAuthMode(config.type),
|
||||
baseUrl: config.baseUrl,
|
||||
apiProtocol: config.type === 'custom' || config.type === 'ollama'
|
||||
? 'openai-completions'
|
||||
: getProviderDefinition(config.type)?.providerConfig?.api,
|
||||
model: config.model,
|
||||
fallbackModels: config.fallbackModels,
|
||||
fallbackAccountIds: config.fallbackProviderIds,
|
||||
enabled: config.enabled,
|
||||
isDefault: options?.isDefault ?? false,
|
||||
createdAt: config.createdAt,
|
||||
updatedAt: config.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function providerAccountToConfig(account: ProviderAccount): ProviderConfig {
|
||||
return {
|
||||
id: account.id,
|
||||
name: account.label,
|
||||
type: account.vendorId,
|
||||
baseUrl: account.baseUrl,
|
||||
model: account.model,
|
||||
fallbackModels: account.fallbackModels,
|
||||
fallbackProviderIds: account.fallbackAccountIds,
|
||||
enabled: account.enabled,
|
||||
createdAt: account.createdAt,
|
||||
updatedAt: account.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listProviderAccounts(): Promise<ProviderAccount[]> {
|
||||
const store = await getClawXProviderStore();
|
||||
const accounts = store.get('providerAccounts') as Record<string, ProviderAccount> | undefined;
|
||||
return Object.values(accounts ?? {});
|
||||
}
|
||||
|
||||
export async function getProviderAccount(accountId: string): Promise<ProviderAccount | null> {
|
||||
const store = await getClawXProviderStore();
|
||||
const accounts = store.get('providerAccounts') as Record<string, ProviderAccount> | undefined;
|
||||
return accounts?.[accountId] ?? null;
|
||||
}
|
||||
|
||||
export async function saveProviderAccount(account: ProviderAccount): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const accounts = (store.get('providerAccounts') ?? {}) as Record<string, ProviderAccount>;
|
||||
accounts[account.id] = account;
|
||||
store.set('providerAccounts', accounts);
|
||||
store.set('schemaVersion', PROVIDER_STORE_SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
export async function deleteProviderAccount(accountId: string): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const accounts = (store.get('providerAccounts') ?? {}) as Record<string, ProviderAccount>;
|
||||
delete accounts[accountId];
|
||||
store.set('providerAccounts', accounts);
|
||||
|
||||
if (store.get('defaultProviderAccountId') === accountId) {
|
||||
store.delete('defaultProviderAccountId');
|
||||
}
|
||||
}
|
||||
|
||||
export async function setDefaultProviderAccount(accountId: string): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
store.set('defaultProviderAccountId', accountId);
|
||||
|
||||
const accounts = (store.get('providerAccounts') ?? {}) as Record<string, ProviderAccount>;
|
||||
for (const account of Object.values(accounts)) {
|
||||
account.isDefault = account.id === accountId;
|
||||
}
|
||||
store.set('providerAccounts', accounts);
|
||||
}
|
||||
|
||||
export async function getDefaultProviderAccountId(): Promise<string | undefined> {
|
||||
const store = await getClawXProviderStore();
|
||||
return store.get('defaultProviderAccountId') as string | undefined;
|
||||
}
|
||||
23
electron/services/providers/store-instance.ts
Normal file
23
electron/services/providers/store-instance.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Lazy-load electron-store (ESM module) from the main process only.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let providerStore: any = null;
|
||||
|
||||
export async function getClawXProviderStore() {
|
||||
if (!providerStore) {
|
||||
const Store = (await import('electron-store')).default;
|
||||
providerStore = new Store({
|
||||
name: 'clawx-providers',
|
||||
defaults: {
|
||||
schemaVersion: 0,
|
||||
providers: {} as Record<string, unknown>,
|
||||
providerAccounts: {} as Record<string, unknown>,
|
||||
apiKeys: {} as Record<string, string>,
|
||||
providerSecrets: {} as Record<string, unknown>,
|
||||
defaultProvider: null as string | null,
|
||||
defaultProviderAccountId: null as string | null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return providerStore;
|
||||
}
|
||||
82
electron/services/secrets/secret-store.ts
Normal file
82
electron/services/secrets/secret-store.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { ProviderSecret } from '../../shared/providers/types';
|
||||
import { getClawXProviderStore } from '../providers/store-instance';
|
||||
|
||||
export interface SecretStore {
|
||||
get(accountId: string): Promise<ProviderSecret | null>;
|
||||
set(secret: ProviderSecret): Promise<void>;
|
||||
delete(accountId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class ElectronStoreSecretStore implements SecretStore {
|
||||
async get(accountId: string): Promise<ProviderSecret | null> {
|
||||
const store = await getClawXProviderStore();
|
||||
const secrets = (store.get('providerSecrets') ?? {}) as Record<string, ProviderSecret>;
|
||||
const secret = secrets[accountId];
|
||||
if (secret) {
|
||||
return secret;
|
||||
}
|
||||
|
||||
const apiKeys = (store.get('apiKeys') ?? {}) as Record<string, string>;
|
||||
const apiKey = apiKeys[accountId];
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'api_key',
|
||||
accountId,
|
||||
apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
async set(secret: ProviderSecret): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const secrets = (store.get('providerSecrets') ?? {}) as Record<string, ProviderSecret>;
|
||||
secrets[secret.accountId] = secret;
|
||||
store.set('providerSecrets', secrets);
|
||||
|
||||
// Keep legacy apiKeys in sync until the rest of the app moves to account-based secrets.
|
||||
const apiKeys = (store.get('apiKeys') ?? {}) as Record<string, string>;
|
||||
if (secret.type === 'api_key') {
|
||||
apiKeys[secret.accountId] = secret.apiKey;
|
||||
} else if (secret.type === 'local') {
|
||||
if (secret.apiKey) {
|
||||
apiKeys[secret.accountId] = secret.apiKey;
|
||||
} else {
|
||||
delete apiKeys[secret.accountId];
|
||||
}
|
||||
} else {
|
||||
delete apiKeys[secret.accountId];
|
||||
}
|
||||
store.set('apiKeys', apiKeys);
|
||||
}
|
||||
|
||||
async delete(accountId: string): Promise<void> {
|
||||
const store = await getClawXProviderStore();
|
||||
const secrets = (store.get('providerSecrets') ?? {}) as Record<string, ProviderSecret>;
|
||||
delete secrets[accountId];
|
||||
store.set('providerSecrets', secrets);
|
||||
|
||||
const apiKeys = (store.get('apiKeys') ?? {}) as Record<string, string>;
|
||||
delete apiKeys[accountId];
|
||||
store.set('apiKeys', apiKeys);
|
||||
}
|
||||
}
|
||||
|
||||
const secretStore = new ElectronStoreSecretStore();
|
||||
|
||||
export function getSecretStore(): SecretStore {
|
||||
return secretStore;
|
||||
}
|
||||
|
||||
export async function getProviderSecret(accountId: string): Promise<ProviderSecret | null> {
|
||||
return getSecretStore().get(accountId);
|
||||
}
|
||||
|
||||
export async function setProviderSecret(secret: ProviderSecret): Promise<void> {
|
||||
await getSecretStore().set(secret);
|
||||
}
|
||||
|
||||
export async function deleteProviderSecret(accountId: string): Promise<void> {
|
||||
await getSecretStore().delete(accountId);
|
||||
}
|
||||
Reference in New Issue
Block a user