添加Claude和Codex环境变量检查 (#242)
* feat(env): add environment variable conflict detection and management 实现了系统环境变量冲突检测与管理功能: 核心功能: - 自动检测会影响 Claude/Codex 的系统环境变量 - 支持 Windows 注册表和 Unix shell 配置文件检测 - 提供可视化的环境变量冲突警告横幅 - 支持批量选择和删除环境变量 - 删除前自动备份,支持后续恢复 技术实现: - Rust 后端: 跨平台环境变量检测与管理 - React 前端: EnvWarningBanner 组件交互界面 - 国际化支持: 中英文界面 - 类型安全: 完整的 TypeScript 类型定义 * refactor(env): remove unused imports and function Remove unused HashMap and PathBuf imports, and delete the unused get_source_description function to clean up the code.
This commit is contained in:
@@ -77,5 +77,6 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
13
src-tauri/Cargo.lock
generated
13
src-tauri/Cargo.lock
generated
@@ -625,6 +625,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.8.2",
|
"toml 0.8.2",
|
||||||
"toml_edit 0.22.27",
|
"toml_edit 0.22.27",
|
||||||
|
"winreg 0.52.0",
|
||||||
"zip 2.4.2",
|
"zip 2.4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1109,7 +1110,7 @@ dependencies = [
|
|||||||
"rustc_version",
|
"rustc_version",
|
||||||
"toml 0.9.7",
|
"toml 0.9.7",
|
||||||
"vswhom",
|
"vswhom",
|
||||||
"winreg",
|
"winreg 0.55.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6285,6 +6286,16 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.55.0"
|
version = "0.55.0"
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ tempfile = "3"
|
|||||||
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
winreg = "0.52"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2 = "0.5"
|
objc2 = "0.5"
|
||||||
objc2-app-kit = { version = "0.2", features = ["NSColor"] }
|
objc2-app-kit = { version = "0.2", features = ["NSColor"] }
|
||||||
|
|||||||
20
src-tauri/src/commands/env.rs
Normal file
20
src-tauri/src/commands/env.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use crate::services::env_checker::{check_env_conflicts as check_conflicts, EnvConflict};
|
||||||
|
use crate::services::env_manager::{delete_env_vars as delete_vars, restore_from_backup, BackupInfo};
|
||||||
|
|
||||||
|
/// Check environment variable conflicts for a specific app
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn check_env_conflicts(app: String) -> Result<Vec<EnvConflict>, String> {
|
||||||
|
check_conflicts(&app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete environment variables with backup
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn delete_env_vars(conflicts: Vec<EnvConflict>) -> Result<BackupInfo, String> {
|
||||||
|
delete_vars(conflicts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore environment variables from backup file
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn restore_env_backup(backup_path: String) -> Result<(), String> {
|
||||||
|
restore_from_backup(backup_path)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod env;
|
||||||
mod import_export;
|
mod import_export;
|
||||||
mod mcp;
|
mod mcp;
|
||||||
mod misc;
|
mod misc;
|
||||||
@@ -11,6 +12,7 @@ mod settings;
|
|||||||
pub mod skill;
|
pub mod skill;
|
||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
|
pub use env::*;
|
||||||
pub use import_export::*;
|
pub use import_export::*;
|
||||||
pub use mcp::*;
|
pub use mcp::*;
|
||||||
pub use misc::*;
|
pub use misc::*;
|
||||||
|
|||||||
@@ -586,6 +586,10 @@ pub fn run() {
|
|||||||
commands::open_file_dialog,
|
commands::open_file_dialog,
|
||||||
commands::sync_current_providers_live,
|
commands::sync_current_providers_live,
|
||||||
update_tray_menu,
|
update_tray_menu,
|
||||||
|
// Environment variable management
|
||||||
|
commands::check_env_conflicts,
|
||||||
|
commands::delete_env_vars,
|
||||||
|
commands::restore_env_backup,
|
||||||
// Skill management
|
// Skill management
|
||||||
commands::get_skills,
|
commands::get_skills,
|
||||||
commands::install_skill,
|
commands::install_skill,
|
||||||
|
|||||||
161
src-tauri/src/services/env_checker.rs
Normal file
161
src-tauri/src/services/env_checker.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EnvConflict {
|
||||||
|
pub var_name: String,
|
||||||
|
pub var_value: String,
|
||||||
|
pub source_type: String, // "system" | "file"
|
||||||
|
pub source_path: String, // Registry path or file path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use winreg::enums::*;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
/// Check environment variables for conflicts
|
||||||
|
pub fn check_env_conflicts(app: &str) -> Result<Vec<EnvConflict>, String> {
|
||||||
|
let keywords = get_keywords_for_app(app);
|
||||||
|
let mut conflicts = Vec::new();
|
||||||
|
|
||||||
|
// Check system environment variables
|
||||||
|
conflicts.extend(check_system_env(&keywords)?);
|
||||||
|
|
||||||
|
// Check shell configuration files (Unix only)
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
conflicts.extend(check_shell_configs(&keywords)?);
|
||||||
|
|
||||||
|
Ok(conflicts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get relevant keywords for each app
|
||||||
|
fn get_keywords_for_app(app: &str) -> Vec<&str> {
|
||||||
|
match app.to_lowercase().as_str() {
|
||||||
|
"claude" => vec!["ANTHROPIC"],
|
||||||
|
"codex" => vec!["OPENAI"],
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check system environment variables (Windows Registry or Unix env)
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||||
|
let mut conflicts = Vec::new();
|
||||||
|
|
||||||
|
// Check HKEY_CURRENT_USER\Environment
|
||||||
|
if let Ok(hkcu) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") {
|
||||||
|
for (name, value) in hkcu.enum_values().filter_map(Result::ok) {
|
||||||
|
if keywords.iter().any(|k| name.to_uppercase().contains(k)) {
|
||||||
|
if let Ok(val) = value.to_string() {
|
||||||
|
conflicts.push(EnvConflict {
|
||||||
|
var_name: name.clone(),
|
||||||
|
var_value: val,
|
||||||
|
source_type: "system".to_string(),
|
||||||
|
source_path: "HKEY_CURRENT_USER\\Environment".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
|
||||||
|
if let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE)
|
||||||
|
.open_subkey("SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment")
|
||||||
|
{
|
||||||
|
for (name, value) in hklm.enum_values().filter_map(Result::ok) {
|
||||||
|
if keywords.iter().any(|k| name.to_uppercase().contains(k)) {
|
||||||
|
if let Ok(val) = value.to_string() {
|
||||||
|
conflicts.push(EnvConflict {
|
||||||
|
var_name: name.clone(),
|
||||||
|
var_value: val,
|
||||||
|
source_type: "system".to_string(),
|
||||||
|
source_path: "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(conflicts)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn check_system_env(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||||
|
let mut conflicts = Vec::new();
|
||||||
|
|
||||||
|
// Check current process environment
|
||||||
|
for (key, value) in std::env::vars() {
|
||||||
|
if keywords.iter().any(|k| key.to_uppercase().contains(k)) {
|
||||||
|
conflicts.push(EnvConflict {
|
||||||
|
var_name: key,
|
||||||
|
var_value: value,
|
||||||
|
source_type: "system".to_string(),
|
||||||
|
source_path: "Process Environment".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(conflicts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check shell configuration files for environment variable exports (Unix only)
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn check_shell_configs(keywords: &[&str]) -> Result<Vec<EnvConflict>, String> {
|
||||||
|
let mut conflicts = Vec::new();
|
||||||
|
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||||
|
let config_files = vec![
|
||||||
|
format!("{}/.bashrc", home),
|
||||||
|
format!("{}/.bash_profile", home),
|
||||||
|
format!("{}/.zshrc", home),
|
||||||
|
format!("{}/.zprofile", home),
|
||||||
|
format!("{}/.profile", home),
|
||||||
|
"/etc/profile".to_string(),
|
||||||
|
"/etc/bashrc".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for file_path in config_files {
|
||||||
|
if let Ok(content) = fs::read_to_string(&file_path) {
|
||||||
|
// Parse lines for export statements
|
||||||
|
for (line_num, line) in content.lines().enumerate() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
|
||||||
|
// Match patterns like: export VAR=value or VAR=value
|
||||||
|
if trimmed.starts_with("export ") || (!trimmed.starts_with('#') && trimmed.contains('=')) {
|
||||||
|
let export_line = trimmed.strip_prefix("export ").unwrap_or(trimmed);
|
||||||
|
|
||||||
|
if let Some(eq_pos) = export_line.find('=') {
|
||||||
|
let var_name = export_line[..eq_pos].trim();
|
||||||
|
let var_value = export_line[eq_pos + 1..].trim();
|
||||||
|
|
||||||
|
// Check if variable name contains any keyword
|
||||||
|
if keywords.iter().any(|k| var_name.to_uppercase().contains(k)) {
|
||||||
|
conflicts.push(EnvConflict {
|
||||||
|
var_name: var_name.to_string(),
|
||||||
|
var_value: var_value.trim_matches('"').trim_matches('\'').to_string(),
|
||||||
|
source_type: "file".to_string(),
|
||||||
|
source_path: format!("{}:{}", file_path, line_num + 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(conflicts)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_keywords() {
|
||||||
|
assert_eq!(get_keywords_for_app("claude"), vec!["ANTHROPIC"]);
|
||||||
|
assert_eq!(get_keywords_for_app("codex"), vec!["OPENAI"]);
|
||||||
|
assert_eq!(get_keywords_for_app("unknown"), Vec::<&str>::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
236
src-tauri/src/services/env_manager.rs
Normal file
236
src-tauri/src/services/env_manager.rs
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
use super::env_checker::EnvConflict;
|
||||||
|
use chrono::Utc;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use winreg::enums::*;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BackupInfo {
|
||||||
|
pub backup_path: String,
|
||||||
|
pub timestamp: String,
|
||||||
|
pub conflicts: Vec<EnvConflict>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete environment variables with automatic backup
|
||||||
|
pub fn delete_env_vars(conflicts: Vec<EnvConflict>) -> Result<BackupInfo, String> {
|
||||||
|
// Step 1: Create backup
|
||||||
|
let backup_info = create_backup(&conflicts)?;
|
||||||
|
|
||||||
|
// Step 2: Delete variables
|
||||||
|
for conflict in &conflicts {
|
||||||
|
match delete_single_env(conflict) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
// If deletion fails, we keep the backup but return error
|
||||||
|
return Err(format!(
|
||||||
|
"删除环境变量失败: {}. 备份已保存到: {}",
|
||||||
|
e, backup_info.backup_path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(backup_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create backup file before deletion
|
||||||
|
fn create_backup(conflicts: &[EnvConflict]) -> Result<BackupInfo, String> {
|
||||||
|
// Get backup directory
|
||||||
|
let backup_dir = get_backup_dir()?;
|
||||||
|
fs::create_dir_all(&backup_dir).map_err(|e| format!("创建备份目录失败: {}", e))?;
|
||||||
|
|
||||||
|
// Generate backup file name with timestamp
|
||||||
|
let timestamp = Utc::now().format("%Y%m%d_%H%M%S").to_string();
|
||||||
|
let backup_file = backup_dir.join(format!("env-backup-{}.json", timestamp));
|
||||||
|
|
||||||
|
// Create backup data
|
||||||
|
let backup_info = BackupInfo {
|
||||||
|
backup_path: backup_file.to_string_lossy().to_string(),
|
||||||
|
timestamp: timestamp.clone(),
|
||||||
|
conflicts: conflicts.to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write backup file
|
||||||
|
let json = serde_json::to_string_pretty(&backup_info)
|
||||||
|
.map_err(|e| format!("序列化备份数据失败: {}", e))?;
|
||||||
|
|
||||||
|
fs::write(&backup_file, json).map_err(|e| format!("写入备份文件失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(backup_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get backup directory path
|
||||||
|
fn get_backup_dir() -> Result<PathBuf, String> {
|
||||||
|
let home = dirs::home_dir().ok_or("无法获取用户主目录")?;
|
||||||
|
Ok(home.join(".cc-switch").join("backups"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a single environment variable
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn delete_single_env(conflict: &EnvConflict) -> Result<(), String> {
|
||||||
|
match conflict.source_type.as_str() {
|
||||||
|
"system" => {
|
||||||
|
if conflict.source_path.contains("HKEY_CURRENT_USER") {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER)
|
||||||
|
.open_subkey_with_flags("Environment", KEY_ALL_ACCESS)
|
||||||
|
.map_err(|e| format!("打开注册表失败: {}", e))?;
|
||||||
|
|
||||||
|
hkcu.delete_value(&conflict.var_name)
|
||||||
|
.map_err(|e| format!("删除注册表项失败: {}", e))?;
|
||||||
|
} else if conflict.source_path.contains("HKEY_LOCAL_MACHINE") {
|
||||||
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE)
|
||||||
|
.open_subkey_with_flags(
|
||||||
|
"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
|
||||||
|
KEY_ALL_ACCESS,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("打开系统注册表失败 (需要管理员权限): {}", e))?;
|
||||||
|
|
||||||
|
hklm.delete_value(&conflict.var_name)
|
||||||
|
.map_err(|e| format!("删除系统注册表项失败: {}", e))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
"file" => Err("Windows 系统不应该有文件类型的环境变量".to_string()),
|
||||||
|
_ => Err(format!("未知的环境变量来源类型: {}", conflict.source_type)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn delete_single_env(conflict: &EnvConflict) -> Result<(), String> {
|
||||||
|
match conflict.source_type.as_str() {
|
||||||
|
"file" => {
|
||||||
|
// Parse file path and line number from source_path (format: "path:line")
|
||||||
|
let parts: Vec<&str> = conflict.source_path.split(':').collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return Err("无效的文件路径格式".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = parts[0];
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
let content = fs::read_to_string(file_path)
|
||||||
|
.map_err(|e| format!("读取文件失败 {}: {}", file_path, e))?;
|
||||||
|
|
||||||
|
// Filter out the line containing the environment variable
|
||||||
|
let new_content: Vec<String> = content
|
||||||
|
.lines()
|
||||||
|
.filter(|line| {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
let export_line = trimmed.strip_prefix("export ").unwrap_or(trimmed);
|
||||||
|
|
||||||
|
// Check if this line sets the target variable
|
||||||
|
if let Some(eq_pos) = export_line.find('=') {
|
||||||
|
let var_name = export_line[..eq_pos].trim();
|
||||||
|
var_name != conflict.var_name
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
fs::write(file_path, new_content.join("\n"))
|
||||||
|
.map_err(|e| format!("写入文件失败 {}: {}", file_path, e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
"system" => {
|
||||||
|
// On Unix, we can't directly delete process environment variables
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(format!("未知的环境变量来源类型: {}", conflict.source_type)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore environment variables from backup
|
||||||
|
pub fn restore_from_backup(backup_path: String) -> Result<(), String> {
|
||||||
|
// Read backup file
|
||||||
|
let content =
|
||||||
|
fs::read_to_string(&backup_path).map_err(|e| format!("读取备份文件失败: {}", e))?;
|
||||||
|
|
||||||
|
let backup_info: BackupInfo = serde_json::from_str(&content)
|
||||||
|
.map_err(|e| format!("解析备份文件失败: {}", e))?;
|
||||||
|
|
||||||
|
// Restore each variable
|
||||||
|
for conflict in &backup_info.conflicts {
|
||||||
|
restore_single_env(conflict)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore a single environment variable
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn restore_single_env(conflict: &EnvConflict) -> Result<(), String> {
|
||||||
|
match conflict.source_type.as_str() {
|
||||||
|
"system" => {
|
||||||
|
if conflict.source_path.contains("HKEY_CURRENT_USER") {
|
||||||
|
let (hkcu, _) = RegKey::predef(HKEY_CURRENT_USER)
|
||||||
|
.create_subkey("Environment")
|
||||||
|
.map_err(|e| format!("打开注册表失败: {}", e))?;
|
||||||
|
|
||||||
|
hkcu.set_value(&conflict.var_name, &conflict.var_value)
|
||||||
|
.map_err(|e| format!("恢复注册表项失败: {}", e))?;
|
||||||
|
} else if conflict.source_path.contains("HKEY_LOCAL_MACHINE") {
|
||||||
|
let (hklm, _) = RegKey::predef(HKEY_LOCAL_MACHINE)
|
||||||
|
.create_subkey(
|
||||||
|
"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("打开系统注册表失败 (需要管理员权限): {}", e))?;
|
||||||
|
|
||||||
|
hklm.set_value(&conflict.var_name, &conflict.var_value)
|
||||||
|
.map_err(|e| format!("恢复系统注册表项失败: {}", e))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(format!("无法恢复类型为 {} 的环境变量", conflict.source_type)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn restore_single_env(conflict: &EnvConflict) -> Result<(), String> {
|
||||||
|
match conflict.source_type.as_str() {
|
||||||
|
"file" => {
|
||||||
|
// Parse file path from source_path
|
||||||
|
let parts: Vec<&str> = conflict.source_path.split(':').collect();
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Err("无效的文件路径格式".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = parts[0];
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
let mut content = fs::read_to_string(file_path)
|
||||||
|
.map_err(|e| format!("读取文件失败 {}: {}", file_path, e))?;
|
||||||
|
|
||||||
|
// Append the environment variable line
|
||||||
|
let export_line = format!("\nexport {}={}", conflict.var_name, conflict.var_value);
|
||||||
|
content.push_str(&export_line);
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
fs::write(file_path, content)
|
||||||
|
.map_err(|e| format!("写入文件失败 {}: {}", file_path, e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(format!("无法恢复类型为 {} 的环境变量", conflict.source_type)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup_dir_creation() {
|
||||||
|
let backup_dir = get_backup_dir();
|
||||||
|
assert!(backup_dir.is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod env_checker;
|
||||||
|
pub mod env_manager;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
pub mod prompt;
|
pub mod prompt;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
|
|||||||
72
src/App.tsx
72
src/App.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Plus, Settings, Edit3 } from "lucide-react";
|
import { Plus, Settings, Edit3 } from "lucide-react";
|
||||||
import type { Provider } from "@/types";
|
import type { Provider } from "@/types";
|
||||||
|
import type { EnvConflict } from "@/types/env";
|
||||||
import { useProvidersQuery } from "@/lib/query";
|
import { useProvidersQuery } from "@/lib/query";
|
||||||
import {
|
import {
|
||||||
providersApi,
|
providersApi,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
type AppId,
|
type AppId,
|
||||||
type ProviderSwitchEvent,
|
type ProviderSwitchEvent,
|
||||||
} from "@/lib/api";
|
} from "@/lib/api";
|
||||||
|
import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env";
|
||||||
import { useProviderActions } from "@/hooks/useProviderActions";
|
import { useProviderActions } from "@/hooks/useProviderActions";
|
||||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||||
import { AppSwitcher } from "@/components/AppSwitcher";
|
import { AppSwitcher } from "@/components/AppSwitcher";
|
||||||
@@ -19,6 +21,7 @@ import { EditProviderDialog } from "@/components/providers/EditProviderDialog";
|
|||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
import { SettingsDialog } from "@/components/settings/SettingsDialog";
|
import { SettingsDialog } from "@/components/settings/SettingsDialog";
|
||||||
import { UpdateBadge } from "@/components/UpdateBadge";
|
import { UpdateBadge } from "@/components/UpdateBadge";
|
||||||
|
import { EnvWarningBanner } from "@/components/env/EnvWarningBanner";
|
||||||
import UsageScriptModal from "@/components/UsageScriptModal";
|
import UsageScriptModal from "@/components/UsageScriptModal";
|
||||||
import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel";
|
import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel";
|
||||||
import PromptPanel from "@/components/prompts/PromptPanel";
|
import PromptPanel from "@/components/prompts/PromptPanel";
|
||||||
@@ -45,6 +48,8 @@ function App() {
|
|||||||
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
|
const [editingProvider, setEditingProvider] = useState<Provider | null>(null);
|
||||||
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
|
const [usageProvider, setUsageProvider] = useState<Provider | null>(null);
|
||||||
const [confirmDelete, setConfirmDelete] = useState<Provider | null>(null);
|
const [confirmDelete, setConfirmDelete] = useState<Provider | null>(null);
|
||||||
|
const [envConflicts, setEnvConflicts] = useState<EnvConflict[]>([]);
|
||||||
|
const [showEnvBanner, setShowEnvBanner] = useState(false);
|
||||||
|
|
||||||
const { data, isLoading, refetch } = useProvidersQuery(activeApp);
|
const { data, isLoading, refetch } = useProvidersQuery(activeApp);
|
||||||
const providers = useMemo(() => data?.providers ?? {}, [data]);
|
const providers = useMemo(() => data?.providers ?? {}, [data]);
|
||||||
@@ -83,6 +88,52 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, [activeApp, refetch]);
|
}, [activeApp, refetch]);
|
||||||
|
|
||||||
|
// 应用启动时检测所有应用的环境变量冲突
|
||||||
|
useEffect(() => {
|
||||||
|
const checkEnvOnStartup = async () => {
|
||||||
|
try {
|
||||||
|
const allConflicts = await checkAllEnvConflicts();
|
||||||
|
const flatConflicts = Object.values(allConflicts).flat();
|
||||||
|
|
||||||
|
if (flatConflicts.length > 0) {
|
||||||
|
setEnvConflicts(flatConflicts);
|
||||||
|
setShowEnvBanner(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to check environment conflicts on startup:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkEnvOnStartup();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 切换应用时检测当前应用的环境变量冲突
|
||||||
|
useEffect(() => {
|
||||||
|
const checkEnvOnSwitch = async () => {
|
||||||
|
try {
|
||||||
|
const conflicts = await checkEnvConflicts(activeApp);
|
||||||
|
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
// 合并新检测到的冲突
|
||||||
|
setEnvConflicts((prev) => {
|
||||||
|
const existingKeys = new Set(
|
||||||
|
prev.map((c) => `${c.varName}:${c.sourcePath}`)
|
||||||
|
);
|
||||||
|
const newConflicts = conflicts.filter(
|
||||||
|
(c) => !existingKeys.has(`${c.varName}:${c.sourcePath}`)
|
||||||
|
);
|
||||||
|
return [...prev, ...newConflicts];
|
||||||
|
});
|
||||||
|
setShowEnvBanner(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to check environment conflicts on app switch:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkEnvOnSwitch();
|
||||||
|
}, [activeApp]);
|
||||||
|
|
||||||
// 打开网站链接
|
// 打开网站链接
|
||||||
const handleOpenWebsite = async (url: string) => {
|
const handleOpenWebsite = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -173,6 +224,27 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col bg-gray-50 dark:bg-gray-950">
|
<div className="flex h-screen flex-col bg-gray-50 dark:bg-gray-950">
|
||||||
|
{/* 环境变量警告横幅 */}
|
||||||
|
{showEnvBanner && envConflicts.length > 0 && (
|
||||||
|
<EnvWarningBanner
|
||||||
|
conflicts={envConflicts}
|
||||||
|
onDismiss={() => setShowEnvBanner(false)}
|
||||||
|
onDeleted={async () => {
|
||||||
|
// 删除后重新检测
|
||||||
|
try {
|
||||||
|
const allConflicts = await checkAllEnvConflicts();
|
||||||
|
const flatConflicts = Object.values(allConflicts).flat();
|
||||||
|
setEnvConflicts(flatConflicts);
|
||||||
|
if (flatConflicts.length === 0) {
|
||||||
|
setShowEnvBanner(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[App] Failed to re-check conflicts after deletion:", error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<header className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-4 dark:border-gray-800 dark:bg-gray-900">
|
<header className="flex-shrink-0 border-b border-gray-200 bg-white px-6 py-4 dark:border-gray-800 dark:bg-gray-900">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|||||||
271
src/components/env/EnvWarningBanner.tsx
vendored
Normal file
271
src/components/env/EnvWarningBanner.tsx
vendored
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AlertTriangle, ChevronDown, ChevronUp, X, Trash2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import type { EnvConflict } from "@/types/env";
|
||||||
|
import { deleteEnvVars } from "@/lib/api/env";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
interface EnvWarningBannerProps {
|
||||||
|
conflicts: EnvConflict[];
|
||||||
|
onDismiss: () => void;
|
||||||
|
onDeleted: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnvWarningBanner({
|
||||||
|
conflicts,
|
||||||
|
onDismiss,
|
||||||
|
onDeleted,
|
||||||
|
}: EnvWarningBannerProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [selectedConflicts, setSelectedConflicts] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
|
|
||||||
|
if (conflicts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelection = (key: string) => {
|
||||||
|
const newSelection = new Set(selectedConflicts);
|
||||||
|
if (newSelection.has(key)) {
|
||||||
|
newSelection.delete(key);
|
||||||
|
} else {
|
||||||
|
newSelection.add(key);
|
||||||
|
}
|
||||||
|
setSelectedConflicts(newSelection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSelectAll = () => {
|
||||||
|
if (selectedConflicts.size === conflicts.length) {
|
||||||
|
setSelectedConflicts(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedConflicts(
|
||||||
|
new Set(conflicts.map((c) => `${c.varName}:${c.sourcePath}`)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setShowConfirmDialog(false);
|
||||||
|
setIsDeleting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conflictsToDelete = conflicts.filter((c) =>
|
||||||
|
selectedConflicts.has(`${c.varName}:${c.sourcePath}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conflictsToDelete.length === 0) {
|
||||||
|
toast.warning(t("env.error.noSelection"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupInfo = await deleteEnvVars(conflictsToDelete);
|
||||||
|
|
||||||
|
toast.success(t("env.delete.success"), {
|
||||||
|
description: t("env.backup.location", {
|
||||||
|
path: backupInfo.backupPath,
|
||||||
|
}),
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空选择并通知父组件
|
||||||
|
setSelectedConflicts(new Set());
|
||||||
|
onDeleted();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除环境变量失败:", error);
|
||||||
|
toast.error(t("env.delete.error"), {
|
||||||
|
description: String(error),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSourceDescription = (conflict: EnvConflict): string => {
|
||||||
|
if (conflict.sourceType === "system") {
|
||||||
|
if (conflict.sourcePath.includes("HKEY_CURRENT_USER")) {
|
||||||
|
return t("env.source.userRegistry");
|
||||||
|
} else if (conflict.sourcePath.includes("HKEY_LOCAL_MACHINE")) {
|
||||||
|
return t("env.source.systemRegistry");
|
||||||
|
} else {
|
||||||
|
return t("env.source.systemEnv");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return conflict.sourcePath;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-b border-yellow-200 dark:border-yellow-900/50">
|
||||||
|
<div className="container mx-auto px-4 py-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-yellow-900 dark:text-yellow-100">
|
||||||
|
{t("env.warning.title")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-yellow-800 dark:text-yellow-200 mt-0.5">
|
||||||
|
{t("env.warning.description", { count: conflicts.length })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="text-yellow-900 dark:text-yellow-100 hover:bg-yellow-100 dark:hover:bg-yellow-900/50"
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<>
|
||||||
|
{t("env.actions.collapse")}
|
||||||
|
<ChevronUp className="h-4 w-4 ml-1" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{t("env.actions.expand")}
|
||||||
|
<ChevronDown className="h-4 w-4 ml-1" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onDismiss}
|
||||||
|
className="text-yellow-900 dark:text-yellow-100 hover:bg-yellow-100 dark:hover:bg-yellow-900/50"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
<div className="flex items-center gap-2 pb-2 border-b border-yellow-200 dark:border-yellow-900/50">
|
||||||
|
<Checkbox
|
||||||
|
id="select-all"
|
||||||
|
checked={selectedConflicts.size === conflicts.length}
|
||||||
|
onCheckedChange={toggleSelectAll}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="select-all"
|
||||||
|
className="text-sm font-medium text-yellow-900 dark:text-yellow-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("env.actions.selectAll")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-96 overflow-y-auto space-y-2">
|
||||||
|
{conflicts.map((conflict) => {
|
||||||
|
const key = `${conflict.varName}:${conflict.sourcePath}`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-start gap-3 p-3 bg-white dark:bg-gray-900 rounded-md border border-yellow-200 dark:border-yellow-900/50"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={key}
|
||||||
|
checked={selectedConflicts.has(key)}
|
||||||
|
onCheckedChange={() => toggleSelection(key)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<label
|
||||||
|
htmlFor={key}
|
||||||
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
{conflict.varName}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 break-all">
|
||||||
|
{t("env.field.value")}: {conflict.varValue}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||||
|
{t("env.field.source")}: {getSourceDescription(conflict)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-2 border-t border-yellow-200 dark:border-yellow-900/50">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedConflicts(new Set())}
|
||||||
|
disabled={selectedConflicts.size === 0}
|
||||||
|
className="text-yellow-900 dark:text-yellow-100 border-yellow-300 dark:border-yellow-800"
|
||||||
|
>
|
||||||
|
{t("env.actions.clearSelection")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowConfirmDialog(true)}
|
||||||
|
disabled={selectedConflicts.size === 0 || isDeleting}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
{isDeleting
|
||||||
|
? t("env.actions.deleting")
|
||||||
|
: t("env.actions.deleteSelected", {
|
||||||
|
count: selectedConflicts.size,
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||||
|
{t("env.confirm.title")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="space-y-2">
|
||||||
|
<p>{t("env.confirm.message", { count: selectedConflicts.size })}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("env.confirm.backupNotice")}
|
||||||
|
</p>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowConfirmDialog(false)}
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDelete}>
|
||||||
|
{t("env.confirm.confirm")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -608,6 +608,43 @@
|
|||||||
"deleteMessage": "Are you sure you want to delete prompt \"{{name}}\"?"
|
"deleteMessage": "Are you sure you want to delete prompt \"{{name}}\"?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"env": {
|
||||||
|
"warning": {
|
||||||
|
"title": "Environment Variable Conflicts Detected",
|
||||||
|
"description": "Found {{count}} environment variables that may override your configuration"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"expand": "View Details",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"selectAll": "Select All",
|
||||||
|
"clearSelection": "Clear Selection",
|
||||||
|
"deleteSelected": "Delete Selected ({{count}})",
|
||||||
|
"deleting": "Deleting..."
|
||||||
|
},
|
||||||
|
"field": {
|
||||||
|
"value": "Value",
|
||||||
|
"source": "Source"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"userRegistry": "User Environment Variable (Registry)",
|
||||||
|
"systemRegistry": "System Environment Variable (Registry)",
|
||||||
|
"systemEnv": "System Environment Variable"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"success": "Environment variables deleted successfully",
|
||||||
|
"error": "Failed to delete environment variables"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"location": "Backup location: {{path}}"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"title": "Confirm Delete Environment Variables",
|
||||||
|
"message": "Are you sure you want to delete {{count}} environment variable(s)?",
|
||||||
|
"backupNotice": "A backup will be created automatically before deletion. You can restore it later. Changes take effect after restarting the application or terminal.",
|
||||||
|
"confirm": "Confirm Delete"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"noSelection": "Please select environment variables to delete"
|
||||||
"skills": {
|
"skills": {
|
||||||
"manage": "Skills",
|
"manage": "Skills",
|
||||||
"title": "Claude Skills Management",
|
"title": "Claude Skills Management",
|
||||||
|
|||||||
@@ -608,6 +608,43 @@
|
|||||||
"deleteMessage": "确定要删除提示词 \"{{name}}\" 吗?"
|
"deleteMessage": "确定要删除提示词 \"{{name}}\" 吗?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"env": {
|
||||||
|
"warning": {
|
||||||
|
"title": "检测到系统环境变量冲突",
|
||||||
|
"description": "发现 {{count}} 个环境变量可能会覆盖您的配置"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"expand": "查看详情",
|
||||||
|
"collapse": "收起",
|
||||||
|
"selectAll": "全选",
|
||||||
|
"clearSelection": "取消选择",
|
||||||
|
"deleteSelected": "删除选中 ({{count}})",
|
||||||
|
"deleting": "删除中..."
|
||||||
|
},
|
||||||
|
"field": {
|
||||||
|
"value": "值",
|
||||||
|
"source": "来源"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"userRegistry": "用户环境变量 (注册表)",
|
||||||
|
"systemRegistry": "系统环境变量 (注册表)",
|
||||||
|
"systemEnv": "系统环境变量"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"success": "环境变量已成功删除",
|
||||||
|
"error": "删除环境变量失败"
|
||||||
|
},
|
||||||
|
"backup": {
|
||||||
|
"location": "备份位置: {{path}}"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"title": "确认删除环境变量",
|
||||||
|
"message": "确定要删除 {{count}} 个环境变量吗?",
|
||||||
|
"backupNotice": "删除前将自动备份,您可以稍后恢复。删除后需要重启应用或终端才能生效。",
|
||||||
|
"confirm": "确认删除"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"noSelection": "请选择要删除的环境变量"
|
||||||
"skills": {
|
"skills": {
|
||||||
"manage": "Skills",
|
"manage": "Skills",
|
||||||
"title": "Claude Skills 管理",
|
"title": "Claude Skills 管理",
|
||||||
|
|||||||
60
src/lib/api/env.ts
Normal file
60
src/lib/api/env.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { EnvConflict, BackupInfo } from "@/types/env";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境变量管理 API
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查指定应用的环境变量冲突
|
||||||
|
* @param appType 应用类型 ("claude" | "codex" | "gemini")
|
||||||
|
* @returns 环境变量冲突列表
|
||||||
|
*/
|
||||||
|
export async function checkEnvConflicts(
|
||||||
|
appType: string,
|
||||||
|
): Promise<EnvConflict[]> {
|
||||||
|
return invoke<EnvConflict[]>("check_env_conflicts", { app: appType });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定的环境变量 (会自动备份)
|
||||||
|
* @param conflicts 要删除的环境变量冲突列表
|
||||||
|
* @returns 备份信息
|
||||||
|
*/
|
||||||
|
export async function deleteEnvVars(
|
||||||
|
conflicts: EnvConflict[],
|
||||||
|
): Promise<BackupInfo> {
|
||||||
|
return invoke<BackupInfo>("delete_env_vars", { conflicts });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从备份文件恢复环境变量
|
||||||
|
* @param backupPath 备份文件路径
|
||||||
|
*/
|
||||||
|
export async function restoreEnvBackup(backupPath: string): Promise<void> {
|
||||||
|
return invoke<void>("restore_env_backup", { backupPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查所有应用的环境变量冲突
|
||||||
|
* @returns 按应用类型分组的环境变量冲突
|
||||||
|
*/
|
||||||
|
export async function checkAllEnvConflicts(): Promise<
|
||||||
|
Record<string, EnvConflict[]>
|
||||||
|
> {
|
||||||
|
const apps = ["claude", "codex", "gemini"];
|
||||||
|
const results: Record<string, EnvConflict[]> = {};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
apps.map(async (app) => {
|
||||||
|
try {
|
||||||
|
results[app] = await checkEnvConflicts(app);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`检查 ${app} 环境变量失败:`, error);
|
||||||
|
results[app] = [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
29
src/types/env.ts
Normal file
29
src/types/env.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 环境变量冲突检测相关类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境变量冲突信息
|
||||||
|
*/
|
||||||
|
export interface EnvConflict {
|
||||||
|
/** 环境变量名称 */
|
||||||
|
varName: string;
|
||||||
|
/** 环境变量的值 */
|
||||||
|
varValue: string;
|
||||||
|
/** 来源类型: "system" 表示系统环境变量, "file" 表示配置文件 */
|
||||||
|
sourceType: "system" | "file";
|
||||||
|
/** 来源路径 (注册表路径或文件路径:行号) */
|
||||||
|
sourcePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备份信息
|
||||||
|
*/
|
||||||
|
export interface BackupInfo {
|
||||||
|
/** 备份文件路径 */
|
||||||
|
backupPath: string;
|
||||||
|
/** 备份时间戳 */
|
||||||
|
timestamp: string;
|
||||||
|
/** 被备份的环境变量冲突列表 */
|
||||||
|
conflicts: EnvConflict[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user