refactor: 清理 Electron 遗留代码并优化项目结构
- 删除 Electron 主进程代码 (src/main/) - 删除构建产物文件夹 (build/, dist/, release/) - 清理 package.json 中的 Electron 依赖和脚本 - 删除 TypeScript 配置中的 Electron 相关文件 - 优化前端代码结构至 Tauri 标准结构 (src/renderer → src/) - 删除移动端图标和不必要文件 - 更新文档说明技术栈变更为 Tauri
75
README.md
@@ -24,61 +24,15 @@
|
||||
|
||||
### Windows 用户
|
||||
|
||||
从 [Releases](../../releases) 页面下载:
|
||||
|
||||
- **安装版**: `CC-Switch-Setup-x.x.x.exe`
|
||||
- 自动创建桌面快捷方式和开始菜单项
|
||||
- **绿色版**: `CC-Switch-x.x.x.exe`
|
||||
- 无需安装,直接运行
|
||||
从 [Releases](../../releases) 页面下载最新版本的 Windows 安装包。
|
||||
|
||||
### macOS 用户
|
||||
|
||||
从 [Releases](../../releases) 页面下载:
|
||||
|
||||
- **通用版本**: `CC Switch-x.x.x-mac.zip` - Intel 版本,兼容所有 Mac(包括 M 系列芯片)
|
||||
|
||||
#### macOS 安装说明
|
||||
|
||||
通过 Rosetta 2 在 M 系列 Mac 上运行良好,兼容性最佳。
|
||||
|
||||
由于作者没有苹果开发者账号,应用使用 ad-hoc 签名(未经苹果官方认证),首次打开时可能出现"未知开发者"警告。这是正常的安全提示,处理方法:
|
||||
|
||||
**方法 1 - 系统设置**:
|
||||
|
||||
1. 双击应用弹出未知作者警告时选择"取消"
|
||||
2. 打开"系统设置" → "隐私与安全性"
|
||||
3. 在底部找到被阻止的应用,点击"仍要打开"
|
||||
4. 确认后即可正常使用
|
||||
|
||||
**方法 2 - 自行编译**:
|
||||
|
||||
1. Clone 代码到本地:`git clone https://github.com/farion1231/cc-switch.git`
|
||||
2. 安装依赖:`pnpm install`
|
||||
3. 编译代码:`pnpm run build`
|
||||
4. 打包应用:`pnpm run dist`
|
||||
5. 在项目 release 目录找到编译好的应用包
|
||||
|
||||
**安全保障**:
|
||||
|
||||
- 应用已通过 ad-hoc 代码签名,确保文件完整性
|
||||
- 源代码完全开源,可在 GitHub 审查
|
||||
- 本地存储配置,无网络传输风险
|
||||
|
||||
**技术说明**:
|
||||
|
||||
- 使用 Intel x64 架构,通过 Rosetta 2 在 M 系列芯片上运行
|
||||
- 兼容性和稳定性最佳,性能损失可接受
|
||||
- 避免了 ARM64 原生版本的签名复杂性问题
|
||||
从 [Releases](../../releases) 页面下载最新版本的 macOS 应用包。
|
||||
|
||||
### Linux 用户
|
||||
|
||||
- **AppImage**: `CC Switch-x.x.x.AppImage`
|
||||
|
||||
下载后添加执行权限:
|
||||
|
||||
```bash
|
||||
chmod +x CC-Switch-x.x.x.AppImage
|
||||
```
|
||||
从 [Releases](../../releases) 页面下载最新版本的 Linux 应用。
|
||||
|
||||
## 使用说明
|
||||
|
||||
@@ -92,35 +46,34 @@ chmod +x CC-Switch-x.x.x.AppImage
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
# 或
|
||||
npm install
|
||||
|
||||
# 开发模式
|
||||
pnpm run dev
|
||||
|
||||
# 构建应用
|
||||
pnpm run build
|
||||
|
||||
# 打包发布
|
||||
pnpm run dist
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Electron
|
||||
- Tauri 2.0
|
||||
- React
|
||||
- TypeScript
|
||||
- Vite
|
||||
- Rust
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── src/
|
||||
│ ├── main/ # 主进程代码
|
||||
│ ├── renderer/ # 渲染进程代码
|
||||
│ └── shared/ # 共享类型和工具
|
||||
├── build/ # 应用图标资源
|
||||
└── dist/ # 构建输出目录
|
||||
├── src/ # 前端代码 (React)
|
||||
│ ├── components/ # React 组件
|
||||
│ ├── config/ # 配置文件
|
||||
│ ├── lib/ # 工具库
|
||||
│ └── utils/ # 工具函数
|
||||
├── src-tauri/ # Tauri 后端代码 (Rust)
|
||||
│ ├── src/ # Rust 源代码
|
||||
│ └── icons/ # 应用图标资源
|
||||
└── screenshots/ # 截图资源
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
# Tauri 重构计划
|
||||
|
||||
## 项目概述
|
||||
|
||||
将 CC Switch 从 Electron 框架迁移到 Tauri,以大幅减少应用体积并提升性能。
|
||||
|
||||
### 目标收益
|
||||
|
||||
- **体积优化**: 77MB → ~8MB (减少 90%)
|
||||
- **内存占用**: 减少 60-70%
|
||||
- **启动速度**: 提升 3-5 倍
|
||||
- **安全性**: Rust 内存安全 + 细粒度权限控制
|
||||
|
||||
## 技术栈对比
|
||||
|
||||
| 技术层 | Electron (当前) | Tauri (目标) |
|
||||
| -------- | -------------------- | ------------------------- |
|
||||
| 后端 | Node.js + TypeScript | Rust |
|
||||
| 前端 | React + TypeScript | React + TypeScript (不变) |
|
||||
| IPC | Electron IPC | Tauri Commands |
|
||||
| 文件操作 | Node.js fs | Rust std::fs |
|
||||
| 配置存储 | electron-store | tauri-plugin-store |
|
||||
| 打包 | electron-builder | Tauri CLI |
|
||||
| WebView | Chromium (内置) | 系统 WebView |
|
||||
|
||||
## 迁移计划
|
||||
|
||||
### Phase 1: 环境准备 (Day 1 上午)
|
||||
|
||||
- [x] 安装 Rust 开发环境
|
||||
```bash
|
||||
# Windows: 下载 rustup-init.exe
|
||||
# https://www.rust-lang.org/tools/install
|
||||
```
|
||||
- [x] 安装 Tauri CLI
|
||||
```bash
|
||||
pnpm add -g @tauri-apps/cli
|
||||
```
|
||||
- [x] 在现有项目中集成 Tauri
|
||||
|
||||
```bash
|
||||
# 安装 Tauri CLI 作为开发依赖
|
||||
pnpm add -D @tauri-apps/cli
|
||||
|
||||
# 在现有项目中初始化 Tauri
|
||||
pnpm tauri init
|
||||
|
||||
# 安装 Tauri API 包
|
||||
pnpm add @tauri-apps/api
|
||||
```
|
||||
|
||||
### Phase 2: 项目结构调整 (Day 1 下午)
|
||||
|
||||
- [x] 创建 Tauri 项目配置
|
||||
- `src-tauri/` - Rust 后端代码
|
||||
- `src-tauri/tauri.conf.json` - Tauri 配置
|
||||
- `src-tauri/Cargo.toml` - Rust 依赖管理
|
||||
- [x] 迁移前端构建配置
|
||||
- 调整 Vite 配置适配 Tauri ✅
|
||||
- 更新 package.json scripts ✅
|
||||
- [x] 配置应用图标和元数据
|
||||
|
||||
### Phase 3: 后端功能迁移 (Day 2)
|
||||
|
||||
#### 3.1 核心功能模块 (上午)
|
||||
|
||||
- [x] **配置文件管理** (`src-tauri/src/config.rs`)
|
||||
- 读取 ~/.claude/settings.json
|
||||
- 写入配置文件
|
||||
- 备份/恢复配置
|
||||
- [x] **供应商管理** (`src-tauri/src/provider.rs`)
|
||||
- 供应商列表的增删改查
|
||||
- 供应商配置切换逻辑
|
||||
- 配置文件命名规则 (settings-{name}.json)
|
||||
|
||||
#### 3.2 Tauri Commands 实现 (下午)
|
||||
|
||||
```rust
|
||||
// 需要实现的命令列表 - 已完成
|
||||
#[tauri::command]
|
||||
async fn get_providers() -> Result<HashMap<String, Provider>, String>
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_current_provider() -> Result<String, String>
|
||||
|
||||
#[tauri::command]
|
||||
async fn add_provider(provider: Provider) -> Result<bool, String>
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_provider(provider: Provider) -> Result<bool, String>
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_provider(id: String) -> Result<bool, String>
|
||||
|
||||
#[tauri::command]
|
||||
async fn switch_provider(id: String) -> Result<bool, String>
|
||||
|
||||
#[tauri::command]
|
||||
async fn import_default_config() -> Result<bool, String>
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_claude_config_status() -> Result<ConfigStatus, String>
|
||||
```
|
||||
|
||||
#### 3.3 数据存储 (`src-tauri/src/store.rs`)
|
||||
|
||||
- [x] 使用 tauri-plugin-store 替代 electron-store
|
||||
- [x] 迁移配置存储逻辑 (~/.cc-switch/config.json)
|
||||
|
||||
### Phase 4: 前端适配 (Day 2 傍晚)
|
||||
|
||||
#### 4.1 API 层重构
|
||||
|
||||
- [x] 创建 `src/lib/tauri-api.ts`
|
||||
- 替换 Electron IPC 调用为 Tauri invoke
|
||||
- 保持 API 接口一致,减少组件改动
|
||||
|
||||
```typescript
|
||||
// 示例:迁移前后对比
|
||||
// Electron (旧)
|
||||
window.electronAPI.getProviders();
|
||||
|
||||
// Tauri (新)
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
invoke("get_providers");
|
||||
```
|
||||
|
||||
#### 4.2 最小化前端改动
|
||||
|
||||
- [x] 更新 preload 桥接逻辑
|
||||
- [x] 调整窗口控制相关代码
|
||||
- [x] 处理文件路径差异
|
||||
|
||||
### Phase 5: 测试与优化 (Day 3 上午)
|
||||
|
||||
#### 5.1 功能测试清单
|
||||
|
||||
- [ ] 供应商列表显示
|
||||
- [ ] 添加新供应商
|
||||
- [ ] 编辑供应商信息
|
||||
- [ ] 删除供应商
|
||||
- [ ] 切换供应商配置
|
||||
- [ ] 导入默认配置
|
||||
- [ ] 预设模板功能
|
||||
- [ ] API Key 快速输入
|
||||
|
||||
#### 5.2 跨平台测试
|
||||
|
||||
- [ ] Windows 10/11 测试
|
||||
- [ ] 不考虑 Windows 7/8 兼容性
|
||||
- [ ] macOS 测试 (如有条件)
|
||||
- [ ] Linux 测试 (如有条件)
|
||||
|
||||
#### 5.3 性能优化
|
||||
|
||||
- [ ] Rust 代码优化 (release 模式)
|
||||
- [ ] 减少不必要的文件 I/O
|
||||
- [ ] 优化启动加载流程
|
||||
|
||||
### Phase 6: 构建与发布 (Day 3 下午)
|
||||
|
||||
#### 6.1 构建配置
|
||||
|
||||
- [ ] 配置 GitHub Actions CI/CD
|
||||
- [ ] 设置代码签名 (Windows/macOS)
|
||||
- [ ] 配置自动更新机制
|
||||
|
||||
#### 6.2 打包发布
|
||||
|
||||
- [ ] Windows NSIS 安装包
|
||||
- [ ] Windows 便携版 (portable)
|
||||
- [ ] macOS .app 包
|
||||
- [ ] Linux AppImage
|
||||
|
||||
#### 6.3 版本发布
|
||||
|
||||
- [ ] 创建 3.0.0-beta.1 预发布
|
||||
- [ ] 编写迁移说明文档
|
||||
- [ ] 更新 README.md
|
||||
|
||||
## 风险与应对
|
||||
|
||||
### 技术风险
|
||||
|
||||
1. **Rust 学习曲线**
|
||||
|
||||
- 风险:Rust 语法相对复杂
|
||||
- 应对:专注于基础文件 I/O,使用成熟库
|
||||
|
||||
2. **WebView2 兼容性**
|
||||
|
||||
- 不需要支持旧版 Windows
|
||||
|
||||
3. **跨平台差异**
|
||||
- 风险:不同系统的文件路径处理
|
||||
- 应对:使用 Tauri API 统一处理路径
|
||||
|
||||
### 用户体验风险
|
||||
|
||||
1. **界面渲染差异**
|
||||
|
||||
- 风险:WebView 渲染可能与 Chromium 有细微差异
|
||||
- 应对:充分测试,必要时调整 CSS
|
||||
|
||||
2. **功能回归**
|
||||
- 风险:迁移过程中遗漏功能
|
||||
- 应对:严格按照测试清单验证
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如果 Tauri 版本出现严重问题:
|
||||
|
||||
1. 立即从 electron-legacy 分支发布修复版本
|
||||
2. 在 GitHub Release 页面提供两个版本下载
|
||||
3. 明确标注版本差异和适用场景
|
||||
|
||||
## 时间线
|
||||
|
||||
- **Day 1**: 环境搭建 + 项目结构
|
||||
- **Day 2**: 后端迁移 + 前端适配
|
||||
- **Day 3**: 测试优化 + 构建发布
|
||||
- **Total**: 3 个工作日完成迁移
|
||||
|
||||
## 成功标准
|
||||
|
||||
- ✅ 应用体积 < 10MB
|
||||
- ✅ 冷启动时间 < 1 秒
|
||||
- ✅ 所有现有功能正常工作
|
||||
- ✅ 通过所有测试用例
|
||||
- ✅ 成功构建三平台安装包
|
||||
|
||||
## 后续优化 (可选)
|
||||
|
||||
- 添加系统托盘功能
|
||||
- 实现自动更新机制
|
||||
- 添加快捷键支持
|
||||
- 优化动画效果
|
||||
- 支持深色模式跟随系统
|
||||
|
||||
---
|
||||
|
||||
_最后更新:2024-12-23_
|
||||
_负责人:Jason Young_
|
||||
_状态:进行中 - Phase 4 已完成_
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 161 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 312 KiB |
67
package.json
@@ -2,19 +2,12 @@
|
||||
"name": "cc-switch",
|
||||
"version": "2.0.3",
|
||||
"description": "Claude Code 供应商切换工具",
|
||||
"main": "dist/main/index.js",
|
||||
"scripts": {
|
||||
"dev": "pnpm tauri dev",
|
||||
"build": "pnpm tauri build",
|
||||
"tauri": "tauri",
|
||||
"dev:renderer": "vite",
|
||||
"build:renderer": "vite build",
|
||||
"dev:electron": "tsc -p tsconfig.main.json && electron .",
|
||||
"dev:electron:watch": "tsc -p tsconfig.main.json && concurrently -k \"tsc -w -p tsconfig.main.json\" \"npm:electron\"",
|
||||
"electron": "electron .",
|
||||
"build:main": "tsc -p tsconfig.main.json",
|
||||
"start:electron": "electron .",
|
||||
"dist:electron": "electron-builder"
|
||||
"build:renderer": "vite build"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Jason Young",
|
||||
@@ -24,9 +17,6 @@
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"concurrently": "^8.2.0",
|
||||
"electron": "^32.3.3",
|
||||
"electron-builder": "^24.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
@@ -35,60 +25,5 @@
|
||||
"@tauri-apps/plugin-shell": "^2.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.ccswitch.app",
|
||||
"productName": "CC Switch",
|
||||
"compression": "maximum",
|
||||
"publish": null,
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"icon": "build/icon.ico",
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
},
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools",
|
||||
"icon": "build/icon.icns",
|
||||
"identity": "-",
|
||||
"hardenedRuntime": false,
|
||||
"entitlements": null,
|
||||
"entitlementsInherit": null,
|
||||
"target": [
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "build/icon.ico"
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage",
|
||||
"icon": "build/icon.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,277 +0,0 @@
|
||||
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,
|
||||
importCurrentConfigAsDefault,
|
||||
getProviderConfigPath,
|
||||
fileExists,
|
||||
} from "./services";
|
||||
import { store } from "./store";
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../main/preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
// 使用 macOS 隐藏式标题栏,仅在 macOS 启用
|
||||
...(isMac ? { titleBarStyle: "hiddenInset" as const } : {}),
|
||||
autoHideMenuBar: true,
|
||||
});
|
||||
|
||||
if (app.isPackaged) {
|
||||
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
||||
} else {
|
||||
mainWindow.loadURL("http://localhost:3000");
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// IPC handlers
|
||||
ipcMain.handle("getProviders", async () => {
|
||||
return await store.get("providers", {} as Record<string, Provider>);
|
||||
});
|
||||
|
||||
ipcMain.handle("getCurrentProvider", async () => {
|
||||
return await store.get("current", "");
|
||||
});
|
||||
|
||||
ipcMain.handle("addProvider", async (_, provider: Provider) => {
|
||||
try {
|
||||
// 1. 保存供应商配置到独立文件
|
||||
const saveSuccess = await saveProviderConfig(provider);
|
||||
if (!saveSuccess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 更新应用配置
|
||||
const providers = await store.get("providers", {} as Record<string, Provider>);
|
||||
providers[provider.id] = provider;
|
||||
await store.set("providers", providers);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("添加供应商失败:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("deleteProvider", async (_, id: string) => {
|
||||
try {
|
||||
const providers = await store.get("providers", {} as Record<string, Provider>);
|
||||
const provider = providers[id];
|
||||
|
||||
// 1. 删除供应商配置文件
|
||||
const deleteSuccess = await deleteProviderConfig(id, provider?.name);
|
||||
if (!deleteSuccess) {
|
||||
console.error("删除供应商配置文件失败");
|
||||
// 仍然继续删除应用配置,避免配置不同步
|
||||
}
|
||||
|
||||
// 2. 更新应用配置
|
||||
delete providers[id];
|
||||
await store.set("providers", providers);
|
||||
|
||||
// 3. 如果删除的是当前供应商,清空当前选择
|
||||
const currentProviderId = await 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 = await store.get("providers", {} as Record<string, Provider>);
|
||||
const currentProviderId = await 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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 保存更新后的配置到文件
|
||||
const saveSuccess = await saveProviderConfig(provider);
|
||||
if (!saveSuccess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 更新应用配置
|
||||
providers[provider.id] = provider;
|
||||
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 = await store.get("providers", {} as Record<string, Provider>);
|
||||
const provider = providers[providerId];
|
||||
const currentProviderId = await store.get("current", "");
|
||||
|
||||
if (!provider) {
|
||||
console.error(`供应商不存在: ${providerId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行切换
|
||||
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("importCurrentConfigAsDefault", async () => {
|
||||
try {
|
||||
const result = await importCurrentConfigAsDefault();
|
||||
|
||||
if (result.success && result.provider) {
|
||||
// 将默认供应商添加到store中
|
||||
const providers = await store.get("providers", {} as Record<string, Provider>);
|
||||
providers[result.provider.id] = result.provider;
|
||||
await store.set("providers", providers);
|
||||
|
||||
// 设置为当前选中的供应商
|
||||
await store.set("current", result.provider.id);
|
||||
|
||||
return { success: true, providerId: result.provider.id };
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("导入默认配置失败:", error);
|
||||
return { success: false };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("getClaudeCodeConfigPath", () => {
|
||||
return getClaudeCodeConfig().path;
|
||||
});
|
||||
|
||||
ipcMain.handle("selectConfigFile", async () => {
|
||||
if (!mainWindow) return null;
|
||||
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ["openFile"],
|
||||
title: "选择 Claude Code 配置文件",
|
||||
filters: [
|
||||
{ name: "JSON 文件", extensions: ["json"] },
|
||||
{ name: "所有文件", extensions: ["*"] },
|
||||
],
|
||||
defaultPath: "settings.json",
|
||||
});
|
||||
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.filePaths[0];
|
||||
});
|
||||
|
||||
ipcMain.handle("openConfigFolder", async () => {
|
||||
try {
|
||||
const { dir } = getClaudeCodeConfig();
|
||||
await shell.openPath(dir);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("打开配置文件夹失败:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("openExternal", async (_, url: string) => {
|
||||
try {
|
||||
await shell.openExternal(url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("打开外部链接失败:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { Provider } from '../shared/types'
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getProviders: () => ipcRenderer.invoke('getProviders'),
|
||||
getCurrentProvider: () => ipcRenderer.invoke('getCurrentProvider'),
|
||||
addProvider: (provider: Provider) => ipcRenderer.invoke('addProvider', provider),
|
||||
deleteProvider: (id: string) => ipcRenderer.invoke('deleteProvider', id),
|
||||
updateProvider: (provider: Provider) => ipcRenderer.invoke('updateProvider', provider),
|
||||
switchProvider: (providerId: string) => ipcRenderer.invoke('switchProvider', providerId),
|
||||
importCurrentConfigAsDefault: () => ipcRenderer.invoke('importCurrentConfigAsDefault'),
|
||||
getClaudeCodeConfigPath: () => ipcRenderer.invoke('getClaudeCodeConfigPath'),
|
||||
selectConfigFile: () => ipcRenderer.invoke('selectConfigFile'),
|
||||
openConfigFolder: () => ipcRenderer.invoke('openConfigFolder'),
|
||||
openExternal: (url: string) => ipcRenderer.invoke('openExternal', url)
|
||||
})
|
||||
|
||||
// 暴露平台信息给渲染进程,用于平台特定样式控制
|
||||
contextBridge.exposeInMainWorld('platform', {
|
||||
isMac: process.platform === 'darwin'
|
||||
})
|
||||
@@ -1,181 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
export function getClaudeCodeConfig() {
|
||||
// Claude Code 配置文件路径
|
||||
const configDir = path.join(os.homedir(), ".claude");
|
||||
const configPath = path.join(configDir, "settings.json");
|
||||
|
||||
return { path: configPath, dir: configDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取供应商配置文件路径(基于供应商名称)
|
||||
*/
|
||||
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`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存供应商配置到独立文件
|
||||
*/
|
||||
export async function saveProviderConfig(provider: Provider): Promise<boolean> {
|
||||
try {
|
||||
const { dir } = getClaudeCodeConfig();
|
||||
const providerConfigPath = getProviderConfigPath(
|
||||
provider.id,
|
||||
provider.name
|
||||
);
|
||||
|
||||
// 确保目录存在
|
||||
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<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换供应商配置(基于文件复制)
|
||||
*/
|
||||
export async function switchProvider(
|
||||
provider: Provider,
|
||||
currentProviderId?: string,
|
||||
providers?: Record<string, Provider>
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const { path: settingsPath, dir: configDir } = getClaudeCodeConfig();
|
||||
|
||||
// 确保目录存在
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
|
||||
const newSettingsPath = getProviderConfigPath(provider.id, provider.name);
|
||||
|
||||
// 检查目标配置文件是否存在
|
||||
if (!(await fileExists(newSettingsPath))) {
|
||||
console.error(`供应商配置文件不存在: ${newSettingsPath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. 如果当前存在settings.json,先备份到当前供应商的配置文件
|
||||
if (await fileExists(settingsPath)) {
|
||||
if (currentProviderId && providers && providers[currentProviderId]) {
|
||||
const currentProvider = providers[currentProviderId];
|
||||
const currentProviderPath = getProviderConfigPath(
|
||||
currentProviderId,
|
||||
currentProvider.name
|
||||
);
|
||||
// 使用复制而不是重命名,避免删除原文件
|
||||
await fs.copyFile(settingsPath, currentProviderPath);
|
||||
console.log(`已备份当前供应商配置: ${currentProvider.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 复制新配置到settings.json(保留原文件)
|
||||
await fs.copyFile(newSettingsPath, settingsPath);
|
||||
|
||||
console.log(`成功切换到供应商: ${provider.name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("切换供应商失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入当前配置为默认供应商
|
||||
*/
|
||||
export async function importCurrentConfigAsDefault(): 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);
|
||||
|
||||
// 创建默认供应商对象
|
||||
const provider: Provider = {
|
||||
id: "default",
|
||||
name: "default",
|
||||
settingsConfig: settingsConfig,
|
||||
};
|
||||
|
||||
// 保存默认供应商的配置到独立文件
|
||||
const saveSuccess = await saveProviderConfig(provider);
|
||||
if (!saveSuccess) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
console.log(`已导入当前配置为默认供应商,配置文件:settings-default.json`);
|
||||
return { success: true, provider };
|
||||
} catch (error) {
|
||||
console.error("导入默认配置失败:", error);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除供应商配置文件
|
||||
*/
|
||||
export async function deleteProviderConfig(
|
||||
providerId: string,
|
||||
providerName?: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const providerConfigPath = getProviderConfigPath(providerId, providerName);
|
||||
|
||||
if (await fileExists(providerConfigPath)) {
|
||||
await fs.unlink(providerConfigPath);
|
||||
console.log(`已删除供应商配置文件: ${providerConfigPath}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("删除供应商配置失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { AppConfig } from '../shared/types'
|
||||
|
||||
export class SimpleStore {
|
||||
private configPath: string
|
||||
private configDir: string
|
||||
private data: AppConfig = { providers: {}, current: '' }
|
||||
private initPromise: Promise<void>
|
||||
|
||||
constructor() {
|
||||
this.configDir = path.join(os.homedir(), '.cc-switch')
|
||||
this.configPath = path.join(this.configDir, 'config.json')
|
||||
// 立即开始加载,但不阻塞构造函数
|
||||
this.initPromise = this.loadData()
|
||||
}
|
||||
|
||||
private async loadData(): Promise<void> {
|
||||
try {
|
||||
const content = await fs.readFile(this.configPath, 'utf-8')
|
||||
this.data = JSON.parse(content)
|
||||
} catch (error) {
|
||||
// 文件不存在或格式错误,使用默认数据
|
||||
this.data = { providers: {}, current: '' }
|
||||
await this.saveData()
|
||||
}
|
||||
}
|
||||
|
||||
private async saveData(): Promise<void> {
|
||||
try {
|
||||
// 确保目录存在
|
||||
await fs.mkdir(this.configDir, { recursive: true })
|
||||
// 写入配置文件
|
||||
await fs.writeFile(this.configPath, JSON.stringify(this.data, null, 2), 'utf-8')
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(key: keyof AppConfig, defaultValue?: T): Promise<T> {
|
||||
await this.initPromise // 等待初始化完成
|
||||
const value = this.data[key] as T
|
||||
return value !== undefined ? value : (defaultValue as T)
|
||||
}
|
||||
|
||||
async set<K extends keyof AppConfig>(key: K, value: AppConfig[K]): Promise<void> {
|
||||
await this.initPromise // 等待初始化完成
|
||||
this.data[key] = value
|
||||
await this.saveData()
|
||||
}
|
||||
|
||||
// 获取配置文件路径,用于调试
|
||||
getConfigPath(): string {
|
||||
return this.configPath
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例
|
||||
export const store = new SimpleStore()
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/main/**/*", "src/shared/**/*"]
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
root: resolve(__dirname, 'src/renderer'),
|
||||
base: './',
|
||||
build: {
|
||||
outDir: resolve(__dirname, 'build'),
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true
|
||||
},
|
||||
server: {
|
||||
|
||||