perpare to fix RequestFinishedEvent
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = `<tr><td colspan="9" class="p-8 text-center text-muted-foreground"><i class="fas fa-spinner fa-spin mr-2"></i> 加载日志中...</td></tr>`;
|
||||
}
|
||||
|
||||
render(logs) {
|
||||
render(logs, pagination) {
|
||||
if (!this.container) return;
|
||||
|
||||
if (!logs || logs.length === 0) {
|
||||
this.container.innerHTML = `<tr><td colspan="9" class="p-8 text-center text-muted-foreground">没有找到相关的日志记录。</td></tr>`;
|
||||
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: `<span class="inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-600">成功</span>`
|
||||
};
|
||||
}
|
||||
// 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: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass(rule.style)}">${code}</span>`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. 静态错误码匹配 (例如 "INVALID_ARGUMENT")
|
||||
if (log.ErrorCode && STATIC_ERROR_MAP[log.ErrorCode]) {
|
||||
const mapping = STATIC_ERROR_MAP[log.ErrorCode];
|
||||
return {
|
||||
type: mapping.type,
|
||||
statusCodeHtml: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass(mapping.style)}">${log.ErrorCode}</span>`
|
||||
};
|
||||
}
|
||||
// 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: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass(mapping.style)}">${code}</span>`
|
||||
};
|
||||
}
|
||||
}
|
||||
// 5. 边界情况: ErrorCode 和 ErrorMessage 都为空
|
||||
if (!log.ErrorCode && !log.ErrorMessage) {
|
||||
return { type: '未知', statusCodeHtml: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass('gray')}">N/A</span>` };
|
||||
}
|
||||
// 6. 最终的降级处理
|
||||
return { type: '未知错误', statusCodeHtml: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass('default')}">失败</span>` };
|
||||
}
|
||||
|
||||
_formatModelName(modelName) {
|
||||
// [修正] 移除了对 MODEL_STYLE_MAP 的依赖,简化为统一的样式
|
||||
// 这样可以避免因 MODEL_STYLE_MAP 未定义而产生的潜在错误
|
||||
const styleClass = ''; // 可根据需要添加回样式逻辑
|
||||
return `<div class="inline-block rounded bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5"><span class="font-quinquefive text-xs tracking-wider ${styleClass}">${modelName}</span></div>`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<span class="inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-600">成功</span>`
|
||||
: `<span class="inline-flex items-center rounded-md bg-destructive/10 px-2 py-1 text-xs font-medium text-destructive">${log.ErrorCode || '失败'}</span>`;
|
||||
|
||||
// 使用 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 `
|
||||
<tr class="border-b border-b-border transition-colors hover:bg-muted/80" data-log-id="${log.ID}">
|
||||
<td class="p-4 align-middle"><input type="checkbox" class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500"></td>
|
||||
<td class="p-4 align-middle font-mono text-muted-foreground">#${log.ID}</td>
|
||||
<td class="p-4 align-middle font-medium font-mono">${apiKeyName}</td>
|
||||
<td class="p-4 align-middle">${groupName}</td>
|
||||
<td class="p-4 align-middle text-foreground">${log.ErrorMessage || (log.IsSuccess ? '' : '未知错误')}</td>
|
||||
<td class="p-4 align-middle">${errorTag}</td>
|
||||
<td class="p-4 align-middle font-mono">${log.ModelName}</td>
|
||||
<td class="p-4 align-middle text-muted-foreground text-xs">${requestTime}</td>
|
||||
<td class="p-4 align-middle">
|
||||
<tr class="table-row" data-log-id="${log.ID}" ${errorMessageAttr}>
|
||||
<td class="table-cell"><input type="checkbox" class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500"></td>
|
||||
<td class="table-cell font-mono text-muted-foreground">${index}</td>
|
||||
<td class="table-cell font-medium font-mono">${apiKeyName}</td>
|
||||
<td class="table-cell">${groupName}</td>
|
||||
<td class="table-cell">${errorInfo.type}</td>
|
||||
<td class="table-cell">${errorInfo.statusCodeHtml}</td>
|
||||
<td class="table-cell">${modelNameFormatted}</td>
|
||||
<td class="table-cell text-muted-foreground text-xs">${requestTime}</td>
|
||||
<td class="table-cell">
|
||||
<button class="btn btn-ghost btn-icon btn-sm" aria-label="查看详情">
|
||||
<i class="fas fa-ellipsis-h h-4 w-4"></i>
|
||||
</button>
|
||||
@@ -57,4 +158,5 @@ class LogList {
|
||||
}
|
||||
}
|
||||
|
||||
// [核心修正] 移除了文件末尾所有多余的代码,只保留最核心的默认导出
|
||||
export default LogList;
|
||||
|
||||
Reference in New Issue
Block a user