Files
zn-ai/electron/service/provider-api-service.ts
DEV_DSW 416399e7a8 feat: implement menu service for context menu management
feat: add provider API service for managing provider accounts and keys
feat: create provider runtime sync service for agent runtime management
feat: introduce script execution service for running automation scripts
feat: develop script store service for managing script metadata and storage
feat: implement theme service for managing application theme settings
feat: add updater service for handling application updates
feat: create window service for managing application windows and their states
2026-04-22 09:26:39 +08:00

241 lines
6.5 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import logManager from '@electron/service/logger';
import { PROVIDER_TYPE_INFO } from '@runtime/lib/providers';
import type {
ProviderAccount,
ProviderVendorInfo,
ProviderWithKeyInfo,
} from '@runtime/lib/providers';
import { getUserDataDir } from '@electron/utils/paths';
interface ProviderStore {
accounts: ProviderAccount[];
defaultAccountId: string | null;
}
const defaultStore: ProviderStore = {
accounts: [],
defaultAccountId: null,
};
const storePath = path.join(getUserDataDir(), 'provider-accounts.json');
const keysPath = path.join(getUserDataDir(), 'provider-keys.json');
function readJson<T>(filePath: string, defaultValue: T): T {
try {
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
}
} catch (e) {
logManager.error(`Failed to read ${filePath}:`, e);
}
return defaultValue;
}
function writeJson(filePath: string, data: unknown) {
try {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
} catch (e) {
logManager.error(`Failed to write ${filePath}:`, e);
}
}
function getStore(): ProviderStore {
return readJson(storePath, defaultStore);
}
function saveStore(store: ProviderStore) {
writeJson(storePath, store);
}
function getKeys(): Record<string, string> {
return readJson(keysPath, {});
}
function saveKeys(keys: Record<string, string>) {
writeJson(keysPath, keys);
}
function mapToProviderWithKeyInfo(account: ProviderAccount): ProviderWithKeyInfo {
const keys = getKeys();
const hasKey = !!keys[account.id];
return {
id: account.id,
name: account.label,
type: account.vendorId as any,
baseUrl: account.baseUrl,
apiProtocol: account.apiProtocol,
headers: account.headers,
model: account.model,
fallbackModels: account.fallbackModels,
fallbackProviderIds: account.fallbackAccountIds,
enabled: account.enabled,
createdAt: account.createdAt,
updatedAt: account.updatedAt,
hasKey,
keyMasked: hasKey ? '••••••••' : null,
};
}
type ProviderChangeListener = () => void;
const listeners: ProviderChangeListener[] = [];
export function onProviderChange(listener: ProviderChangeListener): () => void {
listeners.push(listener);
return () => {
const idx = listeners.indexOf(listener);
if (idx > -1) listeners.splice(idx, 1);
};
}
function notifyChange() {
listeners.forEach((l) => l());
}
function mapToVendorInfo(info: typeof PROVIDER_TYPE_INFO[number]): ProviderVendorInfo {
return {
...info,
category:
info.id === 'ollama'
? 'local'
: info.id === 'custom'
? 'custom'
: 'compatible',
supportedAuthModes: info.requiresApiKey
? info.isOAuth
? ['api_key', 'oauth_browser']
: ['api_key']
: info.isOAuth
? ['local', 'oauth_browser']
: ['local'],
defaultAuthMode: info.requiresApiKey ? 'api_key' : 'local',
supportsMultipleAccounts: true,
} as ProviderVendorInfo;
}
function sanitizeAccount(account: ProviderAccount): ProviderAccount {
let model = account.model;
if (model) {
// Fix corrupted DeepSeek model IDs stored in legacy accounts
if (model === 'deepseek-chat/deepseek-reasoner' || model.startsWith('deepseek-chat/')) {
model = 'deepseek-chat';
} else if (model.startsWith('deepseek-reasoner/')) {
model = 'deepseek-reasoner';
}
}
if (model !== account.model) {
return { ...account, model };
}
return account;
}
export const providerApiService = {
getVendors(): ProviderVendorInfo[] {
return PROVIDER_TYPE_INFO.map(mapToVendorInfo);
},
getAccounts(): ProviderAccount[] {
return getStore().accounts.map(sanitizeAccount);
},
getProviders(): ProviderWithKeyInfo[] {
return getStore().accounts.map(sanitizeAccount).map(mapToProviderWithKeyInfo);
},
getDefault(): { accountId: string | null } {
return { accountId: getStore().defaultAccountId };
},
createAccount(body: { account: ProviderAccount; apiKey?: string }) {
const store = getStore();
const account = { ...body.account, updatedAt: new Date().toISOString() };
store.accounts.push(account);
if (body.apiKey) {
const keys = getKeys();
keys[account.id] = body.apiKey;
saveKeys(keys);
}
saveStore(store);
notifyChange();
return { success: true };
},
updateAccount(
accountId: string,
body: { updates: Partial<ProviderAccount>; apiKey?: string }
) {
const store = getStore();
const idx = store.accounts.findIndex((a) => a.id === accountId);
if (idx === -1) return { success: false, error: 'Account not found' };
store.accounts[idx] = {
...store.accounts[idx],
...body.updates,
updatedAt: new Date().toISOString(),
};
if (body.apiKey) {
const keys = getKeys();
keys[accountId] = body.apiKey;
saveKeys(keys);
}
saveStore(store);
notifyChange();
return { success: true };
},
deleteAccount(accountId: string) {
const store = getStore();
store.accounts = store.accounts.filter((a) => a.id !== accountId);
if (store.defaultAccountId === accountId) store.defaultAccountId = null;
saveStore(store);
const keys = getKeys();
delete keys[accountId];
saveKeys(keys);
notifyChange();
return { success: true };
},
setDefault(body: { accountId: string }) {
const store = getStore();
const accountExists = store.accounts.some((a) => a.id === body.accountId);
if (!accountExists) {
return { success: false, error: 'Account not found' };
}
store.defaultAccountId = body.accountId;
saveStore(store);
notifyChange();
return { success: true };
},
validateApiKey(body: {
providerId: string;
apiKey: string;
options?: { baseUrl?: string; apiProtocol?: string };
}) {
if (!body.apiKey || body.apiKey.trim().length === 0) {
return { valid: false, error: 'API key is required' };
}
// TODO: perform real validation against provider endpoint
return { valid: true };
},
getApiKey(providerId: string) {
const keys = getKeys();
return { apiKey: keys[providerId] || null };
},
deleteApiKey(accountId: string) {
const keys = getKeys();
delete keys[accountId];
saveKeys(keys);
notifyChange();
return { success: true };
},
async getUsageHistory(limit?: number) {
const { getRecentTokenUsageHistory } = await import('@electron/utils/token-usage');
return getRecentTokenUsageHistory(limit);
},
};