diff --git a/app/service/chat/openai_chat_service.py b/app/service/chat/openai_chat_service.py index 2ea3d0a..b627fc0 100644 --- a/app/service/chat/openai_chat_service.py +++ b/app/service/chat/openai_chat_service.py @@ -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: diff --git a/app/static/js/keys_status.js b/app/static/js/keys_status.js index bb524ce..d5a54ae 100644 --- a/app/static/js/keys_status.js +++ b/app/static/js/keys_status.js @@ -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 = '详情'; - 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 = ` +
+ +
错误码: ${errorCode}
+ ${keyErrorPairs.length} 个密钥 +
+ + `; - 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 = '详情'; // 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 = '详情'; + 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 = '详情'; + } 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 = '收起'; + } + }; + + 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 = '收起'; // 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); }