添加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:
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)]
|
||||
|
||||
mod config;
|
||||
mod env;
|
||||
mod import_export;
|
||||
mod mcp;
|
||||
mod misc;
|
||||
@@ -11,6 +12,7 @@ mod settings;
|
||||
pub mod skill;
|
||||
|
||||
pub use config::*;
|
||||
pub use env::*;
|
||||
pub use import_export::*;
|
||||
pub use mcp::*;
|
||||
pub use misc::*;
|
||||
|
||||
@@ -586,6 +586,10 @@ pub fn run() {
|
||||
commands::open_file_dialog,
|
||||
commands::sync_current_providers_live,
|
||||
update_tray_menu,
|
||||
// Environment variable management
|
||||
commands::check_env_conflicts,
|
||||
commands::delete_env_vars,
|
||||
commands::restore_env_backup,
|
||||
// Skill management
|
||||
commands::get_skills,
|
||||
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 env_checker;
|
||||
pub mod env_manager;
|
||||
pub mod mcp;
|
||||
pub mod prompt;
|
||||
pub mod provider;
|
||||
|
||||
Reference in New Issue
Block a user