refactor provider 1

This commit is contained in:
paisley
2026-03-07 20:52:12 +08:00
parent 5b7688e4b1
commit 17cee4e053
11 changed files with 904 additions and 192 deletions

View 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);
}

View 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;
}

View 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;
}

View 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;
}

View 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);
}