feat: 实现代理健康检查功能

添加完整的代理检测系统,包括:

后端功能:
- 新增代理检测服务模块 (app/service/proxy/)
  * ProxyCheckService: 核心检测逻辑,支持HTTP/HTTPS/SOCKS5代理
  * 内置缓存机制,避免重复检测 (10秒缓存期)
  * 支持并发检测,可配置最大并发数
- 新增4个API端点 (app/router/config_routes.py):
  * POST /api/config/proxy/check - 单个代理检测
  * POST /api/config/proxy/check-all - 批量代理检测
  * GET /api/config/proxy/cache-stats - 缓存统计
  * POST /api/config/proxy/clear-cache - 清空缓存

前端功能:
- 代理列表中每项添加状态图标和检测按钮
- 支持单个代理实时检测
- 新增检测所有代理功能按钮
- 代理检测结果模态框,显示:
  * 检测进度条和状态
  * 可用/不可用代理统计
  * 详细的检测结果列表(响应时间、错误信息)
  * 重试失败代理功能

技术特性:
- 10秒超时机制,避免长时间等待
- 异步并发检测,提升批量检测效率
- 缓存机制减少重复请求
- 完整的错误处理和用户友好提示
- 响应式UI设计,支持实时状态更新
This commit is contained in:
icesixgod
2025-07-24 19:41:03 +08:00
parent ccd4722a77
commit 9f5892a987
5 changed files with 713 additions and 0 deletions
+311
View File
@@ -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 --