feat(proxy): add global proxy settings support

Add ability to configure a global HTTP/HTTPS proxy for all outbound
requests including provider API calls, speed tests, and stream checks.
This commit is contained in:
YoVinchen
2026-01-11 12:26:15 +08:00
parent 7bc9dbbbb0
commit 42d1d23618
18 changed files with 686 additions and 88 deletions
@@ -0,0 +1,114 @@
/**
* 全局出站代理设置组件
*
* 提供配置全局代理的输入界面。
*/
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Loader2, TestTube2, Globe } from "lucide-react";
import {
useGlobalProxyUrl,
useSetGlobalProxyUrl,
useTestProxy,
} from "@/hooks/useGlobalProxy";
export function GlobalProxySettings() {
const { t } = useTranslation();
const { data: savedUrl, isLoading } = useGlobalProxyUrl();
const setMutation = useSetGlobalProxyUrl();
const testMutation = useTestProxy();
const [url, setUrl] = useState("");
const [dirty, setDirty] = useState(false);
// 同步远程配置
useEffect(() => {
if (savedUrl !== undefined) {
setUrl(savedUrl || "");
setDirty(false);
}
}, [savedUrl]);
const handleSave = async () => {
await setMutation.mutateAsync(url.trim());
setDirty(false);
};
const handleTest = async () => {
if (url.trim()) {
await testMutation.mutateAsync(url.trim());
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && dirty && !setMutation.isPending) {
handleSave();
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="space-y-3">
{/* 标题 */}
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<Label className="text-sm font-medium">
{t("settings.globalProxy.label")}
</Label>
</div>
{/* 描述 */}
<p className="text-xs text-muted-foreground">
{t("settings.globalProxy.hint")}
</p>
{/* 输入框和按钮 */}
<div className="flex gap-2">
<Input
placeholder="http://127.0.0.1:7890 / socks5://127.0.0.1:1080"
value={url}
onChange={(e) => {
setUrl(e.target.value);
setDirty(true);
}}
onKeyDown={handleKeyDown}
className="font-mono text-sm flex-1"
/>
<Button
variant="outline"
size="icon"
disabled={!url.trim() || testMutation.isPending}
onClick={handleTest}
title={t("settings.globalProxy.test")}
>
{testMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<TestTube2 className="h-4 w-4" />
)}
</Button>
<Button
onClick={handleSave}
disabled={!dirty || setMutation.isPending}
size="sm"
>
{setMutation.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{t("common.save")}
</Button>
</div>
</div>
);
}
+2
View File
@@ -34,6 +34,7 @@ import { WindowSettings } from "@/components/settings/WindowSettings";
import { DirectorySettings } from "@/components/settings/DirectorySettings";
import { ImportExportSection } from "@/components/settings/ImportExportSection";
import { AboutSection } from "@/components/settings/AboutSection";
import { GlobalProxySettings } from "@/components/settings/GlobalProxySettings";
import { ProxyPanel } from "@/components/proxy";
import { PricingConfigPanel } from "@/components/usage/PricingConfigPanel";
import { ModelTestConfigPanel } from "@/components/usage/ModelTestConfigPanel";
@@ -238,6 +239,7 @@ export function SettingsPage({
settings={settings}
onChange={handleAutoSave}
/>
<GlobalProxySettings />
</motion.div>
) : null}
</TabsContent>
+84
View File
@@ -0,0 +1,84 @@
/**
* 全局出站代理 React Hooks
*
* 提供获取、设置和测试全局代理的 React Query hooks。
*/
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import {
getGlobalProxyUrl,
setGlobalProxyUrl,
testProxyUrl,
getUpstreamProxyStatus,
type ProxyTestResult,
type UpstreamProxyStatus,
} from "@/lib/api/globalProxy";
/**
* 获取全局代理 URL
*/
export function useGlobalProxyUrl() {
return useQuery({
queryKey: ["globalProxyUrl"],
queryFn: getGlobalProxyUrl,
});
}
/**
* 设置全局代理 URL
*/
export function useSetGlobalProxyUrl() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: setGlobalProxyUrl,
onSuccess: () => {
toast.success(t("settings.globalProxy.saved"));
queryClient.invalidateQueries({ queryKey: ["globalProxyUrl"] });
queryClient.invalidateQueries({ queryKey: ["upstreamProxyStatus"] });
},
onError: (error: Error) => {
toast.error(
t("settings.globalProxy.saveFailed", { error: error.message })
);
},
});
}
/**
* 测试代理连接
*/
export function useTestProxy() {
const { t } = useTranslation();
return useMutation({
mutationFn: testProxyUrl,
onSuccess: (result: ProxyTestResult) => {
if (result.success) {
toast.success(
t("settings.globalProxy.testSuccess", { latency: result.latencyMs })
);
} else {
toast.error(
t("settings.globalProxy.testFailed", { error: result.error })
);
}
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
/**
* 获取当前出站代理状态
*/
export function useUpstreamProxyStatus() {
return useQuery<UpstreamProxyStatus>({
queryKey: ["upstreamProxyStatus"],
queryFn: getUpstreamProxyStatus,
});
}
+11 -2
View File
@@ -271,7 +271,16 @@
"restartLater": "Restart Later",
"restartFailed": "Application restart failed, please manually close and reopen.",
"devModeRestartHint": "Dev Mode: Automatic restart not supported, please manually restart the application.",
"saving": "Saving..."
"saving": "Saving...",
"globalProxy": {
"label": "Global Proxy",
"hint": "Proxy all requests (API, Skills download, etc.). Leave empty for direct connection. Supports HTTP and SOCKS5.",
"test": "Test Connection",
"saved": "Proxy settings saved",
"saveFailed": "Save failed: {{error}}",
"testSuccess": "Connected! Latency {{latency}}ms",
"testFailed": "Connection failed: {{error}}"
}
},
"apps": {
"claude": "Claude Code",
@@ -1229,4 +1238,4 @@
"configJsonPreview": "Config JSON Preview",
"configJsonPreviewHint": "The following configurations will be synced to each app (only the displayed fields will be overwritten, other custom settings will be preserved)"
}
}
}
+11 -2
View File
@@ -271,7 +271,16 @@
"restartLater": "稍后重启",
"restartFailed": "应用重启失败,请手动关闭后重新打开。",
"devModeRestartHint": "开发模式下不支持自动重启,请手动重新启动应用。",
"saving": "正在保存..."
"saving": "正在保存...",
"globalProxy": {
"label": "全局代理",
"hint": "代理所有请求(API、Skills 下载等)。留空表示直连。支持 HTTP 和 SOCKS5 代理。",
"test": "测试连接",
"saved": "代理设置已保存",
"saveFailed": "保存失败:{{error}}",
"testSuccess": "连接成功!延迟 {{latency}}ms",
"testFailed": "连接失败:{{error}}"
}
},
"apps": {
"claude": "Claude Code",
@@ -1229,4 +1238,4 @@
"configJsonPreview": "配置 JSON 预览",
"configJsonPreviewHint": "以下是将要同步到各应用的配置内容(仅覆盖显示的字段,保留其他自定义配置)"
}
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* 全局出站代理 API
*
* 提供获取、设置和测试全局代理的功能。
*/
import { invoke } from "@tauri-apps/api/core";
/**
* 代理测试结果
*/
export interface ProxyTestResult {
success: boolean;
latencyMs: number;
error: string | null;
}
/**
* 出站代理状态
*/
export interface UpstreamProxyStatus {
enabled: boolean;
proxyUrl: string | null;
}
/**
* 获取全局代理 URL
*
* @returns 代理 URLnull 表示未配置(直连)
*/
export async function getGlobalProxyUrl(): Promise<string | null> {
return invoke<string | null>("get_global_proxy_url");
}
/**
* 设置全局代理 URL
*
* @param url - 代理 URL(如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080
* 空字符串表示清除代理(直连)
*/
export async function setGlobalProxyUrl(url: string): Promise<void> {
return invoke("set_global_proxy_url", { url });
}
/**
* 测试代理连接
*
* @param url - 要测试的代理 URL
* @returns 测试结果,包含是否成功、延迟和错误信息
*/
export async function testProxyUrl(url: string): Promise<ProxyTestResult> {
return invoke<ProxyTestResult>("test_proxy_url", { url });
}
/**
* 获取当前出站代理状态
*
* @returns 代理状态,包含是否启用和代理 URL
*/
export async function getUpstreamProxyStatus(): Promise<UpstreamProxyStatus> {
return invoke<UpstreamProxyStatus>("get_upstream_proxy_status");
}