Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94e93137a2 | ||
|
|
db832a9654 | ||
|
|
45a639e73f | ||
|
|
f74d641f86 | ||
|
|
fcfa9574e8 | ||
|
|
d739bb36e5 | ||
|
|
0bcc04adce | ||
|
|
fee0762e3e | ||
|
|
1a8ae85e55 | ||
|
|
c5aa244d65 | ||
|
|
0bedbb2663 | ||
|
|
5f3caa1484 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -5,6 +5,25 @@ 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.4.0] - 2025-10-01
|
||||
|
||||
### ✨ Features
|
||||
- Enable internationalization via i18next with a Chinese default and English fallback, plus an in-app language switcher
|
||||
- Add Claude plugin sync alongside the existing VS Code integration controls
|
||||
- Extend provider presets with optional API key URLs and updated models, including DeepSeek-V3.1-Terminus and Qwen3-Max
|
||||
- Support portable mode launches and enforce a single running instance to avoid conflicts
|
||||
|
||||
### 🔧 Improvements
|
||||
- Allow minimizing the window to the system tray and add macOS Dock visibility management for tray workflows
|
||||
- Refresh the Settings modal with a scrollable layout, save icon, and cleaner language section
|
||||
- Smooth provider toggle states with consistent button widths/icons and prevent layout shifts when switching between Claude and Codex
|
||||
- Adjust the Windows MSI installer to target per-user LocalAppData and improve component tracking reliability
|
||||
|
||||
### 🐛 Fixes
|
||||
- Remove the unnecessary OpenAI auth requirement from third-party provider configurations
|
||||
- Fix layout shifts while switching app types with Claude plugin sync enabled
|
||||
- Align Enable/In Use button states to avoid visual jank across VS Code and Codex views
|
||||
|
||||
## [3.3.0] - 2025-09-22
|
||||
|
||||
### ✨ Features
|
||||
|
||||
18
README.md
18
README.md
@@ -1,11 +1,13 @@
|
||||
# Claude Code & Codex 供应商切换器
|
||||
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://tauri.app/)
|
||||
|
||||
一个用于管理和切换 Claude Code 与 Codex 不同供应商配置的桌面应用。
|
||||
|
||||
> v3.4.0 :新增 i18next 国际化(还有部分未完成)、对新模型(qwen-3-max, GLM-4.6, DeepSeek-V3.2-Exp)的支持、Claude 插件、单实例守护、托盘最小化及安装器优化等。
|
||||
|
||||
> v3.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。
|
||||
|
||||
> v3.2.0 :全新 UI、macOS系统托盘、内置更新器、原子写入与回滚、改进暗色样式、单一事实源(SSOT)与一次性迁移/归档。
|
||||
@@ -14,14 +16,14 @@
|
||||
|
||||
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
|
||||
|
||||
## 功能特性(v3.3.0)
|
||||
## 功能特性(v3.4.0)
|
||||
|
||||
- **VS Code Codex 插件一键配置**:供应商卡片支持「应用到 VS Code / 从 VS Code 移除」,默认开启自动同步,并可跨 Code / Insiders / VSCodium 写入 `settings.json`
|
||||
- **通用配置片段**:Claude 与 Codex 共用 JSON/TOML 片段,提供编辑器 lint、内容校验、统一错误提示与本地持久化
|
||||
- **Codex 配置向导**:新增显示名称、专用 API Key URL、HTML5 校验与预设模板,方便快速配置第三方服务
|
||||
- **系统托盘与快捷操作**:窗口隐藏时仍可通过托盘切换供应商,并在自动同步开启时触发 VS Code 写入
|
||||
- **平台适配**:新增 Windows WSL 环境支持、Linux 自动禁用模态背景模糊解决白屏问题、macOS Dock 点击即可恢复窗口
|
||||
- **UI优化**:多处 UI 和使用体验优化
|
||||
- **国际化与语言切换**:内置 i18next,默认显示中文,可在设置中快速切换到英文,界面文文案自动实时刷新。
|
||||
- **Claude 插件同步**:在 VS Code 同步按钮旁新增 Claude 插件同步选项,与 Codex 同步互不冲突,切换供应商后立即应用。
|
||||
- **供应商预设扩展**:新增 DeepSeek--V3.2-Exp、Qwen3-Max、GLM-4.6 等最新模型。
|
||||
- **系统托盘与窗口行为**:窗口关闭可最小化到托盘,macOS 支持托盘模式下隐藏/显示 Dock,托盘切换时同步 Claude/Codex/插件状态。
|
||||
- **单实例**:保证同一时间仅运行一个实例,避免多开冲突。
|
||||
- **UI 与安装体验优化**:设置面板改为可滚动布局并加入保存图标,按钮宽度与状态一致性加强,Windows MSI 安装默认写入 per-user LocalAppData 并改进组件跟踪,Windows 便携版现在指向最新 release 页面,不再自动更为为安装版。
|
||||
|
||||
## 界面预览
|
||||
|
||||
|
||||
76
README_i18n.md
Normal file
76
README_i18n.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# CC Switch 国际化功能说明
|
||||
|
||||
## 已完成的工作
|
||||
|
||||
1. **安装依赖**:添加了 `react-i18next` 和 `i18next` 包
|
||||
2. **配置国际化**:在 `src/i18n/` 目录下创建了配置文件
|
||||
3. **翻译文件**:创建了英文和中文翻译文件
|
||||
4. **组件更新**:替换了主要组件中的硬编码文案
|
||||
5. **语言切换器**:添加了语言切换按钮
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── i18n/
|
||||
│ ├── index.ts # 国际化配置文件
|
||||
│ └── locales/
|
||||
│ ├── en.json # 英文翻译
|
||||
│ └── zh.json # 中文翻译
|
||||
├── components/
|
||||
│ └── LanguageSwitcher.tsx # 语言切换组件
|
||||
└── main.tsx # 导入国际化配置
|
||||
```
|
||||
|
||||
## 默认语言设置
|
||||
|
||||
- **默认语言**:英文 (en)
|
||||
- **回退语言**:英文 (en)
|
||||
|
||||
## 使用方式
|
||||
|
||||
1. 在组件中导入 `useTranslation`:
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function MyComponent() {
|
||||
const { t } = useTranslation();
|
||||
return <div>{t('common.save')}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
2. 切换语言:
|
||||
```tsx
|
||||
const { i18n } = useTranslation();
|
||||
i18n.changeLanguage('zh'); // 切换到中文
|
||||
```
|
||||
|
||||
## 翻译键结构
|
||||
|
||||
- `common.*` - 通用文案(保存、取消、设置等)
|
||||
- `header.*` - 头部相关文案
|
||||
- `provider.*` - 供应商相关文案
|
||||
- `notifications.*` - 通知消息
|
||||
- `settings.*` - 设置页面文案
|
||||
- `apps.*` - 应用名称
|
||||
- `console.*` - 控制台日志信息
|
||||
|
||||
## 测试功能
|
||||
|
||||
应用已添加了语言切换按钮(地球图标),点击可以在中英文之间切换,验证国际化功能是否正常工作。
|
||||
|
||||
## 已更新的组件
|
||||
|
||||
- ✅ App.tsx - 主应用组件
|
||||
- ✅ ConfirmDialog.tsx - 确认对话框
|
||||
- ✅ AddProviderModal.tsx - 添加供应商弹窗
|
||||
- ✅ EditProviderModal.tsx - 编辑供应商弹窗
|
||||
- ✅ ProviderList.tsx - 供应商列表
|
||||
- ✅ LanguageSwitcher.tsx - 语言切换器
|
||||
- 🔄 SettingsModal.tsx - 设置弹窗(部分完成)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有新的文案都应该添加到翻译文件中,而不是硬编码
|
||||
2. 翻译键名应该有意义且结构化
|
||||
3. 可以通过修改 `src/i18n/index.ts` 中的 `lng` 配置来更改默认语言
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cc-switch",
|
||||
"version": "3.3.1",
|
||||
"version": "3.4.0",
|
||||
"description": "Claude Code & Codex 供应商切换工具",
|
||||
"scripts": {
|
||||
"dev": "pnpm tauri dev",
|
||||
@@ -37,10 +37,12 @@
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"i18next": "^25.5.2",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^16.0.0",
|
||||
"tailwindcss": "^4.1.13"
|
||||
}
|
||||
}
|
||||
|
||||
65
pnpm-lock.yaml
generated
65
pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ importers:
|
||||
codemirror:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
i18next:
|
||||
specifier: ^25.5.2
|
||||
version: 25.5.2(typescript@5.9.2)
|
||||
jsonc-parser:
|
||||
specifier: ^3.2.1
|
||||
version: 3.3.1
|
||||
@@ -53,6 +56,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-i18next:
|
||||
specifier: ^16.0.0
|
||||
version: 16.0.0(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)
|
||||
tailwindcss:
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.13
|
||||
@@ -159,6 +165,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.28.4':
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -750,6 +760,17 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
i18next@25.5.2:
|
||||
resolution: {integrity: sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
jiti@2.5.1:
|
||||
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
||||
hasBin: true
|
||||
@@ -890,6 +911,22 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-i18next@16.0.0:
|
||||
resolution: {integrity: sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==}
|
||||
peerDependencies:
|
||||
i18next: '>= 25.5.2'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
react-refresh@0.17.0:
|
||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -973,6 +1010,10 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
@@ -1079,6 +1120,8 @@ snapshots:
|
||||
'@babel/core': 7.28.0
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@@ -1591,6 +1634,16 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
|
||||
i18next@25.5.2(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
|
||||
jiti@2.5.1: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -1692,6 +1745,16 @@ snapshots:
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-i18next@16.0.0(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 25.5.2(typescript@5.9.2)
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
typescript: 5.9.2
|
||||
|
||||
react-refresh@0.17.0: {}
|
||||
|
||||
react@18.3.1:
|
||||
@@ -1767,6 +1830,8 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
lightningcss: 1.30.1
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -563,7 +563,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc-switch"
|
||||
version = "3.3.1"
|
||||
version = "3.4.0"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"log",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cc-switch"
|
||||
version = "3.3.1"
|
||||
version = "3.4.0"
|
||||
description = "Claude Code & Codex 供应商配置管理工具"
|
||||
authors = ["Jason Young"]
|
||||
license = "MIT"
|
||||
|
||||
103
src-tauri/src/claude_plugin.rs
Normal file
103
src-tauri/src/claude_plugin.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const CLAUDE_DIR: &str = ".claude";
|
||||
const CLAUDE_CONFIG_FILE: &str = "config.json";
|
||||
const CLAUDE_CONFIG_PAYLOAD: &str = "{\n \"primaryApiKey\": \"any\"\n}\n";
|
||||
|
||||
fn claude_dir() -> Result<PathBuf, String> {
|
||||
let home = dirs::home_dir().ok_or_else(|| "无法获取用户主目录".to_string())?;
|
||||
Ok(home.join(CLAUDE_DIR))
|
||||
}
|
||||
|
||||
pub fn claude_config_path() -> Result<PathBuf, String> {
|
||||
Ok(claude_dir()?.join(CLAUDE_CONFIG_FILE))
|
||||
}
|
||||
|
||||
pub fn ensure_claude_dir_exists() -> Result<PathBuf, String> {
|
||||
let dir = claude_dir()?;
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir).map_err(|e| format!("创建 Claude 配置目录失败: {}", e))?;
|
||||
}
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
pub fn read_claude_config() -> Result<Option<String>, String> {
|
||||
let path = claude_config_path()?;
|
||||
if path.exists() {
|
||||
let content =
|
||||
fs::read_to_string(&path).map_err(|e| format!("读取 Claude 配置失败: {}", e))?;
|
||||
Ok(Some(content))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_managed_config(content: &str) -> bool {
|
||||
match serde_json::from_str::<serde_json::Value>(content) {
|
||||
Ok(value) => value
|
||||
.get("primaryApiKey")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|val| val == "any")
|
||||
.unwrap_or(false),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_claude_config() -> Result<bool, String> {
|
||||
let path = claude_config_path()?;
|
||||
ensure_claude_dir_exists()?;
|
||||
let need_write = match read_claude_config()? {
|
||||
Some(existing) => existing != CLAUDE_CONFIG_PAYLOAD,
|
||||
None => true,
|
||||
};
|
||||
if need_write {
|
||||
fs::write(&path, CLAUDE_CONFIG_PAYLOAD)
|
||||
.map_err(|e| format!("写入 Claude 配置失败: {}", e))?;
|
||||
}
|
||||
Ok(need_write)
|
||||
}
|
||||
|
||||
pub fn clear_claude_config() -> Result<bool, String> {
|
||||
let path = claude_config_path()?;
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let content = match read_claude_config()? {
|
||||
Some(content) => content,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
let mut value = match serde_json::from_str::<serde_json::Value>(&content) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
|
||||
let obj = match value.as_object_mut() {
|
||||
Some(obj) => obj,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
if obj.remove("primaryApiKey").is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let serialized = serde_json::to_string_pretty(&value)
|
||||
.map_err(|e| format!("序列化 Claude 配置失败: {}", e))?;
|
||||
fs::write(&path, format!("{}\n", serialized))
|
||||
.map_err(|e| format!("写入 Claude 配置失败: {}", e))?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn claude_config_status() -> Result<(bool, PathBuf), String> {
|
||||
let path = claude_config_path()?;
|
||||
Ok((path.exists(), path))
|
||||
}
|
||||
|
||||
pub fn is_claude_config_applied() -> Result<bool, String> {
|
||||
match read_claude_config()? {
|
||||
Some(content) => Ok(is_managed_config(&content)),
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use tauri_plugin_dialog::DialogExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
use crate::app_config::AppType;
|
||||
use crate::claude_plugin;
|
||||
use crate::codex_config;
|
||||
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
||||
use crate::provider::Provider;
|
||||
@@ -730,3 +731,37 @@ pub async fn write_vscode_settings(content: String) -> Result<bool, String> {
|
||||
Err("未找到 VS Code 用户设置文件".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude 插件:获取 ~/.claude/config.json 状态
|
||||
#[tauri::command]
|
||||
pub async fn get_claude_plugin_status() -> Result<ConfigStatus, String> {
|
||||
match claude_plugin::claude_config_status() {
|
||||
Ok((exists, path)) => Ok(ConfigStatus {
|
||||
exists,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude 插件:读取配置内容(若不存在返回 Ok(None))
|
||||
#[tauri::command]
|
||||
pub async fn read_claude_plugin_config() -> Result<Option<String>, String> {
|
||||
claude_plugin::read_claude_config()
|
||||
}
|
||||
|
||||
/// Claude 插件:写入/清除固定配置
|
||||
#[tauri::command]
|
||||
pub async fn apply_claude_plugin_config(official: bool) -> Result<bool, String> {
|
||||
if official {
|
||||
claude_plugin::clear_claude_config()
|
||||
} else {
|
||||
claude_plugin::write_claude_config()
|
||||
}
|
||||
}
|
||||
|
||||
/// Claude 插件:检测是否已写入目标配置
|
||||
#[tauri::command]
|
||||
pub async fn is_claude_plugin_applied() -> Result<bool, String> {
|
||||
claude_plugin::is_claude_config_applied()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod app_config;
|
||||
mod claude_plugin;
|
||||
mod codex_config;
|
||||
mod commands;
|
||||
mod config;
|
||||
@@ -9,12 +10,12 @@ mod store;
|
||||
mod vscode;
|
||||
|
||||
use store::AppState;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::RunEvent;
|
||||
use tauri::{
|
||||
menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem},
|
||||
tray::{TrayIconBuilder, TrayIconEvent},
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::{ActivationPolicy, RunEvent};
|
||||
use tauri::{Emitter, Manager};
|
||||
|
||||
/// 创建动态托盘菜单
|
||||
@@ -116,6 +117,23 @@ fn create_tray_menu(
|
||||
.map_err(|e| format!("构建菜单失败: {}", e))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn apply_tray_policy(app: &tauri::AppHandle, dock_visible: bool) {
|
||||
let desired_policy = if dock_visible {
|
||||
ActivationPolicy::Regular
|
||||
} else {
|
||||
ActivationPolicy::Accessory
|
||||
};
|
||||
|
||||
if let Err(err) = app.set_dock_visibility(dock_visible) {
|
||||
log::warn!("设置 Dock 显示状态失败: {}", err);
|
||||
}
|
||||
|
||||
if let Err(err) = app.set_activation_policy(desired_policy) {
|
||||
log::warn!("设置激活策略失败: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理托盘菜单事件
|
||||
fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
|
||||
log::info!("处理托盘菜单事件: {}", event_id);
|
||||
@@ -130,6 +148,10 @@ fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
apply_tray_policy(app, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
@@ -267,6 +289,10 @@ pub fn run() {
|
||||
{
|
||||
let _ = window.set_skip_taskbar(true);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
apply_tray_policy(&window.app_handle(), false);
|
||||
}
|
||||
} else {
|
||||
window.app_handle().exit(0);
|
||||
}
|
||||
@@ -393,6 +419,10 @@ pub fn run() {
|
||||
commands::get_vscode_settings_status,
|
||||
commands::read_vscode_settings,
|
||||
commands::write_vscode_settings,
|
||||
commands::get_claude_plugin_status,
|
||||
commands::read_claude_plugin_config,
|
||||
commands::apply_claude_plugin_config,
|
||||
commands::is_claude_plugin_applied,
|
||||
update_tray_menu,
|
||||
]);
|
||||
|
||||
@@ -413,6 +443,7 @@ pub fn run() {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
apply_tray_policy(app_handle, true);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -15,6 +15,8 @@ pub struct AppSettings {
|
||||
pub claude_config_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub codex_config_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<String>,
|
||||
}
|
||||
|
||||
fn default_show_in_tray() -> bool {
|
||||
@@ -32,6 +34,7 @@ impl Default for AppSettings {
|
||||
minimize_to_tray_on_close: true,
|
||||
claude_config_dir: None,
|
||||
codex_config_dir: None,
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,6 +58,13 @@ impl AppSettings {
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
self.language = self
|
||||
.language
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| matches!(*s, "en" | "zh"))
|
||||
.map(|s| s.to_string());
|
||||
}
|
||||
|
||||
pub fn load() -> Self {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CC Switch",
|
||||
"version": "3.3.1",
|
||||
"version": "3.4.0",
|
||||
"identifier": "com.ccswitch.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
99
src/App.tsx
99
src/App.tsx
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Provider } from "./types";
|
||||
import { AppType } from "./lib/tauri-api";
|
||||
import ProviderList from "./components/ProviderList";
|
||||
@@ -17,6 +18,7 @@ import { getCodexBaseUrl } from "./utils/providerConfigUtils";
|
||||
import { useVSCodeAutoSync } from "./hooks/useVSCodeAutoSync";
|
||||
|
||||
function App() {
|
||||
const { t } = useTranslation();
|
||||
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
||||
const { isAutoSyncEnabled } = useVSCodeAutoSync();
|
||||
const [activeApp, setActiveApp] = useState<AppType>("claude");
|
||||
@@ -24,7 +26,7 @@ function App() {
|
||||
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
||||
null,
|
||||
null
|
||||
);
|
||||
const [notification, setNotification] = useState<{
|
||||
message: string;
|
||||
@@ -44,7 +46,7 @@ function App() {
|
||||
const showNotification = (
|
||||
message: string,
|
||||
type: "success" | "error",
|
||||
duration = 3000,
|
||||
duration = 3000
|
||||
) => {
|
||||
// 清除之前的定时器
|
||||
if (timeoutRef.current) {
|
||||
@@ -88,7 +90,7 @@ function App() {
|
||||
try {
|
||||
unlisten = await window.api.onProviderSwitched(async (data) => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("收到供应商切换事件:", data);
|
||||
console.log(t("console.providerSwitchReceived"), data);
|
||||
}
|
||||
|
||||
// 如果当前应用类型匹配,则重新加载数据
|
||||
@@ -100,9 +102,13 @@ function App() {
|
||||
if (data.appType === "codex" && isAutoSyncEnabled) {
|
||||
await syncCodexToVSCode(data.providerId, true);
|
||||
}
|
||||
|
||||
if (data.appType === "claude") {
|
||||
await syncClaudePlugin(data.providerId, true);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("设置供应商切换监听器失败:", error);
|
||||
console.error(t("console.setupListenerFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,16 +158,16 @@ function App() {
|
||||
await loadProviders();
|
||||
setEditingProviderId(null);
|
||||
// 显示编辑成功提示
|
||||
showNotification("供应商配置已保存", "success", 2000);
|
||||
showNotification(t("notifications.providerSaved"), "success", 2000);
|
||||
// 更新托盘菜单
|
||||
await window.api.updateTrayMenu();
|
||||
} catch (error) {
|
||||
console.error("更新供应商失败:", error);
|
||||
console.error(t("console.updateProviderFailed"), error);
|
||||
setEditingProviderId(null);
|
||||
const errorMessage = extractErrorMessage(error);
|
||||
const message = errorMessage
|
||||
? `保存失败:${errorMessage}`
|
||||
: "保存失败,请重试";
|
||||
? t("notifications.saveFailed", { error: errorMessage })
|
||||
: t("notifications.saveFailedGeneric");
|
||||
showNotification(message, "error", errorMessage ? 6000 : 3000);
|
||||
}
|
||||
};
|
||||
@@ -170,13 +176,13 @@ function App() {
|
||||
const provider = providers[id];
|
||||
setConfirmDialog({
|
||||
isOpen: true,
|
||||
title: "删除供应商",
|
||||
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`,
|
||||
title: t("confirm.deleteProvider"),
|
||||
message: t("confirm.deleteProviderMessage", { name: provider?.name }),
|
||||
onConfirm: async () => {
|
||||
await window.api.deleteProvider(id, activeApp);
|
||||
await loadProviders();
|
||||
setConfirmDialog(null);
|
||||
showNotification("供应商删除成功", "success");
|
||||
showNotification(t("notifications.providerDeleted"), "success");
|
||||
// 更新托盘菜单
|
||||
await window.api.updateTrayMenu();
|
||||
},
|
||||
@@ -190,9 +196,9 @@ function App() {
|
||||
if (!status.exists) {
|
||||
if (!silent) {
|
||||
showNotification(
|
||||
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||
t("notifications.vscodeSettingsNotFound"),
|
||||
"error",
|
||||
3000,
|
||||
3000
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -208,11 +214,7 @@ function App() {
|
||||
const parsed = getCodexBaseUrl(provider);
|
||||
if (!parsed) {
|
||||
if (!silent) {
|
||||
showNotification(
|
||||
"当前配置缺少 base_url,无法写入 VS Code",
|
||||
"error",
|
||||
4000,
|
||||
);
|
||||
showNotification(t("notifications.missingBaseUrl"), "error", 4000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -226,31 +228,58 @@ function App() {
|
||||
if (updatedSettings !== raw) {
|
||||
await window.api.writeVSCodeSettings(updatedSettings);
|
||||
if (!silent) {
|
||||
showNotification("已同步到 VS Code", "success", 1500);
|
||||
showNotification(t("notifications.syncedToVSCode"), "success", 1500);
|
||||
}
|
||||
}
|
||||
|
||||
// 触发providers重新加载,以更新VS Code按钮状态
|
||||
await loadProviders();
|
||||
} catch (error: any) {
|
||||
console.error("同步到VS Code失败:", error);
|
||||
console.error(t("console.syncToVSCodeFailed"), error);
|
||||
if (!silent) {
|
||||
const errorMessage = error?.message || "同步 VS Code 失败";
|
||||
const errorMessage =
|
||||
error?.message || t("notifications.syncVSCodeFailed");
|
||||
showNotification(errorMessage, "error", 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 同步 Claude 插件配置(写入/移除固定 JSON)
|
||||
const syncClaudePlugin = async (providerId: string, silent = false) => {
|
||||
try {
|
||||
const provider = providers[providerId];
|
||||
if (!provider) return;
|
||||
const isOfficial = provider.category === "official";
|
||||
await window.api.applyClaudePluginConfig({ official: isOfficial });
|
||||
if (!silent) {
|
||||
showNotification(
|
||||
isOfficial
|
||||
? t("notifications.removedFromClaudePlugin")
|
||||
: t("notifications.appliedToClaudePlugin"),
|
||||
"success",
|
||||
2000,
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("同步 Claude 插件失败:", error);
|
||||
if (!silent) {
|
||||
const message =
|
||||
error?.message || t("notifications.syncClaudePluginFailed");
|
||||
showNotification(message, "error", 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchProvider = async (id: string) => {
|
||||
const success = await window.api.switchProvider(id, activeApp);
|
||||
if (success) {
|
||||
setCurrentProviderId(id);
|
||||
// 显示重启提示
|
||||
const appName = activeApp === "claude" ? "Claude Code" : "Codex";
|
||||
const appName = t(`apps.${activeApp}`);
|
||||
showNotification(
|
||||
`切换成功!请重启 ${appName} 终端以生效`,
|
||||
t("notifications.switchSuccess", { appName }),
|
||||
"success",
|
||||
2000,
|
||||
2000
|
||||
);
|
||||
// 更新托盘菜单
|
||||
await window.api.updateTrayMenu();
|
||||
@@ -259,8 +288,12 @@ function App() {
|
||||
if (activeApp === "codex" && isAutoSyncEnabled) {
|
||||
await syncCodexToVSCode(id, true); // silent模式,不显示通知
|
||||
}
|
||||
|
||||
if (activeApp === "claude") {
|
||||
await syncClaudePlugin(id, true);
|
||||
}
|
||||
} else {
|
||||
showNotification("切换失败,请检查配置", "error");
|
||||
showNotification(t("notifications.switchFailed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -271,13 +304,13 @@ function App() {
|
||||
|
||||
if (result.success) {
|
||||
await loadProviders();
|
||||
showNotification("已从现有配置创建默认供应商", "success", 3000);
|
||||
showNotification(t("notifications.autoImported"), "success", 3000);
|
||||
// 更新托盘菜单
|
||||
await window.api.updateTrayMenu();
|
||||
}
|
||||
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
|
||||
} catch (error) {
|
||||
console.error("自动导入默认配置失败:", error);
|
||||
console.error(t("console.autoImportFailed"), error);
|
||||
// 静默处理,不影响用户体验
|
||||
}
|
||||
};
|
||||
@@ -293,14 +326,18 @@ function App() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xl font-semibold text-blue-500 dark:text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 transition-colors"
|
||||
title="在 GitHub 上查看"
|
||||
title={t("header.viewOnGithub")}
|
||||
>
|
||||
CC Switch
|
||||
</a>
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className={buttonStyles.icon}
|
||||
title={isDarkMode ? "切换到亮色模式" : "切换到暗色模式"}
|
||||
title={
|
||||
isDarkMode
|
||||
? t("header.toggleLightMode")
|
||||
: t("header.toggleDarkMode")
|
||||
}
|
||||
>
|
||||
{isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</button>
|
||||
@@ -308,7 +345,7 @@ function App() {
|
||||
<button
|
||||
onClick={() => setIsSettingsOpen(true)}
|
||||
className={buttonStyles.icon}
|
||||
title="设置"
|
||||
title={t("common.settings")}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</button>
|
||||
@@ -324,7 +361,7 @@ function App() {
|
||||
className={`inline-flex items-center gap-2 ${buttonStyles.primary}`}
|
||||
>
|
||||
<Plus size={16} />
|
||||
添加供应商
|
||||
{t("header.addProvider")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Provider } from "../types";
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
import ProviderForm from "./ProviderForm";
|
||||
@@ -14,11 +15,13 @@ const AddProviderModal: React.FC<AddProviderModalProps> = ({
|
||||
onAdd,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ProviderForm
|
||||
appType={appType}
|
||||
title="添加新供应商"
|
||||
submitText="添加"
|
||||
title={t("provider.addNewProvider")}
|
||||
submitText={t("common.add")}
|
||||
showPresets={true}
|
||||
onSubmit={onAdd}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertTriangle, X } from "lucide-react";
|
||||
import { isLinux } from "../lib/platform";
|
||||
|
||||
@@ -16,11 +17,13 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
confirmText = "确定",
|
||||
cancelText = "取消",
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
@@ -65,13 +68,13 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
||||
className="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-900 hover:bg-white dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
autoFocus
|
||||
>
|
||||
{cancelText}
|
||||
{cancelText || t("common.cancel")}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 text-sm font-medium bg-red-500 text-white hover:bg-red-500/90 rounded-md transition-colors"
|
||||
>
|
||||
{confirmText}
|
||||
{confirmText || t("common.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Provider } from "../types";
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
import ProviderForm from "./ProviderForm";
|
||||
@@ -16,6 +17,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
||||
onSave,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = (data: Omit<Provider, "id">) => {
|
||||
onSave({
|
||||
...provider,
|
||||
@@ -26,8 +29,8 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
||||
return (
|
||||
<ProviderForm
|
||||
appType={appType}
|
||||
title="编辑供应商"
|
||||
submitText="保存"
|
||||
title={t("common.edit")}
|
||||
submitText={t("common.save")}
|
||||
initialData={provider}
|
||||
showPresets={false}
|
||||
onSubmit={handleSubmit}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Provider } from "../types";
|
||||
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
||||
import { Play, Edit3, Trash2, CheckCircle2, Users, Check } from "lucide-react";
|
||||
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
import {
|
||||
@@ -35,6 +36,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
appType,
|
||||
onNotify,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// 提取API地址(兼容不同供应商配置:Claude env / Codex TOML)
|
||||
const getApiUrl = (provider: Provider): string => {
|
||||
try {
|
||||
@@ -49,9 +51,9 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
const match = cfg.config.match(/base_url\s*=\s*(['"])([^'\"]+)\1/);
|
||||
if (match && match[2]) return match[2];
|
||||
}
|
||||
return "未配置官网地址";
|
||||
return t("provider.notConfigured");
|
||||
} catch {
|
||||
return "配置错误";
|
||||
return t("provider.configError");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,7 +61,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
try {
|
||||
await window.api.openExternal(url);
|
||||
} catch (error) {
|
||||
console.error("打开链接失败:", error);
|
||||
console.error(t("console.openLinkFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,6 +70,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
// VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否"已应用"变化
|
||||
const [vscodeAppliedFor, setVscodeAppliedFor] = useState<string | null>(null);
|
||||
const { enableAutoSync, disableAutoSync } = useVSCodeAutoSync();
|
||||
const [claudeApplied, setClaudeApplied] = useState<boolean>(false);
|
||||
|
||||
// 当当前供应商或 appType 变化时,尝试读取 VS Code settings 并检测状态
|
||||
useEffect(() => {
|
||||
@@ -102,15 +105,29 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
check();
|
||||
}, [appType, currentProviderId, providers]);
|
||||
|
||||
// 检查 Claude 插件配置是否已应用
|
||||
useEffect(() => {
|
||||
const checkClaude = async () => {
|
||||
if (appType !== "claude" || !currentProviderId) {
|
||||
setClaudeApplied(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const applied = await window.api.isClaudePluginApplied();
|
||||
setClaudeApplied(applied);
|
||||
} catch (error) {
|
||||
console.error("检测 Claude 插件配置失败:", error);
|
||||
setClaudeApplied(false);
|
||||
}
|
||||
};
|
||||
checkClaude();
|
||||
}, [appType, currentProviderId, providers]);
|
||||
|
||||
const handleApplyToVSCode = async (provider: Provider) => {
|
||||
try {
|
||||
const status = await window.api.getVSCodeSettingsStatus();
|
||||
if (!status.exists) {
|
||||
onNotify?.(
|
||||
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||
"error",
|
||||
3000
|
||||
);
|
||||
onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +138,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
if (!isOfficial) {
|
||||
const parsed = getCodexBaseUrl(provider);
|
||||
if (!parsed) {
|
||||
onNotify?.("当前配置缺少 base_url,无法写入 VS Code", "error", 4000);
|
||||
onNotify?.(t("notifications.missingBaseUrl"), "error", 4000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -131,7 +148,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
|
||||
if (next === raw) {
|
||||
// 幂等:没有变化也提示成功
|
||||
onNotify?.("已应用到 VS Code,重启 Codex 插件以生效", "success", 3000);
|
||||
onNotify?.(t("notifications.appliedToVSCode"), "success", 3000);
|
||||
setVscodeAppliedFor(provider.id);
|
||||
// 用户手动应用时,启用自动同步
|
||||
enableAutoSync();
|
||||
@@ -139,13 +156,14 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
}
|
||||
|
||||
await window.api.writeVSCodeSettings(next);
|
||||
onNotify?.("已应用到 VS Code,重启 Codex 插件以生效", "success", 3000);
|
||||
onNotify?.(t("notifications.appliedToVSCode"), "success", 3000);
|
||||
setVscodeAppliedFor(provider.id);
|
||||
// 用户手动应用时,启用自动同步
|
||||
enableAutoSync();
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
const msg = e && e.message ? e.message : "应用到 VS Code 失败";
|
||||
const msg =
|
||||
e && e.message ? e.message : t("notifications.syncVSCodeFailed");
|
||||
onNotify?.(msg, "error", 5000);
|
||||
}
|
||||
};
|
||||
@@ -154,11 +172,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
try {
|
||||
const status = await window.api.getVSCodeSettingsStatus();
|
||||
if (!status.exists) {
|
||||
onNotify?.(
|
||||
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||
"error",
|
||||
3000
|
||||
);
|
||||
onNotify?.(t("notifications.vscodeSettingsNotFound"), "error", 3000);
|
||||
return;
|
||||
}
|
||||
const raw = await window.api.readVSCodeSettings();
|
||||
@@ -167,20 +181,51 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
isOfficial: true,
|
||||
});
|
||||
if (next === raw) {
|
||||
onNotify?.("已从 VS Code 移除,重启 Codex 插件以生效", "success", 3000);
|
||||
onNotify?.(t("notifications.removedFromVSCode"), "success", 3000);
|
||||
setVscodeAppliedFor(null);
|
||||
// 用户手动移除时,禁用自动同步
|
||||
disableAutoSync();
|
||||
return;
|
||||
}
|
||||
await window.api.writeVSCodeSettings(next);
|
||||
onNotify?.("已从 VS Code 移除,重启 Codex 插件以生效", "success", 3000);
|
||||
onNotify?.(t("notifications.removedFromVSCode"), "success", 3000);
|
||||
setVscodeAppliedFor(null);
|
||||
// 用户手动移除时,禁用自动同步
|
||||
disableAutoSync();
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
const msg = e && e.message ? e.message : "移除失败";
|
||||
const msg =
|
||||
e && e.message ? e.message : t("notifications.syncVSCodeFailed");
|
||||
onNotify?.(msg, "error", 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyToClaudePlugin = async () => {
|
||||
try {
|
||||
await window.api.applyClaudePluginConfig({ official: false });
|
||||
onNotify?.(t("notifications.appliedToClaudePlugin"), "success", 3000);
|
||||
setClaudeApplied(true);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const msg =
|
||||
error && error.message
|
||||
? error.message
|
||||
: t("notifications.syncClaudePluginFailed");
|
||||
onNotify?.(msg, "error", 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFromClaudePlugin = async () => {
|
||||
try {
|
||||
await window.api.applyClaudePluginConfig({ official: true });
|
||||
onNotify?.(t("notifications.removedFromClaudePlugin"), "success", 3000);
|
||||
setClaudeApplied(false);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const msg =
|
||||
error && error.message
|
||||
? error.message
|
||||
: t("notifications.syncClaudePluginFailed");
|
||||
onNotify?.(msg, "error", 5000);
|
||||
}
|
||||
};
|
||||
@@ -214,10 +259,10 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
<Users size={24} className="text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
还没有添加任何供应商
|
||||
{t("provider.noProviders")}
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||
点击右上角的"添加供应商"按钮开始配置您的第一个API供应商
|
||||
{t("provider.noProvidersDescription")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -247,7 +292,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 size={12} />
|
||||
当前使用
|
||||
{t("provider.currentlyUsing")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -275,50 +320,82 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{appType === "codex" &&
|
||||
provider.category !== "official" && (
|
||||
<button
|
||||
onClick={() =>
|
||||
vscodeAppliedFor === provider.id
|
||||
? handleRemoveFromVSCode()
|
||||
: handleApplyToVSCode(provider)
|
||||
}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[130px] whitespace-nowrap justify-center",
|
||||
!isCurrent && "invisible",
|
||||
vscodeAppliedFor === provider.id
|
||||
? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20"
|
||||
: "border border-gray-300 text-gray-700 hover:border-blue-300 hover:text-blue-600 hover:bg-blue-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-700 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
|
||||
)}
|
||||
title={
|
||||
vscodeAppliedFor === provider.id
|
||||
? "从 VS Code 移除我们写入的配置"
|
||||
: "将当前供应商应用到 VS Code"
|
||||
}
|
||||
>
|
||||
{vscodeAppliedFor === provider.id
|
||||
? "从 VS Code 移除"
|
||||
: "应用到 VS Code"}
|
||||
</button>
|
||||
)}
|
||||
{/* 同步按钮占位容器 - 只在对应模式下渲染,避免布局跳动 */}
|
||||
{appType === "codex" ? (
|
||||
<div className="w-[130px]">
|
||||
{provider.category !== "official" && isCurrent && (
|
||||
<button
|
||||
onClick={() =>
|
||||
vscodeAppliedFor === provider.id
|
||||
? handleRemoveFromVSCode()
|
||||
: handleApplyToVSCode(provider)
|
||||
}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-full whitespace-nowrap justify-center",
|
||||
vscodeAppliedFor === provider.id
|
||||
? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20"
|
||||
: "border border-gray-300 text-gray-700 hover:border-blue-300 hover:text-blue-600 hover:bg-blue-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-700 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
|
||||
)}
|
||||
title={
|
||||
vscodeAppliedFor === provider.id
|
||||
? t("provider.removeFromVSCode")
|
||||
: t("provider.applyToVSCode")
|
||||
}
|
||||
>
|
||||
{vscodeAppliedFor === provider.id
|
||||
? t("provider.removeFromVSCode")
|
||||
: t("provider.applyToVSCode")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{appType === "claude" ? (
|
||||
<div className="w-[130px]">
|
||||
{provider.category !== "official" && isCurrent && (
|
||||
<button
|
||||
onClick={() =>
|
||||
claudeApplied
|
||||
? handleRemoveFromClaudePlugin()
|
||||
: handleApplyToClaudePlugin()
|
||||
}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-full whitespace-nowrap justify-center",
|
||||
claudeApplied
|
||||
? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20"
|
||||
: "border border-gray-300 text-gray-700 hover:border-green-300 hover:text-green-600 hover:bg-green-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-green-700 dark:hover:text-green-400 dark:hover:bg-green-900/20"
|
||||
)}
|
||||
title={
|
||||
claudeApplied
|
||||
? t("provider.removeFromClaudePlugin")
|
||||
: t("provider.applyToClaudePlugin")
|
||||
}
|
||||
>
|
||||
{claudeApplied
|
||||
? t("provider.removeFromClaudePlugin")
|
||||
: t("provider.applyToClaudePlugin")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={isCurrent}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[76px] justify-center whitespace-nowrap",
|
||||
"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[90px] justify-center whitespace-nowrap",
|
||||
isCurrent
|
||||
? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed"
|
||||
: "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||
)}
|
||||
>
|
||||
{!isCurrent && <Play size={14} />}
|
||||
{isCurrent ? "使用中" : "启用"}
|
||||
{isCurrent ? <Check size={14} /> : <Play size={14} />}
|
||||
{isCurrent ? t("provider.inUse") : t("provider.enable")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onEdit(provider.id)}
|
||||
className={buttonStyles.icon}
|
||||
title="编辑供应商"
|
||||
title={t("provider.editProvider")}
|
||||
>
|
||||
<Edit3 size={16} />
|
||||
</button>
|
||||
@@ -332,7 +409,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10"
|
||||
)}
|
||||
title="删除供应商"
|
||||
title={t("provider.deleteProvider")}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
X,
|
||||
RefreshCw,
|
||||
@@ -24,12 +25,33 @@ interface SettingsModalProps {
|
||||
}
|
||||
|
||||
export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const normalizeLanguage = (lang?: string | null): "zh" | "en" =>
|
||||
lang === "en" ? "en" : "zh";
|
||||
|
||||
const readPersistedLanguage = (): "zh" | "en" => {
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = window.localStorage.getItem("language");
|
||||
if (stored === "en" || stored === "zh") {
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
return normalizeLanguage(i18n.language);
|
||||
};
|
||||
|
||||
const persistedLanguage = readPersistedLanguage();
|
||||
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
showInTray: true,
|
||||
minimizeToTrayOnClose: true,
|
||||
claudeConfigDir: undefined,
|
||||
codexConfigDir: undefined,
|
||||
language: persistedLanguage,
|
||||
});
|
||||
const [initialLanguage, setInitialLanguage] = useState<"zh" | "en">(
|
||||
persistedLanguage,
|
||||
);
|
||||
const [configPath, setConfigPath] = useState<string>("");
|
||||
const [version, setVersion] = useState<string>("");
|
||||
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
||||
@@ -54,9 +76,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
const appVersion = await getVersion();
|
||||
setVersion(appVersion);
|
||||
} catch (error) {
|
||||
console.error("获取版本信息失败:", error);
|
||||
console.error(t("console.getVersionFailed"), error);
|
||||
// 失败时不硬编码版本号,显示为未知
|
||||
setVersion("未知");
|
||||
setVersion(t("common.unknown"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,6 +93,12 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
(loadedSettings as any)?.minimizeToTrayOnClose ??
|
||||
(loadedSettings as any)?.minimize_to_tray_on_close ??
|
||||
true;
|
||||
const storedLanguage = normalizeLanguage(
|
||||
typeof (loadedSettings as any)?.language === "string"
|
||||
? (loadedSettings as any).language
|
||||
: persistedLanguage,
|
||||
);
|
||||
|
||||
setSettings({
|
||||
showInTray,
|
||||
minimizeToTrayOnClose,
|
||||
@@ -82,9 +110,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
typeof (loadedSettings as any)?.codexConfigDir === "string"
|
||||
? (loadedSettings as any).codexConfigDir
|
||||
: undefined,
|
||||
language: storedLanguage,
|
||||
});
|
||||
setInitialLanguage(storedLanguage);
|
||||
if (i18n.language !== storedLanguage) {
|
||||
void i18n.changeLanguage(storedLanguage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载设置失败:", error);
|
||||
console.error(t("console.loadSettingsFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,7 +128,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
setConfigPath(path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取配置路径失败:", error);
|
||||
console.error(t("console.getConfigPathFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,7 +141,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
setResolvedClaudeDir(claudeDir || "");
|
||||
setResolvedCodexDir(codexDir || "");
|
||||
} catch (error) {
|
||||
console.error("获取配置目录失败:", error);
|
||||
console.error(t("console.getConfigDirFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,12 +150,13 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
const portable = await window.api.isPortable();
|
||||
setIsPortable(portable);
|
||||
} catch (error) {
|
||||
console.error("检测便携模式失败:", error);
|
||||
console.error(t("console.detectPortableFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
const selectedLanguage = settings.language === "en" ? "en" : "zh";
|
||||
const payload: Settings = {
|
||||
...settings,
|
||||
claudeConfigDir:
|
||||
@@ -133,15 +167,42 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
settings.codexConfigDir && settings.codexConfigDir.trim() !== ""
|
||||
? settings.codexConfigDir.trim()
|
||||
: undefined,
|
||||
language: selectedLanguage,
|
||||
};
|
||||
await window.api.saveSettings(payload);
|
||||
setSettings(payload);
|
||||
try {
|
||||
window.localStorage.setItem("language", selectedLanguage);
|
||||
} catch (error) {
|
||||
console.warn("[Settings] Failed to persist language preference", error);
|
||||
}
|
||||
setInitialLanguage(selectedLanguage);
|
||||
if (i18n.language !== selectedLanguage) {
|
||||
void i18n.changeLanguage(selectedLanguage);
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("保存设置失败:", error);
|
||||
console.error(t("console.saveSettingsFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = (lang: "zh" | "en") => {
|
||||
setSettings((prev) => ({ ...prev, language: lang }));
|
||||
if (i18n.language !== lang) {
|
||||
void i18n.changeLanguage(lang);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (settings.language !== initialLanguage) {
|
||||
setSettings((prev) => ({ ...prev, language: initialLanguage }));
|
||||
if (i18n.language !== initialLanguage) {
|
||||
void i18n.changeLanguage(initialLanguage);
|
||||
}
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCheckUpdate = async () => {
|
||||
if (hasUpdate && updateHandle) {
|
||||
if (isPortable) {
|
||||
@@ -155,7 +216,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
await updateHandle.downloadAndInstall();
|
||||
await relaunchApp();
|
||||
} catch (error) {
|
||||
console.error("更新失败:", error);
|
||||
console.error(t("console.updateFailed"), error);
|
||||
// 更新失败时回退到打开 Releases 页面
|
||||
await window.api.checkForUpdates();
|
||||
} finally {
|
||||
@@ -176,7 +237,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("检查更新失败:", error);
|
||||
console.error(t("console.checkUpdateFailed"), error);
|
||||
// 在开发模式下,模拟已是最新版本的响应
|
||||
if (import.meta.env.DEV) {
|
||||
setShowUpToDate(true);
|
||||
@@ -197,7 +258,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
try {
|
||||
await window.api.openAppConfigFolder();
|
||||
} catch (error) {
|
||||
console.error("打开配置文件夹失败:", error);
|
||||
console.error(t("console.openConfigFolderFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -228,7 +289,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
setResolvedCodexDir(sanitized);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("选择配置目录失败:", error);
|
||||
console.error(t("console.selectConfigDirFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -238,7 +299,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
const folder = app === "claude" ? ".claude" : ".codex";
|
||||
return await join(home, folder);
|
||||
} catch (error) {
|
||||
console.error("获取默认配置目录失败:", error);
|
||||
console.error(t("console.getDefaultConfigDirFailed"), error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -266,8 +327,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
const handleOpenReleaseNotes = async () => {
|
||||
try {
|
||||
const targetVersion = updateInfo?.availableVersion || version;
|
||||
const unknownLabel = t("common.unknown");
|
||||
// 如果未知或为空,回退到 releases 首页
|
||||
if (!targetVersion || targetVersion === "未知") {
|
||||
if (!targetVersion || targetVersion === unknownLabel) {
|
||||
await window.api.openExternal(
|
||||
"https://github.com/farion1231/cc-switch/releases"
|
||||
);
|
||||
@@ -280,7 +342,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
`https://github.com/farion1231/cc-switch/releases/tag/${tag}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("打开更新日志失败:", error);
|
||||
console.error(t("console.openReleaseNotesFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -288,7 +350,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
onMouseDown={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
if (e.target === e.currentTarget) handleCancel();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -300,10 +362,10 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
{/* 标题栏 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
|
||||
<h2 className="text-lg font-semibold text-blue-500 dark:text-blue-400">
|
||||
设置
|
||||
{t("settings.title")}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
onClick={handleCancel}
|
||||
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
||||
>
|
||||
<X size={20} className="text-gray-500 dark:text-gray-400" />
|
||||
@@ -312,19 +374,50 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
|
||||
{/* 设置内容 */}
|
||||
<div className="px-6 py-4 space-y-6 overflow-y-auto flex-1">
|
||||
{/* 语言设置 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
{t("settings.language")}
|
||||
</h3>
|
||||
<div className="inline-flex p-0.5 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleLanguageChange("zh")}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all min-w-[80px] ${
|
||||
(settings.language ?? "zh") === "zh"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
{t("settings.languageOptionChinese")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleLanguageChange("en")}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all min-w-[80px] ${
|
||||
settings.language === "en"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
{t("settings.languageOptionEnglish")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 窗口行为设置 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
窗口行为
|
||||
{t("settings.windowBehavior")}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
关闭时最小化到托盘
|
||||
{t("settings.minimizeToTray")}
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。
|
||||
{t("settings.minimizeToTrayDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
@@ -347,18 +440,18 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
{/* 配置文件位置 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
配置文件位置
|
||||
{t("settings.configFileLocation")}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 px-3 py-2 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||
<span className="text-xs font-mono text-gray-500 dark:text-gray-400">
|
||||
{configPath || "加载中..."}
|
||||
{configPath || t("common.loading")}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenConfigFolder}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title="打开文件夹"
|
||||
title={t("settings.openFolder")}
|
||||
>
|
||||
<FolderOpen
|
||||
size={18}
|
||||
@@ -371,16 +464,15 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
{/* 配置目录覆盖 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
配置目录覆盖(高级)
|
||||
{t("settings.configDirectoryOverride")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3 leading-relaxed">
|
||||
在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定 WSL
|
||||
里的配置目录,供应商数据与主环境保持一致。
|
||||
{t("settings.configDirectoryDescription")}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
Claude Code 配置目录
|
||||
{t("settings.claudeConfigDir")}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
@@ -392,14 +484,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
claudeConfigDir: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="例如:/home/<你的用户名>/.claude"
|
||||
placeholder={t("settings.browsePlaceholderClaude")}
|
||||
className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleBrowseConfigDir("claude")}
|
||||
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title="浏览目录"
|
||||
title={t("settings.browseDirectory")}
|
||||
>
|
||||
<FolderSearch size={16} />
|
||||
</button>
|
||||
@@ -407,7 +499,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
type="button"
|
||||
onClick={() => handleResetConfigDir("claude")}
|
||||
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title="恢复默认目录(需保存后生效)"
|
||||
title={t("settings.resetDefault")}
|
||||
>
|
||||
<Undo2 size={16} />
|
||||
</button>
|
||||
@@ -416,7 +508,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
Codex 配置目录
|
||||
{t("settings.codexConfigDir")}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
@@ -428,14 +520,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
codexConfigDir: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="例如:/home/<你的用户名>/.codex"
|
||||
placeholder={t("settings.browsePlaceholderCodex")}
|
||||
className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleBrowseConfigDir("codex")}
|
||||
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title="浏览目录"
|
||||
title={t("settings.browseDirectory")}
|
||||
>
|
||||
<FolderSearch size={16} />
|
||||
</button>
|
||||
@@ -443,7 +535,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
type="button"
|
||||
onClick={() => handleResetConfigDir("codex")}
|
||||
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title="恢复默认目录(需保存后生效)"
|
||||
title={t("settings.resetDefault")}
|
||||
>
|
||||
<Undo2 size={16} />
|
||||
</button>
|
||||
@@ -455,7 +547,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
{/* 关于 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
关于
|
||||
{t("common.about")}
|
||||
</h3>
|
||||
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-start justify-between">
|
||||
@@ -465,7 +557,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
CC Switch
|
||||
</p>
|
||||
<p className="mt-1 text-gray-500 dark:text-gray-400">
|
||||
版本 {version}
|
||||
{t("common.version")} {version}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -474,12 +566,14 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
onClick={handleOpenReleaseNotes}
|
||||
className="px-2 py-1 text-xs font-medium text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 rounded-lg hover:bg-blue-500/10 transition-colors"
|
||||
title={
|
||||
hasUpdate ? "查看该版本更新日志" : "查看当前版本更新日志"
|
||||
hasUpdate
|
||||
? t("settings.viewReleaseNotes")
|
||||
: t("settings.viewCurrentReleaseNotes")
|
||||
}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<ExternalLink size={12} />
|
||||
更新日志
|
||||
{t("settings.releaseNotes")}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -498,25 +592,27 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
{isDownloading ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Download size={12} className="animate-pulse" />
|
||||
更新中...
|
||||
{t("settings.updating")}
|
||||
</span>
|
||||
) : isCheckingUpdate ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<RefreshCw size={12} className="animate-spin" />
|
||||
检查中...
|
||||
{t("settings.checking")}
|
||||
</span>
|
||||
) : hasUpdate ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Download size={12} />
|
||||
更新到 v{updateInfo?.availableVersion}
|
||||
{t("settings.updateTo", {
|
||||
version: updateInfo?.availableVersion ?? "",
|
||||
})}
|
||||
</span>
|
||||
) : showUpToDate ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Check size={12} />
|
||||
已是最新
|
||||
{t("settings.upToDate")}
|
||||
</span>
|
||||
) : (
|
||||
"检查更新"
|
||||
t("settings.checkForUpdates")
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -528,17 +624,17 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
{/* 底部按钮 */}
|
||||
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-800">
|
||||
<button
|
||||
onClick={onClose}
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
>
|
||||
取消
|
||||
{t("common.cancel")}
|
||||
</button>
|
||||
<button
|
||||
onClick={saveSettings}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 rounded-lg transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Save size={16} />
|
||||
保存
|
||||
{t("common.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ProviderCategory } from "../types";
|
||||
export interface CodexProviderPreset {
|
||||
name: string;
|
||||
websiteUrl: string;
|
||||
// 第三方供应商可提供单独的获取 API Key 链接
|
||||
apiKeyUrl?: string;
|
||||
auth: Record<string, any>; // 将写入 ~/.codex/auth.json
|
||||
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
||||
isOfficial?: boolean; // 标识是否为官方预设
|
||||
|
||||
@@ -30,8 +30,8 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "DeepSeek-V3.1-Terminus",
|
||||
ANTHROPIC_SMALL_FAST_MODEL: "DeepSeek-V3.1-Terminus",
|
||||
ANTHROPIC_MODEL: "DeepSeek-V3.2-Exp",
|
||||
ANTHROPIC_SMALL_FAST_MODEL: "DeepSeek-V3.2-Exp",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -83,8 +83,8 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.5",
|
||||
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.5",
|
||||
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.6",
|
||||
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.6",
|
||||
},
|
||||
},
|
||||
category: "aggregator",
|
||||
|
||||
59
src/i18n/index.ts
Normal file
59
src/i18n/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import en from "./locales/en.json";
|
||||
import zh from "./locales/zh.json";
|
||||
|
||||
const DEFAULT_LANGUAGE: "zh" | "en" = "zh";
|
||||
|
||||
const getInitialLanguage = (): "zh" | "en" => {
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const stored = window.localStorage.getItem("language");
|
||||
if (stored === "zh" || stored === "en") {
|
||||
return stored;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[i18n] Failed to read stored language preference", error);
|
||||
}
|
||||
}
|
||||
|
||||
const navigatorLang =
|
||||
typeof navigator !== "undefined"
|
||||
? navigator.language?.toLowerCase() ?? navigator.languages?.[0]?.toLowerCase()
|
||||
: undefined;
|
||||
|
||||
if (navigatorLang?.startsWith("zh")) {
|
||||
return "zh";
|
||||
}
|
||||
|
||||
if (navigatorLang?.startsWith("en")) {
|
||||
return "en";
|
||||
}
|
||||
|
||||
return DEFAULT_LANGUAGE;
|
||||
};
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
zh: {
|
||||
translation: zh,
|
||||
},
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: getInitialLanguage(), // 根据本地存储或系统语言选择默认语言
|
||||
fallbackLng: "en", // 如果缺少中文翻译则退回英文
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // React 已经默认转义
|
||||
},
|
||||
|
||||
// 开发模式下显示调试信息
|
||||
debug: false,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
120
src/i18n/locales/en.json
Normal file
120
src/i18n/locales/en.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "CC Switch",
|
||||
"description": "Claude Code & Codex Provider Switching Tool"
|
||||
},
|
||||
"common": {
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"close": "Close",
|
||||
"settings": "Settings",
|
||||
"about": "About",
|
||||
"version": "Version",
|
||||
"loading": "Loading...",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"header": {
|
||||
"viewOnGithub": "View on GitHub",
|
||||
"toggleDarkMode": "Switch to Dark Mode",
|
||||
"toggleLightMode": "Switch to Light Mode",
|
||||
"addProvider": "Add Provider",
|
||||
"switchToChinese": "Switch to Chinese",
|
||||
"switchToEnglish": "Switch to English"
|
||||
},
|
||||
"provider": {
|
||||
"noProviders": "No providers added yet",
|
||||
"noProvidersDescription": "Click the \"Add Provider\" button in the top right to configure your first API provider",
|
||||
"currentlyUsing": "Currently Using",
|
||||
"enable": "Enable",
|
||||
"inUse": "In Use",
|
||||
"editProvider": "Edit Provider",
|
||||
"deleteProvider": "Delete Provider",
|
||||
"addNewProvider": "Add New Provider",
|
||||
"configError": "Configuration Error",
|
||||
"notConfigured": "Not configured for official website",
|
||||
"applyToVSCode": "Apply to VS Code",
|
||||
"removeFromVSCode": "Remove from VS Code",
|
||||
"applyToClaudePlugin": "Apply to Claude plugin",
|
||||
"removeFromClaudePlugin": "Remove from Claude plugin"
|
||||
},
|
||||
"notifications": {
|
||||
"providerSaved": "Provider configuration saved",
|
||||
"providerDeleted": "Provider deleted successfully",
|
||||
"switchSuccess": "Switch successful! Please restart {{appName}} terminal to take effect",
|
||||
"switchFailed": "Switch failed, please check configuration",
|
||||
"autoImported": "Default provider created from existing configuration",
|
||||
"appliedToVSCode": "Applied to VS Code, restart Codex plugin to take effect",
|
||||
"removedFromVSCode": "Removed from VS Code, restart Codex plugin to take effect",
|
||||
"syncedToVSCode": "Synced to VS Code",
|
||||
"vscodeSettingsNotFound": "VS Code user settings file (settings.json) not found",
|
||||
"missingBaseUrl": "Current configuration missing base_url, cannot write to VS Code",
|
||||
"saveFailed": "Save failed: {{error}}",
|
||||
"saveFailedGeneric": "Save failed, please try again",
|
||||
"syncVSCodeFailed": "Sync to VS Code failed",
|
||||
"appliedToClaudePlugin": "Applied to Claude plugin",
|
||||
"removedFromClaudePlugin": "Removed from Claude plugin",
|
||||
"syncClaudePluginFailed": "Sync Claude plugin failed"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "Delete Provider",
|
||||
"deleteProviderMessage": "Are you sure you want to delete provider \"{{name}}\"? This action cannot be undone."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"general": "General",
|
||||
"language": "Language",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"windowBehavior": "Window Behavior",
|
||||
"minimizeToTray": "Minimize to tray on close",
|
||||
"minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.",
|
||||
"configFileLocation": "Configuration File Location",
|
||||
"openFolder": "Open Folder",
|
||||
"configDirectoryOverride": "Configuration Directory Override (Advanced)",
|
||||
"configDirectoryDescription": "When using Claude Code or Codex in environments like WSL, you can manually specify the configuration directory in WSL to keep provider data consistent with the main environment.",
|
||||
"claudeConfigDir": "Claude Code Configuration Directory",
|
||||
"codexConfigDir": "Codex Configuration Directory",
|
||||
"browsePlaceholderClaude": "e.g., /home/<your-username>/.claude",
|
||||
"browsePlaceholderCodex": "e.g., /home/<your-username>/.codex",
|
||||
"browseDirectory": "Browse Directory",
|
||||
"resetDefault": "Reset to default directory (takes effect after saving)",
|
||||
"checkForUpdates": "Check for Updates",
|
||||
"updateTo": "Update to v{{version}}",
|
||||
"updating": "Updating...",
|
||||
"checking": "Checking...",
|
||||
"upToDate": "Up to Date",
|
||||
"releaseNotes": "Release Notes",
|
||||
"viewReleaseNotes": "View release notes for this version",
|
||||
"viewCurrentReleaseNotes": "View current version release notes"
|
||||
},
|
||||
"apps": {
|
||||
"claude": "Claude Code",
|
||||
"codex": "Codex"
|
||||
},
|
||||
"console": {
|
||||
"providerSwitchReceived": "Received provider switch event:",
|
||||
"setupListenerFailed": "Failed to setup provider switch listener:",
|
||||
"updateProviderFailed": "Update provider failed:",
|
||||
"syncToVSCodeFailed": "Sync to VS Code failed:",
|
||||
"autoImportFailed": "Auto import default configuration failed:",
|
||||
"openLinkFailed": "Failed to open link:",
|
||||
"getVersionFailed": "Failed to get version info:",
|
||||
"loadSettingsFailed": "Failed to load settings:",
|
||||
"getConfigPathFailed": "Failed to get config path:",
|
||||
"getConfigDirFailed": "Failed to get config directory:",
|
||||
"detectPortableFailed": "Failed to detect portable mode:",
|
||||
"saveSettingsFailed": "Failed to save settings:",
|
||||
"updateFailed": "Update failed:",
|
||||
"checkUpdateFailed": "Check for updates failed:",
|
||||
"openConfigFolderFailed": "Failed to open config folder:",
|
||||
"selectConfigDirFailed": "Failed to select config directory:",
|
||||
"getDefaultConfigDirFailed": "Failed to get default config directory:",
|
||||
"openReleaseNotesFailed": "Failed to open release notes:"
|
||||
}
|
||||
}
|
||||
120
src/i18n/locales/zh.json
Normal file
120
src/i18n/locales/zh.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "CC Switch",
|
||||
"description": "Claude Code & Codex 供应商切换工具"
|
||||
},
|
||||
"common": {
|
||||
"add": "添加",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
"close": "关闭",
|
||||
"settings": "设置",
|
||||
"about": "关于",
|
||||
"version": "版本",
|
||||
"loading": "加载中...",
|
||||
"success": "成功",
|
||||
"error": "错误",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"header": {
|
||||
"viewOnGithub": "在 GitHub 上查看",
|
||||
"toggleDarkMode": "切换到暗色模式",
|
||||
"toggleLightMode": "切换到亮色模式",
|
||||
"addProvider": "添加供应商",
|
||||
"switchToChinese": "切换到中文",
|
||||
"switchToEnglish": "切换到英文"
|
||||
},
|
||||
"provider": {
|
||||
"noProviders": "还没有添加任何供应商",
|
||||
"noProvidersDescription": "点击右上角的\"添加供应商\"按钮开始配置您的第一个API供应商",
|
||||
"currentlyUsing": "当前使用",
|
||||
"enable": "启用",
|
||||
"inUse": "使用中",
|
||||
"editProvider": "编辑供应商",
|
||||
"deleteProvider": "删除供应商",
|
||||
"addNewProvider": "添加新供应商",
|
||||
"configError": "配置错误",
|
||||
"notConfigured": "未配置官网地址",
|
||||
"applyToVSCode": "应用到 VS Code",
|
||||
"removeFromVSCode": "从 VS Code 移除",
|
||||
"applyToClaudePlugin": "应用到 Claude 插件",
|
||||
"removeFromClaudePlugin": "从 Claude 插件移除"
|
||||
},
|
||||
"notifications": {
|
||||
"providerSaved": "供应商配置已保存",
|
||||
"providerDeleted": "供应商删除成功",
|
||||
"switchSuccess": "切换成功!请重启 {{appName}} 终端以生效",
|
||||
"switchFailed": "切换失败,请检查配置",
|
||||
"autoImported": "已从现有配置创建默认供应商",
|
||||
"appliedToVSCode": "已应用到 VS Code,重启 Codex 插件以生效",
|
||||
"removedFromVSCode": "已从 VS Code 移除,重启 Codex 插件以生效",
|
||||
"syncedToVSCode": "已同步到 VS Code",
|
||||
"vscodeSettingsNotFound": "未找到 VS Code 用户设置文件 (settings.json)",
|
||||
"missingBaseUrl": "当前配置缺少 base_url,无法写入 VS Code",
|
||||
"saveFailed": "保存失败:{{error}}",
|
||||
"saveFailedGeneric": "保存失败,请重试",
|
||||
"syncVSCodeFailed": "同步 VS Code 失败",
|
||||
"appliedToClaudePlugin": "已应用到 Claude 插件",
|
||||
"removedFromClaudePlugin": "已从 Claude 插件移除",
|
||||
"syncClaudePluginFailed": "同步 Claude 插件失败"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteProvider": "删除供应商",
|
||||
"deleteProviderMessage": "确定要删除供应商 \"{{name}}\" 吗?此操作无法撤销。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"general": "通用",
|
||||
"language": "界面语言",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"windowBehavior": "窗口行为",
|
||||
"minimizeToTray": "关闭时最小化到托盘",
|
||||
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
|
||||
"configFileLocation": "配置文件位置",
|
||||
"openFolder": "打开文件夹",
|
||||
"configDirectoryOverride": "配置目录覆盖(高级)",
|
||||
"configDirectoryDescription": "在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定 WSL 里的配置目录,供应商数据与主环境保持一致。",
|
||||
"claudeConfigDir": "Claude Code 配置目录",
|
||||
"codexConfigDir": "Codex 配置目录",
|
||||
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
|
||||
"browsePlaceholderCodex": "例如:/home/<你的用户名>/.codex",
|
||||
"browseDirectory": "浏览目录",
|
||||
"resetDefault": "恢复默认目录(需保存后生效)",
|
||||
"checkForUpdates": "检查更新",
|
||||
"updateTo": "更新到 v{{version}}",
|
||||
"updating": "更新中...",
|
||||
"checking": "检查中...",
|
||||
"upToDate": "已是最新",
|
||||
"releaseNotes": "更新日志",
|
||||
"viewReleaseNotes": "查看该版本更新日志",
|
||||
"viewCurrentReleaseNotes": "查看当前版本更新日志"
|
||||
},
|
||||
"apps": {
|
||||
"claude": "Claude Code",
|
||||
"codex": "Codex"
|
||||
},
|
||||
"console": {
|
||||
"providerSwitchReceived": "收到供应商切换事件:",
|
||||
"setupListenerFailed": "设置供应商切换监听器失败:",
|
||||
"updateProviderFailed": "更新供应商失败:",
|
||||
"syncToVSCodeFailed": "同步到VS Code失败:",
|
||||
"autoImportFailed": "自动导入默认配置失败:",
|
||||
"openLinkFailed": "打开链接失败:",
|
||||
"getVersionFailed": "获取版本信息失败:",
|
||||
"loadSettingsFailed": "加载设置失败:",
|
||||
"getConfigPathFailed": "获取配置路径失败:",
|
||||
"getConfigDirFailed": "获取配置目录失败:",
|
||||
"detectPortableFailed": "检测便携模式失败:",
|
||||
"saveSettingsFailed": "保存设置失败:",
|
||||
"updateFailed": "更新失败:",
|
||||
"checkUpdateFailed": "检查更新失败:",
|
||||
"openConfigFolderFailed": "打开配置文件夹失败:",
|
||||
"selectConfigDirFailed": "选择配置目录失败:",
|
||||
"getDefaultConfigDirFailed": "获取默认配置目录失败:",
|
||||
"openReleaseNotesFailed": "打开更新日志失败:"
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export const cardStyles = {
|
||||
|
||||
// 带悬浮效果的卡片
|
||||
interactive:
|
||||
"bg-white rounded-lg border border-gray-200 p-4 hover:border-gray-300 hover:shadow-sm dark:bg-gray-900 dark:border-gray-700 dark:hover:border-gray-600 transition-all duration-200",
|
||||
"bg-white rounded-lg border border-gray-200 p-4 hover:border-gray-300 hover:shadow-sm dark:bg-gray-900 dark:border-gray-700 dark:hover:border-gray-600 transition-[border-color,box-shadow] duration-200",
|
||||
|
||||
// 选中/激活态卡片
|
||||
selected:
|
||||
|
||||
@@ -306,6 +306,46 @@ export const tauriAPI = {
|
||||
throw new Error(`写入 VS Code 设置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:获取 ~/.claude/config.json 状态
|
||||
getClaudePluginStatus: async (): Promise<ConfigStatus> => {
|
||||
try {
|
||||
return await invoke<ConfigStatus>("get_claude_plugin_status");
|
||||
} catch (error) {
|
||||
console.error("获取 Claude 插件状态失败:", error);
|
||||
return { exists: false, path: "", error: String(error) };
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:读取配置内容
|
||||
readClaudePluginConfig: async (): Promise<string | null> => {
|
||||
try {
|
||||
return await invoke<string | null>("read_claude_plugin_config");
|
||||
} catch (error) {
|
||||
throw new Error(`读取 Claude 插件配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:应用或移除固定配置
|
||||
applyClaudePluginConfig: async (options: {
|
||||
official: boolean;
|
||||
}): Promise<boolean> => {
|
||||
const { official } = options;
|
||||
try {
|
||||
return await invoke<boolean>("apply_claude_plugin_config", { official });
|
||||
} catch (error) {
|
||||
throw new Error(`写入 Claude 插件配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Claude 插件:检测是否已应用目标配置
|
||||
isClaudePluginApplied: async (): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke<boolean>("is_claude_plugin_applied");
|
||||
} catch (error) {
|
||||
throw new Error(`检测 Claude 插件配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 创建全局 API 对象,兼容现有代码
|
||||
|
||||
@@ -5,6 +5,8 @@ import { UpdateProvider } from "./contexts/UpdateContext";
|
||||
import "./index.css";
|
||||
// 导入 Tauri API(自动绑定到 window.api)
|
||||
import "./lib/tauri-api";
|
||||
// 导入国际化配置
|
||||
import "./i18n";
|
||||
|
||||
// 根据平台添加 body class,便于平台特定样式
|
||||
try {
|
||||
@@ -23,5 +25,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<UpdateProvider>
|
||||
<App />
|
||||
</UpdateProvider>
|
||||
</React.StrictMode>,
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -30,4 +30,6 @@ export interface Settings {
|
||||
claudeConfigDir?: string;
|
||||
// 覆盖 Codex 配置目录(可选)
|
||||
codexConfigDir?: string;
|
||||
// 首选语言(可选,默认中文)
|
||||
language?: "en" | "zh";
|
||||
}
|
||||
|
||||
7
src/vite-env.d.ts
vendored
7
src/vite-env.d.ts
vendored
@@ -46,6 +46,13 @@ declare global {
|
||||
getVSCodeSettingsStatus: () => Promise<ConfigStatus>;
|
||||
readVSCodeSettings: () => Promise<string>;
|
||||
writeVSCodeSettings: (content: string) => Promise<boolean>;
|
||||
// Claude 插件配置能力
|
||||
getClaudePluginStatus: () => Promise<ConfigStatus>;
|
||||
readClaudePluginConfig: () => Promise<string | null>;
|
||||
applyClaudePluginConfig: (options: {
|
||||
official: boolean;
|
||||
}) => Promise<boolean>;
|
||||
isClaudePluginApplied: () => Promise<boolean>;
|
||||
};
|
||||
platform: {
|
||||
isMac: boolean;
|
||||
|
||||
Reference in New Issue
Block a user