Files

1 line
82 KiB
JSON

{"repo": "2404589803/llm-api-testsuite", "files": {"index.js": {"2f87bace9ca03abb43cbe25d8f9797bfb48a22b5": {"t": 1785746018.219666, "n": 11055, "h": "60a4bcf327c51769", "c": "const baseUrls = {\n \"Baichuan\": {\n \"base_url\": \"https://api.baichuan-ai.com/v1\",\n \"chinese_name\": \"百川\"\n },\n \"DeepSeek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"chinese_name\": \"深度求索\"\n },\n \"Moonshot\": {\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"chinese_name\": \"月之暗面\"\n },\n \"OpenAI\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"chinese_name\": \"OpenAI\"\n },\n \"Qwen\": {\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"chinese_name\": \"通义千问\"\n },\n \"StepFun\": {\n \"base_url\": \"https://api.stepfun.com/v1\",\n \"chinese_name\": \"阶跃星辰\"\n },\n \"Zhipu\": {\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4/\",\n \"chinese_name\": \"智谱\"\n }\n};\n\nlet baseUrl = '';\nlet apiKey = '';\nlet models = [];\nlet currentTestingModel = null;\nlet isExpanded = false;\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n const apiKeyInput = document.getElementById('apiKey');\n const apiKeyMask = document.getElementById('apiKeyMask');\n\n // 添加选项到下拉菜单\n for (const [key, value] of Object.entries(baseUrls)) {\n const option = document.createElement('option');\n option.value = key;\n option.textContent = value.chinese_name;\n baseUrlSelect.appendChild(option);\n }\n\n // 添加自定义选项\n const customOption = document.createElement('option');\n customOption.value = 'custom';\n customOption.textContent = '自定义';\n baseUrlSelect.appendChild(customOption);\n\n // 默认选择 OpenAI\n baseUrlSelect.value = 'OpenAI';\n\n // 处理下拉菜单变化\n baseUrlSelect.addEventListener('change', () => {\n if (baseUrlSelect.value === 'custom') {\n customBaseUrl.classList.remove('hidden');\n } else {\n customBaseUrl.classList.add('hidden');\n }\n });\n\n // 处理 API Key 输入\n apiKeyInput.addEventListener('input', () => {\n const maxVisibleChars = Math.floor(apiKeyInput.offsetWidth / 8) - 3; // 估算可见字符数\n const visibleStars = '*'.repeat(Math.min(apiKeyInput.value.length, maxVisibleChars));\n apiKeyMask.textContent = visibleStars + (apiKeyInput.value.length > maxVisibleChars ? '...' : '');\n });\n\n apiKeyInput.addEventListener('focus', () => {\n apiKeyMask.style.display = 'none';\n });\n\n apiKeyInput.addEventListener('blur', () => {\n if (apiKeyInput.value) {\n apiKeyMask.style.display = 'block';\n }\n });\n\n // 点击遮罩层时显示实际的 API Key 并允许编辑\n apiKeyMask.addEventListener('click', () => {\n apiKeyMask.style.display = 'none';\n apiKeyInput.focus();\n });\n\n document.getElementById('fetchModels').addEventListener('click', fetchModels);\n});\n\nasync function fetchModels() {\n const fetchButton = document.getElementById('fetchModels');\n const modelList = document.getElementById('modelList');\n \n fetchButton.disabled = true;\n fetchButton.innerHTML = '<svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 inline-block align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>获取中';\n\n // 清空之前的模型列表\n modelList.innerHTML = '';\n\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n \n if (baseUrlSelect.value === 'custom') {\n baseUrl = customBaseUrl.value.trim();\n } else {\n baseUrl = baseUrls[baseUrlSelect.value].base_url;\n }\n apiKey = document.getElementById('apiKey').value;\n\n if (!baseUrl || !apiKey) {\n displayError('请选择服务商(或输入自定义 Base URL)并输入 API Key');\n resetFetchButton(fetchButton);\n return;\n }\n\n try {\n const response = await fetch(`${baseUrl}/models`, {\n headers: { 'Authorization': `Bearer ${apiKey}` }\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n models = data.data;\n await Promise.all(models.map(model => pingModel(model.id)));\n sortAndDisplayModels();\n } catch (error) {\n displayError(`获取模型列表失败: ${error.message}`);\n } finally {\n resetFetchButton(fetchButton);\n }\n}\n\nfunction displayError(message) {\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = `\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\" role=\"alert\">\n <strong class=\"font-bold\">错误:</strong>\n <span class=\"block sm:inline\">${message}</span>\n </div>\n `;\n}\n\nfunction resetFetchButton(button) {\n button.disabled = false;\n button.textContent = '获取模型列表';\n}\n\n// 排序并显示模型列表\nfunction sortAndDisplayModels() {\n models.sort((a, b) => {\n if (a.id === currentTestingModel) return -1;\n if (b.id === currentTestingModel) return 1;\n if (a.pingSuccess && !b.pingSuccess) return -1;\n if (!a.pingSuccess && b.pingSuccess) return 1;\n return a.latency - b.latency;\n });\n\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = '';\n\n const modelGrid = document.createElement('div');\n modelGrid.className = 'grid grid-cols-1 gap-4 w-full';\n modelList.appendChild(modelGrid);\n\n models.forEach((model, index) => {\n const modelDiv = document.createElement('div');\n modelDiv.className = `flex flex-col p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-300 ${currentTestingModel && model.id !== currentTestingModel && !isExpanded ? 'hidden' : ''}`;\n modelDiv.innerHTML = `\n <div class=\"flex items-center space-x-3 mb-2\">\n <span class=\"w-3 h-3 rounded-full flex-shrink-0 ${model.pingSuccess ? 'bg-green-500' : 'bg-red-500'}\"></span>\n <span class=\"font-medium truncate flex-grow\" title=\"${model.id}\">${model.id}</span>\n </div>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm text-gray-500 flex-shrink-0\">${model.pingSuccess ? `${model.latency}ms` : 'N/A'}</span>\n <button onclick=\"startModelTest('${model.id}')\" class=\"test-button text-gray-500 hover:text-gray-700 text-sm transition duration-300 flex-shrink-0\">测试</button>\n </div>\n `;\n const hoverContent = model.pingSuccess \n ? `返回结果: ${model.response.substring(0, 20)}${model.response.length > 20 ? '...' : ''}`\n : `错误: ${model.error}`;\n modelDiv.setAttribute('title', hoverContent);\n modelGrid.appendChild(modelDiv);\n });\n\n if (currentTestingModel) {\n const toggleButton = document.createElement('button');\n toggleButton.id = 'toggleModels';\n toggleButton.className = 'mt-4 text-blue-500 hover:text-blue-700 transition duration-300';\n toggleButton.textContent = isExpanded ? '折叠模型列表' : '展开模型列表';\n toggleButton.onclick = toggleModelVisibility;\n modelList.appendChild(toggleButton);\n }\n}\n\n// Ping 模型\nasync function pingModel(modelId) {\n try {\n const startTime = Date.now();\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: modelId,\n messages: [{ role: \"user\", content: \"Hello\" }],\n max_tokens: 5\n })\n });\n\n if (response.ok) {\n const data = await response.json();\n const endTime = Date.now();\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = true;\n model.latency = endTime - startTime;\n model.response = data.choices[0].message.content;\n } else {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n } catch (error) {\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = false;\n model.latency = Infinity;\n model.error = error.message;\n }\n}\n\n// 更新切换按钮\nfunction updateToggleButton() {\n const modelCard = document.querySelector('#modelList > div');\n let toggleButton = modelCard.querySelector('.toggle-button');\n if (!toggleButton) {\n toggleButton = document.createElement('button');\n toggleButton.className = 'toggle-button col-span-1 sm:col-span-2 mt-4 py-2 text-gray-600 hover:text-gray-800 font-medium transition-colors duration-200';\n }\n\n if (models.length > 6) {\n toggleButton.textContent = '显示更多';\n toggleButton.onclick = () => {\n const hiddenItems = modelCard.querySelectorAll('.hidden');\n if (hiddenItems.length > 0) {\n hiddenItems.forEach(item => item.classList.remove('hidden'));\n toggleButton.textContent = '折叠';\n } else {\n Array.from(modelCard.children).forEach((item, index) => {\n if (index >= 6 && !item.classList.contains('toggle-button')) {\n item.classList.add('hidden');\n }\n });\n toggleButton.textContent = '显示更多';\n }\n };\n modelCard.appendChild(toggleButton);\n } else if (toggleButton.parentNode) {\n toggleButton.parentNode.removeChild(toggleButton);\n }\n}\n\n// 测试模型\nfunction startModelTest(modelId) {\n currentTestingModel = modelId;\n isExpanded = false;\n sortAndDisplayModels();\n const testButton = document.querySelector(`#modelList button[onclick=\"startModelTest('${modelId}')\"]`);\n testButton.innerHTML = '<svg class=\"animate-spin h-5 w-5 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>';\n testButton.disabled = true;\n \n // 清除之前的测试结果\n const existingResults = document.getElementById(`test-results-${modelId}`);\n if (existingResults) {\n existingResults.remove();\n }\n \n // 调用实际的测试函数\n testModel(modelId).then(() => {\n testButton.innerHTML = '测试';\n testButton.disabled = false;\n });\n}\n\nfunction toggleModelVisibility() {\n isExpanded = !isExpanded;\n sortAndDisplayModels();\n}\n\n// 在 unit_test.js 中修改 testModel 函数\nasync function testModel(modelId) {\n // ... 原有的测试逻辑 ...\n\n // 测试完成后,保持当前视图\n sortAndDisplayModels();\n}"}, "59b902d1738a437ec1458c86bdf63ff6ac8d6a24": {"t": 1785758837.4511402, "n": 11055, "h": "60a4bcf327c51769", "c": "const baseUrls = {\n \"Baichuan\": {\n \"base_url\": \"https://api.baichuan-ai.com/v1\",\n \"chinese_name\": \"百川\"\n },\n \"DeepSeek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"chinese_name\": \"深度求索\"\n },\n \"Moonshot\": {\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"chinese_name\": \"月之暗面\"\n },\n \"OpenAI\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"chinese_name\": \"OpenAI\"\n },\n \"Qwen\": {\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"chinese_name\": \"通义千问\"\n },\n \"StepFun\": {\n \"base_url\": \"https://api.stepfun.com/v1\",\n \"chinese_name\": \"阶跃星辰\"\n },\n \"Zhipu\": {\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4/\",\n \"chinese_name\": \"智谱\"\n }\n};\n\nlet baseUrl = '';\nlet apiKey = '';\nlet models = [];\nlet currentTestingModel = null;\nlet isExpanded = false;\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n const apiKeyInput = document.getElementById('apiKey');\n const apiKeyMask = document.getElementById('apiKeyMask');\n\n // 添加选项到下拉菜单\n for (const [key, value] of Object.entries(baseUrls)) {\n const option = document.createElement('option');\n option.value = key;\n option.textContent = value.chinese_name;\n baseUrlSelect.appendChild(option);\n }\n\n // 添加自定义选项\n const customOption = document.createElement('option');\n customOption.value = 'custom';\n customOption.textContent = '自定义';\n baseUrlSelect.appendChild(customOption);\n\n // 默认选择 OpenAI\n baseUrlSelect.value = 'OpenAI';\n\n // 处理下拉菜单变化\n baseUrlSelect.addEventListener('change', () => {\n if (baseUrlSelect.value === 'custom') {\n customBaseUrl.classList.remove('hidden');\n } else {\n customBaseUrl.classList.add('hidden');\n }\n });\n\n // 处理 API Key 输入\n apiKeyInput.addEventListener('input', () => {\n const maxVisibleChars = Math.floor(apiKeyInput.offsetWidth / 8) - 3; // 估算可见字符数\n const visibleStars = '*'.repeat(Math.min(apiKeyInput.value.length, maxVisibleChars));\n apiKeyMask.textContent = visibleStars + (apiKeyInput.value.length > maxVisibleChars ? '...' : '');\n });\n\n apiKeyInput.addEventListener('focus', () => {\n apiKeyMask.style.display = 'none';\n });\n\n apiKeyInput.addEventListener('blur', () => {\n if (apiKeyInput.value) {\n apiKeyMask.style.display = 'block';\n }\n });\n\n // 点击遮罩层时显示实际的 API Key 并允许编辑\n apiKeyMask.addEventListener('click', () => {\n apiKeyMask.style.display = 'none';\n apiKeyInput.focus();\n });\n\n document.getElementById('fetchModels').addEventListener('click', fetchModels);\n});\n\nasync function fetchModels() {\n const fetchButton = document.getElementById('fetchModels');\n const modelList = document.getElementById('modelList');\n \n fetchButton.disabled = true;\n fetchButton.innerHTML = '<svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 inline-block align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>获取中';\n\n // 清空之前的模型列表\n modelList.innerHTML = '';\n\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n \n if (baseUrlSelect.value === 'custom') {\n baseUrl = customBaseUrl.value.trim();\n } else {\n baseUrl = baseUrls[baseUrlSelect.value].base_url;\n }\n apiKey = document.getElementById('apiKey').value;\n\n if (!baseUrl || !apiKey) {\n displayError('请选择服务商(或输入自定义 Base URL)并输入 API Key');\n resetFetchButton(fetchButton);\n return;\n }\n\n try {\n const response = await fetch(`${baseUrl}/models`, {\n headers: { 'Authorization': `Bearer ${apiKey}` }\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n models = data.data;\n await Promise.all(models.map(model => pingModel(model.id)));\n sortAndDisplayModels();\n } catch (error) {\n displayError(`获取模型列表失败: ${error.message}`);\n } finally {\n resetFetchButton(fetchButton);\n }\n}\n\nfunction displayError(message) {\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = `\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\" role=\"alert\">\n <strong class=\"font-bold\">错误:</strong>\n <span class=\"block sm:inline\">${message}</span>\n </div>\n `;\n}\n\nfunction resetFetchButton(button) {\n button.disabled = false;\n button.textContent = '获取模型列表';\n}\n\n// 排序并显示模型列表\nfunction sortAndDisplayModels() {\n models.sort((a, b) => {\n if (a.id === currentTestingModel) return -1;\n if (b.id === currentTestingModel) return 1;\n if (a.pingSuccess && !b.pingSuccess) return -1;\n if (!a.pingSuccess && b.pingSuccess) return 1;\n return a.latency - b.latency;\n });\n\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = '';\n\n const modelGrid = document.createElement('div');\n modelGrid.className = 'grid grid-cols-1 gap-4 w-full';\n modelList.appendChild(modelGrid);\n\n models.forEach((model, index) => {\n const modelDiv = document.createElement('div');\n modelDiv.className = `flex flex-col p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-300 ${currentTestingModel && model.id !== currentTestingModel && !isExpanded ? 'hidden' : ''}`;\n modelDiv.innerHTML = `\n <div class=\"flex items-center space-x-3 mb-2\">\n <span class=\"w-3 h-3 rounded-full flex-shrink-0 ${model.pingSuccess ? 'bg-green-500' : 'bg-red-500'}\"></span>\n <span class=\"font-medium truncate flex-grow\" title=\"${model.id}\">${model.id}</span>\n </div>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm text-gray-500 flex-shrink-0\">${model.pingSuccess ? `${model.latency}ms` : 'N/A'}</span>\n <button onclick=\"startModelTest('${model.id}')\" class=\"test-button text-gray-500 hover:text-gray-700 text-sm transition duration-300 flex-shrink-0\">测试</button>\n </div>\n `;\n const hoverContent = model.pingSuccess \n ? `返回结果: ${model.response.substring(0, 20)}${model.response.length > 20 ? '...' : ''}`\n : `错误: ${model.error}`;\n modelDiv.setAttribute('title', hoverContent);\n modelGrid.appendChild(modelDiv);\n });\n\n if (currentTestingModel) {\n const toggleButton = document.createElement('button');\n toggleButton.id = 'toggleModels';\n toggleButton.className = 'mt-4 text-blue-500 hover:text-blue-700 transition duration-300';\n toggleButton.textContent = isExpanded ? '折叠模型列表' : '展开模型列表';\n toggleButton.onclick = toggleModelVisibility;\n modelList.appendChild(toggleButton);\n }\n}\n\n// Ping 模型\nasync function pingModel(modelId) {\n try {\n const startTime = Date.now();\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: modelId,\n messages: [{ role: \"user\", content: \"Hello\" }],\n max_tokens: 5\n })\n });\n\n if (response.ok) {\n const data = await response.json();\n const endTime = Date.now();\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = true;\n model.latency = endTime - startTime;\n model.response = data.choices[0].message.content;\n } else {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n } catch (error) {\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = false;\n model.latency = Infinity;\n model.error = error.message;\n }\n}\n\n// 更新切换按钮\nfunction updateToggleButton() {\n const modelCard = document.querySelector('#modelList > div');\n let toggleButton = modelCard.querySelector('.toggle-button');\n if (!toggleButton) {\n toggleButton = document.createElement('button');\n toggleButton.className = 'toggle-button col-span-1 sm:col-span-2 mt-4 py-2 text-gray-600 hover:text-gray-800 font-medium transition-colors duration-200';\n }\n\n if (models.length > 6) {\n toggleButton.textContent = '显示更多';\n toggleButton.onclick = () => {\n const hiddenItems = modelCard.querySelectorAll('.hidden');\n if (hiddenItems.length > 0) {\n hiddenItems.forEach(item => item.classList.remove('hidden'));\n toggleButton.textContent = '折叠';\n } else {\n Array.from(modelCard.children).forEach((item, index) => {\n if (index >= 6 && !item.classList.contains('toggle-button')) {\n item.classList.add('hidden');\n }\n });\n toggleButton.textContent = '显示更多';\n }\n };\n modelCard.appendChild(toggleButton);\n } else if (toggleButton.parentNode) {\n toggleButton.parentNode.removeChild(toggleButton);\n }\n}\n\n// 测试模型\nfunction startModelTest(modelId) {\n currentTestingModel = modelId;\n isExpanded = false;\n sortAndDisplayModels();\n const testButton = document.querySelector(`#modelList button[onclick=\"startModelTest('${modelId}')\"]`);\n testButton.innerHTML = '<svg class=\"animate-spin h-5 w-5 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>';\n testButton.disabled = true;\n \n // 清除之前的测试结果\n const existingResults = document.getElementById(`test-results-${modelId}`);\n if (existingResults) {\n existingResults.remove();\n }\n \n // 调用实际的测试函数\n testModel(modelId).then(() => {\n testButton.innerHTML = '测试';\n testButton.disabled = false;\n });\n}\n\nfunction toggleModelVisibility() {\n isExpanded = !isExpanded;\n sortAndDisplayModels();\n}\n\n// 在 unit_test.js 中修改 testModel 函数\nasync function testModel(modelId) {\n // ... 原有的测试逻辑 ...\n\n // 测试完成后,保持当前视图\n sortAndDisplayModels();\n}"}, "c5357f1f0b188e41230b708a4e9ea48b3a2a148e": {"t": 1785758838.6996493, "n": 11054, "h": "65f2900256c2f5d7", "c": "const baseUrls = {\n \"Baichuan\": {\n \"base_url\": \"https://api.baichuan-ai.com/v1\",\n \"chinese_name\": \"百川\"\n },\n \"DeepSeek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"chinese_name\": \"深度求索\"\n },\n \"Moonshot\": {\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"chinese_name\": \"月之暗面\"\n },\n \"OpenAI\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"chinese_name\": \"OpenAI\"\n },\n \"Qwen\": {\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"chinese_name\": \"通义千问\"\n },\n \"StepFun\": {\n \"base_url\": \"https://api.stepfun.com/v1\",\n \"chinese_name\": \"阶跃星辰\"\n },\n \"Zhipu\": {\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"chinese_name\": \"智谱\"\n }\n};\n\nlet baseUrl = '';\nlet apiKey = '';\nlet models = [];\nlet currentTestingModel = null;\nlet isExpanded = false;\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n const apiKeyInput = document.getElementById('apiKey');\n const apiKeyMask = document.getElementById('apiKeyMask');\n\n // 添加选项到下拉菜单\n for (const [key, value] of Object.entries(baseUrls)) {\n const option = document.createElement('option');\n option.value = key;\n option.textContent = value.chinese_name;\n baseUrlSelect.appendChild(option);\n }\n\n // 添加自定义选项\n const customOption = document.createElement('option');\n customOption.value = 'custom';\n customOption.textContent = '自定义';\n baseUrlSelect.appendChild(customOption);\n\n // 默认选择 OpenAI\n baseUrlSelect.value = 'OpenAI';\n\n // 处理下拉菜单变化\n baseUrlSelect.addEventListener('change', () => {\n if (baseUrlSelect.value === 'custom') {\n customBaseUrl.classList.remove('hidden');\n } else {\n customBaseUrl.classList.add('hidden');\n }\n });\n\n // 处理 API Key 输入\n apiKeyInput.addEventListener('input', () => {\n const maxVisibleChars = Math.floor(apiKeyInput.offsetWidth / 8) - 3; // 估算可见字符数\n const visibleStars = '*'.repeat(Math.min(apiKeyInput.value.length, maxVisibleChars));\n apiKeyMask.textContent = visibleStars + (apiKeyInput.value.length > maxVisibleChars ? '...' : '');\n });\n\n apiKeyInput.addEventListener('focus', () => {\n apiKeyMask.style.display = 'none';\n });\n\n apiKeyInput.addEventListener('blur', () => {\n if (apiKeyInput.value) {\n apiKeyMask.style.display = 'block';\n }\n });\n\n // 点击遮罩层时显示实际的 API Key 并允许编辑\n apiKeyMask.addEventListener('click', () => {\n apiKeyMask.style.display = 'none';\n apiKeyInput.focus();\n });\n\n document.getElementById('fetchModels').addEventListener('click', fetchModels);\n});\n\nasync function fetchModels() {\n const fetchButton = document.getElementById('fetchModels');\n const modelList = document.getElementById('modelList');\n \n fetchButton.disabled = true;\n fetchButton.innerHTML = '<svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 inline-block align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>获取中';\n\n // 清空之前的模型列表\n modelList.innerHTML = '';\n\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n \n if (baseUrlSelect.value === 'custom') {\n baseUrl = customBaseUrl.value.trim();\n } else {\n baseUrl = baseUrls[baseUrlSelect.value].base_url;\n }\n apiKey = document.getElementById('apiKey').value;\n\n if (!baseUrl || !apiKey) {\n displayError('请选择服务商(或输入自定义 Base URL)并输入 API Key');\n resetFetchButton(fetchButton);\n return;\n }\n\n try {\n const response = await fetch(`${baseUrl}/models`, {\n headers: { 'Authorization': `Bearer ${apiKey}` }\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n models = data.data;\n await Promise.all(models.map(model => pingModel(model.id)));\n sortAndDisplayModels();\n } catch (error) {\n displayError(`获取模型列表失败: ${error.message}`);\n } finally {\n resetFetchButton(fetchButton);\n }\n}\n\nfunction displayError(message) {\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = `\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\" role=\"alert\">\n <strong class=\"font-bold\">错误:</strong>\n <span class=\"block sm:inline\">${message}</span>\n </div>\n `;\n}\n\nfunction resetFetchButton(button) {\n button.disabled = false;\n button.textContent = '获取模型列表';\n}\n\n// 排序并显示模型列表\nfunction sortAndDisplayModels() {\n models.sort((a, b) => {\n if (a.id === currentTestingModel) return -1;\n if (b.id === currentTestingModel) return 1;\n if (a.pingSuccess && !b.pingSuccess) return -1;\n if (!a.pingSuccess && b.pingSuccess) return 1;\n return a.latency - b.latency;\n });\n\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = '';\n\n const modelGrid = document.createElement('div');\n modelGrid.className = 'grid grid-cols-1 gap-4 w-full';\n modelList.appendChild(modelGrid);\n\n models.forEach((model, index) => {\n const modelDiv = document.createElement('div');\n modelDiv.className = `flex flex-col p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-300 ${currentTestingModel && model.id !== currentTestingModel && !isExpanded ? 'hidden' : ''}`;\n modelDiv.innerHTML = `\n <div class=\"flex items-center space-x-3 mb-2\">\n <span class=\"w-3 h-3 rounded-full flex-shrink-0 ${model.pingSuccess ? 'bg-green-500' : 'bg-red-500'}\"></span>\n <span class=\"font-medium truncate flex-grow\" title=\"${model.id}\">${model.id}</span>\n </div>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm text-gray-500 flex-shrink-0\">${model.pingSuccess ? `${model.latency}ms` : 'N/A'}</span>\n <button onclick=\"startModelTest('${model.id}')\" class=\"test-button text-gray-500 hover:text-gray-700 text-sm transition duration-300 flex-shrink-0\">测试</button>\n </div>\n `;\n const hoverContent = model.pingSuccess \n ? `返回结果: ${model.response.substring(0, 20)}${model.response.length > 20 ? '...' : ''}`\n : `错误: ${model.error}`;\n modelDiv.setAttribute('title', hoverContent);\n modelGrid.appendChild(modelDiv);\n });\n\n if (currentTestingModel) {\n const toggleButton = document.createElement('button');\n toggleButton.id = 'toggleModels';\n toggleButton.className = 'mt-4 text-blue-500 hover:text-blue-700 transition duration-300';\n toggleButton.textContent = isExpanded ? '折叠模型列表' : '展开模型列表';\n toggleButton.onclick = toggleModelVisibility;\n modelList.appendChild(toggleButton);\n }\n}\n\n// Ping 模型\nasync function pingModel(modelId) {\n try {\n const startTime = Date.now();\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: modelId,\n messages: [{ role: \"user\", content: \"Hello\" }],\n max_tokens: 5\n })\n });\n\n if (response.ok) {\n const data = await response.json();\n const endTime = Date.now();\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = true;\n model.latency = endTime - startTime;\n model.response = data.choices[0].message.content;\n } else {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n } catch (error) {\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = false;\n model.latency = Infinity;\n model.error = error.message;\n }\n}\n\n// 更新切换按钮\nfunction updateToggleButton() {\n const modelCard = document.querySelector('#modelList > div');\n let toggleButton = modelCard.querySelector('.toggle-button');\n if (!toggleButton) {\n toggleButton = document.createElement('button');\n toggleButton.className = 'toggle-button col-span-1 sm:col-span-2 mt-4 py-2 text-gray-600 hover:text-gray-800 font-medium transition-colors duration-200';\n }\n\n if (models.length > 6) {\n toggleButton.textContent = '显示更多';\n toggleButton.onclick = () => {\n const hiddenItems = modelCard.querySelectorAll('.hidden');\n if (hiddenItems.length > 0) {\n hiddenItems.forEach(item => item.classList.remove('hidden'));\n toggleButton.textContent = '折叠';\n } else {\n Array.from(modelCard.children).forEach((item, index) => {\n if (index >= 6 && !item.classList.contains('toggle-button')) {\n item.classList.add('hidden');\n }\n });\n toggleButton.textContent = '显示更多';\n }\n };\n modelCard.appendChild(toggleButton);\n } else if (toggleButton.parentNode) {\n toggleButton.parentNode.removeChild(toggleButton);\n }\n}\n\n// 测试模型\nfunction startModelTest(modelId) {\n currentTestingModel = modelId;\n isExpanded = false;\n sortAndDisplayModels();\n const testButton = document.querySelector(`#modelList button[onclick=\"startModelTest('${modelId}')\"]`);\n testButton.innerHTML = '<svg class=\"animate-spin h-5 w-5 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>';\n testButton.disabled = true;\n \n // 清除之前的测试结果\n const existingResults = document.getElementById(`test-results-${modelId}`);\n if (existingResults) {\n existingResults.remove();\n }\n \n // 调用实际的测试函数\n testModel(modelId).then(() => {\n testButton.innerHTML = '测试';\n testButton.disabled = false;\n });\n}\n\nfunction toggleModelVisibility() {\n isExpanded = !isExpanded;\n sortAndDisplayModels();\n}\n\n// 在 unit_test.js 中修改 testModel 函数\nasync function testModel(modelId) {\n // ... 原有的测试逻辑 ...\n\n // 测试完成后,保持当前视图\n sortAndDisplayModels();\n}"}, "d97296b0cc547ed3041857caba2442a81cf06a64": {"t": 1785758840.0218751, "n": 10045, "h": "52a648b0d48bda34", "c": "const baseUrls = {\n \"Baichuan\": {\n \"base_url\": \"https://api.baichuan-ai.com/v1\",\n \"chinese_name\": \"百川\"\n },\n \"DeepSeek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"chinese_name\": \"深度求索\"\n },\n \"Moonshot\": {\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"chinese_name\": \"月之暗面\"\n },\n \"OpenAI\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"chinese_name\": \"OpenAI\"\n },\n \"Qwen\": {\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"chinese_name\": \"通义千问\"\n },\n \"StepFun\": {\n \"base_url\": \"https://api.stepfun.com/v1\",\n \"chinese_name\": \"阶跃星辰\"\n },\n \"Zhipu\": {\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"chinese_name\": \"智谱\"\n }\n};\n\nlet baseUrl = '';\nlet apiKey = '';\nlet models = [];\nlet currentTestingModel = null;\nlet isExpanded = false;\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n const apiKeyInput = document.getElementById('apiKey');\n const apiKeyMask = document.getElementById('apiKeyMask');\n\n // 添加选项到下拉菜单\n for (const [key, value] of Object.entries(baseUrls)) {\n const option = document.createElement('option');\n option.value = key;\n option.textContent = value.chinese_name;\n baseUrlSelect.appendChild(option);\n }\n\n // 添加自定义选项\n const customOption = document.createElement('option');\n customOption.value = 'custom';\n customOption.textContent = '自定义';\n baseUrlSelect.appendChild(customOption);\n\n // 默认选择 OpenAI\n baseUrlSelect.value = 'OpenAI';\n\n // 处理下拉菜单变化\n baseUrlSelect.addEventListener('change', () => {\n if (baseUrlSelect.value === 'custom') {\n customBaseUrl.classList.remove('hidden');\n } else {\n customBaseUrl.classList.add('hidden');\n }\n });\n\n // 处理 API Key 输入\n apiKeyInput.addEventListener('input', () => {\n const maxVisibleChars = Math.floor(apiKeyInput.offsetWidth / 8) - 3; // 估算可见字符数\n const visibleStars = '*'.repeat(Math.min(apiKeyInput.value.length, maxVisibleChars));\n apiKeyMask.textContent = visibleStars + (apiKeyInput.value.length > maxVisibleChars ? '...' : '');\n });\n\n apiKeyInput.addEventListener('focus', () => {\n apiKeyMask.style.display = 'none';\n });\n\n apiKeyInput.addEventListener('blur', () => {\n if (apiKeyInput.value) {\n apiKeyMask.style.display = 'block';\n }\n });\n\n // 点击遮罩层时显示实际的 API Key 并允许编辑\n apiKeyMask.addEventListener('click', () => {\n apiKeyMask.style.display = 'none';\n apiKeyInput.focus();\n });\n\n document.getElementById('fetchModels').addEventListener('click', fetchModels);\n});\n\n// 通用的 API 请求函数\nasync function makeApiRequest(url, key, modelId, messages) {\n const startTime = Date.now();\n try {\n const response = await fetch(`${url}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${key}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: modelId,\n messages: messages,\n max_tokens: 5\n })\n });\n\n const endTime = Date.now();\n const latency = endTime - startTime;\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n return { data, latency };\n } catch (error) {\n throw error;\n }\n}\n\nasync function fetchModels() {\n const fetchButton = document.getElementById('fetchModels');\n const modelList = document.getElementById('modelList');\n \n fetchButton.disabled = true;\n fetchButton.innerHTML = '<svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 inline-block align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>获取中';\n\n // 清空之前的模型列表\n modelList.innerHTML = '';\n\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n \n if (baseUrlSelect.value === 'custom') {\n baseUrl = customBaseUrl.value.trim();\n } else {\n baseUrl = baseUrls[baseUrlSelect.value].base_url;\n }\n apiKey = document.getElementById('apiKey').value;\n\n if (!baseUrl || !apiKey) {\n displayError('请选择服务商(或输入自定义 Base URL)并输入 API Key');\n resetFetchButton(fetchButton);\n return;\n }\n\n try {\n const response = await fetch(`${baseUrl}/models`, {\n headers: { 'Authorization': `Bearer ${apiKey}` }\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n models = data.data;\n await Promise.all(models.map(model => pingModel(model.id)));\n sortAndDisplayModels();\n } catch (error) {\n displayError(`获取模型列表失败: ${error.message}`);\n } finally {\n resetFetchButton(fetchButton);\n }\n}\n\nasync function pingModel(modelId) {\n try {\n const messages = [{ role: \"user\", content: \"Hello\" }];\n const { data, latency } = await makeApiRequest(baseUrl, apiKey, modelId, messages);\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = true;\n model.latency = latency;\n model.response = data.choices[0].message.content;\n } catch (error) {\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = false;\n model.latency = Infinity;\n model.error = error.message;\n }\n}\n\n// 排序并显示模型列表\nfunction sortAndDisplayModels() {\n models.sort((a, b) => {\n if (a.id === currentTestingModel) return -1;\n if (b.id === currentTestingModel) return 1;\n if (a.pingSuccess && !b.pingSuccess) return -1;\n if (!a.pingSuccess && b.pingSuccess) return 1;\n return a.latency - b.latency;\n });\n\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = '';\n\n const modelGrid = document.createElement('div');\n modelGrid.className = 'grid grid-cols-1 gap-4 w-full';\n modelList.appendChild(modelGrid);\n\n models.forEach((model, index) => {\n const modelDiv = document.createElement('div');\n modelDiv.className = `flex flex-col p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-300 ${currentTestingModel && model.id !== currentTestingModel && !isExpanded ? 'hidden' : ''}`;\n modelDiv.innerHTML = `\n <div class=\"flex items-center space-x-3 mb-2\">\n <span class=\"w-3 h-3 rounded-full flex-shrink-0 ${model.pingSuccess ? 'bg-green-500' : 'bg-red-500'}\"></span>\n <span class=\"font-medium truncate flex-grow\" title=\"${model.id}\">${model.id}</span>\n </div>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm text-gray-500 flex-shrink-0\">${model.pingSuccess ? `${model.latency}ms` : 'N/A'}</span>\n <button onclick=\"startModelTest('${model.id}')\" class=\"test-button text-gray-500 hover:text-gray-700 text-sm transition duration-300 flex-shrink-0\">测试</button>\n </div>\n `;\n const hoverContent = model.pingSuccess \n ? `返回结果: ${model.response.substring(0, 20)}${model.response.length > 20 ? '...' : ''}`\n : `错误: ${model.error}`;\n modelDiv.setAttribute('title', hoverContent);\n modelGrid.appendChild(modelDiv);\n });\n\n if (currentTestingModel) {\n const toggleButton = document.createElement('button');\n toggleButton.id = 'toggleModels';\n toggleButton.className = 'mt-4 text-blue-500 hover:text-blue-700 transition duration-300';\n toggleButton.textContent = isExpanded ? '折叠模型列表' : '展开模型列表';\n toggleButton.onclick = toggleModelVisibility;\n modelList.appendChild(toggleButton);\n }\n}\n\nfunction displayError(message) {\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = `\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\" role=\"alert\">\n <strong class=\"font-bold\">错误:</strong>\n <span class=\"block sm:inline\">${message}</span>\n </div>\n `;\n}\n\nfunction resetFetchButton(button) {\n button.disabled = false;\n button.textContent = '获取模型列表';\n}\n\n// 测试模型\nfunction startModelTest(modelId) {\n currentTestingModel = modelId;\n isExpanded = false;\n sortAndDisplayModels();\n const testButton = document.querySelector(`#modelList button[onclick=\"startModelTest('${modelId}')\"]`);\n testButton.innerHTML = '<svg class=\"animate-spin h-5 w-5 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>';\n testButton.disabled = true;\n \n // 清除之前的测试结果\n const existingResults = document.getElementById(`test-results-${modelId}`);\n if (existingResults) {\n existingResults.remove();\n }\n \n // 调用实际的测试函数\n testModel(modelId).then(() => {\n testButton.innerHTML = '测试';\n testButton.disabled = false;\n });\n}\n\nfunction toggleModelVisibility() {\n isExpanded = !isExpanded;\n sortAndDisplayModels();\n}\n\n// 在 unit_test.js 中修改 testModel 函数\nasync function testModel(modelId) {\n // ... 原有的测试逻辑 ...\n // 测试完成后,保持当前视图\n sortAndDisplayModels();\n}"}, "73ff38f83dfca6dbdd1f38f13acbbdca2733c7e5": {"t": 1785758841.3117335, "n": 11054, "h": "65f2900256c2f5d7", "c": "const baseUrls = {\n \"Baichuan\": {\n \"base_url\": \"https://api.baichuan-ai.com/v1\",\n \"chinese_name\": \"百川\"\n },\n \"DeepSeek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"chinese_name\": \"深度求索\"\n },\n \"Moonshot\": {\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"chinese_name\": \"月之暗面\"\n },\n \"OpenAI\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"chinese_name\": \"OpenAI\"\n },\n \"Qwen\": {\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"chinese_name\": \"通义千问\"\n },\n \"StepFun\": {\n \"base_url\": \"https://api.stepfun.com/v1\",\n \"chinese_name\": \"阶跃星辰\"\n },\n \"Zhipu\": {\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"chinese_name\": \"智谱\"\n }\n};\n\nlet baseUrl = '';\nlet apiKey = '';\nlet models = [];\nlet currentTestingModel = null;\nlet isExpanded = false;\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n const apiKeyInput = document.getElementById('apiKey');\n const apiKeyMask = document.getElementById('apiKeyMask');\n\n // 添加选项到下拉菜单\n for (const [key, value] of Object.entries(baseUrls)) {\n const option = document.createElement('option');\n option.value = key;\n option.textContent = value.chinese_name;\n baseUrlSelect.appendChild(option);\n }\n\n // 添加自定义选项\n const customOption = document.createElement('option');\n customOption.value = 'custom';\n customOption.textContent = '自定义';\n baseUrlSelect.appendChild(customOption);\n\n // 默认选择 OpenAI\n baseUrlSelect.value = 'OpenAI';\n\n // 处理下拉菜单变化\n baseUrlSelect.addEventListener('change', () => {\n if (baseUrlSelect.value === 'custom') {\n customBaseUrl.classList.remove('hidden');\n } else {\n customBaseUrl.classList.add('hidden');\n }\n });\n\n // 处理 API Key 输入\n apiKeyInput.addEventListener('input', () => {\n const maxVisibleChars = Math.floor(apiKeyInput.offsetWidth / 8) - 3; // 估算可见字符数\n const visibleStars = '*'.repeat(Math.min(apiKeyInput.value.length, maxVisibleChars));\n apiKeyMask.textContent = visibleStars + (apiKeyInput.value.length > maxVisibleChars ? '...' : '');\n });\n\n apiKeyInput.addEventListener('focus', () => {\n apiKeyMask.style.display = 'none';\n });\n\n apiKeyInput.addEventListener('blur', () => {\n if (apiKeyInput.value) {\n apiKeyMask.style.display = 'block';\n }\n });\n\n // 点击遮罩层时显示实际的 API Key 并允许编辑\n apiKeyMask.addEventListener('click', () => {\n apiKeyMask.style.display = 'none';\n apiKeyInput.focus();\n });\n\n document.getElementById('fetchModels').addEventListener('click', fetchModels);\n});\n\nasync function fetchModels() {\n const fetchButton = document.getElementById('fetchModels');\n const modelList = document.getElementById('modelList');\n \n fetchButton.disabled = true;\n fetchButton.innerHTML = '<svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 inline-block align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>获取中';\n\n // 清空之前的模型列表\n modelList.innerHTML = '';\n\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n \n if (baseUrlSelect.value === 'custom') {\n baseUrl = customBaseUrl.value.trim();\n } else {\n baseUrl = baseUrls[baseUrlSelect.value].base_url;\n }\n apiKey = document.getElementById('apiKey').value;\n\n if (!baseUrl || !apiKey) {\n displayError('请选择服务商(或输入自定义 Base URL)并输入 API Key');\n resetFetchButton(fetchButton);\n return;\n }\n\n try {\n const response = await fetch(`${baseUrl}/models`, {\n headers: { 'Authorization': `Bearer ${apiKey}` }\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n models = data.data;\n await Promise.all(models.map(model => pingModel(model.id)));\n sortAndDisplayModels();\n } catch (error) {\n displayError(`获取模型列表失败: ${error.message}`);\n } finally {\n resetFetchButton(fetchButton);\n }\n}\n\nfunction displayError(message) {\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = `\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\" role=\"alert\">\n <strong class=\"font-bold\">错误:</strong>\n <span class=\"block sm:inline\">${message}</span>\n </div>\n `;\n}\n\nfunction resetFetchButton(button) {\n button.disabled = false;\n button.textContent = '获取模型列表';\n}\n\n// 排序并显示模型列表\nfunction sortAndDisplayModels() {\n models.sort((a, b) => {\n if (a.id === currentTestingModel) return -1;\n if (b.id === currentTestingModel) return 1;\n if (a.pingSuccess && !b.pingSuccess) return -1;\n if (!a.pingSuccess && b.pingSuccess) return 1;\n return a.latency - b.latency;\n });\n\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = '';\n\n const modelGrid = document.createElement('div');\n modelGrid.className = 'grid grid-cols-1 gap-4 w-full';\n modelList.appendChild(modelGrid);\n\n models.forEach((model, index) => {\n const modelDiv = document.createElement('div');\n modelDiv.className = `flex flex-col p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-300 ${currentTestingModel && model.id !== currentTestingModel && !isExpanded ? 'hidden' : ''}`;\n modelDiv.innerHTML = `\n <div class=\"flex items-center space-x-3 mb-2\">\n <span class=\"w-3 h-3 rounded-full flex-shrink-0 ${model.pingSuccess ? 'bg-green-500' : 'bg-red-500'}\"></span>\n <span class=\"font-medium truncate flex-grow\" title=\"${model.id}\">${model.id}</span>\n </div>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm text-gray-500 flex-shrink-0\">${model.pingSuccess ? `${model.latency}ms` : 'N/A'}</span>\n <button onclick=\"startModelTest('${model.id}')\" class=\"test-button text-gray-500 hover:text-gray-700 text-sm transition duration-300 flex-shrink-0\">测试</button>\n </div>\n `;\n const hoverContent = model.pingSuccess \n ? `返回结果: ${model.response.substring(0, 20)}${model.response.length > 20 ? '...' : ''}`\n : `错误: ${model.error}`;\n modelDiv.setAttribute('title', hoverContent);\n modelGrid.appendChild(modelDiv);\n });\n\n if (currentTestingModel) {\n const toggleButton = document.createElement('button');\n toggleButton.id = 'toggleModels';\n toggleButton.className = 'mt-4 text-blue-500 hover:text-blue-700 transition duration-300';\n toggleButton.textContent = isExpanded ? '折叠模型列表' : '展开模型列表';\n toggleButton.onclick = toggleModelVisibility;\n modelList.appendChild(toggleButton);\n }\n}\n\n// Ping 模型\nasync function pingModel(modelId) {\n try {\n const startTime = Date.now();\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: modelId,\n messages: [{ role: \"user\", content: \"Hello\" }],\n max_tokens: 5\n })\n });\n\n if (response.ok) {\n const data = await response.json();\n const endTime = Date.now();\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = true;\n model.latency = endTime - startTime;\n model.response = data.choices[0].message.content;\n } else {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n } catch (error) {\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = false;\n model.latency = Infinity;\n model.error = error.message;\n }\n}\n\n// 更新切换按钮\nfunction updateToggleButton() {\n const modelCard = document.querySelector('#modelList > div');\n let toggleButton = modelCard.querySelector('.toggle-button');\n if (!toggleButton) {\n toggleButton = document.createElement('button');\n toggleButton.className = 'toggle-button col-span-1 sm:col-span-2 mt-4 py-2 text-gray-600 hover:text-gray-800 font-medium transition-colors duration-200';\n }\n\n if (models.length > 6) {\n toggleButton.textContent = '显示更多';\n toggleButton.onclick = () => {\n const hiddenItems = modelCard.querySelectorAll('.hidden');\n if (hiddenItems.length > 0) {\n hiddenItems.forEach(item => item.classList.remove('hidden'));\n toggleButton.textContent = '折叠';\n } else {\n Array.from(modelCard.children).forEach((item, index) => {\n if (index >= 6 && !item.classList.contains('toggle-button')) {\n item.classList.add('hidden');\n }\n });\n toggleButton.textContent = '显示更多';\n }\n };\n modelCard.appendChild(toggleButton);\n } else if (toggleButton.parentNode) {\n toggleButton.parentNode.removeChild(toggleButton);\n }\n}\n\n// 测试模型\nfunction startModelTest(modelId) {\n currentTestingModel = modelId;\n isExpanded = false;\n sortAndDisplayModels();\n const testButton = document.querySelector(`#modelList button[onclick=\"startModelTest('${modelId}')\"]`);\n testButton.innerHTML = '<svg class=\"animate-spin h-5 w-5 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>';\n testButton.disabled = true;\n \n // 清除之前的测试结果\n const existingResults = document.getElementById(`test-results-${modelId}`);\n if (existingResults) {\n existingResults.remove();\n }\n \n // 调用实际的测试函数\n testModel(modelId).then(() => {\n testButton.innerHTML = '测试';\n testButton.disabled = false;\n });\n}\n\nfunction toggleModelVisibility() {\n isExpanded = !isExpanded;\n sortAndDisplayModels();\n}\n\n// 在 unit_test.js 中修改 testModel 函数\nasync function testModel(modelId) {\n // ... 原有的测试逻辑 ...\n\n // 测试完成后,保持当前视图\n sortAndDisplayModels();\n}"}, "dc6ea00bd294dbe9c4a5ad495c0de4ec9d25d192": {"t": 1785758843.0580175, "n": 11071, "h": "7abf53c726837bf8", "c": "const baseUrls = {\n \"Baichuan\": {\n \"base_url\": \"https://api.baichuan-ai.com/v1\",\n \"chinese_name\": \"百川\"\n },\n \"DeepSeek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"chinese_name\": \"深度求索\"\n },\n \"Moonshot\": {\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"chinese_name\": \"月之暗面\"\n },\n \"OpenAI\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"chinese_name\": \"OpenAI\"\n },\n \"Qwen\": {\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"chinese_name\": \"通义千问\"\n },\n \"StepFun\": {\n \"base_url\": \"https://api.stepfun.com/v1\",\n \"chinese_name\": \"阶跃星辰\"\n },\n \"Zhipu\": {\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4/chat/completions\",\n \"chinese_name\": \"智谱\"\n }\n};\n\nlet baseUrl = '';\nlet apiKey = '';\nlet models = [];\nlet currentTestingModel = null;\nlet isExpanded = false;\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n const apiKeyInput = document.getElementById('apiKey');\n const apiKeyMask = document.getElementById('apiKeyMask');\n\n // 添加选项到下拉菜单\n for (const [key, value] of Object.entries(baseUrls)) {\n const option = document.createElement('option');\n option.value = key;\n option.textContent = value.chinese_name;\n baseUrlSelect.appendChild(option);\n }\n\n // 添加自定义选项\n const customOption = document.createElement('option');\n customOption.value = 'custom';\n customOption.textContent = '自定义';\n baseUrlSelect.appendChild(customOption);\n\n // 默认选择 OpenAI\n baseUrlSelect.value = 'OpenAI';\n\n // 处理下拉菜单变化\n baseUrlSelect.addEventListener('change', () => {\n if (baseUrlSelect.value === 'custom') {\n customBaseUrl.classList.remove('hidden');\n } else {\n customBaseUrl.classList.add('hidden');\n }\n });\n\n // 处理 API Key 输入\n apiKeyInput.addEventListener('input', () => {\n const maxVisibleChars = Math.floor(apiKeyInput.offsetWidth / 8) - 3; // 估算可见字符数\n const visibleStars = '*'.repeat(Math.min(apiKeyInput.value.length, maxVisibleChars));\n apiKeyMask.textContent = visibleStars + (apiKeyInput.value.length > maxVisibleChars ? '...' : '');\n });\n\n apiKeyInput.addEventListener('focus', () => {\n apiKeyMask.style.display = 'none';\n });\n\n apiKeyInput.addEventListener('blur', () => {\n if (apiKeyInput.value) {\n apiKeyMask.style.display = 'block';\n }\n });\n\n // 点击遮罩层时显示实际的 API Key 并允许编辑\n apiKeyMask.addEventListener('click', () => {\n apiKeyMask.style.display = 'none';\n apiKeyInput.focus();\n });\n\n document.getElementById('fetchModels').addEventListener('click', fetchModels);\n});\n\nasync function fetchModels() {\n const fetchButton = document.getElementById('fetchModels');\n const modelList = document.getElementById('modelList');\n \n fetchButton.disabled = true;\n fetchButton.innerHTML = '<svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 inline-block align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>获取中';\n\n // 清空之前的模型列表\n modelList.innerHTML = '';\n\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n \n if (baseUrlSelect.value === 'custom') {\n baseUrl = customBaseUrl.value.trim();\n } else {\n baseUrl = baseUrls[baseUrlSelect.value].base_url;\n }\n apiKey = document.getElementById('apiKey').value;\n\n if (!baseUrl || !apiKey) {\n displayError('请选择服务商(或输入自定义 Base URL)并输入 API Key');\n resetFetchButton(fetchButton);\n return;\n }\n\n try {\n const response = await fetch(`${baseUrl}/models`, {\n headers: { 'Authorization': `Bearer ${apiKey}` }\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n models = data.data;\n await Promise.all(models.map(model => pingModel(model.id)));\n sortAndDisplayModels();\n } catch (error) {\n displayError(`获取模型列表失败: ${error.message}`);\n } finally {\n resetFetchButton(fetchButton);\n }\n}\n\nfunction displayError(message) {\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = `\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\" role=\"alert\">\n <strong class=\"font-bold\">错误:</strong>\n <span class=\"block sm:inline\">${message}</span>\n </div>\n `;\n}\n\nfunction resetFetchButton(button) {\n button.disabled = false;\n button.textContent = '获取模型列表';\n}\n\n// 排序并显示模型列表\nfunction sortAndDisplayModels() {\n models.sort((a, b) => {\n if (a.id === currentTestingModel) return -1;\n if (b.id === currentTestingModel) return 1;\n if (a.pingSuccess && !b.pingSuccess) return -1;\n if (!a.pingSuccess && b.pingSuccess) return 1;\n return a.latency - b.latency;\n });\n\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = '';\n\n const modelGrid = document.createElement('div');\n modelGrid.className = 'grid grid-cols-1 gap-4 w-full';\n modelList.appendChild(modelGrid);\n\n models.forEach((model, index) => {\n const modelDiv = document.createElement('div');\n modelDiv.className = `flex flex-col p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-300 ${currentTestingModel && model.id !== currentTestingModel && !isExpanded ? 'hidden' : ''}`;\n modelDiv.innerHTML = `\n <div class=\"flex items-center space-x-3 mb-2\">\n <span class=\"w-3 h-3 rounded-full flex-shrink-0 ${model.pingSuccess ? 'bg-green-500' : 'bg-red-500'}\"></span>\n <span class=\"font-medium truncate flex-grow\" title=\"${model.id}\">${model.id}</span>\n </div>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm text-gray-500 flex-shrink-0\">${model.pingSuccess ? `${model.latency}ms` : 'N/A'}</span>\n <button onclick=\"startModelTest('${model.id}')\" class=\"test-button text-gray-500 hover:text-gray-700 text-sm transition duration-300 flex-shrink-0\">测试</button>\n </div>\n `;\n const hoverContent = model.pingSuccess \n ? `返回结果: ${model.response.substring(0, 20)}${model.response.length > 20 ? '...' : ''}`\n : `错误: ${model.error}`;\n modelDiv.setAttribute('title', hoverContent);\n modelGrid.appendChild(modelDiv);\n });\n\n if (currentTestingModel) {\n const toggleButton = document.createElement('button');\n toggleButton.id = 'toggleModels';\n toggleButton.className = 'mt-4 text-blue-500 hover:text-blue-700 transition duration-300';\n toggleButton.textContent = isExpanded ? '折叠模型列表' : '展开模型列表';\n toggleButton.onclick = toggleModelVisibility;\n modelList.appendChild(toggleButton);\n }\n}\n\n// Ping 模型\nasync function pingModel(modelId) {\n try {\n const startTime = Date.now();\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: modelId,\n messages: [{ role: \"user\", content: \"Hello\" }],\n max_tokens: 5\n })\n });\n\n if (response.ok) {\n const data = await response.json();\n const endTime = Date.now();\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = true;\n model.latency = endTime - startTime;\n model.response = data.choices[0].message.content;\n } else {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n } catch (error) {\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = false;\n model.latency = Infinity;\n model.error = error.message;\n }\n}\n\n// 更新切换按钮\nfunction updateToggleButton() {\n const modelCard = document.querySelector('#modelList > div');\n let toggleButton = modelCard.querySelector('.toggle-button');\n if (!toggleButton) {\n toggleButton = document.createElement('button');\n toggleButton.className = 'toggle-button col-span-1 sm:col-span-2 mt-4 py-2 text-gray-600 hover:text-gray-800 font-medium transition-colors duration-200';\n }\n\n if (models.length > 6) {\n toggleButton.textContent = '显示更多';\n toggleButton.onclick = () => {\n const hiddenItems = modelCard.querySelectorAll('.hidden');\n if (hiddenItems.length > 0) {\n hiddenItems.forEach(item => item.classList.remove('hidden'));\n toggleButton.textContent = '折叠';\n } else {\n Array.from(modelCard.children).forEach((item, index) => {\n if (index >= 6 && !item.classList.contains('toggle-button')) {\n item.classList.add('hidden');\n }\n });\n toggleButton.textContent = '显示更多';\n }\n };\n modelCard.appendChild(toggleButton);\n } else if (toggleButton.parentNode) {\n toggleButton.parentNode.removeChild(toggleButton);\n }\n}\n\n// 测试模型\nfunction startModelTest(modelId) {\n currentTestingModel = modelId;\n isExpanded = false;\n sortAndDisplayModels();\n const testButton = document.querySelector(`#modelList button[onclick=\"startModelTest('${modelId}')\"]`);\n testButton.innerHTML = '<svg class=\"animate-spin h-5 w-5 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>';\n testButton.disabled = true;\n \n // 清除之前的测试结果\n const existingResults = document.getElementById(`test-results-${modelId}`);\n if (existingResults) {\n existingResults.remove();\n }\n \n // 调用实际的测试函数\n testModel(modelId).then(() => {\n testButton.innerHTML = '测试';\n testButton.disabled = false;\n });\n}\n\nfunction toggleModelVisibility() {\n isExpanded = !isExpanded;\n sortAndDisplayModels();\n}\n\n// 在 unit_test.js 中修改 testModel 函数\nasync function testModel(modelId) {\n // ... 原有的测试逻辑 ...\n\n // 测试完成后,保持当前视图\n sortAndDisplayModels();\n}"}, "67ad1e95e83a3273c94ae6d54ee8414e59342c98": {"t": 1785758844.9620051, "n": 11055, "h": "60a4bcf327c51769", "c": "const baseUrls = {\n \"Baichuan\": {\n \"base_url\": \"https://api.baichuan-ai.com/v1\",\n \"chinese_name\": \"百川\"\n },\n \"DeepSeek\": {\n \"base_url\": \"https://api.deepseek.com\",\n \"chinese_name\": \"深度求索\"\n },\n \"Moonshot\": {\n \"base_url\": \"https://api.moonshot.cn/v1\",\n \"chinese_name\": \"月之暗面\"\n },\n \"OpenAI\": {\n \"base_url\": \"https://api.openai.com/v1\",\n \"chinese_name\": \"OpenAI\"\n },\n \"Qwen\": {\n \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n \"chinese_name\": \"通义千问\"\n },\n \"StepFun\": {\n \"base_url\": \"https://api.stepfun.com/v1\",\n \"chinese_name\": \"阶跃星辰\"\n },\n \"Zhipu\": {\n \"base_url\": \"https://open.bigmodel.cn/api/paas/v4/\",\n \"chinese_name\": \"智谱\"\n }\n};\n\nlet baseUrl = '';\nlet apiKey = '';\nlet models = [];\nlet currentTestingModel = null;\nlet isExpanded = false;\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n const apiKeyInput = document.getElementById('apiKey');\n const apiKeyMask = document.getElementById('apiKeyMask');\n\n // 添加选项到下拉菜单\n for (const [key, value] of Object.entries(baseUrls)) {\n const option = document.createElement('option');\n option.value = key;\n option.textContent = value.chinese_name;\n baseUrlSelect.appendChild(option);\n }\n\n // 添加自定义选项\n const customOption = document.createElement('option');\n customOption.value = 'custom';\n customOption.textContent = '自定义';\n baseUrlSelect.appendChild(customOption);\n\n // 默认选择 OpenAI\n baseUrlSelect.value = 'OpenAI';\n\n // 处理下拉菜单变化\n baseUrlSelect.addEventListener('change', () => {\n if (baseUrlSelect.value === 'custom') {\n customBaseUrl.classList.remove('hidden');\n } else {\n customBaseUrl.classList.add('hidden');\n }\n });\n\n // 处理 API Key 输入\n apiKeyInput.addEventListener('input', () => {\n const maxVisibleChars = Math.floor(apiKeyInput.offsetWidth / 8) - 3; // 估算可见字符数\n const visibleStars = '*'.repeat(Math.min(apiKeyInput.value.length, maxVisibleChars));\n apiKeyMask.textContent = visibleStars + (apiKeyInput.value.length > maxVisibleChars ? '...' : '');\n });\n\n apiKeyInput.addEventListener('focus', () => {\n apiKeyMask.style.display = 'none';\n });\n\n apiKeyInput.addEventListener('blur', () => {\n if (apiKeyInput.value) {\n apiKeyMask.style.display = 'block';\n }\n });\n\n // 点击遮罩层时显示实际的 API Key 并允许编辑\n apiKeyMask.addEventListener('click', () => {\n apiKeyMask.style.display = 'none';\n apiKeyInput.focus();\n });\n\n document.getElementById('fetchModels').addEventListener('click', fetchModels);\n});\n\nasync function fetchModels() {\n const fetchButton = document.getElementById('fetchModels');\n const modelList = document.getElementById('modelList');\n \n fetchButton.disabled = true;\n fetchButton.innerHTML = '<svg class=\"animate-spin -ml-1 mr-3 h-5 w-5 inline-block align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>获取中';\n\n // 清空之前的模型列表\n modelList.innerHTML = '';\n\n const baseUrlSelect = document.getElementById('baseUrlSelect');\n const customBaseUrl = document.getElementById('customBaseUrl');\n \n if (baseUrlSelect.value === 'custom') {\n baseUrl = customBaseUrl.value.trim();\n } else {\n baseUrl = baseUrls[baseUrlSelect.value].base_url;\n }\n apiKey = document.getElementById('apiKey').value;\n\n if (!baseUrl || !apiKey) {\n displayError('请选择服务商(或输入自定义 Base URL)并输入 API Key');\n resetFetchButton(fetchButton);\n return;\n }\n\n try {\n const response = await fetch(`${baseUrl}/models`, {\n headers: { 'Authorization': `Bearer ${apiKey}` }\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n models = data.data;\n await Promise.all(models.map(model => pingModel(model.id)));\n sortAndDisplayModels();\n } catch (error) {\n displayError(`获取模型列表失败: ${error.message}`);\n } finally {\n resetFetchButton(fetchButton);\n }\n}\n\nfunction displayError(message) {\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = `\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\" role=\"alert\">\n <strong class=\"font-bold\">错误:</strong>\n <span class=\"block sm:inline\">${message}</span>\n </div>\n `;\n}\n\nfunction resetFetchButton(button) {\n button.disabled = false;\n button.textContent = '获取模型列表';\n}\n\n// 排序并显示模型列表\nfunction sortAndDisplayModels() {\n models.sort((a, b) => {\n if (a.id === currentTestingModel) return -1;\n if (b.id === currentTestingModel) return 1;\n if (a.pingSuccess && !b.pingSuccess) return -1;\n if (!a.pingSuccess && b.pingSuccess) return 1;\n return a.latency - b.latency;\n });\n\n const modelList = document.getElementById('modelList');\n modelList.innerHTML = '';\n\n const modelGrid = document.createElement('div');\n modelGrid.className = 'grid grid-cols-1 gap-4 w-full';\n modelList.appendChild(modelGrid);\n\n models.forEach((model, index) => {\n const modelDiv = document.createElement('div');\n modelDiv.className = `flex flex-col p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition duration-300 ${currentTestingModel && model.id !== currentTestingModel && !isExpanded ? 'hidden' : ''}`;\n modelDiv.innerHTML = `\n <div class=\"flex items-center space-x-3 mb-2\">\n <span class=\"w-3 h-3 rounded-full flex-shrink-0 ${model.pingSuccess ? 'bg-green-500' : 'bg-red-500'}\"></span>\n <span class=\"font-medium truncate flex-grow\" title=\"${model.id}\">${model.id}</span>\n </div>\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm text-gray-500 flex-shrink-0\">${model.pingSuccess ? `${model.latency}ms` : 'N/A'}</span>\n <button onclick=\"startModelTest('${model.id}')\" class=\"test-button text-gray-500 hover:text-gray-700 text-sm transition duration-300 flex-shrink-0\">测试</button>\n </div>\n `;\n const hoverContent = model.pingSuccess \n ? `返回结果: ${model.response.substring(0, 20)}${model.response.length > 20 ? '...' : ''}`\n : `错误: ${model.error}`;\n modelDiv.setAttribute('title', hoverContent);\n modelGrid.appendChild(modelDiv);\n });\n\n if (currentTestingModel) {\n const toggleButton = document.createElement('button');\n toggleButton.id = 'toggleModels';\n toggleButton.className = 'mt-4 text-blue-500 hover:text-blue-700 transition duration-300';\n toggleButton.textContent = isExpanded ? '折叠模型列表' : '展开模型列表';\n toggleButton.onclick = toggleModelVisibility;\n modelList.appendChild(toggleButton);\n }\n}\n\n// Ping 模型\nasync function pingModel(modelId) {\n try {\n const startTime = Date.now();\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: modelId,\n messages: [{ role: \"user\", content: \"Hello\" }],\n max_tokens: 5\n })\n });\n\n if (response.ok) {\n const data = await response.json();\n const endTime = Date.now();\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = true;\n model.latency = endTime - startTime;\n model.response = data.choices[0].message.content;\n } else {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n } catch (error) {\n const model = models.find(m => m.id === modelId);\n model.pingSuccess = false;\n model.latency = Infinity;\n model.error = error.message;\n }\n}\n\n// 更新切换按钮\nfunction updateToggleButton() {\n const modelCard = document.querySelector('#modelList > div');\n let toggleButton = modelCard.querySelector('.toggle-button');\n if (!toggleButton) {\n toggleButton = document.createElement('button');\n toggleButton.className = 'toggle-button col-span-1 sm:col-span-2 mt-4 py-2 text-gray-600 hover:text-gray-800 font-medium transition-colors duration-200';\n }\n\n if (models.length > 6) {\n toggleButton.textContent = '显示更多';\n toggleButton.onclick = () => {\n const hiddenItems = modelCard.querySelectorAll('.hidden');\n if (hiddenItems.length > 0) {\n hiddenItems.forEach(item => item.classList.remove('hidden'));\n toggleButton.textContent = '折叠';\n } else {\n Array.from(modelCard.children).forEach((item, index) => {\n if (index >= 6 && !item.classList.contains('toggle-button')) {\n item.classList.add('hidden');\n }\n });\n toggleButton.textContent = '显示更多';\n }\n };\n modelCard.appendChild(toggleButton);\n } else if (toggleButton.parentNode) {\n toggleButton.parentNode.removeChild(toggleButton);\n }\n}\n\n// 测试模型\nfunction startModelTest(modelId) {\n currentTestingModel = modelId;\n isExpanded = false;\n sortAndDisplayModels();\n const testButton = document.querySelector(`#modelList button[onclick=\"startModelTest('${modelId}')\"]`);\n testButton.innerHTML = '<svg class=\"animate-spin h-5 w-5 text-gray-500\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>';\n testButton.disabled = true;\n \n // 清除之前的测试结果\n const existingResults = document.getElementById(`test-results-${modelId}`);\n if (existingResults) {\n existingResults.remove();\n }\n \n // 调用实际的测试函数\n testModel(modelId).then(() => {\n testButton.innerHTML = '测试';\n testButton.disabled = false;\n });\n}\n\nfunction toggleModelVisibility() {\n isExpanded = !isExpanded;\n sortAndDisplayModels();\n}\n\n// 在 unit_test.js 中修改 testModel 函数\nasync function testModel(modelId) {\n // ... 原有的测试逻辑 ...\n\n // 测试完成后,保持当前视图\n sortAndDisplayModels();\n}"}}}}