mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-12 02:19:59 +08:00
feat(keys): 按错误码分组展示校验失败的密钥
- **功能增强 (密钥状态页面):** - 在密钥校验结果弹窗中,将校验失败的密钥按照HTTP错误码进行分组展示。 - 每个错误码分组都是可折叠的,并显示该分组下的密钥数量。 - 为每个分组添加了独立的“复制”按钮,方便用户一键复制相同错误类型的所有密钥。 - 优化了错误详情的展示方式,使其在分组内切换,界面更整洁。 - 此项改动极大地改善了当大量密钥校验失败时的用户体验,能够更快速地定位和处理问题。 - **错误修复 (OpenAI服务):** - 修复了 `openai_chat_service.py` 中一个正则表达式的错误。该正则是用于从错误日志中提取HTTP状态码,错误的 `\\d+` 已被修正为 `\d+`,确保能正确捕获状态码。
This commit is contained in:
@@ -511,7 +511,7 @@ class OpenAIChatService:
|
||||
f"Streaming API call failed with error: {error_log_msg}. Attempt {retries} of {max_retries} with key {current_attempt_key}"
|
||||
)
|
||||
|
||||
match = re.search(r"status code (\\d+)", error_log_msg)
|
||||
match = re.search(r"status code (\d+)", error_log_msg)
|
||||
if match:
|
||||
status_code = int(match.group(1))
|
||||
else:
|
||||
|
||||
@@ -502,7 +502,7 @@ function showVerificationResultModal(data) {
|
||||
messageElement.appendChild(successDiv);
|
||||
}
|
||||
|
||||
// 失败列表
|
||||
// 失败列表 - 按错误码分组展示
|
||||
if (Object.keys(failedKeys).length > 0) {
|
||||
const failDiv = document.createElement("div");
|
||||
failDiv.className = "mb-1"; // 减少底部边距
|
||||
@@ -531,66 +531,143 @@ function showVerificationResultModal(data) {
|
||||
failHeader.appendChild(copyFailBtn);
|
||||
failDiv.appendChild(failHeader);
|
||||
|
||||
const failList = document.createElement("ul");
|
||||
failList.className =
|
||||
"text-sm text-gray-600 max-h-32 overflow-y-auto bg-red-50 p-2 rounded border border-red-200 space-y-1"; // 增加最大高度和间距
|
||||
// 按错误码分组失败的密钥
|
||||
const errorGroups = {};
|
||||
Object.entries(failedKeys).forEach(([key, error]) => {
|
||||
const li = document.createElement("li");
|
||||
// li.className = 'flex justify-between items-center'; // Restore original layout
|
||||
li.className = "flex flex-col items-start"; // Start with vertical layout
|
||||
// 提取错误码或使用完整错误信息作为分组键
|
||||
let errorCode = error;
|
||||
|
||||
// 尝试提取常见的错误码模式
|
||||
const errorCodePatterns = [
|
||||
/status code (\d+)/,
|
||||
];
|
||||
|
||||
for (const pattern of errorCodePatterns) {
|
||||
const match = error.match(pattern);
|
||||
if (match) {
|
||||
errorCode = match[1] || match[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配到特定模式,使用500
|
||||
if (errorCode === error) {
|
||||
errorCode = 500;
|
||||
}
|
||||
|
||||
if (!errorGroups[errorCode]) {
|
||||
errorGroups[errorCode] = [];
|
||||
}
|
||||
errorGroups[errorCode].push({ key, error });
|
||||
});
|
||||
|
||||
const keySpanContainer = document.createElement("div");
|
||||
keySpanContainer.className = "flex justify-between items-center w-full"; // Ensure key and button are on the same line initially
|
||||
// 创建分组展示容器
|
||||
const groupsContainer = document.createElement("div");
|
||||
groupsContainer.className = "space-y-3 max-h-64 overflow-y-auto bg-red-50 p-2 rounded border border-red-200";
|
||||
|
||||
const keySpan = document.createElement("span");
|
||||
keySpan.className = "font-mono";
|
||||
// Store full key in dataset, display masked
|
||||
keySpan.dataset.fullKey = key;
|
||||
keySpan.textContent =
|
||||
key.substring(0, 4) + "..." + key.substring(key.length - 4);
|
||||
// 按错误码分组展示
|
||||
Object.entries(errorGroups).forEach(([errorCode, keyErrorPairs]) => {
|
||||
const groupDiv = document.createElement("div");
|
||||
groupDiv.className = "border border-red-300 rounded-lg bg-white p-2";
|
||||
|
||||
const detailsButton = document.createElement("button");
|
||||
detailsButton.className =
|
||||
"ml-2 px-2 py-0.5 bg-red-200 hover:bg-red-300 text-red-700 text-xs rounded transition-colors";
|
||||
detailsButton.innerHTML = '<i class="fas fa-info-circle mr-1"></i>详情';
|
||||
detailsButton.dataset.error = error; // 将错误信息存储在 data 属性中
|
||||
detailsButton.onclick = (e) => {
|
||||
e.stopPropagation(); // Prevent modal close
|
||||
const button = e.currentTarget;
|
||||
const listItem = button.closest("li");
|
||||
const errorMsg = button.dataset.error;
|
||||
const errorDetailsId = `error-details-${key.replace(
|
||||
/[^a-zA-Z0-9]/g,
|
||||
""
|
||||
)}`; // Create unique ID
|
||||
let errorDiv = listItem.querySelector(`#${errorDetailsId}`);
|
||||
// 错误码标题
|
||||
const groupHeader = document.createElement("div");
|
||||
groupHeader.className = "flex justify-between items-center mb-2 cursor-pointer";
|
||||
groupHeader.innerHTML = `
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-chevron-down group-toggle-icon text-red-600 transition-transform duration-200"></i>
|
||||
<h5 class="font-semibold text-red-700 text-sm">错误码: ${errorCode}</h5>
|
||||
<span class="bg-red-100 text-red-600 px-2 py-0.5 rounded-full text-xs font-medium">${keyErrorPairs.length} 个密钥</span>
|
||||
</div>
|
||||
<button class="px-2 py-0.5 bg-red-200 hover:bg-red-300 text-red-700 text-xs rounded transition-colors group-copy-btn">
|
||||
<i class="fas fa-copy mr-1"></i>复制组内密钥
|
||||
</button>
|
||||
`;
|
||||
|
||||
if (errorDiv) {
|
||||
// Collapse: Remove error div and reset li layout
|
||||
errorDiv.remove();
|
||||
// listItem.className = 'flex justify-between items-center'; // Restore original layout
|
||||
listItem.className = "flex flex-col items-start"; // Keep vertical layout
|
||||
button.innerHTML = '<i class="fas fa-info-circle mr-1"></i>详情'; // Restore button text
|
||||
// 复制组内密钥功能
|
||||
const groupCopyBtn = groupHeader.querySelector('.group-copy-btn');
|
||||
groupCopyBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const groupKeys = keyErrorPairs.map(pair => pair.key);
|
||||
copyToClipboard(groupKeys.join("\n"))
|
||||
.then(() =>
|
||||
showNotification(
|
||||
`已复制 ${groupKeys.length} 个密钥 (错误码: ${errorCode})`,
|
||||
"success"
|
||||
)
|
||||
)
|
||||
.catch(() => showNotification("复制失败", "error"));
|
||||
};
|
||||
|
||||
// 密钥列表容器
|
||||
const keysList = document.createElement("div");
|
||||
keysList.className = "group-keys-list space-y-1";
|
||||
|
||||
keyErrorPairs.forEach(({ key, error }) => {
|
||||
const keyItem = document.createElement("div");
|
||||
keyItem.className = "flex flex-col items-start bg-gray-50 p-2 rounded border";
|
||||
|
||||
const keySpanContainer = document.createElement("div");
|
||||
keySpanContainer.className = "flex justify-between items-center w-full";
|
||||
|
||||
const keySpan = document.createElement("span");
|
||||
keySpan.className = "font-mono text-sm";
|
||||
keySpan.dataset.fullKey = key;
|
||||
keySpan.textContent = key.substring(0, 4) + "..." + key.substring(key.length - 4);
|
||||
|
||||
const detailsButton = document.createElement("button");
|
||||
detailsButton.className = "ml-2 px-2 py-0.5 bg-red-200 hover:bg-red-300 text-red-700 text-xs rounded transition-colors";
|
||||
detailsButton.innerHTML = '<i class="fas fa-info-circle mr-1"></i>详情';
|
||||
detailsButton.dataset.error = error;
|
||||
detailsButton.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const button = e.currentTarget;
|
||||
const keyItem = button.closest(".bg-gray-50");
|
||||
const errorMsg = button.dataset.error;
|
||||
const errorDetailsId = `error-details-${key.replace(/[^a-zA-Z0-9]/g, "")}`;
|
||||
let errorDiv = keyItem.querySelector(`#${errorDetailsId}`);
|
||||
|
||||
if (errorDiv) {
|
||||
errorDiv.remove();
|
||||
button.innerHTML = '<i class="fas fa-info-circle mr-1"></i>详情';
|
||||
} else {
|
||||
errorDiv = document.createElement("div");
|
||||
errorDiv.id = errorDetailsId;
|
||||
errorDiv.className = "w-full mt-2 text-xs text-red-600 bg-red-50 p-2 rounded border border-red-100 whitespace-pre-wrap break-words";
|
||||
errorDiv.textContent = errorMsg;
|
||||
keyItem.appendChild(errorDiv);
|
||||
button.innerHTML = '<i class="fas fa-chevron-up mr-1"></i>收起';
|
||||
}
|
||||
};
|
||||
|
||||
keySpanContainer.appendChild(keySpan);
|
||||
keySpanContainer.appendChild(detailsButton);
|
||||
keyItem.appendChild(keySpanContainer);
|
||||
keysList.appendChild(keyItem);
|
||||
});
|
||||
|
||||
// 分组折叠/展开功能
|
||||
groupHeader.onclick = (e) => {
|
||||
if (e.target.closest('.group-copy-btn')) return; // 避免复制按钮触发折叠
|
||||
|
||||
const toggleIcon = groupHeader.querySelector('.group-toggle-icon');
|
||||
const isCollapsed = keysList.style.display === 'none';
|
||||
|
||||
if (isCollapsed) {
|
||||
keysList.style.display = 'block';
|
||||
toggleIcon.style.transform = 'rotate(0deg)';
|
||||
} else {
|
||||
// Expand: Create and append error div, change li layout
|
||||
errorDiv = document.createElement("div");
|
||||
errorDiv.id = errorDetailsId;
|
||||
errorDiv.className =
|
||||
"w-full mt-1 pl-0 text-xs text-red-600 bg-red-50 p-1 rounded border border-red-100 whitespace-pre-wrap break-words"; // Adjusted padding
|
||||
errorDiv.textContent = errorMsg;
|
||||
listItem.appendChild(errorDiv);
|
||||
listItem.className = "flex flex-col items-start"; // Change layout to vertical
|
||||
button.innerHTML = '<i class="fas fa-chevron-up mr-1"></i>收起'; // Change button text
|
||||
// Move button to be alongside the keySpan for vertical layout (already done)
|
||||
keysList.style.display = 'none';
|
||||
toggleIcon.style.transform = 'rotate(-90deg)';
|
||||
}
|
||||
};
|
||||
|
||||
keySpanContainer.appendChild(keySpan); // Add keySpan to container
|
||||
keySpanContainer.appendChild(detailsButton); // Add button to container
|
||||
li.appendChild(keySpanContainer); // Add container to list item
|
||||
failList.appendChild(li);
|
||||
groupDiv.appendChild(groupHeader);
|
||||
groupDiv.appendChild(keysList);
|
||||
groupsContainer.appendChild(groupDiv);
|
||||
});
|
||||
failDiv.appendChild(failList);
|
||||
|
||||
failDiv.appendChild(groupsContainer);
|
||||
messageElement.appendChild(failDiv);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user