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
241 lines
6.5 KiB
TypeScript
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);
|
|
},
|
|
};
|