chore: format code and fix bundle identifier for v3.0.0 release

- Format all TypeScript/React code with Prettier
- Format all Rust code with cargo fmt
- Fix bundle identifier from .app to .desktop to avoid macOS conflicts
- Prepare codebase for v3.0.0 Tauri release
This commit is contained in:
Jason
2025-08-27 11:00:53 +08:00
parent 5e2e80b00d
commit 642e7a3817
23 changed files with 359 additions and 321 deletions

View File

@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build()
}

View File

@@ -2,46 +2,48 @@ use std::collections::HashMap;
use tauri::State;
use tauri_plugin_opener::OpenerExt;
use crate::config::{
import_current_config_as_default, get_claude_settings_path,
ConfigStatus,
};
use crate::config::{ConfigStatus, get_claude_settings_path, import_current_config_as_default};
use crate::provider::Provider;
use crate::store::AppState;
/// 获取所有供应商
#[tauri::command]
pub async fn get_providers(state: State<'_, AppState>) -> Result<HashMap<String, Provider>, String> {
let manager = state.provider_manager.lock()
pub async fn get_providers(
state: State<'_, AppState>,
) -> Result<HashMap<String, Provider>, String> {
let manager = state
.provider_manager
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
Ok(manager.get_all_providers().clone())
}
/// 获取当前供应商ID
#[tauri::command]
pub async fn get_current_provider(state: State<'_, AppState>) -> Result<String, String> {
let manager = state.provider_manager.lock()
let manager = state
.provider_manager
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
Ok(manager.current.clone())
}
/// 添加供应商
#[tauri::command]
pub async fn add_provider(
state: State<'_, AppState>,
provider: Provider,
) -> Result<bool, String> {
let mut manager = state.provider_manager.lock()
pub async fn add_provider(state: State<'_, AppState>, provider: Provider) -> Result<bool, String> {
let mut manager = state
.provider_manager
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
manager.add_provider(provider)?;
// 保存配置
drop(manager); // 释放锁
state.save()?;
Ok(true)
}
@@ -51,59 +53,57 @@ pub async fn update_provider(
state: State<'_, AppState>,
provider: Provider,
) -> Result<bool, String> {
let mut manager = state.provider_manager.lock()
let mut manager = state
.provider_manager
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
manager.update_provider(provider)?;
// 保存配置
drop(manager); // 释放锁
state.save()?;
Ok(true)
}
/// 删除供应商
#[tauri::command]
pub async fn delete_provider(
state: State<'_, AppState>,
id: String,
) -> Result<bool, String> {
let mut manager = state.provider_manager.lock()
pub async fn delete_provider(state: State<'_, AppState>, id: String) -> Result<bool, String> {
let mut manager = state
.provider_manager
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
manager.delete_provider(&id)?;
// 保存配置
drop(manager); // 释放锁
state.save()?;
Ok(true)
}
/// 切换供应商
#[tauri::command]
pub async fn switch_provider(
state: State<'_, AppState>,
id: String,
) -> Result<bool, String> {
let mut manager = state.provider_manager.lock()
pub async fn switch_provider(state: State<'_, AppState>, id: String) -> Result<bool, String> {
let mut manager = state
.provider_manager
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
manager.switch_provider(&id)?;
// 保存配置
drop(manager); // 释放锁
state.save()?;
Ok(true)
}
/// 导入当前配置为默认供应商
#[tauri::command]
pub async fn import_default_config(
state: State<'_, AppState>,
) -> Result<bool, String> {
pub async fn import_default_config(state: State<'_, AppState>) -> Result<bool, String> {
// 若已存在 default 供应商,则直接返回,避免重复导入
{
let manager = state
@@ -117,7 +117,7 @@ pub async fn import_default_config(
// 导入配置
let settings_config = import_current_config_as_default()?;
// 创建默认供应商
let provider = Provider::with_id(
"default".to_string(),
@@ -125,22 +125,24 @@ pub async fn import_default_config(
settings_config,
None,
);
// 添加到管理器
let mut manager = state.provider_manager.lock()
let mut manager = state
.provider_manager
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
manager.add_provider(provider)?;
// 如果没有当前供应商,设置为 default
if manager.current.is_empty() {
manager.current = "default".to_string();
}
// 保存配置
drop(manager); // 释放锁
state.save()?;
Ok(true)
}
@@ -160,18 +162,17 @@ pub async fn get_claude_code_config_path() -> Result<String, String> {
#[tauri::command]
pub async fn open_config_folder(app: tauri::AppHandle) -> Result<bool, String> {
let config_dir = crate::config::get_claude_config_dir();
// 确保目录存在
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir)
.map_err(|e| format!("创建目录失败: {}", e))?;
std::fs::create_dir_all(&config_dir).map_err(|e| format!("创建目录失败: {}", e))?;
}
// 使用 opener 插件打开文件夹
app.opener()
.open_path(config_dir.to_string_lossy().to_string(), None::<String>)
.map_err(|e| format!("打开文件夹失败: {}", e))?;
Ok(true)
}
@@ -184,11 +185,11 @@ pub async fn open_external(app: tauri::AppHandle, url: String) -> Result<bool, S
} else {
format!("https://{}", url)
};
// 使用 opener 插件打开链接
app.opener()
.open_url(&url, None::<String>)
.map_err(|e| format!("打开链接失败: {}", e))?;
Ok(true)
}

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
/// 获取 Claude Code 配置目录路径
pub fn get_claude_config_dir() -> PathBuf {
@@ -49,7 +49,7 @@ pub fn get_provider_config_path(provider_id: &str, provider_name: Option<&str>)
let base_name = provider_name
.map(|name| sanitize_provider_name(name))
.unwrap_or_else(|| sanitize_provider_name(provider_id));
get_claude_config_dir().join(format!("settings-{}.json", base_name))
}
@@ -58,41 +58,35 @@ pub fn read_json_file<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, Stri
if !path.exists() {
return Err(format!("文件不存在: {}", path.display()));
}
let content = fs::read_to_string(path)
.map_err(|e| format!("读取文件失败: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("解析 JSON 失败: {}", e))
let content = fs::read_to_string(path).map_err(|e| format!("读取文件失败: {}", e))?;
serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {}", e))
}
/// 写入 JSON 配置文件
pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), String> {
// 确保目录存在
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("创建目录失败: {}", e))?;
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
}
let json = serde_json::to_string_pretty(data)
.map_err(|e| format!("序列化 JSON 失败: {}", e))?;
fs::write(path, json)
.map_err(|e| format!("写入文件失败: {}", e))
let json =
serde_json::to_string_pretty(data).map_err(|e| format!("序列化 JSON 失败: {}", e))?;
fs::write(path, json).map_err(|e| format!("写入文件失败: {}", e))
}
/// 复制文件
pub fn copy_file(from: &Path, to: &Path) -> Result<(), String> {
fs::copy(from, to)
.map_err(|e| format!("复制文件失败: {}", e))?;
fs::copy(from, to).map_err(|e| format!("复制文件失败: {}", e))?;
Ok(())
}
/// 删除文件
pub fn delete_file(path: &Path) -> Result<(), String> {
if path.exists() {
fs::remove_file(path)
.map_err(|e| format!("删除文件失败: {}", e))?;
fs::remove_file(path).map_err(|e| format!("删除文件失败: {}", e))?;
}
Ok(())
}
@@ -125,18 +119,18 @@ pub fn backup_config(from: &Path, to: &Path) -> Result<(), String> {
/// 导入当前 Claude Code 配置为默认供应商
pub fn import_current_config_as_default() -> Result<Value, String> {
let settings_path = get_claude_settings_path();
if !settings_path.exists() {
return Err("Claude Code 配置文件不存在".to_string());
}
// 读取当前配置
let settings_config: Value = read_json_file(&settings_path)?;
// 保存为 default 供应商
let default_provider_path = get_provider_config_path("default", Some("default"));
write_json_file(&default_provider_path, &settings_config)?;
log::info!("已导入当前配置为默认供应商");
Ok(settings_config)
}

View File

@@ -1,7 +1,7 @@
mod commands;
mod config;
mod provider;
mod store;
mod commands;
use store::AppState;
use tauri::Manager;
@@ -16,33 +16,32 @@ pub fn run() {
{
// 设置 macOS 标题栏背景色为主界面蓝色
if let Some(window) = app.get_webview_window("main") {
use objc2::runtime::AnyObject;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2_app_kit::NSColor;
let ns_window_ptr = window.ns_window().unwrap();
let ns_window: Retained<AnyObject> = unsafe {
Retained::retain(ns_window_ptr as *mut AnyObject).unwrap()
};
let ns_window: Retained<AnyObject> =
unsafe { Retained::retain(ns_window_ptr as *mut AnyObject).unwrap() };
// 使用与主界面 banner 相同的蓝色 #3498db
// #3498db = RGB(52, 152, 219)
let bg_color = unsafe {
NSColor::colorWithRed_green_blue_alpha(
52.0/255.0, // R: 52
152.0/255.0, // G: 152
219.0/255.0, // B: 219
1.0, // Alpha: 1.0
52.0 / 255.0, // R: 52
152.0 / 255.0, // G: 152
219.0 / 255.0, // B: 219
1.0, // Alpha: 1.0
)
};
unsafe {
use objc2::msg_send;
let _: () = msg_send![&*ns_window, setBackgroundColor: &*bg_color];
}
}
}
// 初始化日志
if cfg!(debug_assertions) {
app.handle().plugin(
@@ -51,20 +50,20 @@ pub fn run() {
.build(),
)?;
}
// 初始化应用状态(仅创建一次,并在本函数末尾注入 manage
let app_state = AppState::new();
// 如果没有供应商且存在 Claude Code 配置,自动导入
{
let manager = app_state.provider_manager.lock().unwrap();
if manager.providers.is_empty() {
drop(manager); // 释放锁
let settings_path = config::get_claude_settings_path();
if settings_path.exists() {
log::info!("检测到 Claude Code 配置,自动导入为默认供应商");
if let Ok(settings_config) = config::import_current_config_as_default() {
let mut manager = app_state.provider_manager.lock().unwrap();
let provider = provider::Provider::with_id(
@@ -73,7 +72,7 @@ pub fn run() {
settings_config,
None,
);
if manager.add_provider(provider).is_ok() {
manager.current = "default".to_string();
drop(manager);
@@ -84,7 +83,7 @@ pub fn run() {
}
}
}
// 将同一个实例注入到全局状态,避免重复创建导致的不一致
app.manage(app_state);
Ok(())

View File

@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cc_switch_lib::run();
cc_switch_lib::run();
}

View File

@@ -4,8 +4,8 @@ use std::collections::HashMap;
use std::path::Path;
use crate::config::{
copy_file, delete_file, get_provider_config_path, read_json_file, write_json_file,
get_claude_settings_path, backup_config
backup_config, copy_file, delete_file, get_claude_settings_path, get_provider_config_path,
read_json_file, write_json_file,
};
/// 供应商结构体
@@ -22,7 +22,12 @@ pub struct Provider {
impl Provider {
/// 从现有ID创建供应商
pub fn with_id(id: String, name: String, settings_config: Value, website_url: Option<String>) -> Self {
pub fn with_id(
id: String,
name: String,
settings_config: Value,
website_url: Option<String>,
) -> Self {
Self {
id,
name,
@@ -55,110 +60,118 @@ impl ProviderManager {
log::info!("配置文件不存在,创建新的供应商管理器");
return Ok(Self::default());
}
read_json_file(path)
}
/// 保存供应商列表
pub fn save_to_file(&self, path: &Path) -> Result<(), String> {
write_json_file(path, self)
}
/// 添加供应商
pub fn add_provider(&mut self, provider: Provider) -> Result<(), String> {
// 保存供应商配置到独立文件
let config_path = get_provider_config_path(&provider.id, Some(&provider.name));
write_json_file(&config_path, &provider.settings_config)?;
// 添加到管理器
self.providers.insert(provider.id.clone(), provider);
Ok(())
}
/// 更新供应商
pub fn update_provider(&mut self, provider: Provider) -> Result<(), String> {
// 检查供应商是否存在
if !self.providers.contains_key(&provider.id) {
return Err(format!("供应商不存在: {}", provider.id));
}
// 如果名称改变了,需要处理配置文件
if let Some(old_provider) = self.providers.get(&provider.id) {
if old_provider.name != provider.name {
// 删除旧配置文件
let old_config_path = get_provider_config_path(&provider.id, Some(&old_provider.name));
let old_config_path =
get_provider_config_path(&provider.id, Some(&old_provider.name));
delete_file(&old_config_path).ok(); // 忽略删除错误
}
}
// 保存新配置文件
let config_path = get_provider_config_path(&provider.id, Some(&provider.name));
write_json_file(&config_path, &provider.settings_config)?;
// 更新管理器
self.providers.insert(provider.id.clone(), provider);
Ok(())
}
/// 删除供应商
pub fn delete_provider(&mut self, provider_id: &str) -> Result<(), String> {
// 检查是否为当前供应商
if self.current == provider_id {
return Err("不能删除当前正在使用的供应商".to_string());
}
// 获取供应商信息
let provider = self.providers.get(provider_id)
let provider = self
.providers
.get(provider_id)
.ok_or_else(|| format!("供应商不存在: {}", provider_id))?;
// 删除配置文件
let config_path = get_provider_config_path(provider_id, Some(&provider.name));
delete_file(&config_path)?;
// 从管理器删除
self.providers.remove(provider_id);
Ok(())
}
/// 切换供应商
pub fn switch_provider(&mut self, provider_id: &str) -> Result<(), String> {
// 检查供应商是否存在
let provider = self.providers.get(provider_id)
let provider = self
.providers
.get(provider_id)
.ok_or_else(|| format!("供应商不存在: {}", provider_id))?;
let settings_path = get_claude_settings_path();
let provider_config_path = get_provider_config_path(provider_id, Some(&provider.name));
// 检查供应商配置文件是否存在
if !provider_config_path.exists() {
return Err(format!("供应商配置文件不存在: {}", provider_config_path.display()));
return Err(format!(
"供应商配置文件不存在: {}",
provider_config_path.display()
));
}
// 如果当前有配置,先备份到当前供应商
if settings_path.exists() && !self.current.is_empty() {
if let Some(current_provider) = self.providers.get(&self.current) {
let current_provider_path = get_provider_config_path(&self.current, Some(&current_provider.name));
let current_provider_path =
get_provider_config_path(&self.current, Some(&current_provider.name));
backup_config(&settings_path, &current_provider_path)?;
log::info!("已备份当前供应商配置: {}", current_provider.name);
}
}
// 确保主配置父目录存在
if let Some(parent) = settings_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("创建目录失败: {}", e))?;
std::fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
}
// 复制新供应商配置到主配置
copy_file(&provider_config_path, &settings_path)?;
// 更新当前供应商
self.current = provider_id.to_string();
log::info!("成功切换到供应商: {}", provider.name);
Ok(())
}
/// 获取所有供应商
pub fn get_all_providers(&self) -> &HashMap<String, Provider> {
&self.providers

View File

@@ -1,6 +1,6 @@
use std::sync::Mutex;
use crate::config::get_app_config_path;
use crate::provider::ProviderManager;
use std::sync::Mutex;
/// 全局应用状态
pub struct AppState {
@@ -11,25 +11,26 @@ impl AppState {
/// 创建新的应用状态
pub fn new() -> Self {
let config_path = get_app_config_path();
let provider_manager = ProviderManager::load_from_file(&config_path)
.unwrap_or_else(|e| {
log::warn!("加载配置失败: {}, 使用默认配置", e);
ProviderManager::default()
});
let provider_manager = ProviderManager::load_from_file(&config_path).unwrap_or_else(|e| {
log::warn!("加载配置失败: {}, 使用默认配置", e);
ProviderManager::default()
});
Self {
provider_manager: Mutex::new(provider_manager),
}
}
/// 保存配置到文件
pub fn save(&self) -> Result<(), String> {
let config_path = get_app_config_path();
let manager = self.provider_manager.lock()
let manager = self
.provider_manager
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
manager.save_to_file(&config_path)
}
// 保留按需扩展:若未来需要热加载,可在此实现
}

View File

@@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "CC Switch",
"version": "3.0.0",
"identifier": "com.ccswitch.app",
"identifier": "com.ccswitch.desktop",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:3000",

View File

@@ -26,7 +26,8 @@
gap: 1rem;
}
.refresh-btn, .add-btn {
.refresh-btn,
.add-btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
@@ -123,12 +124,12 @@
left: 50%;
transform: translateX(-50%);
z-index: 100;
padding: 0.75rem 1.25rem;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
width: fit-content;
white-space: nowrap;
}

View File

@@ -10,9 +10,12 @@ function App() {
const [providers, setProviders] = useState<Record<string, Provider>>({});
const [currentProviderId, setCurrentProviderId] = useState<string>("");
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [configStatus, setConfigStatus] = useState<{ exists: boolean; path: string } | null>(null);
const [configStatus, setConfigStatus] = useState<{
exists: boolean;
path: string;
} | null>(null);
const [editingProviderId, setEditingProviderId] = useState<string | null>(
null
null,
);
const [notification, setNotification] = useState<{
message: string;
@@ -31,7 +34,7 @@ function App() {
const showNotification = (
message: string,
type: "success" | "error",
duration = 3000
duration = 3000,
) => {
// 清除之前的定时器
if (timeoutRef.current) {
@@ -73,7 +76,7 @@ function App() {
const currentId = await window.api.getCurrentProvider();
setProviders(loadedProviders);
setCurrentProviderId(currentId);
// 如果供应商列表为空,尝试自动导入现有配置为"default"供应商
if (Object.keys(loadedProviders).length === 0) {
await handleAutoImportDefault();
@@ -82,7 +85,10 @@ function App() {
const loadConfigStatus = async () => {
const status = await window.api.getClaudeConfigStatus();
setConfigStatus({ exists: Boolean(status?.exists), path: String(status?.path || "") });
setConfigStatus({
exists: Boolean(status?.exists),
path: String(status?.path || ""),
});
};
// 生成唯一ID
@@ -137,7 +143,7 @@ function App() {
showNotification(
"切换成功!请重启 Claude Code 终端以生效",
"success",
2000
2000,
);
} else {
showNotification("切换失败,请检查配置", "error");
@@ -147,18 +153,22 @@ function App() {
// 自动导入现有配置为"default"供应商
const handleAutoImportDefault = async () => {
try {
const result = await window.api.importCurrentConfigAsDefault()
const result = await window.api.importCurrentConfigAsDefault();
if (result.success) {
await loadProviders()
showNotification("已自动导入现有配置为 default 供应商", "success", 3000)
await loadProviders();
showNotification(
"已自动导入现有配置为 default 供应商",
"success",
3000,
);
}
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
} catch (error) {
console.error('自动导入默认配置失败:', error)
console.error("自动导入默认配置失败:", error);
// 静默处理,不影响用户体验
}
}
};
const handleOpenConfigFolder = async () => {
await window.api.openConfigFolder();

View File

@@ -22,7 +22,7 @@
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 1001;
display: flex; /* 纵向布局,便于底栏固定 */
display: flex; /* 纵向布局,便于底栏固定 */
flex-direction: column;
}
@@ -40,7 +40,10 @@
}
/* 左侧占位以保证标题居中(与右侧关闭按钮宽度相当) */
.modal-spacer { width: 32px; flex: 0 0 32px; }
.modal-spacer {
width: 32px;
flex: 0 0 32px;
}
.modal-title {
flex: 1;
@@ -69,16 +72,17 @@
color: #fff;
}
.modal-form { /* 表单外层包裹 body + footer */
.modal-form {
/* 表单外层包裹 body + footer */
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0; /* 允许子元素正确计算高度 */
min-height: 0; /* 允许子元素正确计算高度 */
}
.modal-body {
padding: 1.25rem 1.5rem 1.5rem;
overflow: auto; /* 仅内容区滚动 */
overflow: auto; /* 仅内容区滚动 */
flex: 1 1 auto;
min-height: 0;
}
@@ -175,7 +179,8 @@
border-color: #3498db;
}
.modal-footer { /* 固定在弹窗底部(非滚动区) */
.modal-footer {
/* 固定在弹窗底部(非滚动区) */
display: flex;
gap: 1rem;
justify-content: flex-end;

View File

@@ -68,7 +68,9 @@
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: background-color 0.2s, transform 0.1s;
transition:
background-color 0.2s,
transform 0.1s;
min-width: 70px;
}
@@ -102,4 +104,4 @@
.confirm-btn:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import './ConfirmDialog.css';
import React from "react";
import "./ConfirmDialog.css";
interface ConfirmDialogProps {
isOpen: boolean;
@@ -15,10 +15,10 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
isOpen,
title,
message,
confirmText = '确定',
cancelText = '取消',
confirmText = "确定",
cancelText = "取消",
onConfirm,
onCancel
onCancel,
}) => {
if (!isOpen) return null;
@@ -32,15 +32,15 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
<p>{message}</p>
</div>
<div className="confirm-actions">
<button
className="confirm-btn cancel-btn"
<button
className="confirm-btn cancel-btn"
onClick={onCancel}
autoFocus
>
{cancelText}
</button>
<button
className="confirm-btn confirm-btn-primary"
<button
className="confirm-btn confirm-btn-primary"
onClick={onConfirm}
>
{confirmText}
@@ -49,4 +49,4 @@ export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
</div>
</div>
);
};
};

View File

@@ -1,20 +1,24 @@
import React from 'react'
import { Provider } from '../types'
import ProviderForm from './ProviderForm'
import React from "react";
import { Provider } from "../types";
import ProviderForm from "./ProviderForm";
interface EditProviderModalProps {
provider: Provider
onSave: (provider: Provider) => void
onClose: () => void
provider: Provider;
onSave: (provider: Provider) => void;
onClose: () => void;
}
const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave, onClose }) => {
const handleSubmit = (data: Omit<Provider, 'id'>) => {
const EditProviderModal: React.FC<EditProviderModalProps> = ({
provider,
onSave,
onClose,
}) => {
const handleSubmit = (data: Omit<Provider, "id">) => {
onSave({
...provider,
...data
})
}
...data,
});
};
return (
<ProviderForm
@@ -25,7 +29,7 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({ provider, onSave,
onSubmit={handleSubmit}
onClose={onClose}
/>
)
}
);
};
export default EditProviderModal
export default EditProviderModal;

View File

@@ -80,7 +80,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
@@ -117,7 +117,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 更新JSON配置
const updatedConfig = updateCoAuthoredSetting(
formData.settingsConfig,
checked
checked,
);
setFormData({
...formData,
@@ -152,7 +152,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const configString = setApiKeyInConfig(
formData.settingsConfig,
key.trim(),
{ createIfMissing: selectedPreset !== null }
{ createIfMissing: selectedPreset !== null },
);
// 更新表单配置
@@ -174,7 +174,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
useEffect(() => {
if (initialData) {
const parsedKey = getApiKeyFromConfig(
JSON.stringify(initialData.settingsConfig)
JSON.stringify(initialData.settingsConfig),
);
if (parsedKey) setApiKey(parsedKey);
}
@@ -255,7 +255,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
/>
</div>
<div className={`form-group api-key-group ${!showApiKey ? 'hidden' : ''}`}>
<div
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
>
<label htmlFor="apiKey">API Key *</label>
<input
type="text"

View File

@@ -203,4 +203,4 @@
.delete-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}

View File

@@ -1,13 +1,13 @@
import React from 'react'
import { Provider } from '../types'
import './ProviderList.css'
import React from "react";
import { Provider } from "../types";
import "./ProviderList.css";
interface ProviderListProps {
providers: Record<string, Provider>
currentProviderId: string
onSwitch: (id: string) => void
onDelete: (id: string) => void
onEdit: (id: string) => void
providers: Record<string, Provider>;
currentProviderId: string;
onSwitch: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string) => void;
}
const ProviderList: React.FC<ProviderListProps> = ({
@@ -15,28 +15,28 @@ const ProviderList: React.FC<ProviderListProps> = ({
currentProviderId,
onSwitch,
onDelete,
onEdit
onEdit,
}) => {
// 提取API地址
const getApiUrl = (provider: Provider): string => {
try {
const config = provider.settingsConfig
const config = provider.settingsConfig;
if (config?.env?.ANTHROPIC_BASE_URL) {
return config.env.ANTHROPIC_BASE_URL
return config.env.ANTHROPIC_BASE_URL;
}
return '未设置'
return "未设置";
} catch {
return '配置错误'
return "配置错误";
}
}
};
const handleUrlClick = async (url: string) => {
try {
await window.api.openExternal(url)
await window.api.openExternal(url);
} catch (error) {
console.error('打开链接失败:', error)
console.error("打开链接失败:", error);
}
}
};
return (
<div className="provider-list">
@@ -48,25 +48,27 @@ const ProviderList: React.FC<ProviderListProps> = ({
) : (
<div className="provider-items">
{Object.values(providers).map((provider) => {
const isCurrent = provider.id === currentProviderId
const isCurrent = provider.id === currentProviderId;
return (
<div
key={provider.id}
className={`provider-item ${isCurrent ? 'current' : ''}`}
<div
key={provider.id}
className={`provider-item ${isCurrent ? "current" : ""}`}
>
<div className="provider-info">
<div className="provider-name">
<span>{provider.name}</span>
{isCurrent && <span className="current-badge">使</span>}
{isCurrent && (
<span className="current-badge">使</span>
)}
</div>
<div className="provider-url">
{provider.websiteUrl ? (
<a
href="#"
<a
href="#"
onClick={(e) => {
e.preventDefault()
handleUrlClick(provider.websiteUrl!)
e.preventDefault();
handleUrlClick(provider.websiteUrl!);
}}
className="url-link"
title={`访问 ${provider.websiteUrl}`}
@@ -80,23 +82,23 @@ const ProviderList: React.FC<ProviderListProps> = ({
)}
</div>
</div>
<div className="provider-actions">
<button
<button
className="enable-btn"
onClick={() => onSwitch(provider.id)}
disabled={isCurrent}
>
</button>
<button
<button
className="edit-btn"
onClick={() => onEdit(provider.id)}
disabled={isCurrent}
>
</button>
<button
<button
className="delete-btn"
onClick={() => onDelete(provider.id)}
disabled={isCurrent}
@@ -105,12 +107,12 @@ const ProviderList: React.FC<ProviderListProps> = ({
</button>
</div>
</div>
)
);
})}
</div>
)}
</div>
)
}
);
};
export default ProviderList
export default ProviderList;

View File

@@ -63,5 +63,4 @@ export const providerPresets: ProviderPreset[] = [
},
},
},
];

View File

@@ -5,9 +5,9 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;

View File

@@ -1,5 +1,5 @@
import { invoke } from '@tauri-apps/api/core';
import { Provider } from '../types';
import { invoke } from "@tauri-apps/api/core";
import { Provider } from "../types";
// 定义配置状态类型
interface ConfigStatus {
@@ -19,9 +19,9 @@ export const tauriAPI = {
// 获取所有供应商
getProviders: async (): Promise<Record<string, Provider>> => {
try {
return await invoke('get_providers');
return await invoke("get_providers");
} catch (error) {
console.error('获取供应商列表失败:', error);
console.error("获取供应商列表失败:", error);
return {};
}
},
@@ -29,19 +29,19 @@ export const tauriAPI = {
// 获取当前供应商ID
getCurrentProvider: async (): Promise<string> => {
try {
return await invoke('get_current_provider');
return await invoke("get_current_provider");
} catch (error) {
console.error('获取当前供应商失败:', error);
return '';
console.error("获取当前供应商失败:", error);
return "";
}
},
// 添加供应商
addProvider: async (provider: Provider): Promise<boolean> => {
try {
return await invoke('add_provider', { provider });
return await invoke("add_provider", { provider });
} catch (error) {
console.error('添加供应商失败:', error);
console.error("添加供应商失败:", error);
throw error;
}
},
@@ -49,9 +49,9 @@ export const tauriAPI = {
// 更新供应商
updateProvider: async (provider: Provider): Promise<boolean> => {
try {
return await invoke('update_provider', { provider });
return await invoke("update_provider", { provider });
} catch (error) {
console.error('更新供应商失败:', error);
console.error("更新供应商失败:", error);
throw error;
}
},
@@ -59,9 +59,9 @@ export const tauriAPI = {
// 删除供应商
deleteProvider: async (id: string): Promise<boolean> => {
try {
return await invoke('delete_provider', { id });
return await invoke("delete_provider", { id });
} catch (error) {
console.error('删除供应商失败:', error);
console.error("删除供应商失败:", error);
throw error;
}
},
@@ -69,9 +69,9 @@ export const tauriAPI = {
// 切换供应商
switchProvider: async (providerId: string): Promise<boolean> => {
try {
return await invoke('switch_provider', { id: providerId });
return await invoke("switch_provider", { id: providerId });
} catch (error) {
console.error('切换供应商失败:', error);
console.error("切换供应商失败:", error);
return false;
}
},
@@ -79,16 +79,16 @@ export const tauriAPI = {
// 导入当前配置为默认供应商
importCurrentConfigAsDefault: async (): Promise<ImportResult> => {
try {
const success = await invoke<boolean>('import_default_config');
const success = await invoke<boolean>("import_default_config");
return {
success,
message: success ? '成功导入默认配置' : '导入失败'
message: success ? "成功导入默认配置" : "导入失败",
};
} catch (error) {
console.error('导入默认配置失败:', error);
console.error("导入默认配置失败:", error);
return {
success: false,
message: String(error)
message: String(error),
};
}
},
@@ -96,23 +96,23 @@ export const tauriAPI = {
// 获取 Claude Code 配置文件路径
getClaudeCodeConfigPath: async (): Promise<string> => {
try {
return await invoke('get_claude_code_config_path');
return await invoke("get_claude_code_config_path");
} catch (error) {
console.error('获取配置路径失败:', error);
return '';
console.error("获取配置路径失败:", error);
return "";
}
},
// 获取 Claude Code 配置状态
getClaudeConfigStatus: async (): Promise<ConfigStatus> => {
try {
return await invoke('get_claude_config_status');
return await invoke("get_claude_config_status");
} catch (error) {
console.error('获取配置状态失败:', error);
console.error("获取配置状态失败:", error);
return {
exists: false,
path: '',
error: String(error)
path: "",
error: String(error),
};
}
},
@@ -120,34 +120,33 @@ export const tauriAPI = {
// 打开配置文件夹
openConfigFolder: async (): Promise<void> => {
try {
await invoke('open_config_folder');
await invoke("open_config_folder");
} catch (error) {
console.error('打开配置文件夹失败:', error);
console.error("打开配置文件夹失败:", error);
}
},
// 打开外部链接
openExternal: async (url: string): Promise<void> => {
try {
await invoke('open_external', { url });
await invoke("open_external", { url });
} catch (error) {
console.error('打开外部链接失败:', error);
console.error("打开外部链接失败:", error);
}
},
// 选择配置文件Tauri 暂不实现,保留接口兼容性)
selectConfigFile: async (): Promise<string | null> => {
console.warn('selectConfigFile 在 Tauri 版本中暂不支持');
console.warn("selectConfigFile 在 Tauri 版本中暂不支持");
return null;
}
},
};
// 创建全局 API 对象,兼容现有代码
if (typeof window !== 'undefined') {
if (typeof window !== "undefined") {
// 绑定到 window.api避免 Electron 命名造成误解
// API 内部已做 try/catch非 Tauri 环境下也会安全返回默认值
(window as any).api = tauriAPI;
}
export default tauriAPI;

View File

@@ -1,24 +1,24 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
// 导入 Tauri API自动绑定到 window.api
import './lib/tauri-api'
import "./lib/tauri-api";
// 根据平台添加 body class便于平台特定样式
try {
const ua = navigator.userAgent || ''
const plat = (navigator.platform || '').toLowerCase()
const isMac = /mac/i.test(ua) || plat.includes('mac')
const ua = navigator.userAgent || "";
const plat = (navigator.platform || "").toLowerCase();
const isMac = /mac/i.test(ua) || plat.includes("mac");
if (isMac) {
document.body.classList.add('is-mac')
document.body.classList.add("is-mac");
}
} catch {
// 忽略平台检测失败
}
ReactDOM.createRoot(document.getElementById('root')!).render(
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
</React.StrictMode>,
);

View File

@@ -1,91 +1,97 @@
// 供应商配置处理工具函数
// 处理includeCoAuthoredBy字段的添加/删除
export const updateCoAuthoredSetting = (jsonString: string, disable: boolean): string => {
export const updateCoAuthoredSetting = (
jsonString: string,
disable: boolean,
): string => {
try {
const config = JSON.parse(jsonString)
const config = JSON.parse(jsonString);
if (disable) {
// 添加或更新includeCoAuthoredBy字段
config.includeCoAuthoredBy = false
config.includeCoAuthoredBy = false;
} else {
// 删除includeCoAuthoredBy字段
delete config.includeCoAuthoredBy
delete config.includeCoAuthoredBy;
}
return JSON.stringify(config, null, 2)
return JSON.stringify(config, null, 2);
} catch (err) {
// 如果JSON解析失败返回原始字符串
return jsonString
return jsonString;
}
}
};
// 从JSON配置中检查是否包含includeCoAuthoredBy设置
export const checkCoAuthoredSetting = (jsonString: string): boolean => {
try {
const config = JSON.parse(jsonString)
return config.includeCoAuthoredBy === false
const config = JSON.parse(jsonString);
return config.includeCoAuthoredBy === false;
} catch (err) {
return false
return false;
}
}
};
// 从JSON配置中提取并处理官网地址
export const extractWebsiteUrl = (jsonString: string): string => {
try {
const config = JSON.parse(jsonString)
const baseUrl = config?.env?.ANTHROPIC_BASE_URL
if (baseUrl && typeof baseUrl === 'string') {
const config = JSON.parse(jsonString);
const baseUrl = config?.env?.ANTHROPIC_BASE_URL;
if (baseUrl && typeof baseUrl === "string") {
// 去掉 "api." 前缀
return baseUrl.replace(/^https?:\/\/api\./, 'https://')
return baseUrl.replace(/^https?:\/\/api\./, "https://");
}
} catch (err) {
// 忽略JSON解析错误
}
return ''
}
return "";
};
// 读取配置中的 API Keyenv.ANTHROPIC_AUTH_TOKEN
export const getApiKeyFromConfig = (jsonString: string): string => {
try {
const config = JSON.parse(jsonString)
const key = config?.env?.ANTHROPIC_AUTH_TOKEN
return typeof key === 'string' ? key : ''
const config = JSON.parse(jsonString);
const key = config?.env?.ANTHROPIC_AUTH_TOKEN;
return typeof key === "string" ? key : "";
} catch (err) {
return ''
return "";
}
}
};
// 判断配置中是否存在 API Key 字段
export const hasApiKeyField = (jsonString: string): boolean => {
try {
const config = JSON.parse(jsonString)
return Object.prototype.hasOwnProperty.call(config?.env ?? {}, 'ANTHROPIC_AUTH_TOKEN')
const config = JSON.parse(jsonString);
return Object.prototype.hasOwnProperty.call(
config?.env ?? {},
"ANTHROPIC_AUTH_TOKEN",
);
} catch (err) {
return false
return false;
}
}
};
// 写入/更新配置中的 API Key默认不新增缺失字段
export const setApiKeyInConfig = (
jsonString: string,
apiKey: string,
options: { createIfMissing?: boolean } = {}
options: { createIfMissing?: boolean } = {},
): string => {
const { createIfMissing = false } = options
const { createIfMissing = false } = options;
try {
const config = JSON.parse(jsonString)
const config = JSON.parse(jsonString);
if (!config.env) {
if (!createIfMissing) return jsonString
config.env = {}
if (!createIfMissing) return jsonString;
config.env = {};
}
if (!('ANTHROPIC_AUTH_TOKEN' in config.env) && !createIfMissing) {
return jsonString
if (!("ANTHROPIC_AUTH_TOKEN" in config.env) && !createIfMissing) {
return jsonString;
}
config.env.ANTHROPIC_AUTH_TOKEN = apiKey
return JSON.stringify(config, null, 2)
config.env.ANTHROPIC_AUTH_TOKEN = apiKey;
return JSON.stringify(config, null, 2);
} catch (err) {
return jsonString
return jsonString;
}
}
};

2
src/vite-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="vite/client" />
import { Provider } from './types';
import { Provider } from "./types";
interface ImportResult {
success: boolean;