diff --git a/src/main/index.ts b/src/main/index.ts index 73bb5c2..059fdf0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,231 +1,269 @@ -import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' -import path from 'path' -import { Provider } from '../shared/types' -import { - switchProvider, - getClaudeCodeConfig, - saveProviderConfig, +import { app, BrowserWindow, ipcMain, dialog, shell } from "electron"; +import path from "path"; +import fs from "fs/promises"; +import { Provider } from "../shared/types"; +import { + switchProvider, + getClaudeCodeConfig, + saveProviderConfig, deleteProviderConfig, sanitizeProviderName, - importCurrentConfig -} from './services' -import { store } from './store' + importCurrentConfig, + getProviderConfigPath, + fileExists, +} from "./services"; +import { store } from "./store"; -let mainWindow: BrowserWindow | null = null +let mainWindow: BrowserWindow | null = null; function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { - preload: path.join(__dirname, '../main/preload.js'), + preload: path.join(__dirname, "../main/preload.js"), contextIsolation: true, - nodeIntegration: false + nodeIntegration: false, }, - titleBarStyle: 'hiddenInset', - autoHideMenuBar: true - }) + titleBarStyle: "hiddenInset", + autoHideMenuBar: true, + }); if (app.isPackaged) { - mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) + mainWindow.loadFile(path.join(__dirname, "../renderer/index.html")); } else { - mainWindow.loadURL('http://localhost:3000') - mainWindow.webContents.openDevTools() + mainWindow.loadURL("http://localhost:3000"); + mainWindow.webContents.openDevTools(); } - mainWindow.on('closed', () => { - mainWindow = null - }) + mainWindow.on("closed", () => { + mainWindow = null; + }); } app.whenReady().then(() => { - createWindow() + createWindow(); - app.on('activate', () => { + app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { - createWindow() + createWindow(); } - }) -}) + }); +}); -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); } -}) +}); // IPC handlers -ipcMain.handle('getProviders', () => { - return store.get('providers', {} as Record) -}) +ipcMain.handle("getProviders", () => { + return store.get("providers", {} as Record); +}); -ipcMain.handle('getCurrentProvider', () => { - return store.get('current', '') -}) +ipcMain.handle("getCurrentProvider", () => { + return store.get("current", ""); +}); -ipcMain.handle('addProvider', async (_, provider: Provider) => { +ipcMain.handle("addProvider", async (_, provider: Provider) => { try { // 1. 保存供应商配置到独立文件 - const saveSuccess = await saveProviderConfig(provider) + const saveSuccess = await saveProviderConfig(provider); if (!saveSuccess) { - return false + return false; } - + // 2. 更新应用配置 - const providers = store.get('providers', {} as Record) + const providers = store.get("providers", {} as Record); providers[provider.id] = { ...provider, createdAt: Date.now(), - updatedAt: Date.now() - } - await store.set('providers', providers) - - return true - } catch (error) { - console.error('添加供应商失败:', error) - return false - } -}) + updatedAt: Date.now(), + }; + await store.set("providers", providers); -ipcMain.handle('deleteProvider', async (_, id: string) => { + return true; + } catch (error) { + console.error("添加供应商失败:", error); + return false; + } +}); + +ipcMain.handle("deleteProvider", async (_, id: string) => { try { + const providers = store.get("providers", {} as Record); + const provider = providers[id]; + // 1. 删除供应商配置文件 - const deleteSuccess = await deleteProviderConfig(id) + const deleteSuccess = await deleteProviderConfig(id, provider?.name); if (!deleteSuccess) { - console.error('删除供应商配置文件失败') + console.error("删除供应商配置文件失败"); // 仍然继续删除应用配置,避免配置不同步 } - - // 2. 更新应用配置 - const providers = store.get('providers', {} as Record) - delete providers[id] - await store.set('providers', providers) - - // 3. 如果删除的是当前供应商,清空当前选择 - const currentProviderId = store.get('current', '') - if (currentProviderId === id) { - await store.set('current', '') - } - - return true - } catch (error) { - console.error('删除供应商失败:', error) - return false - } -}) -ipcMain.handle('updateProvider', async (_, provider: Provider) => { - try { - const providers = store.get('providers', {} as Record) - const currentProviderId = store.get('current', '') - - // 1. 保存更新后的配置到文件 - const saveSuccess = await saveProviderConfig({ - ...provider, - updatedAt: Date.now() - }) - if (!saveSuccess) { - return false - } - // 2. 更新应用配置 - providers[provider.id] = { - ...provider, - updatedAt: Date.now() + delete providers[id]; + await store.set("providers", providers); + + // 3. 如果删除的是当前供应商,清空当前选择 + const currentProviderId = store.get("current", ""); + if (currentProviderId === id) { + await store.set("current", ""); } - await store.set('providers', providers) - - // 3. 如果编辑的是当前激活的供应商,需要重新切换以应用更改 - if (provider.id === currentProviderId) { - const switchSuccess = await switchProvider(provider, currentProviderId) - if (!switchSuccess) { - console.error('更新当前供应商的Claude Code配置失败') - return false + + return true; + } catch (error) { + console.error("删除供应商失败:", error); + return false; + } +}); + +ipcMain.handle("updateProvider", async (_, provider: Provider) => { + try { + const providers = store.get("providers", {} as Record); + const currentProviderId = store.get("current", ""); + const oldProvider = providers[provider.id]; + + // 1. 如果名字发生变化,需要重命名配置文件 + if (oldProvider && oldProvider.name !== provider.name) { + const oldConfigPath = getProviderConfigPath( + provider.id, + oldProvider.name + ); + const newConfigPath = getProviderConfigPath(provider.id, provider.name); + + // 如果旧配置文件存在且路径不同,需要重命名 + if ( + (await fileExists(oldConfigPath)) && + oldConfigPath !== newConfigPath + ) { + // 如果新路径已存在文件,先删除避免冲突 + if (await fileExists(newConfigPath)) { + await fs.unlink(newConfigPath); + } + await fs.rename(oldConfigPath, newConfigPath); + console.log( + `已重命名配置文件: ${oldProvider.name} -> ${provider.name}` + ); } } - - return true - } catch (error) { - console.error('更新供应商失败:', error) - return false - } -}) -ipcMain.handle('switchProvider', async (_, providerId: string) => { + // 2. 保存更新后的配置到文件 + const saveSuccess = await saveProviderConfig({ + ...provider, + updatedAt: Date.now(), + }); + if (!saveSuccess) { + return false; + } + + // 3. 更新应用配置 + providers[provider.id] = { + ...provider, + updatedAt: Date.now(), + }; + await store.set("providers", providers); + + // 4. 如果编辑的是当前激活的供应商,需要重新切换以应用更改 + if (provider.id === currentProviderId) { + const switchSuccess = await switchProvider( + provider, + currentProviderId, + providers + ); + if (!switchSuccess) { + console.error("更新当前供应商的Claude Code配置失败"); + return false; + } + } + + return true; + } catch (error) { + console.error("更新供应商失败:", error); + return false; + } +}); + +ipcMain.handle("switchProvider", async (_, providerId: string) => { try { - const providers = store.get('providers', {} as Record) - const provider = providers[providerId] - const currentProviderId = store.get('current', '') - + const providers = store.get("providers", {} as Record); + const provider = providers[providerId]; + const currentProviderId = store.get("current", ""); + if (!provider) { - console.error(`供应商不存在: ${providerId}`) - return false + console.error(`供应商不存在: ${providerId}`); + return false; } - - // 执行切换 - const success = await switchProvider(provider, currentProviderId) - if (success) { - await store.set('current', providerId) - console.log(`成功切换到供应商: ${provider.name}`) - } - - return success - } catch (error) { - console.error('切换供应商失败:', error) - return false - } -}) -ipcMain.handle('importCurrentConfig', async (_, name: string) => { + // 执行切换 + const success = await switchProvider( + provider, + currentProviderId, + providers + ); + if (success) { + await store.set("current", providerId); + console.log(`成功切换到供应商: ${provider.name}`); + } + + return success; + } catch (error) { + console.error("切换供应商失败:", error); + return false; + } +}); + +ipcMain.handle("importCurrentConfig", async (_, name: string) => { try { - const result = await importCurrentConfig(name) - + const result = await importCurrentConfig(name); + if (result.success && result.provider) { // 将导入的供应商添加到store中 - const providers = store.get('providers', {} as Record) - providers[result.provider.id] = result.provider - await store.set('providers', providers) - - return { success: true, providerId: result.provider.id } + const providers = store.get("providers", {} as Record); + providers[result.provider.id] = result.provider; + await store.set("providers", providers); + + return { success: true, providerId: result.provider.id }; } - - return result + + return result; } catch (error: any) { - console.error('导入配置失败:', error) - return { success: false } + console.error("导入配置失败:", error); + return { success: false }; } -}) +}); -ipcMain.handle('getClaudeCodeConfigPath', () => { - return getClaudeCodeConfig().path -}) +ipcMain.handle("getClaudeCodeConfigPath", () => { + return getClaudeCodeConfig().path; +}); + +ipcMain.handle("selectConfigFile", async () => { + if (!mainWindow) return null; -ipcMain.handle('selectConfigFile', async () => { - if (!mainWindow) return null - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openFile'], - title: '选择 Claude Code 配置文件', + properties: ["openFile"], + title: "选择 Claude Code 配置文件", filters: [ - { name: 'JSON 文件', extensions: ['json'] }, - { name: '所有文件', extensions: ['*'] } + { name: "JSON 文件", extensions: ["json"] }, + { name: "所有文件", extensions: ["*"] }, ], - defaultPath: 'settings.json' - }) - - if (result.canceled || result.filePaths.length === 0) { - return null - } - - return result.filePaths[0] -}) + defaultPath: "settings.json", + }); -ipcMain.handle('openExternal', async (_, url: string) => { - try { - await shell.openExternal(url) - return true - } catch (error) { - console.error('打开外部链接失败:', error) - return false + if (result.canceled || result.filePaths.length === 0) { + return null; } -}) \ No newline at end of file + + return result.filePaths[0]; +}); + +ipcMain.handle("openExternal", async (_, url: string) => { + try { + await shell.openExternal(url); + return true; + } catch (error) { + console.error("打开外部链接失败:", error); + return false; + } +}); diff --git a/src/main/services.ts b/src/main/services.ts index ae8b628..1ec23f4 100644 --- a/src/main/services.ts +++ b/src/main/services.ts @@ -1,29 +1,37 @@ -import fs from 'fs/promises' -import path from 'path' -import os from 'os' -import { Provider } from '../shared/types' +import fs from "fs/promises"; +import path from "path"; +import os from "os"; +import { Provider } from "../shared/types"; /** * 清理供应商名称,确保文件名安全 */ export function sanitizeProviderName(name: string): string { - return name.replace(/[<>:"/\\|?*]/g, '-').toLowerCase() + return name.replace(/[<>:"/\\|?*]/g, "-").toLowerCase(); } export function getClaudeCodeConfig() { // Claude Code 配置文件路径 - const configDir = path.join(os.homedir(), '.claude') - const configPath = path.join(configDir, 'settings.json') + const configDir = path.join(os.homedir(), ".claude"); + const configPath = path.join(configDir, "settings.json"); - return { path: configPath, dir: configDir } + return { path: configPath, dir: configDir }; } /** - * 获取供应商配置文件路径 + * 获取供应商配置文件路径(基于供应商名称) */ -export function getProviderConfigPath(providerId: string): string { - const { dir } = getClaudeCodeConfig() - return path.join(dir, `settings-${sanitizeProviderName(providerId)}.json`) +export function getProviderConfigPath( + providerId: string, + providerName?: string +): string { + const { dir } = getClaudeCodeConfig(); + + // 如果提供了名称,使用名称;否则使用ID(向后兼容) + const baseName = providerName + ? sanitizeProviderName(providerName) + : sanitizeProviderName(providerId); + return path.join(dir, `settings-${baseName}.json`); } /** @@ -31,23 +39,26 @@ export function getProviderConfigPath(providerId: string): string { */ export async function saveProviderConfig(provider: Provider): Promise { try { - const { dir } = getClaudeCodeConfig() - const providerConfigPath = getProviderConfigPath(provider.id) - + const { dir } = getClaudeCodeConfig(); + const providerConfigPath = getProviderConfigPath( + provider.id, + provider.name + ); + // 确保目录存在 - await fs.mkdir(dir, { recursive: true }) - + await fs.mkdir(dir, { recursive: true }); + // 保存配置到供应商专用文件 await fs.writeFile( providerConfigPath, JSON.stringify(provider.settingsConfig, null, 2), - 'utf-8' - ) - - return true + "utf-8" + ); + + return true; } catch (error) { - console.error('保存供应商配置失败:', error) - return false + console.error("保存供应商配置失败:", error); + return false; } } @@ -56,119 +67,137 @@ export async function saveProviderConfig(provider: Provider): Promise { */ export async function fileExists(filePath: string): Promise { try { - await fs.access(filePath) - return true + await fs.access(filePath); + return true; } catch { - return false + return false; } } /** * 切换供应商配置(基于文件重命名) */ -export async function switchProvider(provider: Provider, currentProviderId?: string): Promise { +export async function switchProvider( + provider: Provider, + currentProviderId?: string, + providers?: Record +): Promise { try { - const { path: settingsPath, dir: configDir } = getClaudeCodeConfig() - const newSettingsPath = getProviderConfigPath(provider.id) - + const { path: settingsPath, dir: configDir } = getClaudeCodeConfig(); + const newSettingsPath = getProviderConfigPath(provider.id, provider.name); + // 确保目录存在 - await fs.mkdir(configDir, { recursive: true }) - + await fs.mkdir(configDir, { recursive: true }); + // 检查目标配置文件是否存在 if (!(await fileExists(newSettingsPath))) { - console.error(`供应商配置文件不存在: ${newSettingsPath}`) - return false + console.error(`供应商配置文件不存在: ${newSettingsPath}`); + return false; } - + // 1. 如果当前存在settings.json,先备份到当前供应商的配置文件 if (await fileExists(settingsPath)) { - if (currentProviderId) { - const currentProviderPath = getProviderConfigPath(currentProviderId) - await fs.rename(settingsPath, currentProviderPath) + if (currentProviderId && providers && providers[currentProviderId]) { + const currentProvider = providers[currentProviderId]; + const currentProviderPath = getProviderConfigPath( + currentProviderId, + currentProvider.name + ); + await fs.rename(settingsPath, currentProviderPath); } else { // 如果没有当前供应商ID,创建临时备份 - const backupPath = path.join(configDir, `settings-backup-${Date.now()}.json`) - await fs.rename(settingsPath, backupPath) - console.log(`已备份当前配置到: ${backupPath}`) + const backupPath = path.join( + configDir, + `settings-backup-${Date.now()}.json` + ); + await fs.rename(settingsPath, backupPath); + console.log(`已备份当前配置到: ${backupPath}`); } } - + // 2. 将目标供应商配置重命名为settings.json - await fs.rename(newSettingsPath, settingsPath) - - console.log(`成功切换到供应商: ${provider.name}`) - return true + await fs.rename(newSettingsPath, settingsPath); + + console.log(`成功切换到供应商: ${provider.name}`); + return true; } catch (error) { - console.error('切换供应商失败:', error) - return false + console.error("切换供应商失败:", error); + return false; } } /** * 导入当前 settings.json 配置为一个供应商 */ -export async function importCurrentConfig(name: string): Promise<{ success: boolean; provider?: Provider }> { +export async function importCurrentConfig( + name: string +): Promise<{ success: boolean; provider?: Provider }> { try { - const { path: settingsPath } = getClaudeCodeConfig() - + const { path: settingsPath } = getClaudeCodeConfig(); + // 检查当前配置是否存在 if (!(await fileExists(settingsPath))) { - return { success: false } + return { success: false }; } - + // 读取当前配置 - const configContent = await fs.readFile(settingsPath, 'utf-8') - const settingsConfig = JSON.parse(configContent) - + const configContent = await fs.readFile(settingsPath, "utf-8"); + const settingsConfig = JSON.parse(configContent); + // 生成唯一的供应商ID - let providerId = name.toLowerCase().replace(/[^a-z0-9]/g, '-') - let counter = 1 - + let providerId = name.toLowerCase().replace(/[^a-z0-9]/g, "-"); + let counter = 1; + // 检查ID是否已存在,如果存在则添加数字后缀 - while (await fileExists(getProviderConfigPath(providerId))) { - providerId = `${name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${counter}` - counter++ + while (await fileExists(getProviderConfigPath(providerId, name))) { + providerId = `${name + .toLowerCase() + .replace(/[^a-z0-9]/g, "-")}-${counter}`; + counter++; } - + // 创建供应商对象 const provider: Provider = { id: providerId, name: name, settingsConfig: settingsConfig, createdAt: Date.now(), - updatedAt: Date.now() - } - + updatedAt: Date.now(), + }; + // 保存为供应商配置 - const success = await saveProviderConfig(provider) - + const success = await saveProviderConfig(provider); + if (success) { - console.log(`已导入当前配置为供应商: ${name} (${providerId})`) - return { success: true, provider } + console.log(`已导入当前配置为供应商: ${name} (${providerId})`); + return { success: true, provider }; } else { - return { success: false } + return { success: false }; } } catch (error: any) { - console.error('导入当前配置失败:', error) - return { success: false } + console.error("导入当前配置失败:", error); + return { success: false }; } } /** * 删除供应商配置文件 */ -export async function deleteProviderConfig(providerId: string): Promise { +export async function deleteProviderConfig( + providerId: string, + providerName?: string +): Promise { try { - const providerConfigPath = getProviderConfigPath(providerId) - + const providerConfigPath = getProviderConfigPath(providerId, providerName); + if (await fileExists(providerConfigPath)) { - await fs.unlink(providerConfigPath) - console.log(`已删除供应商配置文件: ${providerConfigPath}`) + await fs.unlink(providerConfigPath); + console.log(`已删除供应商配置文件: ${providerConfigPath}`); } - - return true + + return true; } catch (error) { - console.error('删除供应商配置失败:', error) - return false + console.error("删除供应商配置失败:", error); + return false; } -} \ No newline at end of file +}