From d0973caf37c26524969ea91dd045c91348cc9666 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sun, 26 Oct 2025 03:16:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=96=87=E4=BB=B6=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/claude_config.rs | 132 ++++++++++++----------- src-tauri/src/commands/relay_stations.rs | 2 +- src/App.tsx | 2 +- src/components/RelayStationManager.tsx | 22 +++- src/components/TabContent.tsx | 2 +- src/components/TabManager.tsx | 2 +- src/main.tsx | 25 ++++- 7 files changed, 117 insertions(+), 70 deletions(-) diff --git a/src-tauri/src/claude_config.rs b/src-tauri/src/claude_config.rs index 04aed96..bd4a91f 100644 --- a/src-tauri/src/claude_config.rs +++ b/src-tauri/src/claude_config.rs @@ -7,7 +7,7 @@ use std::fs; use std::path::PathBuf; /// Claude 配置文件结构 -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ClaudeConfig { #[serde(default)] pub env: ClaudeEnv, @@ -24,7 +24,7 @@ pub struct ClaudeConfig { pub extra_fields: std::collections::HashMap, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct StatusLineConfig { #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub config_type: Option, @@ -181,27 +181,51 @@ pub fn restore_claude_config() -> Result<(), String> { Ok(()) } -/// 根据中转站配置更新 Claude 配置(仅更新 API 相关字段) +/// 根据中转站配置更新 Claude 配置(先恢复源文件,再应用配置) pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), String> { - // 先备份当前配置 - backup_claude_config()?; + log::info!("[CLAUDE_CONFIG] Applying relay station: {}", station.name); - // 读取当前配置 + // 第一步:确保源文件备份存在(如果不存在则创建) + let backup_path = get_config_backup_path()?; + let config_path = get_claude_config_path()?; + + if !backup_path.exists() { + if config_path.exists() { + log::info!("[CLAUDE_CONFIG] Creating source backup on first use"); + init_source_backup()?; + } else { + log::warn!("[CLAUDE_CONFIG] No source config found, will create default"); + } + } + + // 第二步:恢复源文件备份(确保使用干净的基准配置) + if backup_path.exists() { + log::info!("[CLAUDE_CONFIG] Restoring source config from backup"); + fs::copy(&backup_path, &config_path).map_err(|e| { + log::error!("[CLAUDE_CONFIG] Failed to restore source config: {}", e); + format!("恢复源配置文件失败: {}", e) + })?; + } + + // 第三步:读取恢复后的配置(现在是源文件或默认配置) let mut config = read_claude_config()?; - // 更新三个关键字段: + // 第四步:仅更新中转站相关字段,保留其他所有配置 // 1. ANTHROPIC_BASE_URL config.env.anthropic_base_url = Some(station.api_url.clone()); + log::info!("[CLAUDE_CONFIG] Set ANTHROPIC_BASE_URL: {}", station.api_url); // 2. ANTHROPIC_AUTH_TOKEN config.env.anthropic_auth_token = Some(station.system_token.clone()); + log::info!("[CLAUDE_CONFIG] Set ANTHROPIC_AUTH_TOKEN"); // 3. apiKeyHelper - 设置为 echo 格式 config.api_key_helper = Some(format!("echo '{}'", station.system_token)); + log::info!("[CLAUDE_CONFIG] Set apiKeyHelper"); - // 处理 adapter_config 中的自定义字段 + // 第五步:处理 adapter_config 中的自定义字段(合并而非覆盖) if let Some(ref adapter_config) = station.adapter_config { - log::info!("[CLAUDE_CONFIG] Applying adapter_config: {:?}", adapter_config); + log::info!("[CLAUDE_CONFIG] Merging adapter_config: {:?}", adapter_config); // 遍历 adapter_config 中的所有字段 for (key, value) in adapter_config { @@ -222,70 +246,52 @@ pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), Strin } } - // 如果是特定适配器,可能需要特殊处理 URL 格式 - match station.adapter.as_str() { - "packycode" => { - // PackyCode 使用原始配置,不做特殊处理 - } - "custom" => { - // 自定义适配器,使用原始配置 - } - _ => {} - } - - // 写入更新后的配置 + // 第六步:写入更新后的配置 write_claude_config(&config)?; - log::info!("已将中转站 {} 的 API 配置(apiKeyHelper, ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN)及自定义配置应用到 Claude 配置文件", station.name); + log::info!("[CLAUDE_CONFIG] Successfully applied station config (merged with source config)"); 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 - } + log::info!("[CLAUDE_CONFIG] Clearing relay station config"); + + // 恢复源文件备份 + let backup_path = get_config_backup_path()?; + let config_path = get_claude_config_path()?; + + if backup_path.exists() { + log::info!("[CLAUDE_CONFIG] Restoring from source backup"); + fs::copy(&backup_path, &config_path).map_err(|e| { + log::error!("[CLAUDE_CONFIG] Failed to restore: {}", e); + format!("恢复源配置文件失败: {}", e) + })?; + log::info!("[CLAUDE_CONFIG] Successfully restored source config"); } 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; - config.model = backup.model.clone(); - // 如果备份中有 ANTHROPIC_AUTH_TOKEN,也恢复它 - if backup.env.anthropic_auth_token.is_some() { - config.env.anthropic_auth_token = backup.env.anthropic_auth_token; - } - // 恢复备份中的 extra_fields - config.extra_fields = backup.extra_fields.clone(); - log::info!("[CLAUDE_CONFIG] Restored model from backup: {:?}", backup.model); - log::info!("[CLAUDE_CONFIG] Restored {} extra fields from backup", config.extra_fields.len()); - } else { - // 如果没有备份,清除 apiKeyHelper 和 model - config.api_key_helper = None; - config.model = None; - // 清除所有额外的自定义字段 - config.extra_fields.clear(); - log::info!("[CLAUDE_CONFIG] Cleared model and all extra fields (no backup found)"); + log::warn!("[CLAUDE_CONFIG] No source backup found, creating empty config"); + // 如果没有备份,创建一个最小配置 + let empty_config = ClaudeConfig::default(); + write_claude_config(&empty_config)?; } - // 写入更新后的配置 - write_claude_config(&config)?; + Ok(()) +} + +/// 初始化源文件备份(仅在首次启用中转站时调用) +pub fn init_source_backup() -> Result<(), String> { + let config_path = get_claude_config_path()?; + let backup_path = get_config_backup_path()?; + + if !backup_path.exists() && config_path.exists() { + log::info!("[CLAUDE_CONFIG] Creating initial source backup"); + fs::copy(&config_path, &backup_path).map_err(|e| { + log::error!("[CLAUDE_CONFIG] Failed to create source backup: {}", e); + format!("创建源文件备份失败: {}", e) + })?; + log::info!("[CLAUDE_CONFIG] Source backup created at: {:?}", backup_path); + } - log::info!("已清除 Claude 配置文件中的中转站设置"); Ok(()) } diff --git a/src-tauri/src/commands/relay_stations.rs b/src-tauri/src/commands/relay_stations.rs index ba3907a..0267b0b 100644 --- a/src-tauri/src/commands/relay_stations.rs +++ b/src-tauri/src/commands/relay_stations.rs @@ -571,7 +571,7 @@ pub async fn relay_station_toggle_enable( // 获取要启用的中转站信息 let station = relay_station_get_internal(&conn, &id)?; - // 将中转站配置应用到 Claude 配置文件 + // 将中转站配置应用到 Claude 配置文件(会自动确保源文件备份存在) claude_config::apply_relay_station_to_config(&station).map_err(|e| { log::error!("Failed to apply relay station config: {}", e); format!("配置文件写入失败: {}", e) diff --git a/src/App.tsx b/src/App.tsx index e75b164..5bfc4b3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, lazy, Suspense } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Loader2, ArrowLeft } from "lucide-react"; -import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api"; +import { api, type Project, type Session } from "@/lib/api"; import { OutputCacheProvider } from "@/lib/outputCache"; import { TabProvider } from "@/contexts/TabContext"; import { ThemeProvider } from "@/contexts/ThemeContext"; diff --git a/src/components/RelayStationManager.tsx b/src/components/RelayStationManager.tsx index 3673410..a99bc64 100644 --- a/src/components/RelayStationManager.tsx +++ b/src/components/RelayStationManager.tsx @@ -75,6 +75,12 @@ const RelayStationManager: React.FC = ({ onBack }) => const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [stationToDelete, setStationToDelete] = useState(null); const [togglingEnable, setTogglingEnable] = useState>({}); + + // 处理选中中转站的逻辑(用于切换时恢复自定义JSON) + const handleSelectStation = (station: RelayStation) => { + setSelectedStation(station); + setShowEditDialog(true); + }; const [currentConfig, setCurrentConfig] = useState>({}); const [loadingConfig, setLoadingConfig] = useState(false); const [jsonConfigView, setJsonConfigView] = useState(false); @@ -757,7 +763,7 @@ const RelayStationManager: React.FC = ({ onBack }) => station={station} getStatusBadge={getStatusBadge} getAdapterDisplayName={getAdapterDisplayName} - setSelectedStation={setSelectedStation} + setSelectedStation={handleSelectStation} setShowEditDialog={setShowEditDialog} openDeleteDialog={openDeleteDialog} quotaData={quotaData} @@ -1766,6 +1772,20 @@ const EditStationDialog: React.FC<{ return ''; }); + // 监听station变化,更新自定义JSON + useEffect(() => { + if (station.adapter_config) { + const { service_type, ...customFields } = station.adapter_config as any; + if (Object.keys(customFields).length > 0) { + setCustomJson(JSON.stringify(customFields, null, 2)); + } else { + setCustomJson(''); + } + } else { + setCustomJson(''); + } + }, [station.id]); // 只监听station.id变化,避免循环更新 + const [showSpeedTestModal, setShowSpeedTestModal] = useState(false); const [speedTestResults, setSpeedTestResults] = useState<{ url: string; name: string; responseTime: number | null; status: 'testing' | 'success' | 'failed' }[]>([]); const [speedTestInProgress, setSpeedTestInProgress] = useState(false); diff --git a/src/components/TabContent.tsx b/src/components/TabContent.tsx index 5afa52c..853d93b 100644 --- a/src/components/TabContent.tsx +++ b/src/components/TabContent.tsx @@ -4,7 +4,7 @@ import { useTabState } from '@/hooks/useTabState'; import { useScreenTracking } from '@/hooks/useAnalytics'; import { Tab } from '@/contexts/TabContext'; import { Loader2 } from 'lucide-react'; -import { api, type Project, type Session, type ClaudeMdFile } from '@/lib/api'; +import { api, type Project, type Session } from '@/lib/api'; import { ProjectList } from '@/components/ProjectList'; import { SessionList } from '@/components/SessionList'; import { RunningClaudeSessions } from '@/components/RunningClaudeSessions'; diff --git a/src/components/TabManager.tsx b/src/components/TabManager.tsx index ffc789e..dbe1821 100644 --- a/src/components/TabManager.tsx +++ b/src/components/TabManager.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence, Reorder } from 'framer-motion'; -import { X, Plus, MessageSquare, Bot, AlertCircle, Loader2, Folder, BarChart, Server, Settings, FileText } from 'lucide-react'; +import { X, Plus, MessageSquare, Bot, AlertCircle, Loader2, Folder, BarChart, Server, Settings } from 'lucide-react'; import { useTabState } from '@/hooks/useTabState'; import { Tab, useTabContext } from '@/contexts/TabContext'; import { cn } from '@/lib/utils'; diff --git a/src/main.tsx b/src/main.tsx index 05f1f49..c3f09b0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -20,19 +20,40 @@ try { // 全局捕获未处理的Promise拒绝,防止Monaco Editor错误 window.addEventListener('unhandledrejection', (event) => { const error = event.reason; - if (error && (error.message || error.toString()).includes('URL is not valid')) { + if (error && (error.message || error.toString() || '').toLowerCase().includes('url is not valid')) { event.preventDefault(); + // 不输出任何日志,完全静默 } }); // 全局捕获window.onerror window.addEventListener('error', (event) => { - if (event.error && (event.error.message || event.error.toString()).includes('URL is not valid')) { + if (event.error && (event.error.message || event.error.toString() || '').toLowerCase().includes('url is not valid')) { event.preventDefault(); return true; } }); +// 捕获console.error并过滤Monaco错误 +const originalConsoleError = console.error; +console.error = (...args) => { + const message = args.join(' '); + if (message.includes('URL is not valid')) { + return; // 静默过滤 + } + originalConsoleError.apply(console, args); +}; + +// 捕获console.warn并过滤Monaco警告 +const originalConsoleWarn = console.warn; +console.warn = (...args) => { + const message = args.join(' '); + if (message.includes('Monaco') && message.includes('URL')) { + return; // 静默过滤 + } + originalConsoleWarn.apply(console, args); +}; + // Initialize analytics before rendering (will no-op if no consent or no key) analytics.initialize();