- merge: merge origin/main, resolve conflicts and preserve both feature sets
- feat(tauri): register import/export and file dialogs; keep endpoint speed test and custom endpoints - feat(api): add updateTrayMenu and onProviderSwitched; wire import/export APIs - feat(types): extend global API declarations (import/export) - chore(presets): GLM preset supports both new and legacy model keys - chore(rust): add chrono dependency; refresh lockfile
This commit is contained in:
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
v22.4.1
|
||||
3
src-tauri/Cargo.lock
generated
3
src-tauri/Cargo.lock
generated
@@ -565,6 +565,7 @@ dependencies = [
|
||||
name = "cc-switch"
|
||||
version = "3.4.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
"futures",
|
||||
"log",
|
||||
@@ -631,8 +632,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link 0.2.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ tauri-build = { version = "2.4.0", features = [] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
chrono = "0.4"
|
||||
tauri = { version = "2.8.2", features = ["tray-icon"] }
|
||||
tauri-plugin-log = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
|
||||
170
src-tauri/src/import_export.rs
Normal file
170
src-tauri/src/import_export.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use chrono::Utc;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// 默认仅保留最近 10 份备份,避免目录无限膨胀
|
||||
const MAX_BACKUPS: usize = 10;
|
||||
|
||||
/// 创建配置文件备份
|
||||
pub fn create_backup(config_path: &PathBuf) -> Result<String, String> {
|
||||
if !config_path.exists() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
|
||||
let backup_id = format!("backup_{}", timestamp);
|
||||
|
||||
let backup_dir = config_path
|
||||
.parent()
|
||||
.ok_or("Invalid config path")?
|
||||
.join("backups");
|
||||
|
||||
// 创建备份目录
|
||||
fs::create_dir_all(&backup_dir)
|
||||
.map_err(|e| format!("Failed to create backup directory: {}", e))?;
|
||||
|
||||
let backup_path = backup_dir.join(format!("{}.json", backup_id));
|
||||
|
||||
// 复制配置文件到备份
|
||||
fs::copy(config_path, backup_path).map_err(|e| format!("Failed to create backup: {}", e))?;
|
||||
|
||||
// 备份完成后清理旧的备份文件(仅保留最近 MAX_BACKUPS 份)
|
||||
cleanup_old_backups(&backup_dir, MAX_BACKUPS)?;
|
||||
|
||||
Ok(backup_id)
|
||||
}
|
||||
|
||||
fn cleanup_old_backups(backup_dir: &PathBuf, retain: usize) -> Result<(), String> {
|
||||
if retain == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut entries: Vec<_> = match fs::read_dir(backup_dir) {
|
||||
Ok(iter) => iter
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.path()
|
||||
.extension()
|
||||
.map(|ext| ext == "json")
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
|
||||
if entries.len() <= retain {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let remove_count = entries.len().saturating_sub(retain);
|
||||
|
||||
entries.sort_by(|a, b| {
|
||||
let a_time = a.metadata().and_then(|m| m.modified()).ok();
|
||||
let b_time = b.metadata().and_then(|m| m.modified()).ok();
|
||||
a_time.cmp(&b_time)
|
||||
});
|
||||
|
||||
for entry in entries.into_iter().take(remove_count) {
|
||||
if let Err(err) = fs::remove_file(entry.path()) {
|
||||
log::warn!(
|
||||
"Failed to remove old backup {}: {}",
|
||||
entry.path().display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 导出配置文件
|
||||
#[tauri::command]
|
||||
pub async fn export_config_to_file(file_path: String) -> Result<Value, String> {
|
||||
// 读取当前配置文件
|
||||
let config_path = crate::config::get_app_config_path();
|
||||
let config_content = fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("Failed to read configuration: {}", e))?;
|
||||
|
||||
// 写入到指定文件
|
||||
fs::write(&file_path, &config_content).map_err(|e| format!("Failed to write file: {}", e))?;
|
||||
|
||||
Ok(json!({
|
||||
"success": true,
|
||||
"message": "Configuration exported successfully",
|
||||
"filePath": file_path
|
||||
}))
|
||||
}
|
||||
|
||||
/// 从文件导入配置
|
||||
#[tauri::command]
|
||||
pub async fn import_config_from_file(
|
||||
file_path: String,
|
||||
state: tauri::State<'_, crate::store::AppState>,
|
||||
) -> Result<Value, String> {
|
||||
// 读取导入的文件
|
||||
let import_content =
|
||||
fs::read_to_string(&file_path).map_err(|e| format!("Failed to read import file: {}", e))?;
|
||||
|
||||
// 验证并解析为配置对象
|
||||
let new_config: crate::app_config::MultiAppConfig = serde_json::from_str(&import_content)
|
||||
.map_err(|e| format!("Invalid configuration file: {}", e))?;
|
||||
|
||||
// 备份当前配置
|
||||
let config_path = crate::config::get_app_config_path();
|
||||
let backup_id = create_backup(&config_path)?;
|
||||
|
||||
// 写入新配置到磁盘
|
||||
fs::write(&config_path, &import_content)
|
||||
.map_err(|e| format!("Failed to write configuration: {}", e))?;
|
||||
|
||||
// 更新内存中的状态
|
||||
{
|
||||
let mut config_state = state
|
||||
.config
|
||||
.lock()
|
||||
.map_err(|e| format!("Failed to lock config: {}", e))?;
|
||||
*config_state = new_config;
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"success": true,
|
||||
"message": "Configuration imported successfully",
|
||||
"backupId": backup_id
|
||||
}))
|
||||
}
|
||||
|
||||
/// 保存文件对话框
|
||||
#[tauri::command]
|
||||
pub async fn save_file_dialog<R: tauri::Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
default_name: String,
|
||||
) -> Result<Option<String>, String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
let dialog = app.dialog();
|
||||
let result = dialog
|
||||
.file()
|
||||
.add_filter("JSON", &["json"])
|
||||
.set_file_name(&default_name)
|
||||
.blocking_save_file();
|
||||
|
||||
Ok(result.map(|p| p.to_string()))
|
||||
}
|
||||
|
||||
/// 打开文件对话框
|
||||
#[tauri::command]
|
||||
pub async fn open_file_dialog<R: tauri::Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> Result<Option<String>, String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
let dialog = app.dialog();
|
||||
let result = dialog
|
||||
.file()
|
||||
.add_filter("JSON", &["json"])
|
||||
.blocking_pick_file();
|
||||
|
||||
Ok(result.map(|p| p.to_string()))
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod claude_plugin;
|
||||
mod codex_config;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod import_export;
|
||||
mod migration;
|
||||
mod provider;
|
||||
mod settings;
|
||||
@@ -420,11 +421,17 @@ pub fn run() {
|
||||
commands::read_claude_plugin_config,
|
||||
commands::apply_claude_plugin_config,
|
||||
commands::is_claude_plugin_applied,
|
||||
// ours: endpoint speed test + custom endpoint management
|
||||
commands::test_api_endpoints,
|
||||
commands::get_custom_endpoints,
|
||||
commands::add_custom_endpoint,
|
||||
commands::remove_custom_endpoint,
|
||||
commands::update_endpoint_last_used,
|
||||
// theirs: config import/export and dialogs
|
||||
import_export::export_config_to_file,
|
||||
import_export::import_config_from_file,
|
||||
import_export::save_file_dialog,
|
||||
import_export::open_file_dialog,
|
||||
update_tray_menu,
|
||||
]);
|
||||
|
||||
|
||||
14
src/App.tsx
14
src/App.tsx
@@ -229,6 +229,15 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSuccess = async () => {
|
||||
await loadProviders();
|
||||
try {
|
||||
await window.api.updateTrayMenu();
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to refresh tray menu after import", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动从 live 导入一条默认供应商(仅首次初始化时)
|
||||
const handleAutoImportDefault = async () => {
|
||||
try {
|
||||
@@ -357,7 +366,10 @@ function App() {
|
||||
)}
|
||||
|
||||
{isSettingsOpen && (
|
||||
<SettingsModal onClose={() => setIsSettingsOpen(false)} />
|
||||
<SettingsModal
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
onImportSuccess={handleImportSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
103
src/components/ImportProgressModal.tsx
Normal file
103
src/components/ImportProgressModal.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useEffect } from "react";
|
||||
import { CheckCircle, Loader2, AlertCircle } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ImportProgressModalProps {
|
||||
status: 'importing' | 'success' | 'error';
|
||||
message?: string;
|
||||
backupId?: string;
|
||||
onComplete?: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function ImportProgressModal({
|
||||
status,
|
||||
message,
|
||||
backupId,
|
||||
onComplete,
|
||||
onSuccess
|
||||
}: ImportProgressModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'success') {
|
||||
console.log('[ImportProgressModal] Success detected, starting 2 second countdown');
|
||||
// 成功后等待2秒自动关闭并刷新数据
|
||||
const timer = setTimeout(() => {
|
||||
console.log('[ImportProgressModal] 2 seconds elapsed, calling callbacks...');
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => {
|
||||
console.log('[ImportProgressModal] Cleanup timer');
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
}, [status, onComplete, onSuccess]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/50 dark:bg-black/70 backdrop-blur-sm" />
|
||||
|
||||
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-2xl p-8 max-w-md w-full mx-4">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{status === 'importing' && (
|
||||
<>
|
||||
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{t("settings.importing")}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t("common.loading")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{t("settings.importSuccess")}
|
||||
</h3>
|
||||
{backupId && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
{t("settings.backupId")}: {backupId}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t("settings.autoReload")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<>
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
{t("settings.importFailed")}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{message || t("settings.configCorrupted")}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}}
|
||||
className="mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{t("common.close")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -199,7 +199,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{appType === "claude" ? (
|
||||
<div className="w-[130px]">
|
||||
<div className="flex-shrink-0">
|
||||
{provider.category !== "official" && isCurrent && (
|
||||
<button
|
||||
onClick={() =>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Save,
|
||||
} from "lucide-react";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { ImportProgressModal } from "./ImportProgressModal";
|
||||
import { homeDir, join } from "@tauri-apps/api/path";
|
||||
import "../lib/tauri-api";
|
||||
import { relaunchApp } from "../lib/updater";
|
||||
@@ -22,9 +23,10 @@ import { isLinux } from "../lib/platform";
|
||||
|
||||
interface SettingsModalProps {
|
||||
onClose: () => void;
|
||||
onImportSuccess?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
export default function SettingsModal({ onClose, onImportSuccess }: SettingsModalProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const normalizeLanguage = (lang?: string | null): "zh" | "en" =>
|
||||
@@ -63,6 +65,13 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } =
|
||||
useUpdate();
|
||||
|
||||
// 导入/导出相关状态
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [importStatus, setImportStatus] = useState<'idle' | 'importing' | 'success' | 'error'>('idle');
|
||||
const [importError, setImportError] = useState<string>("");
|
||||
const [importBackupId, setImportBackupId] = useState<string>("");
|
||||
const [selectedImportFile, setSelectedImportFile] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
loadConfigPath();
|
||||
@@ -346,6 +355,66 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// 导出配置处理函数
|
||||
const handleExportConfig = async () => {
|
||||
try {
|
||||
const defaultName = `cc-switch-config-${new Date().toISOString().split('T')[0]}.json`;
|
||||
const filePath = await window.api.saveFileDialog(defaultName);
|
||||
|
||||
if (!filePath) return; // 用户取消了
|
||||
|
||||
const result = await window.api.exportConfigToFile(filePath);
|
||||
|
||||
if (result.success) {
|
||||
alert(`${t("settings.configExported")}\n${result.filePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("导出配置失败:", error);
|
||||
alert(`${t("settings.exportFailed")}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择要导入的文件
|
||||
const handleSelectImportFile = async () => {
|
||||
try {
|
||||
const filePath = await window.api.openFileDialog();
|
||||
if (filePath) {
|
||||
setSelectedImportFile(filePath);
|
||||
setImportStatus('idle'); // 重置状态
|
||||
setImportError('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('选择文件失败:', error);
|
||||
alert(`${t("settings.selectFileFailed")}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 执行导入
|
||||
const handleExecuteImport = async () => {
|
||||
if (!selectedImportFile || isImporting) return;
|
||||
|
||||
setIsImporting(true);
|
||||
setImportStatus('importing');
|
||||
|
||||
try {
|
||||
const result = await window.api.importConfigFromFile(selectedImportFile);
|
||||
|
||||
if (result.success) {
|
||||
setImportBackupId(result.backupId || '');
|
||||
setImportStatus('success');
|
||||
// ImportProgressModal 会在2秒后触发数据刷新回调
|
||||
} else {
|
||||
setImportError(result.message || t("settings.configCorrupted"));
|
||||
setImportStatus('error');
|
||||
}
|
||||
} catch (error) {
|
||||
setImportError(String(error));
|
||||
setImportStatus('error');
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@@ -542,6 +611,56 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导入导出 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
{t("settings.importExport")}
|
||||
</h3>
|
||||
<div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||
<div className="space-y-3">
|
||||
{/* 导出按钮 */}
|
||||
<button
|
||||
onClick={handleExportConfig}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium rounded-lg transition-colors bg-gray-500 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-700 text-white"
|
||||
>
|
||||
<Save size={12} />
|
||||
{t("settings.exportConfig")}
|
||||
</button>
|
||||
|
||||
{/* 导入区域 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSelectImportFile}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium rounded-lg transition-colors bg-gray-500 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-700 text-white"
|
||||
>
|
||||
<FolderOpen size={12} />
|
||||
{t("settings.selectConfigFile")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExecuteImport}
|
||||
disabled={!selectedImportFile || isImporting}
|
||||
className={`px-3 py-2 text-xs font-medium rounded-lg transition-colors text-white ${
|
||||
!selectedImportFile || isImporting
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700'
|
||||
}`}
|
||||
>
|
||||
{isImporting ? t("settings.importing") : t("settings.import")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 显示选择的文件 */}
|
||||
{selectedImportFile && (
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 px-2 py-1 bg-gray-50 dark:bg-gray-900 rounded break-all">
|
||||
{selectedImportFile.split('/').pop() || selectedImportFile.split('\\').pop() || selectedImportFile}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关于 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
@@ -636,6 +755,28 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Progress Modal */}
|
||||
{importStatus !== 'idle' && (
|
||||
<ImportProgressModal
|
||||
status={importStatus}
|
||||
message={importError}
|
||||
backupId={importBackupId}
|
||||
onComplete={() => {
|
||||
setImportStatus('idle');
|
||||
setImportError('');
|
||||
setSelectedImportFile('');
|
||||
}}
|
||||
onSuccess={() => {
|
||||
if (onImportSuccess) {
|
||||
void onImportSuccess();
|
||||
}
|
||||
void window.api
|
||||
.updateTrayMenu()
|
||||
.catch((error) => console.error("[SettingsModal] Failed to refresh tray menu", error));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,8 +54,12 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "GLM-4.5",
|
||||
ANTHROPIC_SMALL_FAST_MODEL: "GLM-4.5-Air",
|
||||
// 兼容旧键名,保持前端读取一致
|
||||
ANTHROPIC_MODEL: "GLM-4.6",
|
||||
ANTHROPIC_SMALL_FAST_MODEL: "glm-4.5-air",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.5-air",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.6",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.6",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
|
||||
@@ -61,6 +61,19 @@
|
||||
"title": "Settings",
|
||||
"general": "General",
|
||||
"language": "Language",
|
||||
"importExport": "Import/Export Config",
|
||||
"exportConfig": "Export Config to File",
|
||||
"selectConfigFile": "Select Config File",
|
||||
"import": "Import",
|
||||
"importing": "Importing...",
|
||||
"importSuccess": "Import Successful!",
|
||||
"importFailed": "Import Failed",
|
||||
"configExported": "Config exported to:",
|
||||
"exportFailed": "Export failed",
|
||||
"selectFileFailed": "Failed to select file",
|
||||
"configCorrupted": "Config file may be corrupted or invalid",
|
||||
"backupId": "Backup ID",
|
||||
"autoReload": "Data will refresh automatically in 2 seconds...",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"windowBehavior": "Window Behavior",
|
||||
|
||||
@@ -61,6 +61,19 @@
|
||||
"title": "设置",
|
||||
"general": "通用",
|
||||
"language": "界面语言",
|
||||
"importExport": "导入导出配置",
|
||||
"exportConfig": "导出配置到文件",
|
||||
"selectConfigFile": "选择配置文件",
|
||||
"import": "导入",
|
||||
"importing": "导入中...",
|
||||
"importSuccess": "导入成功!",
|
||||
"importFailed": "导入失败",
|
||||
"configExported": "配置已导出到:",
|
||||
"exportFailed": "导出失败",
|
||||
"selectFileFailed": "选择文件失败",
|
||||
"configCorrupted": "配置文件可能已损坏或格式不正确",
|
||||
"backupId": "备份ID",
|
||||
"autoReload": "数据将在2秒后自动刷新...",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"windowBehavior": "窗口行为",
|
||||
|
||||
@@ -139,40 +139,22 @@ export const tauriAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
// 获取 Claude Code 配置状态
|
||||
getClaudeConfigStatus: async (): Promise<ConfigStatus> => {
|
||||
try {
|
||||
return await invoke("get_claude_config_status");
|
||||
} catch (error) {
|
||||
console.error("获取配置状态失败:", error);
|
||||
return {
|
||||
exists: false,
|
||||
path: "",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 获取应用配置状态(通用)
|
||||
getConfigStatus: async (app?: AppType): Promise<ConfigStatus> => {
|
||||
try {
|
||||
return await invoke("get_config_status", { app_type: app, app });
|
||||
} catch (error) {
|
||||
console.error("获取配置状态失败:", error);
|
||||
return {
|
||||
exists: false,
|
||||
path: "",
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 打开配置文件夹
|
||||
// 打开配置目录(按应用类型)
|
||||
openConfigFolder: async (app?: AppType): Promise<void> => {
|
||||
try {
|
||||
await invoke("open_config_folder", { app_type: app, app });
|
||||
} catch (error) {
|
||||
console.error("打开配置文件夹失败:", error);
|
||||
console.error("打开配置目录失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// 选择配置目录(可选默认路径)
|
||||
selectConfigDirectory: async (defaultPath?: string): Promise<string | null> => {
|
||||
try {
|
||||
return await invoke("pick_directory", { defaultPath });
|
||||
} catch (error) {
|
||||
console.error("选择配置目录失败:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -188,47 +170,20 @@ export const tauriAPI = {
|
||||
// 更新托盘菜单
|
||||
updateTrayMenu: async (): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke("update_tray_menu");
|
||||
return await invoke<boolean>("update_tray_menu");
|
||||
} catch (error) {
|
||||
console.error("更新托盘菜单失败:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 监听供应商切换事件
|
||||
onProviderSwitched: async (
|
||||
callback: (data: { appType: string; providerId: string }) => void,
|
||||
): Promise<UnlistenFn> => {
|
||||
return await listen("provider-switched", (event) => {
|
||||
callback(event.payload as { appType: string; providerId: string });
|
||||
});
|
||||
},
|
||||
|
||||
// 选择配置目录
|
||||
selectConfigDirectory: async (
|
||||
defaultPath?: string,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const sanitized =
|
||||
defaultPath && defaultPath.trim() !== ""
|
||||
? defaultPath
|
||||
: undefined;
|
||||
return await invoke<string | null>("pick_directory", {
|
||||
defaultPath: sanitized,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("选择配置目录失败:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取设置
|
||||
// 获取应用设置
|
||||
getSettings: async (): Promise<Settings> => {
|
||||
try {
|
||||
return await invoke("get_settings");
|
||||
} catch (error) {
|
||||
console.error("获取设置失败:", error);
|
||||
return { showInTray: true, minimizeToTrayOnClose: true };
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -320,6 +275,7 @@ export const tauriAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
// ours: 第三方/自定义供应商——测速与端点管理
|
||||
// 第三方/自定义供应商:批量测试端点延迟
|
||||
testApiEndpoints: async (
|
||||
urls: string[],
|
||||
@@ -423,6 +379,70 @@ export const tauriAPI = {
|
||||
// 不抛出错误,因为这不是关键操作
|
||||
}
|
||||
},
|
||||
|
||||
// theirs: 导入导出与文件对话框
|
||||
// 导出配置到文件
|
||||
exportConfigToFile: async (filePath: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
filePath: string;
|
||||
}> => {
|
||||
try {
|
||||
return await invoke("export_config_to_file", { filePath });
|
||||
} catch (error) {
|
||||
throw new Error(`导出配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 从文件导入配置
|
||||
importConfigFromFile: async (filePath: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
backupId?: string;
|
||||
}> => {
|
||||
try {
|
||||
return await invoke("import_config_from_file", { filePath });
|
||||
} catch (error) {
|
||||
throw new Error(`导入配置失败: ${String(error)}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 保存文件对话框
|
||||
saveFileDialog: async (defaultName: string): Promise<string | null> => {
|
||||
try {
|
||||
const result = await invoke<string | null>("save_file_dialog", { defaultName });
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("打开保存对话框失败:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 打开文件对话框
|
||||
openFileDialog: async (): Promise<string | null> => {
|
||||
try {
|
||||
const result = await invoke<string | null>("open_file_dialog");
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("打开文件对话框失败:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 监听供应商切换事件
|
||||
onProviderSwitched: async (
|
||||
callback: (data: { appType: string; providerId: string }) => void,
|
||||
): Promise<UnlistenFn> => {
|
||||
const unlisten = await listen("provider-switched", (event) => {
|
||||
try {
|
||||
// 事件 payload 形如 { appType: string, providerId: string }
|
||||
callback(event.payload as any);
|
||||
} catch (e) {
|
||||
console.error("处理 provider-switched 事件失败: ", e);
|
||||
}
|
||||
});
|
||||
return unlisten;
|
||||
},
|
||||
};
|
||||
|
||||
// 创建全局 API 对象,兼容现有代码
|
||||
|
||||
12
src/vite-env.d.ts
vendored
12
src/vite-env.d.ts
vendored
@@ -29,6 +29,18 @@ declare global {
|
||||
getClaudeConfigStatus: () => Promise<ConfigStatus>;
|
||||
getConfigStatus: (app?: AppType) => Promise<ConfigStatus>;
|
||||
getConfigDir: (app?: AppType) => Promise<string>;
|
||||
saveFileDialog: (defaultName: string) => Promise<string | null>;
|
||||
openFileDialog: () => Promise<string | null>;
|
||||
exportConfigToFile: (filePath: string) => Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
filePath: string;
|
||||
}>;
|
||||
importConfigFromFile: (filePath: string) => Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
backupId?: string;
|
||||
}>;
|
||||
selectConfigDirectory: (defaultPath?: string) => Promise<string | null>;
|
||||
openConfigFolder: (app?: AppType) => Promise<void>;
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user