// Filename: frontend/js/pages/logs/logList.js
import { escapeHTML } from '../../utils/utils.js';
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+)$/;
class LogList {
constructor(container, dataStore) {
this.container = container;
this.dataStore = dataStore;
if (!this.container) console.error("LogList: container element (tbody) not found.");
}
renderLoading() {
if (!this.container) return;
this.container.innerHTML = `
| 加载日志中... |
`;
}
render(logs, pagination) {
if (!this.container) return;
if (!logs || logs.length === 0) {
this.container.innerHTML = `| 没有找到相关的日志记录。 |
`;
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) {
// 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) {
const styleClass = '';
return `${modelName}
`;
}
createLogRowHtml(log, index) {
const group = this.dataStore.groups.get(log.GroupID);
const groupName = group ? group.display_name : (log.GroupID ? `Group #${log.GroupID}` : 'N/A');
const key = this.dataStore.keys.get(log.KeyID);
let apiKeyDisplay;
if (key && key.APIKey && key.APIKey.length >= 8) {
const masked = `${key.APIKey.substring(0, 4)}......${key.APIKey.substring(key.APIKey.length - 4)}`;
apiKeyDisplay = escapeHTML(masked);
} else {
apiKeyDisplay = 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} |
${apiKeyDisplay} |
${groupName} |
${errorInfo.type} |
${errorInfo.statusCodeHtml} |
${modelNameFormatted} |
${requestTime} |
|
`;
}
}
export default LogList;