From 35b2636bce3a4b21b77310212cb8ff3c457194d9 Mon Sep 17 00:00:00 2001 From: coso Date: Thu, 25 Dec 2025 09:38:01 +0800 Subject: [PATCH] =?UTF-8?q?v0.18.0:=20=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20-=20=E5=A4=96=E9=83=A8=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8F=98=E6=9B=B4=E6=A3=80=E6=B5=8B=E4=B8=8E=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 4 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/network_cmd.rs | 40 ++ src-tauri/src/lib.rs | 2 + src-tauri/src/services/live_sync.rs | 16 +- src-tauri/src/services/mcp_sync.rs | 20 +- src-tauri/tauri.conf.json | 2 +- src/components/api-server/ApiServerPage.tsx | 262 +++++------- src/components/api-server/LogsTab.tsx | 72 +--- src/components/clients/AppTabs.tsx | 6 +- src/components/clients/ClientsPage.tsx | 25 +- src/components/clients/LiveConfigModal.tsx | 2 +- src/components/clients/ProviderCard.tsx | 100 +++-- src/components/clients/ProviderList.tsx | 392 ++++++++++++++++-- .../config/ConfigManagementPage.tsx | 50 ++- src/components/config/ConfigPage.tsx | 14 +- .../provider-pool/ProviderPoolPage.tsx | 82 ++-- src/components/settings/DirectorySettings.tsx | 116 +++--- .../settings/ExtensionsSettings.tsx | 14 +- src/components/settings/GeneralSettings.tsx | 256 +++++------- src/components/settings/QuotaSettings.tsx | 129 +++--- src/components/settings/RoutingSettings.tsx | 24 +- src/components/settings/SettingsPage.tsx | 15 +- src/components/switch/LiveConfigModal.tsx | 2 +- src/components/ui/ExperimentalBanner.tsx | 21 + src/hooks/useTauri.ts | 14 + src/pages/FlowMonitorPage.tsx | 21 +- 30 files changed, 982 insertions(+), 726 deletions(-) create mode 100644 src-tauri/src/commands/network_cmd.rs create mode 100644 src/components/ui/ExperimentalBanner.tsx diff --git a/package-lock.json b/package-lock.json index 99ca3b953..883b7749d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxycast", - "version": "0.17.10", + "version": "0.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxycast", - "version": "0.17.10", + "version": "0.18.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", diff --git a/package.json b/package.json index 09873a5cc..128ecae4b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.17.12", + "version": "0.18.0", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 289aa51c9..87e231041 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3377,7 +3377,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.17.12" +version = "0.18.0" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 85788db8d..410c96a5d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.17.12" +version = "0.18.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index ec7374b6c..22be44dab 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -3,6 +3,7 @@ pub mod config_cmd; pub mod flow_monitor_cmd; pub mod injection_cmd; pub mod mcp_cmd; +pub mod network_cmd; pub mod oauth_cmd; pub mod plugin_cmd; pub mod prompt_cmd; diff --git a/src-tauri/src/commands/network_cmd.rs b/src-tauri/src/commands/network_cmd.rs new file mode 100644 index 000000000..3448dd8fa --- /dev/null +++ b/src-tauri/src/commands/network_cmd.rs @@ -0,0 +1,40 @@ +//! 网络相关命令 +//! +//! 提供获取本地网络接口信息的功能 + +use serde::Serialize; +use std::net::UdpSocket; + +/// 网络接口信息 +#[derive(Debug, Clone, Serialize)] +pub struct NetworkInfo { + /// 本地回环地址 + pub localhost: String, + /// 内网 IP 地址(局域网) + pub lan_ip: Option, +} + +/// 获取本地网络信息 +/// +/// 返回 localhost 和内网 IP 地址,用于客户端连接 +#[tauri::command] +pub fn get_network_info() -> Result { + let lan_ip = get_local_ip(); + + Ok(NetworkInfo { + localhost: "127.0.0.1".to_string(), + lan_ip, + }) +} + +/// 获取本机内网 IP 地址 +/// +/// 通过创建 UDP socket 连接外部地址来获取本机的内网 IP +fn get_local_ip() -> Option { + // 创建一个 UDP socket 并连接到外部地址(不会真正发送数据) + // 这样可以获取到本机用于出站连接的 IP 地址 + let socket = UdpSocket::bind("0.0.0.0:0").ok()?; + socket.connect("8.8.8.8:80").ok()?; + let local_addr = socket.local_addr().ok()?; + Some(local_addr.ip().to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4f79d30d0..393643034 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2241,6 +2241,8 @@ pub fn run() { commands::window_cmd::is_fullscreen, // Auto fix commands commands::auto_fix_cmd::auto_fix_configuration, + // Network commands + commands::network_cmd::get_network_info, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/services/live_sync.rs b/src-tauri/src/services/live_sync.rs index 9e0584334..e45b18980 100644 --- a/src-tauri/src/services/live_sync.rs +++ b/src-tauri/src/services/live_sync.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; pub fn get_app_config_path(app_type: &AppType) -> Option { let home = dirs::home_dir()?; match app_type { - AppType::Claude => Some(home.join(".claude.json")), + AppType::Claude => Some(home.join(".claude").join("settings.json")), AppType::Codex => Some(home.join(".codex")), AppType::Gemini => Some(home.join(".gemini")), AppType::ProxyCast => None, @@ -56,12 +56,18 @@ fn clean_claude_auth_conflict(settings: &mut Value) { } } -/// Sync Claude settings to ~/.claude.json +/// Sync Claude settings to ~/.claude/settings.json fn sync_claude_settings( provider: &Provider, ) -> Result<(), Box> { let home = dirs::home_dir().ok_or("Cannot find home directory")?; - let config_path = home.join(".claude.json"); + let claude_dir = home.join(".claude"); + let config_path = claude_dir.join("settings.json"); + + // Ensure .claude directory exists + if !claude_dir.exists() { + std::fs::create_dir_all(&claude_dir)?; + } // Read existing settings to preserve other fields let mut settings: Value = if config_path.exists() { @@ -197,7 +203,7 @@ pub fn read_live_settings( match app_type { AppType::Claude => { - let path = home.join(".claude.json"); + let path = home.join(".claude").join("settings.json"); if !path.exists() { return Err("Claude settings file not found".into()); } @@ -394,7 +400,7 @@ pub fn check_config_sync( fn get_config_last_modified(app_type: &AppType) -> Option { let home = dirs::home_dir()?; let path = match app_type { - AppType::Claude => home.join(".claude.json"), + AppType::Claude => home.join(".claude").join("settings.json"), AppType::Codex => home.join(".codex").join("auth.json"), AppType::Gemini => home.join(".gemini").join(".env"), AppType::ProxyCast => return None, diff --git a/src-tauri/src/services/mcp_sync.rs b/src-tauri/src/services/mcp_sync.rs index 057d1b73d..e392c35bf 100644 --- a/src-tauri/src/services/mcp_sync.rs +++ b/src-tauri/src/services/mcp_sync.rs @@ -24,7 +24,7 @@ fn escape_toml_string(s: &str) -> String { pub fn get_mcp_config_path(app_type: &AppType) -> Option { let home = dirs::home_dir()?; match app_type { - AppType::Claude => Some(home.join(".claude.json")), + AppType::Claude => Some(home.join(".claude").join("settings.json")), AppType::Codex => Some(home.join(".codex").join("config.toml")), AppType::Gemini => Some(home.join(".gemini").join("settings.json")), AppType::ProxyCast => None, @@ -73,13 +73,19 @@ pub fn sync_mcp_to_app( } } -/// Sync MCP servers to Claude's .claude.json -/// Claude uses the mcpServers field in ~/.claude.json +/// Sync MCP servers to Claude's ~/.claude/settings.json +/// Claude uses the mcpServers field in ~/.claude/settings.json fn sync_mcp_to_claude( servers: &[&McpServer], ) -> Result<(), Box> { let home = dirs::home_dir().ok_or("Cannot find home directory")?; - let config_path = home.join(".claude.json"); + let claude_dir = home.join(".claude"); + let config_path = claude_dir.join("settings.json"); + + // Ensure .claude directory exists + if !claude_dir.exists() { + std::fs::create_dir_all(&claude_dir)?; + } // Read existing settings let mut settings: Value = if config_path.exists() { @@ -255,7 +261,7 @@ pub fn remove_mcp_from_app( fn remove_mcp_from_claude(server_id: &str) -> Result<(), Box> { let home = dirs::home_dir().ok_or("Cannot find home directory")?; - let config_path = home.join(".claude.json"); + let config_path = home.join(".claude").join("settings.json"); if !config_path.exists() { return Ok(()); @@ -353,11 +359,11 @@ pub fn remove_mcp_from_all_apps( Ok(()) } -/// Import MCP servers from Claude's .claude.json +/// Import MCP servers from Claude's ~/.claude/settings.json pub fn import_mcp_from_claude( ) -> Result, Box> { let home = dirs::home_dir().ok_or("Cannot find home directory")?; - let config_path = home.join(".claude.json"); + let config_path = home.join(".claude").join("settings.json"); if !config_path.exists() { return Ok(Vec::new()); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b22859591..808abf4ee 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", -"version": "0.17.12", +"version": "0.18.0", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 5b7c96ad0..d137951d9 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -1,20 +1,14 @@ import { useState, useEffect } from "react"; import { - Activity, - Server, - Zap, - Clock, Play, Copy, Check, ChevronDown, ChevronUp, - Settings, RefreshCw, } from "lucide-react"; import { LogsTab } from "./LogsTab"; import { RoutesTab } from "./RoutesTab"; -import { HelpTip } from "@/components/HelpTip"; import { ProviderIcon } from "@/icons/providers"; import { startServer, @@ -29,6 +23,8 @@ import { TestResult, getDefaultProvider, setDefaultProvider, + getNetworkInfo, + NetworkInfo, } from "@/hooks/useTauri"; import { providerPoolApi, ProviderPoolOverview } from "@/lib/api/providerPool"; @@ -62,6 +58,9 @@ export function ApiServerPage() { text: string; } | null>(null); + // 网络信息 + const [networkInfo, setNetworkInfo] = useState(null); + // 自动清除消息 useEffect(() => { if (message) { @@ -96,11 +95,21 @@ export function ApiServerPage() { fetchStatus(); fetchConfig(); loadDefaultProvider(); + loadNetworkInfo(); const statusInterval = setInterval(fetchStatus, 3000); return () => clearInterval(statusInterval); }, []); + const loadNetworkInfo = async () => { + try { + const info = await getNetworkInfo(); + setNetworkInfo(info); + } catch (e) { + console.error("Failed to get network info:", e); + } + }; + const loadDefaultProvider = async () => { try { const dp = await getDefaultProvider(); @@ -228,12 +237,6 @@ export function ApiServerPage() { } }; - const formatUptime = (secs: number) => { - const h = Math.floor(secs / 3600); - const m = Math.floor((secs % 3600) / 60); - return `${h}h ${m}m`; - }; - const serverUrl = status ? `http://${status.host}:${status.port}` : `http://localhost:${config?.server.port ?? 8999}`; @@ -435,37 +438,43 @@ export function ApiServerPage() { }; return ( -
-
-

API Server

-

- 本地代理服务器,将凭证池中的凭证转换为标准 API -

+
+
+
+

API Server

+

+ 本地代理服务器,支持 OpenAI/Anthropic 格式 + {networkInfo && ( + <> + {" "} + + {networkInfo.localhost}:{config?.server.port ?? 8999} + + {networkInfo.lan_ip && ( + <> + {" | "} + + {networkInfo.lan_ip}:{config?.server.port ?? 8999} + + (局域网) + + )} + + )} + + {" "} + {status?.running ? "运行中" : "已停止"} + {" · "} + {status?.requests || 0} 请求 + {" · "} + {defaultProvider} + +

+
- -
    -
  • - 启动服务后,API 地址为{" "} - - http://localhost:8999 - -
  • -
  • - 支持 OpenAI 格式{" "} - - /v1/chat/completions - {" "} - 和 Anthropic 格式{" "} - - /v1/messages - -
  • -
  • 在下方选择"默认 Provider",请求会自动使用该类型凭证池中的凭证
  • -
  • 可在 Claude Code、Cherry Studio、Cursor 等工具中配置使用
  • -
-
- {message && (
)} - {/* Status Cards */} -
-
-
- - 状态 -
-
-
- - {status?.running ? "运行中" : "已停止"} - -
-
- -
-
- - 请求数 -
-
{status?.requests || 0}
-
- -
-
- - 运行时间 -
-
- {formatUptime(status?.uptime_secs || 0)} -
-
- -
-
- - 默认 Provider -
-
{defaultProvider}
-
-
- {/* Tabs */}
{[ @@ -550,16 +515,12 @@ export function ApiServerPage() { {/* Server Control Tab */} {activeTab === "server" && ( -
- {/* Server Control */} -
-

- - 服务控制 -

-
+
+ {/* Server Control - 紧凑版 */} +
+
- -
-
- - setEditPort(e.target.value)} - className="w-full rounded-lg border bg-background px-3 py-2" - /> +
+
+ 端口: + setEditPort(e.target.value)} + className="w-20 rounded border bg-background px-2 py-1 text-sm" + /> +
+
+ API Key: + setEditApiKey(e.target.value)} + className="w-40 rounded border bg-background px-2 py-1 text-sm" + /> +
+
-
- - setEditApiKey(e.target.value)} - className="w-full rounded-lg border bg-background px-3 py-2" - /> -
-
- -
-
- API 地址:{" "} - {serverUrl} -
-
- {/* Default Provider */} -
-

默认 Provider

-

- 选择默认使用的凭证池类型,请求会自动从该类型的凭证池中轮询选择可用凭证 -

- {providerSwitchMsg && ( -
- - {providerSwitchMsg} -
- )} + {/* Default Provider - 紧凑版 */} +
+
+ 默认 Provider + {providerSwitchMsg && ( + + + {providerSwitchMsg} + + )} +
{( [ - { id: "kiro", label: "Kiro (AWS)", iconType: "kiro" }, - { - id: "gemini", - label: "Gemini (Google)", - iconType: "gemini", - }, - { id: "qwen", label: "Qwen (阿里)", iconType: "qwen" }, + { id: "kiro", label: "Kiro", iconType: "kiro" }, + { id: "gemini", label: "Gemini", iconType: "gemini" }, + { id: "qwen", label: "Qwen", iconType: "qwen" }, { id: "antigravity", - label: "Antigravity (Gemini 3 Pro)", + label: "Antigravity", iconType: "gemini", }, { id: "openai", label: "OpenAI", iconType: "openai" }, - { - id: "claude", - label: "Claude (Anthropic)", - iconType: "claude", - }, + { id: "claude", label: "Claude", iconType: "claude" }, ] as const ).map((p) => { const overview = poolOverview.find( @@ -657,16 +599,16 @@ export function ApiServerPage() { key={p.id} onClick={() => handleSetDefaultProvider(p.id)} disabled={loading} - className={`flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium ${ + className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm transition-colors ${ defaultProvider === p.id - ? "bg-primary text-primary-foreground" - : "border hover:bg-muted" + ? "border-primary bg-primary/10 text-primary" + : "border-border bg-card hover:bg-muted text-muted-foreground hover:text-foreground" } disabled:opacity-50`} > - + {p.label} {count > 0 && ( - ({count}) + ({count}) )} ); diff --git a/src/components/api-server/LogsTab.tsx b/src/components/api-server/LogsTab.tsx index a83ed0dca..2a1979d2b 100644 --- a/src/components/api-server/LogsTab.tsx +++ b/src/components/api-server/LogsTab.tsx @@ -1,12 +1,9 @@ import { useState, useEffect, useRef } from "react"; -import { Trash2, Download, ArrowUp } from "lucide-react"; +import { Trash2, Download } from "lucide-react"; import { getLogs, clearLogs, LogEntry } from "@/hooks/useTauri"; export function LogsTab() { const [logs, setLogs] = useState([]); - const [autoScroll, setAutoScroll] = useState(true); - const [showScrollTop, setShowScrollTop] = useState(false); - const logsEndRef = useRef(null); const logsContainerRef = useRef(null); useEffect(() => { @@ -15,29 +12,24 @@ export function LogsTab() { return () => clearInterval(interval); }, []); - useEffect(() => { - if (autoScroll && logsEndRef.current) { - logsEndRef.current.scrollIntoView({ behavior: "smooth" }); - } - }, [logs, autoScroll]); - - const handleScroll = () => { - if (logsContainerRef.current) { - setShowScrollTop(logsContainerRef.current.scrollTop > 200); - } - }; - - const scrollToTop = () => { - if (logsContainerRef.current) { - logsContainerRef.current.scrollTo({ top: 0, behavior: "smooth" }); - setAutoScroll(false); - } - }; - const fetchLogs = async () => { try { const l = await getLogs(); - setLogs(l); + // 过滤掉 API 调用相关的日志,只保留系统日志 + // API 调用日志已由 Flow Monitor 接管 + const systemLogs = l.filter((log) => { + const msg = log.message; + // 排除 API 请求相关日志 + if (msg.includes("[REQ]")) return false; + if (msg.includes("[ROUTE]")) return false; + if (msg.includes("[CLIENT]")) return false; + if (msg.includes("request_id=")) return false; + if (msg.includes("POST /v1/")) return false; + if (msg.includes("GET /v1/")) return false; + if (msg.includes("Using pool credential")) return false; + return true; + }); + setLogs(systemLogs); } catch (e) { // 如果后端还没实现,使用空数组 console.error(e); @@ -95,38 +87,28 @@ export function LogsTab() { }; return ( -
+
-
-
+
{logs.length === 0 ? (

@@ -136,7 +118,7 @@ export function LogsTab() { logs.map((log, i) => (

{new Date(log.timestamp).toLocaleTimeString()} @@ -150,17 +132,7 @@ export function LogsTab() {
)) )} -
- {showScrollTop && ( - - )}
); diff --git a/src/components/clients/AppTabs.tsx b/src/components/clients/AppTabs.tsx index afced7bfa..d2424b5eb 100644 --- a/src/components/clients/AppTabs.tsx +++ b/src/components/clients/AppTabs.tsx @@ -41,10 +41,10 @@ export function AppTabs({ activeApp, onAppChange }: AppTabsProps) { key={app.id} onClick={() => onAppChange(app.id)} className={cn( - "flex items-center gap-2 px-4 py-2 rounded-t-lg text-sm font-medium transition-colors", + "flex items-center gap-2 px-4 py-2 rounded-lg border text-sm font-medium transition-colors", activeApp === app.id - ? "bg-primary text-primary-foreground" - : "hover:bg-muted text-muted-foreground", + ? "border-primary bg-primary/10 text-primary" + : "border-border bg-card hover:bg-muted text-muted-foreground hover:text-foreground", )} title={app.description} > diff --git a/src/components/clients/ClientsPage.tsx b/src/components/clients/ClientsPage.tsx index 127756b39..1328bc141 100644 --- a/src/components/clients/ClientsPage.tsx +++ b/src/components/clients/ClientsPage.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { AppType } from "@/lib/api/switch"; import { AppTabs } from "./AppTabs"; import { ProviderList } from "./ProviderList"; -import { HelpTip } from "@/components/HelpTip"; interface ClientsPageProps { hideHeader?: boolean; @@ -12,29 +11,21 @@ export function ClientsPage({ hideHeader = false }: ClientsPageProps) { const [activeApp, setActiveApp] = useState("claude"); return ( -
+
{!hideHeader && (

配置切换

-

- 一键切换 Claude Code / Codex / Gemini CLI 的 API 配置,快速在不同 - Provider 间切换 +

+ 一键切换 API 配置,可独立使用。添加 "ProxyCast" 可将凭证池转为标准 + API( + + localhost:8999 + + )

)} - -

- 添加名为 "ProxyCast" 的 Provider - 后,可将凭证池中的凭证(Kiro/Gemini/Claude 等)转换为标准 - OpenAI/Anthropic API, 供 Claude Code、Codex、Cherry Studio - 等工具使用。配置 API 地址为{" "} - - http://localhost:8999 - -

-
-
diff --git a/src/components/clients/LiveConfigModal.tsx b/src/components/clients/LiveConfigModal.tsx index 18e5ffcec..2f60d4037 100644 --- a/src/components/clients/LiveConfigModal.tsx +++ b/src/components/clients/LiveConfigModal.tsx @@ -8,7 +8,7 @@ interface LiveConfigModalProps { } const configPaths: Record = { - claude: "~/.claude.json", + claude: "~/.claude/settings.json", codex: "~/.codex/auth.json & config.toml", gemini: "~/.gemini/.env & settings.json", proxycast: "", diff --git a/src/components/clients/ProviderCard.tsx b/src/components/clients/ProviderCard.tsx index 768cd6f64..19cea2b2f 100644 --- a/src/components/clients/ProviderCard.tsx +++ b/src/components/clients/ProviderCard.tsx @@ -1,4 +1,4 @@ -import { Check, Edit2, Trash2, Zap } from "lucide-react"; +import { Check, Edit2, Trash2 } from "lucide-react"; import { Provider } from "@/lib/api/switch"; import { cn } from "@/lib/utils"; import { ProviderIcon } from "@/icons/providers"; @@ -50,44 +50,70 @@ export function ProviderCard({ onEdit, onDelete, }: ProviderCardProps) { + const isProxyCast = + provider.category === "custom" && provider.name === "ProxyCast"; + return (
!isCurrent && onSwitch()} className={cn( - "relative rounded-lg border p-4 transition-all", + "group relative rounded-xl border p-3 transition-all cursor-pointer", isCurrent - ? "border-primary bg-primary/5 ring-1 ring-primary" - : "hover:border-muted-foreground/50", + ? "border-primary bg-gradient-to-r from-primary/10 to-transparent shadow-sm" + : "hover:border-primary/50 hover:shadow-md", )} > + {/* 选中标记 */} {isCurrent && ( -
+
)} -
-
-
- -
-
-

{provider.name}

+
+ {/* 图标 */} +
+ +
+ + {/* 名称和分类 */} +
+
+

{provider.name}

{provider.category && ( - + {provider.category} )}
+ {isProxyCast ? ( +

+ 凭证池 → 标准 API +

+ ) : provider.notes ? ( +

+ {provider.notes} +

+ ) : null}
-
+ {/* 操作按钮 */} +
e.stopPropagation()} + >
- - {provider.category === "custom" && provider.name === "ProxyCast" && ( -

- 本地代理服务,将凭证池中的凭证转换为标准 API -

- )} - {provider.notes && ( -

- {provider.notes} -

- )} - -
- {isCurrent ? ( - 当前使用中 - ) : ( - - )} -
); } diff --git a/src/components/clients/ProviderList.tsx b/src/components/clients/ProviderList.tsx index 52891617d..0e9be64aa 100644 --- a/src/components/clients/ProviderList.tsx +++ b/src/components/clients/ProviderList.tsx @@ -1,6 +1,18 @@ -import { useState } from "react"; -import { Plus, RefreshCw, Eye, GitCompare } from "lucide-react"; -import { AppType, SyncCheckResult } from "@/lib/api/switch"; +import { useState, useEffect, useMemo } from "react"; +import { + Plus, + RefreshCw, + Eye, + GitCompare, + Download, + AlertTriangle, +} from "lucide-react"; +import { + AppType, + SyncCheckResult, + switchApi, + Provider, +} from "@/lib/api/switch"; import { useSwitch } from "@/hooks/useSwitch"; import { ProviderCard } from "./ProviderCard"; import { ProviderForm } from "./ProviderForm"; @@ -8,6 +20,168 @@ import { LiveConfigModal } from "./LiveConfigModal"; import { ConfigSyncDialog } from "./ConfigSyncDialog"; import { ConfirmDialog } from "@/components/ConfirmDialog"; +// 敏感字段关键词 +const SENSITIVE_KEYS = [ + "key", + "token", + "secret", + "password", + "auth", + "credential", + "api_key", + "apikey", + "access_token", + "refresh_token", +]; + +// 脱敏函数:对敏感值进行遮盖 +function maskSensitiveValue(value: string): string { + if (value.length <= 8) return "****"; + return value.slice(0, 4) + "****" + value.slice(-4); +} + +// 递归脱敏对象中的敏感字段 +function maskSensitiveData( + data: Record, +): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(data)) { + const lowerKey = key.toLowerCase(); + const isSensitive = SENSITIVE_KEYS.some((k) => lowerKey.includes(k)); + + if (typeof value === "string" && isSensitive && value.length > 0) { + result[key] = maskSensitiveValue(value); + } else if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) + ) { + result[key] = maskSensitiveData(value as Record); + } else { + result[key] = value; + } + } + + return result; +} + +// 比较两个配置是否匹配(忽略非关键字段) +function configsMatch( + liveConfig: Record, + providerConfig: Record, +): boolean { + // 提取关键字段进行比较 + // Claude: { env: { ANTHROPIC_AUTH_TOKEN: ..., ANTHROPIC_API_KEY: ... } } + // Gemini: { env: { GEMINI_API_KEY: ..., GOOGLE_API_KEY: ... } } + // Codex: { auth: { OPENAI_API_KEY: ... }, config: ... } + const getLiveEnv = (config: Record) => + (config.env as Record) || {}; + const getProviderEnv = (config: Record) => + (config.env as Record) || {}; + + // Codex 特殊处理:从 auth 对象中提取 + const getLiveAuth = (config: Record) => + (config.auth as Record) || {}; + const getProviderAuth = (config: Record) => + (config.auth as Record) || {}; + + const liveEnv = getLiveEnv(liveConfig); + const providerEnv = getProviderEnv(providerConfig); + const liveAuth = getLiveAuth(liveConfig); + const providerAuth = getProviderAuth(providerConfig); + + // 比较关键认证字段(Claude/Gemini) + const envKeysToCompare = [ + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_API_KEY", + "ANTHROPIC_BASE_URL", + "GOOGLE_API_KEY", + "GEMINI_API_KEY", + ]; + + // 检查是否有任何关键字段匹配 + let hasAnyMatch = false; + + // 比较 env 字段 + for (const key of envKeysToCompare) { + const liveVal = liveEnv[key] as string | undefined; + const providerVal = providerEnv[key] as string | undefined; + + if (liveVal && providerVal) { + if (liveVal !== providerVal) { + return false; + } + hasAnyMatch = true; + } else if (liveVal && !providerVal) { + return false; + } else if (!liveVal && providerVal && key !== "ANTHROPIC_BASE_URL") { + return false; + } + } + + // 比较 Codex auth.OPENAI_API_KEY + const liveApiKey = liveAuth.OPENAI_API_KEY as string | undefined; + const providerApiKey = providerAuth.OPENAI_API_KEY as string | undefined; + + if (liveApiKey && providerApiKey) { + if (liveApiKey !== providerApiKey) { + return false; + } + hasAnyMatch = true; + } else if (liveApiKey && !providerApiKey) { + return false; + } else if (!liveApiKey && providerApiKey) { + return false; + } + + return hasAnyMatch; +} + +// 从实际配置中提取简短描述 +function getLiveConfigSummary(config: Record): string { + const env = (config.env as Record) || {}; + const auth = (config.auth as Record) || {}; + + if (env.ANTHROPIC_AUTH_TOKEN) { + const baseUrl = env.ANTHROPIC_BASE_URL as string; + if (baseUrl) { + try { + const url = new URL(baseUrl); + return `OAuth · ${url.host}`; + } catch { + return `OAuth · ${baseUrl}`; + } + } + return "Claude OAuth"; + } + + if (env.ANTHROPIC_API_KEY) { + const baseUrl = env.ANTHROPIC_BASE_URL as string; + if (baseUrl) { + try { + const url = new URL(baseUrl); + return `API Key · ${url.host}`; + } catch { + return `API Key · ${baseUrl}`; + } + } + return "Claude API Key"; + } + + // Gemini: GEMINI_API_KEY 或 GOOGLE_API_KEY + if (env.GEMINI_API_KEY || env.GOOGLE_API_KEY) { + return "Gemini API Key"; + } + + // Codex: auth.OPENAI_API_KEY + if (auth.OPENAI_API_KEY) { + return "Codex API Key"; + } + + return "未知配置"; +} + interface ProviderListProps { appType: AppType; } @@ -37,6 +211,88 @@ export function ProviderList({ appType }: ProviderListProps) { const [syncResult, setSyncResult] = useState(null); const [checkingSync, setCheckingSync] = useState(false); + // 实际生效的配置 + const [liveConfig, setLiveConfig] = useState | null>( + null, + ); + const [loadingLiveConfig, setLoadingLiveConfig] = useState(false); + const [importingConfig, setImportingConfig] = useState(false); + + // 始终读取当前生效的配置(用于检测外部变更) + useEffect(() => { + const loadConfig = async () => { + if (appType === "proxycast") return; + setLoadingLiveConfig(true); + try { + const config = await switchApi.readLiveSettings(appType); + setLiveConfig(config); + } catch { + setLiveConfig(null); + } finally { + setLoadingLiveConfig(false); + } + }; + + if (!loading) { + loadConfig(); + } + }, [loading, appType]); + + // 检测实际配置是否与当前选中的 provider 匹配 + const configMismatch = useMemo(() => { + if (!liveConfig || !currentProvider || loadingLiveConfig) return null; + if (Object.keys(liveConfig).length === 0) return null; + + const matches = configsMatch(liveConfig, currentProvider.settings_config); + if (matches) return null; + + // 检查是否有其他 provider 匹配实际配置 + const matchingProvider = providers.find((p) => + configsMatch(liveConfig, p.settings_config), + ); + + return { + liveConfig, + liveSummary: getLiveConfigSummary(liveConfig), + matchingProvider, + }; + }, [liveConfig, currentProvider, providers, loadingLiveConfig]); + + const handleImportCurrentConfig = async () => { + if (!liveConfig) return; + setImportingConfig(true); + try { + // 直接使用读取到的配置创建新的 provider + const providerName = `导入配置 ${new Date().toLocaleDateString()}`; + await addProvider({ + name: providerName, + app_type: appType, + settings_config: liveConfig, + category: "custom", + }); + // 重新加载配置 + const config = await switchApi.readLiveSettings(appType); + setLiveConfig(config); + await refresh(); + } catch (e) { + alert("导入失败: " + (e instanceof Error ? e.message : String(e))); + } finally { + setImportingConfig(false); + } + }; + + // 切换到匹配的 provider + const handleSwitchToMatching = async (provider: Provider) => { + try { + await switchToProvider(provider.id); + // 切换后重新读取实际配置,更新 UI 状态 + const config = await switchApi.readLiveSettings(appType); + setLiveConfig(config); + } catch (e) { + alert("切换失败: " + (e instanceof Error ? e.message : String(e))); + } + }; + const handleAdd = () => { setEditingProvider(null); setShowForm(true); @@ -104,19 +360,19 @@ export function ProviderList({ appType }: ProviderListProps) { if (loading) { return ( -
- +
+
); } if (error) { return ( -
+

{error}

@@ -125,25 +381,36 @@ export function ProviderList({ appType }: ProviderListProps) { } return ( -
+
+ {/* 工具栏 */}
-
-

- 当前: {currentProvider?.name || "未设置"} -

+
+ {loadingLiveConfig ? ( + + + 检测中... + + ) : configMismatch ? ( + + + 实际: {configMismatch.liveSummary} + + ) : ( + 当前: {currentProvider?.name || "未设置"} + )}
-
+
+ {/* 配置不匹配警告 */} + {configMismatch && ( +
+
+ +
+

+ 检测到外部配置变更 +

+

+ 实际生效的配置与当前选中的 "{currentProvider?.name}" 不一致 +

+
+
+
+ {configMismatch.matchingProvider ? ( + + ) : ( + + )} + +
+
+ )} + + {/* Provider 列表 */} {providers.length === 0 ? ( -
-

暂无 Provider 配置

-

点击上方按钮添加第一个配置

+
+ {loadingLiveConfig ? ( +
+ +
+ ) : liveConfig && Object.keys(liveConfig).length > 0 ? ( +
+
+

+ 检测到当前配置,可一键导入 +

+ +
+
+                {JSON.stringify(maskSensitiveData(liveConfig), null, 2)}
+              
+
+ ) : ( +
+

暂无配置

+ +
+ )}
) : ( -
+
{providers.map((provider) => ( ("switch"); + // 根据当前 tab 显示不同的描述 + const getDescription = () => { + if (activeTab === "switch") { + return ( + <> + 一键切换 API 配置,可独立使用。添加 "ProxyCast" 可将凭证池转为标准 + API( + + localhost:8999 + + ) + + ); + } + return ( + <> + 编辑 YAML 配置文件。实验功能,不影响核心使用, + + 问题反馈 + + + ); + }; + return ( -
+

配置管理

-

管理客户端配置和配置文件

+

{getDescription()}

{/* Tab 切换 */} @@ -36,6 +75,9 @@ export function ConfigManagementPage() { > {tab.label} + {tab.experimental && ( + (实验) + )} ))}
diff --git a/src/components/config/ConfigPage.tsx b/src/components/config/ConfigPage.tsx index 6cc697b49..3781dbf1e 100644 --- a/src/components/config/ConfigPage.tsx +++ b/src/components/config/ConfigPage.tsx @@ -88,7 +88,7 @@ export const ConfigPage = forwardRef( } return ( -
+
{!hideHeader && (
@@ -96,8 +96,16 @@ export const ConfigPage = forwardRef( 配置管理 -

- 编辑 YAML 配置文件,导入导出配置 +

+ 编辑 YAML 配置文件。实验功能,不影响核心使用, + + 问题反馈 +

diff --git a/src/components/provider-pool/ProviderPoolPage.tsx b/src/components/provider-pool/ProviderPoolPage.tsx index a6c097bc1..81591ee41 100644 --- a/src/components/provider-pool/ProviderPoolPage.tsx +++ b/src/components/provider-pool/ProviderPoolPage.tsx @@ -14,7 +14,6 @@ import { AddCredentialModal } from "./AddCredentialModal"; import { EditCredentialModal } from "./EditCredentialModal"; import { ErrorDisplay, useErrorDisplay } from "./ErrorDisplay"; import { ConfirmDialog } from "@/components/ConfirmDialog"; -import { HelpTip } from "@/components/HelpTip"; import { getConfig, saveConfig, Config } from "@/hooks/useTauri"; import { VertexAISection } from "./VertexAISection"; import { AmpConfigSection } from "./AmpConfigSection"; @@ -322,53 +321,24 @@ export const ProviderPoolPage = forwardRef(

凭证池

-

- 管理多个 AI 服务凭证,支持负载均衡和健康检测 +

+ 管理多个 AI 服务凭证,自动轮询负载均衡。在 API Server 选择默认 + Provider 后自动使用对应凭证

-
- - -
+
- -
    -
  • - Kiro/Gemini/Qwen - :上传对应工具的凭证文件,ProxyCast 会自动管理 Token 刷新 -
  • -
  • - OpenAI/Claude:直接填入 API - Key,用于转发请求 -
  • -
  • 多个凭证会自动轮询负载均衡,单个凭证失效不影响服务
  • -
  • - 在 API Server 页面选择默认 - Provider 后,请求会自动使用对应凭证池 -
  • -
-
- {error && (
{error} @@ -382,10 +352,10 @@ export const ProviderPoolPage = forwardRef( setActiveCategory("oauth"); setActiveTab(oauthProviderTypes[0]); }} - className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${ + className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${ activeCategory === "oauth" - ? "bg-primary text-primary-foreground" - : "bg-muted text-muted-foreground hover:text-foreground hover:bg-muted/80" + ? "border-primary bg-primary/10 text-primary" + : "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted" }`} > OAuth 凭证 @@ -395,10 +365,10 @@ export const ProviderPoolPage = forwardRef( setActiveCategory("apikey"); setActiveTab(apiKeyProviderTypes[0]); }} - className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${ + className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${ activeCategory === "apikey" - ? "bg-primary text-primary-foreground" - : "bg-muted text-muted-foreground hover:text-foreground hover:bg-muted/80" + ? "border-primary bg-primary/10 text-primary" + : "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted" }`} > API Key @@ -408,10 +378,10 @@ export const ProviderPoolPage = forwardRef( setActiveCategory("config"); setActiveTab("vertex"); }} - className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${ + className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${ activeCategory === "config" - ? "bg-primary text-primary-foreground" - : "bg-muted text-muted-foreground hover:text-foreground hover:bg-muted/80" + ? "border-primary bg-primary/10 text-primary" + : "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted" }`} > 其他配置 @@ -429,7 +399,7 @@ export const ProviderPoolPage = forwardRef( key={providerType} onClick={() => setActiveTab(providerType)} title={providerLabels[providerType]} - className={`group relative flex items-center gap-2 px-3 py-2 rounded-lg border transition-all ${ + className={`group relative flex items-center justify-center gap-2 min-w-[120px] px-3 py-2 rounded-lg border transition-all ${ isActive ? "border-primary bg-primary/10 text-primary shadow-sm" : "border-border bg-card hover:border-primary/50 hover:bg-muted text-muted-foreground hover:text-foreground" @@ -465,7 +435,7 @@ export const ProviderPoolPage = forwardRef( key={providerType} onClick={() => setActiveTab(providerType)} title={providerLabels[providerType]} - className={`group relative flex items-center gap-2 px-3 py-2 rounded-lg border transition-all ${ + className={`group relative flex items-center justify-center gap-2 min-w-[120px] px-3 py-2 rounded-lg border transition-all ${ isActive ? "border-primary bg-primary/10 text-primary shadow-sm" : "border-border bg-card hover:border-primary/50 hover:bg-muted text-muted-foreground hover:text-foreground" @@ -499,7 +469,7 @@ export const ProviderPoolPage = forwardRef( +
+

+ 自定义各应用配置文件目录,修改后需重启生效

-
-
- {directoryItems.map((item) => ( -
-
-
- -

- {item.description} -

-
- -
-
+
+ {directoryItems.map((item) => ( +
+ + {item.label} +
- + setDirs((prev) => ({ ...prev, [item.key]: e.target.value })) } - className="w-full pl-9 pr-3 py-2 rounded-lg border bg-background text-sm font-mono focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none" + className="w-full pl-8 pr-8 py-1.5 rounded border bg-background text-sm font-mono focus:ring-1 focus:ring-primary/20 focus:border-primary outline-none" placeholder={defaultDirs[item.key]} /> +
-
- ))} -
- -
- + ))} +
{/* 数据管理 */} -
-
+
+

数据管理

-

导入或导出配置数据

-
- -
- - +
+ + +
diff --git a/src/components/settings/ExtensionsSettings.tsx b/src/components/settings/ExtensionsSettings.tsx index 4b88748d0..f38306dd8 100644 --- a/src/components/settings/ExtensionsSettings.tsx +++ b/src/components/settings/ExtensionsSettings.tsx @@ -19,11 +19,19 @@ export function ExtensionsSettings() { const [activeTab, setActiveTab] = useState("mcp"); return ( -
+

扩展管理

-

- 管理 MCP 服务器、Prompts 和 Skills +

+ 管理 MCP 服务器、Prompts 和 Skills。实验功能,不影响核心使用, + + 问题反馈 +

diff --git a/src/components/settings/GeneralSettings.tsx b/src/components/settings/GeneralSettings.tsx index d6cf8edb2..4a932e02a 100644 --- a/src/components/settings/GeneralSettings.tsx +++ b/src/components/settings/GeneralSettings.tsx @@ -1,5 +1,9 @@ +/** + * @file GeneralSettings.tsx + * @description 通用设置页面 - 主题、代理、启动行为配置 + */ import { useState, useEffect } from "react"; -import { Moon, Sun, Monitor, Globe, RefreshCw } from "lucide-react"; +import { Moon, Sun, Monitor, RefreshCw, Info } from "lucide-react"; import { cn, validateProxyUrl } from "@/lib/utils"; import { getConfig, saveConfig, Config } from "@/hooks/useTauri"; @@ -22,13 +26,10 @@ export function GeneralSettings() { const [configLoading, setConfigLoading] = useState(true); useEffect(() => { - // 读取当前主题 const savedTheme = localStorage.getItem("theme") as Theme | null; if (savedTheme) { setTheme(savedTheme); } - - // 加载配置 loadConfig(); }, []); @@ -38,7 +39,6 @@ export function GeneralSettings() { const c = await getConfig(); setConfig(c); setProxyUrl(c.proxy_url || ""); - // 加载最小化到托盘设置 setMinimizeToTray(c.minimize_to_tray ?? true); } catch (e) { console.error("加载配置失败:", e); @@ -50,8 +50,6 @@ export function GeneralSettings() { const handleThemeChange = (newTheme: Theme) => { setTheme(newTheme); localStorage.setItem("theme", newTheme); - - // 应用主题 const root = document.documentElement; if (newTheme === "system") { const systemDark = window.matchMedia( @@ -66,9 +64,7 @@ export function GeneralSettings() { const handleProxyUrlChange = (value: string) => { setProxyUrl(value); if (value && !validateProxyUrl(value)) { - setProxyError( - "代理 URL 格式无效,请使用 http://、https:// 或 socks5:// 开头的地址", - ); + setProxyError("格式无效,请使用 http://、https:// 或 socks5:// 开头"); } else { setProxyError(null); } @@ -76,27 +72,18 @@ export function GeneralSettings() { const handleSaveProxy = async () => { if (!config) return; - - // 验证格式 if (proxyUrl && !validateProxyUrl(proxyUrl)) { - setProxyError( - "代理 URL 格式无效,请使用 http://、https:// 或 socks5:// 开头的地址", - ); + setProxyError("格式无效,请使用 http://、https:// 或 socks5:// 开头"); return; } - setProxySaving(true); setProxyMessage(null); - try { - const newConfig = { - ...config, - proxy_url: proxyUrl.trim() || null, - }; + const newConfig = { ...config, proxy_url: proxyUrl.trim() || null }; await saveConfig(newConfig); setConfig(newConfig); - setProxyMessage({ type: "success", text: "代理设置已保存" }); - setTimeout(() => setProxyMessage(null), 3000); + setProxyMessage({ type: "success", text: "已保存" }); + setTimeout(() => setProxyMessage(null), 2000); } catch (e: unknown) { const errorMessage = e instanceof Error ? e.message : String(e); setProxyMessage({ type: "error", text: `保存失败: ${errorMessage}` }); @@ -108,166 +95,125 @@ export function GeneralSettings() { const themeOptions = [ { id: "light" as Theme, label: "浅色", icon: Sun }, { id: "dark" as Theme, label: "深色", icon: Moon }, - { id: "system" as Theme, label: "跟随系统", icon: Monitor }, + { id: "system" as Theme, label: "系统", icon: Monitor }, ]; return ( -
- {/* 网络代理设置 */} -
-
- -
-

网络代理

-

- 配置 HTTP/HTTPS/SOCKS5 代理,用于访问海外 API 服务 -

-
+
+ {/* 网络代理 */} +
+
+

全局代理

+ {proxyMessage && ( + + {proxyMessage.text} + + )}
{configLoading ? ( -
- +
+
) : ( -
- {/* 代理消息提示 */} - {proxyMessage && ( -
- {proxyMessage.text} -
- )} - -
- +
+
handleProxyUrlChange(e.target.value)} - placeholder="例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080" + placeholder="http://127.0.0.1:7890 或 socks5://127.0.0.1:1080" className={cn( - "w-full px-3 py-2 rounded-lg border bg-background text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none", - proxyError && - "border-destructive focus:border-destructive focus:ring-destructive/20", + "flex-1 px-3 py-1.5 rounded border bg-background text-sm focus:ring-1 focus:ring-primary/20 focus:border-primary outline-none", + proxyError && "border-destructive", )} /> - {proxyError ? ( -

{proxyError}

- ) : ( -

- 留空表示不使用代理。支持 http://、https://、socks5:// 协议 -

- )} +
- -
-

- 代理优先级说明: + {proxyError ? ( +

{proxyError}

+ ) : ( +

+ + 凭证级代理优先于全局代理,留空表示直连

-
    -
  • 凭证级代理优先于全局代理
  • -
  • 如果凭证未设置代理,则使用此全局代理
  • -
  • 全局代理为空时,直接连接 API 服务
  • -
-
- - + )}
)}
- {/* 主题设置 */} -
-
+ {/* 主题 */} +
+

主题

-

选择界面显示主题

-
-
- {themeOptions.map((option) => ( - - ))} +
+ {themeOptions.map((option) => ( + + ))} +
- {/* 启动设置 */} -
-
-

启动行为

-

- 配置应用启动和关闭行为 -

-
+ {/* 启动行为 */} +
+

启动行为

-
- + -
+ } + }} + className="w-4 h-4 rounded border-gray-300" + /> +
); diff --git a/src/components/settings/QuotaSettings.tsx b/src/components/settings/QuotaSettings.tsx index 6bbde17a5..65e20c559 100644 --- a/src/components/settings/QuotaSettings.tsx +++ b/src/components/settings/QuotaSettings.tsx @@ -1,11 +1,16 @@ +/** + * @file QuotaSettings.tsx + * @description 配额超限策略设置 + */ import { useState, useEffect } from "react"; -import { RefreshCw, CheckCircle2, AlertTriangle } from "lucide-react"; +import { RefreshCw } from "lucide-react"; import { getConfig, saveConfig, Config, QuotaExceededConfig, } from "@/hooks/useTauri"; +import { cn } from "@/lib/utils"; export function QuotaSettings() { const [config, setConfig] = useState(null); @@ -34,11 +39,11 @@ export function QuotaSettings() { setMessage(null); try { await saveConfig(config); - setMessage({ type: "success", text: "配额设置已保存" }); - setTimeout(() => setMessage(null), 3000); + setMessage({ type: "success", text: "已保存" }); + setTimeout(() => setMessage(null), 2000); } catch (e: unknown) { const errorMessage = e instanceof Error ? e.message : String(e); - setMessage({ type: "error", text: `保存失败: ${errorMessage}` }); + setMessage({ type: "error", text: `失败: ${errorMessage}` }); } setSaving(false); }; @@ -53,8 +58,8 @@ export function QuotaSettings() { if (!config) { return ( -
-
+
+
); } @@ -62,44 +67,35 @@ export function QuotaSettings() { const quota = config.quota_exceeded; return ( -
-
- -
-

配额超限策略

-

- 配置配额超限时的自动切换行为 -

+
+
+

配额超限策略

+
+ {message && ( + + {message.text} + + )} +
- {/* 消息提示 */} - {message && ( -
- {message.type === "success" ? ( - - ) : ( - - )} - {message.text} -
- )} - -
- {/* 自动切换凭证 */} -