diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa9690098..abd247795 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,9 @@ jobs: - platform: macos-latest target: aarch64-apple-darwin name: macOS-arm64 + - platform: macos-latest + target: x86_64-apple-darwin + name: macOS-x64 - platform: windows-latest target: x86_64-pc-windows-msvc name: Windows-x64 @@ -73,6 +76,7 @@ jobs: ### Downloads - **macOS (Apple Silicon)**: `proxycast_*_aarch64.dmg` + - **macOS (Intel)**: `proxycast_*_x64.dmg` - **Windows (64-bit)**: `proxycast_*_x64-setup.exe` ### macOS 首次安装 diff --git a/package.json b/package.json index d62fe425b..f9d455af2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.13.0", + "version": "0.14.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c28fa7a9c..4d5d0bafa 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.13.0" +version = "0.14.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 40b00dc94..73f427bed 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.13.0", + "version": "0.14.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 988024324..501b3507c 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -15,6 +15,7 @@ import { import { LogsTab } from "./LogsTab"; import { RoutesTab } from "./RoutesTab"; import { HelpTip } from "@/components/HelpTip"; +import { ProviderIcon } from "@/icons/providers"; import { startServer, stopServer, @@ -260,7 +261,7 @@ export function ApiServerPage() { path: "/v1/chat/completions", needsAuth: true, body: JSON.stringify({ - model: "claude-sonnet-4-5", + model: "claude-opus-4-5-20251101", messages: [{ role: "user", content: "Say hi in one word" }], }), }, @@ -271,7 +272,7 @@ export function ApiServerPage() { path: "/v1/messages", needsAuth: true, body: JSON.stringify({ - model: "claude-sonnet-4-5", + model: "claude-opus-4-5-20251101", max_tokens: 100, messages: [ { @@ -555,12 +556,24 @@ export function ApiServerPage() {
{( [ - { 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)" }, + { id: "kiro", label: "Kiro (AWS)", iconType: "kiro" }, + { + id: "gemini", + label: "Gemini (Google)", + iconType: "gemini", + }, + { id: "qwen", label: "Qwen (阿里)", iconType: "qwen" }, + { + id: "antigravity", + label: "Antigravity (Gemini 3 Pro)", + iconType: "gemini", + }, + { id: "openai", label: "OpenAI", iconType: "openai" }, + { + id: "claude", + label: "Claude (Anthropic)", + iconType: "claude", + }, ] as const ).map((p) => { const overview = poolOverview.find( @@ -572,12 +585,13 @@ export function ApiServerPage() { key={p.id} onClick={() => handleSetDefaultProvider(p.id)} disabled={loading} - className={`rounded-lg px-4 py-2 text-sm font-medium ${ + className={`flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium ${ defaultProvider === p.id ? "bg-primary text-primary-foreground" : "border hover:bg-muted" } disabled:opacity-50`} > + {p.label} {count > 0 && ( ({count}) diff --git a/src/components/clients/AppTabs.tsx b/src/components/clients/AppTabs.tsx index d40a10126..afced7bfa 100644 --- a/src/components/clients/AppTabs.tsx +++ b/src/components/clients/AppTabs.tsx @@ -1,15 +1,36 @@ import { cn } from "@/lib/utils"; import { AppType } from "@/lib/api/switch"; +import { ProviderIcon } from "@/icons/providers"; interface AppTabsProps { activeApp: AppType; onAppChange: (app: AppType) => void; } -const apps: { id: AppType; label: string; description: string }[] = [ - { id: "claude", label: "Claude Code", description: "Claude CLI 配置" }, - { id: "codex", label: "Codex", description: "OpenAI Codex CLI" }, - { id: "gemini", label: "Gemini", description: "Google Gemini CLI" }, +const apps: { + id: AppType; + label: string; + description: string; + iconType: string; +}[] = [ + { + id: "claude", + label: "Claude Code", + description: "Claude CLI 配置", + iconType: "claude", + }, + { + id: "codex", + label: "Codex", + description: "OpenAI Codex CLI", + iconType: "openai", + }, + { + id: "gemini", + label: "Gemini", + description: "Google Gemini CLI", + iconType: "gemini", + }, ]; export function AppTabs({ activeApp, onAppChange }: AppTabsProps) { @@ -20,13 +41,14 @@ export function AppTabs({ activeApp, onAppChange }: AppTabsProps) { key={app.id} onClick={() => onAppChange(app.id)} className={cn( - "px-4 py-2 rounded-t-lg text-sm font-medium transition-colors", + "flex items-center gap-2 px-4 py-2 rounded-t-lg text-sm font-medium transition-colors", activeApp === app.id ? "bg-primary text-primary-foreground" : "hover:bg-muted text-muted-foreground", )} title={app.description} > + {app.label} ))} diff --git a/src/components/clients/ProviderCard.tsx b/src/components/clients/ProviderCard.tsx index a27193afa..768cd6f64 100644 --- a/src/components/clients/ProviderCard.tsx +++ b/src/components/clients/ProviderCard.tsx @@ -1,6 +1,39 @@ import { Check, Edit2, Trash2, Zap } from "lucide-react"; import { Provider } from "@/lib/api/switch"; import { cn } from "@/lib/utils"; +import { ProviderIcon } from "@/icons/providers"; + +// 从供应商名称和分类推断图标类型 +function getProviderTypeFromName(name: string, category: string): string { + const lowerName = name.toLowerCase(); + + // 精确匹配 + if (lowerName.includes("智谱") || lowerName.includes("glm")) return "zhipu"; + if (lowerName.includes("proxycast")) return "proxycast"; + if (lowerName.includes("deepseek")) return "deepseek"; + if (lowerName.includes("kimi")) return "kimi"; + if (lowerName.includes("minimax")) return "minimax"; + if (lowerName.includes("doubao") || lowerName.includes("豆包")) + return "doubao"; + if (lowerName.includes("qwen") || lowerName.includes("通义")) return "qwen"; + if (lowerName.includes("claude")) return "claude"; + if (lowerName.includes("anthropic")) return "anthropic"; + if (lowerName.includes("openai")) return "openai"; + if (lowerName.includes("gemini")) return "gemini"; + if (lowerName.includes("google")) return "google"; + if (lowerName.includes("kiro")) return "kiro"; + if (lowerName.includes("azure")) return "azure"; + if (lowerName.includes("alibaba") || lowerName.includes("阿里")) + return "alibaba"; + if (lowerName.includes("copilot")) return "copilot"; + + // 根据分类推断 + if (category === "custom") return "custom"; + if (category === "proxy") return "amp"; + + // 默认回退 + return lowerName.replace(/\s+/g, ""); +} interface ProviderCardProps { provider: Provider; @@ -34,11 +67,15 @@ export function ProviderCard({
-
- {provider.icon || provider.name.charAt(0).toUpperCase()} +
+

{provider.name}

diff --git a/src/components/clients/ProviderForm.tsx b/src/components/clients/ProviderForm.tsx index 5b452d6f5..8045d86c7 100644 --- a/src/components/clients/ProviderForm.tsx +++ b/src/components/clients/ProviderForm.tsx @@ -3,6 +3,7 @@ import { X, ExternalLink, Wand2, Eye, EyeOff, Database } from "lucide-react"; import { Provider, AppType } from "@/lib/api/switch"; import { getConfig } from "@/hooks/useTauri"; import { cn } from "@/lib/utils"; +import { ProviderIcon } from "@/icons/providers"; import { providerPoolApi, CredentialDisplay, @@ -372,7 +373,6 @@ export function ProviderForm({ const generateJsonFromFields = useCallback(() => { const env: Record = {}; if (apiKey) { - env.ANTHROPIC_AUTH_TOKEN = apiKey; env.ANTHROPIC_API_KEY = apiKey; } if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl; @@ -790,10 +790,7 @@ export function ProviderForm({ : "border-border hover:border-muted-foreground/50", )} > - + {preset.name} ))} diff --git a/src/components/provider-pool/ProviderPoolPage.tsx b/src/components/provider-pool/ProviderPoolPage.tsx index 65b9d35a3..976bc761c 100644 --- a/src/components/provider-pool/ProviderPoolPage.tsx +++ b/src/components/provider-pool/ProviderPoolPage.tsx @@ -19,6 +19,7 @@ import { getConfig, saveConfig, Config } from "@/hooks/useTauri"; import { GeminiApiKeySection } from "./GeminiApiKeySection"; import { VertexAISection } from "./VertexAISection"; import { AmpConfigSection } from "./AmpConfigSection"; +import { ProviderIcon } from "@/icons/providers"; import type { PoolProviderType, CredentialDisplay, @@ -381,6 +382,7 @@ export const ProviderPoolPage = forwardRef( : "border-transparent text-muted-foreground hover:text-foreground" }`} > + {providerLabels[providerType]} {count > 0 && ( diff --git a/src/components/switch/AppTabs.tsx b/src/components/switch/AppTabs.tsx index d40a10126..afced7bfa 100644 --- a/src/components/switch/AppTabs.tsx +++ b/src/components/switch/AppTabs.tsx @@ -1,15 +1,36 @@ import { cn } from "@/lib/utils"; import { AppType } from "@/lib/api/switch"; +import { ProviderIcon } from "@/icons/providers"; interface AppTabsProps { activeApp: AppType; onAppChange: (app: AppType) => void; } -const apps: { id: AppType; label: string; description: string }[] = [ - { id: "claude", label: "Claude Code", description: "Claude CLI 配置" }, - { id: "codex", label: "Codex", description: "OpenAI Codex CLI" }, - { id: "gemini", label: "Gemini", description: "Google Gemini CLI" }, +const apps: { + id: AppType; + label: string; + description: string; + iconType: string; +}[] = [ + { + id: "claude", + label: "Claude Code", + description: "Claude CLI 配置", + iconType: "claude", + }, + { + id: "codex", + label: "Codex", + description: "OpenAI Codex CLI", + iconType: "openai", + }, + { + id: "gemini", + label: "Gemini", + description: "Google Gemini CLI", + iconType: "gemini", + }, ]; export function AppTabs({ activeApp, onAppChange }: AppTabsProps) { @@ -20,13 +41,14 @@ export function AppTabs({ activeApp, onAppChange }: AppTabsProps) { key={app.id} onClick={() => onAppChange(app.id)} className={cn( - "px-4 py-2 rounded-t-lg text-sm font-medium transition-colors", + "flex items-center gap-2 px-4 py-2 rounded-t-lg text-sm font-medium transition-colors", activeApp === app.id ? "bg-primary text-primary-foreground" : "hover:bg-muted text-muted-foreground", )} title={app.description} > + {app.label} ))} diff --git a/src/components/switch/ProviderCard.tsx b/src/components/switch/ProviderCard.tsx index 3ad4669d1..31de6e5ee 100644 --- a/src/components/switch/ProviderCard.tsx +++ b/src/components/switch/ProviderCard.tsx @@ -1,6 +1,39 @@ import { Check, Edit2, Trash2, Zap } from "lucide-react"; import { Provider } from "@/lib/api/switch"; import { cn } from "@/lib/utils"; +import { ProviderIcon } from "@/icons/providers"; + +// 从供应商名称和分类推断图标类型 +function getProviderTypeFromName(name: string, category: string): string { + const lowerName = name.toLowerCase(); + + // 精确匹配 + if (lowerName.includes("智谱") || lowerName.includes("glm")) return "zhipu"; + if (lowerName.includes("proxycast")) return "proxycast"; + if (lowerName.includes("deepseek")) return "deepseek"; + if (lowerName.includes("kimi")) return "kimi"; + if (lowerName.includes("minimax")) return "minimax"; + if (lowerName.includes("doubao") || lowerName.includes("豆包")) + return "doubao"; + if (lowerName.includes("qwen") || lowerName.includes("通义")) return "qwen"; + if (lowerName.includes("claude")) return "claude"; + if (lowerName.includes("anthropic")) return "anthropic"; + if (lowerName.includes("openai")) return "openai"; + if (lowerName.includes("gemini")) return "gemini"; + if (lowerName.includes("google")) return "google"; + if (lowerName.includes("kiro")) return "kiro"; + if (lowerName.includes("azure")) return "azure"; + if (lowerName.includes("alibaba") || lowerName.includes("阿里")) + return "alibaba"; + if (lowerName.includes("copilot")) return "copilot"; + + // 根据分类推断 + if (category === "custom") return "custom"; + if (category === "proxy") return "amp"; + + // 默认回退 + return lowerName.replace(/\s+/g, ""); +} interface ProviderCardProps { provider: Provider; @@ -34,11 +67,15 @@ export function ProviderCard({
-
- {provider.icon || provider.name.charAt(0).toUpperCase()} +
+

{provider.name}

diff --git a/src/components/switch/ProviderForm.tsx b/src/components/switch/ProviderForm.tsx index 052bb87b0..a1f783768 100644 --- a/src/components/switch/ProviderForm.tsx +++ b/src/components/switch/ProviderForm.tsx @@ -3,6 +3,7 @@ import { X, ExternalLink, Wand2, Eye, EyeOff } from "lucide-react"; import { Provider, AppType } from "@/lib/api/switch"; import { getConfig } from "@/hooks/useTauri"; import { cn } from "@/lib/utils"; +import { ProviderIcon } from "@/icons/providers"; interface ProviderFormProps { appType: AppType; @@ -344,7 +345,6 @@ export function ProviderForm({ const generateJsonFromFields = useCallback(() => { const env: Record = {}; if (apiKey) { - env.ANTHROPIC_AUTH_TOKEN = apiKey; env.ANTHROPIC_API_KEY = apiKey; } if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl; @@ -666,10 +666,7 @@ export function ProviderForm({ : "border-border hover:border-muted-foreground/50", )} > - + {preset.name} ))} diff --git a/src/icons/providers/anthropic.svg b/src/icons/providers/anthropic.svg new file mode 100644 index 000000000..5b81844cb --- /dev/null +++ b/src/icons/providers/anthropic.svg @@ -0,0 +1 @@ +Anthropic \ No newline at end of file diff --git a/src/icons/providers/gemini.svg b/src/icons/providers/gemini.svg new file mode 100644 index 000000000..f1cf35757 --- /dev/null +++ b/src/icons/providers/gemini.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/src/icons/providers/index.tsx b/src/icons/providers/index.tsx new file mode 100644 index 000000000..e2d7bd51a --- /dev/null +++ b/src/icons/providers/index.tsx @@ -0,0 +1,69 @@ +import React, { useMemo } from "react"; +import { cn } from "@/lib/utils"; +import { providerIcons, providerTypeToIcon } from "./utils"; + +interface ProviderIconProps { + providerType: string; + size?: number | string; + className?: string; + showFallback?: boolean; +} + +export const ProviderIcon: React.FC = ({ + providerType, + size = 24, + className, + showFallback = true, +}) => { + const iconName = providerTypeToIcon[providerType] || providerType; + const iconSvg = providerIcons[iconName]; + + const sizeStyle = useMemo(() => { + const sizeValue = typeof size === "number" ? `${size}px` : size; + return { + width: sizeValue, + height: sizeValue, + fontSize: sizeValue, + lineHeight: 1, + }; + }, [size]); + + if (iconSvg) { + return ( + + ); + } + + // Fallback:显示首字母 + if (showFallback) { + const initials = providerType + .split("_") + .map((word) => word[0]) + .join("") + .toUpperCase() + .slice(0, 2); + const fallbackFontSize = + typeof size === "number" ? `${Math.max(size * 0.5, 12)}px` : "0.5em"; + return ( + + {initials} + + ); + } + + return null; +}; diff --git a/src/icons/providers/openai.svg b/src/icons/providers/openai.svg new file mode 100644 index 000000000..50d94d6c1 --- /dev/null +++ b/src/icons/providers/openai.svg @@ -0,0 +1 @@ +OpenAI \ No newline at end of file diff --git a/src/icons/providers/utils.ts b/src/icons/providers/utils.ts new file mode 100644 index 000000000..252bab937 --- /dev/null +++ b/src/icons/providers/utils.ts @@ -0,0 +1,61 @@ +// Provider 图标辅助函数 + +// Provider 类型到图标名称的映射 +const providerTypeToIcon: Record = { + kiro: "kiro", + gemini: "gemini", + qwen: "qwen", + antigravity: "gemini", + openai: "openai", + claude: "claude", + anthropic: "anthropic", + codex: "openai", + claude_oauth: "claude", + iflow: "openai", + amp: "amp", + google: "google", + alibaba: "alibaba", + copilot: "copilot", + deepseek: "deepseek", + zhipu: "zhipu", + kimi: "kimi", + minimax: "minimax", + doubao: "doubao", + azure: "azure", +}; + +// Provider 图标 SVG 内联定义 +export const providerIcons: Record = { + aws: ``, + gemini: ``, + anthropic: ``, + claude: ``, + qwen: ``, + google: ``, + openai: ``, + alibaba: ``, + copilot: ``, + amp: ``, + kiro: ``, + deepseek: ``, + zhipu: ``, + kimi: ``, + minimax: ``, + doubao: ``, + azure: ``, +}; + +// 导出图标名称检查函数 +export const hasProviderIcon = (providerType: string): boolean => { + const iconName = providerTypeToIcon[providerType] || providerType; + return iconName in providerIcons; +}; + +// 导出获取图标 SVG 函数 +export const getProviderIcon = (providerType: string): string | undefined => { + const iconName = providerTypeToIcon[providerType] || providerType; + return providerIcons[iconName]; +}; + +// 导出 Provider 类型映射 +export { providerTypeToIcon };