Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea81c3d839 | ||
|
|
aa05a8475f |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -5,25 +5,6 @@ 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/),
|
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).
|
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
|
## [3.3.0] - 2025-09-22
|
||||||
|
|
||||||
### ✨ Features
|
### ✨ Features
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -1,13 +1,11 @@
|
|||||||
# Claude Code & Codex 供应商切换器
|
# 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://github.com/farion1231/cc-switch/releases)
|
||||||
[](https://tauri.app/)
|
[](https://tauri.app/)
|
||||||
|
|
||||||
一个用于管理和切换 Claude Code 与 Codex 不同供应商配置的桌面应用。
|
一个用于管理和切换 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.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。
|
||||||
|
|
||||||
> v3.2.0 :全新 UI、macOS系统托盘、内置更新器、原子写入与回滚、改进暗色样式、单一事实源(SSOT)与一次性迁移/归档。
|
> v3.2.0 :全新 UI、macOS系统托盘、内置更新器、原子写入与回滚、改进暗色样式、单一事实源(SSOT)与一次性迁移/归档。
|
||||||
@@ -16,14 +14,14 @@
|
|||||||
|
|
||||||
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
|
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
|
||||||
|
|
||||||
## 功能特性(v3.4.0)
|
## 功能特性(v3.3.0)
|
||||||
|
|
||||||
- **国际化与语言切换**:内置 i18next,默认显示中文,可在设置中快速切换到英文,界面文文案自动实时刷新。
|
- **VS Code Codex 插件一键配置**:供应商卡片支持「应用到 VS Code / 从 VS Code 移除」,默认开启自动同步,并可跨 Code / Insiders / VSCodium 写入 `settings.json`
|
||||||
- **Claude 插件同步**:在 VS Code 同步按钮旁新增 Claude 插件同步选项,与 Codex 同步互不冲突,切换供应商后立即应用。
|
- **通用配置片段**:Claude 与 Codex 共用 JSON/TOML 片段,提供编辑器 lint、内容校验、统一错误提示与本地持久化
|
||||||
- **供应商预设扩展**:新增 DeepSeek--V3.2-Exp、Qwen3-Max、GLM-4.6 等最新模型。
|
- **Codex 配置向导**:新增显示名称、专用 API Key URL、HTML5 校验与预设模板,方便快速配置第三方服务
|
||||||
- **系统托盘与窗口行为**:窗口关闭可最小化到托盘,macOS 支持托盘模式下隐藏/显示 Dock,托盘切换时同步 Claude/Codex/插件状态。
|
- **系统托盘与快捷操作**:窗口隐藏时仍可通过托盘切换供应商,并在自动同步开启时触发 VS Code 写入
|
||||||
- **单实例**:保证同一时间仅运行一个实例,避免多开冲突。
|
- **平台适配**:新增 Windows WSL 环境支持、Linux 自动禁用模态背景模糊解决白屏问题、macOS Dock 点击即可恢复窗口
|
||||||
- **UI 与安装体验优化**:设置面板改为可滚动布局并加入保存图标,按钮宽度与状态一致性加强,Windows MSI 安装默认写入 per-user LocalAppData 并改进组件跟踪,Windows 便携版现在指向最新 release 页面,不再自动更为为安装版。
|
- **UI优化**:多处 UI 和使用体验优化
|
||||||
|
|
||||||
## 界面预览
|
## 界面预览
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cc-switch",
|
"name": "cc-switch",
|
||||||
"version": "3.4.0",
|
"version": "3.3.1",
|
||||||
"description": "Claude Code & Codex 供应商切换工具",
|
"description": "Claude Code & Codex 供应商切换工具",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm tauri dev",
|
"dev": "pnpm tauri dev",
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -563,7 +563,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.4.0"
|
version = "3.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.4.0"
|
version = "3.3.1"
|
||||||
description = "Claude Code & Codex 供应商配置管理工具"
|
description = "Claude Code & Codex 供应商配置管理工具"
|
||||||
authors = ["Jason Young"]
|
authors = ["Jason Young"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
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,7 +6,6 @@ use tauri_plugin_dialog::DialogExt;
|
|||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
use crate::app_config::AppType;
|
use crate::app_config::AppType;
|
||||||
use crate::claude_plugin;
|
|
||||||
use crate::codex_config;
|
use crate::codex_config;
|
||||||
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
||||||
use crate::provider::Provider;
|
use crate::provider::Provider;
|
||||||
@@ -731,37 +730,3 @@ pub async fn write_vscode_settings(content: String) -> Result<bool, String> {
|
|||||||
Err("未找到 VS Code 用户设置文件".to_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,5 +1,4 @@
|
|||||||
mod app_config;
|
mod app_config;
|
||||||
mod claude_plugin;
|
|
||||||
mod codex_config;
|
mod codex_config;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -10,12 +9,12 @@ mod store;
|
|||||||
mod vscode;
|
mod vscode;
|
||||||
|
|
||||||
use store::AppState;
|
use store::AppState;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use tauri::RunEvent;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem},
|
menu::{CheckMenuItem, Menu, MenuBuilder, MenuItem},
|
||||||
tray::{TrayIconBuilder, TrayIconEvent},
|
tray::{TrayIconBuilder, TrayIconEvent},
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use tauri::{ActivationPolicy, RunEvent};
|
|
||||||
use tauri::{Emitter, Manager};
|
use tauri::{Emitter, Manager};
|
||||||
|
|
||||||
/// 创建动态托盘菜单
|
/// 创建动态托盘菜单
|
||||||
@@ -117,23 +116,6 @@ fn create_tray_menu(
|
|||||||
.map_err(|e| format!("构建菜单失败: {}", e))
|
.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) {
|
fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
|
||||||
log::info!("处理托盘菜单事件: {}", event_id);
|
log::info!("处理托盘菜单事件: {}", event_id);
|
||||||
@@ -148,10 +130,6 @@ fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
|
|||||||
let _ = window.unminimize();
|
let _ = window.unminimize();
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
apply_tray_policy(app, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
@@ -289,10 +267,6 @@ pub fn run() {
|
|||||||
{
|
{
|
||||||
let _ = window.set_skip_taskbar(true);
|
let _ = window.set_skip_taskbar(true);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
apply_tray_policy(&window.app_handle(), false);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
window.app_handle().exit(0);
|
window.app_handle().exit(0);
|
||||||
}
|
}
|
||||||
@@ -419,10 +393,6 @@ pub fn run() {
|
|||||||
commands::get_vscode_settings_status,
|
commands::get_vscode_settings_status,
|
||||||
commands::read_vscode_settings,
|
commands::read_vscode_settings,
|
||||||
commands::write_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,
|
update_tray_menu,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -443,7 +413,6 @@ pub fn run() {
|
|||||||
let _ = window.unminimize();
|
let _ = window.unminimize();
|
||||||
let _ = window.show();
|
let _ = window.show();
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
apply_tray_policy(app_handle, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ pub struct AppSettings {
|
|||||||
pub claude_config_dir: Option<String>,
|
pub claude_config_dir: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub codex_config_dir: Option<String>,
|
pub codex_config_dir: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub language: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_show_in_tray() -> bool {
|
fn default_show_in_tray() -> bool {
|
||||||
@@ -34,7 +32,6 @@ impl Default for AppSettings {
|
|||||||
minimize_to_tray_on_close: true,
|
minimize_to_tray_on_close: true,
|
||||||
claude_config_dir: None,
|
claude_config_dir: None,
|
||||||
codex_config_dir: None,
|
codex_config_dir: None,
|
||||||
language: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,13 +55,6 @@ impl AppSettings {
|
|||||||
.map(|s| s.trim())
|
.map(|s| s.trim())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.map(|s| s.to_string());
|
.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 {
|
pub fn load() -> Self {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "CC Switch",
|
"productName": "CC Switch",
|
||||||
"version": "3.4.0",
|
"version": "3.3.1",
|
||||||
"identifier": "com.ccswitch.desktop",
|
"identifier": "com.ccswitch.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
36
src/App.tsx
36
src/App.tsx
@@ -9,6 +9,7 @@ import { ConfirmDialog } from "./components/ConfirmDialog";
|
|||||||
import { AppSwitcher } from "./components/AppSwitcher";
|
import { AppSwitcher } from "./components/AppSwitcher";
|
||||||
import SettingsModal from "./components/SettingsModal";
|
import SettingsModal from "./components/SettingsModal";
|
||||||
import { UpdateBadge } from "./components/UpdateBadge";
|
import { UpdateBadge } from "./components/UpdateBadge";
|
||||||
|
import LanguageSwitcher from "./components/LanguageSwitcher";
|
||||||
import { Plus, Settings, Moon, Sun } from "lucide-react";
|
import { Plus, Settings, Moon, Sun } from "lucide-react";
|
||||||
import { buttonStyles } from "./lib/styles";
|
import { buttonStyles } from "./lib/styles";
|
||||||
import { useDarkMode } from "./hooks/useDarkMode";
|
import { useDarkMode } from "./hooks/useDarkMode";
|
||||||
@@ -102,10 +103,6 @@ function App() {
|
|||||||
if (data.appType === "codex" && isAutoSyncEnabled) {
|
if (data.appType === "codex" && isAutoSyncEnabled) {
|
||||||
await syncCodexToVSCode(data.providerId, true);
|
await syncCodexToVSCode(data.providerId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.appType === "claude") {
|
|
||||||
await syncClaudePlugin(data.providerId, true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(t("console.setupListenerFailed"), error);
|
console.error(t("console.setupListenerFailed"), error);
|
||||||
@@ -244,32 +241,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同步 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 handleSwitchProvider = async (id: string) => {
|
||||||
const success = await window.api.switchProvider(id, activeApp);
|
const success = await window.api.switchProvider(id, activeApp);
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -288,10 +259,6 @@ function App() {
|
|||||||
if (activeApp === "codex" && isAutoSyncEnabled) {
|
if (activeApp === "codex" && isAutoSyncEnabled) {
|
||||||
await syncCodexToVSCode(id, true); // silent模式,不显示通知
|
await syncCodexToVSCode(id, true); // silent模式,不显示通知
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeApp === "claude") {
|
|
||||||
await syncClaudePlugin(id, true);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showNotification(t("notifications.switchFailed"), "error");
|
showNotification(t("notifications.switchFailed"), "error");
|
||||||
}
|
}
|
||||||
@@ -341,6 +308,7 @@ function App() {
|
|||||||
>
|
>
|
||||||
{isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
|
{isDarkMode ? <Sun size={18} /> : <Moon size={18} />}
|
||||||
</button>
|
</button>
|
||||||
|
<LanguageSwitcher />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSettingsOpen(true)}
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
|||||||
31
src/components/LanguageSwitcher.tsx
Normal file
31
src/components/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Globe } from "lucide-react";
|
||||||
|
import { buttonStyles } from "../lib/styles";
|
||||||
|
|
||||||
|
const LanguageSwitcher: React.FC = () => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const toggleLanguage = () => {
|
||||||
|
const newLang = i18n.language === "en" ? "zh" : "en";
|
||||||
|
i18n.changeLanguage(newLang);
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleKey =
|
||||||
|
i18n.language === "en"
|
||||||
|
? "header.switchToChinese"
|
||||||
|
: "header.switchToEnglish";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleLanguage}
|
||||||
|
className={buttonStyles.icon}
|
||||||
|
title={t(titleKey)}
|
||||||
|
aria-label={t(titleKey)}
|
||||||
|
>
|
||||||
|
<Globe size={18} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageSwitcher;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Provider } from "../types";
|
import { Provider } from "../types";
|
||||||
import { Play, Edit3, Trash2, CheckCircle2, Users, Check } from "lucide-react";
|
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
||||||
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
||||||
import { AppType } from "../lib/tauri-api";
|
import { AppType } from "../lib/tauri-api";
|
||||||
import {
|
import {
|
||||||
@@ -70,7 +70,6 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
// VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否"已应用"变化
|
// VS Code 按钮:仅在 Codex + 当前供应商显示;按钮文案根据是否"已应用"变化
|
||||||
const [vscodeAppliedFor, setVscodeAppliedFor] = useState<string | null>(null);
|
const [vscodeAppliedFor, setVscodeAppliedFor] = useState<string | null>(null);
|
||||||
const { enableAutoSync, disableAutoSync } = useVSCodeAutoSync();
|
const { enableAutoSync, disableAutoSync } = useVSCodeAutoSync();
|
||||||
const [claudeApplied, setClaudeApplied] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// 当当前供应商或 appType 变化时,尝试读取 VS Code settings 并检测状态
|
// 当当前供应商或 appType 变化时,尝试读取 VS Code settings 并检测状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -105,24 +104,6 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
check();
|
check();
|
||||||
}, [appType, currentProviderId, providers]);
|
}, [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) => {
|
const handleApplyToVSCode = async (provider: Provider) => {
|
||||||
try {
|
try {
|
||||||
const status = await window.api.getVSCodeSettingsStatus();
|
const status = await window.api.getVSCodeSettingsStatus();
|
||||||
@@ -200,36 +181,6 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 对供应商列表进行排序
|
// 对供应商列表进行排序
|
||||||
const sortedProviders = Object.values(providers).sort((a, b) => {
|
const sortedProviders = Object.values(providers).sort((a, b) => {
|
||||||
// 按添加时间排序
|
// 按添加时间排序
|
||||||
@@ -320,10 +271,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<div className="flex items-center gap-2 ml-4">
|
||||||
{/* 同步按钮占位容器 - 只在对应模式下渲染,避免布局跳动 */}
|
{appType === "codex" &&
|
||||||
{appType === "codex" ? (
|
provider.category !== "official" && (
|
||||||
<div className="w-[130px]">
|
|
||||||
{provider.category !== "official" && isCurrent && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
vscodeAppliedFor === provider.id
|
vscodeAppliedFor === provider.id
|
||||||
@@ -331,7 +280,8 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
: handleApplyToVSCode(provider)
|
: handleApplyToVSCode(provider)
|
||||||
}
|
}
|
||||||
className={cn(
|
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",
|
"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
|
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-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"
|
: "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"
|
||||||
@@ -347,48 +297,17 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
: t("provider.applyToVSCode")}
|
: t("provider.applyToVSCode")}
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => onSwitch(provider.id)}
|
onClick={() => onSwitch(provider.id)}
|
||||||
disabled={isCurrent}
|
disabled={isCurrent}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
"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",
|
||||||
isCurrent
|
isCurrent
|
||||||
? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed"
|
? "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"
|
: "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCurrent ? <Check size={14} /> : <Play size={14} />}
|
{!isCurrent && <Play size={14} />}
|
||||||
{isCurrent ? t("provider.inUse") : t("provider.enable")}
|
{isCurrent ? t("provider.inUse") : t("provider.enable")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -25,33 +25,13 @@ interface SettingsModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsModal({ onClose }: SettingsModalProps) {
|
export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = 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>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
showInTray: true,
|
showInTray: true,
|
||||||
minimizeToTrayOnClose: true,
|
minimizeToTrayOnClose: true,
|
||||||
claudeConfigDir: undefined,
|
claudeConfigDir: undefined,
|
||||||
codexConfigDir: undefined,
|
codexConfigDir: undefined,
|
||||||
language: persistedLanguage,
|
|
||||||
});
|
});
|
||||||
const [initialLanguage, setInitialLanguage] = useState<"zh" | "en">(
|
|
||||||
persistedLanguage,
|
|
||||||
);
|
|
||||||
const [configPath, setConfigPath] = useState<string>("");
|
const [configPath, setConfigPath] = useState<string>("");
|
||||||
const [version, setVersion] = useState<string>("");
|
const [version, setVersion] = useState<string>("");
|
||||||
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
||||||
@@ -93,12 +73,6 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
(loadedSettings as any)?.minimizeToTrayOnClose ??
|
(loadedSettings as any)?.minimizeToTrayOnClose ??
|
||||||
(loadedSettings as any)?.minimize_to_tray_on_close ??
|
(loadedSettings as any)?.minimize_to_tray_on_close ??
|
||||||
true;
|
true;
|
||||||
const storedLanguage = normalizeLanguage(
|
|
||||||
typeof (loadedSettings as any)?.language === "string"
|
|
||||||
? (loadedSettings as any).language
|
|
||||||
: persistedLanguage,
|
|
||||||
);
|
|
||||||
|
|
||||||
setSettings({
|
setSettings({
|
||||||
showInTray,
|
showInTray,
|
||||||
minimizeToTrayOnClose,
|
minimizeToTrayOnClose,
|
||||||
@@ -110,12 +84,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
typeof (loadedSettings as any)?.codexConfigDir === "string"
|
typeof (loadedSettings as any)?.codexConfigDir === "string"
|
||||||
? (loadedSettings as any).codexConfigDir
|
? (loadedSettings as any).codexConfigDir
|
||||||
: undefined,
|
: undefined,
|
||||||
language: storedLanguage,
|
|
||||||
});
|
});
|
||||||
setInitialLanguage(storedLanguage);
|
|
||||||
if (i18n.language !== storedLanguage) {
|
|
||||||
void i18n.changeLanguage(storedLanguage);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(t("console.loadSettingsFailed"), error);
|
console.error(t("console.loadSettingsFailed"), error);
|
||||||
}
|
}
|
||||||
@@ -156,7 +125,6 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const selectedLanguage = settings.language === "en" ? "en" : "zh";
|
|
||||||
const payload: Settings = {
|
const payload: Settings = {
|
||||||
...settings,
|
...settings,
|
||||||
claudeConfigDir:
|
claudeConfigDir:
|
||||||
@@ -167,42 +135,15 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
settings.codexConfigDir && settings.codexConfigDir.trim() !== ""
|
settings.codexConfigDir && settings.codexConfigDir.trim() !== ""
|
||||||
? settings.codexConfigDir.trim()
|
? settings.codexConfigDir.trim()
|
||||||
: undefined,
|
: undefined,
|
||||||
language: selectedLanguage,
|
|
||||||
};
|
};
|
||||||
await window.api.saveSettings(payload);
|
await window.api.saveSettings(payload);
|
||||||
setSettings(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();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(t("console.saveSettingsFailed"), 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 () => {
|
const handleCheckUpdate = async () => {
|
||||||
if (hasUpdate && updateHandle) {
|
if (hasUpdate && updateHandle) {
|
||||||
if (isPortable) {
|
if (isPortable) {
|
||||||
@@ -350,7 +291,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
if (e.target === e.currentTarget) handleCancel();
|
if (e.target === e.currentTarget) onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -365,7 +306,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
{t("settings.title")}
|
{t("settings.title")}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={handleCancel}
|
onClick={onClose}
|
||||||
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
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" />
|
<X size={20} className="text-gray-500 dark:text-gray-400" />
|
||||||
@@ -374,37 +315,6 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
|||||||
|
|
||||||
{/* 设置内容 */}
|
{/* 设置内容 */}
|
||||||
<div className="px-6 py-4 space-y-6 overflow-y-auto flex-1">
|
<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>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||||
@@ -624,7 +534,7 @@ 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">
|
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-800">
|
||||||
<button
|
<button
|
||||||
onClick={handleCancel}
|
onClick={onClose}
|
||||||
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"
|
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")}
|
{t("common.cancel")}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { ProviderCategory } from "../types";
|
|||||||
export interface CodexProviderPreset {
|
export interface CodexProviderPreset {
|
||||||
name: string;
|
name: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
// 第三方供应商可提供单独的获取 API Key 链接
|
|
||||||
apiKeyUrl?: string;
|
|
||||||
auth: Record<string, any>; // 将写入 ~/.codex/auth.json
|
auth: Record<string, any>; // 将写入 ~/.codex/auth.json
|
||||||
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "DeepSeek-V3.2-Exp",
|
ANTHROPIC_MODEL: "DeepSeek-V3.1-Terminus",
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "DeepSeek-V3.2-Exp",
|
ANTHROPIC_SMALL_FAST_MODEL: "DeepSeek-V3.1-Terminus",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -83,8 +83,8 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.6",
|
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.5",
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.6",
|
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "aggregator",
|
category: "aggregator",
|
||||||
|
|||||||
@@ -4,36 +4,6 @@ import { initReactI18next } from "react-i18next";
|
|||||||
import en from "./locales/en.json";
|
import en from "./locales/en.json";
|
||||||
import zh from "./locales/zh.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 = {
|
const resources = {
|
||||||
en: {
|
en: {
|
||||||
translation: en,
|
translation: en,
|
||||||
@@ -45,8 +15,8 @@ const resources = {
|
|||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
resources,
|
resources,
|
||||||
lng: getInitialLanguage(), // 根据本地存储或系统语言选择默认语言
|
lng: "en", // 默认语言设置为英文
|
||||||
fallbackLng: "en", // 如果缺少中文翻译则退回英文
|
fallbackLng: "en", // 回退语言也设置为英文
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // React 已经默认转义
|
escapeValue: false, // React 已经默认转义
|
||||||
|
|||||||
@@ -39,9 +39,7 @@
|
|||||||
"configError": "Configuration Error",
|
"configError": "Configuration Error",
|
||||||
"notConfigured": "Not configured for official website",
|
"notConfigured": "Not configured for official website",
|
||||||
"applyToVSCode": "Apply to VS Code",
|
"applyToVSCode": "Apply to VS Code",
|
||||||
"removeFromVSCode": "Remove from VS Code",
|
"removeFromVSCode": "Remove from VS Code"
|
||||||
"applyToClaudePlugin": "Apply to Claude plugin",
|
|
||||||
"removeFromClaudePlugin": "Remove from Claude plugin"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"providerSaved": "Provider configuration saved",
|
"providerSaved": "Provider configuration saved",
|
||||||
@@ -56,10 +54,7 @@
|
|||||||
"missingBaseUrl": "Current configuration missing base_url, cannot write to VS Code",
|
"missingBaseUrl": "Current configuration missing base_url, cannot write to VS Code",
|
||||||
"saveFailed": "Save failed: {{error}}",
|
"saveFailed": "Save failed: {{error}}",
|
||||||
"saveFailedGeneric": "Save failed, please try again",
|
"saveFailedGeneric": "Save failed, please try again",
|
||||||
"syncVSCodeFailed": "Sync to VS Code failed",
|
"syncVSCodeFailed": "Sync to VS Code failed"
|
||||||
"appliedToClaudePlugin": "Applied to Claude plugin",
|
|
||||||
"removedFromClaudePlugin": "Removed from Claude plugin",
|
|
||||||
"syncClaudePluginFailed": "Sync Claude plugin failed"
|
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"deleteProvider": "Delete Provider",
|
"deleteProvider": "Delete Provider",
|
||||||
@@ -67,10 +62,6 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"general": "General",
|
|
||||||
"language": "Language",
|
|
||||||
"languageOptionChinese": "中文",
|
|
||||||
"languageOptionEnglish": "English",
|
|
||||||
"windowBehavior": "Window Behavior",
|
"windowBehavior": "Window Behavior",
|
||||||
"minimizeToTray": "Minimize to tray on close",
|
"minimizeToTray": "Minimize to tray on close",
|
||||||
"minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.",
|
"minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.",
|
||||||
|
|||||||
@@ -39,9 +39,7 @@
|
|||||||
"configError": "配置错误",
|
"configError": "配置错误",
|
||||||
"notConfigured": "未配置官网地址",
|
"notConfigured": "未配置官网地址",
|
||||||
"applyToVSCode": "应用到 VS Code",
|
"applyToVSCode": "应用到 VS Code",
|
||||||
"removeFromVSCode": "从 VS Code 移除",
|
"removeFromVSCode": "从 VS Code 移除"
|
||||||
"applyToClaudePlugin": "应用到 Claude 插件",
|
|
||||||
"removeFromClaudePlugin": "从 Claude 插件移除"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"providerSaved": "供应商配置已保存",
|
"providerSaved": "供应商配置已保存",
|
||||||
@@ -56,10 +54,7 @@
|
|||||||
"missingBaseUrl": "当前配置缺少 base_url,无法写入 VS Code",
|
"missingBaseUrl": "当前配置缺少 base_url,无法写入 VS Code",
|
||||||
"saveFailed": "保存失败:{{error}}",
|
"saveFailed": "保存失败:{{error}}",
|
||||||
"saveFailedGeneric": "保存失败,请重试",
|
"saveFailedGeneric": "保存失败,请重试",
|
||||||
"syncVSCodeFailed": "同步 VS Code 失败",
|
"syncVSCodeFailed": "同步 VS Code 失败"
|
||||||
"appliedToClaudePlugin": "已应用到 Claude 插件",
|
|
||||||
"removedFromClaudePlugin": "已从 Claude 插件移除",
|
|
||||||
"syncClaudePluginFailed": "同步 Claude 插件失败"
|
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"deleteProvider": "删除供应商",
|
"deleteProvider": "删除供应商",
|
||||||
@@ -67,10 +62,6 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "设置",
|
"title": "设置",
|
||||||
"general": "通用",
|
|
||||||
"language": "界面语言",
|
|
||||||
"languageOptionChinese": "中文",
|
|
||||||
"languageOptionEnglish": "English",
|
|
||||||
"windowBehavior": "窗口行为",
|
"windowBehavior": "窗口行为",
|
||||||
"minimizeToTray": "关闭时最小化到托盘",
|
"minimizeToTray": "关闭时最小化到托盘",
|
||||||
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
|
"minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const cardStyles = {
|
|||||||
|
|
||||||
// 带悬浮效果的卡片
|
// 带悬浮效果的卡片
|
||||||
interactive:
|
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-[border-color,box-shadow] 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-all duration-200",
|
||||||
|
|
||||||
// 选中/激活态卡片
|
// 选中/激活态卡片
|
||||||
selected:
|
selected:
|
||||||
|
|||||||
@@ -306,46 +306,6 @@ export const tauriAPI = {
|
|||||||
throw new Error(`写入 VS Code 设置失败: ${String(error)}`);
|
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 对象,兼容现有代码
|
// 创建全局 API 对象,兼容现有代码
|
||||||
|
|||||||
@@ -30,6 +30,4 @@ export interface Settings {
|
|||||||
claudeConfigDir?: string;
|
claudeConfigDir?: string;
|
||||||
// 覆盖 Codex 配置目录(可选)
|
// 覆盖 Codex 配置目录(可选)
|
||||||
codexConfigDir?: string;
|
codexConfigDir?: string;
|
||||||
// 首选语言(可选,默认中文)
|
|
||||||
language?: "en" | "zh";
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/vite-env.d.ts
vendored
7
src/vite-env.d.ts
vendored
@@ -46,13 +46,6 @@ declare global {
|
|||||||
getVSCodeSettingsStatus: () => Promise<ConfigStatus>;
|
getVSCodeSettingsStatus: () => Promise<ConfigStatus>;
|
||||||
readVSCodeSettings: () => Promise<string>;
|
readVSCodeSettings: () => Promise<string>;
|
||||||
writeVSCodeSettings: (content: string) => Promise<boolean>;
|
writeVSCodeSettings: (content: string) => Promise<boolean>;
|
||||||
// Claude 插件配置能力
|
|
||||||
getClaudePluginStatus: () => Promise<ConfigStatus>;
|
|
||||||
readClaudePluginConfig: () => Promise<string | null>;
|
|
||||||
applyClaudePluginConfig: (options: {
|
|
||||||
official: boolean;
|
|
||||||
}) => Promise<boolean>;
|
|
||||||
isClaudePluginApplied: () => Promise<boolean>;
|
|
||||||
};
|
};
|
||||||
platform: {
|
platform: {
|
||||||
isMac: boolean;
|
isMac: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user