mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-11 18:09:55 +08:00
feat: 添加代理支持 (HTTP/SOCKS5)
为应用程序添加了通过代理服务器访问 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` 设置默认空列表。
此功能使得用户可以在需要通过代理访问外部网络的环境下使用该应用。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
// 更新标签按钮状态
|
||||
|
||||
@@ -182,6 +182,19 @@
|
||||
<input type="number" id="MAX_RETRIES" name="MAX_RETRIES" min="0" max="10" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
|
||||
<small class="text-gray-500 mt-1 block">API请求失败后的最大重试次数</small>
|
||||
</div>
|
||||
<!-- 代理服务器列表 -->
|
||||
<div class="mb-6">
|
||||
<label for="PROXIES" class="block font-semibold mb-2 text-gray-700">代理服务器列表</label>
|
||||
<div class="array-container bg-white rounded-lg border border-gray-200 p-4 mb-2" id="PROXIES_container">
|
||||
<!-- 代理项将在这里动态添加 -->
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2" id="addProxyBtn">
|
||||
<i class="fas fa-plus"></i> 添加代理
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-gray-500 mt-1 block">代理服务器列表,支持 http 和 socks5 格式,例如: http://user:pass@host:port 或 socks5://host:port。点击按钮可批量添加。</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型相关配置 -->
|
||||
@@ -511,6 +524,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proxy Add Modal -->
|
||||
<div id="proxyModal" class="modal">
|
||||
<div class="w-full max-w-lg mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden animate-fade-in">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold text-gray-800">批量添加代理服务器</h2>
|
||||
<button id="closeProxyModalBtn" class="text-gray-400 hover:text-gray-600 text-xl">×</button>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-4">每行粘贴一个或多个代理地址,将自动提取有效地址并去重。</p>
|
||||
<textarea id="proxyBulkInput" rows="10" placeholder="在此处粘贴代理地址 (例如 http://user:pass@host:port 或 socks5://host:port)..." class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 font-mono text-sm"></textarea>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button type="button" id="confirmAddProxyBtn" class="bg-primary-600 hover:bg-primary-700 text-white px-6 py-2 rounded-lg font-medium transition">确认添加</button>
|
||||
<button type="button" id="cancelAddProxyBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-6 py-2 rounded-lg font-medium transition">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Confirmation Modal -->
|
||||
<div id="resetConfirmModal" class="modal">
|
||||
<div class="w-full max-w-md mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden animate-fade-in">
|
||||
|
||||
Reference in New Issue
Block a user