diff --git a/.air.toml b/.air.toml index b61d0f9..8813871 100644 --- a/.air.toml +++ b/.air.toml @@ -1,37 +1,27 @@ # .air.toml - -# [_meta] 部分是为了让IDE(如VSCode)知道这是TOML文件,可选 [_meta] "version" = "v1.49.0" -# 工作目录,"." 表示项目根目录 +# 工作目录 root = "." -# 临时文件目录,air会在这里生成可执行文件 tmp_dir = "tmp" [build] - # 编译的入口文件,请确保路径与您的项目结构一致 - cmd = "go build -o ./tmp/main.exe ./cmd/server" - # cmd = "cmd /c build.bat" - # 编译后生成的可执行文件 - bin = "tmp/main.exe" - # 监视以下后缀名的文件,一旦变动就触发重新编译 - # include_ext = ["go", "tpl", "tmpl", "html", "css", "js"] + cmd = "go build -o ./tmp/main ./cmd/server" + bin = "tmp/main" + include_ext = ["go", "tpl", "tmpl", "html"] - # 排除以下目录,避免不必要地重载 + # 排除目录 exclude_dir = ["assets", "tmp", "vendor", "web/static/images"] # 发生构建错误时,只打印日志而不退出 stop_on_error = false - # 给构建日志加上一个漂亮的前缀,方便识别。 + # 日志前缀 log = "air-build.log" - # 增加一点延迟,防止文件系统在保存瞬间触发多次事件。 delay = 1000 # ms [log] - # 日志输出带时间 time = true color = true [misc] - # 删除临时文件 clean_on_exit = true diff --git a/dev-css.sh b/dev-css.sh new file mode 100755 index 0000000..dcd85dd --- /dev/null +++ b/dev-css.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "[CSS] Starting Tailwind watcher..." +tailwindcss -i ./frontend/input.css -o ./web/static/css/output.css --watch diff --git a/dev-js.sh b/dev-js.sh new file mode 100644 index 0000000..93f793d --- /dev/null +++ b/dev-js.sh @@ -0,0 +1,8 @@ +#!/bin/bash +echo "[JS] Starting esbuild watcher..." +esbuild ./frontend/js/main.js \ + --bundle \ + --outdir=./web/static/js \ + --splitting \ + --format=esm \ + --watch=forever diff --git a/dev.sh b/dev.sh deleted file mode 100755 index 2a7b103..0000000 --- a/dev.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -echo "=========================================" -echo " Starting DEVELOPMENT WATCH mode..." -echo "=========================================" -echo "Press Ctrl+C to stop." -echo "" - -echo "[DEV] Starting Tailwind CSS watcher..." -tailwindcss -i ./frontend/input.css -o ./web/static/css/output.css --watch & -TAILWIND_PID=$! - -echo "[DEV] Starting JavaScript watcher with Code Splitting..." -esbuild ./frontend/js/main.js \ - --bundle \ - --outdir=./web/static/js \ - --splitting \ - --format=esm \ - --watch & -ESBUILD_PID=$! - -trap "kill $TAILWIND_PID $ESBUILD_PID 2>/dev/null" EXIT - -wait diff --git a/frontend/input.css b/frontend/input.css index 59b9694..58f6350 100644 --- a/frontend/input.css +++ b/frontend/input.css @@ -751,3 +751,30 @@ .swal2-popup.swal2-custom-style .swal2-icon .swal2-icon-content { @apply text-4xl; } + +@layer components { + /* --- [新增] 可复用的表格组件样式 --- */ + .table { + @apply w-full caption-bottom text-sm; + } + .table-header { + /* 使用语义化颜色,自动适应暗色模式 */ + @apply sticky top-0 z-10 border-b border-border bg-muted/50; + } + .table-header .table-row { + /* 表头的 hover 效果通常与数据行不同,或者没有 */ + @apply hover:bg-transparent; + } + .table-body { + @apply [&_tr:last-child]:border-0; + } + .table-row { + @apply border-b border-border transition-colors hover:bg-muted/80; + } + .table-head-cell { + @apply h-12 px-4 text-left align-middle font-medium text-muted-foreground; + } + .table-cell { + @apply p-4 align-middle; + } +} \ No newline at end of file diff --git a/frontend/js/pages/logs/index.js b/frontend/js/pages/logs/index.js index 5e2f63a..e9c99ba 100644 --- a/frontend/js/pages/logs/index.js +++ b/frontend/js/pages/logs/index.js @@ -7,8 +7,7 @@ class LogsPage { constructor() { this.state = { logs: [], - // [修正] 暂时将分页状态设为默认值,直到后端添加分页支持 - pagination: { page: 1, pages: 1, total: 0 }, + pagination: { page: 1, pages: 1, total: 0, page_size: 20 }, // 包含 page_size isLoading: true, filters: { page: 1, page_size: 20 } }; @@ -45,32 +44,35 @@ class LogsPage { const url = `/admin/logs?page=${this.state.filters.page}&page_size=${this.state.filters.page_size}`; const responseData = await apiFetchJson(url); - // [核心修正] 调整条件以匹配当前 API 返回的 { success: true, data: [...] } 结构 if (responseData && responseData.success && Array.isArray(responseData.data)) { - - // [核心修正] 直接从 responseData.data 获取日志数组 this.state.logs = responseData.data; - // [临时] 由于当前响应不包含分页信息,我们暂时不更新 this.state.pagination - // 等待后端完善分页后,再恢复这里的逻辑 + // [假设] 由于当前响应不包含分页信息,我们基于请求和返回的数据来模拟 + // TODO: 当后端API返回分页对象时,替换此处的模拟数据 + this.state.pagination = { + page: this.state.filters.page, + page_size: this.state.filters.page_size, + total: responseData.data.length, // 这是一个不准确的临时值 + pages: Math.ceil(responseData.data.length / this.state.filters.page_size) // 同样不准确 + }; - this.logList.render(this.state.logs); + // [修改] 将分页状态传递给 render 方法 + this.logList.render(this.state.logs, this.state.pagination); - // this.renderPaginationControls(); } else { console.error("API response for logs is incorrect:", responseData); - this.logList.render([]); + this.logList.render([], this.state.pagination); } - } catch (error) { + } catch (error) + { console.error("Failed to load logs:", error); - // this.logList.renderError(error); + this.logList.render([], this.state.pagination); } finally { this.state.isLoading = false; } } } -// 导出符合 main.js 规范的 default 函数 export default function() { const page = new LogsPage(); page.init(); diff --git a/frontend/js/pages/logs/logList.js b/frontend/js/pages/logs/logList.js index c16d21d..b0747b7 100644 --- a/frontend/js/pages/logs/logList.js +++ b/frontend/js/pages/logs/logList.js @@ -1,11 +1,57 @@ // Filename: frontend/js/pages/logs/logList.js +// --- [扩展] 静态错误码与样式的映射表 (源自Gemini官方文档) --- +const STATIC_ERROR_MAP = { + 'API_KEY_INVALID': { type: '密钥无效', style: 'red' }, + 'INVALID_ARGUMENT': { type: '参数无效', style: 'red' }, + 'PERMISSION_DENIED': { type: '权限不足', style: 'red' }, + 'NOT_FOUND': { type: '资源未找到', style: 'gray' }, + 'RESOURCE_EXHAUSTED': { type: '资源耗尽', style: 'orange' }, + 'QUOTA_EXCEEDED': { type: '配额耗尽', style: 'orange' }, + 'DEADLINE_EXCEEDED': { type: '请求超时', style: 'yellow' }, + 'CANCELLED': { type: '请求已取消', style: 'gray' }, + 'INTERNAL': { type: 'Google内部错误', style: 'yellow' }, + 'UNAVAILABLE': { type: '服务不可用', style: 'yellow' }, +}; +// --- [更新] HTTP状态码到类型和样式的动态映射表 --- +const STATUS_CODE_MAP = { + 400: { type: '错误请求', style: 'red' }, + 401: { type: '认证失败', style: 'red' }, + 403: { type: '禁止访问', style: 'red' }, + 404: { type: '资源未找到', style: 'gray' }, + 413: { type: '请求体过大', style: 'orange' }, + 429: { type: '请求频率过高', style: 'orange' }, + 500: { type: '内部服务错误', style: 'yellow' }, + 503: { type: '服务不可用', style: 'yellow' } +}; +// --- [新增] 特殊场景判断规则 (高优先级) --- +const SPECIAL_CASE_MAP = [ + { code: 400, keyword: 'api key not found', type: '无效密钥', style: 'red' }, + // 之前实现的模型配置错误规则也可以移到这里,更加规范 + { code: 404, keyword: 'call listmodels', type: '模型配置错误', style: 'orange' } +]; + +// --- 样式名称到 Tailwind 类的转换器 --- +const styleToClass = (style) => { + switch (style) { + case 'red': return 'bg-red-500/10 text-red-600'; + case 'orange': return 'bg-orange-500/10 text-orange-600'; + case 'yellow': return 'bg-yellow-500/10 text-yellow-600'; + case 'gray': return 'bg-zinc-500/10 text-zinc-600'; + default: return 'bg-destructive/10 text-destructive'; + } +}; + +// [修正] 修正了正则表达式的名称,使其语义清晰 +const errorCodeRegex = /(\d+)$/; + +// [修正] 移除了 MODEL_STYLE_MAP 的声明,因为它未在 _formatModelName 中使用 +// 如果未来需要,可以重新添加 +// const MODEL_STYLE_MAP = { ... }; class LogList { constructor(container) { - this.container = container; - if (!this.container) { - console.error("LogList: container element (tbody) not found."); - } + this.container = container; + if (!this.container) console.error("LogList: container element (tbody) not found."); } renderLoading() { @@ -13,41 +59,96 @@ class LogList { this.container.innerHTML = ` 加载日志中...`; } - render(logs) { + render(logs, pagination) { if (!this.container) return; - if (!logs || logs.length === 0) { this.container.innerHTML = `没有找到相关的日志记录。`; return; } - - const logsHtml = logs.map(log => this.createLogRowHtml(log)).join(''); + const { page, page_size } = pagination; + const startIndex = (page - 1) * page_size; + const logsHtml = logs.map((log, index) => this.createLogRowHtml(log, startIndex + index + 1)).join(''); this.container.innerHTML = logsHtml; } - createLogRowHtml(log) { - // [后端协作点] 假设后端未来会提供 GroupDisplayName 和 APIKeyName + _interpretError(log) { + // 1. 成功状态 + if (log.IsSuccess) { + return { + type: 'N/A', + statusCodeHtml: `成功` + }; + } + // 2. [新增] 特殊场景优先判断 (结合ErrorCode和ErrorMessage) + const codeMatch = log.ErrorCode ? log.ErrorCode.match(errorCodeRegex) : null; + if (codeMatch && codeMatch[1] && log.ErrorMessage) { + const code = parseInt(codeMatch[1], 10); + const lowerCaseMsg = log.ErrorMessage.toLowerCase(); + for (const rule of SPECIAL_CASE_MAP) { + if (code === rule.code && lowerCaseMsg.includes(rule.keyword)) { + return { + type: rule.type, + statusCodeHtml: `${code}` + }; + } + } + } + // 3. 静态错误码匹配 (例如 "INVALID_ARGUMENT") + if (log.ErrorCode && STATIC_ERROR_MAP[log.ErrorCode]) { + const mapping = STATIC_ERROR_MAP[log.ErrorCode]; + return { + type: mapping.type, + statusCodeHtml: `${log.ErrorCode}` + }; + } + // 4. 动态解析HTTP状态码 (例如 "UPSTREAM_429") + if (codeMatch && codeMatch[1]) { + const code = parseInt(codeMatch[1], 10); + let mapping = STATUS_CODE_MAP[code]; + // 为所有 5xx 错误提供降级 + if (!mapping && code >= 500 && code < 600) { + mapping = STATUS_CODE_MAP[500]; + } + if (mapping) { + return { + type: mapping.type, + statusCodeHtml: `${code}` + }; + } + } + // 5. 边界情况: ErrorCode 和 ErrorMessage 都为空 + if (!log.ErrorCode && !log.ErrorMessage) { + return { type: '未知', statusCodeHtml: `N/A` }; + } + // 6. 最终的降级处理 + return { type: '未知错误', statusCodeHtml: `失败` }; + } + + _formatModelName(modelName) { + // [修正] 移除了对 MODEL_STYLE_MAP 的依赖,简化为统一的样式 + // 这样可以避免因 MODEL_STYLE_MAP 未定义而产生的潜在错误 + const styleClass = ''; // 可根据需要添加回样式逻辑 + return `
${modelName}
`; + } + + createLogRowHtml(log, index) { const groupName = log.GroupDisplayName || (log.GroupID ? `Group #${log.GroupID}` : 'N/A'); const apiKeyName = log.APIKeyName || (log.KeyID ? `Key #${log.KeyID}` : 'N/A'); - - const errorTag = log.IsSuccess - ? `成功` - : `${log.ErrorCode || '失败'}`; - - // 使用 toLocaleString 格式化时间,更符合用户本地习惯 + const errorInfo = this._interpretError(log); + const modelNameFormatted = this._formatModelName(log.ModelName); + const errorMessageAttr = log.ErrorMessage ? `data-error-message="${escape(log.ErrorMessage)}"` : ''; const requestTime = new Date(log.RequestTime).toLocaleString(); - return ` - - - #${log.ID} - ${apiKeyName} - ${groupName} - ${log.ErrorMessage || (log.IsSuccess ? '' : '未知错误')} - ${errorTag} - ${log.ModelName} - ${requestTime} - + + + ${index} + ${apiKeyName} + ${groupName} + ${errorInfo.type} + ${errorInfo.statusCodeHtml} + ${modelNameFormatted} + ${requestTime} + @@ -57,4 +158,5 @@ class LogList { } } +// [核心修正] 移除了文件末尾所有多余的代码,只保留最核心的默认导出 export default LogList; diff --git a/tailwind.config.js b/tailwind.config.js index 844eae2..bfb268e 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -13,6 +13,7 @@ module.exports = { // 定义语义化颜色 fontFamily: { sans: ["Inter", "sans-serif", "Pixelify Sans"], + quinquefive: ['QuinqueFive', 'sans-serif'], mono: [ "JetBrains Mono", "SFMono-Regular", diff --git a/tmp/main b/tmp/main new file mode 100755 index 0000000..af7eb10 Binary files /dev/null and b/tmp/main differ diff --git a/web/static/css/font-awesome/webfonts/quinquefive.woff2 b/web/static/css/font-awesome/webfonts/quinquefive.woff2 new file mode 100644 index 0000000..b6e68d7 Binary files /dev/null and b/web/static/css/font-awesome/webfonts/quinquefive.woff2 differ diff --git a/web/static/css/output.css b/web/static/css/output.css index e43c14e..4f07fd0 100644 --- a/web/static/css/output.css +++ b/web/static/css/output.css @@ -1,4 +1,4 @@ -/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ +/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */ @layer properties; @layer theme, base, components, utilities; @layer theme { @@ -71,6 +71,7 @@ --color-blue-900: oklch(37.9% 0.146 265.522); --color-indigo-100: oklch(93% 0.034 272.788); --color-indigo-300: oklch(78.5% 0.115 274.713); + --color-indigo-500: oklch(58.5% 0.233 277.117); --color-indigo-800: oklch(39.8% 0.195 277.366); --color-indigo-900: oklch(35.9% 0.144 278.697); --color-violet-300: oklch(81.1% 0.111 293.571); @@ -80,6 +81,8 @@ --color-purple-100: oklch(94.6% 0.033 307.174); --color-purple-300: oklch(82.7% 0.119 306.383); --color-purple-400: oklch(71.4% 0.203 305.504); + --color-purple-500: oklch(62.7% 0.265 303.9); + --color-purple-600: oklch(55.8% 0.288 302.321); --color-purple-800: oklch(43.8% 0.218 303.724); --color-purple-900: oklch(38.1% 0.176 304.987); --color-pink-100: oklch(94.8% 0.028 342.258); @@ -329,6 +332,9 @@ .pointer-events-none { pointer-events: none; } + .collapse { + visibility: collapse; + } .invisible { visibility: hidden; } @@ -342,7 +348,7 @@ padding: 0; margin: -1px; overflow: hidden; - clip: rect(0, 0, 0, 0); + clip-path: inset(50%); white-space: nowrap; border-width: 0; } @@ -484,6 +490,9 @@ .m-0 { margin: calc(var(--spacing) * 0); } + .m-7 { + margin: calc(var(--spacing) * 7); + } .mx-1 { margin-inline: calc(var(--spacing) * 1); } @@ -496,6 +505,9 @@ .my-1\.5 { margin-block: calc(var(--spacing) * 1.5); } + .mt-0 { + margin-top: calc(var(--spacing) * 0); + } .mt-0\.5 { margin-top: calc(var(--spacing) * 0.5); } @@ -607,10 +619,19 @@ .table { display: table; } + .table-cell { + display: table-cell; + } + .table-row { + display: table-row; + } .size-6 { width: calc(var(--spacing) * 6); height: calc(var(--spacing) * 6); } + .h-0 { + height: calc(var(--spacing) * 0); + } .h-0\.5 { height: calc(var(--spacing) * 0.5); } @@ -692,6 +713,9 @@ .w-0 { width: calc(var(--spacing) * 0); } + .w-1 { + width: calc(var(--spacing) * 1); + } .w-1\/4 { width: calc(1/4 * 100%); } @@ -797,6 +821,9 @@ .flex-1 { flex: 1; } + .flex-shrink { + flex-shrink: 1; + } .shrink-0 { flex-shrink: 0; } @@ -809,11 +836,14 @@ .caption-bottom { caption-side: bottom; } + .border-collapse { + border-collapse: collapse; + } .origin-center { transform-origin: center; } .origin-top-right { - transform-origin: top right; + transform-origin: 100% 0; } .-translate-x-1 { --tw-translate-x: calc(var(--spacing) * -1); @@ -835,6 +865,10 @@ --tw-translate-x: 100%; translate: var(--tw-translate-x) var(--tw-translate-y); } + .-translate-y-1 { + --tw-translate-y: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -991,6 +1025,9 @@ margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); } } + .gap-x-1 { + column-gap: calc(var(--spacing) * 1); + } .gap-x-1\.5 { column-gap: calc(var(--spacing) * 1.5); } @@ -1136,6 +1173,9 @@ --tw-border-style: none; border-style: none; } + .border-black { + border-color: var(--color-black); + } .border-black\/10 { border-color: color-mix(in srgb, #000 10%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1163,6 +1203,9 @@ .border-green-200 { border-color: var(--color-green-200); } + .border-primary { + border-color: var(--color-primary); + } .border-primary\/20 { border-color: var(--color-primary); @supports (color: color-mix(in lab, red, red)) { @@ -1199,6 +1242,9 @@ .border-zinc-300 { border-color: var(--color-zinc-300); } + .border-zinc-700 { + border-color: var(--color-zinc-700); + } .border-zinc-700\/50 { border-color: color-mix(in srgb, oklch(37% 0.013 285.805) 50%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1274,6 +1320,9 @@ .bg-gray-500 { background-color: var(--color-gray-500); } + .bg-gray-950 { + background-color: var(--color-gray-950); + } .bg-gray-950\/5 { background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 5%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1334,6 +1383,12 @@ .bg-orange-500 { background-color: var(--color-orange-500); } + .bg-orange-500\/10 { + background-color: color-mix(in srgb, oklch(70.5% 0.213 47.604) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-orange-500) 10%, transparent); + } + } .bg-orange-500\/20 { background-color: color-mix(in srgb, oklch(70.5% 0.213 47.604) 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1358,6 +1413,15 @@ .bg-purple-100 { background-color: var(--color-purple-100); } + .bg-purple-500 { + background-color: var(--color-purple-500); + } + .bg-purple-500\/10 { + background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-purple-500) 10%, transparent); + } + } .bg-red-50 { background-color: var(--color-red-50); } @@ -1370,6 +1434,12 @@ .bg-red-500 { background-color: var(--color-red-500); } + .bg-red-500\/10 { + background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-red-500) 10%, transparent); + } + } .bg-red-500\/20 { background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1427,6 +1497,12 @@ .bg-yellow-500 { background-color: var(--color-yellow-500); } + .bg-yellow-500\/10 { + background-color: color-mix(in srgb, oklch(79.5% 0.184 86.047) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-yellow-500) 10%, transparent); + } + } .bg-yellow-500\/20 { background-color: color-mix(in srgb, oklch(79.5% 0.184 86.047) 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1445,6 +1521,12 @@ .bg-zinc-500 { background-color: var(--color-zinc-500); } + .bg-zinc-500\/10 { + background-color: color-mix(in srgb, oklch(55.2% 0.016 285.938) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-zinc-500) 10%, transparent); + } + } .bg-zinc-800 { background-color: var(--color-zinc-800); } @@ -1461,6 +1543,10 @@ --tw-gradient-position: to right in oklab; background-image: linear-gradient(var(--tw-gradient-stops)); } + .from-blue-500 { + --tw-gradient-from: var(--color-blue-500); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } .from-blue-500\/30 { --tw-gradient-from: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1531,6 +1617,9 @@ .px-8 { padding-inline: calc(var(--spacing) * 8); } + .py-0 { + padding-block: calc(var(--spacing) * 0); + } .py-0\.5 { padding-block: calc(var(--spacing) * 0.5); } @@ -1579,6 +1668,9 @@ .pr-20 { padding-right: calc(var(--spacing) * 20); } + .pb-1 { + padding-bottom: calc(var(--spacing) * 1); + } .pb-1\.5 { padding-bottom: calc(var(--spacing) * 1.5); } @@ -1618,6 +1710,9 @@ .align-middle { vertical-align: middle; } + .font-\[\'Pixelify_Sans\'\] { + font-family: 'Pixelify Sans'; + } .font-mono { font-family: var(--font-mono); } @@ -1763,6 +1858,9 @@ .text-green-800 { color: var(--color-green-800); } + .text-indigo-500 { + color: var(--color-indigo-500); + } .text-indigo-800 { color: var(--color-indigo-800); } @@ -1775,6 +1873,9 @@ .text-orange-500 { color: var(--color-orange-500); } + .text-orange-600 { + color: var(--color-orange-600); + } .text-orange-800 { color: var(--color-orange-800); } @@ -1784,6 +1885,9 @@ .text-primary-foreground { color: var(--color-primary-foreground); } + .text-purple-600 { + color: var(--color-purple-600); + } .text-purple-800 { color: var(--color-purple-800); } @@ -1853,6 +1957,9 @@ .italic { font-style: italic; } + .underline { + text-decoration-line: underline; + } .opacity-0 { opacity: 0%; } @@ -1910,6 +2017,10 @@ --tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, oklab(from rgb(0 0 0 / 0.05) l a b / 25%)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .inset-shadow-sm { + --tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, rgb(0 0 0 / 0.05)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .ring-black { --tw-ring-color: var(--color-black); } @@ -1931,6 +2042,10 @@ --tw-ring-color: color-mix(in oklab, var(--color-black) 15%, transparent); } } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } .blur { --tw-blur: blur(8px); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); @@ -1957,7 +2072,7 @@ backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); } .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } @@ -2029,6 +2144,9 @@ -webkit-user-select: none; user-select: none; } + .\[rows\:\%v\] { + rows: %v; + } .group-hover\:opacity-100 { &:is(:where(.group):hover *) { @media (hover: hover) { @@ -3451,19 +3569,12 @@ width: 100%; align-items: flex-start; border-radius: var(--radius-lg); - background-color: color-mix(in srgb, #fff 80%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-white) 80%, transparent); - } + background-color: color-mix(in oklab, var(--color-white) 80%, transparent); padding: calc(var(--spacing) * 3); --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - --tw-ring-color: color-mix(in srgb, #000 5%, transparent); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-black) 5%, transparent); - } + --tw-ring-color: color-mix(in oklab, var(--color-black) 5%, transparent); --tw-backdrop-blur: blur(var(--blur-md)); -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); @@ -3654,7 +3765,6 @@ top: calc(1/2 * 100%); left: calc(1/2 * 100%); --tw-translate-x: calc(calc(1/2 * 100%) * -1); - translate: var(--tw-translate-x) var(--tw-translate-y); --tw-translate-y: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); opacity: 0%; @@ -4366,7 +4476,7 @@ padding-block: calc(var(--spacing) * 2); font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); --tw-duration: 150ms; @@ -4552,7 +4662,7 @@ line-height: var(--tw-leading, var(--text-xs--line-height)); --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); --tw-duration: 150ms; @@ -4602,7 +4712,7 @@ line-height: var(--tw-leading, var(--text-xs--line-height)); --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); --tw-duration: 150ms; @@ -4814,6 +4924,68 @@ font-size: var(--text-4xl); line-height: var(--tw-leading, var(--text-4xl--line-height)); } +@layer components { + .table { + width: 100%; + caption-side: bottom; + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .table-header { + position: sticky; + top: calc(var(--spacing) * 0); + z-index: 10; + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + border-color: var(--color-border); + background-color: var(--color-muted); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-muted) 50%, transparent); + } + } + .table-header .table-row { + &:hover { + @media (hover: hover) { + background-color: transparent; + } + } + } + .table-body { + & tr:last-child { + border-style: var(--tw-border-style); + border-width: 0px; + } + } + .table-row { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + border-color: var(--color-border); + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + &:hover { + @media (hover: hover) { + background-color: var(--color-muted); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-muted) 80%, transparent); + } + } + } + } + .table-head-cell { + height: calc(var(--spacing) * 12); + padding-inline: calc(var(--spacing) * 4); + text-align: left; + vertical-align: middle; + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + color: var(--color-muted-foreground); + } + .table-cell { + padding: calc(var(--spacing) * 4); + vertical-align: middle; + } +} @property --tw-translate-x { syntax: "*"; inherits: false; @@ -4988,6 +5160,11 @@ inherits: false; initial-value: 0 0 #0000; } +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @property --tw-blur { syntax: "*"; inherits: false; @@ -5100,11 +5277,6 @@ inherits: false; initial-value: 1; } -@property --tw-outline-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} @keyframes spin { to { transform: rotate(360deg); @@ -5162,6 +5334,7 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; @@ -5189,7 +5362,6 @@ --tw-scale-x: 1; --tw-scale-y: 1; --tw-scale-z: 1; - --tw-outline-style: solid; } } } diff --git a/web/static/js/logs-4C4JG7BT.js b/web/static/js/logs-4C4JG7BT.js new file mode 100644 index 0000000..78e58a3 --- /dev/null +++ b/web/static/js/logs-4C4JG7BT.js @@ -0,0 +1,208 @@ +import { + apiFetchJson +} from "./chunk-PLQL6WIO.js"; + +// frontend/js/pages/logs/logList.js +var STATIC_ERROR_MAP = { + "API_KEY_INVALID": { type: "\u5BC6\u94A5\u65E0\u6548", style: "red" }, + "INVALID_ARGUMENT": { type: "\u53C2\u6570\u65E0\u6548", style: "red" }, + "PERMISSION_DENIED": { type: "\u6743\u9650\u4E0D\u8DB3", style: "red" }, + "NOT_FOUND": { type: "\u8D44\u6E90\u672A\u627E\u5230", style: "gray" }, + "RESOURCE_EXHAUSTED": { type: "\u8D44\u6E90\u8017\u5C3D", style: "orange" }, + "QUOTA_EXCEEDED": { type: "\u914D\u989D\u8017\u5C3D", style: "orange" }, + "DEADLINE_EXCEEDED": { type: "\u8BF7\u6C42\u8D85\u65F6", style: "yellow" }, + "CANCELLED": { type: "\u8BF7\u6C42\u5DF2\u53D6\u6D88", style: "gray" }, + "INTERNAL": { type: "Google\u5185\u90E8\u9519\u8BEF", style: "yellow" }, + "UNAVAILABLE": { type: "\u670D\u52A1\u4E0D\u53EF\u7528", style: "yellow" } +}; +var STATUS_CODE_MAP = { + 400: { type: "\u9519\u8BEF\u8BF7\u6C42", style: "red" }, + 401: { type: "\u8BA4\u8BC1\u5931\u8D25", style: "red" }, + 403: { type: "\u7981\u6B62\u8BBF\u95EE", style: "red" }, + 404: { type: "\u8D44\u6E90\u672A\u627E\u5230", style: "gray" }, + 413: { type: "\u8BF7\u6C42\u4F53\u8FC7\u5927", style: "orange" }, + 429: { type: "\u8BF7\u6C42\u9891\u7387\u8FC7\u9AD8", style: "orange" }, + 500: { type: "\u5185\u90E8\u670D\u52A1\u9519\u8BEF", style: "yellow" }, + 503: { type: "\u670D\u52A1\u4E0D\u53EF\u7528", style: "yellow" } +}; +var SPECIAL_CASE_MAP = [ + { code: 400, keyword: "api key not found", type: "\u65E0\u6548\u5BC6\u94A5", style: "red" }, + // 之前实现的模型配置错误规则也可以移到这里,更加规范 + { code: 404, keyword: "call listmodels", type: "\u6A21\u578B\u914D\u7F6E\u9519\u8BEF", style: "orange" } +]; +var styleToClass = (style) => { + switch (style) { + case "red": + return "bg-red-500/10 text-red-600"; + case "orange": + return "bg-orange-500/10 text-orange-600"; + case "yellow": + return "bg-yellow-500/10 text-yellow-600"; + case "gray": + return "bg-zinc-500/10 text-zinc-600"; + default: + return "bg-destructive/10 text-destructive"; + } +}; +var errorCodeRegex = /(\d+)$/; +var LogList = class { + constructor(container) { + this.container = container; + if (!this.container) console.error("LogList: container element (tbody) not found."); + } + renderLoading() { + if (!this.container) return; + this.container.innerHTML = ` \u52A0\u8F7D\u65E5\u5FD7\u4E2D...`; + } + render(logs, pagination) { + if (!this.container) return; + if (!logs || logs.length === 0) { + this.container.innerHTML = `\u6CA1\u6709\u627E\u5230\u76F8\u5173\u7684\u65E5\u5FD7\u8BB0\u5F55\u3002`; + return; + } + const { page, page_size } = pagination; + const startIndex = (page - 1) * page_size; + const logsHtml = logs.map((log, index) => this.createLogRowHtml(log, startIndex + index + 1)).join(""); + this.container.innerHTML = logsHtml; + } + _interpretError(log) { + if (log.IsSuccess) { + return { + type: "N/A", + statusCodeHtml: `\u6210\u529F` + }; + } + const codeMatch = log.ErrorCode ? log.ErrorCode.match(errorCodeRegex) : null; + if (codeMatch && codeMatch[1] && log.ErrorMessage) { + const code = parseInt(codeMatch[1], 10); + const lowerCaseMsg = log.ErrorMessage.toLowerCase(); + for (const rule of SPECIAL_CASE_MAP) { + if (code === rule.code && lowerCaseMsg.includes(rule.keyword)) { + return { + type: rule.type, + statusCodeHtml: `${code}` + }; + } + } + } + if (log.ErrorCode && STATIC_ERROR_MAP[log.ErrorCode]) { + const mapping = STATIC_ERROR_MAP[log.ErrorCode]; + return { + type: mapping.type, + statusCodeHtml: `${log.ErrorCode}` + }; + } + if (codeMatch && codeMatch[1]) { + const code = parseInt(codeMatch[1], 10); + let mapping = STATUS_CODE_MAP[code]; + if (!mapping && code >= 500 && code < 600) { + mapping = STATUS_CODE_MAP[500]; + } + if (mapping) { + return { + type: mapping.type, + statusCodeHtml: `${code}` + }; + } + } + if (!log.ErrorCode && !log.ErrorMessage) { + return { type: "\u672A\u77E5", statusCodeHtml: `N/A` }; + } + return { type: "\u672A\u77E5\u9519\u8BEF", statusCodeHtml: `\u5931\u8D25` }; + } + _formatModelName(modelName) { + const styleClass = ""; + return `
${modelName}
`; + } + createLogRowHtml(log, index) { + const groupName = log.GroupDisplayName || (log.GroupID ? `Group #${log.GroupID}` : "N/A"); + const apiKeyName = log.APIKeyName || (log.KeyID ? `Key #${log.KeyID}` : "N/A"); + const errorInfo = this._interpretError(log); + const modelNameFormatted = this._formatModelName(log.ModelName); + const errorMessageAttr = log.ErrorMessage ? `data-error-message="${escape(log.ErrorMessage)}"` : ""; + const requestTime = new Date(log.RequestTime).toLocaleString(); + return ` + + + ${index} + ${apiKeyName} + ${groupName} + ${errorInfo.type} + ${errorInfo.statusCodeHtml} + ${modelNameFormatted} + ${requestTime} + + + + + `; + } +}; +var logList_default = LogList; + +// frontend/js/pages/logs/index.js +var LogsPage = class { + constructor() { + this.state = { + logs: [], + pagination: { page: 1, pages: 1, total: 0, page_size: 20 }, + // 包含 page_size + isLoading: true, + filters: { page: 1, page_size: 20 } + }; + this.elements = { + tableBody: document.getElementById("logs-table-body") + }; + this.initialized = !!this.elements.tableBody; + if (this.initialized) { + this.logList = new logList_default(this.elements.tableBody); + } + } + async init() { + if (!this.initialized) { + console.error("LogsPage: Could not initialize. Essential container element 'logs-table-body' is missing."); + return; + } + this.initEventListeners(); + await this.loadAndRenderLogs(); + } + initEventListeners() { + } + async loadAndRenderLogs() { + this.state.isLoading = true; + this.logList.renderLoading(); + try { + const url = `/admin/logs?page=${this.state.filters.page}&page_size=${this.state.filters.page_size}`; + const responseData = await apiFetchJson(url); + if (responseData && responseData.success && Array.isArray(responseData.data)) { + this.state.logs = responseData.data; + this.state.pagination = { + page: this.state.filters.page, + page_size: this.state.filters.page_size, + total: responseData.data.length, + // 这是一个不准确的临时值 + pages: Math.ceil(responseData.data.length / this.state.filters.page_size) + // 同样不准确 + }; + this.logList.render(this.state.logs, this.state.pagination); + } else { + console.error("API response for logs is incorrect:", responseData); + this.logList.render([], this.state.pagination); + } + } catch (error) { + console.error("Failed to load logs:", error); + this.logList.render([], this.state.pagination); + } finally { + this.state.isLoading = false; + } + } +}; +function logs_default() { + const page = new LogsPage(); + page.init(); +} +export { + logs_default as default +}; diff --git a/web/static/js/logs-FGZ2SMPN.js b/web/static/js/logs-FGZ2SMPN.js deleted file mode 100644 index 7cf702a..0000000 --- a/web/static/js/logs-FGZ2SMPN.js +++ /dev/null @@ -1,106 +0,0 @@ -import { - apiFetchJson -} from "./chunk-PLQL6WIO.js"; - -// frontend/js/pages/logs/logList.js -var LogList = class { - constructor(container) { - this.container = container; - if (!this.container) { - console.error("LogList: container element (tbody) not found."); - } - } - renderLoading() { - if (!this.container) return; - this.container.innerHTML = ` \u52A0\u8F7D\u65E5\u5FD7\u4E2D...`; - } - render(logs) { - if (!this.container) return; - if (!logs || logs.length === 0) { - this.container.innerHTML = `\u6CA1\u6709\u627E\u5230\u76F8\u5173\u7684\u65E5\u5FD7\u8BB0\u5F55\u3002`; - return; - } - const logsHtml = logs.map((log) => this.createLogRowHtml(log)).join(""); - this.container.innerHTML = logsHtml; - } - createLogRowHtml(log) { - const groupName = log.GroupDisplayName || (log.GroupID ? `Group #${log.GroupID}` : "N/A"); - const apiKeyName = log.APIKeyName || (log.KeyID ? `Key #${log.KeyID}` : "N/A"); - const errorTag = log.IsSuccess ? `\u6210\u529F` : `${log.ErrorCode || "\u5931\u8D25"}`; - const requestTime = new Date(log.RequestTime).toLocaleString(); - return ` - - - #${log.ID} - ${apiKeyName} - ${groupName} - ${log.ErrorMessage || (log.IsSuccess ? "" : "\u672A\u77E5\u9519\u8BEF")} - ${errorTag} - ${log.ModelName} - ${requestTime} - - - - - `; - } -}; -var logList_default = LogList; - -// frontend/js/pages/logs/index.js -var LogsPage = class { - constructor() { - this.state = { - logs: [], - // [修正] 暂时将分页状态设为默认值,直到后端添加分页支持 - pagination: { page: 1, pages: 1, total: 0 }, - isLoading: true, - filters: { page: 1, page_size: 20 } - }; - this.elements = { - tableBody: document.getElementById("logs-table-body") - }; - this.initialized = !!this.elements.tableBody; - if (this.initialized) { - this.logList = new logList_default(this.elements.tableBody); - } - } - async init() { - if (!this.initialized) { - console.error("LogsPage: Could not initialize. Essential container element 'logs-table-body' is missing."); - return; - } - this.initEventListeners(); - await this.loadAndRenderLogs(); - } - initEventListeners() { - } - async loadAndRenderLogs() { - this.state.isLoading = true; - this.logList.renderLoading(); - try { - const url = `/admin/logs?page=${this.state.filters.page}&page_size=${this.state.filters.page_size}`; - const responseData = await apiFetchJson(url); - if (responseData && responseData.success && Array.isArray(responseData.data)) { - this.state.logs = responseData.data; - this.logList.render(this.state.logs); - } else { - console.error("API response for logs is incorrect:", responseData); - this.logList.render([]); - } - } catch (error) { - console.error("Failed to load logs:", error); - } finally { - this.state.isLoading = false; - } - } -}; -function logs_default() { - const page = new LogsPage(); - page.init(); -} -export { - logs_default as default -}; diff --git a/web/static/js/main.js b/web/static/js/main.js index ba3b043..1849eff 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -181,7 +181,7 @@ var pageModules = { // esbuild 看到这个 import() 语法,就会自动将 dashboard.js 及其依赖打包成一个独立的 chunk 文件 "dashboard": () => import("./dashboard-CJJWKYPR.js"), "keys": () => import("./keys-A2UAJYOX.js"), - "logs": () => import("./logs-FGZ2SMPN.js") + "logs": () => import("./logs-4C4JG7BT.js") // 'settings': () => import('./pages/settings.js'), // 未来启用 settings 页面 // 未来新增的页面,只需在这里添加一行映射,esbuild会自动处理 }; diff --git a/web/templates/base.html b/web/templates/base.html index d6bc473..55df2f2 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -7,7 +7,7 @@ - + {% block title %}GEMINI BALANCER{% endblock %} diff --git a/web/templates/logs.html b/web/templates/logs.html index 19d2cbe..ce517e5 100644 --- a/web/templates/logs.html +++ b/web/templates/logs.html @@ -76,61 +76,26 @@
- - - -
- + + + + + - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+ IDGemini 密钥错误类型错误码模型名称请求时间操作序号Gemini 密钥群组名称错误类型状态码模型名称请求时间操作
#12346AIza...s7f1API Key Invalid429gemini-1.5-pro-latest2024-05-21 10:31:15 - -
#12347AIza...s7f2Quota Exceeded429gemini-1.0-pro2024-05-21 10:32:15 - -
#12348AIza...s7f3Server Error500gemini-1.5-pro-latest2024-05-21 10:33:15 - -