feat: 重构对话功能
This commit is contained in:
239
electron/service/provider-api-service/index.ts
Normal file
239
electron/service/provider-api-service/index.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { app } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import logManager from '@electron/service/logger';
|
||||
import { PROVIDER_TYPE_INFO } from '@lib/providers';
|
||||
import type {
|
||||
ProviderAccount,
|
||||
ProviderVendorInfo,
|
||||
ProviderWithKeyInfo,
|
||||
} from '@lib/providers';
|
||||
|
||||
interface ProviderStore {
|
||||
accounts: ProviderAccount[];
|
||||
defaultAccountId: string | null;
|
||||
}
|
||||
|
||||
const defaultStore: ProviderStore = {
|
||||
accounts: [],
|
||||
defaultAccountId: null,
|
||||
};
|
||||
|
||||
const storePath = path.join(app.getPath('userData'), 'provider-accounts.json');
|
||||
const keysPath = path.join(app.getPath('userData'), '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 };
|
||||
},
|
||||
|
||||
getUsageHistory() {
|
||||
return [] as any[];
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user