From c268f962afc30a7b15f926f3da602bea2cf334af Mon Sep 17 00:00:00 2001 From: farion1231 Date: Thu, 7 Aug 2025 15:48:30 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=A4=A7=E9=87=8D=E6=9E=84=EF=BC=9A?= =?UTF-8?q?=E4=BB=8E=E5=AD=97=E6=AE=B5=E6=9B=BF=E6=8D=A2=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=88=B0=E5=AE=8C=E6=95=B4=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现基于文件重命名的供应商切换机制,支持完整settings.json配置切换 - 移除所有向后兼容代码,简化为纯JSON配置模式 - 添加导入当前配置功能,解决首次使用时配置丢失问题 - 移除描述字段,简化用户界面 - 完整的错误处理和回滚机制确保配置安全 - 清理所有调试代码,优化代码质量 --- src/main/index.ts | 148 +++++++++--- src/main/preload.ts | 1 + src/main/services.ts | 178 ++++++++++++--- src/renderer/App.css | 15 ++ src/renderer/App.tsx | 29 +++ src/renderer/components/AddProviderModal.css | 57 +---- src/renderer/components/AddProviderModal.tsx | 212 +++++++----------- src/renderer/components/EditProviderModal.tsx | 140 +++++------- src/renderer/components/ImportConfigModal.tsx | 67 ++++++ src/renderer/components/ProviderList.tsx | 17 +- src/shared/types.ts | 6 +- src/shared/utils.ts | 42 ---- 12 files changed, 548 insertions(+), 364 deletions(-) create mode 100644 src/renderer/components/ImportConfigModal.tsx delete mode 100644 src/shared/utils.ts diff --git a/src/main/index.ts b/src/main/index.ts index acb48c7..73bb5c2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,7 +1,14 @@ import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' import path from 'path' import { Provider } from '../shared/types' -import { switchProvider, getClaudeCodeConfig } from './services' +import { + switchProvider, + getClaudeCodeConfig, + saveProviderConfig, + deleteProviderConfig, + sanitizeProviderName, + importCurrentConfig +} from './services' import { store } from './store' let mainWindow: BrowserWindow | null = null @@ -57,49 +64,136 @@ ipcMain.handle('getCurrentProvider', () => { }) ipcMain.handle('addProvider', async (_, provider: Provider) => { - const providers = store.get('providers', {} as Record) - providers[provider.id] = provider - await store.set('providers', providers) - return true + try { + // 1. 保存供应商配置到独立文件 + const saveSuccess = await saveProviderConfig(provider) + if (!saveSuccess) { + return false + } + + // 2. 更新应用配置 + 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 + } }) ipcMain.handle('deleteProvider', async (_, id: string) => { - const providers = store.get('providers', {} as Record) - delete providers[id] - await store.set('providers', providers) - return true + try { + // 1. 删除供应商配置文件 + const deleteSuccess = await deleteProviderConfig(id) + if (!deleteSuccess) { + 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) => { - const providers = store.get('providers', {} as Record) - const currentProviderId = store.get('current', '') - - providers[provider.id] = provider - await store.set('providers', providers) - - // 如果编辑的是当前激活的供应商,同时更新Claude Code配置 - if (provider.id === currentProviderId) { - const success = await switchProvider(provider) - if (!success) { - console.error('更新当前供应商的Claude Code配置失败') + 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() + } + 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 } - - return true }) ipcMain.handle('switchProvider', async (_, providerId: string) => { - const providers = store.get('providers', {} as Record) - const provider = providers[providerId] - if (provider) { - const success = await switchProvider(provider) + try { + const providers = store.get('providers', {} as Record) + const provider = providers[providerId] + const currentProviderId = store.get('current', '') + + if (!provider) { + 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) => { + try { + 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 } + } + + return result + } catch (error: any) { + console.error('导入配置失败:', error) + return { success: false } } - return false }) ipcMain.handle('getClaudeCodeConfigPath', () => { diff --git a/src/main/preload.ts b/src/main/preload.ts index 7523054..cf8cc27 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -8,6 +8,7 @@ contextBridge.exposeInMainWorld('electronAPI', { deleteProvider: (id: string) => ipcRenderer.invoke('deleteProvider', id), updateProvider: (provider: Provider) => ipcRenderer.invoke('updateProvider', provider), switchProvider: (providerId: string) => ipcRenderer.invoke('switchProvider', providerId), + importCurrentConfig: (name: string) => ipcRenderer.invoke('importCurrentConfig', name), getClaudeCodeConfigPath: () => ipcRenderer.invoke('getClaudeCodeConfigPath'), selectConfigFile: () => ipcRenderer.invoke('selectConfigFile'), openExternal: (url: string) => ipcRenderer.invoke('openExternal', url) diff --git a/src/main/services.ts b/src/main/services.ts index 14e7101..ae8b628 100644 --- a/src/main/services.ts +++ b/src/main/services.ts @@ -3,13 +3,11 @@ import path from 'path' import os from 'os' import { Provider } from '../shared/types' -interface ClaudeCodeConfig { - env?: { - ANTHROPIC_AUTH_TOKEN?: string - ANTHROPIC_BASE_URL?: string - [key: string]: string | undefined - } - [key: string]: any +/** + * 清理供应商名称,确保文件名安全 + */ +export function sanitizeProviderName(name: string): string { + return name.replace(/[<>:"/\\|?*]/g, '-').toLowerCase() } export function getClaudeCodeConfig() { @@ -20,37 +18,157 @@ export function getClaudeCodeConfig() { return { path: configPath, dir: configDir } } -export async function switchProvider(provider: Provider): Promise { - try { - const { path: configPath, dir: configDir } = getClaudeCodeConfig() +/** + * 获取供应商配置文件路径 + */ +export function getProviderConfigPath(providerId: string): string { + const { dir } = getClaudeCodeConfig() + return path.join(dir, `settings-${sanitizeProviderName(providerId)}.json`) +} +/** + * 保存供应商配置到独立文件 + */ +export async function saveProviderConfig(provider: Provider): Promise { + try { + const { dir } = getClaudeCodeConfig() + const providerConfigPath = getProviderConfigPath(provider.id) + + // 确保目录存在 + await fs.mkdir(dir, { recursive: true }) + + // 保存配置到供应商专用文件 + await fs.writeFile( + providerConfigPath, + JSON.stringify(provider.settingsConfig, null, 2), + 'utf-8' + ) + + return true + } catch (error) { + console.error('保存供应商配置失败:', error) + return false + } +} + +/** + * 检查文件是否存在 + */ +export async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +/** + * 切换供应商配置(基于文件重命名) + */ +export async function switchProvider(provider: Provider, currentProviderId?: string): Promise { + try { + const { path: settingsPath, dir: configDir } = getClaudeCodeConfig() + const newSettingsPath = getProviderConfigPath(provider.id) + // 确保目录存在 await fs.mkdir(configDir, { recursive: true }) - - // 读取现有配置 - let config: ClaudeCodeConfig = {} - try { - const content = await fs.readFile(configPath, 'utf-8') - config = JSON.parse(content) - } catch { - // 文件不存在或解析失败,使用空配置 + + // 检查目标配置文件是否存在 + if (!(await fileExists(newSettingsPath))) { + console.error(`供应商配置文件不存在: ${newSettingsPath}`) + return false } - - // 确保 env 对象存在 - if (!config.env) { - config.env = {} + + // 1. 如果当前存在settings.json,先备份到当前供应商的配置文件 + if (await fileExists(settingsPath)) { + if (currentProviderId) { + const currentProviderPath = getProviderConfigPath(currentProviderId) + 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}`) + } } - - // 更新环境变量配置 - config.env.ANTHROPIC_AUTH_TOKEN = provider.apiKey - config.env.ANTHROPIC_BASE_URL = provider.apiUrl - - // 写回配置文件 - await fs.writeFile(configPath, JSON.stringify(config, null, 2)) - + + // 2. 将目标供应商配置重命名为settings.json + await fs.rename(newSettingsPath, settingsPath) + + console.log(`成功切换到供应商: ${provider.name}`) return true } catch (error) { console.error('切换供应商失败:', error) return false } +} + +/** + * 导入当前 settings.json 配置为一个供应商 + */ +export async function importCurrentConfig(name: string): Promise<{ success: boolean; provider?: Provider }> { + try { + const { path: settingsPath } = getClaudeCodeConfig() + + // 检查当前配置是否存在 + if (!(await fileExists(settingsPath))) { + return { success: false } + } + + // 读取当前配置 + 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 + + // 检查ID是否已存在,如果存在则添加数字后缀 + while (await fileExists(getProviderConfigPath(providerId))) { + 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() + } + + // 保存为供应商配置 + const success = await saveProviderConfig(provider) + + if (success) { + console.log(`已导入当前配置为供应商: ${name} (${providerId})`) + return { success: true, provider } + } else { + return { success: false } + } + } catch (error: any) { + console.error('导入当前配置失败:', error) + return { success: false } + } +} + +/** + * 删除供应商配置文件 + */ +export async function deleteProviderConfig(providerId: string): Promise { + try { + const providerConfigPath = getProviderConfigPath(providerId) + + if (await fileExists(providerConfigPath)) { + await fs.unlink(providerConfigPath) + console.log(`已删除供应商配置文件: ${providerConfigPath}`) + } + + return true + } catch (error) { + console.error('删除供应商配置失败:', error) + return false + } } \ No newline at end of file diff --git a/src/renderer/App.css b/src/renderer/App.css index 5fa383d..627bc03 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -47,9 +47,24 @@ cursor: not-allowed; } +.import-btn { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.import-btn:hover { + background: rgba(255, 255, 255, 0.3); +} + +.import-btn:focus { + outline: none; +} + .add-btn { background: #27ae60; color: white; + border: none; } .add-btn:hover { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index dfd1fca..326027b 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -3,6 +3,7 @@ import { Provider } from "../shared/types"; import ProviderList from "./components/ProviderList"; import AddProviderModal from "./components/AddProviderModal"; import EditProviderModal from "./components/EditProviderModal"; +import ImportConfigModal from "./components/ImportConfigModal"; import { ConfirmDialog } from "./components/ConfirmDialog"; import "./App.css"; @@ -10,6 +11,7 @@ function App() { const [providers, setProviders] = useState>({}); const [currentProviderId, setCurrentProviderId] = useState(""); const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [configPath, setConfigPath] = useState(""); const [editingProviderId, setEditingProviderId] = useState( null @@ -139,6 +141,23 @@ function App() { } }; + const handleImportCurrentConfig = async (name: string) => { + try { + const result = await window.electronAPI.importCurrentConfig(name) + + if (result.success) { + await loadProviders() + setIsImportModalOpen(false) + showNotification(`成功导入当前配置为供应商: ${name}`, "success", 3000) + } else { + showNotification("导入失败,请检查当前是否有有效的配置文件", "error") + } + } catch (error) { + console.error('导入配置失败:', error) + showNotification("导入配置时发生错误", "error") + } + } + const handleSelectConfigFile = async () => { const selectedPath = await window.electronAPI.selectConfigFile(); if (selectedPath) { @@ -151,6 +170,9 @@ function App() {

Claude Code 供应商切换器

+ @@ -202,6 +224,13 @@ function App() { /> )} + {isImportModalOpen && ( + setIsImportModalOpen(false)} + /> + )} + {editingProviderId && providers[editingProviderId] && ( = ({ }) => { const [formData, setFormData] = useState({ name: "", - apiUrl: "", - apiKey: "", websiteUrl: "", + settingsConfig: "" }); - const [showPassword, setShowPassword] = useState(false); const [error, setError] = useState(""); + // 预设的供应商配置模板 + const presets = [ + { + name: "Anthropic 官方", + websiteUrl: "https://console.anthropic.com", + settingsConfig: { + "env": { + "ANTHROPIC_BASE_URL": "https://api.anthropic.com", + "ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here" + } + } + }, + { + name: "PackyCode", + websiteUrl: "https://www.packycode.com", + settingsConfig: { + "env": { + "ANTHROPIC_BASE_URL": "https://api.packycode.com", + "ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here" + } + } + }, + { + name: "YesCode", + websiteUrl: "https://yes.vg", + settingsConfig: { + "env": { + "ANTHROPIC_BASE_URL": "https://co.yes.vg", + "ANTHROPIC_AUTH_TOKEN": "cr-your-api-key-here" + } + } + } + ]; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setError(""); - if (!formData.name || !formData.apiUrl || !formData.apiKey) { - setError("请填写所有必填字段"); + if (!formData.name) { + setError("请填写供应商名称"); return; } - onAdd(formData); + if (!formData.settingsConfig.trim()) { + setError("请填写配置内容"); + return; + } + + let settingsConfig: object; + + try { + settingsConfig = JSON.parse(formData.settingsConfig); + } catch (err) { + setError("配置JSON格式错误,请检查语法"); + return; + } + + onAdd({ + name: formData.name, + websiteUrl: formData.websiteUrl, + settingsConfig + }); }; const handleChange = ( - e: React.ChangeEvent + e: React.ChangeEvent ) => { const { name, value } = e.target; - const newFormData = { + setFormData({ ...formData, [name]: value, - }; - - // 如果修改的是API地址,自动推测网站地址 - if (name === "apiUrl") { - newFormData.websiteUrl = inferWebsiteUrl(value); - } - - setFormData(newFormData); + }); }; - const handleApiUrlBlur = (e: React.FocusEvent) => { - const apiUrl = e.target.value.trim(); - if (apiUrl) { - let normalizedApiUrl = apiUrl; - - // 如果没有协议,添加 https:// - if (!normalizedApiUrl.match(/^https?:\/\//)) { - normalizedApiUrl = "https://" + normalizedApiUrl; - } - - setFormData((prev) => ({ - ...prev, - apiUrl: normalizedApiUrl, - websiteUrl: inferWebsiteUrl(normalizedApiUrl), - })); - } - }; - - // 预设的供应商配置 - const presets = [ - { - name: "Anthropic 官方", - apiUrl: "https://api.anthropic.com", - }, - { - name: "PackyCode", - apiUrl: "https://api.packycode.com", - }, - { - name: "YesCode", - apiUrl: "https://co.yes.vg", - }, - { - name: "AnyRouter", - apiUrl: "https://anyrouter.top", - }, - ]; - - const applyPreset = (preset: (typeof presets)[0]) => { - const newFormData = { - ...formData, + const applyPreset = (preset: typeof presets[0]) => { + setFormData({ name: preset.name, - apiUrl: preset.apiUrl, - }; - // 应用预设时也自动推测网站地址 - newFormData.websiteUrl = inferWebsiteUrl(preset.apiUrl); - setFormData(newFormData); + websiteUrl: preset.websiteUrl, + settingsConfig: JSON.stringify(preset.settingsConfig, null, 2) + }); }; return ( @@ -107,7 +108,7 @@ const AddProviderModal: React.FC = ({ {error &&
{error}
}
- +
{presets.map((preset, index) => (
- + -
- -
- - - - 用于在面板中显示可访问的网站链接,留空则显示API地址 -
- -
- - -
+ +