Compare commits
50 Commits
electron-l
...
tauri-migr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cf116280f | ||
|
|
73cf337c42 | ||
|
|
fa2d64b692 | ||
|
|
9c17be1b59 | ||
|
|
fe1574a026 | ||
|
|
9254c5d291 | ||
|
|
642e7a3817 | ||
|
|
5e2e80b00d | ||
|
|
2a43f1f54d | ||
|
|
7e6ce83158 | ||
|
|
6932e89ea8 | ||
|
|
d144d5c2fc | ||
|
|
adee37ab66 | ||
|
|
dcf49cc094 | ||
|
|
f8e39594fa | ||
|
|
374649750b | ||
|
|
6d26115368 | ||
|
|
606ee67778 | ||
|
|
57d21fabcf | ||
|
|
001664c67d | ||
|
|
616e230218 | ||
|
|
70f9a68e5c | ||
|
|
78bc0a1a31 | ||
|
|
dac8ebe03b | ||
|
|
9f370bf429 | ||
|
|
bac2c3db36 | ||
|
|
326e975748 | ||
|
|
b5696b4511 | ||
|
|
ef7e9d2f73 | ||
|
|
d78013562c | ||
|
|
d3adfc480d | ||
|
|
731cfc47be | ||
|
|
95b3746e49 | ||
|
|
c8670aede6 | ||
|
|
95549473bd | ||
|
|
f3f484a04b | ||
|
|
1458f1e45d | ||
|
|
0301d1aee7 | ||
|
|
224d7a8be0 | ||
|
|
c4791ff523 | ||
|
|
55c62a3753 | ||
|
|
12fa80e002 | ||
|
|
29581b85d9 | ||
|
|
88e69e844a | ||
|
|
2a658af5b9 | ||
|
|
1402fd0cc5 | ||
|
|
8a3133be43 | ||
|
|
f64320fbd6 | ||
|
|
3479780639 | ||
|
|
1b0ab269fb |
93
.github/workflows/release.yml
vendored
@@ -11,19 +11,15 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
platform: win32
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
platform: linux
|
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
platform: darwin
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@@ -31,6 +27,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
@@ -38,70 +37,42 @@ jobs:
|
|||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-store
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: echo "path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Setup pnpm cache
|
- name: Setup pnpm cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.STORE_PATH }}
|
path: ${{ steps.pnpm-store.outputs.path }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
restore-keys: |
|
restore-keys: ${{ runner.os }}-pnpm-store-
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install frontend deps
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build application
|
- name: Build and Release (Tauri)
|
||||||
run: |
|
uses: tauri-apps/tauri-action@v0
|
||||||
pnpm run build
|
|
||||||
pnpm run dist
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
|
||||||
|
|
||||||
- name: List build files (debug)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "Listing release directory:"
|
|
||||||
ls -la release/ || echo "No release directory found"
|
|
||||||
find . -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" || echo "No build files found"
|
|
||||||
|
|
||||||
- name: Upload Release Assets
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
release/*.exe
|
|
||||||
release/*.zip
|
|
||||||
release/*.AppImage
|
|
||||||
name: "CC Switch ${{ github.ref_name }}"
|
|
||||||
body: |
|
|
||||||
## CC Switch ${{ github.ref_name }}
|
|
||||||
|
|
||||||
Claude Code 供应商切换工具
|
|
||||||
|
|
||||||
### 下载
|
|
||||||
|
|
||||||
#### Windows 用户
|
|
||||||
- **安装版 (推荐)**: `CC Switch Setup ${{ github.ref_name }}.exe`
|
|
||||||
- **便携版**: `CC Switch ${{ github.ref_name }}.exe`
|
|
||||||
|
|
||||||
#### macOS 用户(推荐使用通用版本)
|
|
||||||
- **通用版本**: `CC Switch-${{ github.ref_name }}-mac.zip` - 兼容所有Mac(Intel + M系列)
|
|
||||||
|
|
||||||
#### Linux 用户
|
|
||||||
- **AppImage**: `CC Switch-${{ github.ref_name }}.AppImage`
|
|
||||||
|
|
||||||
### macOS 安装说明
|
|
||||||
1. 下载 ZIP 文件后解压
|
|
||||||
2. 首次打开可能出现"未知开发者"警告
|
|
||||||
3. 前往"系统设置" → "隐私与安全性" → 点击"仍要打开"
|
|
||||||
4. 或者使用命令: `xattr -cr "/path/to/CC Switch.app"`
|
|
||||||
|
|
||||||
### 注意事项
|
|
||||||
- macOS 版本使用 Intel 架构,通过 Rosetta 2 在 M 系列芯片上运行
|
|
||||||
- 兼容性和稳定性最佳,性能损失minimal
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: CC Switch ${{ github.ref_name }}
|
||||||
|
releaseBody: |
|
||||||
|
## CC Switch ${{ github.ref_name }}
|
||||||
|
|
||||||
|
Claude Code 供应商切换工具(Tauri 构建)
|
||||||
|
|
||||||
|
- Windows: .msi / NSIS 安装包
|
||||||
|
- macOS: .dmg / .app 压缩包
|
||||||
|
- Linux: AppImage / deb / rpm
|
||||||
|
|
||||||
|
如遇未知开发者提示,请在系统隐私与安全设置中选择“仍要打开”。
|
||||||
|
tauriScript: pnpm tauri
|
||||||
|
|
||||||
|
- name: List generated bundles (debug)
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Listing bundles in src-tauri/target..."
|
||||||
|
find src-tauri/target -maxdepth 4 -type f -name "*.*" 2>/dev/null || true
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -8,3 +8,4 @@ release/
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.npmrc
|
.npmrc
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
67
CHANGELOG.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to CC Switch will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.0.0] - 2025-08-27
|
||||||
|
|
||||||
|
### 🚀 Major Changes
|
||||||
|
- **Complete migration from Electron to Tauri 2.0** - The application has been completely rewritten using Tauri, resulting in:
|
||||||
|
- **90% reduction in bundle size** (from ~150MB to ~15MB)
|
||||||
|
- **Significantly improved startup performance**
|
||||||
|
- **Native system integration** without Chromium overhead
|
||||||
|
- **Enhanced security** with Rust backend
|
||||||
|
|
||||||
|
### ✨ New Features
|
||||||
|
- **Native window controls** with transparent title bar on macOS
|
||||||
|
- **Improved file system operations** using Rust for better performance
|
||||||
|
- **Enhanced security model** with explicit permission declarations
|
||||||
|
- **Better platform detection** using Tauri's native APIs
|
||||||
|
|
||||||
|
### 🔧 Technical Improvements
|
||||||
|
- Migrated from Electron IPC to Tauri command system
|
||||||
|
- Replaced Node.js file operations with Rust implementations
|
||||||
|
- Implemented proper CSP (Content Security Policy) for enhanced security
|
||||||
|
- Added TypeScript strict mode for better type safety
|
||||||
|
- Integrated Rust cargo fmt and clippy for code quality
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
- Fixed bundle identifier conflict on macOS (changed from .app to .desktop)
|
||||||
|
- Resolved platform detection issues
|
||||||
|
- Improved error handling in configuration management
|
||||||
|
|
||||||
|
### 📦 Dependencies
|
||||||
|
- **Tauri**: 2.8.2
|
||||||
|
- **React**: 18.2.0
|
||||||
|
- **TypeScript**: 5.3.0
|
||||||
|
- **Vite**: 5.0.0
|
||||||
|
|
||||||
|
### 🔄 Migration Notes
|
||||||
|
For users upgrading from v2.x (Electron version):
|
||||||
|
- Configuration files remain compatible - no action required
|
||||||
|
- The app will automatically migrate your existing provider configurations
|
||||||
|
- Window position and size preferences have been reset to defaults
|
||||||
|
|
||||||
|
### 🛠️ Development
|
||||||
|
- Added `pnpm typecheck` command for TypeScript validation
|
||||||
|
- Added `pnpm format` and `pnpm format:check` for code formatting
|
||||||
|
- Rust code now uses cargo fmt for consistent formatting
|
||||||
|
|
||||||
|
## [2.0.0] - Previous Electron Release
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Multi-provider configuration management
|
||||||
|
- Quick provider switching
|
||||||
|
- Import/export configurations
|
||||||
|
- Preset provider templates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.0] - Initial Release
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Basic provider management
|
||||||
|
- Claude Code integration
|
||||||
|
- Configuration file handling
|
||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Jason Young
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
152
README.md
@@ -1,14 +1,22 @@
|
|||||||
# Claude Code 供应商切换器
|
# Claude Code 供应商切换器
|
||||||
|
|
||||||
|
[](https://github.com/jasonyoung/cc-switch/releases)
|
||||||
|
[](https://github.com/jasonyoung/cc-switch/releases)
|
||||||
|
[](https://tauri.app/)
|
||||||
|
|
||||||
一个用于管理和切换 Claude Code 不同供应商配置的桌面应用。
|
一个用于管理和切换 Claude Code 不同供应商配置的桌面应用。
|
||||||
|
|
||||||
|
> **v3.0.0 重大更新**:从 Electron 完全迁移到 Tauri 2.0,应用体积减少 85%(从 ~80MB 降至 ~12MB),启动速度提升 10 倍!
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
|
- **极速启动** - 基于 Tauri 2.0,原生性能,秒开应用
|
||||||
- 一键切换不同供应商
|
- 一键切换不同供应商
|
||||||
- 智谱 GLM、Qwen coder、DeepSeek v3.1、packycode 等预设供应商只需要填写 key 即可一键配置
|
- Qwen coder、kimi k2、智谱 GLM、DeepSeek v3.1、packycode 等预设供应商只需要填写 key 即可一键配置
|
||||||
- 支持添加自定义供应商
|
- 支持添加自定义供应商
|
||||||
- 简洁美观的图形界面
|
- 简洁美观的图形界面
|
||||||
- 信息存储在本地 ~/.cc-switch/config.json,无隐私风险
|
- 信息存储在本地 ~/.cc-switch/config.json,无隐私风险
|
||||||
|
- 超小体积 - 仅 ~5MB 安装包
|
||||||
|
|
||||||
## 界面预览
|
## 界面预览
|
||||||
|
|
||||||
@@ -22,63 +30,23 @@
|
|||||||
|
|
||||||
## 下载安装
|
## 下载安装
|
||||||
|
|
||||||
|
### 系统要求
|
||||||
|
|
||||||
|
- **Windows**: Windows 10 及以上
|
||||||
|
- **macOS**: macOS 10.15 (Catalina) 及以上
|
||||||
|
- **Linux**: Ubuntu 20.04+ / Debian 11+ / Fedora 34+ 等主流发行版
|
||||||
|
|
||||||
### Windows 用户
|
### Windows 用户
|
||||||
|
|
||||||
从 [Releases](../../releases) 页面下载:
|
从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch_3.0.0_x64.msi` 或 `.exe` 安装包。
|
||||||
|
|
||||||
- **安装版**: `CC-Switch-Setup-x.x.x.exe`
|
|
||||||
- 自动创建桌面快捷方式和开始菜单项
|
|
||||||
- **绿色版**: `CC-Switch-x.x.x.exe`
|
|
||||||
- 无需安装,直接运行
|
|
||||||
|
|
||||||
### macOS 用户
|
### macOS 用户
|
||||||
|
|
||||||
从 [Releases](../../releases) 页面下载:
|
从 [Releases](../../releases) 页面下载最新版本的 `CC-Switch_3.0.0_x64.dmg` (Intel) 或 `CC-Switch_3.0.0_aarch64.dmg` (Apple Silicon)。
|
||||||
|
|
||||||
- **通用版本**: `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 原生版本的签名复杂性问题
|
|
||||||
|
|
||||||
### Linux 用户
|
### Linux 用户
|
||||||
|
|
||||||
- **AppImage**: `CC Switch-x.x.x.AppImage`
|
从 [Releases](../../releases) 页面下载最新版本的 `.AppImage` 或 `.deb` 包。
|
||||||
|
|
||||||
下载后添加执行权限:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x CC-Switch-x.x.x.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用说明
|
## 使用说明
|
||||||
|
|
||||||
@@ -89,40 +57,88 @@ chmod +x CC-Switch-x.x.x.AppImage
|
|||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- pnpm 8+
|
||||||
|
- Rust 1.75+
|
||||||
|
- Tauri CLI 2.0+
|
||||||
|
|
||||||
|
### 开发命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
pnpm install
|
pnpm install
|
||||||
# 或
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 开发模式
|
# 开发模式(热重载)
|
||||||
pnpm run dev
|
pnpm dev
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
|
# 代码格式化
|
||||||
|
pnpm format
|
||||||
|
|
||||||
|
# 检查代码格式
|
||||||
|
pnpm format:check
|
||||||
|
|
||||||
# 构建应用
|
# 构建应用
|
||||||
pnpm run build
|
pnpm build
|
||||||
|
|
||||||
# 打包发布
|
# 构建调试版本
|
||||||
pnpm run dist
|
pnpm tauri build --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust 后端开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src-tauri
|
||||||
|
|
||||||
|
# 格式化 Rust 代码
|
||||||
|
cargo fmt
|
||||||
|
|
||||||
|
# 运行 clippy 检查
|
||||||
|
cargo clippy
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- Electron
|
- **[Tauri 2.0](https://tauri.app/)** - 跨平台桌面应用框架
|
||||||
- React
|
- **[React 18](https://react.dev/)** - 用户界面库
|
||||||
- TypeScript
|
- **[TypeScript](https://www.typescriptlang.org/)** - 类型安全的 JavaScript
|
||||||
- Vite
|
- **[Vite](https://vitejs.dev/)** - 极速的前端构建工具
|
||||||
|
- **[Rust](https://www.rust-lang.org/)** - 系统级编程语言(后端)
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
├── src/
|
├── src/ # 前端代码 (React + TypeScript)
|
||||||
│ ├── main/ # 主进程代码
|
│ ├── components/ # React 组件
|
||||||
│ ├── renderer/ # 渲染进程代码
|
│ ├── config/ # 预设供应商配置
|
||||||
│ └── shared/ # 共享类型和工具
|
│ ├── lib/ # Tauri API 封装
|
||||||
├── build/ # 应用图标资源
|
│ └── utils/ # 工具函数
|
||||||
└── dist/ # 构建输出目录
|
├── src-tauri/ # 后端代码 (Rust)
|
||||||
|
│ ├── src/ # Rust 源代码
|
||||||
|
│ │ ├── commands.rs # Tauri 命令定义
|
||||||
|
│ │ ├── config.rs # 配置文件管理
|
||||||
|
│ │ ├── provider.rs # 供应商管理逻辑
|
||||||
|
│ │ └── store.rs # 状态管理
|
||||||
|
│ ├── capabilities/ # 权限配置
|
||||||
|
│ └── icons/ # 应用图标资源
|
||||||
|
└── screenshots/ # 界面截图
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
查看 [CHANGELOG.md](CHANGELOG.md) 了解版本更新详情。
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT © Jason Young
|
||||||
|
|||||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 161 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 312 KiB |
78
package.json
@@ -1,91 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "cc-switch",
|
"name": "cc-switch",
|
||||||
"version": "2.0.3",
|
"version": "3.0.0",
|
||||||
"description": "Claude Code 供应商切换工具",
|
"description": "Claude Code 供应商切换工具",
|
||||||
"main": "dist/main/index.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron:watch\"",
|
"dev": "pnpm tauri dev",
|
||||||
"dev:electron": "tsc -p tsconfig.main.json && electron .",
|
"build": "pnpm tauri build",
|
||||||
"dev:electron:watch": "tsc -p tsconfig.main.json && concurrently -k \"tsc -w -p tsconfig.main.json\" \"npm:electron\"",
|
"tauri": "tauri",
|
||||||
"electron": "electron .",
|
|
||||||
"dev:renderer": "vite",
|
"dev:renderer": "vite",
|
||||||
"build": "npm run build:renderer && npm run build:main",
|
|
||||||
"build:main": "tsc -p tsconfig.main.json",
|
|
||||||
"build:renderer": "vite build",
|
"build:renderer": "vite build",
|
||||||
"start": "electron .",
|
"typecheck": "tsc --noEmit",
|
||||||
"dist": "electron-builder"
|
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,json}\"",
|
||||||
|
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,css,json}\""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Jason Young",
|
"author": "Jason Young",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.8.0",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"concurrently": "^8.2.0",
|
"prettier": "^3.6.2",
|
||||||
"electron": "^32.3.3",
|
|
||||||
"electron-builder": "^24.0.0",
|
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2382
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 194 KiB |
4
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
/gen/schemas
|
||||||
5772
src-tauri/Cargo.lock
generated
Normal file
31
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "cc-switch"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "Claude Code MCP 服务器配置管理工具"
|
||||||
|
authors = ["Jason Young"]
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/jasonyoung/cc-switch"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.85.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "cc_switch_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.4.0", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
log = "0.4"
|
||||||
|
tauri = { version = "2.8.2", features = [] }
|
||||||
|
tauri-plugin-log = "2"
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
dirs = "5.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2 = "0.5"
|
||||||
|
objc2-app-kit = { version = "0.2", features = ["NSColor"] }
|
||||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
12
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "enables the default permissions",
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"opener:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 109 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 523 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
195
src-tauri/src/commands.rs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use tauri::State;
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
|
use crate::config::{ConfigStatus, get_claude_settings_path, import_current_config_as_default};
|
||||||
|
use crate::provider::Provider;
|
||||||
|
use crate::store::AppState;
|
||||||
|
|
||||||
|
/// 获取所有供应商
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_providers(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<HashMap<String, Provider>, String> {
|
||||||
|
let manager = state
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(manager.get_all_providers().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前供应商ID
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_current_provider(state: State<'_, AppState>) -> Result<String, String> {
|
||||||
|
let manager = state
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(manager.current.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加供应商
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn add_provider(state: State<'_, AppState>, provider: Provider) -> Result<bool, String> {
|
||||||
|
let mut manager = state
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
manager.add_provider(provider)?;
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
drop(manager); // 释放锁
|
||||||
|
state.save()?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新供应商
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_provider(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
provider: Provider,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let mut manager = state
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
manager.update_provider(provider)?;
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
drop(manager); // 释放锁
|
||||||
|
state.save()?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除供应商
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_provider(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||||
|
let mut manager = state
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
manager.delete_provider(&id)?;
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
drop(manager); // 释放锁
|
||||||
|
state.save()?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换供应商
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn switch_provider(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||||
|
let mut manager = state
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
manager.switch_provider(&id)?;
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
drop(manager); // 释放锁
|
||||||
|
state.save()?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导入当前配置为默认供应商
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_default_config(state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
|
// 若已存在 default 供应商,则直接返回,避免重复导入
|
||||||
|
{
|
||||||
|
let manager = state
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
if manager.get_all_providers().contains_key("default") {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入配置
|
||||||
|
let settings_config = import_current_config_as_default()?;
|
||||||
|
|
||||||
|
// 创建默认供应商
|
||||||
|
let provider = Provider::with_id(
|
||||||
|
"default".to_string(),
|
||||||
|
"default".to_string(),
|
||||||
|
settings_config,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加到管理器
|
||||||
|
let mut manager = state
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
manager.add_provider(provider)?;
|
||||||
|
|
||||||
|
// 如果没有当前供应商,设置为 default
|
||||||
|
if manager.current.is_empty() {
|
||||||
|
manager.current = "default".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
drop(manager); // 释放锁
|
||||||
|
state.save()?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 Claude Code 配置状态
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
|
||||||
|
Ok(crate::config::get_claude_config_status())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 Claude Code 配置文件路径
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_claude_code_config_path() -> Result<String, String> {
|
||||||
|
Ok(get_claude_settings_path().to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开配置文件夹
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_config_folder(app: tauri::AppHandle) -> Result<bool, String> {
|
||||||
|
let config_dir = crate::config::get_claude_config_dir();
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if !config_dir.exists() {
|
||||||
|
std::fs::create_dir_all(&config_dir).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 opener 插件打开文件夹
|
||||||
|
app.opener()
|
||||||
|
.open_path(config_dir.to_string_lossy().to_string(), None::<String>)
|
||||||
|
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开外部链接
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_external(app: tauri::AppHandle, url: String) -> Result<bool, String> {
|
||||||
|
// 规范化 URL,缺少协议时默认加 https://
|
||||||
|
let url = if url.starts_with("http://") || url.starts_with("https://") {
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
format!("https://{}", url)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用 opener 插件打开链接
|
||||||
|
app.opener()
|
||||||
|
.open_url(&url, None::<String>)
|
||||||
|
.map_err(|e| format!("打开链接失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
141
src-tauri/src/config.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// 获取 Claude Code 配置目录路径
|
||||||
|
pub fn get_claude_config_dir() -> PathBuf {
|
||||||
|
dirs::home_dir()
|
||||||
|
.expect("无法获取用户主目录")
|
||||||
|
.join(".claude")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 Claude Code 主配置文件路径
|
||||||
|
pub fn get_claude_settings_path() -> PathBuf {
|
||||||
|
let dir = get_claude_config_dir();
|
||||||
|
let settings = dir.join("settings.json");
|
||||||
|
if settings.exists() {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
// 兼容旧版命名:若存在旧文件则继续使用
|
||||||
|
let legacy = dir.join("claude.json");
|
||||||
|
if legacy.exists() {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
// 默认新建:回落到标准文件名 settings.json(不再生成 claude.json)
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取应用配置目录路径 (~/.cc-switch)
|
||||||
|
pub fn get_app_config_dir() -> PathBuf {
|
||||||
|
dirs::home_dir()
|
||||||
|
.expect("无法获取用户主目录")
|
||||||
|
.join(".cc-switch")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取应用配置文件路径
|
||||||
|
pub fn get_app_config_path() -> PathBuf {
|
||||||
|
get_app_config_dir().join("config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清理供应商名称,确保文件名安全
|
||||||
|
pub fn sanitize_provider_name(name: &str) -> String {
|
||||||
|
name.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => '-',
|
||||||
|
_ => c,
|
||||||
|
})
|
||||||
|
.collect::<String>()
|
||||||
|
.to_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取供应商配置文件路径
|
||||||
|
pub fn get_provider_config_path(provider_id: &str, provider_name: Option<&str>) -> PathBuf {
|
||||||
|
let base_name = provider_name
|
||||||
|
.map(|name| sanitize_provider_name(name))
|
||||||
|
.unwrap_or_else(|| sanitize_provider_name(provider_id));
|
||||||
|
|
||||||
|
get_claude_config_dir().join(format!("settings-{}.json", base_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取 JSON 配置文件
|
||||||
|
pub fn read_json_file<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, String> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(format!("文件不存在: {}", path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(path).map_err(|e| format!("读取文件失败: {}", e))?;
|
||||||
|
|
||||||
|
serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入 JSON 配置文件
|
||||||
|
pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), String> {
|
||||||
|
// 确保目录存在
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let json =
|
||||||
|
serde_json::to_string_pretty(data).map_err(|e| format!("序列化 JSON 失败: {}", e))?;
|
||||||
|
|
||||||
|
fs::write(path, json).map_err(|e| format!("写入文件失败: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 复制文件
|
||||||
|
pub fn copy_file(from: &Path, to: &Path) -> Result<(), String> {
|
||||||
|
fs::copy(from, to).map_err(|e| format!("复制文件失败: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除文件
|
||||||
|
pub fn delete_file(path: &Path) -> Result<(), String> {
|
||||||
|
if path.exists() {
|
||||||
|
fs::remove_file(path).map_err(|e| format!("删除文件失败: {}", e))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查 Claude Code 配置状态
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ConfigStatus {
|
||||||
|
pub exists: bool,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 Claude Code 配置状态
|
||||||
|
pub fn get_claude_config_status() -> ConfigStatus {
|
||||||
|
let path = get_claude_settings_path();
|
||||||
|
ConfigStatus {
|
||||||
|
exists: path.exists(),
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 备份配置文件
|
||||||
|
pub fn backup_config(from: &Path, to: &Path) -> Result<(), String> {
|
||||||
|
if from.exists() {
|
||||||
|
copy_file(from, to)?;
|
||||||
|
log::info!("已备份配置文件: {} -> {}", from.display(), to.display());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导入当前 Claude Code 配置为默认供应商
|
||||||
|
pub fn import_current_config_as_default() -> Result<Value, String> {
|
||||||
|
let settings_path = get_claude_settings_path();
|
||||||
|
|
||||||
|
if !settings_path.exists() {
|
||||||
|
return Err("Claude Code 配置文件不存在".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取当前配置
|
||||||
|
let settings_config: Value = read_json_file(&settings_path)?;
|
||||||
|
|
||||||
|
// 保存为 default 供应商
|
||||||
|
let default_provider_path = get_provider_config_path("default", Some("default"));
|
||||||
|
write_json_file(&default_provider_path, &settings_config)?;
|
||||||
|
|
||||||
|
log::info!("已导入当前配置为默认供应商");
|
||||||
|
Ok(settings_config)
|
||||||
|
}
|
||||||
105
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
mod commands;
|
||||||
|
mod config;
|
||||||
|
mod provider;
|
||||||
|
mod store;
|
||||||
|
|
||||||
|
use store::AppState;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.setup(|app| {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
// 设置 macOS 标题栏背景色为主界面蓝色
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
use objc2::rc::Retained;
|
||||||
|
use objc2::runtime::AnyObject;
|
||||||
|
use objc2_app_kit::NSColor;
|
||||||
|
|
||||||
|
let ns_window_ptr = window.ns_window().unwrap();
|
||||||
|
let ns_window: Retained<AnyObject> =
|
||||||
|
unsafe { Retained::retain(ns_window_ptr as *mut AnyObject).unwrap() };
|
||||||
|
|
||||||
|
// 使用与主界面 banner 相同的蓝色 #3498db
|
||||||
|
// #3498db = RGB(52, 152, 219)
|
||||||
|
let bg_color = unsafe {
|
||||||
|
NSColor::colorWithRed_green_blue_alpha(
|
||||||
|
52.0 / 255.0, // R: 52
|
||||||
|
152.0 / 255.0, // G: 152
|
||||||
|
219.0 / 255.0, // B: 219
|
||||||
|
1.0, // Alpha: 1.0
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
use objc2::msg_send;
|
||||||
|
let _: () = msg_send![&*ns_window, setBackgroundColor: &*bg_color];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化日志
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
app.handle().plugin(
|
||||||
|
tauri_plugin_log::Builder::default()
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
|
.build(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化应用状态(仅创建一次,并在本函数末尾注入 manage)
|
||||||
|
let app_state = AppState::new();
|
||||||
|
|
||||||
|
// 如果没有供应商且存在 Claude Code 配置,自动导入
|
||||||
|
{
|
||||||
|
let manager = app_state.provider_manager.lock().unwrap();
|
||||||
|
if manager.providers.is_empty() {
|
||||||
|
drop(manager); // 释放锁
|
||||||
|
|
||||||
|
let settings_path = config::get_claude_settings_path();
|
||||||
|
if settings_path.exists() {
|
||||||
|
log::info!("检测到 Claude Code 配置,自动导入为默认供应商");
|
||||||
|
|
||||||
|
if let Ok(settings_config) = config::import_current_config_as_default() {
|
||||||
|
let mut manager = app_state.provider_manager.lock().unwrap();
|
||||||
|
let provider = provider::Provider::with_id(
|
||||||
|
"default".to_string(),
|
||||||
|
"default".to_string(),
|
||||||
|
settings_config,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
if manager.add_provider(provider).is_ok() {
|
||||||
|
manager.current = "default".to_string();
|
||||||
|
drop(manager);
|
||||||
|
let _ = app_state.save();
|
||||||
|
log::info!("成功导入默认供应商");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将同一个实例注入到全局状态,避免重复创建导致的不一致
|
||||||
|
app.manage(app_state);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
commands::get_providers,
|
||||||
|
commands::get_current_provider,
|
||||||
|
commands::add_provider,
|
||||||
|
commands::update_provider,
|
||||||
|
commands::delete_provider,
|
||||||
|
commands::switch_provider,
|
||||||
|
commands::import_default_config,
|
||||||
|
commands::get_claude_config_status,
|
||||||
|
commands::get_claude_code_config_path,
|
||||||
|
commands::open_config_folder,
|
||||||
|
commands::open_external,
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
cc_switch_lib::run();
|
||||||
|
}
|
||||||
179
src-tauri/src/provider.rs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::config::{
|
||||||
|
backup_config, copy_file, delete_file, get_claude_settings_path, get_provider_config_path,
|
||||||
|
read_json_file, write_json_file,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 供应商结构体
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Provider {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "settingsConfig")]
|
||||||
|
pub settings_config: Value,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "websiteUrl")]
|
||||||
|
pub website_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Provider {
|
||||||
|
/// 从现有ID创建供应商
|
||||||
|
pub fn with_id(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
settings_config: Value,
|
||||||
|
website_url: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
settings_config,
|
||||||
|
website_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 供应商管理器
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProviderManager {
|
||||||
|
pub providers: HashMap<String, Provider>,
|
||||||
|
pub current: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProviderManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
providers: HashMap::new(),
|
||||||
|
current: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProviderManager {
|
||||||
|
/// 加载供应商列表
|
||||||
|
pub fn load_from_file(path: &Path) -> Result<Self, String> {
|
||||||
|
if !path.exists() {
|
||||||
|
log::info!("配置文件不存在,创建新的供应商管理器");
|
||||||
|
return Ok(Self::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
read_json_file(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存供应商列表
|
||||||
|
pub fn save_to_file(&self, path: &Path) -> Result<(), String> {
|
||||||
|
write_json_file(path, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 添加供应商
|
||||||
|
pub fn add_provider(&mut self, provider: Provider) -> Result<(), String> {
|
||||||
|
// 保存供应商配置到独立文件
|
||||||
|
let config_path = get_provider_config_path(&provider.id, Some(&provider.name));
|
||||||
|
write_json_file(&config_path, &provider.settings_config)?;
|
||||||
|
|
||||||
|
// 添加到管理器
|
||||||
|
self.providers.insert(provider.id.clone(), provider);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新供应商
|
||||||
|
pub fn update_provider(&mut self, provider: Provider) -> Result<(), String> {
|
||||||
|
// 检查供应商是否存在
|
||||||
|
if !self.providers.contains_key(&provider.id) {
|
||||||
|
return Err(format!("供应商不存在: {}", provider.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果名称改变了,需要处理配置文件
|
||||||
|
if let Some(old_provider) = self.providers.get(&provider.id) {
|
||||||
|
if old_provider.name != provider.name {
|
||||||
|
// 删除旧配置文件
|
||||||
|
let old_config_path =
|
||||||
|
get_provider_config_path(&provider.id, Some(&old_provider.name));
|
||||||
|
delete_file(&old_config_path).ok(); // 忽略删除错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存新配置文件
|
||||||
|
let config_path = get_provider_config_path(&provider.id, Some(&provider.name));
|
||||||
|
write_json_file(&config_path, &provider.settings_config)?;
|
||||||
|
|
||||||
|
// 更新管理器
|
||||||
|
self.providers.insert(provider.id.clone(), provider);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除供应商
|
||||||
|
pub fn delete_provider(&mut self, provider_id: &str) -> Result<(), String> {
|
||||||
|
// 检查是否为当前供应商
|
||||||
|
if self.current == provider_id {
|
||||||
|
return Err("不能删除当前正在使用的供应商".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取供应商信息
|
||||||
|
let provider = self
|
||||||
|
.providers
|
||||||
|
.get(provider_id)
|
||||||
|
.ok_or_else(|| format!("供应商不存在: {}", provider_id))?;
|
||||||
|
|
||||||
|
// 删除配置文件
|
||||||
|
let config_path = get_provider_config_path(provider_id, Some(&provider.name));
|
||||||
|
delete_file(&config_path)?;
|
||||||
|
|
||||||
|
// 从管理器删除
|
||||||
|
self.providers.remove(provider_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换供应商
|
||||||
|
pub fn switch_provider(&mut self, provider_id: &str) -> Result<(), String> {
|
||||||
|
// 检查供应商是否存在
|
||||||
|
let provider = self
|
||||||
|
.providers
|
||||||
|
.get(provider_id)
|
||||||
|
.ok_or_else(|| format!("供应商不存在: {}", provider_id))?;
|
||||||
|
|
||||||
|
let settings_path = get_claude_settings_path();
|
||||||
|
let provider_config_path = get_provider_config_path(provider_id, Some(&provider.name));
|
||||||
|
|
||||||
|
// 检查供应商配置文件是否存在
|
||||||
|
if !provider_config_path.exists() {
|
||||||
|
return Err(format!(
|
||||||
|
"供应商配置文件不存在: {}",
|
||||||
|
provider_config_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前有配置,先备份到当前供应商
|
||||||
|
if settings_path.exists() && !self.current.is_empty() {
|
||||||
|
if let Some(current_provider) = self.providers.get(&self.current) {
|
||||||
|
let current_provider_path =
|
||||||
|
get_provider_config_path(&self.current, Some(¤t_provider.name));
|
||||||
|
backup_config(&settings_path, ¤t_provider_path)?;
|
||||||
|
log::info!("已备份当前供应商配置: {}", current_provider.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保主配置父目录存在
|
||||||
|
if let Some(parent) = settings_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制新供应商配置到主配置
|
||||||
|
copy_file(&provider_config_path, &settings_path)?;
|
||||||
|
|
||||||
|
// 更新当前供应商
|
||||||
|
self.current = provider_id.to_string();
|
||||||
|
|
||||||
|
log::info!("成功切换到供应商: {}", provider.name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取所有供应商
|
||||||
|
pub fn get_all_providers(&self) -> &HashMap<String, Provider> {
|
||||||
|
&self.providers
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src-tauri/src/store.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::config::get_app_config_path;
|
||||||
|
use crate::provider::ProviderManager;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
/// 全局应用状态
|
||||||
|
pub struct AppState {
|
||||||
|
pub provider_manager: Mutex<ProviderManager>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
/// 创建新的应用状态
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let config_path = get_app_config_path();
|
||||||
|
let provider_manager = ProviderManager::load_from_file(&config_path).unwrap_or_else(|e| {
|
||||||
|
log::warn!("加载配置失败: {}, 使用默认配置", e);
|
||||||
|
ProviderManager::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
provider_manager: Mutex::new(provider_manager),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存配置到文件
|
||||||
|
pub fn save(&self) -> Result<(), String> {
|
||||||
|
let config_path = get_app_config_path();
|
||||||
|
let manager = self
|
||||||
|
.provider_manager
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| format!("获取锁失败: {}", e))?;
|
||||||
|
|
||||||
|
manager.save_to_file(&config_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留按需扩展:若未来需要热加载,可在此实现
|
||||||
|
}
|
||||||
40
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "CC Switch",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"identifier": "com.ccswitch.desktop",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../dist",
|
||||||
|
"devUrl": "http://localhost:3000",
|
||||||
|
"beforeDevCommand": "pnpm run dev:renderer",
|
||||||
|
"beforeBuildCommand": "pnpm run build:renderer"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "",
|
||||||
|
"width": 900,
|
||||||
|
"height": 650,
|
||||||
|
"minWidth": 800,
|
||||||
|
"minHeight": 600,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false,
|
||||||
|
"titleBarStyle": "Transparent"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https: http:"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["app", "dmg", "nsis", "appimage"],
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,14 +7,13 @@
|
|||||||
.app-header {
|
.app-header {
|
||||||
background: #3498db;
|
background: #3498db;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 1rem 2rem;
|
padding: 0.75rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
/* 允许作为 Electron 的拖拽区域(macOS 隐藏标题栏时生效) */
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
min-height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header h1 {
|
.app-header h1 {
|
||||||
@@ -25,19 +24,16 @@
|
|||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
/* header 内的交互元素需要排除拖拽,否则无法点击 */
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.refresh-btn, .add-btn {
|
.refresh-btn,
|
||||||
|
.add-btn {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
/* 明确按钮不可拖拽,确保可点击 */
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.refresh-btn {
|
.refresh-btn {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Provider } from "../shared/types";
|
import { Provider } from "./types";
|
||||||
import ProviderList from "./components/ProviderList";
|
import ProviderList from "./components/ProviderList";
|
||||||
import AddProviderModal from "./components/AddProviderModal";
|
import AddProviderModal from "./components/AddProviderModal";
|
||||||
import EditProviderModal from "./components/EditProviderModal";
|
import EditProviderModal from "./components/EditProviderModal";
|
||||||
@@ -10,9 +10,12 @@ function App() {
|
|||||||
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
||||||
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const [configPath, setConfigPath] = useState<string>("");
|
const [configStatus, setConfigStatus] = useState<{
|
||||||
|
exists: boolean;
|
||||||
|
path: string;
|
||||||
|
} | null>(null);
|
||||||
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
const [notification, setNotification] = useState<{
|
const [notification, setNotification] = useState<{
|
||||||
message: string;
|
message: string;
|
||||||
@@ -25,13 +28,13 @@ function App() {
|
|||||||
message: string;
|
message: string;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
// 设置通知的辅助函数
|
// 设置通知的辅助函数
|
||||||
const showNotification = (
|
const showNotification = (
|
||||||
message: string,
|
message: string,
|
||||||
type: "success" | "error",
|
type: "success" | "error",
|
||||||
duration = 3000
|
duration = 3000,
|
||||||
) => {
|
) => {
|
||||||
// 清除之前的定时器
|
// 清除之前的定时器
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
@@ -56,7 +59,7 @@ function App() {
|
|||||||
// 加载供应商列表
|
// 加载供应商列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProviders();
|
loadProviders();
|
||||||
loadConfigPath();
|
loadConfigStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 清理定时器
|
// 清理定时器
|
||||||
@@ -69,8 +72,8 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadProviders = async () => {
|
const loadProviders = async () => {
|
||||||
const loadedProviders = await window.electronAPI.getProviders();
|
const loadedProviders = await window.api.getProviders();
|
||||||
const currentId = await window.electronAPI.getCurrentProvider();
|
const currentId = await window.api.getCurrentProvider();
|
||||||
setProviders(loadedProviders);
|
setProviders(loadedProviders);
|
||||||
setCurrentProviderId(currentId);
|
setCurrentProviderId(currentId);
|
||||||
|
|
||||||
@@ -80,9 +83,12 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadConfigPath = async () => {
|
const loadConfigStatus = async () => {
|
||||||
const path = await window.electronAPI.getClaudeCodeConfigPath();
|
const status = await window.api.getClaudeConfigStatus();
|
||||||
setConfigPath(path);
|
setConfigStatus({
|
||||||
|
exists: Boolean(status?.exists),
|
||||||
|
path: String(status?.path || ""),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
@@ -95,14 +101,14 @@ function App() {
|
|||||||
...provider,
|
...provider,
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
};
|
};
|
||||||
await window.electronAPI.addProvider(newProvider);
|
await window.api.addProvider(newProvider);
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
setIsAddModalOpen(false);
|
setIsAddModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditProvider = async (provider: Provider) => {
|
const handleEditProvider = async (provider: Provider) => {
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.updateProvider(provider);
|
await window.api.updateProvider(provider);
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
setEditingProviderId(null);
|
setEditingProviderId(null);
|
||||||
// 显示编辑成功提示
|
// 显示编辑成功提示
|
||||||
@@ -121,7 +127,7 @@ function App() {
|
|||||||
title: "删除供应商",
|
title: "删除供应商",
|
||||||
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`,
|
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await window.electronAPI.deleteProvider(id);
|
await window.api.deleteProvider(id);
|
||||||
await loadProviders();
|
await loadProviders();
|
||||||
setConfirmDialog(null);
|
setConfirmDialog(null);
|
||||||
showNotification("供应商删除成功", "success");
|
showNotification("供应商删除成功", "success");
|
||||||
@@ -130,14 +136,14 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSwitchProvider = async (id: string) => {
|
const handleSwitchProvider = async (id: string) => {
|
||||||
const success = await window.electronAPI.switchProvider(id);
|
const success = await window.api.switchProvider(id);
|
||||||
if (success) {
|
if (success) {
|
||||||
setCurrentProviderId(id);
|
setCurrentProviderId(id);
|
||||||
// 显示重启提示
|
// 显示重启提示
|
||||||
showNotification(
|
showNotification(
|
||||||
"切换成功!请重启 Claude Code 终端以生效",
|
"切换成功!请重启 Claude Code 终端以生效",
|
||||||
"success",
|
"success",
|
||||||
2000
|
2000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showNotification("切换失败,请检查配置", "error");
|
showNotification("切换失败,请检查配置", "error");
|
||||||
@@ -147,21 +153,25 @@ function App() {
|
|||||||
// 自动导入现有配置为"default"供应商
|
// 自动导入现有配置为"default"供应商
|
||||||
const handleAutoImportDefault = async () => {
|
const handleAutoImportDefault = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.importCurrentConfigAsDefault()
|
const result = await window.api.importCurrentConfigAsDefault();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadProviders()
|
await loadProviders();
|
||||||
showNotification("已自动导入现有配置为 default 供应商", "success", 3000)
|
showNotification(
|
||||||
|
"已自动导入现有配置为 default 供应商",
|
||||||
|
"success",
|
||||||
|
3000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
|
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('自动导入默认配置失败:', error)
|
console.error("自动导入默认配置失败:", error);
|
||||||
// 静默处理,不影响用户体验
|
// 静默处理,不影响用户体验
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleOpenConfigFolder = async () => {
|
const handleOpenConfigFolder = async () => {
|
||||||
await window.electronAPI.openConfigFolder();
|
await window.api.openConfigFolder();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -199,9 +209,12 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{configPath && (
|
{configStatus && (
|
||||||
<div className="config-path">
|
<div className="config-path">
|
||||||
<span>配置文件位置: {configPath}</span>
|
<span>
|
||||||
|
配置文件位置: {configStatus.path}
|
||||||
|
{!configStatus.exists ? "(未创建,切换或保存时会自动创建)" : ""}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
className="browse-btn"
|
className="browse-btn"
|
||||||
onClick={handleOpenConfigFolder}
|
onClick={handleOpenConfigFolder}
|
||||||
@@ -13,20 +13,78 @@
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 2rem;
|
padding: 0;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 600px;
|
max-width: 640px;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow: hidden; /* 由 body 滚动,标题栏固定 */
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
|
display: flex; /* 纵向布局,便于底栏固定 */
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content h2 {
|
/* 模拟窗口标题栏 */
|
||||||
margin-bottom: 1.5rem;
|
.modal-titlebar {
|
||||||
color: #2c3e50;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 3rem; /* 与主窗口标题栏一致 */
|
||||||
|
padding: 0 12px; /* 接近主头部的水平留白 */
|
||||||
|
background: #3498db; /* 与 .app-header 相同 */
|
||||||
|
color: #fff;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧占位以保证标题居中(与右侧关闭按钮宽度相当) */
|
||||||
|
.modal-spacer {
|
||||||
|
width: 32px;
|
||||||
|
flex: 0 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-form {
|
||||||
|
/* 表单外层包裹 body + footer */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0; /* 允许子元素正确计算高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.25rem 1.5rem 1.5rem;
|
||||||
|
overflow: auto; /* 仅内容区滚动 */
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
@@ -75,6 +133,18 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 官方按钮橙色主题(Anthropic 风格) */
|
||||||
|
.preset-btn.official {
|
||||||
|
border: 1px solid #d97706;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn.official:hover,
|
||||||
|
.preset-btn.official.selected {
|
||||||
|
background: #d97706;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
@@ -86,6 +156,18 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* API Key 输入框容器 - 预留空间避免抖动 */
|
||||||
|
.form-group.api-key-group {
|
||||||
|
min-height: 88px; /* 固定高度:label + input + 间距 */
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.api-key-group.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group input,
|
.form-group input,
|
||||||
.form-group textarea {
|
.form-group textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -109,11 +191,14 @@
|
|||||||
border-color: #3498db;
|
border-color: #3498db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.modal-footer {
|
||||||
|
/* 固定在弹窗底部(非滚动区) */
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 2rem;
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-top: 1px solid #ecf0f1;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn,
|
.cancel-btn,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Provider } from "../../shared/types";
|
import { Provider } from "../types";
|
||||||
import ProviderForm from "./ProviderForm";
|
import ProviderForm from "./ProviderForm";
|
||||||
|
|
||||||
interface AddProviderModalProps {
|
interface AddProviderModalProps {
|
||||||
@@ -68,7 +68,9 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: background-color 0.2s, transform 0.1s;
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
transform 0.1s;
|
||||||
min-width: 70px;
|
min-width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import './ConfirmDialog.css';
|
import "./ConfirmDialog.css";
|
||||||
|
|
||||||
interface ConfirmDialogProps {
|
interface ConfirmDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -15,10 +15,10 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
confirmText = '确定',
|
confirmText = "确定",
|
||||||
cancelText = '取消',
|
cancelText = "取消",
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel
|
onCancel,
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
35
src/components/EditProviderModal.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Provider } from "../types";
|
||||||
|
import ProviderForm from "./ProviderForm";
|
||||||
|
|
||||||
|
interface EditProviderModalProps {
|
||||||
|
provider: Provider;
|
||||||
|
onSave: (provider: Provider) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
||||||
|
provider,
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const handleSubmit = (data: Omit<Provider, "id">) => {
|
||||||
|
onSave({
|
||||||
|
...provider,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProviderForm
|
||||||
|
title="编辑供应商"
|
||||||
|
submitText="保存"
|
||||||
|
initialData={provider}
|
||||||
|
showPresets={false}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditProviderModal;
|
||||||
355
src/components/ProviderForm.tsx
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Provider } from "../types";
|
||||||
|
import {
|
||||||
|
updateCoAuthoredSetting,
|
||||||
|
checkCoAuthoredSetting,
|
||||||
|
extractWebsiteUrl,
|
||||||
|
getApiKeyFromConfig,
|
||||||
|
hasApiKeyField,
|
||||||
|
setApiKeyInConfig,
|
||||||
|
} from "../utils/providerConfigUtils";
|
||||||
|
import { providerPresets } from "../config/providerPresets";
|
||||||
|
import "./AddProviderModal.css";
|
||||||
|
|
||||||
|
interface ProviderFormProps {
|
||||||
|
title: string;
|
||||||
|
submitText: string;
|
||||||
|
initialData?: Provider;
|
||||||
|
showPresets?: boolean;
|
||||||
|
onSubmit: (data: Omit<Provider, "id">) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||||
|
title,
|
||||||
|
submitText,
|
||||||
|
initialData,
|
||||||
|
showPresets = false,
|
||||||
|
onSubmit,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: initialData?.name || "",
|
||||||
|
websiteUrl: initialData?.websiteUrl || "",
|
||||||
|
settingsConfig: initialData
|
||||||
|
? JSON.stringify(initialData.settingsConfig, null, 2)
|
||||||
|
: "",
|
||||||
|
});
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
|
||||||
|
// 初始化时检查禁用签名状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
||||||
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||||
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
|
}
|
||||||
|
}, [initialData]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
if (!formData.name) {
|
||||||
|
setError("请填写供应商名称");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.settingsConfig.trim()) {
|
||||||
|
setError("请填写配置内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let settingsConfig: Record<string, any>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
settingsConfig = JSON.parse(formData.settingsConfig);
|
||||||
|
} catch (err) {
|
||||||
|
setError("配置JSON格式错误,请检查语法");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit({
|
||||||
|
name: formData.name,
|
||||||
|
websiteUrl: formData.websiteUrl,
|
||||||
|
settingsConfig,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
if (name === "settingsConfig") {
|
||||||
|
// 当用户修改配置时,尝试自动提取官网地址
|
||||||
|
const extractedWebsiteUrl = extractWebsiteUrl(value);
|
||||||
|
|
||||||
|
// 同时检查并同步选择框状态
|
||||||
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(value);
|
||||||
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
|
|
||||||
|
// 同步 API Key 输入框显示与值
|
||||||
|
const parsedKey = getApiKeyFromConfig(value);
|
||||||
|
setApiKey(parsedKey);
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
// 只有在官网地址为空时才自动填入
|
||||||
|
websiteUrl: formData.websiteUrl || extractedWebsiteUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理选择框变化
|
||||||
|
const handleCoAuthoredToggle = (checked: boolean) => {
|
||||||
|
setDisableCoAuthored(checked);
|
||||||
|
|
||||||
|
// 更新JSON配置
|
||||||
|
const updatedConfig = updateCoAuthoredSetting(
|
||||||
|
formData.settingsConfig,
|
||||||
|
checked,
|
||||||
|
);
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
settingsConfig: updatedConfig,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPreset = (preset: (typeof providerPresets)[0], index: number) => {
|
||||||
|
const configString = JSON.stringify(preset.settingsConfig, null, 2);
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
name: preset.name,
|
||||||
|
websiteUrl: preset.websiteUrl,
|
||||||
|
settingsConfig: configString,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置选中的预设
|
||||||
|
setSelectedPreset(index);
|
||||||
|
|
||||||
|
// 清空 API Key 输入框,让用户重新输入
|
||||||
|
setApiKey("");
|
||||||
|
|
||||||
|
// 同步选择框状态
|
||||||
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||||
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 API Key 输入并自动更新配置
|
||||||
|
const handleApiKeyChange = (key: string) => {
|
||||||
|
setApiKey(key);
|
||||||
|
|
||||||
|
const configString = setApiKeyInConfig(
|
||||||
|
formData.settingsConfig,
|
||||||
|
key.trim(),
|
||||||
|
{ createIfMissing: selectedPreset !== null },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新表单配置
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
settingsConfig: configString,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 同步选择框状态
|
||||||
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
||||||
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据当前配置决定是否展示 API Key 输入框
|
||||||
|
const showApiKey =
|
||||||
|
selectedPreset !== null || hasApiKeyField(formData.settingsConfig);
|
||||||
|
|
||||||
|
// 判断当前选中的预设是否是官方
|
||||||
|
const isOfficialPreset =
|
||||||
|
selectedPreset !== null &&
|
||||||
|
providerPresets[selectedPreset]?.isOfficial === true;
|
||||||
|
|
||||||
|
// 初始时从配置中同步 API Key(编辑模式)
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
const parsedKey = getApiKeyFromConfig(
|
||||||
|
JSON.stringify(initialData.settingsConfig),
|
||||||
|
);
|
||||||
|
if (parsedKey) setApiKey(parsedKey);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 支持按下 ESC 关闭弹窗
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="modal-overlay"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-titlebar">
|
||||||
|
<div className="modal-spacer" />
|
||||||
|
<div className="modal-title" title={title}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="modal-close-btn"
|
||||||
|
aria-label="关闭"
|
||||||
|
onClick={onClose}
|
||||||
|
title="关闭"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="modal-form">
|
||||||
|
<div className="modal-body">
|
||||||
|
{error && <div className="error-message">{error}</div>}
|
||||||
|
|
||||||
|
{showPresets && (
|
||||||
|
<div className="presets">
|
||||||
|
<label>一键导入(只需要填写 key)</label>
|
||||||
|
<div className="preset-buttons">
|
||||||
|
{providerPresets.map((preset, index) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
className={`preset-btn ${
|
||||||
|
selectedPreset === index ? "selected" : ""
|
||||||
|
} ${preset.isOfficial ? "official" : ""}`}
|
||||||
|
onClick={() => applyPreset(preset, index)}
|
||||||
|
>
|
||||||
|
{preset.name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="name">供应商名称 *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="例如:Anthropic 官方"
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
|
||||||
|
>
|
||||||
|
<label htmlFor="apiKey">API Key *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="apiKey"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||||
|
placeholder={
|
||||||
|
isOfficialPreset
|
||||||
|
? "官方登录无需填写 API Key,直接保存即可"
|
||||||
|
: "只需要填这里,下方配置会自动填充"
|
||||||
|
}
|
||||||
|
disabled={isOfficialPreset}
|
||||||
|
autoComplete="off"
|
||||||
|
style={
|
||||||
|
isOfficialPreset
|
||||||
|
? {
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
|
cursor: "not-allowed",
|
||||||
|
color: "#999",
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="websiteUrl">官网地址</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="websiteUrl"
|
||||||
|
name="websiteUrl"
|
||||||
|
value={formData.websiteUrl}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="https://example.com(可选)"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="label-with-checkbox">
|
||||||
|
<label htmlFor="settingsConfig">
|
||||||
|
Claude Code 配置 (JSON) *
|
||||||
|
</label>
|
||||||
|
<label className="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={disableCoAuthored}
|
||||||
|
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
|
||||||
|
/>
|
||||||
|
禁止 Claude Code 签名
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="settingsConfig"
|
||||||
|
name="settingsConfig"
|
||||||
|
value={formData.settingsConfig}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={`{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
rows={12}
|
||||||
|
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<small className="field-hint">
|
||||||
|
完整的 Claude Code settings.json 配置内容
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button type="button" className="cancel-btn" onClick={onClose}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="submit-btn">
|
||||||
|
{submitText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProviderForm;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { Provider } from '../../shared/types'
|
import { Provider } from "../types";
|
||||||
import './ProviderList.css'
|
import "./ProviderList.css";
|
||||||
|
|
||||||
interface ProviderListProps {
|
interface ProviderListProps {
|
||||||
providers: Record<string, Provider>
|
providers: Record<string, Provider>;
|
||||||
currentProviderId: string
|
currentProviderId: string;
|
||||||
onSwitch: (id: string) => void
|
onSwitch: (id: string) => void;
|
||||||
onDelete: (id: string) => void
|
onDelete: (id: string) => void;
|
||||||
onEdit: (id: string) => void
|
onEdit: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProviderList: React.FC<ProviderListProps> = ({
|
const ProviderList: React.FC<ProviderListProps> = ({
|
||||||
@@ -15,28 +15,28 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
currentProviderId,
|
currentProviderId,
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEdit
|
onEdit,
|
||||||
}) => {
|
}) => {
|
||||||
// 提取API地址
|
// 提取API地址
|
||||||
const getApiUrl = (provider: Provider): string => {
|
const getApiUrl = (provider: Provider): string => {
|
||||||
try {
|
try {
|
||||||
const config = provider.settingsConfig
|
const config = provider.settingsConfig;
|
||||||
if (config?.env?.ANTHROPIC_BASE_URL) {
|
if (config?.env?.ANTHROPIC_BASE_URL) {
|
||||||
return config.env.ANTHROPIC_BASE_URL
|
return config.env.ANTHROPIC_BASE_URL;
|
||||||
}
|
}
|
||||||
return '未设置'
|
return "未设置";
|
||||||
} catch {
|
} catch {
|
||||||
return '配置错误'
|
return "配置错误";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUrlClick = async (url: string) => {
|
const handleUrlClick = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.openExternal(url)
|
await window.api.openExternal(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('打开链接失败:', error)
|
console.error("打开链接失败:", error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="provider-list">
|
<div className="provider-list">
|
||||||
@@ -48,25 +48,27 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="provider-items">
|
<div className="provider-items">
|
||||||
{Object.values(providers).map((provider) => {
|
{Object.values(providers).map((provider) => {
|
||||||
const isCurrent = provider.id === currentProviderId
|
const isCurrent = provider.id === currentProviderId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
className={`provider-item ${isCurrent ? 'current' : ''}`}
|
className={`provider-item ${isCurrent ? "current" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="provider-info">
|
<div className="provider-info">
|
||||||
<div className="provider-name">
|
<div className="provider-name">
|
||||||
<span>{provider.name}</span>
|
<span>{provider.name}</span>
|
||||||
{isCurrent && <span className="current-badge">当前使用</span>}
|
{isCurrent && (
|
||||||
|
<span className="current-badge">当前使用</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="provider-url">
|
<div className="provider-url">
|
||||||
{provider.websiteUrl ? (
|
{provider.websiteUrl ? (
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
handleUrlClick(provider.websiteUrl!)
|
handleUrlClick(provider.websiteUrl!);
|
||||||
}}
|
}}
|
||||||
className="url-link"
|
className="url-link"
|
||||||
title={`访问 ${provider.websiteUrl}`}
|
title={`访问 ${provider.websiteUrl}`}
|
||||||
@@ -105,12 +107,12 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProviderList
|
export default ProviderList;
|
||||||
@@ -5,12 +5,21 @@ export interface ProviderPreset {
|
|||||||
name: string;
|
name: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
settingsConfig: object;
|
settingsConfig: object;
|
||||||
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
}
|
}
|
||||||
|
|
||||||
export const providerPresets: ProviderPreset[] = [
|
export const providerPresets: ProviderPreset[] = [
|
||||||
|
{
|
||||||
|
name: "Claude官方登录",
|
||||||
|
websiteUrl: "https://www.anthropic.com/claude-code",
|
||||||
|
settingsConfig: {
|
||||||
|
env: {},
|
||||||
|
},
|
||||||
|
isOfficial: true, // 明确标识为官方预设
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "DeepSeek v3.1",
|
name: "DeepSeek v3.1",
|
||||||
websiteUrl: "https://platform.deepseek.com/",
|
websiteUrl: "https://platform.deepseek.com",
|
||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
||||||
@@ -41,6 +50,18 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Kimi k2",
|
||||||
|
websiteUrl: "https://platform.moonshot.cn/console",
|
||||||
|
settingsConfig: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic",
|
||||||
|
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
||||||
|
ANTHROPIC_MODEL: "kimi-k2-turbo-preview",
|
||||||
|
ANTHROPIC_SMALL_FAST_MODEL: "kimi-k2-turbo-preview",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "PackyCode",
|
name: "PackyCode",
|
||||||
websiteUrl: "https://www.packycode.com",
|
websiteUrl: "https://www.packycode.com",
|
||||||
@@ -51,14 +72,4 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "AnyRouter",
|
|
||||||
websiteUrl: "https://anyrouter.top",
|
|
||||||
settingsConfig: {
|
|
||||||
env: {
|
|
||||||
ANTHROPIC_BASE_URL: "https://anyrouter.top",
|
|
||||||
ANTHROPIC_AUTH_TOKEN: "sk-your-api-key-here",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family:
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||||
sans-serif;
|
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
152
src/lib/tauri-api.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { Provider } from "../types";
|
||||||
|
|
||||||
|
// 定义配置状态类型
|
||||||
|
interface ConfigStatus {
|
||||||
|
exists: boolean;
|
||||||
|
path: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义导入结果类型
|
||||||
|
interface ImportResult {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tauri API 封装,提供统一的全局 API 接口
|
||||||
|
export const tauriAPI = {
|
||||||
|
// 获取所有供应商
|
||||||
|
getProviders: async (): Promise<Record<string, Provider>> => {
|
||||||
|
try {
|
||||||
|
return await invoke("get_providers");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取供应商列表失败:", error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取当前供应商ID
|
||||||
|
getCurrentProvider: async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
return await invoke("get_current_provider");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取当前供应商失败:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加供应商
|
||||||
|
addProvider: async (provider: Provider): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke("add_provider", { provider });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("添加供应商失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新供应商
|
||||||
|
updateProvider: async (provider: Provider): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke("update_provider", { provider });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("更新供应商失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除供应商
|
||||||
|
deleteProvider: async (id: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke("delete_provider", { id });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除供应商失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换供应商
|
||||||
|
switchProvider: async (providerId: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke("switch_provider", { id: providerId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("切换供应商失败:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 导入当前配置为默认供应商
|
||||||
|
importCurrentConfigAsDefault: async (): Promise<ImportResult> => {
|
||||||
|
try {
|
||||||
|
const success = await invoke<boolean>("import_default_config");
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
message: success ? "成功导入默认配置" : "导入失败",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("导入默认配置失败:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取 Claude Code 配置文件路径
|
||||||
|
getClaudeCodeConfigPath: async (): Promise<string> => {
|
||||||
|
try {
|
||||||
|
return await invoke("get_claude_code_config_path");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取配置路径失败:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取 Claude Code 配置状态
|
||||||
|
getClaudeConfigStatus: async (): Promise<ConfigStatus> => {
|
||||||
|
try {
|
||||||
|
return await invoke("get_claude_config_status");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取配置状态失败:", error);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
path: "",
|
||||||
|
error: String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 打开配置文件夹
|
||||||
|
openConfigFolder: async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await invoke("open_config_folder");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("打开配置文件夹失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 打开外部链接
|
||||||
|
openExternal: async (url: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await invoke("open_external", { url });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("打开外部链接失败:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择配置文件(Tauri 暂不实现,保留接口兼容性)
|
||||||
|
selectConfigFile: async (): Promise<string | null> => {
|
||||||
|
console.warn("selectConfigFile 在 Tauri 版本中暂不支持");
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建全局 API 对象,兼容现有代码
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
// 绑定到 window.api,避免 Electron 命名造成误解
|
||||||
|
// API 内部已做 try/catch,非 Tauri 环境下也会安全返回默认值
|
||||||
|
(window as any).api = tauriAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tauriAPI;
|
||||||
24
src/main.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
// 导入 Tauri API(自动绑定到 window.api)
|
||||||
|
import "./lib/tauri-api";
|
||||||
|
|
||||||
|
// 根据平台添加 body class,便于平台特定样式
|
||||||
|
try {
|
||||||
|
const ua = navigator.userAgent || "";
|
||||||
|
const plat = (navigator.platform || "").toLowerCase();
|
||||||
|
const isMac = /mac/i.test(ua) || plat.includes("mac");
|
||||||
|
if (isMac) {
|
||||||
|
document.body.classList.add("is-mac");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略平台检测失败
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
@@ -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,31 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Provider } from '../../shared/types'
|
|
||||||
import ProviderForm from './ProviderForm'
|
|
||||||
|
|
||||||
interface EditProviderModalProps {
|
|
||||||
provider: Provider
|
|
||||||
onSave: (provider: Provider) => void
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave, onClose }) => {
|
|
||||||
const handleSubmit = (data: Omit<Provider, 'id'>) => {
|
|
||||||
onSave({
|
|
||||||
...provider,
|
|
||||||
...data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProviderForm
|
|
||||||
title="编辑供应商"
|
|
||||||
submitText="保存"
|
|
||||||
initialData={provider}
|
|
||||||
showPresets={false}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onClose={onClose}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditProviderModal
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Provider } from "../../shared/types";
|
|
||||||
import {
|
|
||||||
updateCoAuthoredSetting,
|
|
||||||
checkCoAuthoredSetting,
|
|
||||||
extractWebsiteUrl,
|
|
||||||
} from "../utils/providerConfigUtils";
|
|
||||||
import { providerPresets } from "../config/providerPresets";
|
|
||||||
import "./AddProviderModal.css";
|
|
||||||
|
|
||||||
interface ProviderFormProps {
|
|
||||||
title: string;
|
|
||||||
submitText: string;
|
|
||||||
initialData?: Provider;
|
|
||||||
showPresets?: boolean;
|
|
||||||
onSubmit: (data: Omit<Provider, "id">) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProviderForm: React.FC<ProviderFormProps> = ({
|
|
||||||
title,
|
|
||||||
submitText,
|
|
||||||
initialData,
|
|
||||||
showPresets = false,
|
|
||||||
onSubmit,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: initialData?.name || "",
|
|
||||||
websiteUrl: initialData?.websiteUrl || "",
|
|
||||||
settingsConfig: initialData
|
|
||||||
? JSON.stringify(initialData.settingsConfig, null, 2)
|
|
||||||
: "",
|
|
||||||
});
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
|
||||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
|
|
||||||
const [apiKey, setApiKey] = useState("");
|
|
||||||
|
|
||||||
// 初始化时检查禁用签名状态
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData) {
|
|
||||||
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
|
||||||
}
|
|
||||||
}, [initialData]);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
if (!formData.name) {
|
|
||||||
setError("请填写供应商名称");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.settingsConfig.trim()) {
|
|
||||||
setError("请填写配置内容");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let settingsConfig: Record<string, any>;
|
|
||||||
|
|
||||||
try {
|
|
||||||
settingsConfig = JSON.parse(formData.settingsConfig);
|
|
||||||
} catch (err) {
|
|
||||||
setError("配置JSON格式错误,请检查语法");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit({
|
|
||||||
name: formData.name,
|
|
||||||
websiteUrl: formData.websiteUrl,
|
|
||||||
settingsConfig,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (
|
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
||||||
) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
|
|
||||||
if (name === "settingsConfig") {
|
|
||||||
// 当用户修改配置时,尝试自动提取官网地址
|
|
||||||
const extractedWebsiteUrl = extractWebsiteUrl(value);
|
|
||||||
|
|
||||||
// 同时检查并同步选择框状态
|
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(value);
|
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[name]: value,
|
|
||||||
// 只有在官网地址为空时才自动填入
|
|
||||||
websiteUrl: formData.websiteUrl || extractedWebsiteUrl,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[name]: value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理选择框变化
|
|
||||||
const handleCoAuthoredToggle = (checked: boolean) => {
|
|
||||||
setDisableCoAuthored(checked);
|
|
||||||
|
|
||||||
// 更新JSON配置
|
|
||||||
const updatedConfig = updateCoAuthoredSetting(
|
|
||||||
formData.settingsConfig,
|
|
||||||
checked
|
|
||||||
);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
settingsConfig: updatedConfig,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyPreset = (preset: (typeof providerPresets)[0], index: number) => {
|
|
||||||
const configString = JSON.stringify(preset.settingsConfig, null, 2);
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
name: preset.name,
|
|
||||||
websiteUrl: preset.websiteUrl,
|
|
||||||
settingsConfig: configString,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置选中的预设
|
|
||||||
setSelectedPreset(index);
|
|
||||||
|
|
||||||
// 清空 API Key 输入框,让用户重新输入
|
|
||||||
setApiKey("");
|
|
||||||
|
|
||||||
// 同步选择框状态
|
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理 API Key 输入并自动更新配置
|
|
||||||
const handleApiKeyChange = (key: string) => {
|
|
||||||
setApiKey(key);
|
|
||||||
|
|
||||||
if (selectedPreset !== null && key.trim()) {
|
|
||||||
// 获取当前选中的预设配置
|
|
||||||
const preset = providerPresets[selectedPreset];
|
|
||||||
const updatedConfig = JSON.parse(JSON.stringify(preset.settingsConfig));
|
|
||||||
|
|
||||||
// 替换配置中的 API Key
|
|
||||||
if (updatedConfig.env && updatedConfig.env.ANTHROPIC_AUTH_TOKEN) {
|
|
||||||
updatedConfig.env.ANTHROPIC_AUTH_TOKEN = key.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const configString = JSON.stringify(updatedConfig, null, 2);
|
|
||||||
|
|
||||||
// 更新表单配置
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
settingsConfig: configString,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 同步选择框状态
|
|
||||||
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
|
||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="modal-overlay">
|
|
||||||
<div className="modal-content">
|
|
||||||
<h2>{title}</h2>
|
|
||||||
|
|
||||||
{error && <div className="error-message">{error}</div>}
|
|
||||||
|
|
||||||
{showPresets && (
|
|
||||||
<div className="presets">
|
|
||||||
<label>快速选择模板:</label>
|
|
||||||
<div className="preset-buttons">
|
|
||||||
{providerPresets.map((preset, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
type="button"
|
|
||||||
className={`preset-btn ${
|
|
||||||
selectedPreset === index ? "selected" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => applyPreset(preset, index)}
|
|
||||||
>
|
|
||||||
{preset.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor="name">供应商名称 *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="例如:Anthropic 官方"
|
|
||||||
required
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedPreset !== null && (
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor="apiKey">API Key *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="apiKey"
|
|
||||||
value={apiKey}
|
|
||||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
|
||||||
placeholder="只需要填这里,下方配置会自动填充"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label htmlFor="websiteUrl">官网地址</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
id="websiteUrl"
|
|
||||||
name="websiteUrl"
|
|
||||||
value={formData.websiteUrl}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="https://example.com(可选)"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<div className="label-with-checkbox">
|
|
||||||
<label htmlFor="settingsConfig">Claude Code 配置 (JSON) *</label>
|
|
||||||
<label className="checkbox-label">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={disableCoAuthored}
|
|
||||||
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
|
|
||||||
/>
|
|
||||||
禁止 Claude Code 签名
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
id="settingsConfig"
|
|
||||||
name="settingsConfig"
|
|
||||||
value={formData.settingsConfig}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder={`{
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
rows={12}
|
|
||||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<small className="field-hint">
|
|
||||||
完整的 Claude Code settings.json 配置内容
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-actions">
|
|
||||||
<button type="button" className="cancel-btn" onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="submit-btn">
|
|
||||||
{submitText}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProviderForm;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import App from './App'
|
|
||||||
import './index.css'
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
platform?: { isMac?: boolean }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据平台添加 body class,便于平台特定样式
|
|
||||||
if (window.platform?.isMac) {
|
|
||||||
document.body.classList.add('is-mac')
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// 供应商配置处理工具函数
|
|
||||||
|
|
||||||
// 处理includeCoAuthoredBy字段的添加/删除
|
|
||||||
export const updateCoAuthoredSetting = (jsonString: string, disable: boolean): string => {
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(jsonString)
|
|
||||||
|
|
||||||
if (disable) {
|
|
||||||
// 添加或更新includeCoAuthoredBy字段
|
|
||||||
config.includeCoAuthoredBy = false
|
|
||||||
} else {
|
|
||||||
// 删除includeCoAuthoredBy字段
|
|
||||||
delete config.includeCoAuthoredBy
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify(config, null, 2)
|
|
||||||
} catch (err) {
|
|
||||||
// 如果JSON解析失败,返回原始字符串
|
|
||||||
return jsonString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从JSON配置中检查是否包含includeCoAuthoredBy设置
|
|
||||||
export const checkCoAuthoredSetting = (jsonString: string): boolean => {
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(jsonString)
|
|
||||||
return config.includeCoAuthoredBy === false
|
|
||||||
} catch (err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从JSON配置中提取并处理官网地址
|
|
||||||
export const extractWebsiteUrl = (jsonString: string): string => {
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(jsonString)
|
|
||||||
const baseUrl = config?.env?.ANTHROPIC_BASE_URL
|
|
||||||
|
|
||||||
if (baseUrl && typeof baseUrl === 'string') {
|
|
||||||
// 去掉 "api." 前缀
|
|
||||||
return baseUrl.replace(/^https?:\/\/api\./, 'https://')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// 忽略JSON解析错误
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
export interface Provider {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
settingsConfig: Record<string, any> // 完整的Claude Code settings.json配置
|
|
||||||
websiteUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppConfig {
|
|
||||||
providers: Record<string, Provider>
|
|
||||||
current: string
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
electronAPI: {
|
|
||||||
getProviders: () => Promise<Record<string, Provider>>
|
|
||||||
getCurrentProvider: () => Promise<string>
|
|
||||||
addProvider: (provider: Provider) => Promise<boolean>
|
|
||||||
deleteProvider: (id: string) => Promise<boolean>
|
|
||||||
updateProvider: (provider: Provider) => Promise<boolean>
|
|
||||||
switchProvider: (providerId: string) => Promise<boolean>
|
|
||||||
importCurrentConfigAsDefault: () => Promise<{ success: boolean; providerId?: string }>
|
|
||||||
getClaudeCodeConfigPath: () => Promise<string>
|
|
||||||
selectConfigFile: () => Promise<string | null>
|
|
||||||
openConfigFolder: () => Promise<boolean>
|
|
||||||
openExternal: (url: string) => Promise<boolean>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
src/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface Provider {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
settingsConfig: Record<string, any>; // 完整的 Claude Code settings.json 配置
|
||||||
|
websiteUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
providers: Record<string, Provider>;
|
||||||
|
current: string;
|
||||||
|
}
|
||||||
97
src/utils/providerConfigUtils.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// 供应商配置处理工具函数
|
||||||
|
|
||||||
|
// 处理includeCoAuthoredBy字段的添加/删除
|
||||||
|
export const updateCoAuthoredSetting = (
|
||||||
|
jsonString: string,
|
||||||
|
disable: boolean,
|
||||||
|
): string => {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
if (disable) {
|
||||||
|
// 添加或更新includeCoAuthoredBy字段
|
||||||
|
config.includeCoAuthoredBy = false;
|
||||||
|
} else {
|
||||||
|
// 删除includeCoAuthoredBy字段
|
||||||
|
delete config.includeCoAuthoredBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(config, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
// 如果JSON解析失败,返回原始字符串
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从JSON配置中检查是否包含includeCoAuthoredBy设置
|
||||||
|
export const checkCoAuthoredSetting = (jsonString: string): boolean => {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(jsonString);
|
||||||
|
return config.includeCoAuthoredBy === false;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从JSON配置中提取并处理官网地址
|
||||||
|
export const extractWebsiteUrl = (jsonString: string): string => {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(jsonString);
|
||||||
|
const baseUrl = config?.env?.ANTHROPIC_BASE_URL;
|
||||||
|
|
||||||
|
if (baseUrl && typeof baseUrl === "string") {
|
||||||
|
// 去掉 "api." 前缀
|
||||||
|
return baseUrl.replace(/^https?:\/\/api\./, "https://");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 忽略JSON解析错误
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 读取配置中的 API Key(env.ANTHROPIC_AUTH_TOKEN)
|
||||||
|
export const getApiKeyFromConfig = (jsonString: string): string => {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(jsonString);
|
||||||
|
const key = config?.env?.ANTHROPIC_AUTH_TOKEN;
|
||||||
|
return typeof key === "string" ? key : "";
|
||||||
|
} catch (err) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断配置中是否存在 API Key 字段
|
||||||
|
export const hasApiKeyField = (jsonString: string): boolean => {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(jsonString);
|
||||||
|
return Object.prototype.hasOwnProperty.call(
|
||||||
|
config?.env ?? {},
|
||||||
|
"ANTHROPIC_AUTH_TOKEN",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 写入/更新配置中的 API Key,默认不新增缺失字段
|
||||||
|
export const setApiKeyInConfig = (
|
||||||
|
jsonString: string,
|
||||||
|
apiKey: string,
|
||||||
|
options: { createIfMissing?: boolean } = {},
|
||||||
|
): string => {
|
||||||
|
const { createIfMissing = false } = options;
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(jsonString);
|
||||||
|
if (!config.env) {
|
||||||
|
if (!createIfMissing) return jsonString;
|
||||||
|
config.env = {};
|
||||||
|
}
|
||||||
|
if (!("ANTHROPIC_AUTH_TOKEN" in config.env) && !createIfMissing) {
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
config.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
||||||
|
return JSON.stringify(config, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
};
|
||||||