From ad00ca48e3ca556e706bc0f3523abbb878ae3f1a Mon Sep 17 00:00:00 2001 From: coso Date: Mon, 15 Dec 2025 15:23:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96UI=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E5=92=8C=E6=B7=BB=E5=8A=A0=E5=B8=AE=E5=8A=A9=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=20v0.7.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除冗余的 OpenAI/Claude 自定义 Tab 和可用模型 Tab - 合并启动/停止服务按钮为单个切换按钮 - 添加可折叠的 HelpTip 组件 - 为凭证池、配置切换、API Server、MCP、Prompts、Skills 页面添加帮助说明 - 在关于页面添加使用说明 Q&A - 优化日志页面,添加回到顶部按钮 - 优化 Provider 切换提示,显示在框内 - 重命名 AI Clients 为配置切换,说明更清晰 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 7 + src-tauri/src/server.rs | 83 ++++ src-tauri/tauri.conf.json | 2 +- src/components/Dashboard.tsx | 4 +- src/components/HelpTip.tsx | 61 +++ src/components/Sidebar.tsx | 2 +- src/components/api-server/ApiServerPage.tsx | 422 ++++++++---------- src/components/api-server/LogsTab.tsx | 34 +- src/components/clients/ClientsPage.tsx | 18 +- src/components/clients/ProviderCard.tsx | 5 + src/components/mcp/McpPage.tsx | 14 + src/components/prompts/PromptsPage.tsx | 14 + .../provider-pool/ProviderPoolPage.tsx | 21 +- src/components/settings/AboutSection.tsx | 48 +- src/components/skills/SkillsPage.tsx | 13 +- 18 files changed, 510 insertions(+), 244 deletions(-) create mode 100644 src/components/HelpTip.tsx diff --git a/package.json b/package.json index bd91bb0f3..c2cfb8fa2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.7.0", + "version": "0.7.2", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4df53c5d0..5e8490bc3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3153,7 +3153,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.7.0" +version = "0.7.2" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dda7fceec..c73003d43 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.7.0" +version = "0.7.2" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f32d9c7f7..913aeb7b1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -211,6 +211,13 @@ async fn set_default_provider( let mut s = state.write().await; s.config.default_provider = provider.clone(); + + // 同时更新运行中服务器的 default_provider_ref + { + let mut dp = s.default_provider_ref.write().await; + *dp = provider.clone(); + } + config::save_config(&s.config).map_err(|e| e.to_string())?; logs.write() .await diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs index 4f0fa7d66..a97413dc8 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server.rs @@ -59,6 +59,7 @@ pub struct ServerState { pub qwen_provider: QwenProvider, pub openai_custom_provider: OpenAICustomProvider, pub claude_custom_provider: ClaudeCustomProvider, + pub default_provider_ref: Arc>, shutdown_tx: Option>, } @@ -69,6 +70,7 @@ impl ServerState { let qwen = QwenProvider::new(); let openai_custom = OpenAICustomProvider::new(); let claude_custom = ClaudeCustomProvider::new(); + let default_provider_ref = Arc::new(RwLock::new(config.default_provider.clone())); Self { config, @@ -80,6 +82,7 @@ impl ServerState { qwen_provider: qwen, openai_custom_provider: openai_custom, claude_custom_provider: claude_custom, + default_provider_ref, shutdown_tx: None, } } @@ -111,6 +114,7 @@ impl ServerState { let host = self.config.server.host.clone(); let port = self.config.server.port; let api_key = self.config.server.api_key.clone(); + let default_provider_ref = self.default_provider_ref.clone(); // 重新加载凭证 let _ = self.kiro_provider.load_credentials().await; @@ -121,6 +125,7 @@ impl ServerState { &host, port, &api_key, + default_provider_ref, kiro, logs, rx, @@ -163,6 +168,7 @@ impl Clone for KiroProvider { struct AppState { api_key: String, base_url: String, + default_provider: Arc>, kiro: Arc>, logs: Arc>, kiro_refresh_lock: Arc>, @@ -177,6 +183,7 @@ async fn run_server( host: &str, port: u16, api_key: &str, + default_provider: Arc>, kiro: KiroProvider, logs: Arc>, shutdown: oneshot::Receiver<()>, @@ -188,6 +195,7 @@ async fn run_server( let state = AppState { api_key: api_key.to_string(), base_url, + default_provider, kiro: Arc::new(RwLock::new(kiro)), logs, kiro_refresh_lock: Arc::new(tokio::sync::Mutex::new(())), @@ -311,6 +319,42 @@ async fn chat_completions( ), ); + // 获取当前默认 provider + let default_provider = state.default_provider.read().await.clone(); + + // 尝试从凭证池中选择凭证 + let credential = match &state.db { + Some(db) => state + .pool_service + .select_credential(db, &default_provider, Some(&request.model)) + .ok() + .flatten(), + None => None, + }; + + // 如果找到凭证池中的凭证,使用它 + if let Some(cred) = credential { + state.logs.write().await.add( + "info", + &format!( + "[ROUTE] Using pool credential: type={} name={:?} uuid={}", + cred.provider_type, + cred.name, + &cred.uuid[..8] + ), + ); + return call_provider_openai(&state, &cred, &request).await; + } + + // 回退到旧的单凭证模式 + state.logs.write().await.add( + "debug", + &format!( + "[ROUTE] No pool credential found for '{}', using legacy mode", + default_provider + ), + ); + // 检查是否需要刷新 token(无 token 或即将过期) { let _guard = state.kiro_refresh_lock.lock().await; @@ -595,6 +639,45 @@ async fn anthropic_messages( ); } + // 获取当前默认 provider + let default_provider = state.default_provider.read().await.clone(); + + // 尝试从凭证池中选择凭证 + let credential = match &state.db { + Some(db) => { + // 根据 default_provider 配置选择凭证 + state + .pool_service + .select_credential(db, &default_provider, Some(&request.model)) + .ok() + .flatten() + } + None => None, + }; + + // 如果找到凭证池中的凭证,使用它 + if let Some(cred) = credential { + state.logs.write().await.add( + "info", + &format!( + "[ROUTE] Using pool credential: type={} name={:?} uuid={}", + cred.provider_type, + cred.name, + &cred.uuid[..8] + ), + ); + return call_provider_anthropic(&state, &cred, &request).await; + } + + // 回退到旧的单凭证模式 + state.logs.write().await.add( + "debug", + &format!( + "[ROUTE] No pool credential found for '{}', using legacy mode", + default_provider + ), + ); + // 检查是否需要刷新 token(无 token 或即将过期) { let _guard = state.kiro_refresh_lock.lock().await; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 11696d6cc..e14e187f9 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.7.0", + "version": "0.7.2", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 3ad1213e0..f56642f6c 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -144,8 +144,8 @@ export function Dashboard() { /> diff --git a/src/components/HelpTip.tsx b/src/components/HelpTip.tsx new file mode 100644 index 000000000..89bfb1765 --- /dev/null +++ b/src/components/HelpTip.tsx @@ -0,0 +1,61 @@ +import { useState, ReactNode } from "react"; +import { ChevronDown, ChevronUp, HelpCircle } from "lucide-react"; + +interface HelpTipProps { + title: string; + children: ReactNode; + defaultOpen?: boolean; + variant?: "blue" | "amber" | "green"; +} + +export function HelpTip({ + title, + children, + defaultOpen = false, + variant = "blue", +}: HelpTipProps) { + const [isOpen, setIsOpen] = useState(defaultOpen); + + const variantStyles = { + blue: { + border: "border-blue-200 dark:border-blue-900", + bg: "bg-blue-50 dark:bg-blue-950/30", + title: "text-blue-800 dark:text-blue-300", + icon: "text-blue-600 dark:text-blue-400", + }, + amber: { + border: "border-amber-200 dark:border-amber-900", + bg: "bg-amber-50 dark:bg-amber-950/30", + title: "text-amber-800 dark:text-amber-300", + icon: "text-amber-600 dark:text-amber-400", + }, + green: { + border: "border-green-200 dark:border-green-900", + bg: "bg-green-50 dark:bg-green-950/30", + title: "text-green-800 dark:text-green-300", + icon: "text-green-600 dark:text-green-400", + }, + }; + + const styles = variantStyles[variant]; + + return ( +
+ + {isOpen &&
{children}
} +
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 7b0a3fbaa..9d82324c7 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -30,7 +30,7 @@ interface SidebarProps { const navItems = [ { id: "dashboard" as Page, label: "仪表盘", icon: LayoutDashboard }, { id: "provider-pool" as Page, label: "凭证池", icon: Database }, - { id: "clients" as Page, label: "AI Clients", icon: Monitor }, + { id: "clients" as Page, label: "配置切换", icon: Monitor }, { id: "api-server" as Page, label: "API Server", icon: Globe }, { id: "mcp" as Page, label: "MCP", icon: Plug }, { id: "prompts" as Page, label: "Prompts", icon: MessageSquare }, diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 67925fd7c..e661eb392 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -12,9 +12,9 @@ import { Settings, RefreshCw, } from "lucide-react"; -import { ModelsTab } from "./ModelsTab"; import { LogsTab } from "./LogsTab"; import { RoutesTab } from "./RoutesTab"; +import { HelpTip } from "@/components/HelpTip"; import { startServer, stopServer, @@ -28,14 +28,8 @@ import { TestResult, getDefaultProvider, setDefaultProvider, - // OpenAI/Claude Custom - getOpenAICustomStatus, - setOpenAICustomConfig, - getClaudeCustomStatus, - setClaudeCustomConfig, - OpenAICustomStatus, - ClaudeCustomStatus, } from "@/hooks/useTauri"; +import { providerPoolApi, ProviderPoolOverview } from "@/lib/api/providerPool"; interface TestState { endpoint: string; @@ -45,7 +39,7 @@ interface TestState { httpStatus?: number; } -type TabId = "server" | "routes" | "openai" | "claude" | "models" | "logs"; +type TabId = "server" | "routes" | "logs"; export function ApiServerPage() { const [status, setStatus] = useState(null); @@ -62,25 +56,21 @@ export function ApiServerPage() { const [editApiKey, setEditApiKey] = useState(""); const [defaultProvider, setDefaultProviderState] = useState("kiro"); - // OpenAI Custom state - const [openaiStatus, setOpenaiStatus] = useState( - null, - ); - const [openaiApiKey, setOpenaiApiKey] = useState(""); - const [openaiBaseUrl, setOpenaiBaseUrl] = useState(""); - - // Claude Custom state - const [claudeStatus, setClaudeStatus] = useState( - null, - ); - const [claudeApiKey, setClaudeApiKey] = useState(""); - const [claudeBaseUrl, setClaudeBaseUrl] = useState(""); - const [message, setMessage] = useState<{ type: "success" | "error"; text: string; } | null>(null); + // 自动清除消息 + useEffect(() => { + if (message) { + const timer = setTimeout(() => { + setMessage(null); + }, 5000); + return () => clearTimeout(timer); + } + }, [message]); + const fetchStatus = async () => { try { const s = await getServerStatus(); @@ -101,32 +91,10 @@ export function ApiServerPage() { } }; - const loadOpenAICustomStatus = async () => { - try { - const status = await getOpenAICustomStatus(); - setOpenaiStatus(status); - setOpenaiBaseUrl(status.base_url); - } catch (e) { - console.error("Failed to load OpenAI Custom status:", e); - } - }; - - const loadClaudeCustomStatus = async () => { - try { - const status = await getClaudeCustomStatus(); - setClaudeStatus(status); - setClaudeBaseUrl(status.base_url); - } catch (e) { - console.error("Failed to load Claude Custom status:", e); - } - }; - useEffect(() => { fetchStatus(); fetchConfig(); loadDefaultProvider(); - loadOpenAICustomStatus(); - loadClaudeCustomStatus(); const statusInterval = setInterval(fetchStatus, 3000); return () => clearInterval(statusInterval); @@ -193,54 +161,67 @@ export function ApiServerPage() { setLoading(false); }; + const providerLabels: Record = { + kiro: "Kiro (AWS)", + gemini: "Gemini (Google)", + qwen: "Qwen (阿里)", + antigravity: "Antigravity (Gemini 3 Pro)", + openai: "OpenAI", + claude: "Claude (Anthropic)", + }; + + const [poolOverview, setPoolOverview] = useState([]); + const [providerSwitchMsg, setProviderSwitchMsg] = useState( + null, + ); + + // 加载凭证池概览 + const loadPoolOverview = async () => { + try { + const overview = await providerPoolApi.getOverview(); + setPoolOverview(overview); + } catch (e) { + console.error("Failed to load pool overview:", e); + } + }; + + useEffect(() => { + loadPoolOverview(); + }, []); + + // 自动清除 provider 切换提示 + useEffect(() => { + if (providerSwitchMsg) { + const timer = setTimeout(() => setProviderSwitchMsg(null), 3000); + return () => clearTimeout(timer); + } + }, [providerSwitchMsg]); + const handleSetDefaultProvider = async (providerId: string) => { - setLoading(true); try { await setDefaultProvider(providerId); setDefaultProviderState(providerId); - setMessage({ - type: "success", - text: `默认 Provider 已切换为: ${providerId}`, - }); - } catch (e: unknown) { - const errMsg = e instanceof Error ? e.message : String(e); - setMessage({ type: "error", text: `切换失败: ${errMsg}` }); - } - setLoading(false); - }; - const handleSaveOpenAIConfig = async () => { - setLoading(true); - try { - await setOpenAICustomConfig( - openaiApiKey || null, - openaiBaseUrl || null, - true, - ); - await loadOpenAICustomStatus(); - setMessage({ type: "success", text: "OpenAI 配置已保存" }); - } catch (e: unknown) { - const errMsg = e instanceof Error ? e.message : String(e); - setMessage({ type: "error", text: `保存失败: ${errMsg}` }); - } - setLoading(false); - }; + // 获取最新的凭证池数据 + const freshOverview = await providerPoolApi.getOverview(); + setPoolOverview(freshOverview); - const handleSaveClaudeConfig = async () => { - setLoading(true); - try { - await setClaudeCustomConfig( - claudeApiKey || null, - claudeBaseUrl || null, - true, + // 获取该类型下的凭证数量 + const typeOverview = freshOverview.find( + (o) => o.provider_type === providerId, + ); + const credCount = typeOverview?.stats.total || 0; + const healthyCount = typeOverview?.stats.healthy || 0; + const label = providerLabels[providerId] || providerId; + + setProviderSwitchMsg( + `已切换到 ${label}` + + (credCount > 0 ? `(${healthyCount}/${credCount} 可用)` : ""), ); - await loadClaudeCustomStatus(); - setMessage({ type: "success", text: "Claude 配置已保存" }); } catch (e: unknown) { const errMsg = e instanceof Error ? e.message : String(e); - setMessage({ type: "error", text: `保存失败: ${errMsg}` }); + setProviderSwitchMsg(`切换失败: ${errMsg}`); } - setLoading(false); }; const formatUptime = (secs: number) => { @@ -385,10 +366,33 @@ export function ApiServerPage() {

API Server

- 管理代理 API 服务器和自定义 API 配置 + 本地代理服务器,将凭证池中的凭证转换为标准 API

+ +
    +
  • + 启动服务后,API 地址为{" "} + + http://localhost:8999 + +
  • +
  • + 支持 OpenAI 格式{" "} + + /v1/chat/completions + {" "} + 和 Anthropic 格式{" "} + + /v1/messages + +
  • +
  • 在下方选择"默认 Provider",请求会自动使用该类型凭证池中的凭证
  • +
  • 可在 Claude Code、Cherry Studio、Cursor 等工具中配置使用
  • +
+
+ {message && (
( -
@@ -541,30 +543,101 @@ export function ApiServerPage() { {/* Default Provider */}

默认 Provider

+

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

+ {providerSwitchMsg && ( +
+ + {providerSwitchMsg} +
+ )}
- {["kiro", "gemini", "qwen", "openai", "claude"].map((p) => ( - - ))} + {( + [ + { id: "kiro", label: "Kiro (AWS)" }, + { id: "gemini", label: "Gemini (Google)" }, + { id: "qwen", label: "Qwen (阿里)" }, + { id: "antigravity", label: "Antigravity (Gemini 3 Pro)" }, + { id: "openai", label: "OpenAI" }, + { id: "claude", label: "Claude (Anthropic)" }, + ] as const + ).map((p) => { + const overview = poolOverview.find( + (o) => o.provider_type === p.id, + ); + const count = overview?.stats.total || 0; + return ( + + ); + })}
+ + {/* 当前选中类型的凭证列表 */} + {(() => { + const currentOverview = poolOverview.find( + (o) => o.provider_type === defaultProvider, + ); + const credentials = currentOverview?.credentials || []; + if (credentials.length === 0) { + return ( +
+ 当前类型无可用凭证,请先在凭证池中添加 +
+ ); + } + return ( +
+

+ 当前可用凭证 ({credentials.length}): +

+
+ {credentials.map((cred) => ( +
+
+ + {cred.name || cred.uuid.slice(0, 8)} +
+ + 使用 {cred.usage_count} 次 + +
+ ))} +
+
+ ); + })()}
{/* API Testing */} @@ -696,115 +769,6 @@ export function ApiServerPage() { {/* Routes Tab */} {activeTab === "routes" && } - {/* OpenAI Custom Tab */} - {activeTab === "openai" && ( -
-

OpenAI 自定义 API 配置

-

- 配置自定义 OpenAI 兼容 API 端点,用于转发请求到其他 OpenAI 兼容服务 -

-
-
- - setOpenaiApiKey(e.target.value)} - placeholder="sk-..." - className="w-full rounded-lg border bg-background px-3 py-2 text-sm" - /> -
-
- - setOpenaiBaseUrl(e.target.value)} - placeholder="https://api.openai.com/v1" - className="w-full rounded-lg border bg-background px-3 py-2 text-sm" - /> -
-
- 状态: - - {openaiStatus?.has_api_key ? "已配置" : "未配置"} - -
- -
-
- )} - - {/* Claude Custom Tab */} - {activeTab === "claude" && ( -
-

Claude 自定义 API 配置

-

- 配置自定义 Claude API 端点,用于直接调用 Anthropic API -

-
-
- - setClaudeApiKey(e.target.value)} - placeholder="sk-ant-..." - className="w-full rounded-lg border bg-background px-3 py-2 text-sm" - /> -
-
- - setClaudeBaseUrl(e.target.value)} - placeholder="https://api.anthropic.com" - className="w-full rounded-lg border bg-background px-3 py-2 text-sm" - /> -
-
- 状态: - - {claudeStatus?.has_api_key ? "已配置" : "未配置"} - -
- -
-
- )} - - {/* Models Tab */} - {activeTab === "models" && } - {/* Logs Tab */} {activeTab === "logs" && } diff --git a/src/components/api-server/LogsTab.tsx b/src/components/api-server/LogsTab.tsx index 816f43d1a..986f32534 100644 --- a/src/components/api-server/LogsTab.tsx +++ b/src/components/api-server/LogsTab.tsx @@ -1,11 +1,13 @@ import { useState, useEffect, useRef } from "react"; -import { Trash2, Download } from "lucide-react"; +import { Trash2, Download, ArrowUp } 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(() => { fetchLogs(); @@ -19,6 +21,19 @@ export function LogsTab() { } }, [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(); @@ -107,8 +122,12 @@ export function LogsTab() { -
-
+
+
{logs.length === 0 ? (

暂无日志,启动服务后将显示请求日志 @@ -133,6 +152,15 @@ export function LogsTab() { )}

+ {showScrollTop && ( + + )}
); diff --git a/src/components/clients/ClientsPage.tsx b/src/components/clients/ClientsPage.tsx index 2e389e6b0..0307c9f4c 100644 --- a/src/components/clients/ClientsPage.tsx +++ b/src/components/clients/ClientsPage.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { AppType } from "@/lib/api/switch"; import { AppTabs } from "./AppTabs"; import { ProviderList } from "./ProviderList"; +import { HelpTip } from "@/components/HelpTip"; export function ClientsPage() { const [activeApp, setActiveApp] = useState("claude"); @@ -9,12 +10,25 @@ export function ClientsPage() { return (
-

AI Clients

+

配置切换

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

+ +

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

+
+
diff --git a/src/components/clients/ProviderCard.tsx b/src/components/clients/ProviderCard.tsx index 3ad4669d1..a27193afa 100644 --- a/src/components/clients/ProviderCard.tsx +++ b/src/components/clients/ProviderCard.tsx @@ -79,6 +79,11 @@ export function ProviderCard({
+ {provider.category === "custom" && provider.name === "ProxyCast" && ( +

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

+ )} {provider.notes && (

{provider.notes} diff --git a/src/components/mcp/McpPage.tsx b/src/components/mcp/McpPage.tsx index 32c2102ee..1b273acac 100644 --- a/src/components/mcp/McpPage.tsx +++ b/src/components/mcp/McpPage.tsx @@ -4,6 +4,7 @@ import { useMcpServers } from "@/hooks/useMcpServers"; import { McpServer } from "@/lib/api/mcp"; import { cn } from "@/lib/utils"; import { ConfirmDialog } from "@/components/ConfirmDialog"; +import { HelpTip } from "@/components/HelpTip"; // 预设 MCP 服务器配置 const mcpPresets = [ @@ -295,6 +296,19 @@ export function McpPage() { + +

    +
  • + MCP (Model Context Protocol) 是 AI 工具扩展协议,让 AI + 能访问文件系统、数据库等外部资源 +
  • +
  • + 在此添加 MCP 服务器后,可同步到 Claude Code、Codex、Gemini CLI +
  • +
  • 也可从这些工具导入已有的 MCP 配置,统一管理
  • +
+ + {error && (

{error}

diff --git a/src/components/prompts/PromptsPage.tsx b/src/components/prompts/PromptsPage.tsx index 8c35c15bf..52b94b2a3 100644 --- a/src/components/prompts/PromptsPage.tsx +++ b/src/components/prompts/PromptsPage.tsx @@ -11,6 +11,7 @@ import { AppType, Prompt } from "@/lib/api/prompts"; import { usePrompts } from "@/hooks/usePrompts"; import { cn } from "@/lib/utils"; import { ConfirmDialog } from "@/components/ConfirmDialog"; +import { HelpTip } from "@/components/HelpTip"; const apps: { id: AppType; label: string; filename: string }[] = [ { id: "claude", label: "Claude Code", filename: "CLAUDE.md" }, @@ -185,6 +186,19 @@ export function PromptsPage() {

+ +
    +
  • Prompts 是 AI 工具的系统提示词,定义 AI 的行为和风格
  • +
  • + Claude Code 使用 CLAUDE.md,Codex 使用 AGENTS.md,Gemini 使用 + GEMINI.md +
  • +
  • + 可创建多个提示词模板,一键切换不同场景(如代码审查、文档编写等) +
  • +
+
+ {/* App tabs */}
{apps.map((app) => ( diff --git a/src/components/provider-pool/ProviderPoolPage.tsx b/src/components/provider-pool/ProviderPoolPage.tsx index aa032b903..8f94c8b9a 100644 --- a/src/components/provider-pool/ProviderPoolPage.tsx +++ b/src/components/provider-pool/ProviderPoolPage.tsx @@ -13,6 +13,7 @@ import { AddCredentialModal } from "./AddCredentialModal"; import { EditCredentialModal } from "./EditCredentialModal"; import { ErrorDisplay, useErrorDisplay } from "./ErrorDisplay"; import { ConfirmDialog } from "@/components/ConfirmDialog"; +import { HelpTip } from "@/components/HelpTip"; import type { PoolProviderType, CredentialDisplay, @@ -211,7 +212,7 @@ export const ProviderPoolPage = forwardRef(

凭证池

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

+ +
    +
  • + Kiro/Gemini/Qwen + :上传对应工具的凭证文件,ProxyCast 会自动管理 Token 刷新 +
  • +
  • + OpenAI/Claude:直接填入 API + Key,用于转发请求 +
  • +
  • 多个凭证会自动轮询负载均衡,单个凭证失效不影响服务
  • +
  • + 在 API Server 页面选择默认 + Provider 后,请求会自动使用对应凭证池 +
  • +
+
+ {error && (
{error} diff --git a/src/components/settings/AboutSection.tsx b/src/components/settings/AboutSection.tsx index 6a7e353e2..bbea1ab1a 100644 --- a/src/components/settings/AboutSection.tsx +++ b/src/components/settings/AboutSection.tsx @@ -21,7 +21,7 @@ interface ToolVersion { export function AboutSection() { const [versionInfo] = useState({ - current: "0.7.0", + current: "0.7.2", latest: undefined, hasUpdate: false, }); @@ -122,6 +122,33 @@ export function AboutSection() {
+ {/* 使用说明 Q&A */} +
+

使用说明

+
+ + + + + +
+
+ {/* 本地工具版本 */}

本地工具版本

@@ -172,3 +199,22 @@ function ToolVersionItem({ name, version }: { name: string; version: string }) {
); } + +function QAItem({ question, answer }: { question: string; answer: string }) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + {isOpen && ( +
{answer}
+ )} +
+ ); +} diff --git a/src/components/skills/SkillsPage.tsx b/src/components/skills/SkillsPage.tsx index c5a70ccc0..1d014af9f 100644 --- a/src/components/skills/SkillsPage.tsx +++ b/src/components/skills/SkillsPage.tsx @@ -3,6 +3,7 @@ import { RefreshCw, Search, Settings } from "lucide-react"; import { useSkills } from "@/hooks/useSkills"; import { SkillCard } from "./SkillCard"; import { RepoManagerPanel } from "./RepoManagerPanel"; +import { HelpTip } from "@/components/HelpTip"; import type { AppType } from "@/lib/api/skills"; interface SkillsPageProps { @@ -94,7 +95,9 @@ export const SkillsPage = forwardRef(

Skills

-

浏览和安装 Claude Skills

+

+ 浏览和安装 Claude Code Skills +

+ +
    +
  • Skills 是 Claude Code 的扩展功能包,提供特定领域的专业能力
  • +
  • 安装后会自动添加到 Claude Code 的 skills 目录
  • +
  • 可通过"仓库管理"添加自定义 Skills 仓库
  • +
+
+ {error && (
{error}