From 7da91107046cb457e63e23f0a15340326645fe89 Mon Sep 17 00:00:00 2001 From: snaily Date: Wed, 30 Apr 2025 10:57:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20(HTTP/SOCKS5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为应用程序添加了通过代理服务器访问 Gemini API 的功能。 主要变更包括: * **配置**: * 在 `.env.example` 和 `app/config/config.py` 中添加了 `PROXIES` 配置项,允许用户指定一个或多个 HTTP 或 SOCKS5 代理服务器列表。 * 更新 `README.md` 以包含关于代理配置的说明。 * **后端**: * 修改 `app/service/client/api_client.py` 中的 `GeminiApiClient`,使其在发起请求时能从配置的 `PROXIES` 列表中随机选择一个代理使用。 * 添加了 `app/log/logger.py` 中的 `get_api_client_logger`,用于记录 API 客户端(包括代理使用)的相关日志。 * **前端**: * 在 `app/templates/config_editor.html` 配置编辑器页面添加了代理列表的显示区域和“添加代理”按钮。 * 实现了用于批量添加代理的模态框 UI。 * 在 `app/static/js/config_editor.js` 中添加了处理代理列表显示、打开/关闭模态框以及处理批量添加代理(包括提取、去重和更新 UI)的 JavaScript 逻辑。 * 确保在初始化配置时为 `PROXIES` 设置默认空列表。 此功能使得用户可以在需要通过代理访问外部网络的环境下使用该应用。 --- .env.example | 3 + README.md | 2 + app/config/config.py | 3 +- app/log/logger.py | 5 ++ app/service/client/api_client.py | 19 ++++++- app/static/js/config_editor.js | 94 +++++++++++++++++++++++++++++++- app/templates/config_editor.html | 32 +++++++++++ 7 files changed, 151 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index fc24275..add0e86 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,9 @@ CHECK_INTERVAL_HOURS=1 TIMEZONE=Asia/Shanghai # 请求超时时间(秒) TIME_OUT=300 +# 代理服务器配置 (支持 http 和 socks5) +# 示例: PROXIES=["http://user:pass@host:port", "socks5://host:port"] +PROXIES=[] #########################image_generate 相关配置########################### PAID_KEY=AIzaSyxxxxxxxxxxxxxxxxxxx CREATE_IMAGE_MODEL=imagen-3.0-generate-002 diff --git a/README.md b/README.md index b693b9c..98f9bf1 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ app/ >镜像地址: docker pull ghcr.io/snailyp/gemini-balance:latest * **模型列表自动维护**: 支持openai和gemini模型列表获取,与newapi自动获取模型列表完美兼容,无需手动填写。 * **支持移除不使用的模型**: 默认提供的模型太多,很多用不上,可以通过`FILTERED_MODELS`过滤掉。 +* **代理支持**: 支持配置 HTTP/SOCKS5 代理服务器 (`PROXIES`),用于访问 Gemini API,方便在特殊网络环境下使用。支持批量添加代理。 ## 🚀 快速开始 @@ -166,6 +167,7 @@ app/ | `CHECK_INTERVAL_HOURS` | 可选,检查禁用 Key 是否恢复的时间间隔 (小时) | `1` | | `TIMEZONE` | 可选,应用程序使用的时区 | `Asia/Shanghai` | | `TIME_OUT` | 可选,请求超时时间 (秒) | `300` | +| `PROXIES` | 可选,代理服务器列表 (例如 `http://user:pass@host:port`, `socks5://host:port`) | `[]` | | `LOG_LEVEL` | 可选,日志级别,例如 DEBUG, INFO, WARNING, ERROR, CRITICAL | `INFO` | | **图像生成相关** | | | | `PAID_KEY` | 可选,付费版API Key,用于图片生成等高级功能 | `your-paid-api-key` | diff --git a/app/config/config.py b/app/config/config.py index 5d811bd..c6ae5da 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -30,7 +30,8 @@ class Settings(BaseSettings): TEST_MODEL: str = DEFAULT_MODEL TIME_OUT: int = DEFAULT_TIMEOUT MAX_RETRIES: int = MAX_RETRIES - + PROXIES: List[str] = [] # 新增:代理服务器列表 + # 模型相关配置 SEARCH_MODELS: List[str] = ["gemini-2.0-flash-exp"] IMAGE_MODELS: List[str] = ["gemini-2.0-flash-exp"] diff --git a/app/log/logger.py b/app/log/logger.py index 4408f42..11c1cc6 100644 --- a/app/log/logger.py +++ b/app/log/logger.py @@ -207,5 +207,10 @@ def get_update_logger(): def get_scheduler_routes(): return Logger.setup_logger("scheduler_routes") + def get_message_converter_logger(): return Logger.setup_logger("message_converter") + + +def get_api_client_logger(): + return Logger.setup_logger("api_client") \ No newline at end of file diff --git a/app/service/client/api_client.py b/app/service/client/api_client.py index 1747756..bde5601 100644 --- a/app/service/client/api_client.py +++ b/app/service/client/api_client.py @@ -2,10 +2,13 @@ from typing import Dict, Any, AsyncGenerator import httpx +import random from abc import ABC, abstractmethod - +from app.config.config import settings +from app.log.logger import get_api_client_logger from app.core.constants import DEFAULT_TIMEOUT +logger = get_api_client_logger() class ApiClient(ABC): """API客户端基类""" @@ -41,7 +44,12 @@ class GeminiApiClient(ApiClient): timeout = httpx.Timeout(self.timeout, read=self.timeout) model = self._get_real_model(model) - async with httpx.AsyncClient(timeout=timeout) as client: + proxy_to_use = None + if settings.PROXIES: + proxy_to_use = random.choice(settings.PROXIES) + logger.info(f"using proxy: {proxy_to_use}") + + async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client: # 修改:直接传递代理字符串 url = f"{self.base_url}/models/{model}:generateContent?key={api_key}" response = await client.post(url, json=payload) if response.status_code != 200: @@ -53,7 +61,12 @@ class GeminiApiClient(ApiClient): timeout = httpx.Timeout(self.timeout, read=self.timeout) model = self._get_real_model(model) - async with httpx.AsyncClient(timeout=timeout) as client: + proxy_to_use = None + if settings.PROXIES: + proxy_to_use = random.choice(settings.PROXIES) + logger.info(f"using proxy: {proxy_to_use}") + + async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client: # 修改:直接传递代理字符串 url = f"{self.base_url}/models/{model}:streamGenerateContent?alt=sse&key={api_key}" async with client.stream(method="POST", url=url, json=payload) as response: if response.status_code != 200: diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index aa186e4..0f5b793 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -63,7 +63,16 @@ document.addEventListener('DOMContentLoaded', function() { const cancelBulkDeleteApiKeyBtn = document.getElementById('cancelBulkDeleteApiKeyBtn'); // 新增 const confirmBulkDeleteApiKeyBtn = document.getElementById('confirmBulkDeleteApiKeyBtn'); // 新增 const bulkDeleteApiKeyInput = document.getElementById('bulkDeleteApiKeyInput'); // 新增 - + + // --- 新增:Proxy 模态框相关 --- + const proxyModal = document.getElementById('proxyModal'); + const addProxyBtn = document.getElementById('addProxyBtn'); // Changed from bulkAddProxyBtn + const closeProxyModalBtn = document.getElementById('closeProxyModalBtn'); + const cancelAddProxyBtn = document.getElementById('cancelAddProxyBtn'); + const confirmAddProxyBtn = document.getElementById('confirmAddProxyBtn'); + const proxyBulkInput = document.getElementById('proxyBulkInput'); + // --- 结束:Proxy 模态框相关 --- + // --- 新增:重置确认模态框相关 --- const resetConfirmModal = document.getElementById('resetConfirmModal'); const closeResetModalBtn = document.getElementById('closeResetModalBtn'); @@ -111,8 +120,11 @@ document.addEventListener('DOMContentLoaded', function() { if (event.target == bulkDeleteApiKeyModal) { // 新增对批量删除模态框的处理 bulkDeleteApiKeyModal.classList.remove('show'); } + if (event.target == proxyModal) { // 新增对代理模态框的处理 + proxyModal.classList.remove('show'); + } }); - + // 确认添加 API Key if (confirmAddApiKeyBtn) { confirmAddApiKeyBtn.addEventListener('click', handleBulkAddApiKeys); @@ -158,7 +170,42 @@ document.addEventListener('DOMContentLoaded', function() { } // --- 结束:批量删除 API Key 相关 --- // --- 结束:API Key 相关 --- - + + // --- 新增:Proxy 模态框事件 --- + // 打开模态框 (Changed event listener to addProxyBtn) + if (addProxyBtn) { + addProxyBtn.addEventListener('click', () => { + if (proxyModal) { + proxyModal.classList.add('show'); + } + if (proxyBulkInput) proxyBulkInput.value = ''; // 清空输入框 + }); + } + + // 关闭模态框 (X 按钮) + if (closeProxyModalBtn) { + closeProxyModalBtn.addEventListener('click', () => { + if (proxyModal) { + proxyModal.classList.remove('show'); + } + }); + } + + // 关闭模态框 (取消按钮) + if (cancelAddProxyBtn) { + cancelAddProxyBtn.addEventListener('click', () => { + if (proxyModal) { + proxyModal.classList.remove('show'); + } + }); + } + + // 确认添加 Proxy + if (confirmAddProxyBtn) { + confirmAddProxyBtn.addEventListener('click', handleBulkAddProxies); + } + // --- 结束:Proxy 模态框事件 --- + // --- 新增:重置确认模态框事件监听 (移到 DOMContentLoaded 内部) --- if (closeResetModalBtn) { closeResetModalBtn.addEventListener('click', () => { @@ -265,6 +312,10 @@ async function initConfig() { if (!config.FILTERED_MODELS || !Array.isArray(config.FILTERED_MODELS) || config.FILTERED_MODELS.length === 0) { config.FILTERED_MODELS = ['gemini-1.0-pro-latest']; } + // --- 新增:处理 PROXIES 默认值 --- + if (!config.PROXIES || !Array.isArray(config.PROXIES)) { + config.PROXIES = []; // 默认为空数组 + } // --- 新增:处理新字段的默认值 --- if (!config.THINKING_MODELS || !Array.isArray(config.THINKING_MODELS)) { config.THINKING_MODELS = []; // 默认为空数组 @@ -296,6 +347,7 @@ async function initConfig() { SEARCH_MODELS: ['gemini-1.5-flash-latest'], FILTERED_MODELS: ['gemini-1.0-pro-latest'], UPLOAD_PROVIDER: 'smms', + PROXIES: [], // 添加默认值 THINKING_MODELS: [], THINKING_BUDGET_MAP: {} }; @@ -521,6 +573,42 @@ function handleBulkDeleteApiKeys() { bulkDeleteTextarea.value = ''; } +// --- 新增:处理批量添加 Proxy 的逻辑 --- +function handleBulkAddProxies() { + const proxyBulkInput = document.getElementById('proxyBulkInput'); + const proxyContainer = document.getElementById('PROXIES_container'); + const proxyModal = document.getElementById('proxyModal'); + + if (!proxyBulkInput || !proxyContainer || !proxyModal) return; + + const bulkText = proxyBulkInput.value; + // 匹配 http(s):// 或 socks5:// 格式的代理,允许包含用户名密码 + const proxyRegex = /(?:https?|socks5):\/\/(?:[^:@\/]+(?::[^@\/]+)?@)?(?:[^:\/\s]+)(?::\d+)?/g; + const extractedProxies = bulkText.match(proxyRegex) || []; + + // 获取当前已有的 proxies + const currentProxyInputs = proxyContainer.querySelectorAll('.array-input'); + const currentProxies = Array.from(currentProxyInputs).map(input => input.value).filter(proxy => proxy.trim() !== ''); + + // 合并并去重 + const combinedProxies = new Set([...currentProxies, ...extractedProxies]); + const uniqueProxies = Array.from(combinedProxies); + + // 清空现有列表显示 + const existingItems = proxyContainer.querySelectorAll('.array-item'); + existingItems.forEach(item => item.remove()); + + // 重新填充列表 + uniqueProxies.forEach(proxy => { + addArrayItemWithValue('PROXIES', proxy); + }); + + // 关闭模态框 + proxyModal.classList.remove('show'); + showNotification(`添加/更新了 ${uniqueProxies.length} 个唯一代理`, 'success'); +} +// --- 结束:处理批量添加 Proxy 的逻辑 --- + // 切换标签 function switchTab(tabId) { // 更新标签按钮状态 diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html index 4758a0b..e83e21c 100644 --- a/app/templates/config_editor.html +++ b/app/templates/config_editor.html @@ -182,6 +182,19 @@ API请求失败后的最大重试次数 + +
+ +
+ +
+
+ +
+ 代理服务器列表,支持 http 和 socks5 格式,例如: http://user:pass@host:port 或 socks5://host:port。点击按钮可批量添加。 +
@@ -511,6 +524,25 @@ + + + +