Fix loglist

This commit is contained in:
XOF
2025-11-21 19:33:05 +08:00
parent 1f7aa70810
commit 6a0f344e5c
22 changed files with 380 additions and 357 deletions

View File

@@ -332,9 +332,6 @@
.pointer-events-none {
pointer-events: none;
}
.collapse {
visibility: collapse;
}
.invisible {
visibility: hidden;
}
@@ -490,9 +487,6 @@
.m-0 {
margin: calc(var(--spacing) * 0);
}
.m-7 {
margin: calc(var(--spacing) * 7);
}
.mx-1 {
margin-inline: calc(var(--spacing) * 1);
}
@@ -505,9 +499,6 @@
.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);
}
@@ -604,9 +595,6 @@
.hidden {
display: none;
}
.inline {
display: inline;
}
.inline-block {
display: inline-block;
}
@@ -629,9 +617,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);
}
@@ -713,9 +698,6 @@
.w-0 {
width: calc(var(--spacing) * 0);
}
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\/4 {
width: calc(1/4 * 100%);
}
@@ -821,9 +803,6 @@
.flex-1 {
flex: 1;
}
.flex-shrink {
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
@@ -836,9 +815,6 @@
.caption-bottom {
caption-side: bottom;
}
.border-collapse {
border-collapse: collapse;
}
.origin-center {
transform-origin: center;
}
@@ -865,10 +841,6 @@
--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);
@@ -1025,9 +997,6 @@
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);
}
@@ -1173,9 +1142,6 @@
--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)) {
@@ -1203,9 +1169,6 @@
.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)) {
@@ -1242,9 +1205,6 @@
.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)) {
@@ -1257,9 +1217,6 @@
.border-b-border {
border-bottom-color: var(--color-border);
}
.border-b-zinc-200 {
border-bottom-color: var(--color-zinc-200);
}
.bg-accent {
background-color: var(--color-accent);
}
@@ -1320,9 +1277,6 @@
.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)) {
@@ -1413,15 +1367,6 @@
.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);
}
@@ -1543,10 +1488,6 @@
--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)) {
@@ -1617,9 +1558,6 @@
.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);
}
@@ -1668,9 +1606,6 @@
.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);
}
@@ -1710,9 +1645,6 @@
.align-middle {
vertical-align: middle;
}
.font-\[\'Pixelify_Sans\'\] {
font-family: 'Pixelify Sans';
}
.font-mono {
font-family: var(--font-mono);
}
@@ -1858,9 +1790,6 @@
.text-green-800 {
color: var(--color-green-800);
}
.text-indigo-500 {
color: var(--color-indigo-500);
}
.text-indigo-800 {
color: var(--color-indigo-800);
}
@@ -1885,9 +1814,6 @@
.text-primary-foreground {
color: var(--color-primary-foreground);
}
.text-purple-600 {
color: var(--color-purple-600);
}
.text-purple-800 {
color: var(--color-purple-800);
}
@@ -1924,9 +1850,6 @@
.text-yellow-600 {
color: var(--color-yellow-600);
}
.text-yellow-700 {
color: var(--color-yellow-700);
}
.text-zinc-100 {
color: var(--color-zinc-100);
}
@@ -1957,9 +1880,6 @@
.italic {
font-style: italic;
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0%;
}
@@ -2017,10 +1937,6 @@
--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);
}
@@ -2042,10 +1958,6 @@
--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,);
@@ -2144,9 +2056,6 @@
-webkit-user-select: none;
user-select: none;
}
.\[rows\:\%v\] {
rows: %v;
}
.group-hover\:opacity-100 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
@@ -2366,16 +2275,6 @@
}
}
}
.hover\:bg-zinc-100\/50 {
&:hover {
@media (hover: hover) {
background-color: color-mix(in srgb, oklch(96.7% 0.001 286.375) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-zinc-100) 50%, transparent);
}
}
}
}
.hover\:bg-zinc-200 {
&:hover {
@media (hover: hover) {
@@ -2777,16 +2676,6 @@
border-color: var(--color-zinc-800);
}
}
.dark\:border-b-zinc-700 {
&:where(.dark, .dark *) {
border-bottom-color: var(--color-zinc-700);
}
}
.dark\:border-b-zinc-800 {
&:where(.dark, .dark *) {
border-bottom-color: var(--color-zinc-800);
}
}
.dark\:bg-blue-900 {
&:where(.dark, .dark *) {
background-color: var(--color-blue-900);
@@ -3038,11 +2927,6 @@
color: var(--color-white);
}
}
.dark\:text-yellow-400 {
&:where(.dark, .dark *) {
color: var(--color-yellow-400);
}
}
.dark\:text-zinc-100 {
&:where(.dark, .dark *) {
color: var(--color-zinc-100);
@@ -3147,18 +3031,6 @@
}
}
}
.dark\:hover\:bg-zinc-800\/50 {
&:where(.dark, .dark *) {
&:hover {
@media (hover: hover) {
background-color: color-mix(in srgb, oklch(27.4% 0.006 286.033) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-zinc-800) 50%, transparent);
}
}
}
}
}
.dark\:hover\:text-blue-300 {
&:where(.dark, .dark *) {
&:hover {
@@ -5160,11 +5032,6 @@
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -5277,6 +5144,11 @@
inherits: false;
initial-value: 1;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@keyframes spin {
to {
transform: rotate(360deg);
@@ -5334,7 +5206,6 @@
--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;
@@ -5362,6 +5233,7 @@
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-scale-z: 1;
--tw-outline-style: solid;
}
}
}

View File

@@ -0,0 +1,52 @@
// frontend/js/utils/utils.js
function debounce(func, wait) {
let timeout;
const debounced = function(...args) {
const context = this;
const later = () => {
clearTimeout(timeout);
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
debounced.cancel = () => {
clearTimeout(timeout);
};
return debounced;
}
function isValidApiKeyFormat(key) {
const patterns = [
// Google Gemini API Key: AIzaSy + 33 characters (alphanumeric, _, -)
/^AIzaSy[\w-]{33}$/,
// OpenAI API Key (新格式): sk- + 48 alphanumeric characters
/^sk-[\w]{48}$/,
// Google AI Studio Key: gsk_ + alphanumeric & hyphens
/^gsk_[\w-]{40,}$/,
// Anthropic API Key (示例): sk-ant-api03- + long string
/^sk-ant-api\d{2}-[\w-]{80,}$/,
// Fallback for other potential "sk-" keys with a reasonable length
/^sk-[\w-]{20,}$/
];
return patterns.some((pattern) => pattern.test(key));
}
function escapeHTML(str) {
if (typeof str !== "string") {
return str;
}
return str.replace(/[&<>"']/g, function(match) {
return {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;"
}[match];
});
}
export {
debounce,
isValidApiKeyFormat,
escapeHTML
};

View File

@@ -4,6 +4,11 @@ import {
taskCenterManager,
toastManager
} from "./chunk-EZAP7GR4.js";
import {
debounce,
escapeHTML,
isValidApiKeyFormat
} from "./chunk-A4OOMLXK.js";
import {
apiFetch,
apiFetchJson
@@ -670,53 +675,6 @@ var ApiKeyManager = class {
};
var apiKeyManager = new ApiKeyManager();
// frontend/js/utils/utils.js
function debounce(func, wait) {
let timeout;
const debounced = function(...args) {
const context = this;
const later = () => {
clearTimeout(timeout);
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
debounced.cancel = () => {
clearTimeout(timeout);
};
return debounced;
}
function isValidApiKeyFormat(key) {
const patterns = [
// Google Gemini API Key: AIzaSy + 33 characters (alphanumeric, _, -)
/^AIzaSy[\w-]{33}$/,
// OpenAI API Key (新格式): sk- + 48 alphanumeric characters
/^sk-[\w]{48}$/,
// Google AI Studio Key: gsk_ + alphanumeric & hyphens
/^gsk_[\w-]{40,}$/,
// Anthropic API Key (示例): sk-ant-api03- + long string
/^sk-ant-api\d{2}-[\w-]{80,}$/,
// Fallback for other potential "sk-" keys with a reasonable length
/^sk-[\w-]{20,}$/
];
return patterns.some((pattern) => pattern.test(key));
}
function escapeHTML(str) {
if (typeof str !== "string") {
return str;
}
return str.replace(/[&<>"']/g, function(match) {
return {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;"
}[match];
});
}
// frontend/js/pages/keys/addApiModal.js
var AddApiModal = class {
constructor({ onImportSuccess }) {

View File

@@ -1,3 +1,6 @@
import {
escapeHTML
} from "./chunk-A4OOMLXK.js";
import {
apiFetchJson
} from "./chunk-PLQL6WIO.js";
@@ -27,7 +30,6 @@ var STATUS_CODE_MAP = {
};
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) => {
@@ -46,8 +48,9 @@ var styleToClass = (style) => {
};
var errorCodeRegex = /(\d+)$/;
var LogList = class {
constructor(container) {
constructor(container, dataStore2) {
this.container = container;
this.dataStore = dataStore2;
if (!this.container) console.error("LogList: container element (tbody) not found.");
}
renderLoading() {
@@ -115,8 +118,16 @@ var LogList = class {
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 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)}"` : "";
@@ -125,7 +136,7 @@ var LogList = class {
<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 font-medium font-mono">${apiKeyDisplay}</td>
<td class="table-cell">${groupName}</td>
<td class="table-cell">${errorInfo.type}</td>
<td class="table-cell">${errorInfo.statusCodeHtml}</td>
@@ -143,12 +154,15 @@ var LogList = class {
var logList_default = LogList;
// frontend/js/pages/logs/index.js
var dataStore = {
groups: /* @__PURE__ */ new Map(),
keys: /* @__PURE__ */ new Map()
};
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 }
};
@@ -157,38 +171,41 @@ var LogsPage = class {
};
this.initialized = !!this.elements.tableBody;
if (this.initialized) {
this.logList = new logList_default(this.elements.tableBody);
this.logList = new logList_default(this.elements.tableBody, dataStore);
}
}
async init() {
if (!this.initialized) {
console.error("LogsPage: Could not initialize. Essential container element 'logs-table-body' is missing.");
return;
}
if (!this.initialized) return;
this.initEventListeners();
await this.loadGroupsOnce();
await this.loadAndRenderLogs();
}
initEventListeners() {
}
async loadGroupsOnce() {
if (dataStore.groups.size > 0) return;
try {
const { success, data } = await apiFetchJson("/admin/keygroups");
if (success && Array.isArray(data)) {
data.forEach((group) => dataStore.groups.set(group.id, group));
}
} catch (error) {
console.error("Failed to load key groups:", error);
}
}
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)
// 同样不准确
};
const query = new URLSearchParams(this.state.filters);
const { success, data } = await apiFetchJson(`/admin/logs?${query.toString()}`);
if (success && typeof data === "object") {
const { items, total, page, page_size } = data;
this.state.logs = items;
this.state.pagination = { page, page_size, total, pages: Math.ceil(total / page_size) };
await this.enrichLogsWithKeyNames(items);
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) {
@@ -198,6 +215,21 @@ var LogsPage = class {
this.state.isLoading = false;
}
}
async enrichLogsWithKeyNames(logs) {
const missingKeyIds = [...new Set(
logs.filter((log) => log.KeyID && !dataStore.keys.has(log.KeyID)).map((log) => log.KeyID)
)];
if (missingKeyIds.length === 0) return;
try {
const idsQuery = missingKeyIds.join(",");
const { success, data } = await apiFetchJson(`/admin/apikeys?ids=${idsQuery}`);
if (success && Array.isArray(data)) {
data.forEach((key) => dataStore.keys.set(key.ID, key));
}
} catch (error) {
console.error(`Failed to fetch key details:`, error);
}
}
};
function logs_default() {
const page = new LogsPage();

View File

@@ -180,8 +180,8 @@ var pageModules = {
// 键 'dashboard' 对应一个函数,该函数调用 import() 返回一个 Promise
// esbuild 看到这个 import() 语法,就会自动将 dashboard.js 及其依赖打包成一个独立的 chunk 文件
"dashboard": () => import("./dashboard-CJJWKYPR.js"),
"keys": () => import("./keys-A2UAJYOX.js"),
"logs": () => import("./logs-4C4JG7BT.js")
"keys": () => import("./keys-4GCIJ7HW.js"),
"logs": () => import("./logs-AG4TD2DO.js")
// 'settings': () => import('./pages/settings.js'), // 未来启用 settings 页面
// 未来新增的页面只需在这里添加一行映射esbuild会自动处理
};