diff --git a/package-lock.json b/package-lock.json index 80ddcb2..c0f4b57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "AIClient2API", + "name": "AIClient-2-API", "lockfileVersion": 3, "requires": true, "packages": { @@ -11,9 +11,12 @@ "deepmerge": "^4.3.1", "dotenv": "^16.4.5", "google-auth-library": "^10.1.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "lodash": "^4.17.21", "multer": "^2.0.2", "open": "^10.2.0", + "socks-proxy-agent": "^8.0.5", "undici": "^7.12.0", "uuid": "^11.1.0" }, @@ -97,6 +100,7 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2955,6 +2959,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -4060,6 +4065,19 @@ "dev": true, "license": "MIT" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -4131,6 +4149,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -6028,6 +6055,44 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 4a6368b..f6c1f57 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,12 @@ "deepmerge": "^4.3.1", "dotenv": "^16.4.5", "google-auth-library": "^10.1.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "lodash": "^4.17.21", "multer": "^2.0.2", "open": "^10.2.0", + "socks-proxy-agent": "^8.0.5", "undici": "^7.12.0", "uuid": "^11.1.0" }, diff --git a/src/claude/claude-core.js b/src/claude/claude-core.js index 01789d2..7dd8ea2 100644 --- a/src/claude/claude-core.js +++ b/src/claude/claude-core.js @@ -1,6 +1,7 @@ import axios from 'axios'; import * as http from 'http'; import * as https from 'https'; +import { configureAxiosProxy } from '../proxy-utils.js'; /** * Claude API Core Service Class. @@ -59,6 +60,9 @@ export class ClaudeApiService { axiosConfig.proxy = false; } + // 配置自定义代理 + configureAxiosProxy(axiosConfig, this.config, 'claude-custom'); + return axios.create(axiosConfig); } diff --git a/src/claude/claude-kiro.js b/src/claude/claude-kiro.js index afec11f..eaf8c91 100644 --- a/src/claude/claude-kiro.js +++ b/src/claude/claude-kiro.js @@ -8,7 +8,7 @@ import * as http from 'http'; import * as https from 'https'; import { getProviderModels } from '../provider-models.js'; import { countTokens } from '@anthropic-ai/tokenizer'; -import { json } from 'stream/consumers'; +import { configureAxiosProxy } from '../proxy-utils.js'; const KIRO_CONSTANTS = { REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', @@ -357,6 +357,9 @@ export class KiroApiService { axiosConfig.proxy = false; } + // 配置自定义代理 + configureAxiosProxy(axiosConfig, this.config, 'claude-kiro-oauth'); + this.axiosInstance = axios.create(axiosConfig); this.isInitialized = true; } @@ -528,7 +531,7 @@ async initializeAuth(forceRefresh = false) { if (!this.accessToken) { throw new Error('No access token available after initialization and refresh attempts.'); } -} + } /** * Extract text content from OpenAI message format diff --git a/src/config-manager.js b/src/config-manager.js index b591dc1..0432bf8 100644 --- a/src/config-manager.js +++ b/src/config-manager.js @@ -69,6 +69,8 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP MODEL_PROVIDER: MODEL_PROVIDER.GEMINI_CLI, SYSTEM_PROMPT_FILE_PATH: INPUT_SYSTEM_PROMPT_FILE, // Default value SYSTEM_PROMPT_MODE: 'append', + PROXY_URL: null, // HTTP/HTTPS/SOCKS5 代理地址,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080 + PROXY_ENABLED_PROVIDERS: [], // 启用代理的提供商列表,如 ['gemini-cli-oauth', 'claude-kiro-oauth'] PROMPT_LOG_BASE_NAME: "prompt_log", PROMPT_LOG_MODE: "none", REQUEST_MAX_RETRIES: 3, diff --git a/src/gemini/antigravity-core.js b/src/gemini/antigravity-core.js index e7f4cfc..b546f26 100644 --- a/src/gemini/antigravity-core.js +++ b/src/gemini/antigravity-core.js @@ -10,6 +10,7 @@ import open from 'open'; import { formatExpiryTime } from '../common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiAntigravityOAuth } from '../oauth-handlers.js'; +import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js'; // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 const httpAgent = new http.Agent({ @@ -225,14 +226,25 @@ function ensureRolesInContents(requestBody) { export class AntigravityApiService { constructor(config) { + // 检查是否需要使用代理 + const proxyConfig = getGoogleAuthProxyConfig(config, 'gemini-antigravity'); + // 配置 OAuth2Client 使用自定义的 HTTP agent - this.authClient = new OAuth2Client({ + const oauth2Options = { clientId: OAUTH_CLIENT_ID, clientSecret: OAUTH_CLIENT_SECRET, - transporterOptions: { + }; + + if (proxyConfig) { + oauth2Options.transporterOptions = proxyConfig; + console.log('[Antigravity] Using proxy for OAuth2Client'); + } else { + oauth2Options.transporterOptions = { agent: httpsAgent, - }, - }); + }; + } + + this.authClient = new OAuth2Client(oauth2Options); this.availableModels = []; this.isInitialized = false; @@ -252,6 +264,9 @@ export class AntigravityApiService { this.baseUrlAutopush // ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释 ]; + + // 保存代理配置供后续使用 + this.proxyConfig = getProxyConfigForProvider(config, 'gemini-antigravity'); } async initialize() { diff --git a/src/gemini/gemini-core.js b/src/gemini/gemini-core.js index 6ca6954..cd14ac3 100644 --- a/src/gemini/gemini-core.js +++ b/src/gemini/gemini-core.js @@ -9,6 +9,7 @@ import open from 'open'; import { API_ACTIONS, formatExpiryTime } from '../common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiCliOAuth } from '../oauth-handlers.js'; +import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../proxy-utils.js'; // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 const httpAgent = new http.Agent({ @@ -193,14 +194,25 @@ async function* apply_anti_truncation_to_stream(service, model, requestBody) { export class GeminiApiService { constructor(config) { + // 检查是否需要使用代理 + const proxyConfig = getGoogleAuthProxyConfig(config, 'gemini-cli-oauth'); + // 配置 OAuth2Client 使用自定义的 HTTP agent - this.authClient = new OAuth2Client({ + const oauth2Options = { clientId: OAUTH_CLIENT_ID, clientSecret: OAUTH_CLIENT_SECRET, - transporterOptions: { + }; + + if (proxyConfig) { + oauth2Options.transporterOptions = proxyConfig; + console.log('[Gemini] Using proxy for OAuth2Client'); + } else { + oauth2Options.transporterOptions = { agent: httpsAgent, - }, - }); + }; + } + + this.authClient = new OAuth2Client(oauth2Options); this.availableModels = []; this.isInitialized = false; @@ -212,6 +224,9 @@ export class GeminiApiService { this.codeAssistEndpoint = config.GEMINI_BASE_URL || DEFAULT_CODE_ASSIST_ENDPOINT; this.apiVersion = DEFAULT_CODE_ASSIST_API_VERSION; + + // 保存代理配置供后续使用 + this.proxyConfig = getProxyConfigForProvider(config, 'gemini-cli-oauth'); } async initialize() { diff --git a/src/openai/openai-core.js b/src/openai/openai-core.js index ad77b90..c22a164 100644 --- a/src/openai/openai-core.js +++ b/src/openai/openai-core.js @@ -1,6 +1,7 @@ import axios from 'axios'; import * as http from 'http'; import * as https from 'https'; +import { configureAxiosProxy } from '../proxy-utils.js'; // Assumed OpenAI API specification service for interacting with third-party models export class OpenAIApiService { @@ -43,6 +44,9 @@ export class OpenAIApiService { axiosConfig.proxy = false; } + // 配置自定义代理 + configureAxiosProxy(axiosConfig, config, 'openai-custom'); + this.axiosInstance = axios.create(axiosConfig); } diff --git a/src/openai/openai-responses-core.js b/src/openai/openai-responses-core.js index 80e15c5..b753bc6 100644 --- a/src/openai/openai-responses-core.js +++ b/src/openai/openai-responses-core.js @@ -1,6 +1,7 @@ import axios from 'axios'; import * as http from 'http'; import * as https from 'https'; +import { configureAxiosProxy } from '../proxy-utils.js'; // OpenAI Responses API specification service for interacting with third-party models export class OpenAIResponsesApiService { @@ -42,6 +43,9 @@ export class OpenAIResponsesApiService { if (!this.useSystemProxy) { axiosConfig.proxy = false; } + + // 配置自定义代理 (使用 openai-custom 的代理配置) + configureAxiosProxy(axiosConfig, config, 'openai-custom'); this.axiosInstance = axios.create(axiosConfig); } diff --git a/src/openai/qwen-core.js b/src/openai/qwen-core.js index cfda2b0..7ff8e36 100644 --- a/src/openai/qwen-core.js +++ b/src/openai/qwen-core.js @@ -10,6 +10,7 @@ import { EventEmitter } from 'events'; import { randomUUID } from 'node:crypto'; import { getProviderModels } from '../provider-models.js'; import { handleQwenOAuth } from '../oauth-handlers.js'; +import { configureAxiosProxy } from '../proxy-utils.js'; // --- Constants --- const QWEN_DIR = '.qwen'; @@ -222,6 +223,9 @@ export class QwenApiService { axiosConfig.proxy = false; } + // 配置自定义代理 + configureAxiosProxy(axiosConfig, this.config, 'openai-qwen-oauth'); + this.currentAxiosInstance = axios.create(axiosConfig); this.isInitialized = true; @@ -512,6 +516,9 @@ export class QwenApiService { axiosConfig.proxy = false; } + // 配置自定义代理 + configureAxiosProxy(axiosConfig, this.config, 'openai-qwen-oauth'); + this.currentAxiosInstance = axios.create(axiosConfig); // Process message content before sending the request diff --git a/src/proxy-utils.js b/src/proxy-utils.js new file mode 100644 index 0000000..d1fb27c --- /dev/null +++ b/src/proxy-utils.js @@ -0,0 +1,128 @@ +/** + * 代理工具模块 + * 支持 HTTP、HTTPS 和 SOCKS5 代理 + */ + +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { HttpProxyAgent } from 'http-proxy-agent'; +import { SocksProxyAgent } from 'socks-proxy-agent'; + +/** + * 解析代理URL并返回相应的代理配置 + * @param {string} proxyUrl - 代理URL,如 http://127.0.0.1:7890 或 socks5://127.0.0.1:1080 + * @returns {Object|null} 代理配置对象,包含 httpAgent 和 httpsAgent + */ +export function parseProxyUrl(proxyUrl) { + if (!proxyUrl || typeof proxyUrl !== 'string') { + return null; + } + + const trimmedUrl = proxyUrl.trim(); + if (!trimmedUrl) { + return null; + } + + try { + const url = new URL(trimmedUrl); + const protocol = url.protocol.toLowerCase(); + + if (protocol === 'socks5:' || protocol === 'socks4:' || protocol === 'socks:') { + // SOCKS 代理 + const socksAgent = new SocksProxyAgent(trimmedUrl); + return { + httpAgent: socksAgent, + httpsAgent: socksAgent, + proxyType: 'socks' + }; + } else if (protocol === 'http:' || protocol === 'https:') { + // HTTP/HTTPS 代理 + return { + httpAgent: new HttpProxyAgent(trimmedUrl), + httpsAgent: new HttpsProxyAgent(trimmedUrl), + proxyType: 'http' + }; + } else { + console.warn(`[Proxy] Unsupported proxy protocol: ${protocol}`); + return null; + } + } catch (error) { + console.error(`[Proxy] Failed to parse proxy URL: ${error.message}`); + return null; + } +} + +/** + * 检查指定的提供商是否启用了代理 + * @param {Object} config - 配置对象 + * @param {string} providerType - 提供商类型 + * @returns {boolean} 是否启用代理 + */ +export function isProxyEnabledForProvider(config, providerType) { + if (!config || !config.PROXY_URL || !config.PROXY_ENABLED_PROVIDERS) { + return false; + } + + const enabledProviders = config.PROXY_ENABLED_PROVIDERS; + if (!Array.isArray(enabledProviders)) { + return false; + } + + return enabledProviders.includes(providerType); +} + +/** + * 获取指定提供商的代理配置 + * @param {Object} config - 配置对象 + * @param {string} providerType - 提供商类型 + * @returns {Object|null} 代理配置对象或 null + */ +export function getProxyConfigForProvider(config, providerType) { + if (!isProxyEnabledForProvider(config, providerType)) { + return null; + } + + const proxyConfig = parseProxyUrl(config.PROXY_URL); + if (proxyConfig) { + console.log(`[Proxy] Using ${proxyConfig.proxyType} proxy for ${providerType}: ${config.PROXY_URL}`); + } + return proxyConfig; +} + +/** + * 为 axios 配置代理 + * @param {Object} axiosConfig - axios 配置对象 + * @param {Object} config - 应用配置对象 + * @param {string} providerType - 提供商类型 + * @returns {Object} 更新后的 axios 配置 + */ +export function configureAxiosProxy(axiosConfig, config, providerType) { + const proxyConfig = getProxyConfigForProvider(config, providerType); + + if (proxyConfig) { + // 使用代理 agent + axiosConfig.httpAgent = proxyConfig.httpAgent; + axiosConfig.httpsAgent = proxyConfig.httpsAgent; + // 禁用 axios 内置的代理配置,使用我们的 agent + axiosConfig.proxy = false; + } + + return axiosConfig; +} + +/** + * 为 google-auth-library 配置代理 + * @param {Object} config - 应用配置对象 + * @param {string} providerType - 提供商类型 + * @returns {Object|null} transporter 配置对象或 null + */ +export function getGoogleAuthProxyConfig(config, providerType) { + const proxyConfig = getProxyConfigForProvider(config, providerType); + + if (proxyConfig) { + return { + agent: proxyConfig.httpsAgent + }; + } + + return null; +} diff --git a/src/ui-manager.js b/src/ui-manager.js index ed2e4f0..294b3e5 100644 --- a/src/ui-manager.js +++ b/src/ui-manager.js @@ -701,6 +701,10 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo if (newConfig.PROVIDER_POOLS_FILE_PATH !== undefined) currentConfig.PROVIDER_POOLS_FILE_PATH = newConfig.PROVIDER_POOLS_FILE_PATH; if (newConfig.MAX_ERROR_COUNT !== undefined) currentConfig.MAX_ERROR_COUNT = newConfig.MAX_ERROR_COUNT; if (newConfig.providerFallbackChain !== undefined) currentConfig.providerFallbackChain = newConfig.providerFallbackChain; + + // Proxy settings + if (newConfig.PROXY_URL !== undefined) currentConfig.PROXY_URL = newConfig.PROXY_URL; + if (newConfig.PROXY_ENABLED_PROVIDERS !== undefined) currentConfig.PROXY_ENABLED_PROVIDERS = newConfig.PROXY_ENABLED_PROVIDERS; // Handle system prompt update if (newConfig.systemPrompt !== undefined) { @@ -743,7 +747,9 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN, PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH, MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT, - providerFallbackChain: currentConfig.providerFallbackChain + providerFallbackChain: currentConfig.providerFallbackChain, + PROXY_URL: currentConfig.PROXY_URL, + PROXY_ENABLED_PROVIDERS: currentConfig.PROXY_ENABLED_PROVIDERS }; writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8'); diff --git a/static/app/config-manager.js b/static/app/config-manager.js index 9e43f5a..2ff3386 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -91,6 +91,17 @@ async function loadConfiguration() { } } + // 加载代理配置 + const proxyUrlEl = document.getElementById('proxyUrl'); + if (proxyUrlEl) proxyUrlEl.value = data.PROXY_URL || ''; + + // 加载启用代理的提供商 + const proxyProviderCheckboxes = document.querySelectorAll('input[name="proxyProvider"]'); + const enabledProviders = data.PROXY_ENABLED_PROVIDERS || []; + proxyProviderCheckboxes.forEach(checkbox => { + checkbox.checked = enabledProviders.includes(checkbox.value); + }); + } catch (error) { console.error('Failed to load configuration:', error); } @@ -149,6 +160,13 @@ async function saveConfiguration() { } else { config.providerFallbackChain = {}; } + + // 保存代理配置 + config.PROXY_URL = document.getElementById('proxyUrl')?.value?.trim() || null; + + // 获取启用代理的提供商列表 + const proxyProviderCheckboxes = document.querySelectorAll('input[name="proxyProvider"]:checked'); + config.PROXY_ENABLED_PROVIDERS = Array.from(proxyProviderCheckboxes).map(cb => cb.value); try { await window.apiClient.post('/config', config); diff --git a/static/app/i18n.js b/static/app/i18n.js index 7b8861d..427635d 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -211,6 +211,12 @@ const translations = { 'config.advanced.adminPassword': '后台登录密码', 'config.advanced.adminPasswordPlaceholder': '设置后台登录密码(留空则不修改)', 'config.advanced.adminPasswordNote': '用于保护管理控制台的访问,修改后需要重新登录', + 'config.proxy.title': '代理设置', + 'config.proxy.url': '代理地址', + 'config.proxy.urlPlaceholder': '例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080', + 'config.proxy.urlNote': '支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理', + 'config.proxy.enabledProviders': '启用代理的提供商', + 'config.proxy.enabledProvidersNote': '选择需要通过代理访问的提供商,未选中的提供商将直接连接', 'config.save': '保存配置', 'config.reset': '重置', 'config.placeholder.nodeName': '例如: 我的节点1', @@ -620,6 +626,12 @@ const translations = { 'config.advanced.adminPassword': 'Admin Password', 'config.advanced.adminPasswordPlaceholder': 'Set admin password (leave empty to keep unchanged)', 'config.advanced.adminPasswordNote': 'Used to protect management console access, requires re-login after modification', + 'config.proxy.title': 'Proxy Settings', + 'config.proxy.url': 'Proxy URL', + 'config.proxy.urlPlaceholder': 'e.g.: http://127.0.0.1:7890 or socks5://127.0.0.1:1080', + 'config.proxy.urlNote': 'Supports HTTP, HTTPS and SOCKS5 proxies. Leave empty to disable proxy', + 'config.proxy.enabledProviders': 'Providers Using Proxy', + 'config.proxy.enabledProvidersNote': 'Select providers that should use the proxy. Unselected providers will connect directly', 'config.save': 'Save Configuration', 'config.reset': 'Reset', 'config.placeholder.nodeName': 'e.g.: My Node 1', diff --git a/static/app/styles.css b/static/app/styles.css index 0e2615c..84d4215 100644 --- a/static/app/styles.css +++ b/static/app/styles.css @@ -551,6 +551,65 @@ textarea.form-control { color: var(--primary-color); } +/* 代理配置区域 */ +.proxy-config-section { + border: 1px solid var(--border-color); + border-radius: 0.5rem; + padding: 1rem 1.5rem; + margin-bottom: 1.5rem; + background: var(--bg-primary); +} + +.proxy-config-section h4 { + color: var(--text-primary); + font-size: 1rem; + font-weight: 600; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.proxy-config-section h4 i { + color: var(--primary-color); +} + +/* 复选框组样式 */ +.checkbox-group { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.375rem; + background: var(--bg-secondary); + transition: all 0.2s ease; +} + +.checkbox-label:hover { + border-color: var(--primary-color); + background: var(--bg-hover); +} + +.checkbox-label input[type="checkbox"] { + width: 1rem; + height: 1rem; + accent-color: var(--primary-color); + cursor: pointer; +} + +.checkbox-label input[type="checkbox"]:checked + span { + color: var(--primary-color); + font-weight: 500; +} + .config-row { display: grid; grid-template-columns: 1fr 1fr; diff --git a/static/index.html b/static/index.html index c8448cd..654afbc 100644 --- a/static/index.html +++ b/static/index.html @@ -584,6 +584,46 @@

高级配置

+ +
+

代理设置

+
+ + + 支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理 +
+
+ +
+ + + + + + +
+ 选择需要通过代理访问的提供商,未选中的提供商将直接连接 +
+
+