mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-06 16:16:37 +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.core.security import verify_auth_token
|
||||||
from app.log.logger import Logger, get_config_routes_logger
|
from app.log.logger import Logger, get_config_routes_logger
|
||||||
from app.service.config.config_service import ConfigService
|
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
|
from app.utils.helpers import redact_key_for_logging
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||||
@@ -132,3 +133,93 @@ async def get_ui_models(request: Request):
|
|||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"An unexpected error occurred while fetching UI models: {str(e)}",
|
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)}")
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Proxy service module
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .proxy_check_service import ProxyCheckService
|
||||||
|
|
||||||
|
__all__ = ["ProxyCheckService"]
|
||||||
@@ -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
|
||||||
@@ -185,12 +185,38 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
const cancelAddProxyBtn = document.getElementById("cancelAddProxyBtn");
|
const cancelAddProxyBtn = document.getElementById("cancelAddProxyBtn");
|
||||||
const confirmAddProxyBtn = document.getElementById("confirmAddProxyBtn");
|
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) {
|
if (addProxyBtn) {
|
||||||
addProxyBtn.addEventListener("click", () => {
|
addProxyBtn.addEventListener("click", () => {
|
||||||
openModal(proxyModal);
|
openModal(proxyModal);
|
||||||
if (proxyBulkInput) proxyBulkInput.value = "";
|
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)
|
if (closeProxyModalBtn)
|
||||||
closeProxyModalBtn.addEventListener("click", () => closeModal(proxyModal));
|
closeProxyModalBtn.addEventListener("click", () => closeModal(proxyModal));
|
||||||
if (cancelAddProxyBtn)
|
if (cancelAddProxyBtn)
|
||||||
@@ -1455,6 +1481,45 @@ function createRemoveButton() {
|
|||||||
return removeBtn;
|
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).
|
* Adds a new item to an array configuration section (e.g., API_KEYS, ALLOWED_TOKENS).
|
||||||
* This function is typically called by a "+" button.
|
* This function is typically called by a "+" button.
|
||||||
@@ -1486,6 +1551,7 @@ function addArrayItemWithValue(key, value) {
|
|||||||
const isThinkingModel = key === "THINKING_MODELS";
|
const isThinkingModel = key === "THINKING_MODELS";
|
||||||
const isAllowedToken = key === "ALLOWED_TOKENS";
|
const isAllowedToken = key === "ALLOWED_TOKENS";
|
||||||
const isVertexApiKey = key === "VERTEX_API_KEYS"; // 新增判断
|
const isVertexApiKey = key === "VERTEX_API_KEYS"; // 新增判断
|
||||||
|
const isProxy = key === "PROXIES"; // 新增代理判断
|
||||||
const isSensitive = key === "API_KEYS" || isAllowedToken || isVertexApiKey; // 更新敏感判断
|
const isSensitive = key === "API_KEYS" || isAllowedToken || isVertexApiKey; // 更新敏感判断
|
||||||
const modelId = isThinkingModel ? generateUUID() : null;
|
const modelId = isThinkingModel ? generateUUID() : null;
|
||||||
|
|
||||||
@@ -1513,6 +1579,13 @@ function addArrayItemWithValue(key, value) {
|
|||||||
if (isAllowedToken) {
|
if (isAllowedToken) {
|
||||||
const generateBtn = createGenerateTokenButton();
|
const generateBtn = createGenerateTokenButton();
|
||||||
inputWrapper.appendChild(generateBtn);
|
inputWrapper.appendChild(generateBtn);
|
||||||
|
} else if (isProxy) {
|
||||||
|
// 为代理添加状态显示和检测按钮
|
||||||
|
const proxyStatusIcon = createProxyStatusIcon();
|
||||||
|
inputWrapper.appendChild(proxyStatusIcon);
|
||||||
|
|
||||||
|
const proxyCheckBtn = createProxyCheckButton();
|
||||||
|
inputWrapper.appendChild(proxyCheckBtn);
|
||||||
} else {
|
} else {
|
||||||
// Ensure right-side rounding if no button is present
|
// Ensure right-side rounding if no button is present
|
||||||
input.classList.add("rounded-r-md");
|
input.classList.add("rounded-r-md");
|
||||||
@@ -2299,3 +2372,241 @@ function handleModelSelection(selectedModelId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- End Model Helper Functions --
|
// -- 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> 删除代理
|
<i class="fas fa-trash-alt"></i> 删除代理
|
||||||
</button>
|
</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
|
<button
|
||||||
type="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"
|
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>
|
||||||
</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 -->
|
<!-- Model Helper Modal -->
|
||||||
<div id="modelHelperModal" class="modal">
|
<div id="modelHelperModal" class="modal">
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user