From ef2e30401e8b8f6fbd5285afb896161719186390 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Fri, 8 Aug 2025 00:21:10 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BE=8E=E5=8C=96=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/claude_config.rs | 214 ++++++++++++++++++++++++++++ src/App.tsx | 20 +++ src/components/AnalyticsConsent.tsx | 20 ++- src/components/ClaudiaLogo.tsx | 89 +++++------- src/components/WelcomePage.tsx | 80 +++++------ src/locales/en/common.json | 6 +- src/locales/zh/common.json | 6 +- 7 files changed, 334 insertions(+), 101 deletions(-) create mode 100644 src-tauri/src/claude_config.rs diff --git a/src-tauri/src/claude_config.rs b/src-tauri/src/claude_config.rs new file mode 100644 index 0000000..9b9c7e6 --- /dev/null +++ b/src-tauri/src/claude_config.rs @@ -0,0 +1,214 @@ +use std::fs; +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use dirs::home_dir; +use crate::commands::relay_stations::RelayStation; + +/// Claude 配置文件结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClaudeConfig { + #[serde(default)] + pub env: ClaudeEnv, + #[serde(default)] + pub permissions: Option, + #[serde(default)] + pub model: Option, + #[serde(rename = "apiKeyHelper")] + pub api_key_helper: Option, + #[serde(flatten)] + pub other: Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ClaudeEnv { + #[serde(rename = "ANTHROPIC_AUTH_TOKEN")] + pub anthropic_auth_token: Option, + #[serde(rename = "ANTHROPIC_BASE_URL")] + pub anthropic_base_url: Option, + #[serde(rename = "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")] + pub disable_nonessential_traffic: Option, + #[serde(flatten)] + pub other: Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ClaudePermissions { + #[serde(default)] + pub allow: Vec, + #[serde(default)] + pub deny: Vec, +} + +/// 获取 Claude 配置文件路径 +pub fn get_claude_config_path() -> Result { + let home = home_dir().ok_or_else(|| "无法获取主目录".to_string())?; + Ok(home.join(".claude").join("settings.json")) +} + +/// 获取配置备份文件路径 +pub fn get_config_backup_path() -> Result { + let home = home_dir().ok_or_else(|| "无法获取主目录".to_string())?; + Ok(home.join(".claude").join("settings.backup.json")) +} + +/// 读取 Claude 配置文件 +pub fn read_claude_config() -> Result { + let config_path = get_claude_config_path()?; + + if !config_path.exists() { + // 如果配置文件不存在,创建默认配置 + return Ok(ClaudeConfig { + env: ClaudeEnv::default(), + permissions: Some(ClaudePermissions::default()), + model: None, + api_key_helper: None, + other: json!({}), + }); + } + + let content = fs::read_to_string(&config_path) + .map_err(|e| format!("读取配置文件失败: {}", e))?; + + serde_json::from_str(&content) + .map_err(|e| format!("解析配置文件失败: {}", e)) +} + +/// 写入 Claude 配置文件 +pub fn write_claude_config(config: &ClaudeConfig) -> Result<(), String> { + let config_path = get_claude_config_path()?; + + // 确保目录存在 + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("创建配置目录失败: {}", e))?; + } + + let content = serde_json::to_string_pretty(config) + .map_err(|e| format!("序列化配置失败: {}", e))?; + + fs::write(&config_path, content) + .map_err(|e| format!("写入配置文件失败: {}", e)) +} + +/// 备份当前配置 +pub fn backup_claude_config() -> Result<(), String> { + let config_path = get_claude_config_path()?; + let backup_path = get_config_backup_path()?; + + if config_path.exists() { + fs::copy(&config_path, &backup_path) + .map_err(|e| format!("备份配置文件失败: {}", e))?; + } + + Ok(()) +} + +/// 恢复配置备份 +pub fn restore_claude_config() -> Result<(), String> { + let config_path = get_claude_config_path()?; + let backup_path = get_config_backup_path()?; + + if !backup_path.exists() { + return Err("备份文件不存在".to_string()); + } + + fs::copy(&backup_path, &config_path) + .map_err(|e| format!("恢复配置文件失败: {}", e))?; + + Ok(()) +} + +/// 根据中转站配置更新 Claude 配置 +pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), String> { + // 先备份当前配置 + backup_claude_config()?; + + // 读取当前配置 + let mut config = read_claude_config()?; + + // 更新 API URL + config.env.anthropic_base_url = Some(station.api_url.clone()); + + // 更新 API Token + config.env.anthropic_auth_token = Some(station.system_token.clone()); + + // 将中转站的 token 也设置到 apiKeyHelper + // 格式:echo 'token' + config.api_key_helper = Some(format!("echo '{}'", station.system_token)); + + // 如果是自定义适配器,可能需要特殊处理 + match station.adapter.as_str() { + "newapi" | "oneapi" => { + // NewAPI 和 OneAPI 兼容 OpenAI 格式,不需要特殊处理 + } + "yourapi" => { + // YourAPI 可能需要特殊的路径格式 + if !station.api_url.ends_with("/v1") { + config.env.anthropic_base_url = Some(format!("{}/v1", station.api_url)); + } + } + "custom" => { + // 自定义适配器,使用原始配置 + } + _ => {} + } + + // 写入更新后的配置 + write_claude_config(&config)?; + + log::info!("已将中转站 {} 的配置应用到 Claude 配置文件", station.name); + Ok(()) +} + +/// 清除中转站配置(恢复默认) +pub fn clear_relay_station_from_config() -> Result<(), String> { + // 尝试从备份恢复原始的配置 + let backup_config = if let Ok(backup_path) = get_config_backup_path() { + if backup_path.exists() { + let content = fs::read_to_string(&backup_path).ok(); + content.and_then(|c| serde_json::from_str::(&c).ok()) + } else { + None + } + } else { + None + }; + + // 读取当前配置 + let mut config = read_claude_config()?; + + // 清除 API URL 和 Token + config.env.anthropic_base_url = None; + config.env.anthropic_auth_token = None; + + // 恢复原始的 apiKeyHelper(如果有备份的话) + if let Some(backup) = backup_config { + config.api_key_helper = backup.api_key_helper; + // 如果备份中有 ANTHROPIC_AUTH_TOKEN,也恢复它 + if backup.env.anthropic_auth_token.is_some() { + config.env.anthropic_auth_token = backup.env.anthropic_auth_token; + } + } else { + // 如果没有备份,清除 apiKeyHelper + config.api_key_helper = None; + } + + // 写入更新后的配置 + write_claude_config(&config)?; + + log::info!("已清除 Claude 配置文件中的中转站设置"); + Ok(()) +} + +/// 获取当前配置中的 API URL +pub fn get_current_api_url() -> Result, String> { + let config = read_claude_config()?; + Ok(config.env.anthropic_base_url) +} + +/// 获取当前配置中的 API Token +pub fn get_current_api_token() -> Result, String> { + let config = read_claude_config()?; + Ok(config.env.anthropic_auth_token) +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 39c563d..c9a13fd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,6 +29,7 @@ import { useAppLifecycle, useTrackEvent } from "@/hooks"; import { useTranslation } from "@/hooks/useTranslation"; import { WelcomePage } from "@/components/WelcomePage"; import RelayStationManager from "@/components/RelayStationManager"; +import i18n from "@/lib/i18n"; type View = | "welcome" @@ -75,6 +76,25 @@ function AppContent() { const [hasTrackedFirstChat] = useState(false); // const [hasTrackedFirstAgent] = useState(false); + // Initialize backend language on app startup + useEffect(() => { + const initializeBackendLanguage = async () => { + try { + // Get the current frontend language + const frontendLang = i18n.language; + // Map to backend format + const backendLocale = frontendLang === 'zh' ? 'zh-CN' : 'en-US'; + // Sync to backend + await api.setLanguage(backendLocale); + console.log('Backend language initialized to:', backendLocale); + } catch (error) { + console.error('Failed to initialize backend language:', error); + } + }; + + initializeBackendLanguage(); + }, []); // Run once on app startup + // Track when user reaches different journey stages useEffect(() => { if (view === "projects" && projects.length > 0 && !hasTrackedFirstChat) { diff --git a/src/components/AnalyticsConsent.tsx b/src/components/AnalyticsConsent.tsx index 98e146e..a2977fb 100644 --- a/src/components/AnalyticsConsent.tsx +++ b/src/components/AnalyticsConsent.tsx @@ -159,9 +159,17 @@ export const AnalyticsConsentBanner: React.FC = ({ const checkConsent = async () => { if (hasChecked) return; + // Check if we've already shown the consent dialog before + const hasShownBefore = localStorage.getItem('claudia-analytics-consent-shown'); + if (hasShownBefore === 'true') { + setHasChecked(true); + return; + } + await analytics.initialize(); const settings = analytics.getSettings(); + // Only show if user hasn't made a decision yet if (!settings?.hasConsented) { setVisible(true); } @@ -175,11 +183,21 @@ export const AnalyticsConsentBanner: React.FC = ({ const handleAccept = async () => { await analytics.enable(); + // Mark that we've shown the consent dialog + localStorage.setItem('claudia-analytics-consent-shown', 'true'); setVisible(false); }; const handleDecline = async () => { await analytics.disable(); + // Mark that we've shown the consent dialog + localStorage.setItem('claudia-analytics-consent-shown', 'true'); + setVisible(false); + }; + + const handleClose = () => { + // Even if they close without choosing, mark as shown + localStorage.setItem('claudia-analytics-consent-shown', 'true'); setVisible(false); }; @@ -223,7 +241,7 @@ export const AnalyticsConsentBanner: React.FC = ({ diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 498c0a2..538d290 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -5,7 +5,7 @@ "ccProjects": "CC Projects", "browseClaudeCodeSessions": "Browse your Claude Code sessions", "newClaudeCodeSession": "New Claude Code session", - "ccAgents": "CC Agents", + "ccAgents": "Agent Management", "mcpServers": "MCP Servers", "manageMcpServers": "Manage Model Context Protocol servers", "app": { @@ -38,7 +38,7 @@ }, "navigation": { "projects": "CC Projects", - "agents": "CC Agents", + "agents": "Agent Management", "settings": "Settings", "usage": "Usage Dashboard", "mcp": "MCP Manager", @@ -74,7 +74,7 @@ "sessionHistory": "Session History" }, "agents": { - "title": "CC Agents", + "title": "Agent Management", "newAgent": "New Agent", "createAgent": "Create Agent", "editAgent": "Edit Agent", diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index e2e03fa..cce958d 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -5,7 +5,7 @@ "ccProjects": "Claude Code 项目", "browseClaudeCodeSessions": "浏览您的 Claude Code 会话", "newClaudeCodeSession": "新建 Claude Code 会话", - "ccAgents": "CC 智能体", + "ccAgents": "Agent 管理", "mcpServers": "MCP 服务器", "manageMcpServers": "管理模型上下文协议服务器", "app": { @@ -35,7 +35,7 @@ }, "navigation": { "projects": "Claude Code 项目", - "agents": "CC 智能体", + "agents": "Agent 管理", "settings": "设置", "usage": "用量仪表板", "mcp": "MCP 管理器", @@ -71,7 +71,7 @@ "sessionHistory": "会话历史" }, "agents": { - "title": "CC 智能体", + "title": "Agent 管理", "newAgent": "新建智能体", "createAgent": "创建智能体", "editAgent": "编辑智能体",