mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-06 20:32:47 +08:00
Merge pull request #244 from icesixgod:feature/proxy-health-check
feat: 实现代理健康检查功能
This commit is contained in:
@@ -11,6 +11,7 @@ from pydantic import BaseModel, Field
|
||||
from app.core.security import verify_auth_token
|
||||
from app.log.logger import Logger, get_config_routes_logger
|
||||
from app.service.config.config_service import ConfigService
|
||||
from app.service.proxy.proxy_check_service import get_proxy_check_service, ProxyCheckResult
|
||||
from app.utils.helpers import redact_key_for_logging
|
||||
|
||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||
@@ -132,3 +133,93 @@ async def get_ui_models(request: Request):
|
||||
status_code=500,
|
||||
detail=f"An unexpected error occurred while fetching UI models: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
class ProxyCheckRequest(BaseModel):
|
||||
"""Proxy check request"""
|
||||
proxy: str = Field(..., description="Proxy address to check")
|
||||
use_cache: bool = Field(True, description="Whether to use cached results")
|
||||
|
||||
|
||||
class ProxyBatchCheckRequest(BaseModel):
|
||||
"""Batch proxy check request"""
|
||||
proxies: List[str] = Field(..., description="List of proxy addresses to check")
|
||||
use_cache: bool = Field(True, description="Whether to use cached results")
|
||||
max_concurrent: int = Field(5, description="Maximum concurrent check count", ge=1, le=10)
|
||||
|
||||
|
||||
@router.post("/proxy/check", response_model=ProxyCheckResult)
|
||||
async def check_single_proxy(proxy_request: ProxyCheckRequest, request: Request):
|
||||
"""Check if a single proxy is available"""
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
logger.warning("Unauthorized access attempt to proxy check")
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
|
||||
try:
|
||||
logger.info(f"Checking single proxy: {proxy_request.proxy}")
|
||||
proxy_service = get_proxy_check_service()
|
||||
result = await proxy_service.check_single_proxy(
|
||||
proxy_request.proxy,
|
||||
proxy_request.use_cache
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Proxy check failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Proxy check failed: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/proxy/check-all", response_model=List[ProxyCheckResult])
|
||||
async def check_all_proxies(batch_request: ProxyBatchCheckRequest, request: Request):
|
||||
"""Check multiple proxies availability"""
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
logger.warning("Unauthorized access attempt to batch proxy check")
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
|
||||
try:
|
||||
logger.info(f"Batch checking {len(batch_request.proxies)} proxies")
|
||||
proxy_service = get_proxy_check_service()
|
||||
results = await proxy_service.check_multiple_proxies(
|
||||
batch_request.proxies,
|
||||
batch_request.use_cache,
|
||||
batch_request.max_concurrent
|
||||
)
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Batch proxy check failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Batch proxy check failed: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/proxy/cache-stats")
|
||||
async def get_proxy_cache_stats(request: Request):
|
||||
"""Get proxy check cache statistics"""
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
logger.warning("Unauthorized access attempt to proxy cache stats")
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
|
||||
try:
|
||||
proxy_service = get_proxy_check_service()
|
||||
stats = proxy_service.get_cache_stats()
|
||||
return stats
|
||||
except Exception as e:
|
||||
logger.error(f"Get proxy cache stats failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Get cache stats failed: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/proxy/clear-cache")
|
||||
async def clear_proxy_cache(request: Request):
|
||||
"""Clear proxy check cache"""
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
if not auth_token or not verify_auth_token(auth_token):
|
||||
logger.warning("Unauthorized access attempt to clear proxy cache")
|
||||
return RedirectResponse(url="/", status_code=302)
|
||||
|
||||
try:
|
||||
proxy_service = get_proxy_check_service()
|
||||
proxy_service.clear_cache()
|
||||
return {"success": True, "message": "Proxy check cache cleared"}
|
||||
except Exception as e:
|
||||
logger.error(f"Clear proxy cache failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Clear cache failed: {str(e)}")
|
||||
|
||||
7
app/service/proxy/__init__.py
Normal file
7
app/service/proxy/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Proxy service module
|
||||
"""
|
||||
|
||||
from .proxy_check_service import ProxyCheckService
|
||||
|
||||
__all__ = ["ProxyCheckService"]
|
||||
219
app/service/proxy/proxy_check_service.py
Normal file
219
app/service/proxy/proxy_check_service.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Proxy detection service module
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.log.logger import get_config_routes_logger
|
||||
|
||||
logger = get_config_routes_logger()
|
||||
|
||||
|
||||
class ProxyCheckResult(BaseModel):
|
||||
"""Proxy check result model"""
|
||||
proxy: str
|
||||
is_available: bool
|
||||
response_time: Optional[float] = None
|
||||
error_message: Optional[str] = None
|
||||
checked_at: float
|
||||
|
||||
|
||||
class ProxyCheckService:
|
||||
"""Proxy detection service class"""
|
||||
|
||||
# Target URL for checking
|
||||
CHECK_URL = "https://www.google.com"
|
||||
# Timeout in seconds
|
||||
TIMEOUT_SECONDS = 10
|
||||
# Cache duration in seconds
|
||||
CACHE_DURATION = 10 # 10s
|
||||
|
||||
def __init__(self):
|
||||
self._cache: Dict[str, ProxyCheckResult] = {}
|
||||
|
||||
def _is_valid_proxy_format(self, proxy: str) -> bool:
|
||||
"""Validate proxy format"""
|
||||
try:
|
||||
parsed = urlparse(proxy)
|
||||
return parsed.scheme in ['http', 'https', 'socks5'] and parsed.hostname
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _get_cached_result(self, proxy: str) -> Optional[ProxyCheckResult]:
|
||||
"""Get cached check result"""
|
||||
if proxy in self._cache:
|
||||
result = self._cache[proxy]
|
||||
# Check if cache is expired
|
||||
if time.time() - result.checked_at < self.CACHE_DURATION:
|
||||
logger.debug(f"Using cached proxy check result: {proxy}")
|
||||
return result
|
||||
else:
|
||||
# Remove expired cache
|
||||
del self._cache[proxy]
|
||||
return None
|
||||
|
||||
def _cache_result(self, result: ProxyCheckResult) -> None:
|
||||
"""Cache check result"""
|
||||
self._cache[result.proxy] = result
|
||||
|
||||
async def check_single_proxy(self, proxy: str, use_cache: bool = True) -> ProxyCheckResult:
|
||||
"""
|
||||
Check if a single proxy is available
|
||||
|
||||
Args:
|
||||
proxy: Proxy address in format like http://host:port or socks5://host:port
|
||||
use_cache: Whether to use cached results
|
||||
|
||||
Returns:
|
||||
ProxyCheckResult: Check result
|
||||
"""
|
||||
# Check cache first
|
||||
if use_cache:
|
||||
cached = self._get_cached_result(proxy)
|
||||
if cached:
|
||||
return cached
|
||||
|
||||
# Validate proxy format
|
||||
if not self._is_valid_proxy_format(proxy):
|
||||
result = ProxyCheckResult(
|
||||
proxy=proxy,
|
||||
is_available=False,
|
||||
error_message="Invalid proxy format",
|
||||
checked_at=time.time()
|
||||
)
|
||||
self._cache_result(result)
|
||||
return result
|
||||
|
||||
# Perform check
|
||||
start_time = time.time()
|
||||
try:
|
||||
logger.info(f"Starting proxy check: {proxy}")
|
||||
|
||||
timeout = httpx.Timeout(self.TIMEOUT_SECONDS, read=self.TIMEOUT_SECONDS)
|
||||
async with httpx.AsyncClient(timeout=timeout, proxy=proxy) as client:
|
||||
response = await client.head(self.CHECK_URL)
|
||||
|
||||
response_time = time.time() - start_time
|
||||
|
||||
# Check response status
|
||||
is_available = response.status_code in [200, 204, 301, 302, 307, 308]
|
||||
|
||||
result = ProxyCheckResult(
|
||||
proxy=proxy,
|
||||
is_available=is_available,
|
||||
response_time=round(response_time, 3),
|
||||
error_message=None if is_available else f"HTTP {response.status_code}",
|
||||
checked_at=time.time()
|
||||
)
|
||||
|
||||
logger.info(f"Proxy check completed: {proxy}, available: {is_available}, response_time: {response_time:.3f}s")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
result = ProxyCheckResult(
|
||||
proxy=proxy,
|
||||
is_available=False,
|
||||
error_message="Connection timeout",
|
||||
checked_at=time.time()
|
||||
)
|
||||
logger.warning(f"Proxy check timeout: {proxy}")
|
||||
|
||||
except Exception as e:
|
||||
result = ProxyCheckResult(
|
||||
proxy=proxy,
|
||||
is_available=False,
|
||||
error_message=str(e),
|
||||
checked_at=time.time()
|
||||
)
|
||||
logger.error(f"Proxy check failed: {proxy}, error: {str(e)}")
|
||||
|
||||
# Cache result
|
||||
self._cache_result(result)
|
||||
return result
|
||||
|
||||
async def check_multiple_proxies(
|
||||
self,
|
||||
proxies: List[str],
|
||||
use_cache: bool = True,
|
||||
max_concurrent: int = 5
|
||||
) -> List[ProxyCheckResult]:
|
||||
"""
|
||||
Check multiple proxies concurrently
|
||||
|
||||
Args:
|
||||
proxies: List of proxy addresses
|
||||
use_cache: Whether to use cached results
|
||||
max_concurrent: Maximum concurrent check count
|
||||
|
||||
Returns:
|
||||
List[ProxyCheckResult]: List of check results
|
||||
"""
|
||||
if not proxies:
|
||||
return []
|
||||
|
||||
logger.info(f"Starting batch proxy check for {len(proxies)} proxies")
|
||||
|
||||
# Use semaphore to limit concurrency
|
||||
semaphore = asyncio.Semaphore(max_concurrent)
|
||||
|
||||
async def check_with_semaphore(proxy: str) -> ProxyCheckResult:
|
||||
async with semaphore:
|
||||
return await self.check_single_proxy(proxy, use_cache)
|
||||
|
||||
# Execute checks concurrently
|
||||
tasks = [check_with_semaphore(proxy) for proxy in proxies]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Handle exception results
|
||||
final_results = []
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"Proxy check task exception: {proxies[i]}, error: {str(result)}")
|
||||
final_results.append(ProxyCheckResult(
|
||||
proxy=proxies[i],
|
||||
is_available=False,
|
||||
error_message=f"Check task exception: {str(result)}",
|
||||
checked_at=time.time()
|
||||
))
|
||||
else:
|
||||
final_results.append(result)
|
||||
|
||||
available_count = sum(1 for r in final_results if r.is_available)
|
||||
logger.info(f"Batch proxy check completed: {available_count}/{len(proxies)} proxies available")
|
||||
|
||||
return final_results
|
||||
|
||||
def get_cache_stats(self) -> Dict[str, int]:
|
||||
"""Get cache statistics"""
|
||||
current_time = time.time()
|
||||
valid_cache_count = sum(
|
||||
1 for result in self._cache.values()
|
||||
if current_time - result.checked_at < self.CACHE_DURATION
|
||||
)
|
||||
|
||||
return {
|
||||
"total_cached": len(self._cache),
|
||||
"valid_cached": valid_cache_count,
|
||||
"expired_cached": len(self._cache) - valid_cache_count
|
||||
}
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Clear all cache"""
|
||||
self._cache.clear()
|
||||
logger.info("Proxy check cache cleared")
|
||||
|
||||
|
||||
# Global instance
|
||||
_proxy_check_service: Optional[ProxyCheckService] = None
|
||||
|
||||
|
||||
def get_proxy_check_service() -> ProxyCheckService:
|
||||
"""Get proxy check service instance"""
|
||||
global _proxy_check_service
|
||||
if _proxy_check_service is None:
|
||||
_proxy_check_service = ProxyCheckService()
|
||||
return _proxy_check_service
|
||||
@@ -184,6 +184,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const closeProxyModalBtn = document.getElementById("closeProxyModalBtn");
|
||||
const cancelAddProxyBtn = document.getElementById("cancelAddProxyBtn");
|
||||
const confirmAddProxyBtn = document.getElementById("confirmAddProxyBtn");
|
||||
|
||||
// Proxy Check Elements and Events
|
||||
const checkAllProxiesBtn = document.getElementById("checkAllProxiesBtn");
|
||||
const proxyCheckModal = document.getElementById("proxyCheckModal");
|
||||
const closeProxyCheckModalBtn = document.getElementById("closeProxyCheckModalBtn");
|
||||
const closeProxyCheckBtn = document.getElementById("closeProxyCheckBtn");
|
||||
const retryFailedProxiesBtn = document.getElementById("retryFailedProxiesBtn");
|
||||
|
||||
if (addProxyBtn) {
|
||||
addProxyBtn.addEventListener("click", () => {
|
||||
@@ -191,6 +198,25 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (proxyBulkInput) proxyBulkInput.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
if (checkAllProxiesBtn) {
|
||||
checkAllProxiesBtn.addEventListener("click", checkAllProxies);
|
||||
}
|
||||
|
||||
if (closeProxyCheckModalBtn) {
|
||||
closeProxyCheckModalBtn.addEventListener("click", () => closeModal(proxyCheckModal));
|
||||
}
|
||||
|
||||
if (closeProxyCheckBtn) {
|
||||
closeProxyCheckBtn.addEventListener("click", () => closeModal(proxyCheckModal));
|
||||
}
|
||||
|
||||
if (retryFailedProxiesBtn) {
|
||||
retryFailedProxiesBtn.addEventListener("click", () => {
|
||||
// 重试失败的代理检测
|
||||
checkAllProxies();
|
||||
});
|
||||
}
|
||||
if (closeProxyModalBtn)
|
||||
closeProxyModalBtn.addEventListener("click", () => closeModal(proxyModal));
|
||||
if (cancelAddProxyBtn)
|
||||
@@ -1455,6 +1481,45 @@ function createRemoveButton() {
|
||||
return removeBtn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a proxy status icon for displaying proxy check status.
|
||||
* @returns {HTMLSpanElement} The status icon element.
|
||||
*/
|
||||
function createProxyStatusIcon() {
|
||||
const statusIcon = document.createElement("span");
|
||||
statusIcon.className = "proxy-status-icon px-2 py-2 text-gray-400";
|
||||
statusIcon.innerHTML = '<i class="fas fa-question-circle" title="未检测"></i>';
|
||||
statusIcon.setAttribute("data-status", "unknown");
|
||||
return statusIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a proxy check button for individual proxy checking.
|
||||
* @returns {HTMLButtonElement} The check button element.
|
||||
*/
|
||||
function createProxyCheckButton() {
|
||||
const checkBtn = document.createElement("button");
|
||||
checkBtn.type = "button";
|
||||
checkBtn.className =
|
||||
"proxy-check-btn px-2 py-2 text-blue-500 hover:text-blue-700 focus:outline-none transition-colors duration-150 rounded-r-md";
|
||||
checkBtn.innerHTML = '<i class="fas fa-globe"></i>';
|
||||
checkBtn.title = "检测此代理";
|
||||
|
||||
// 添加点击事件监听器
|
||||
checkBtn.addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const inputElement = this.closest('.flex').querySelector('.array-input');
|
||||
if (inputElement && inputElement.value.trim()) {
|
||||
checkSingleProxy(inputElement.value.trim(), this);
|
||||
} else {
|
||||
showNotification("请先输入代理地址", "warning");
|
||||
}
|
||||
});
|
||||
|
||||
return checkBtn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new item to an array configuration section (e.g., API_KEYS, ALLOWED_TOKENS).
|
||||
* This function is typically called by a "+" button.
|
||||
@@ -1486,6 +1551,7 @@ function addArrayItemWithValue(key, value) {
|
||||
const isThinkingModel = key === "THINKING_MODELS";
|
||||
const isAllowedToken = key === "ALLOWED_TOKENS";
|
||||
const isVertexApiKey = key === "VERTEX_API_KEYS"; // 新增判断
|
||||
const isProxy = key === "PROXIES"; // 新增代理判断
|
||||
const isSensitive = key === "API_KEYS" || isAllowedToken || isVertexApiKey; // 更新敏感判断
|
||||
const modelId = isThinkingModel ? generateUUID() : null;
|
||||
|
||||
@@ -1513,6 +1579,13 @@ function addArrayItemWithValue(key, value) {
|
||||
if (isAllowedToken) {
|
||||
const generateBtn = createGenerateTokenButton();
|
||||
inputWrapper.appendChild(generateBtn);
|
||||
} else if (isProxy) {
|
||||
// 为代理添加状态显示和检测按钮
|
||||
const proxyStatusIcon = createProxyStatusIcon();
|
||||
inputWrapper.appendChild(proxyStatusIcon);
|
||||
|
||||
const proxyCheckBtn = createProxyCheckButton();
|
||||
inputWrapper.appendChild(proxyCheckBtn);
|
||||
} else {
|
||||
// Ensure right-side rounding if no button is present
|
||||
input.classList.add("rounded-r-md");
|
||||
@@ -2299,3 +2372,241 @@ function handleModelSelection(selectedModelId) {
|
||||
}
|
||||
|
||||
// -- End Model Helper Functions --
|
||||
|
||||
// -- Proxy Check Functions --
|
||||
|
||||
/**
|
||||
* 检测单个代理是否可用
|
||||
* @param {string} proxy - 代理地址
|
||||
* @param {HTMLElement} buttonElement - 触发检测的按钮元素
|
||||
*/
|
||||
async function checkSingleProxy(proxy, buttonElement) {
|
||||
const statusIcon = buttonElement.parentElement.querySelector('.proxy-status-icon');
|
||||
const originalButtonContent = buttonElement.innerHTML;
|
||||
|
||||
try {
|
||||
// 更新UI状态为检测中
|
||||
buttonElement.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
buttonElement.disabled = true;
|
||||
if (statusIcon) {
|
||||
statusIcon.className = "proxy-status-icon px-2 py-2 text-blue-500";
|
||||
statusIcon.innerHTML = '<i class="fas fa-spinner fa-spin" title="检测中..."></i>';
|
||||
statusIcon.setAttribute("data-status", "checking");
|
||||
}
|
||||
|
||||
const response = await fetch('/api/config/proxy/check', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
proxy: proxy,
|
||||
use_cache: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`检测请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
updateProxyStatus(statusIcon, result);
|
||||
|
||||
// 显示检测结果通知
|
||||
if (result.is_available) {
|
||||
showNotification(`代理可用 (${result.response_time}s)`, "success");
|
||||
} else {
|
||||
showNotification(`代理不可用: ${result.error_message}`, "error");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('代理检测失败:', error);
|
||||
if (statusIcon) {
|
||||
statusIcon.className = "proxy-status-icon px-2 py-2 text-red-500";
|
||||
statusIcon.innerHTML = '<i class="fas fa-times-circle" title="检测失败"></i>';
|
||||
statusIcon.setAttribute("data-status", "error");
|
||||
}
|
||||
showNotification(`检测失败: ${error.message}`, "error");
|
||||
} finally {
|
||||
// 恢复按钮状态
|
||||
buttonElement.innerHTML = originalButtonContent;
|
||||
buttonElement.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新代理状态图标
|
||||
* @param {HTMLElement} statusIcon - 状态图标元素
|
||||
* @param {Object} result - 检测结果
|
||||
*/
|
||||
function updateProxyStatus(statusIcon, result) {
|
||||
if (!statusIcon) return;
|
||||
|
||||
if (result.is_available) {
|
||||
statusIcon.className = "proxy-status-icon px-2 py-2 text-green-500";
|
||||
statusIcon.innerHTML = `<i class="fas fa-check-circle" title="可用 (${result.response_time}s)"></i>`;
|
||||
statusIcon.setAttribute("data-status", "available");
|
||||
} else {
|
||||
statusIcon.className = "proxy-status-icon px-2 py-2 text-red-500";
|
||||
statusIcon.innerHTML = `<i class="fas fa-times-circle" title="不可用: ${result.error_message}"></i>`;
|
||||
statusIcon.setAttribute("data-status", "unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测所有代理
|
||||
*/
|
||||
async function checkAllProxies() {
|
||||
const proxyContainer = document.getElementById("PROXIES_container");
|
||||
if (!proxyContainer) return;
|
||||
|
||||
const proxyInputs = proxyContainer.querySelectorAll('.array-input');
|
||||
const proxies = Array.from(proxyInputs)
|
||||
.map(input => input.value.trim())
|
||||
.filter(proxy => proxy.length > 0);
|
||||
|
||||
if (proxies.length === 0) {
|
||||
showNotification("没有代理需要检测", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
// 打开检测结果模态框
|
||||
const proxyCheckModal = document.getElementById("proxyCheckModal");
|
||||
if (proxyCheckModal) {
|
||||
openModal(proxyCheckModal);
|
||||
|
||||
// 显示进度
|
||||
const progressContainer = document.getElementById("proxyCheckProgress");
|
||||
const summaryContainer = document.getElementById("proxyCheckSummary");
|
||||
const resultsContainer = document.getElementById("proxyCheckResults");
|
||||
|
||||
if (progressContainer) progressContainer.classList.remove("hidden");
|
||||
if (summaryContainer) summaryContainer.classList.add("hidden");
|
||||
if (resultsContainer) resultsContainer.innerHTML = "";
|
||||
|
||||
// 更新总数
|
||||
const totalCountElement = document.getElementById("totalCount");
|
||||
if (totalCountElement) totalCountElement.textContent = proxies.length;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config/proxy/check-all', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
proxies: proxies,
|
||||
use_cache: true,
|
||||
max_concurrent: 5
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`批量检测请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const results = await response.json();
|
||||
displayProxyCheckResults(results);
|
||||
updateProxyStatusInList(results);
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量代理检测失败:', error);
|
||||
showNotification(`批量检测失败: ${error.message}`, "error");
|
||||
if (resultsContainer) {
|
||||
resultsContainer.innerHTML = `<div class="text-red-500 text-center py-4">检测失败: ${error.message}</div>`;
|
||||
}
|
||||
} finally {
|
||||
// 隐藏进度
|
||||
if (progressContainer) progressContainer.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示代理检测结果
|
||||
* @param {Array} results - 检测结果数组
|
||||
*/
|
||||
function displayProxyCheckResults(results) {
|
||||
const summaryContainer = document.getElementById("proxyCheckSummary");
|
||||
const resultsContainer = document.getElementById("proxyCheckResults");
|
||||
const availableCountElement = document.getElementById("availableCount");
|
||||
const unavailableCountElement = document.getElementById("unavailableCount");
|
||||
const retryButton = document.getElementById("retryFailedProxiesBtn");
|
||||
|
||||
if (!resultsContainer) return;
|
||||
|
||||
// 统计结果
|
||||
const availableCount = results.filter(r => r.is_available).length;
|
||||
const unavailableCount = results.length - availableCount;
|
||||
|
||||
// 更新概览
|
||||
if (availableCountElement) availableCountElement.textContent = availableCount;
|
||||
if (unavailableCountElement) unavailableCountElement.textContent = unavailableCount;
|
||||
if (summaryContainer) summaryContainer.classList.remove("hidden");
|
||||
|
||||
// 显示重试按钮(如果有失败的代理)
|
||||
if (retryButton) {
|
||||
if (unavailableCount > 0) {
|
||||
retryButton.classList.remove("hidden");
|
||||
} else {
|
||||
retryButton.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// 清空并填充结果
|
||||
resultsContainer.innerHTML = "";
|
||||
|
||||
results.forEach(result => {
|
||||
const resultItem = document.createElement("div");
|
||||
resultItem.className = `flex items-center justify-between p-3 border rounded-lg ${
|
||||
result.is_available ? 'border-green-200 bg-green-50' : 'border-red-200 bg-red-50'
|
||||
}`;
|
||||
|
||||
const statusIcon = result.is_available ?
|
||||
'<i class="fas fa-check-circle text-green-500"></i>' :
|
||||
'<i class="fas fa-times-circle text-red-500"></i>';
|
||||
|
||||
const responseTimeText = result.response_time ?
|
||||
` (${result.response_time}s)` : '';
|
||||
|
||||
const errorText = result.error_message ?
|
||||
`<span class="text-red-600 text-sm ml-2">${result.error_message}</span>` : '';
|
||||
|
||||
resultItem.innerHTML = `
|
||||
<div class="flex items-center gap-3">
|
||||
${statusIcon}
|
||||
<span class="font-mono text-sm">${result.proxy}</span>
|
||||
${responseTimeText}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm ${result.is_available ? 'text-green-700' : 'text-red-700'}">
|
||||
${result.is_available ? '可用' : '不可用'}
|
||||
</span>
|
||||
${errorText}
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultsContainer.appendChild(resultItem);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据检测结果更新代理列表中的状态图标
|
||||
* @param {Array} results - 检测结果数组
|
||||
*/
|
||||
function updateProxyStatusInList(results) {
|
||||
const proxyContainer = document.getElementById("PROXIES_container");
|
||||
if (!proxyContainer) return;
|
||||
|
||||
results.forEach(result => {
|
||||
const proxyInputs = proxyContainer.querySelectorAll('.array-input');
|
||||
proxyInputs.forEach(input => {
|
||||
if (input.value.trim() === result.proxy) {
|
||||
const statusIcon = input.parentElement.querySelector('.proxy-status-icon');
|
||||
updateProxyStatus(statusIcon, result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -- End Proxy Check Functions --
|
||||
|
||||
@@ -1232,6 +1232,13 @@ endblock %} {% block head_extra_styles %}
|
||||
>
|
||||
<i class="fas fa-trash-alt"></i> 删除代理
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||
id="checkAllProxiesBtn"
|
||||
>
|
||||
<i class="fas fa-globe"></i> 检测所有代理
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||
@@ -2614,6 +2621,84 @@ endblock %} {% block head_extra_styles %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proxy Check Results Modal -->
|
||||
<div id="proxyCheckModal" class="modal">
|
||||
<div
|
||||
class="w-full max-w-4xl mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
|
||||
style="
|
||||
background-color: rgba(255, 255, 255, 0.98);
|
||||
color: #374151;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
"
|
||||
>
|
||||
<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="closeProxyCheckModalBtn"
|
||||
class="text-gray-300 hover:text-gray-800 text-xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 检测状态和进度 -->
|
||||
<div id="proxyCheckProgress" class="mb-4 hidden">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span class="text-sm text-gray-600">检测中...</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div id="progressBar" class="bg-blue-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
<span id="progressText">准备开始检测...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 检测结果概览 -->
|
||||
<div id="proxyCheckSummary" class="mb-4 hidden">
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<div class="text-2xl font-bold text-green-600" id="availableCount">0</div>
|
||||
<div class="text-sm text-green-700">可用</div>
|
||||
</div>
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<div class="text-2xl font-bold text-red-600" id="unavailableCount">0</div>
|
||||
<div class="text-sm text-red-700">不可用</div>
|
||||
</div>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div class="text-2xl font-bold text-blue-600" id="totalCount">0</div>
|
||||
<div class="text-sm text-blue-700">总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 检测结果列表 -->
|
||||
<div id="proxyCheckResults" class="space-y-2" style="max-height: 400px; overflow-y: auto;">
|
||||
<!-- 检测结果将在这里动态添加 -->
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
id="retryFailedProxiesBtn"
|
||||
class="bg-orange-600 hover:bg-orange-700 text-white px-6 py-2 rounded-lg font-medium transition hidden"
|
||||
>
|
||||
重试失败的代理
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="closeProxyCheckBtn"
|
||||
class="bg-gray-500 bg-opacity-50 hover:bg-opacity-70 text-gray-200 px-6 py-2 rounded-lg font-medium transition"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Helper Modal -->
|
||||
<div id="modelHelperModal" class="modal">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user