diff --git a/src/components/settings/GlobalProxySettings.tsx b/src/components/settings/GlobalProxySettings.tsx
index 6d0bd0bb1..5d0caa24f 100644
--- a/src/components/settings/GlobalProxySettings.tsx
+++ b/src/components/settings/GlobalProxySettings.tsx
@@ -1,14 +1,14 @@
/**
* 全局出站代理设置组件
*
- * 提供配置全局代理的输入界面。
+ * 提供配置全局代理的输入界面,支持用户名密码认证。
*/
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
-import { Loader2, TestTube2, Search } from "lucide-react";
+import { Loader2, TestTube2, Search, Eye, EyeOff, X } from "lucide-react";
import {
useGlobalProxyUrl,
useSetGlobalProxyUrl,
@@ -17,6 +17,56 @@ import {
type DetectedProxy,
} from "@/hooks/useGlobalProxy";
+/** 从完整 URL 提取认证信息 */
+function extractAuth(url: string): {
+ baseUrl: string;
+ username: string;
+ password: string;
+} {
+ if (!url.trim()) return { baseUrl: "", username: "", password: "" };
+
+ try {
+ const parsed = new URL(url);
+ const username = decodeURIComponent(parsed.username || "");
+ const password = decodeURIComponent(parsed.password || "");
+ // 移除认证信息,获取基础 URL
+ parsed.username = "";
+ parsed.password = "";
+ return { baseUrl: parsed.toString(), username, password };
+ } catch {
+ return { baseUrl: url, username: "", password: "" };
+ }
+}
+
+/** 将认证信息合并到 URL */
+function mergeAuth(
+ baseUrl: string,
+ username: string,
+ password: string,
+): string {
+ if (!baseUrl.trim()) return "";
+ if (!username.trim()) return baseUrl;
+
+ try {
+ const parsed = new URL(baseUrl);
+ parsed.username = encodeURIComponent(username.trim());
+ if (password) {
+ parsed.password = encodeURIComponent(password);
+ }
+ return parsed.toString();
+ } catch {
+ // URL 解析失败,尝试手动插入
+ const match = baseUrl.match(/^(\w+:\/\/)(.+)$/);
+ if (match) {
+ const auth = password
+ ? `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`
+ : `${encodeURIComponent(username)}@`;
+ return `${match[1]}${auth}${match[2]}`;
+ }
+ return baseUrl;
+ }
+}
+
export function GlobalProxySettings() {
const { t } = useTranslation();
const { data: savedUrl, isLoading } = useGlobalProxyUrl();
@@ -25,25 +75,37 @@ export function GlobalProxySettings() {
const scanMutation = useScanProxies();
const [url, setUrl] = useState("");
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [showPassword, setShowPassword] = useState(false);
const [dirty, setDirty] = useState(false);
const [detected, setDetected] = useState([]);
+ // 计算完整 URL(含认证信息)
+ const fullUrl = useMemo(
+ () => mergeAuth(url, username, password),
+ [url, username, password],
+ );
+
// 同步远程配置
useEffect(() => {
if (savedUrl !== undefined) {
- setUrl(savedUrl || "");
+ const { baseUrl, username: u, password: p } = extractAuth(savedUrl || "");
+ setUrl(baseUrl);
+ setUsername(u);
+ setPassword(p);
setDirty(false);
}
}, [savedUrl]);
const handleSave = async () => {
- await setMutation.mutateAsync(url.trim());
+ await setMutation.mutateAsync(fullUrl);
setDirty(false);
};
const handleTest = async () => {
- if (url.trim()) {
- await testMutation.mutateAsync(url.trim());
+ if (fullUrl) {
+ await testMutation.mutateAsync(fullUrl);
}
};
@@ -53,11 +115,21 @@ export function GlobalProxySettings() {
};
const handleSelect = (proxyUrl: string) => {
- setUrl(proxyUrl);
+ const { baseUrl, username: u, password: p } = extractAuth(proxyUrl);
+ setUrl(baseUrl);
+ setUsername(u);
+ setPassword(p);
setDirty(true);
setDetected([]);
};
+ const handleClear = () => {
+ setUrl("");
+ setUsername("");
+ setPassword("");
+ setDirty(true);
+ };
+
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && dirty && !setMutation.isPending) {
handleSave();
@@ -80,7 +152,7 @@ export function GlobalProxySettings() {
{t("settings.globalProxy.hint")}
- {/* 输入框和按钮 */}
+ {/* 代理地址输入框和按钮 */}
@@ -118,6 +190,15 @@ export function GlobalProxySettings() {
)}
+
+ {/* 认证信息:用户名 + 密码(可选) */}
+
+
{/* 扫描结果 */}
{detected.length > 0 && (
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 974f0f59b..8df92f7b0 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -278,9 +278,12 @@
"saving": "Saving...",
"globalProxy": {
"label": "Global Proxy",
- "hint": "Proxy all requests (API, Skills download, etc.). Leave empty for direct connection. Supports HTTP and SOCKS5.",
+ "hint": "Proxy all requests (API, Skills download, etc.). Leave empty for direct connection.",
+ "username": "Username (optional)",
+ "password": "Password (optional)",
"test": "Test Connection",
"scan": "Scan Local Proxies",
+ "clear": "Clear",
"scanFailed": "Scan failed: {{error}}",
"saved": "Proxy settings saved",
"saveFailed": "Save failed: {{error}}",
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index 0ec72b728..fc269082e 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -278,9 +278,12 @@
"saving": "保存中...",
"globalProxy": {
"label": "グローバルプロキシ",
- "hint": "すべてのリクエスト(API、Skills ダウンロードなど)をプロキシ経由で送信します。空欄で直接接続。HTTP と SOCKS5 をサポート。",
+ "hint": "すべてのリクエスト(API、Skills ダウンロードなど)をプロキシ経由で送信します。空欄で直接接続。",
+ "username": "ユーザー名(任意)",
+ "password": "パスワード(任意)",
"test": "接続テスト",
"scan": "ローカルプロキシをスキャン",
+ "clear": "クリア",
"scanFailed": "スキャンに失敗しました: {{error}}",
"saved": "プロキシ設定を保存しました",
"saveFailed": "保存に失敗しました: {{error}}",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 39c177793..de9d1f81a 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -278,9 +278,12 @@
"saving": "正在保存...",
"globalProxy": {
"label": "全局代理",
- "hint": "代理所有请求(API、Skills 下载等)。留空表示直连。支持 HTTP 和 SOCKS5 代理。",
+ "hint": "代理所有请求(API、Skills 下载等)。留空表示直连。",
+ "username": "用户名(可选)",
+ "password": "密码(可选)",
"test": "测试连接",
"scan": "扫描本地代理",
+ "clear": "清除",
"scanFailed": "扫描失败:{{error}}",
"saved": "代理设置已保存",
"saveFailed": "保存失败:{{error}}",