diff --git a/config.yaml b/config.yaml
index f0c1824..06c1cd2 100644
--- a/config.yaml
+++ b/config.yaml
@@ -12,7 +12,7 @@ server:
# 日志级别
log:
- level: "debug"
+ level: "info"
# 日志轮转配置
max_size: 100 # MB
diff --git a/frontend/js/components/customSelectV2.js b/frontend/js/components/customSelectV2.js
new file mode 100644
index 0000000..38a4bb7
--- /dev/null
+++ b/frontend/js/components/customSelectV2.js
@@ -0,0 +1,129 @@
+// Filename: frontend/js/components/customSelectV2.js
+import { createPopper } from '../vendor/popper.esm.min.js';
+
+export default class CustomSelectV2 {
+ constructor(container) {
+ this.container = container;
+ this.trigger = this.container.querySelector('.custom-select-trigger');
+ this.nativeSelect = this.container.querySelector('select');
+ this.template = this.container.querySelector('.custom-select-panel-template');
+
+ if (!this.trigger || !this.nativeSelect || !this.template) {
+ console.warn('CustomSelectV2 cannot initialize: missing required elements.', this.container);
+ return;
+ }
+
+ this.panel = null;
+ this.popperInstance = null;
+ this.isOpen = false;
+ this.triggerText = this.trigger.querySelector('span');
+
+ if (typeof CustomSelectV2.openInstance === 'undefined') {
+ CustomSelectV2.openInstance = null;
+ CustomSelectV2.initGlobalListener();
+ }
+
+ this.updateTriggerText();
+ this.bindEvents();
+ }
+
+ static initGlobalListener() {
+ document.addEventListener('click', (event) => {
+ const instance = CustomSelectV2.openInstance;
+ if (instance && !instance.container.contains(event.target) && (!instance.panel || !instance.panel.contains(event.target))) {
+ instance.close();
+ }
+ });
+ }
+
+ createPanel() {
+ const panelFragment = this.template.content.cloneNode(true);
+ this.panel = panelFragment.querySelector('.custom-select-panel');
+ document.body.appendChild(this.panel);
+
+ this.panel.innerHTML = '';
+ Array.from(this.nativeSelect.options).forEach(option => {
+ const item = document.createElement('a');
+ item.href = '#';
+ item.className = 'custom-select-option block w-full text-left px-3 py-1.5 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-700';
+ item.textContent = option.textContent;
+ item.dataset.value = option.value;
+ if (option.selected) { item.classList.add('is-selected'); }
+ this.panel.appendChild(item);
+ });
+
+ this.panel.addEventListener('click', (event) => {
+ event.preventDefault();
+ const optionEl = event.target.closest('.custom-select-option');
+ if (optionEl) { this.selectOption(optionEl); }
+ });
+ }
+
+ bindEvents() {
+ this.trigger.addEventListener('click', (event) => {
+ event.stopPropagation();
+ if (CustomSelectV2.openInstance && CustomSelectV2.openInstance !== this) {
+ CustomSelectV2.openInstance.close();
+ }
+ this.toggle();
+ });
+ }
+
+ selectOption(optionEl) {
+ const selectedValue = optionEl.dataset.value;
+ if (this.nativeSelect.value !== selectedValue) {
+ this.nativeSelect.value = selectedValue;
+ this.nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ this.updateTriggerText();
+ this.close();
+ }
+
+ updateTriggerText() {
+ const selectedOption = this.nativeSelect.options[this.nativeSelect.selectedIndex];
+ if (selectedOption) {
+ this.triggerText.textContent = selectedOption.textContent;
+ }
+ }
+
+ toggle() { this.isOpen ? this.close() : this.open(); }
+
+ open() {
+ if (this.isOpen) return;
+ this.isOpen = true;
+
+ if (!this.panel) { this.createPanel(); }
+
+ this.panel.style.display = 'block';
+ this.panel.offsetHeight;
+
+ this.popperInstance = createPopper(this.trigger, this.panel, {
+ placement: 'top-start',
+ modifiers: [
+ { name: 'offset', options: { offset: [0, 8] } },
+ { name: 'flip', options: { fallbackPlacements: ['bottom-start'] } }
+ ],
+ });
+
+ CustomSelectV2.openInstance = this;
+ }
+
+ close() {
+ if (!this.isOpen) return;
+ this.isOpen = false;
+
+ if (this.popperInstance) {
+ this.popperInstance.destroy();
+ this.popperInstance = null;
+ }
+
+ if (this.panel) {
+ this.panel.remove();
+ this.panel = null;
+ }
+
+ if (CustomSelectV2.openInstance === this) {
+ CustomSelectV2.openInstance = null;
+ }
+ }
+}
diff --git a/frontend/js/components/filterPopover.js b/frontend/js/components/filterPopover.js
new file mode 100644
index 0000000..d40da3e
--- /dev/null
+++ b/frontend/js/components/filterPopover.js
@@ -0,0 +1,95 @@
+// Filename: frontend/js/components/filterPopover.js
+
+import { createPopper } from '../vendor/popper.esm.min.js';
+
+export default class FilterPopover {
+ constructor(triggerElement, options, title) {
+ if (!triggerElement || typeof createPopper !== 'function') {
+ console.error('FilterPopover: Trigger element or Popper.js not found.');
+ return;
+ }
+ this.triggerElement = triggerElement;
+ this.options = options;
+ this.title = title;
+ this.selectedValues = new Set();
+
+ this._createPopoverHTML();
+ this.popperInstance = createPopper(this.triggerElement, this.popoverElement, {
+ placement: 'bottom-start',
+ modifiers: [{ name: 'offset', options: { offset: [0, 8] } }],
+ });
+
+ this._bindEvents();
+ }
+
+ _createPopoverHTML() {
+ this.popoverElement = document.createElement('div');
+ this.popoverElement.className = 'hidden z-50 min-w-[12rem] rounded-md border bg-popover bg-white dark:bg-zinc-800 p-2 text-popover-foreground shadow-md';
+ this.popoverElement.innerHTML = `
+
${this.title}
+
+ ${this.options.map(option => `
+
+ `).join('')}
+
+
+
+
+
+ `;
+ document.body.appendChild(this.popoverElement);
+ }
+
+ _bindEvents() {
+ this.triggerElement.addEventListener('click', () => this.toggle());
+
+ document.addEventListener('click', (event) => {
+ if (!this.popoverElement.contains(event.target) && !this.triggerElement.contains(event.target)) {
+ this.hide();
+ }
+ });
+
+ this.popoverElement.addEventListener('click', (event) => {
+ const target = event.target.closest('button');
+ if (!target) return;
+ const action = target.dataset.action;
+ if (action === 'clear') this._handleClear();
+ if (action === 'apply') this._handleApply();
+ });
+ }
+
+ _handleClear() {
+ this.popoverElement.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
+ this.selectedValues.clear();
+ this._handleApply();
+ }
+
+ _handleApply() {
+ this.selectedValues.clear();
+ this.popoverElement.querySelectorAll('input:checked').forEach(cb => {
+ this.selectedValues.add(cb.value);
+ });
+
+ const filterChangeEvent = new CustomEvent('filter-change', {
+ detail: {
+ filterKey: this.triggerElement.id,
+ selected: this.selectedValues
+ }
+ });
+ this.triggerElement.dispatchEvent(filterChangeEvent);
+
+ this.hide();
+ }
+
+ toggle() {
+ this.popoverElement.classList.toggle('hidden');
+ this.popperInstance.update();
+ }
+
+ hide() {
+ this.popoverElement.classList.add('hidden');
+ }
+}
diff --git a/frontend/js/pages/logs/index.js b/frontend/js/pages/logs/index.js
index ea0c10f..41513fd 100644
--- a/frontend/js/pages/logs/index.js
+++ b/frontend/js/pages/logs/index.js
@@ -1,46 +1,235 @@
// Filename: frontend/js/pages/logs/index.js
-
import { apiFetchJson } from '../../services/api.js';
import LogList from './logList.js';
-
-// [最终版] 创建一个共享的数据仓库,用于缓存 Groups 和 Keys
+import CustomSelectV2 from '../../components/customSelectV2.js';
+import { debounce } from '../../utils/utils.js';
+import FilterPopover from '../../components/filterPopover.js';
+import { STATIC_ERROR_MAP, STATUS_CODE_MAP } from './logList.js';
const dataStore = {
- groups: new Map(),
- keys: new Map(),
+ groups: new Map(),
+ keys: new Map(),
};
-
class LogsPage {
constructor() {
this.state = {
logs: [],
pagination: { page: 1, pages: 1, total: 0, page_size: 20 },
isLoading: true,
- filters: { page: 1, page_size: 20 }
+ // [优化] 统一将所有可筛选字段在此处初始化,便于管理
+ filters: {
+ page: 1,
+ page_size: 20,
+ q: '',
+ key_ids: new Set(),
+ group_ids: new Set(),
+ error_types: new Set(),
+ status_codes: new Set(),
+ },
+ selectedLogIds: new Set(),
};
-
this.elements = {
tableBody: document.getElementById('logs-table-body'),
+ selectedCount: document.querySelector('.flex-1.text-sm span.font-semibold:nth-child(1)'),
+ totalCount: document.querySelector('.flex-1.text-sm span:last-child'),
+ pageSizeSelect: document.querySelector('[data-component="custom-select-v2"] select'),
+ pageInfo: document.querySelector('.flex.w-\\[100px\\]'),
+ paginationBtns: document.querySelectorAll('[data-pagination-controls] button'),
+ selectAllCheckbox: document.querySelector('thead .table-head-cell input[type="checkbox"]'),
+ searchInput: document.getElementById('log-search-input'),
+ errorTypeFilterBtn: document.getElementById('filter-error-type-btn'),
+ errorCodeFilterBtn: document.getElementById('filter-error-code-btn'),
};
-
this.initialized = !!this.elements.tableBody;
-
if (this.initialized) {
this.logList = new LogList(this.elements.tableBody, dataStore);
+ const selectContainer = document.querySelector('[data-component="custom-select-v2"]');
+ if (selectContainer) { new CustomSelectV2(selectContainer); }
+ this.debouncedLoadAndRender = debounce(() => this.loadAndRenderLogs(), 300);
}
}
-
async init() {
if (!this.initialized) return;
+ this.initFilterPopovers();
this.initEventListeners();
- // 页面初始化:先加载群组,再加载日志
await this.loadGroupsOnce();
await this.loadAndRenderLogs();
}
+ initFilterPopovers() {
+ const errorTypeOptions = [
+ ...Object.values(STATUS_CODE_MAP).map(v => ({ value: v.type, label: v.type })),
+ ...Object.values(STATIC_ERROR_MAP).map(v => ({ value: v.type, label: v.type }))
+ ];
+ const uniqueErrorTypeOptions = Array.from(new Map(errorTypeOptions.map(item => [item.value, item])).values());
+ if (this.elements.errorTypeFilterBtn) {
+ new FilterPopover(this.elements.errorTypeFilterBtn, uniqueErrorTypeOptions, '筛选错误类型');
+ }
+ const statusCodeOptions = Object.keys(STATUS_CODE_MAP).map(code => ({ value: code, label: code }));
+ if (this.elements.errorCodeFilterBtn) {
+ new FilterPopover(this.elements.errorCodeFilterBtn, statusCodeOptions, '筛选状态码');
+ }
+ }
+ initEventListeners() {
+ if (this.elements.pageSizeSelect) {
+ this.elements.pageSizeSelect.addEventListener('change', (e) => this.changePageSize(parseInt(e.target.value, 10)));
+ }
+ if (this.elements.paginationBtns.length >= 4) {
+ this.elements.paginationBtns[0].addEventListener('click', () => this.goToPage(1));
+ this.elements.paginationBtns[1].addEventListener('click', () => this.goToPage(this.state.pagination.page - 1));
+ this.elements.paginationBtns[2].addEventListener('click', () => this.goToPage(this.state.pagination.page + 1));
+ this.elements.paginationBtns[3].addEventListener('click', () => this.goToPage(this.state.pagination.pages));
+ }
+ if (this.elements.selectAllCheckbox) {
+ this.elements.selectAllCheckbox.addEventListener('change', (event) => this.handleSelectAllChange(event));
+ }
+ if (this.elements.tableBody) {
+ this.elements.tableBody.addEventListener('change', (event) => {
+ if (event.target.type === 'checkbox') this.handleSelectionChange(event.target);
+ });
+ }
+ if (this.elements.searchInput) {
+ this.elements.searchInput.addEventListener('input', (event) => this.handleSearchInput(event));
+ }
+ if (this.elements.errorTypeFilterBtn) {
+ this.elements.errorTypeFilterBtn.addEventListener('filter-change', (e) => this.handleFilterChange(e));
+ }
+ if (this.elements.errorCodeFilterBtn) {
+ this.elements.errorCodeFilterBtn.addEventListener('filter-change', (e) => this.handleFilterChange(e));
+ }
+ }
+ handleFilterChange(event) {
+ const { filterKey, selected } = event.detail;
+ if (filterKey === 'filter-error-type-btn') {
+ this.state.filters.error_types = selected;
+ } else if (filterKey === 'filter-error-code-btn') {
+ this.state.filters.status_codes = selected;
+ }
+ this.state.filters.page = 1;
+ this.loadAndRenderLogs();
+ }
- initEventListeners() { /* 分页和筛选的事件监听器 */ }
+ handleSearchInput(event) {
+ const searchTerm = event.target.value.trim().toLowerCase();
+
+ // 重置分页和与本次搜索相关的筛选条件
+ this.state.filters.page = 1;
+ this.state.filters.q = '';
+ this.state.filters.key_ids = new Set();
+ this.state.filters.group_ids = new Set();
+
+ if (searchTerm === '') {
+ this.debouncedLoadAndRender();
+ return;
+ }
+ const matchedGroupIds = new Set();
+ dataStore.groups.forEach(group => {
+ if (group.display_name.toLowerCase().includes(searchTerm)) {
+ matchedGroupIds.add(group.id);
+ }
+ });
+ const matchedKeyIds = new Set();
+ dataStore.keys.forEach(key => {
+ if (key.APIKey && key.APIKey.toLowerCase().includes(searchTerm)) {
+ matchedKeyIds.add(key.ID);
+ }
+ });
+ if (matchedGroupIds.size > 0) this.state.filters.group_ids = matchedGroupIds;
+ if (matchedKeyIds.size > 0) this.state.filters.key_ids = matchedKeyIds;
+ // 如果没有找到任何匹配的ID,则回退到原始的全局模糊搜索
+ if (matchedGroupIds.size === 0 && matchedKeyIds.size === 0) {
+ this.state.filters.q = searchTerm;
+ }
+ this.debouncedLoadAndRender();
+ }
+
+ handleSelectionChange(checkbox) {
+ const row = checkbox.closest('.table-row');
+ if (!row) return;
+ const logId = parseInt(row.dataset.logId, 10);
+ if (isNaN(logId)) return;
+ if (checkbox.checked) {
+ this.state.selectedLogIds.add(logId);
+ } else {
+ this.state.selectedLogIds.delete(logId);
+ }
+ this.syncSelectionUI();
+ }
+
+ handleSelectAllChange(event) {
+ const isChecked = event.target.checked;
+ this.state.logs.forEach(log => {
+ if (isChecked) {
+ this.state.selectedLogIds.add(log.ID);
+ } else {
+ this.state.selectedLogIds.delete(log.ID);
+ }
+ });
+ this.syncRowCheckboxes();
+ this.syncSelectionUI();
+ }
+
+ syncRowCheckboxes() {
+ const isAllChecked = this.elements.selectAllCheckbox.checked;
+ this.elements.tableBody.querySelectorAll('input[type="checkbox"]').forEach(cb => {
+ cb.checked = isAllChecked;
+ });
+ }
+
+ syncSelectionUI() {
+ if (!this.elements.selectAllCheckbox || !this.elements.selectedCount) return;
+ const selectedCount = this.state.selectedLogIds.size;
+ const visibleLogsCount = this.state.logs.length;
+ // 1. 更新表头“全选”复选框的状态
+ if (selectedCount === 0) {
+ this.elements.selectAllCheckbox.checked = false;
+ this.elements.selectAllCheckbox.indeterminate = false;
+ } else if (selectedCount < visibleLogsCount) {
+ this.elements.selectAllCheckbox.checked = false;
+ this.elements.selectAllCheckbox.indeterminate = true; // 半选状态
+ } else if (selectedCount === visibleLogsCount && visibleLogsCount > 0) {
+ this.elements.selectAllCheckbox.checked = true;
+ this.elements.selectAllCheckbox.indeterminate = false;
+ }
+ // 2. 更新“已选择”计数
+ this.elements.selectedCount.textContent = selectedCount;
+ // 3. (未来扩展) 更新批量操作按钮的状态
+ // const batchButton = document.querySelector('.batch-action-button');
+ // if (batchButton) batchButton.disabled = selectedCount === 0;
+ }
+
+ changePageSize(newSize) {
+ this.state.filters.page_size = newSize;
+ this.state.filters.page = 1;
+ this.loadAndRenderLogs();
+ }
+ goToPage(page) {
+ if (page < 1 || page > this.state.pagination.pages || this.state.isLoading) return;
+ this.state.filters.page = page;
+ this.loadAndRenderLogs();
+ }
+
+ updatePaginationUI() {
+ const { page, pages, total } = this.state.pagination;
+
+ if (this.elements.pageInfo) {
+ this.elements.pageInfo.textContent = `第 ${page} / ${pages} 页`;
+ }
+
+ if (this.elements.totalCount) {
+ this.elements.totalCount.textContent = total;
+ }
+
+ if (this.elements.paginationBtns.length >= 4) {
+ const isFirstPage = page === 1;
+ const isLastPage = page === pages || pages === 0;
+ this.elements.paginationBtns[0].disabled = isFirstPage;
+ this.elements.paginationBtns[1].disabled = isFirstPage;
+ this.elements.paginationBtns[2].disabled = isLastPage;
+ this.elements.paginationBtns[3].disabled = isLastPage;
+ }
+ }
async loadGroupsOnce() {
- if (dataStore.groups.size > 0) return; // 防止重复加载
+ if (dataStore.groups.size > 0) return;
try {
const { success, data } = await apiFetchJson("/admin/keygroups");
if (success && Array.isArray(data)) {
@@ -53,30 +242,69 @@ class LogsPage {
async loadAndRenderLogs() {
this.state.isLoading = true;
- this.logList.renderLoading();
-
+ this.state.selectedLogIds.clear();
+ this.logList.renderLoading();
+ this.updatePaginationUI();
+ this.syncSelectionUI();
try {
- const query = new URLSearchParams(this.state.filters);
- const { success, data } = await apiFetchJson(`/admin/logs?${query.toString()}`);
+ // --- 查询参数准备阶段 ---
+ const finalParams = {};
+ const { filters } = this.state;
- if (success && typeof data === 'object') {
+ // 1. 复制所有非 Set 类型的参数
+ Object.keys(filters).forEach(key => {
+ if (!(filters[key] instanceof Set)) {
+ finalParams[key] = filters[key];
+ }
+ });
+ // 2. 翻译 'error_types'
+ const translatedErrorCodes = new Set();
+ // 从用户直接选择的状态码开始初始化
+ const translatedStatusCodes = new Set(filters.status_codes);
+ if (filters.error_types.size > 0) {
+ filters.error_types.forEach(type => {
+ for (const [code, obj] of Object.entries(STATUS_CODE_MAP)) {
+ if (obj.type === type) translatedStatusCodes.add(code);
+ }
+ for (const [code, obj] of Object.entries(STATIC_ERROR_MAP)) {
+ if (obj.type === type) translatedErrorCodes.add(code);
+ }
+ });
+ }
+ // 3. 统一处理所有 Set 类型的参数,转换为字符串
+ if (filters.key_ids.size > 0) finalParams.key_ids = [...filters.key_ids].join(',');
+ if (filters.group_ids.size > 0) finalParams.group_ids = [...filters.group_ids].join(',');
+ if (translatedErrorCodes.size > 0) finalParams.error_codes = [...translatedErrorCodes].join(',');
+ if (translatedStatusCodes.size > 0) finalParams.status_codes = [...translatedStatusCodes].join(',');
+ // 4. 清理空值
+ Object.keys(finalParams).forEach(key => {
+ if (finalParams[key] === '' || finalParams[key] === null || finalParams[key] === undefined) {
+ delete finalParams[key];
+ }
+ });
+ const query = new URLSearchParams(finalParams);
+ const { success, data } = await apiFetchJson(`/admin/logs?${query.toString()}`);
+ if (success && typeof data === 'object' && data.items) {
const { items, total, page, page_size } = data;
this.state.logs = items;
- this.state.pagination = { page, page_size, total, pages: Math.ceil(total / page_size) };
-
- // [核心] 在渲染前,按需批量加载本页日志所需的、尚未缓存的Key信息
+ const totalPages = Math.ceil(total / page_size);
+ this.state.pagination = { page, page_size, total, pages: totalPages > 0 ? totalPages : 1 };
await this.enrichLogsWithKeyNames(items);
-
- // 调用 render,此时 dataStore 中已包含所有需要的数据
- this.logList.render(this.state.logs, this.state.pagination);
+ this.logList.render(this.state.logs, this.state.pagination, this.state.selectedLogIds);
} else {
+ this.state.logs = [];
+ this.state.pagination = { ...this.state.pagination, total: 0, pages: 1, page: 1 };
this.logList.render([], this.state.pagination);
}
} catch (error) {
console.error("Failed to load logs:", error);
+ this.state.logs = [];
+ this.state.pagination = { ...this.state.pagination, total: 0, pages: 1, page: 1 };
this.logList.render([], this.state.pagination);
} finally {
this.state.isLoading = false;
+ this.updatePaginationUI();
+ this.syncSelectionUI();
}
}
diff --git a/frontend/js/pages/logs/logList.js b/frontend/js/pages/logs/logList.js
index 79655a7..81be013 100644
--- a/frontend/js/pages/logs/logList.js
+++ b/frontend/js/pages/logs/logList.js
@@ -1,7 +1,7 @@
// Filename: frontend/js/pages/logs/logList.js
import { escapeHTML } from '../../utils/utils.js';
-const STATIC_ERROR_MAP = {
+export const STATIC_ERROR_MAP = {
'API_KEY_INVALID': { type: '密钥无效', style: 'red' },
'INVALID_ARGUMENT': { type: '参数无效', style: 'red' },
'PERMISSION_DENIED': { type: '权限不足', style: 'red' },
@@ -13,8 +13,8 @@ const STATIC_ERROR_MAP = {
'INTERNAL': { type: 'Google内部错误', style: 'yellow' },
'UNAVAILABLE': { type: '服务不可用', style: 'yellow' },
};
-// --- [更新] HTTP状态码到类型和样式的动态映射表 ---
-const STATUS_CODE_MAP = {
+// --- HTTP状态码到类型和样式的动态映射表 ---
+export const STATUS_CODE_MAP = {
400: { type: '错误请求', style: 'red' },
401: { type: '认证失败', style: 'red' },
403: { type: '禁止访问', style: 'red' },
@@ -55,7 +55,7 @@ class LogList {
this.container.innerHTML = `| 加载日志中... |
`;
}
- render(logs, pagination) {
+ render(logs, pagination, selectedLogIds) {
if (!this.container) return;
if (!logs || logs.length === 0) {
this.container.innerHTML = `| 没有找到相关的日志记录。 |
`;
@@ -63,7 +63,10 @@ class LogList {
}
const { page, page_size } = pagination;
const startIndex = (page - 1) * page_size;
- const logsHtml = logs.map((log, index) => this.createLogRowHtml(log, startIndex + index + 1)).join('');
+ const logsHtml = logs.map((log, index) => {
+ const isChecked = selectedLogIds.has(log.ID);
+ return this.createLogRowHtml(log, startIndex + index + 1, isChecked);
+ }).join('');
this.container.innerHTML = logsHtml;
}
@@ -125,7 +128,7 @@ class LogList {
return `${modelName}
`;
}
- createLogRowHtml(log, index) {
+ createLogRowHtml(log, index, isChecked) {
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);
@@ -140,9 +143,13 @@ class LogList {
const modelNameFormatted = this._formatModelName(log.ModelName);
const errorMessageAttr = log.ErrorMessage ? `data-error-message="${escape(log.ErrorMessage)}"` : '';
const requestTime = new Date(log.RequestTime).toLocaleString();
+
+ const checkedAttr = isChecked ? 'checked' : '';
return `
- |
+
+
+ |
${index} |
${apiKeyDisplay} |
${groupName} |
@@ -159,5 +166,4 @@ class LogList {
`;
}
}
-
export default LogList;
diff --git a/frontend/js/vendor/popper.esm.min.js b/frontend/js/vendor/popper.esm.min.js
new file mode 100644
index 0000000..019c695
--- /dev/null
+++ b/frontend/js/vendor/popper.esm.min.js
@@ -0,0 +1,6 @@
+/**
+ * @popperjs/core v2.11.8 - MIT License
+ */
+
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Popper={})}(this,(function(e){"use strict";function t(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function n(e){return e instanceof t(e).Element||e instanceof Element}function r(e){return e instanceof t(e).HTMLElement||e instanceof HTMLElement}function o(e){return"undefined"!=typeof ShadowRoot&&(e instanceof t(e).ShadowRoot||e instanceof ShadowRoot)}var i=Math.max,a=Math.min,s=Math.round;function f(){var e=navigator.userAgentData;return null!=e&&e.brands&&Array.isArray(e.brands)?e.brands.map((function(e){return e.brand+"/"+e.version})).join(" "):navigator.userAgent}function c(){return!/^((?!chrome|android).)*safari/i.test(f())}function p(e,o,i){void 0===o&&(o=!1),void 0===i&&(i=!1);var a=e.getBoundingClientRect(),f=1,p=1;o&&r(e)&&(f=e.offsetWidth>0&&s(a.width)/e.offsetWidth||1,p=e.offsetHeight>0&&s(a.height)/e.offsetHeight||1);var u=(n(e)?t(e):window).visualViewport,l=!c()&&i,d=(a.left+(l&&u?u.offsetLeft:0))/f,h=(a.top+(l&&u?u.offsetTop:0))/p,m=a.width/f,v=a.height/p;return{width:m,height:v,top:h,right:d+m,bottom:h+v,left:d,x:d,y:h}}function u(e){var n=t(e);return{scrollLeft:n.pageXOffset,scrollTop:n.pageYOffset}}function l(e){return e?(e.nodeName||"").toLowerCase():null}function d(e){return((n(e)?e.ownerDocument:e.document)||window.document).documentElement}function h(e){return p(d(e)).left+u(e).scrollLeft}function m(e){return t(e).getComputedStyle(e)}function v(e){var t=m(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function y(e,n,o){void 0===o&&(o=!1);var i,a,f=r(n),c=r(n)&&function(e){var t=e.getBoundingClientRect(),n=s(t.width)/e.offsetWidth||1,r=s(t.height)/e.offsetHeight||1;return 1!==n||1!==r}(n),m=d(n),y=p(e,c,o),g={scrollLeft:0,scrollTop:0},b={x:0,y:0};return(f||!f&&!o)&&(("body"!==l(n)||v(m))&&(g=(i=n)!==t(i)&&r(i)?{scrollLeft:(a=i).scrollLeft,scrollTop:a.scrollTop}:u(i)),r(n)?((b=p(n,!0)).x+=n.clientLeft,b.y+=n.clientTop):m&&(b.x=h(m))),{x:y.left+g.scrollLeft-b.x,y:y.top+g.scrollTop-b.y,width:y.width,height:y.height}}function g(e){var t=p(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function b(e){return"html"===l(e)?e:e.assignedSlot||e.parentNode||(o(e)?e.host:null)||d(e)}function x(e){return["html","body","#document"].indexOf(l(e))>=0?e.ownerDocument.body:r(e)&&v(e)?e:x(b(e))}function w(e,n){var r;void 0===n&&(n=[]);var o=x(e),i=o===(null==(r=e.ownerDocument)?void 0:r.body),a=t(o),s=i?[a].concat(a.visualViewport||[],v(o)?o:[]):o,f=n.concat(s);return i?f:f.concat(w(b(s)))}function O(e){return["table","td","th"].indexOf(l(e))>=0}function j(e){return r(e)&&"fixed"!==m(e).position?e.offsetParent:null}function E(e){for(var n=t(e),i=j(e);i&&O(i)&&"static"===m(i).position;)i=j(i);return i&&("html"===l(i)||"body"===l(i)&&"static"===m(i).position)?n:i||function(e){var t=/firefox/i.test(f());if(/Trident/i.test(f())&&r(e)&&"fixed"===m(e).position)return null;var n=b(e);for(o(n)&&(n=n.host);r(n)&&["html","body"].indexOf(l(n))<0;){var i=m(n);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return n;n=n.parentNode}return null}(e)||n}var D="top",A="bottom",L="right",P="left",M="auto",k=[D,A,L,P],W="start",B="end",H="viewport",T="popper",R=k.reduce((function(e,t){return e.concat([t+"-"+W,t+"-"+B])}),[]),S=[].concat(k,[M]).reduce((function(e,t){return e.concat([t,t+"-"+W,t+"-"+B])}),[]),V=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function q(e){var t=new Map,n=new Set,r=[];function o(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!n.has(e)){var r=t.get(e);r&&o(r)}})),r.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||o(e)})),r}function C(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&o(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function N(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function I(e,r,o){return r===H?N(function(e,n){var r=t(e),o=d(e),i=r.visualViewport,a=o.clientWidth,s=o.clientHeight,f=0,p=0;if(i){a=i.width,s=i.height;var u=c();(u||!u&&"fixed"===n)&&(f=i.offsetLeft,p=i.offsetTop)}return{width:a,height:s,x:f+h(e),y:p}}(e,o)):n(r)?function(e,t){var n=p(e,!1,"fixed"===t);return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}(r,o):N(function(e){var t,n=d(e),r=u(e),o=null==(t=e.ownerDocument)?void 0:t.body,a=i(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=i(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),f=-r.scrollLeft+h(e),c=-r.scrollTop;return"rtl"===m(o||n).direction&&(f+=i(n.clientWidth,o?o.clientWidth:0)-a),{width:a,height:s,x:f,y:c}}(d(e)))}function _(e,t,o,s){var f="clippingParents"===t?function(e){var t=w(b(e)),o=["absolute","fixed"].indexOf(m(e).position)>=0&&r(e)?E(e):e;return n(o)?t.filter((function(e){return n(e)&&C(e,o)&&"body"!==l(e)})):[]}(e):[].concat(t),c=[].concat(f,[o]),p=c[0],u=c.reduce((function(t,n){var r=I(e,n,s);return t.top=i(r.top,t.top),t.right=a(r.right,t.right),t.bottom=a(r.bottom,t.bottom),t.left=i(r.left,t.left),t}),I(e,p,s));return u.width=u.right-u.left,u.height=u.bottom-u.top,u.x=u.left,u.y=u.top,u}function F(e){return e.split("-")[0]}function U(e){return e.split("-")[1]}function z(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function X(e){var t,n=e.reference,r=e.element,o=e.placement,i=o?F(o):null,a=o?U(o):null,s=n.x+n.width/2-r.width/2,f=n.y+n.height/2-r.height/2;switch(i){case D:t={x:s,y:n.y-r.height};break;case A:t={x:s,y:n.y+n.height};break;case L:t={x:n.x+n.width,y:f};break;case P:t={x:n.x-r.width,y:f};break;default:t={x:n.x,y:n.y}}var c=i?z(i):null;if(null!=c){var p="y"===c?"height":"width";switch(a){case W:t[c]=t[c]-(n[p]/2-r[p]/2);break;case B:t[c]=t[c]+(n[p]/2-r[p]/2)}}return t}function Y(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function G(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function J(e,t){void 0===t&&(t={});var r=t,o=r.placement,i=void 0===o?e.placement:o,a=r.strategy,s=void 0===a?e.strategy:a,f=r.boundary,c=void 0===f?"clippingParents":f,u=r.rootBoundary,l=void 0===u?H:u,h=r.elementContext,m=void 0===h?T:h,v=r.altBoundary,y=void 0!==v&&v,g=r.padding,b=void 0===g?0:g,x=Y("number"!=typeof b?b:G(b,k)),w=m===T?"reference":T,O=e.rects.popper,j=e.elements[y?w:m],E=_(n(j)?j:j.contextElement||d(e.elements.popper),c,l,s),P=p(e.elements.reference),M=X({reference:P,element:O,strategy:"absolute",placement:i}),W=N(Object.assign({},O,M)),B=m===T?W:P,R={top:E.top-B.top+x.top,bottom:B.bottom-E.bottom+x.bottom,left:E.left-B.left+x.left,right:B.right-E.right+x.right},S=e.modifiersData.offset;if(m===T&&S){var V=S[i];Object.keys(R).forEach((function(e){var t=[L,A].indexOf(e)>=0?1:-1,n=[D,A].indexOf(e)>=0?"y":"x";R[e]+=V[n]*t}))}return R}var K={placement:"bottom",modifiers:[],strategy:"absolute"};function Q(){for(var e=arguments.length,t=new Array(e),n=0;n=0?-1:1,i="function"==typeof n?n(Object.assign({},t,{placement:e})):n,a=i[0],s=i[1];return a=a||0,s=(s||0)*o,[P,L].indexOf(r)>=0?{x:s,y:a}:{x:a,y:s}}(n,t.rects,i),e}),{}),s=a[t.placement],f=s.x,c=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=f,t.modifiersData.popperOffsets.y+=c),t.modifiersData[r]=a}},se={left:"right",right:"left",bottom:"top",top:"bottom"};function fe(e){return e.replace(/left|right|bottom|top/g,(function(e){return se[e]}))}var ce={start:"end",end:"start"};function pe(e){return e.replace(/start|end/g,(function(e){return ce[e]}))}function ue(e,t){void 0===t&&(t={});var n=t,r=n.placement,o=n.boundary,i=n.rootBoundary,a=n.padding,s=n.flipVariations,f=n.allowedAutoPlacements,c=void 0===f?S:f,p=U(r),u=p?s?R:R.filter((function(e){return U(e)===p})):k,l=u.filter((function(e){return c.indexOf(e)>=0}));0===l.length&&(l=u);var d=l.reduce((function(t,n){return t[n]=J(e,{placement:n,boundary:o,rootBoundary:i,padding:a})[F(n)],t}),{});return Object.keys(d).sort((function(e,t){return d[e]-d[t]}))}var le={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var o=n.mainAxis,i=void 0===o||o,a=n.altAxis,s=void 0===a||a,f=n.fallbackPlacements,c=n.padding,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.flipVariations,h=void 0===d||d,m=n.allowedAutoPlacements,v=t.options.placement,y=F(v),g=f||(y===v||!h?[fe(v)]:function(e){if(F(e)===M)return[];var t=fe(e);return[pe(e),t,pe(t)]}(v)),b=[v].concat(g).reduce((function(e,n){return e.concat(F(n)===M?ue(t,{placement:n,boundary:p,rootBoundary:u,padding:c,flipVariations:h,allowedAutoPlacements:m}):n)}),[]),x=t.rects.reference,w=t.rects.popper,O=new Map,j=!0,E=b[0],k=0;k=0,S=R?"width":"height",V=J(t,{placement:B,boundary:p,rootBoundary:u,altBoundary:l,padding:c}),q=R?T?L:P:T?A:D;x[S]>w[S]&&(q=fe(q));var C=fe(q),N=[];if(i&&N.push(V[H]<=0),s&&N.push(V[q]<=0,V[C]<=0),N.every((function(e){return e}))){E=B,j=!1;break}O.set(B,N)}if(j)for(var I=function(e){var t=b.find((function(t){var n=O.get(t);if(n)return n.slice(0,e).every((function(e){return e}))}));if(t)return E=t,"break"},_=h?3:1;_>0;_--){if("break"===I(_))break}t.placement!==E&&(t.modifiersData[r]._skip=!0,t.placement=E,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function de(e,t,n){return i(e,a(t,n))}var he={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name,o=n.mainAxis,s=void 0===o||o,f=n.altAxis,c=void 0!==f&&f,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.padding,h=n.tether,m=void 0===h||h,v=n.tetherOffset,y=void 0===v?0:v,b=J(t,{boundary:p,rootBoundary:u,padding:d,altBoundary:l}),x=F(t.placement),w=U(t.placement),O=!w,j=z(x),M="x"===j?"y":"x",k=t.modifiersData.popperOffsets,B=t.rects.reference,H=t.rects.popper,T="function"==typeof y?y(Object.assign({},t.rects,{placement:t.placement})):y,R="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),S=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,V={x:0,y:0};if(k){if(s){var q,C="y"===j?D:P,N="y"===j?A:L,I="y"===j?"height":"width",_=k[j],X=_+b[C],Y=_-b[N],G=m?-H[I]/2:0,K=w===W?B[I]:H[I],Q=w===W?-H[I]:-B[I],Z=t.elements.arrow,$=m&&Z?g(Z):{width:0,height:0},ee=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},te=ee[C],ne=ee[N],re=de(0,B[I],$[I]),oe=O?B[I]/2-G-re-te-R.mainAxis:K-re-te-R.mainAxis,ie=O?-B[I]/2+G+re+ne+R.mainAxis:Q+re+ne+R.mainAxis,ae=t.elements.arrow&&E(t.elements.arrow),se=ae?"y"===j?ae.clientTop||0:ae.clientLeft||0:0,fe=null!=(q=null==S?void 0:S[j])?q:0,ce=_+ie-fe,pe=de(m?a(X,_+oe-fe-se):X,_,m?i(Y,ce):Y);k[j]=pe,V[j]=pe-_}if(c){var ue,le="x"===j?D:P,he="x"===j?A:L,me=k[M],ve="y"===M?"height":"width",ye=me+b[le],ge=me-b[he],be=-1!==[D,P].indexOf(x),xe=null!=(ue=null==S?void 0:S[M])?ue:0,we=be?ye:me-B[ve]-H[ve]-xe+R.altAxis,Oe=be?me+B[ve]+H[ve]-xe-R.altAxis:ge,je=m&&be?function(e,t,n){var r=de(e,t,n);return r>n?n:r}(we,me,Oe):de(m?we:ye,me,m?Oe:ge);k[M]=je,V[M]=je-me}t.modifiersData[r]=V}},requiresIfExists:["offset"]};var me={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state,r=e.name,o=e.options,i=n.elements.arrow,a=n.modifiersData.popperOffsets,s=F(n.placement),f=z(s),c=[P,L].indexOf(s)>=0?"height":"width";if(i&&a){var p=function(e,t){return Y("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:G(e,k))}(o.padding,n),u=g(i),l="y"===f?D:P,d="y"===f?A:L,h=n.rects.reference[c]+n.rects.reference[f]-a[f]-n.rects.popper[c],m=a[f]-n.rects.reference[f],v=E(i),y=v?"y"===f?v.clientHeight||0:v.clientWidth||0:0,b=h/2-m/2,x=p[l],w=y-u[c]-p[d],O=y/2-u[c]/2+b,j=de(x,O,w),M=f;n.modifiersData[r]=((t={})[M]=j,t.centerOffset=j-O,t)}},effect:function(e){var t=e.state,n=e.options.element,r=void 0===n?"[data-popper-arrow]":n;null!=r&&("string"!=typeof r||(r=t.elements.popper.querySelector(r)))&&C(t.elements.popper,r)&&(t.elements.arrow=r)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ve(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function ye(e){return[D,L,A,P].some((function(t){return e[t]>=0}))}var ge={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state,n=e.name,r=t.rects.reference,o=t.rects.popper,i=t.modifiersData.preventOverflow,a=J(t,{elementContext:"reference"}),s=J(t,{altBoundary:!0}),f=ve(a,r),c=ve(s,o,i),p=ye(f),u=ye(c);t.modifiersData[n]={referenceClippingOffsets:f,popperEscapeOffsets:c,isReferenceHidden:p,hasPopperEscaped:u},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":u})}},be=Z({defaultModifiers:[ee,te,oe,ie]}),xe=[ee,te,oe,ie,ae,le,he,me,ge],we=Z({defaultModifiers:xe});e.applyStyles=ie,e.arrow=me,e.computeStyles=oe,e.createPopper=we,e.createPopperLite=be,e.defaultModifiers=xe,e.detectOverflow=J,e.eventListeners=ee,e.flip=le,e.hide=ge,e.offset=ae,e.popperGenerator=Z,e.popperOffsets=te,e.preventOverflow=he,Object.defineProperty(e,"__esModule",{value:!0})}));
+//# sourceMappingURL=popper.min.js.map
\ No newline at end of file
diff --git a/internal/channel/gemini_channel.go b/internal/channel/gemini_channel.go
index 9819a81..e0e4fc8 100644
--- a/internal/channel/gemini_channel.go
+++ b/internal/channel/gemini_channel.go
@@ -114,7 +114,7 @@ func (ch *GeminiChannel) ValidateKey(
}
errorBody, _ := io.ReadAll(resp.Body)
- parsedMessage := CustomErrors.ParseUpstreamError(errorBody)
+ parsedMessage, _ := CustomErrors.ParseUpstreamError(errorBody)
return &CustomErrors.APIError{
HTTPStatus: resp.StatusCode,
diff --git a/internal/errors/api_error.go b/internal/errors/api_error.go
index fb90f78..3cfba3b 100644
--- a/internal/errors/api_error.go
+++ b/internal/errors/api_error.go
@@ -15,6 +15,7 @@ import (
type APIError struct {
HTTPStatus int
Code string
+ Status string `json:"status,omitempty"`
Message string
}
@@ -61,11 +62,13 @@ func NewAPIError(base *APIError, message string) *APIError {
}
// NewAPIErrorWithUpstream creates a new APIError specifically for wrapping raw upstream errors.
-func NewAPIErrorWithUpstream(statusCode int, code string, upstreamMessage string) *APIError {
+func NewAPIErrorWithUpstream(statusCode int, code string, bodyBytes []byte) *APIError {
+ msg, status := ParseUpstreamError(bodyBytes)
return &APIError{
HTTPStatus: statusCode,
Code: code,
- Message: upstreamMessage,
+ Message: msg,
+ Status: status,
}
}
diff --git a/internal/errors/upstream_parser.go b/internal/errors/upstream_parser.go
index b91bd6f..eb435c3 100644
--- a/internal/errors/upstream_parser.go
+++ b/internal/errors/upstream_parser.go
@@ -2,7 +2,6 @@ package errors
import (
"encoding/json"
- "strings"
)
const (
@@ -10,67 +9,37 @@ const (
maxErrorBodyLength = 2048
)
-// standardErrorResponse matches formats like: {"error": {"message": "..."}}
-type standardErrorResponse struct {
- Error struct {
- Message string `json:"message"`
- } `json:"error"`
-}
-
-// vendorErrorResponse matches formats like: {"error_msg": "..."}
-type vendorErrorResponse struct {
- ErrorMsg string `json:"error_msg"`
-}
-
-// simpleErrorResponse matches formats like: {"error": "..."}
-type simpleErrorResponse struct {
- Error string `json:"error"`
-}
-
-// rootMessageErrorResponse matches formats like: {"message": "..."}
-type rootMessageErrorResponse struct {
+type upstreamErrorDetail struct {
Message string `json:"message"`
+ Status string `json:"status"`
+}
+
+type upstreamErrorPayload struct {
+ Error upstreamErrorDetail `json:"error"`
}
// ParseUpstreamError attempts to parse a structured error message from an upstream response body
-func ParseUpstreamError(body []byte) string {
- // 1. Attempt to parse the standard OpenAI/Gemini format.
- var stdErr standardErrorResponse
- if err := json.Unmarshal(body, &stdErr); err == nil {
- if msg := strings.TrimSpace(stdErr.Error.Message); msg != "" {
- return truncateString(msg, maxErrorBodyLength)
- }
+func ParseUpstreamError(body []byte) (message string, status string) {
+ if len(body) == 0 {
+ return "Upstream returned an empty error body", ""
}
-
- // 2. Attempt to parse vendor-specific format (e.g., Baidu).
- var vendorErr vendorErrorResponse
- if err := json.Unmarshal(body, &vendorErr); err == nil {
- if msg := strings.TrimSpace(vendorErr.ErrorMsg); msg != "" {
- return truncateString(msg, maxErrorBodyLength)
- }
+ // 优先级 1: 尝试解析 OpenAI 兼容接口返回的 `[{"error": {...}}]` 数组格式
+ var arrayPayload []upstreamErrorPayload
+ if err := json.Unmarshal(body, &arrayPayload); err == nil && len(arrayPayload) > 0 {
+ detail := arrayPayload[0].Error
+ return truncateString(detail.Message, maxErrorBodyLength), detail.Status
}
-
- // 3. Attempt to parse simple error format.
- var simpleErr simpleErrorResponse
- if err := json.Unmarshal(body, &simpleErr); err == nil {
- if msg := strings.TrimSpace(simpleErr.Error); msg != "" {
- return truncateString(msg, maxErrorBodyLength)
- }
+ // 优先级 2: 尝试解析 Gemini 原生接口可能返回的 `{"error": {...}}` 单个对象格式
+ var singlePayload upstreamErrorPayload
+ if err := json.Unmarshal(body, &singlePayload); err == nil && singlePayload.Error.Message != "" {
+ detail := singlePayload.Error
+ return truncateString(detail.Message, maxErrorBodyLength), detail.Status
}
-
- // 4. Attempt to parse root-level message format.
- var rootMsgErr rootMessageErrorResponse
- if err := json.Unmarshal(body, &rootMsgErr); err == nil {
- if msg := strings.TrimSpace(rootMsgErr.Message); msg != "" {
- return truncateString(msg, maxErrorBodyLength)
- }
- }
-
- // 5. Graceful Degradation: If all parsing fails, return the raw (but safe) body.
- return truncateString(string(body), maxErrorBodyLength)
+ // 最终回退: 对于无法识别的 JSON 或纯文本错误
+ return truncateString(string(body), maxErrorBodyLength), ""
}
-// truncateString ensures a string does not exceed a maximum length.
+// truncateString remains unchanged.
func truncateString(s string, maxLength int) string {
if len(s) > maxLength {
return s[:maxLength]
diff --git a/internal/handlers/apikey_handler.go b/internal/handlers/apikey_handler.go
index 60b8bf3..1e8b210 100644
--- a/internal/handlers/apikey_handler.go
+++ b/internal/handlers/apikey_handler.go
@@ -1,4 +1,4 @@
-// Filename: internal/handlers/apikey_handler.go (最终决战版)
+// Filename: internal/handlers/apikey_handler.go
package handlers
import (
@@ -88,7 +88,6 @@ func (h *APIKeyHandler) AddMultipleKeysToGroup(c *gin.Context) {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
return
}
- // [修正] 将请求的 context 传递给 service 层
taskStatus, err := h.keyImportService.StartAddKeysTask(c.Request.Context(), req.KeyGroupID, req.Keys, req.ValidateOnImport)
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
@@ -104,7 +103,6 @@ func (h *APIKeyHandler) UnlinkMultipleKeysFromGroup(c *gin.Context) {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
return
}
- // [修正] 将请求的 context 传递给 service 层
taskStatus, err := h.keyImportService.StartUnlinkKeysTask(c.Request.Context(), req.KeyGroupID, req.Keys)
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
@@ -120,7 +118,6 @@ func (h *APIKeyHandler) HardDeleteMultipleKeys(c *gin.Context) {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
return
}
- // [修正] 将请求的 context 传递给 service 层
taskStatus, err := h.keyImportService.StartHardDeleteKeysTask(c.Request.Context(), req.Keys)
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
@@ -136,7 +133,6 @@ func (h *APIKeyHandler) RestoreMultipleKeys(c *gin.Context) {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
return
}
- // [修正] 将请求的 context 传递给 service 层
taskStatus, err := h.keyImportService.StartRestoreKeysTask(c.Request.Context(), req.Keys)
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
@@ -151,7 +147,6 @@ func (h *APIKeyHandler) TestMultipleKeys(c *gin.Context) {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
return
}
- // [修正] 将请求的 context 传递给 service 层
taskStatus, err := h.keyValidationService.StartTestKeysTask(c.Request.Context(), req.KeyGroupID, req.Keys)
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
@@ -246,7 +241,6 @@ func (h *APIKeyHandler) TestKeysForGroup(c *gin.Context) {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
return
}
- // [修正] 将请求的 context 传递给 service 层
taskStatus, err := h.keyValidationService.StartTestKeysTask(c.Request.Context(), uint(groupID), req.Keys)
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrTaskInProgress, err.Error()))
@@ -366,7 +360,6 @@ func (h *APIKeyHandler) HandleBulkAction(c *gin.Context) {
var apiErr *errors.APIError
switch req.Action {
case "revalidate":
- // [修正] 将请求的 context 传递给 service 层
task, err = h.keyValidationService.StartTestKeysByFilterTask(c.Request.Context(), uint(groupID), req.Filter.Status)
case "set_status":
if req.NewStatus == "" {
@@ -376,7 +369,6 @@ func (h *APIKeyHandler) HandleBulkAction(c *gin.Context) {
targetStatus := models.APIKeyStatus(req.NewStatus)
task, err = h.apiKeyService.StartUpdateStatusByFilterTask(c.Request.Context(), uint(groupID), req.Filter.Status, targetStatus)
case "delete":
- // [修正] 将请求的 context 传递给 service 层
task, err = h.keyImportService.StartUnlinkKeysByFilterTask(c.Request.Context(), uint(groupID), req.Filter.Status)
default:
apiErr = errors.NewAPIError(errors.ErrBadRequest, "Unsupported action: "+req.Action)
diff --git a/internal/handlers/proxy_handler.go b/internal/handlers/proxy_handler.go
index 109215f..f5bf442 100644
--- a/internal/handlers/proxy_handler.go
+++ b/internal/handlers/proxy_handler.go
@@ -262,7 +262,7 @@ func (h *ProxyHandler) createModifyResponseFunc(attemptErr **errors.APIError, is
bodyBytes, err := io.ReadAll(reader)
if err != nil {
- *attemptErr = errors.NewAPIError(errors.ErrBadGateway, "Failed to read upstream response")
+ *attemptErr = errors.NewAPIErrorWithUpstream(http.StatusBadGateway, "UPSTREAM_GATEWAY_ERROR", nil)
resp.Body = io.NopCloser(bytes.NewReader([]byte{}))
return nil
}
@@ -271,8 +271,7 @@ func (h *ProxyHandler) createModifyResponseFunc(attemptErr **errors.APIError, is
*isSuccess = true
*pTokens, *cTokens = extractUsage(bodyBytes)
} else {
- parsedMsg := errors.ParseUpstreamError(bodyBytes)
- *attemptErr = errors.NewAPIErrorWithUpstream(resp.StatusCode, fmt.Sprintf("UPSTREAM_%d", resp.StatusCode), parsedMsg)
+ *attemptErr = errors.NewAPIErrorWithUpstream(resp.StatusCode, fmt.Sprintf("UPSTREAM_%d", resp.StatusCode), bodyBytes)
}
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
return nil
@@ -359,11 +358,15 @@ func (h *ProxyHandler) publishFinalLogEvent(c *gin.Context, startTime time.Time,
if !isSuccess {
errToLog := finalErr
if errToLog == nil && rec != nil {
- errToLog = errors.NewAPIErrorWithUpstream(rec.Code, fmt.Sprintf("UPSTREAM_%d", rec.Code), "Request failed after all retries.")
+ errToLog = errors.NewAPIErrorWithUpstream(rec.Code, fmt.Sprintf("UPSTREAM_%d", rec.Code), rec.Body.Bytes())
}
if errToLog != nil {
+ if errToLog.Code == "" && errToLog.HTTPStatus >= 400 {
+ errToLog.Code = fmt.Sprintf("UPSTREAM_%d", errToLog.HTTPStatus)
+ }
event.Error = errToLog
event.RequestLog.ErrorCode, event.RequestLog.ErrorMessage = errToLog.Code, errToLog.Message
+ event.RequestLog.Status = errToLog.Status
}
}
eventData, err := json.Marshal(event)
@@ -385,6 +388,7 @@ func (h *ProxyHandler) publishRetryLogEvent(c *gin.Context, startTime time.Time,
if attemptErr != nil {
retryEvent.Error = attemptErr
retryEvent.RequestLog.ErrorCode, retryEvent.RequestLog.ErrorMessage = attemptErr.Code, attemptErr.Message
+ retryEvent.RequestLog.Status = attemptErr.Status
}
eventData, err := json.Marshal(retryEvent)
if err != nil {
diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go
index 91ea18b..db21c27 100644
--- a/internal/middleware/auth.go
+++ b/internal/middleware/auth.go
@@ -119,4 +119,4 @@ func extractBearerToken(c *gin.Context) string {
}
return strings.TrimSpace(authHeader[len(prefix):])
-}
+}
\ No newline at end of file
diff --git a/internal/models/models.go b/internal/models/models.go
index 9545e5f..ee2fde1 100644
--- a/internal/models/models.go
+++ b/internal/models/models.go
@@ -101,6 +101,7 @@ type RequestLog struct {
LatencyMs int
IsSuccess bool
StatusCode int
+ Status string `gorm:"type:varchar(100);index"`
ModelName string `gorm:"type:varchar(100);index"`
GroupID *uint `gorm:"index"`
KeyID *uint `gorm:"index"`
diff --git a/internal/repository/auth_token.go b/internal/repository/auth_token.go
index 23fd84b..3e7bbe8 100644
--- a/internal/repository/auth_token.go
+++ b/internal/repository/auth_token.go
@@ -17,8 +17,8 @@ import (
type AuthTokenRepository interface {
GetAllTokensWithGroups() ([]*models.AuthToken, error)
BatchUpdateTokens(updates []*models.TokenUpdateRequest) error
- GetTokenByHashedValue(tokenHash string) (*models.AuthToken, error) // <-- Add this line
- SeedAdminToken(encryptedToken, tokenHash string) error // <-- And this line for the seeder
+ GetTokenByHashedValue(tokenHash string) (*models.AuthToken, error)
+ SeedAdminToken(encryptedToken, tokenHash string) error
}
type gormAuthTokenRepository struct {
diff --git a/internal/service/log_service.go b/internal/service/log_service.go
index 8ff5262..9a5a6a4 100644
--- a/internal/service/log_service.go
+++ b/internal/service/log_service.go
@@ -5,6 +5,7 @@ import (
"fmt"
"gemini-balancer/internal/models"
"strconv"
+ "strings"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
@@ -28,13 +29,16 @@ func (s *LogService) Record(ctx context.Context, log *models.RequestLog) error {
// LogQueryParams 解耦 Gin,使用结构体传参
type LogQueryParams struct {
- Page int
- PageSize int
- ModelName string
- IsSuccess *bool // 使用指针区分"未设置"和"false"
- StatusCode *int
- KeyID *uint64
- GroupID *uint64
+ Page int
+ PageSize int
+ ModelName string
+ IsSuccess *bool // 使用指针区分"未设置"和"false"
+ StatusCode *int
+ KeyIDs []string
+ GroupIDs []string
+ Q string
+ ErrorCodes []string
+ StatusCodes []string
}
func (s *LogService) GetLogs(ctx context.Context, params LogQueryParams) ([]models.RequestLog, int64, error) {
@@ -52,17 +56,12 @@ func (s *LogService) GetLogs(ctx context.Context, params LogQueryParams) ([]mode
// 构建基础查询
query := s.db.WithContext(ctx).Model(&models.RequestLog{})
query = s.applyFilters(query, params)
-
- // 计算总数
if err := query.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count logs: %w", err)
}
-
if total == 0 {
return []models.RequestLog{}, 0, nil
}
-
- // 分页查询
offset := (params.Page - 1) * params.PageSize
if err := query.Order("request_time DESC").
Limit(params.PageSize).
@@ -70,7 +69,6 @@ func (s *LogService) GetLogs(ctx context.Context, params LogQueryParams) ([]mode
Find(&logs).Error; err != nil {
return nil, 0, fmt.Errorf("failed to query logs: %w", err)
}
-
return logs, total, nil
}
@@ -84,11 +82,24 @@ func (s *LogService) applyFilters(query *gorm.DB, params LogQueryParams) *gorm.D
if params.StatusCode != nil {
query = query.Where("status_code = ?", *params.StatusCode)
}
- if params.KeyID != nil {
- query = query.Where("key_id = ?", *params.KeyID)
+ if len(params.KeyIDs) > 0 {
+ query = query.Where("key_id IN (?)", params.KeyIDs)
}
- if params.GroupID != nil {
- query = query.Where("group_id = ?", *params.GroupID)
+ if len(params.GroupIDs) > 0 {
+ query = query.Where("group_id IN (?)", params.GroupIDs)
+ }
+ if len(params.ErrorCodes) > 0 {
+ query = query.Where("error_code IN (?)", params.ErrorCodes)
+ }
+ if len(params.StatusCodes) > 0 {
+ query = query.Where("status_code IN (?)", params.StatusCodes)
+ }
+ if params.Q != "" {
+ searchQuery := "%" + params.Q + "%"
+ query = query.Where(
+ "model_name LIKE ? OR error_code LIKE ? OR error_message LIKE ? OR CAST(status_code AS CHAR) LIKE ?",
+ searchQuery, searchQuery, searchQuery, searchQuery,
+ )
}
return query
}
@@ -132,21 +143,22 @@ func ParseLogQueryParams(queryParams map[string]string) (LogQueryParams, error)
}
}
- if keyIDStr, ok := queryParams["key_id"]; ok {
- if keyID, err := strconv.ParseUint(keyIDStr, 10, 64); err == nil {
- params.KeyID = &keyID
- } else {
- return params, fmt.Errorf("invalid key_id parameter: %s", keyIDStr)
- }
+ if keyIDsStr, ok := queryParams["key_ids"]; ok {
+ params.KeyIDs = strings.Split(keyIDsStr, ",")
}
- if groupIDStr, ok := queryParams["group_id"]; ok {
- if groupID, err := strconv.ParseUint(groupIDStr, 10, 64); err == nil {
- params.GroupID = &groupID
- } else {
- return params, fmt.Errorf("invalid group_id parameter: %s", groupIDStr)
- }
+ if groupIDsStr, ok := queryParams["group_ids"]; ok {
+ params.GroupIDs = strings.Split(groupIDsStr, ",")
}
+ if errorCodesStr, ok := queryParams["error_codes"]; ok {
+ params.ErrorCodes = strings.Split(errorCodesStr, ",")
+ }
+ if statusCodesStr, ok := queryParams["status_codes"]; ok {
+ params.StatusCodes = strings.Split(statusCodesStr, ",")
+ }
+ if q, ok := queryParams["q"]; ok {
+ params.Q = q
+ }
return params, nil
}
diff --git a/web/static/css/output.css b/web/static/css/output.css
index 3e7b5db..32d5368 100644
--- a/web/static/css/output.css
+++ b/web/static/css/output.css
@@ -420,6 +420,9 @@
.bottom-6 {
bottom: calc(var(--spacing) * 6);
}
+ .bottom-full {
+ bottom: 100%;
+ }
.left-0 {
left: calc(var(--spacing) * 0);
}
@@ -447,6 +450,9 @@
.z-50 {
z-index: 50;
}
+ .z-90 {
+ z-index: 90;
+ }
.z-\[100\] {
z-index: 100;
}
@@ -495,6 +501,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);
}
@@ -613,6 +622,9 @@
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);
}
@@ -637,6 +649,9 @@
.h-6 {
height: calc(var(--spacing) * 6);
}
+ .h-7 {
+ height: calc(var(--spacing) * 7);
+ }
.h-8 {
height: calc(var(--spacing) * 8);
}
@@ -694,6 +709,9 @@
.w-0 {
width: calc(var(--spacing) * 0);
}
+ .w-1 {
+ width: calc(var(--spacing) * 1);
+ }
.w-1\/4 {
width: calc(1/4 * 100%);
}
@@ -790,6 +808,9 @@
.min-w-0 {
min-width: calc(var(--spacing) * 0);
}
+ .min-w-\[12rem\] {
+ min-width: 12rem;
+ }
.min-w-full {
min-width: 100%;
}
@@ -799,6 +820,9 @@
.flex-1 {
flex: 1;
}
+ .flex-shrink {
+ flex-shrink: 1;
+ }
.shrink-0 {
flex-shrink: 0;
}
@@ -811,6 +835,9 @@
.caption-bottom {
caption-side: bottom;
}
+ .border-collapse {
+ border-collapse: collapse;
+ }
.origin-center {
transform-origin: center;
}
@@ -837,6 +864,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);
@@ -993,6 +1024,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);
}
@@ -1138,6 +1172,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)) {
@@ -1165,6 +1202,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)) {
@@ -1201,6 +1241,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)) {
@@ -1273,6 +1316,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)) {
@@ -1459,6 +1505,9 @@
.bg-zinc-200 {
background-color: var(--color-zinc-200);
}
+ .bg-zinc-400 {
+ background-color: var(--color-zinc-400);
+ }
.bg-zinc-500 {
background-color: var(--color-zinc-500);
}
@@ -1484,6 +1533,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)) {
@@ -1554,6 +1607,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);
}
@@ -1602,6 +1658,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);
}
@@ -1849,6 +1908,9 @@
.text-zinc-100 {
color: var(--color-zinc-100);
}
+ .text-zinc-200 {
+ color: var(--color-zinc-200);
+ }
.text-zinc-400 {
color: var(--color-zinc-400);
}
@@ -1876,6 +1938,9 @@
.italic {
font-style: italic;
}
+ .underline {
+ text-decoration-line: underline;
+ }
.opacity-0 {
opacity: 0%;
}
@@ -1933,6 +1998,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);
}
@@ -1954,6 +2023,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,);
@@ -2672,6 +2745,11 @@
border-color: var(--color-zinc-800);
}
}
+ .dark\:bg-black {
+ &:where(.dark, .dark *) {
+ background-color: var(--color-black);
+ }
+ }
.dark\:bg-blue-900 {
&:where(.dark, .dark *) {
background-color: var(--color-blue-900);
@@ -2788,6 +2866,16 @@
}
}
}
+ .dark\:bg-zinc-500 {
+ &:where(.dark, .dark *) {
+ background-color: var(--color-zinc-500);
+ }
+ }
+ .dark\:bg-zinc-600 {
+ &:where(.dark, .dark *) {
+ background-color: var(--color-zinc-600);
+ }
+ }
.dark\:bg-zinc-700 {
&:where(.dark, .dark *) {
background-color: var(--color-zinc-700);
@@ -5028,6 +5116,11 @@
inherits: false;
initial-value: 0 0 #0000;
}
+@property --tw-outline-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -5140,11 +5233,6 @@
inherits: false;
initial-value: 1;
}
-@property --tw-outline-style {
- syntax: "*";
- inherits: false;
- initial-value: solid;
-}
@keyframes spin {
to {
transform: rotate(360deg);
@@ -5202,6 +5290,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;
@@ -5229,7 +5318,6 @@
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-scale-z: 1;
- --tw-outline-style: solid;
}
}
}
diff --git a/web/static/js/chunk-JSBRDJBE.js b/web/static/js/chunk-JSBRDJBE.js
new file mode 100644
index 0000000..0e67478
--- /dev/null
+++ b/web/static/js/chunk-JSBRDJBE.js
@@ -0,0 +1,30 @@
+var __create = Object.create;
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __getProtoOf = Object.getPrototypeOf;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __commonJS = (cb, mod) => function __require() {
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+};
+var __copyProps = (to, from, except, desc) => {
+ if (from && typeof from === "object" || typeof from === "function") {
+ for (let key of __getOwnPropNames(from))
+ if (!__hasOwnProp.call(to, key) && key !== except)
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+ }
+ return to;
+};
+var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
+ // If the importer is in node compatibility mode or this is not an ESM
+ // file that has been converted to a CommonJS file using a Babel-
+ // compatible transform (i.e. "__esModule" has not been set), then set
+ // "default" to the CommonJS "module.exports" for node compatibility.
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
+ mod
+));
+
+export {
+ __commonJS,
+ __toESM
+};
diff --git a/web/static/js/dashboard-CJJWKYPR.js b/web/static/js/dashboard-XFUWX3IN.js
similarity index 85%
rename from web/static/js/dashboard-CJJWKYPR.js
rename to web/static/js/dashboard-XFUWX3IN.js
index 32d396e..5541858 100644
--- a/web/static/js/dashboard-CJJWKYPR.js
+++ b/web/static/js/dashboard-XFUWX3IN.js
@@ -1,3 +1,5 @@
+import "./chunk-JSBRDJBE.js";
+
// frontend/js/pages/dashboard.js
function init() {
console.log("[Modern Frontend] Dashboard module loaded. Future logic will execute here.");
diff --git a/web/static/js/keys-4GCIJ7HW.js b/web/static/js/keys-HRP4JR7B.js
similarity index 99%
rename from web/static/js/keys-4GCIJ7HW.js
rename to web/static/js/keys-HRP4JR7B.js
index f0a3f9a..53abea1 100644
--- a/web/static/js/keys-4GCIJ7HW.js
+++ b/web/static/js/keys-HRP4JR7B.js
@@ -13,6 +13,7 @@ import {
apiFetch,
apiFetchJson
} from "./chunk-PLQL6WIO.js";
+import "./chunk-JSBRDJBE.js";
// frontend/js/components/tagInput.js
var TagInput = class {
diff --git a/web/static/js/logs-AG4TD2DO.js b/web/static/js/logs-AG4TD2DO.js
deleted file mode 100644
index 7ec2c23..0000000
--- a/web/static/js/logs-AG4TD2DO.js
+++ /dev/null
@@ -1,240 +0,0 @@
-import {
- escapeHTML
-} from "./chunk-A4OOMLXK.js";
-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, dataStore2) {
- this.container = container;
- this.dataStore = dataStore2;
- 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 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} |
-
-
- |
-
- `;
- }
-};
-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 },
- 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, dataStore);
- }
- }
- async init() {
- 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 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 {
- 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;
- }
- }
- 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();
- page.init();
-}
-export {
- logs_default as default
-};
diff --git a/web/static/js/logs-OFCAHOEI.js b/web/static/js/logs-OFCAHOEI.js
new file mode 100644
index 0000000..ad4bf39
--- /dev/null
+++ b/web/static/js/logs-OFCAHOEI.js
@@ -0,0 +1,1145 @@
+import {
+ debounce,
+ escapeHTML
+} from "./chunk-A4OOMLXK.js";
+import {
+ apiFetchJson
+} from "./chunk-PLQL6WIO.js";
+import {
+ __commonJS,
+ __toESM
+} from "./chunk-JSBRDJBE.js";
+
+// frontend/js/vendor/popper.esm.min.js
+var require_popper_esm_min = __commonJS({
+ "frontend/js/vendor/popper.esm.min.js"(exports, module) {
+ !(function(e, t) {
+ "object" == typeof exports && "undefined" != typeof module ? t(exports) : "function" == typeof define && define.amd ? define(["exports"], t) : t((e = "undefined" != typeof globalThis ? globalThis : e || self).Popper = {});
+ })(exports, (function(e) {
+ "use strict";
+ function t(e2) {
+ if (null == e2) return window;
+ if ("[object Window]" !== e2.toString()) {
+ var t2 = e2.ownerDocument;
+ return t2 && t2.defaultView || window;
+ }
+ return e2;
+ }
+ function n(e2) {
+ return e2 instanceof t(e2).Element || e2 instanceof Element;
+ }
+ function r(e2) {
+ return e2 instanceof t(e2).HTMLElement || e2 instanceof HTMLElement;
+ }
+ function o(e2) {
+ return "undefined" != typeof ShadowRoot && (e2 instanceof t(e2).ShadowRoot || e2 instanceof ShadowRoot);
+ }
+ var i = Math.max, a = Math.min, s = Math.round;
+ function f() {
+ var e2 = navigator.userAgentData;
+ return null != e2 && e2.brands && Array.isArray(e2.brands) ? e2.brands.map((function(e3) {
+ return e3.brand + "/" + e3.version;
+ })).join(" ") : navigator.userAgent;
+ }
+ function c() {
+ return !/^((?!chrome|android).)*safari/i.test(f());
+ }
+ function p(e2, o2, i2) {
+ void 0 === o2 && (o2 = false), void 0 === i2 && (i2 = false);
+ var a2 = e2.getBoundingClientRect(), f2 = 1, p2 = 1;
+ o2 && r(e2) && (f2 = e2.offsetWidth > 0 && s(a2.width) / e2.offsetWidth || 1, p2 = e2.offsetHeight > 0 && s(a2.height) / e2.offsetHeight || 1);
+ var u2 = (n(e2) ? t(e2) : window).visualViewport, l2 = !c() && i2, d2 = (a2.left + (l2 && u2 ? u2.offsetLeft : 0)) / f2, h2 = (a2.top + (l2 && u2 ? u2.offsetTop : 0)) / p2, m2 = a2.width / f2, v2 = a2.height / p2;
+ return { width: m2, height: v2, top: h2, right: d2 + m2, bottom: h2 + v2, left: d2, x: d2, y: h2 };
+ }
+ function u(e2) {
+ var n2 = t(e2);
+ return { scrollLeft: n2.pageXOffset, scrollTop: n2.pageYOffset };
+ }
+ function l(e2) {
+ return e2 ? (e2.nodeName || "").toLowerCase() : null;
+ }
+ function d(e2) {
+ return ((n(e2) ? e2.ownerDocument : e2.document) || window.document).documentElement;
+ }
+ function h(e2) {
+ return p(d(e2)).left + u(e2).scrollLeft;
+ }
+ function m(e2) {
+ return t(e2).getComputedStyle(e2);
+ }
+ function v(e2) {
+ var t2 = m(e2), n2 = t2.overflow, r2 = t2.overflowX, o2 = t2.overflowY;
+ return /auto|scroll|overlay|hidden/.test(n2 + o2 + r2);
+ }
+ function y(e2, n2, o2) {
+ void 0 === o2 && (o2 = false);
+ var i2, a2, f2 = r(n2), c2 = r(n2) && (function(e3) {
+ var t2 = e3.getBoundingClientRect(), n3 = s(t2.width) / e3.offsetWidth || 1, r2 = s(t2.height) / e3.offsetHeight || 1;
+ return 1 !== n3 || 1 !== r2;
+ })(n2), m2 = d(n2), y2 = p(e2, c2, o2), g2 = { scrollLeft: 0, scrollTop: 0 }, b2 = { x: 0, y: 0 };
+ return (f2 || !f2 && !o2) && (("body" !== l(n2) || v(m2)) && (g2 = (i2 = n2) !== t(i2) && r(i2) ? { scrollLeft: (a2 = i2).scrollLeft, scrollTop: a2.scrollTop } : u(i2)), r(n2) ? ((b2 = p(n2, true)).x += n2.clientLeft, b2.y += n2.clientTop) : m2 && (b2.x = h(m2))), { x: y2.left + g2.scrollLeft - b2.x, y: y2.top + g2.scrollTop - b2.y, width: y2.width, height: y2.height };
+ }
+ function g(e2) {
+ var t2 = p(e2), n2 = e2.offsetWidth, r2 = e2.offsetHeight;
+ return Math.abs(t2.width - n2) <= 1 && (n2 = t2.width), Math.abs(t2.height - r2) <= 1 && (r2 = t2.height), { x: e2.offsetLeft, y: e2.offsetTop, width: n2, height: r2 };
+ }
+ function b(e2) {
+ return "html" === l(e2) ? e2 : e2.assignedSlot || e2.parentNode || (o(e2) ? e2.host : null) || d(e2);
+ }
+ function x(e2) {
+ return ["html", "body", "#document"].indexOf(l(e2)) >= 0 ? e2.ownerDocument.body : r(e2) && v(e2) ? e2 : x(b(e2));
+ }
+ function w(e2, n2) {
+ var r2;
+ void 0 === n2 && (n2 = []);
+ var o2 = x(e2), i2 = o2 === (null == (r2 = e2.ownerDocument) ? void 0 : r2.body), a2 = t(o2), s2 = i2 ? [a2].concat(a2.visualViewport || [], v(o2) ? o2 : []) : o2, f2 = n2.concat(s2);
+ return i2 ? f2 : f2.concat(w(b(s2)));
+ }
+ function O(e2) {
+ return ["table", "td", "th"].indexOf(l(e2)) >= 0;
+ }
+ function j(e2) {
+ return r(e2) && "fixed" !== m(e2).position ? e2.offsetParent : null;
+ }
+ function E(e2) {
+ for (var n2 = t(e2), i2 = j(e2); i2 && O(i2) && "static" === m(i2).position; ) i2 = j(i2);
+ return i2 && ("html" === l(i2) || "body" === l(i2) && "static" === m(i2).position) ? n2 : i2 || (function(e3) {
+ var t2 = /firefox/i.test(f());
+ if (/Trident/i.test(f()) && r(e3) && "fixed" === m(e3).position) return null;
+ var n3 = b(e3);
+ for (o(n3) && (n3 = n3.host); r(n3) && ["html", "body"].indexOf(l(n3)) < 0; ) {
+ var i3 = m(n3);
+ if ("none" !== i3.transform || "none" !== i3.perspective || "paint" === i3.contain || -1 !== ["transform", "perspective"].indexOf(i3.willChange) || t2 && "filter" === i3.willChange || t2 && i3.filter && "none" !== i3.filter) return n3;
+ n3 = n3.parentNode;
+ }
+ return null;
+ })(e2) || n2;
+ }
+ var D = "top", A = "bottom", L = "right", P = "left", M = "auto", k = [D, A, L, P], W = "start", B = "end", H = "viewport", T = "popper", R = k.reduce((function(e2, t2) {
+ return e2.concat([t2 + "-" + W, t2 + "-" + B]);
+ }), []), S = [].concat(k, [M]).reduce((function(e2, t2) {
+ return e2.concat([t2, t2 + "-" + W, t2 + "-" + B]);
+ }), []), V = ["beforeRead", "read", "afterRead", "beforeMain", "main", "afterMain", "beforeWrite", "write", "afterWrite"];
+ function q(e2) {
+ var t2 = /* @__PURE__ */ new Map(), n2 = /* @__PURE__ */ new Set(), r2 = [];
+ function o2(e3) {
+ n2.add(e3.name), [].concat(e3.requires || [], e3.requiresIfExists || []).forEach((function(e4) {
+ if (!n2.has(e4)) {
+ var r3 = t2.get(e4);
+ r3 && o2(r3);
+ }
+ })), r2.push(e3);
+ }
+ return e2.forEach((function(e3) {
+ t2.set(e3.name, e3);
+ })), e2.forEach((function(e3) {
+ n2.has(e3.name) || o2(e3);
+ })), r2;
+ }
+ function C(e2, t2) {
+ var n2 = t2.getRootNode && t2.getRootNode();
+ if (e2.contains(t2)) return true;
+ if (n2 && o(n2)) {
+ var r2 = t2;
+ do {
+ if (r2 && e2.isSameNode(r2)) return true;
+ r2 = r2.parentNode || r2.host;
+ } while (r2);
+ }
+ return false;
+ }
+ function N(e2) {
+ return Object.assign({}, e2, { left: e2.x, top: e2.y, right: e2.x + e2.width, bottom: e2.y + e2.height });
+ }
+ function I(e2, r2, o2) {
+ return r2 === H ? N((function(e3, n2) {
+ var r3 = t(e3), o3 = d(e3), i2 = r3.visualViewport, a2 = o3.clientWidth, s2 = o3.clientHeight, f2 = 0, p2 = 0;
+ if (i2) {
+ a2 = i2.width, s2 = i2.height;
+ var u2 = c();
+ (u2 || !u2 && "fixed" === n2) && (f2 = i2.offsetLeft, p2 = i2.offsetTop);
+ }
+ return { width: a2, height: s2, x: f2 + h(e3), y: p2 };
+ })(e2, o2)) : n(r2) ? (function(e3, t2) {
+ var n2 = p(e3, false, "fixed" === t2);
+ return n2.top = n2.top + e3.clientTop, n2.left = n2.left + e3.clientLeft, n2.bottom = n2.top + e3.clientHeight, n2.right = n2.left + e3.clientWidth, n2.width = e3.clientWidth, n2.height = e3.clientHeight, n2.x = n2.left, n2.y = n2.top, n2;
+ })(r2, o2) : N((function(e3) {
+ var t2, n2 = d(e3), r3 = u(e3), o3 = null == (t2 = e3.ownerDocument) ? void 0 : t2.body, a2 = i(n2.scrollWidth, n2.clientWidth, o3 ? o3.scrollWidth : 0, o3 ? o3.clientWidth : 0), s2 = i(n2.scrollHeight, n2.clientHeight, o3 ? o3.scrollHeight : 0, o3 ? o3.clientHeight : 0), f2 = -r3.scrollLeft + h(e3), c2 = -r3.scrollTop;
+ return "rtl" === m(o3 || n2).direction && (f2 += i(n2.clientWidth, o3 ? o3.clientWidth : 0) - a2), { width: a2, height: s2, x: f2, y: c2 };
+ })(d(e2)));
+ }
+ function _(e2, t2, o2, s2) {
+ var f2 = "clippingParents" === t2 ? (function(e3) {
+ var t3 = w(b(e3)), o3 = ["absolute", "fixed"].indexOf(m(e3).position) >= 0 && r(e3) ? E(e3) : e3;
+ return n(o3) ? t3.filter((function(e4) {
+ return n(e4) && C(e4, o3) && "body" !== l(e4);
+ })) : [];
+ })(e2) : [].concat(t2), c2 = [].concat(f2, [o2]), p2 = c2[0], u2 = c2.reduce((function(t3, n2) {
+ var r2 = I(e2, n2, s2);
+ return t3.top = i(r2.top, t3.top), t3.right = a(r2.right, t3.right), t3.bottom = a(r2.bottom, t3.bottom), t3.left = i(r2.left, t3.left), t3;
+ }), I(e2, p2, s2));
+ return u2.width = u2.right - u2.left, u2.height = u2.bottom - u2.top, u2.x = u2.left, u2.y = u2.top, u2;
+ }
+ function F(e2) {
+ return e2.split("-")[0];
+ }
+ function U(e2) {
+ return e2.split("-")[1];
+ }
+ function z(e2) {
+ return ["top", "bottom"].indexOf(e2) >= 0 ? "x" : "y";
+ }
+ function X(e2) {
+ var t2, n2 = e2.reference, r2 = e2.element, o2 = e2.placement, i2 = o2 ? F(o2) : null, a2 = o2 ? U(o2) : null, s2 = n2.x + n2.width / 2 - r2.width / 2, f2 = n2.y + n2.height / 2 - r2.height / 2;
+ switch (i2) {
+ case D:
+ t2 = { x: s2, y: n2.y - r2.height };
+ break;
+ case A:
+ t2 = { x: s2, y: n2.y + n2.height };
+ break;
+ case L:
+ t2 = { x: n2.x + n2.width, y: f2 };
+ break;
+ case P:
+ t2 = { x: n2.x - r2.width, y: f2 };
+ break;
+ default:
+ t2 = { x: n2.x, y: n2.y };
+ }
+ var c2 = i2 ? z(i2) : null;
+ if (null != c2) {
+ var p2 = "y" === c2 ? "height" : "width";
+ switch (a2) {
+ case W:
+ t2[c2] = t2[c2] - (n2[p2] / 2 - r2[p2] / 2);
+ break;
+ case B:
+ t2[c2] = t2[c2] + (n2[p2] / 2 - r2[p2] / 2);
+ }
+ }
+ return t2;
+ }
+ function Y(e2) {
+ return Object.assign({}, { top: 0, right: 0, bottom: 0, left: 0 }, e2);
+ }
+ function G(e2, t2) {
+ return t2.reduce((function(t3, n2) {
+ return t3[n2] = e2, t3;
+ }), {});
+ }
+ function J(e2, t2) {
+ void 0 === t2 && (t2 = {});
+ var r2 = t2, o2 = r2.placement, i2 = void 0 === o2 ? e2.placement : o2, a2 = r2.strategy, s2 = void 0 === a2 ? e2.strategy : a2, f2 = r2.boundary, c2 = void 0 === f2 ? "clippingParents" : f2, u2 = r2.rootBoundary, l2 = void 0 === u2 ? H : u2, h2 = r2.elementContext, m2 = void 0 === h2 ? T : h2, v2 = r2.altBoundary, y2 = void 0 !== v2 && v2, g2 = r2.padding, b2 = void 0 === g2 ? 0 : g2, x2 = Y("number" != typeof b2 ? b2 : G(b2, k)), w2 = m2 === T ? "reference" : T, O2 = e2.rects.popper, j2 = e2.elements[y2 ? w2 : m2], E2 = _(n(j2) ? j2 : j2.contextElement || d(e2.elements.popper), c2, l2, s2), P2 = p(e2.elements.reference), M2 = X({ reference: P2, element: O2, strategy: "absolute", placement: i2 }), W2 = N(Object.assign({}, O2, M2)), B2 = m2 === T ? W2 : P2, R2 = { top: E2.top - B2.top + x2.top, bottom: B2.bottom - E2.bottom + x2.bottom, left: E2.left - B2.left + x2.left, right: B2.right - E2.right + x2.right }, S2 = e2.modifiersData.offset;
+ if (m2 === T && S2) {
+ var V2 = S2[i2];
+ Object.keys(R2).forEach((function(e3) {
+ var t3 = [L, A].indexOf(e3) >= 0 ? 1 : -1, n2 = [D, A].indexOf(e3) >= 0 ? "y" : "x";
+ R2[e3] += V2[n2] * t3;
+ }));
+ }
+ return R2;
+ }
+ var K = { placement: "bottom", modifiers: [], strategy: "absolute" };
+ function Q() {
+ for (var e2 = arguments.length, t2 = new Array(e2), n2 = 0; n2 < e2; n2++) t2[n2] = arguments[n2];
+ return !t2.some((function(e3) {
+ return !(e3 && "function" == typeof e3.getBoundingClientRect);
+ }));
+ }
+ function Z(e2) {
+ void 0 === e2 && (e2 = {});
+ var t2 = e2, r2 = t2.defaultModifiers, o2 = void 0 === r2 ? [] : r2, i2 = t2.defaultOptions, a2 = void 0 === i2 ? K : i2;
+ return function(e3, t3, r3) {
+ void 0 === r3 && (r3 = a2);
+ var i3, s2, f2 = { placement: "bottom", orderedModifiers: [], options: Object.assign({}, K, a2), modifiersData: {}, elements: { reference: e3, popper: t3 }, attributes: {}, styles: {} }, c2 = [], p2 = false, u2 = { state: f2, setOptions: function(r4) {
+ var i4 = "function" == typeof r4 ? r4(f2.options) : r4;
+ l2(), f2.options = Object.assign({}, a2, f2.options, i4), f2.scrollParents = { reference: n(e3) ? w(e3) : e3.contextElement ? w(e3.contextElement) : [], popper: w(t3) };
+ var s3, p3, d2 = (function(e4) {
+ var t4 = q(e4);
+ return V.reduce((function(e5, n2) {
+ return e5.concat(t4.filter((function(e6) {
+ return e6.phase === n2;
+ })));
+ }), []);
+ })((s3 = [].concat(o2, f2.options.modifiers), p3 = s3.reduce((function(e4, t4) {
+ var n2 = e4[t4.name];
+ return e4[t4.name] = n2 ? Object.assign({}, n2, t4, { options: Object.assign({}, n2.options, t4.options), data: Object.assign({}, n2.data, t4.data) }) : t4, e4;
+ }), {}), Object.keys(p3).map((function(e4) {
+ return p3[e4];
+ }))));
+ return f2.orderedModifiers = d2.filter((function(e4) {
+ return e4.enabled;
+ })), f2.orderedModifiers.forEach((function(e4) {
+ var t4 = e4.name, n2 = e4.options, r5 = void 0 === n2 ? {} : n2, o3 = e4.effect;
+ if ("function" == typeof o3) {
+ var i5 = o3({ state: f2, name: t4, instance: u2, options: r5 }), a3 = function() {
+ };
+ c2.push(i5 || a3);
+ }
+ })), u2.update();
+ }, forceUpdate: function() {
+ if (!p2) {
+ var e4 = f2.elements, t4 = e4.reference, n2 = e4.popper;
+ if (Q(t4, n2)) {
+ f2.rects = { reference: y(t4, E(n2), "fixed" === f2.options.strategy), popper: g(n2) }, f2.reset = false, f2.placement = f2.options.placement, f2.orderedModifiers.forEach((function(e5) {
+ return f2.modifiersData[e5.name] = Object.assign({}, e5.data);
+ }));
+ for (var r4 = 0; r4 < f2.orderedModifiers.length; r4++) if (true !== f2.reset) {
+ var o3 = f2.orderedModifiers[r4], i4 = o3.fn, a3 = o3.options, s3 = void 0 === a3 ? {} : a3, c3 = o3.name;
+ "function" == typeof i4 && (f2 = i4({ state: f2, options: s3, name: c3, instance: u2 }) || f2);
+ } else f2.reset = false, r4 = -1;
+ }
+ }
+ }, update: (i3 = function() {
+ return new Promise((function(e4) {
+ u2.forceUpdate(), e4(f2);
+ }));
+ }, function() {
+ return s2 || (s2 = new Promise((function(e4) {
+ Promise.resolve().then((function() {
+ s2 = void 0, e4(i3());
+ }));
+ }))), s2;
+ }), destroy: function() {
+ l2(), p2 = true;
+ } };
+ if (!Q(e3, t3)) return u2;
+ function l2() {
+ c2.forEach((function(e4) {
+ return e4();
+ })), c2 = [];
+ }
+ return u2.setOptions(r3).then((function(e4) {
+ !p2 && r3.onFirstUpdate && r3.onFirstUpdate(e4);
+ })), u2;
+ };
+ }
+ var $ = { passive: true };
+ var ee = { name: "eventListeners", enabled: true, phase: "write", fn: function() {
+ }, effect: function(e2) {
+ var n2 = e2.state, r2 = e2.instance, o2 = e2.options, i2 = o2.scroll, a2 = void 0 === i2 || i2, s2 = o2.resize, f2 = void 0 === s2 || s2, c2 = t(n2.elements.popper), p2 = [].concat(n2.scrollParents.reference, n2.scrollParents.popper);
+ return a2 && p2.forEach((function(e3) {
+ e3.addEventListener("scroll", r2.update, $);
+ })), f2 && c2.addEventListener("resize", r2.update, $), function() {
+ a2 && p2.forEach((function(e3) {
+ e3.removeEventListener("scroll", r2.update, $);
+ })), f2 && c2.removeEventListener("resize", r2.update, $);
+ };
+ }, data: {} };
+ var te = { name: "popperOffsets", enabled: true, phase: "read", fn: function(e2) {
+ var t2 = e2.state, n2 = e2.name;
+ t2.modifiersData[n2] = X({ reference: t2.rects.reference, element: t2.rects.popper, strategy: "absolute", placement: t2.placement });
+ }, data: {} }, ne = { top: "auto", right: "auto", bottom: "auto", left: "auto" };
+ function re(e2) {
+ var n2, r2 = e2.popper, o2 = e2.popperRect, i2 = e2.placement, a2 = e2.variation, f2 = e2.offsets, c2 = e2.position, p2 = e2.gpuAcceleration, u2 = e2.adaptive, l2 = e2.roundOffsets, h2 = e2.isFixed, v2 = f2.x, y2 = void 0 === v2 ? 0 : v2, g2 = f2.y, b2 = void 0 === g2 ? 0 : g2, x2 = "function" == typeof l2 ? l2({ x: y2, y: b2 }) : { x: y2, y: b2 };
+ y2 = x2.x, b2 = x2.y;
+ var w2 = f2.hasOwnProperty("x"), O2 = f2.hasOwnProperty("y"), j2 = P, M2 = D, k2 = window;
+ if (u2) {
+ var W2 = E(r2), H2 = "clientHeight", T2 = "clientWidth";
+ if (W2 === t(r2) && "static" !== m(W2 = d(r2)).position && "absolute" === c2 && (H2 = "scrollHeight", T2 = "scrollWidth"), W2 = W2, i2 === D || (i2 === P || i2 === L) && a2 === B) M2 = A, b2 -= (h2 && W2 === k2 && k2.visualViewport ? k2.visualViewport.height : W2[H2]) - o2.height, b2 *= p2 ? 1 : -1;
+ if (i2 === P || (i2 === D || i2 === A) && a2 === B) j2 = L, y2 -= (h2 && W2 === k2 && k2.visualViewport ? k2.visualViewport.width : W2[T2]) - o2.width, y2 *= p2 ? 1 : -1;
+ }
+ var R2, S2 = Object.assign({ position: c2 }, u2 && ne), V2 = true === l2 ? (function(e3, t2) {
+ var n3 = e3.x, r3 = e3.y, o3 = t2.devicePixelRatio || 1;
+ return { x: s(n3 * o3) / o3 || 0, y: s(r3 * o3) / o3 || 0 };
+ })({ x: y2, y: b2 }, t(r2)) : { x: y2, y: b2 };
+ return y2 = V2.x, b2 = V2.y, p2 ? Object.assign({}, S2, ((R2 = {})[M2] = O2 ? "0" : "", R2[j2] = w2 ? "0" : "", R2.transform = (k2.devicePixelRatio || 1) <= 1 ? "translate(" + y2 + "px, " + b2 + "px)" : "translate3d(" + y2 + "px, " + b2 + "px, 0)", R2)) : Object.assign({}, S2, ((n2 = {})[M2] = O2 ? b2 + "px" : "", n2[j2] = w2 ? y2 + "px" : "", n2.transform = "", n2));
+ }
+ var oe = { name: "computeStyles", enabled: true, phase: "beforeWrite", fn: function(e2) {
+ var t2 = e2.state, n2 = e2.options, r2 = n2.gpuAcceleration, o2 = void 0 === r2 || r2, i2 = n2.adaptive, a2 = void 0 === i2 || i2, s2 = n2.roundOffsets, f2 = void 0 === s2 || s2, c2 = { placement: F(t2.placement), variation: U(t2.placement), popper: t2.elements.popper, popperRect: t2.rects.popper, gpuAcceleration: o2, isFixed: "fixed" === t2.options.strategy };
+ null != t2.modifiersData.popperOffsets && (t2.styles.popper = Object.assign({}, t2.styles.popper, re(Object.assign({}, c2, { offsets: t2.modifiersData.popperOffsets, position: t2.options.strategy, adaptive: a2, roundOffsets: f2 })))), null != t2.modifiersData.arrow && (t2.styles.arrow = Object.assign({}, t2.styles.arrow, re(Object.assign({}, c2, { offsets: t2.modifiersData.arrow, position: "absolute", adaptive: false, roundOffsets: f2 })))), t2.attributes.popper = Object.assign({}, t2.attributes.popper, { "data-popper-placement": t2.placement });
+ }, data: {} };
+ var ie = { name: "applyStyles", enabled: true, phase: "write", fn: function(e2) {
+ var t2 = e2.state;
+ Object.keys(t2.elements).forEach((function(e3) {
+ var n2 = t2.styles[e3] || {}, o2 = t2.attributes[e3] || {}, i2 = t2.elements[e3];
+ r(i2) && l(i2) && (Object.assign(i2.style, n2), Object.keys(o2).forEach((function(e4) {
+ var t3 = o2[e4];
+ false === t3 ? i2.removeAttribute(e4) : i2.setAttribute(e4, true === t3 ? "" : t3);
+ })));
+ }));
+ }, effect: function(e2) {
+ var t2 = e2.state, n2 = { popper: { position: t2.options.strategy, left: "0", top: "0", margin: "0" }, arrow: { position: "absolute" }, reference: {} };
+ return Object.assign(t2.elements.popper.style, n2.popper), t2.styles = n2, t2.elements.arrow && Object.assign(t2.elements.arrow.style, n2.arrow), function() {
+ Object.keys(t2.elements).forEach((function(e3) {
+ var o2 = t2.elements[e3], i2 = t2.attributes[e3] || {}, a2 = Object.keys(t2.styles.hasOwnProperty(e3) ? t2.styles[e3] : n2[e3]).reduce((function(e4, t3) {
+ return e4[t3] = "", e4;
+ }), {});
+ r(o2) && l(o2) && (Object.assign(o2.style, a2), Object.keys(i2).forEach((function(e4) {
+ o2.removeAttribute(e4);
+ })));
+ }));
+ };
+ }, requires: ["computeStyles"] };
+ var ae = { name: "offset", enabled: true, phase: "main", requires: ["popperOffsets"], fn: function(e2) {
+ var t2 = e2.state, n2 = e2.options, r2 = e2.name, o2 = n2.offset, i2 = void 0 === o2 ? [0, 0] : o2, a2 = S.reduce((function(e3, n3) {
+ return e3[n3] = (function(e4, t3, n4) {
+ var r3 = F(e4), o3 = [P, D].indexOf(r3) >= 0 ? -1 : 1, i3 = "function" == typeof n4 ? n4(Object.assign({}, t3, { placement: e4 })) : n4, a3 = i3[0], s3 = i3[1];
+ return a3 = a3 || 0, s3 = (s3 || 0) * o3, [P, L].indexOf(r3) >= 0 ? { x: s3, y: a3 } : { x: a3, y: s3 };
+ })(n3, t2.rects, i2), e3;
+ }), {}), s2 = a2[t2.placement], f2 = s2.x, c2 = s2.y;
+ null != t2.modifiersData.popperOffsets && (t2.modifiersData.popperOffsets.x += f2, t2.modifiersData.popperOffsets.y += c2), t2.modifiersData[r2] = a2;
+ } }, se = { left: "right", right: "left", bottom: "top", top: "bottom" };
+ function fe(e2) {
+ return e2.replace(/left|right|bottom|top/g, (function(e3) {
+ return se[e3];
+ }));
+ }
+ var ce = { start: "end", end: "start" };
+ function pe(e2) {
+ return e2.replace(/start|end/g, (function(e3) {
+ return ce[e3];
+ }));
+ }
+ function ue(e2, t2) {
+ void 0 === t2 && (t2 = {});
+ var n2 = t2, r2 = n2.placement, o2 = n2.boundary, i2 = n2.rootBoundary, a2 = n2.padding, s2 = n2.flipVariations, f2 = n2.allowedAutoPlacements, c2 = void 0 === f2 ? S : f2, p2 = U(r2), u2 = p2 ? s2 ? R : R.filter((function(e3) {
+ return U(e3) === p2;
+ })) : k, l2 = u2.filter((function(e3) {
+ return c2.indexOf(e3) >= 0;
+ }));
+ 0 === l2.length && (l2 = u2);
+ var d2 = l2.reduce((function(t3, n3) {
+ return t3[n3] = J(e2, { placement: n3, boundary: o2, rootBoundary: i2, padding: a2 })[F(n3)], t3;
+ }), {});
+ return Object.keys(d2).sort((function(e3, t3) {
+ return d2[e3] - d2[t3];
+ }));
+ }
+ var le = { name: "flip", enabled: true, phase: "main", fn: function(e2) {
+ var t2 = e2.state, n2 = e2.options, r2 = e2.name;
+ if (!t2.modifiersData[r2]._skip) {
+ for (var o2 = n2.mainAxis, i2 = void 0 === o2 || o2, a2 = n2.altAxis, s2 = void 0 === a2 || a2, f2 = n2.fallbackPlacements, c2 = n2.padding, p2 = n2.boundary, u2 = n2.rootBoundary, l2 = n2.altBoundary, d2 = n2.flipVariations, h2 = void 0 === d2 || d2, m2 = n2.allowedAutoPlacements, v2 = t2.options.placement, y2 = F(v2), g2 = f2 || (y2 === v2 || !h2 ? [fe(v2)] : (function(e3) {
+ if (F(e3) === M) return [];
+ var t3 = fe(e3);
+ return [pe(e3), t3, pe(t3)];
+ })(v2)), b2 = [v2].concat(g2).reduce((function(e3, n3) {
+ return e3.concat(F(n3) === M ? ue(t2, { placement: n3, boundary: p2, rootBoundary: u2, padding: c2, flipVariations: h2, allowedAutoPlacements: m2 }) : n3);
+ }), []), x2 = t2.rects.reference, w2 = t2.rects.popper, O2 = /* @__PURE__ */ new Map(), j2 = true, E2 = b2[0], k2 = 0; k2 < b2.length; k2++) {
+ var B2 = b2[k2], H2 = F(B2), T2 = U(B2) === W, R2 = [D, A].indexOf(H2) >= 0, S2 = R2 ? "width" : "height", V2 = J(t2, { placement: B2, boundary: p2, rootBoundary: u2, altBoundary: l2, padding: c2 }), q2 = R2 ? T2 ? L : P : T2 ? A : D;
+ x2[S2] > w2[S2] && (q2 = fe(q2));
+ var C2 = fe(q2), N2 = [];
+ if (i2 && N2.push(V2[H2] <= 0), s2 && N2.push(V2[q2] <= 0, V2[C2] <= 0), N2.every((function(e3) {
+ return e3;
+ }))) {
+ E2 = B2, j2 = false;
+ break;
+ }
+ O2.set(B2, N2);
+ }
+ if (j2) for (var I2 = function(e3) {
+ var t3 = b2.find((function(t4) {
+ var n3 = O2.get(t4);
+ if (n3) return n3.slice(0, e3).every((function(e4) {
+ return e4;
+ }));
+ }));
+ if (t3) return E2 = t3, "break";
+ }, _2 = h2 ? 3 : 1; _2 > 0; _2--) {
+ if ("break" === I2(_2)) break;
+ }
+ t2.placement !== E2 && (t2.modifiersData[r2]._skip = true, t2.placement = E2, t2.reset = true);
+ }
+ }, requiresIfExists: ["offset"], data: { _skip: false } };
+ function de(e2, t2, n2) {
+ return i(e2, a(t2, n2));
+ }
+ var he = { name: "preventOverflow", enabled: true, phase: "main", fn: function(e2) {
+ var t2 = e2.state, n2 = e2.options, r2 = e2.name, o2 = n2.mainAxis, s2 = void 0 === o2 || o2, f2 = n2.altAxis, c2 = void 0 !== f2 && f2, p2 = n2.boundary, u2 = n2.rootBoundary, l2 = n2.altBoundary, d2 = n2.padding, h2 = n2.tether, m2 = void 0 === h2 || h2, v2 = n2.tetherOffset, y2 = void 0 === v2 ? 0 : v2, b2 = J(t2, { boundary: p2, rootBoundary: u2, padding: d2, altBoundary: l2 }), x2 = F(t2.placement), w2 = U(t2.placement), O2 = !w2, j2 = z(x2), M2 = "x" === j2 ? "y" : "x", k2 = t2.modifiersData.popperOffsets, B2 = t2.rects.reference, H2 = t2.rects.popper, T2 = "function" == typeof y2 ? y2(Object.assign({}, t2.rects, { placement: t2.placement })) : y2, R2 = "number" == typeof T2 ? { mainAxis: T2, altAxis: T2 } : Object.assign({ mainAxis: 0, altAxis: 0 }, T2), S2 = t2.modifiersData.offset ? t2.modifiersData.offset[t2.placement] : null, V2 = { x: 0, y: 0 };
+ if (k2) {
+ if (s2) {
+ var q2, C2 = "y" === j2 ? D : P, N2 = "y" === j2 ? A : L, I2 = "y" === j2 ? "height" : "width", _2 = k2[j2], X2 = _2 + b2[C2], Y2 = _2 - b2[N2], G2 = m2 ? -H2[I2] / 2 : 0, K2 = w2 === W ? B2[I2] : H2[I2], Q2 = w2 === W ? -H2[I2] : -B2[I2], Z2 = t2.elements.arrow, $2 = m2 && Z2 ? g(Z2) : { width: 0, height: 0 }, ee2 = t2.modifiersData["arrow#persistent"] ? t2.modifiersData["arrow#persistent"].padding : { top: 0, right: 0, bottom: 0, left: 0 }, te2 = ee2[C2], ne2 = ee2[N2], re2 = de(0, B2[I2], $2[I2]), oe2 = O2 ? B2[I2] / 2 - G2 - re2 - te2 - R2.mainAxis : K2 - re2 - te2 - R2.mainAxis, ie2 = O2 ? -B2[I2] / 2 + G2 + re2 + ne2 + R2.mainAxis : Q2 + re2 + ne2 + R2.mainAxis, ae2 = t2.elements.arrow && E(t2.elements.arrow), se2 = ae2 ? "y" === j2 ? ae2.clientTop || 0 : ae2.clientLeft || 0 : 0, fe2 = null != (q2 = null == S2 ? void 0 : S2[j2]) ? q2 : 0, ce2 = _2 + ie2 - fe2, pe2 = de(m2 ? a(X2, _2 + oe2 - fe2 - se2) : X2, _2, m2 ? i(Y2, ce2) : Y2);
+ k2[j2] = pe2, V2[j2] = pe2 - _2;
+ }
+ if (c2) {
+ var ue2, le2 = "x" === j2 ? D : P, he2 = "x" === j2 ? A : L, me2 = k2[M2], ve2 = "y" === M2 ? "height" : "width", ye2 = me2 + b2[le2], ge2 = me2 - b2[he2], be2 = -1 !== [D, P].indexOf(x2), xe2 = null != (ue2 = null == S2 ? void 0 : S2[M2]) ? ue2 : 0, we2 = be2 ? ye2 : me2 - B2[ve2] - H2[ve2] - xe2 + R2.altAxis, Oe = be2 ? me2 + B2[ve2] + H2[ve2] - xe2 - R2.altAxis : ge2, je = m2 && be2 ? (function(e3, t3, n3) {
+ var r3 = de(e3, t3, n3);
+ return r3 > n3 ? n3 : r3;
+ })(we2, me2, Oe) : de(m2 ? we2 : ye2, me2, m2 ? Oe : ge2);
+ k2[M2] = je, V2[M2] = je - me2;
+ }
+ t2.modifiersData[r2] = V2;
+ }
+ }, requiresIfExists: ["offset"] };
+ var me = { name: "arrow", enabled: true, phase: "main", fn: function(e2) {
+ var t2, n2 = e2.state, r2 = e2.name, o2 = e2.options, i2 = n2.elements.arrow, a2 = n2.modifiersData.popperOffsets, s2 = F(n2.placement), f2 = z(s2), c2 = [P, L].indexOf(s2) >= 0 ? "height" : "width";
+ if (i2 && a2) {
+ var p2 = (function(e3, t3) {
+ return Y("number" != typeof (e3 = "function" == typeof e3 ? e3(Object.assign({}, t3.rects, { placement: t3.placement })) : e3) ? e3 : G(e3, k));
+ })(o2.padding, n2), u2 = g(i2), l2 = "y" === f2 ? D : P, d2 = "y" === f2 ? A : L, h2 = n2.rects.reference[c2] + n2.rects.reference[f2] - a2[f2] - n2.rects.popper[c2], m2 = a2[f2] - n2.rects.reference[f2], v2 = E(i2), y2 = v2 ? "y" === f2 ? v2.clientHeight || 0 : v2.clientWidth || 0 : 0, b2 = h2 / 2 - m2 / 2, x2 = p2[l2], w2 = y2 - u2[c2] - p2[d2], O2 = y2 / 2 - u2[c2] / 2 + b2, j2 = de(x2, O2, w2), M2 = f2;
+ n2.modifiersData[r2] = ((t2 = {})[M2] = j2, t2.centerOffset = j2 - O2, t2);
+ }
+ }, effect: function(e2) {
+ var t2 = e2.state, n2 = e2.options.element, r2 = void 0 === n2 ? "[data-popper-arrow]" : n2;
+ null != r2 && ("string" != typeof r2 || (r2 = t2.elements.popper.querySelector(r2))) && C(t2.elements.popper, r2) && (t2.elements.arrow = r2);
+ }, requires: ["popperOffsets"], requiresIfExists: ["preventOverflow"] };
+ function ve(e2, t2, n2) {
+ return void 0 === n2 && (n2 = { x: 0, y: 0 }), { top: e2.top - t2.height - n2.y, right: e2.right - t2.width + n2.x, bottom: e2.bottom - t2.height + n2.y, left: e2.left - t2.width - n2.x };
+ }
+ function ye(e2) {
+ return [D, L, A, P].some((function(t2) {
+ return e2[t2] >= 0;
+ }));
+ }
+ var ge = { name: "hide", enabled: true, phase: "main", requiresIfExists: ["preventOverflow"], fn: function(e2) {
+ var t2 = e2.state, n2 = e2.name, r2 = t2.rects.reference, o2 = t2.rects.popper, i2 = t2.modifiersData.preventOverflow, a2 = J(t2, { elementContext: "reference" }), s2 = J(t2, { altBoundary: true }), f2 = ve(a2, r2), c2 = ve(s2, o2, i2), p2 = ye(f2), u2 = ye(c2);
+ t2.modifiersData[n2] = { referenceClippingOffsets: f2, popperEscapeOffsets: c2, isReferenceHidden: p2, hasPopperEscaped: u2 }, t2.attributes.popper = Object.assign({}, t2.attributes.popper, { "data-popper-reference-hidden": p2, "data-popper-escaped": u2 });
+ } }, be = Z({ defaultModifiers: [ee, te, oe, ie] }), xe = [ee, te, oe, ie, ae, le, he, me, ge], we = Z({ defaultModifiers: xe });
+ e.applyStyles = ie, e.arrow = me, e.computeStyles = oe, e.createPopper = we, e.createPopperLite = be, e.defaultModifiers = xe, e.detectOverflow = J, e.eventListeners = ee, e.flip = le, e.hide = ge, e.offset = ae, e.popperGenerator = Z, e.popperOffsets = te, e.preventOverflow = he, Object.defineProperty(e, "__esModule", { value: true });
+ }));
+ }
+});
+
+// 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, dataStore2) {
+ this.container = container;
+ this.dataStore = dataStore2;
+ 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, selectedLogIds) {
+ 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) => {
+ const isChecked = selectedLogIds.has(log.ID);
+ return this.createLogRowHtml(log, startIndex + index + 1, isChecked);
+ }).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, isChecked) {
+ 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();
+ const checkedAttr = isChecked ? "checked" : "";
+ return `
+
+ |
+
+ |
+ ${index} |
+ ${apiKeyDisplay} |
+ ${groupName} |
+ ${errorInfo.type} |
+ ${errorInfo.statusCodeHtml} |
+ ${modelNameFormatted} |
+ ${requestTime} |
+
+
+ |
+
+ `;
+ }
+};
+var logList_default = LogList;
+
+// frontend/js/components/customSelectV2.js
+var import_popper_esm_min = __toESM(require_popper_esm_min());
+var CustomSelectV2 = class _CustomSelectV2 {
+ constructor(container) {
+ this.container = container;
+ this.trigger = this.container.querySelector(".custom-select-trigger");
+ this.nativeSelect = this.container.querySelector("select");
+ this.template = this.container.querySelector(".custom-select-panel-template");
+ if (!this.trigger || !this.nativeSelect || !this.template) {
+ console.warn("CustomSelectV2 cannot initialize: missing required elements.", this.container);
+ return;
+ }
+ this.panel = null;
+ this.popperInstance = null;
+ this.isOpen = false;
+ this.triggerText = this.trigger.querySelector("span");
+ if (typeof _CustomSelectV2.openInstance === "undefined") {
+ _CustomSelectV2.openInstance = null;
+ _CustomSelectV2.initGlobalListener();
+ }
+ this.updateTriggerText();
+ this.bindEvents();
+ }
+ static initGlobalListener() {
+ document.addEventListener("click", (event) => {
+ const instance = _CustomSelectV2.openInstance;
+ if (instance && !instance.container.contains(event.target) && (!instance.panel || !instance.panel.contains(event.target))) {
+ instance.close();
+ }
+ });
+ }
+ createPanel() {
+ const panelFragment = this.template.content.cloneNode(true);
+ this.panel = panelFragment.querySelector(".custom-select-panel");
+ document.body.appendChild(this.panel);
+ this.panel.innerHTML = "";
+ Array.from(this.nativeSelect.options).forEach((option) => {
+ const item = document.createElement("a");
+ item.href = "#";
+ item.className = "custom-select-option block w-full text-left px-3 py-1.5 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-700";
+ item.textContent = option.textContent;
+ item.dataset.value = option.value;
+ if (option.selected) {
+ item.classList.add("is-selected");
+ }
+ this.panel.appendChild(item);
+ });
+ this.panel.addEventListener("click", (event) => {
+ event.preventDefault();
+ const optionEl = event.target.closest(".custom-select-option");
+ if (optionEl) {
+ this.selectOption(optionEl);
+ }
+ });
+ }
+ bindEvents() {
+ this.trigger.addEventListener("click", (event) => {
+ event.stopPropagation();
+ if (_CustomSelectV2.openInstance && _CustomSelectV2.openInstance !== this) {
+ _CustomSelectV2.openInstance.close();
+ }
+ this.toggle();
+ });
+ }
+ selectOption(optionEl) {
+ const selectedValue = optionEl.dataset.value;
+ if (this.nativeSelect.value !== selectedValue) {
+ this.nativeSelect.value = selectedValue;
+ this.nativeSelect.dispatchEvent(new Event("change", { bubbles: true }));
+ }
+ this.updateTriggerText();
+ this.close();
+ }
+ updateTriggerText() {
+ const selectedOption = this.nativeSelect.options[this.nativeSelect.selectedIndex];
+ if (selectedOption) {
+ this.triggerText.textContent = selectedOption.textContent;
+ }
+ }
+ toggle() {
+ this.isOpen ? this.close() : this.open();
+ }
+ open() {
+ if (this.isOpen) return;
+ this.isOpen = true;
+ if (!this.panel) {
+ this.createPanel();
+ }
+ this.panel.style.display = "block";
+ this.panel.offsetHeight;
+ this.popperInstance = (0, import_popper_esm_min.createPopper)(this.trigger, this.panel, {
+ placement: "top-start",
+ modifiers: [
+ { name: "offset", options: { offset: [0, 8] } },
+ { name: "flip", options: { fallbackPlacements: ["bottom-start"] } }
+ ]
+ });
+ _CustomSelectV2.openInstance = this;
+ }
+ close() {
+ if (!this.isOpen) return;
+ this.isOpen = false;
+ if (this.popperInstance) {
+ this.popperInstance.destroy();
+ this.popperInstance = null;
+ }
+ if (this.panel) {
+ this.panel.remove();
+ this.panel = null;
+ }
+ if (_CustomSelectV2.openInstance === this) {
+ _CustomSelectV2.openInstance = null;
+ }
+ }
+};
+
+// frontend/js/components/filterPopover.js
+var import_popper_esm_min2 = __toESM(require_popper_esm_min());
+var FilterPopover = class {
+ constructor(triggerElement, options, title) {
+ if (!triggerElement || typeof import_popper_esm_min2.createPopper !== "function") {
+ console.error("FilterPopover: Trigger element or Popper.js not found.");
+ return;
+ }
+ this.triggerElement = triggerElement;
+ this.options = options;
+ this.title = title;
+ this.selectedValues = /* @__PURE__ */ new Set();
+ this._createPopoverHTML();
+ this.popperInstance = (0, import_popper_esm_min2.createPopper)(this.triggerElement, this.popoverElement, {
+ placement: "bottom-start",
+ modifiers: [{ name: "offset", options: { offset: [0, 8] } }]
+ });
+ this._bindEvents();
+ }
+ _createPopoverHTML() {
+ this.popoverElement = document.createElement("div");
+ this.popoverElement.className = "hidden z-50 min-w-[12rem] rounded-md border bg-popover bg-white dark:bg-zinc-800 p-2 text-popover-foreground shadow-md";
+ this.popoverElement.innerHTML = `
+ ${this.title}
+
+ ${this.options.map((option) => `
+
+ `).join("")}
+
+
+
+
+
+ `;
+ document.body.appendChild(this.popoverElement);
+ }
+ _bindEvents() {
+ this.triggerElement.addEventListener("click", () => this.toggle());
+ document.addEventListener("click", (event) => {
+ if (!this.popoverElement.contains(event.target) && !this.triggerElement.contains(event.target)) {
+ this.hide();
+ }
+ });
+ this.popoverElement.addEventListener("click", (event) => {
+ const target = event.target.closest("button");
+ if (!target) return;
+ const action = target.dataset.action;
+ if (action === "clear") this._handleClear();
+ if (action === "apply") this._handleApply();
+ });
+ }
+ _handleClear() {
+ this.popoverElement.querySelectorAll('input[type="checkbox"]').forEach((cb) => cb.checked = false);
+ this.selectedValues.clear();
+ this._handleApply();
+ }
+ _handleApply() {
+ this.selectedValues.clear();
+ this.popoverElement.querySelectorAll("input:checked").forEach((cb) => {
+ this.selectedValues.add(cb.value);
+ });
+ const filterChangeEvent = new CustomEvent("filter-change", {
+ detail: {
+ filterKey: this.triggerElement.id,
+ selected: this.selectedValues
+ }
+ });
+ this.triggerElement.dispatchEvent(filterChangeEvent);
+ this.hide();
+ }
+ toggle() {
+ this.popoverElement.classList.toggle("hidden");
+ this.popperInstance.update();
+ }
+ hide() {
+ this.popoverElement.classList.add("hidden");
+ }
+};
+
+// 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 },
+ isLoading: true,
+ // [优化] 统一将所有可筛选字段在此处初始化,便于管理
+ filters: {
+ page: 1,
+ page_size: 20,
+ q: "",
+ key_ids: /* @__PURE__ */ new Set(),
+ group_ids: /* @__PURE__ */ new Set(),
+ error_types: /* @__PURE__ */ new Set(),
+ status_codes: /* @__PURE__ */ new Set()
+ },
+ selectedLogIds: /* @__PURE__ */ new Set()
+ };
+ this.elements = {
+ tableBody: document.getElementById("logs-table-body"),
+ selectedCount: document.querySelector(".flex-1.text-sm span.font-semibold:nth-child(1)"),
+ totalCount: document.querySelector(".flex-1.text-sm span:last-child"),
+ pageSizeSelect: document.querySelector('[data-component="custom-select-v2"] select'),
+ pageInfo: document.querySelector(".flex.w-\\[100px\\]"),
+ paginationBtns: document.querySelectorAll("[data-pagination-controls] button"),
+ selectAllCheckbox: document.querySelector('thead .table-head-cell input[type="checkbox"]'),
+ searchInput: document.getElementById("log-search-input"),
+ errorTypeFilterBtn: document.getElementById("filter-error-type-btn"),
+ errorCodeFilterBtn: document.getElementById("filter-error-code-btn")
+ };
+ this.initialized = !!this.elements.tableBody;
+ if (this.initialized) {
+ this.logList = new logList_default(this.elements.tableBody, dataStore);
+ const selectContainer = document.querySelector('[data-component="custom-select-v2"]');
+ if (selectContainer) {
+ new CustomSelectV2(selectContainer);
+ }
+ this.debouncedLoadAndRender = debounce(() => this.loadAndRenderLogs(), 300);
+ }
+ }
+ async init() {
+ if (!this.initialized) return;
+ this.initFilterPopovers();
+ this.initEventListeners();
+ await this.loadGroupsOnce();
+ await this.loadAndRenderLogs();
+ }
+ initFilterPopovers() {
+ const errorTypeOptions = [
+ ...Object.values(STATUS_CODE_MAP).map((v) => ({ value: v.type, label: v.type })),
+ ...Object.values(STATIC_ERROR_MAP).map((v) => ({ value: v.type, label: v.type }))
+ ];
+ const uniqueErrorTypeOptions = Array.from(new Map(errorTypeOptions.map((item) => [item.value, item])).values());
+ if (this.elements.errorTypeFilterBtn) {
+ new FilterPopover(this.elements.errorTypeFilterBtn, uniqueErrorTypeOptions, "\u7B5B\u9009\u9519\u8BEF\u7C7B\u578B");
+ }
+ const statusCodeOptions = Object.keys(STATUS_CODE_MAP).map((code) => ({ value: code, label: code }));
+ if (this.elements.errorCodeFilterBtn) {
+ new FilterPopover(this.elements.errorCodeFilterBtn, statusCodeOptions, "\u7B5B\u9009\u72B6\u6001\u7801");
+ }
+ }
+ initEventListeners() {
+ if (this.elements.pageSizeSelect) {
+ this.elements.pageSizeSelect.addEventListener("change", (e) => this.changePageSize(parseInt(e.target.value, 10)));
+ }
+ if (this.elements.paginationBtns.length >= 4) {
+ this.elements.paginationBtns[0].addEventListener("click", () => this.goToPage(1));
+ this.elements.paginationBtns[1].addEventListener("click", () => this.goToPage(this.state.pagination.page - 1));
+ this.elements.paginationBtns[2].addEventListener("click", () => this.goToPage(this.state.pagination.page + 1));
+ this.elements.paginationBtns[3].addEventListener("click", () => this.goToPage(this.state.pagination.pages));
+ }
+ if (this.elements.selectAllCheckbox) {
+ this.elements.selectAllCheckbox.addEventListener("change", (event) => this.handleSelectAllChange(event));
+ }
+ if (this.elements.tableBody) {
+ this.elements.tableBody.addEventListener("change", (event) => {
+ if (event.target.type === "checkbox") this.handleSelectionChange(event.target);
+ });
+ }
+ if (this.elements.searchInput) {
+ this.elements.searchInput.addEventListener("input", (event) => this.handleSearchInput(event));
+ }
+ if (this.elements.errorTypeFilterBtn) {
+ this.elements.errorTypeFilterBtn.addEventListener("filter-change", (e) => this.handleFilterChange(e));
+ }
+ if (this.elements.errorCodeFilterBtn) {
+ this.elements.errorCodeFilterBtn.addEventListener("filter-change", (e) => this.handleFilterChange(e));
+ }
+ }
+ handleFilterChange(event) {
+ const { filterKey, selected } = event.detail;
+ if (filterKey === "filter-error-type-btn") {
+ this.state.filters.error_types = selected;
+ } else if (filterKey === "filter-error-code-btn") {
+ this.state.filters.status_codes = selected;
+ }
+ this.state.filters.page = 1;
+ this.loadAndRenderLogs();
+ }
+ handleSearchInput(event) {
+ const searchTerm = event.target.value.trim().toLowerCase();
+ this.state.filters.page = 1;
+ this.state.filters.q = "";
+ this.state.filters.key_ids = /* @__PURE__ */ new Set();
+ this.state.filters.group_ids = /* @__PURE__ */ new Set();
+ if (searchTerm === "") {
+ this.debouncedLoadAndRender();
+ return;
+ }
+ const matchedGroupIds = /* @__PURE__ */ new Set();
+ dataStore.groups.forEach((group) => {
+ if (group.display_name.toLowerCase().includes(searchTerm)) {
+ matchedGroupIds.add(group.id);
+ }
+ });
+ const matchedKeyIds = /* @__PURE__ */ new Set();
+ dataStore.keys.forEach((key) => {
+ if (key.APIKey && key.APIKey.toLowerCase().includes(searchTerm)) {
+ matchedKeyIds.add(key.ID);
+ }
+ });
+ if (matchedGroupIds.size > 0) this.state.filters.group_ids = matchedGroupIds;
+ if (matchedKeyIds.size > 0) this.state.filters.key_ids = matchedKeyIds;
+ if (matchedGroupIds.size === 0 && matchedKeyIds.size === 0) {
+ this.state.filters.q = searchTerm;
+ }
+ this.debouncedLoadAndRender();
+ }
+ handleSelectionChange(checkbox) {
+ const row = checkbox.closest(".table-row");
+ if (!row) return;
+ const logId = parseInt(row.dataset.logId, 10);
+ if (isNaN(logId)) return;
+ if (checkbox.checked) {
+ this.state.selectedLogIds.add(logId);
+ } else {
+ this.state.selectedLogIds.delete(logId);
+ }
+ this.syncSelectionUI();
+ }
+ handleSelectAllChange(event) {
+ const isChecked = event.target.checked;
+ this.state.logs.forEach((log) => {
+ if (isChecked) {
+ this.state.selectedLogIds.add(log.ID);
+ } else {
+ this.state.selectedLogIds.delete(log.ID);
+ }
+ });
+ this.syncRowCheckboxes();
+ this.syncSelectionUI();
+ }
+ syncRowCheckboxes() {
+ const isAllChecked = this.elements.selectAllCheckbox.checked;
+ this.elements.tableBody.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
+ cb.checked = isAllChecked;
+ });
+ }
+ syncSelectionUI() {
+ if (!this.elements.selectAllCheckbox || !this.elements.selectedCount) return;
+ const selectedCount = this.state.selectedLogIds.size;
+ const visibleLogsCount = this.state.logs.length;
+ if (selectedCount === 0) {
+ this.elements.selectAllCheckbox.checked = false;
+ this.elements.selectAllCheckbox.indeterminate = false;
+ } else if (selectedCount < visibleLogsCount) {
+ this.elements.selectAllCheckbox.checked = false;
+ this.elements.selectAllCheckbox.indeterminate = true;
+ } else if (selectedCount === visibleLogsCount && visibleLogsCount > 0) {
+ this.elements.selectAllCheckbox.checked = true;
+ this.elements.selectAllCheckbox.indeterminate = false;
+ }
+ this.elements.selectedCount.textContent = selectedCount;
+ }
+ changePageSize(newSize) {
+ this.state.filters.page_size = newSize;
+ this.state.filters.page = 1;
+ this.loadAndRenderLogs();
+ }
+ goToPage(page) {
+ if (page < 1 || page > this.state.pagination.pages || this.state.isLoading) return;
+ this.state.filters.page = page;
+ this.loadAndRenderLogs();
+ }
+ updatePaginationUI() {
+ const { page, pages, total } = this.state.pagination;
+ if (this.elements.pageInfo) {
+ this.elements.pageInfo.textContent = `\u7B2C ${page} / ${pages} \u9875`;
+ }
+ if (this.elements.totalCount) {
+ this.elements.totalCount.textContent = total;
+ }
+ if (this.elements.paginationBtns.length >= 4) {
+ const isFirstPage = page === 1;
+ const isLastPage = page === pages || pages === 0;
+ this.elements.paginationBtns[0].disabled = isFirstPage;
+ this.elements.paginationBtns[1].disabled = isFirstPage;
+ this.elements.paginationBtns[2].disabled = isLastPage;
+ this.elements.paginationBtns[3].disabled = isLastPage;
+ }
+ }
+ 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.state.selectedLogIds.clear();
+ this.logList.renderLoading();
+ this.updatePaginationUI();
+ this.syncSelectionUI();
+ try {
+ const finalParams = {};
+ const { filters } = this.state;
+ Object.keys(filters).forEach((key) => {
+ if (!(filters[key] instanceof Set)) {
+ finalParams[key] = filters[key];
+ }
+ });
+ const translatedErrorCodes = /* @__PURE__ */ new Set();
+ const translatedStatusCodes = new Set(filters.status_codes);
+ if (filters.error_types.size > 0) {
+ filters.error_types.forEach((type) => {
+ for (const [code, obj] of Object.entries(STATUS_CODE_MAP)) {
+ if (obj.type === type) translatedStatusCodes.add(code);
+ }
+ for (const [code, obj] of Object.entries(STATIC_ERROR_MAP)) {
+ if (obj.type === type) translatedErrorCodes.add(code);
+ }
+ });
+ }
+ if (filters.key_ids.size > 0) finalParams.key_ids = [...filters.key_ids].join(",");
+ if (filters.group_ids.size > 0) finalParams.group_ids = [...filters.group_ids].join(",");
+ if (translatedErrorCodes.size > 0) finalParams.error_codes = [...translatedErrorCodes].join(",");
+ if (translatedStatusCodes.size > 0) finalParams.status_codes = [...translatedStatusCodes].join(",");
+ Object.keys(finalParams).forEach((key) => {
+ if (finalParams[key] === "" || finalParams[key] === null || finalParams[key] === void 0) {
+ delete finalParams[key];
+ }
+ });
+ const query = new URLSearchParams(finalParams);
+ const { success, data } = await apiFetchJson(`/admin/logs?${query.toString()}`);
+ if (success && typeof data === "object" && data.items) {
+ const { items, total, page, page_size } = data;
+ this.state.logs = items;
+ const totalPages = Math.ceil(total / page_size);
+ this.state.pagination = { page, page_size, total, pages: totalPages > 0 ? totalPages : 1 };
+ await this.enrichLogsWithKeyNames(items);
+ this.logList.render(this.state.logs, this.state.pagination, this.state.selectedLogIds);
+ } else {
+ this.state.logs = [];
+ this.state.pagination = { ...this.state.pagination, total: 0, pages: 1, page: 1 };
+ this.logList.render([], this.state.pagination);
+ }
+ } catch (error) {
+ console.error("Failed to load logs:", error);
+ this.state.logs = [];
+ this.state.pagination = { ...this.state.pagination, total: 0, pages: 1, page: 1 };
+ this.logList.render([], this.state.pagination);
+ } finally {
+ this.state.isLoading = false;
+ this.updatePaginationUI();
+ this.syncSelectionUI();
+ }
+ }
+ 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();
+ page.init();
+}
+export {
+ logs_default as default
+};
diff --git a/web/static/js/main.js b/web/static/js/main.js
index d010638..8269c35 100644
--- a/web/static/js/main.js
+++ b/web/static/js/main.js
@@ -9,6 +9,7 @@ import {
apiFetch,
apiFetchJson
} from "./chunk-PLQL6WIO.js";
+import "./chunk-JSBRDJBE.js";
// frontend/js/components/slidingTabs.js
var SlidingTabs = class {
@@ -179,9 +180,9 @@ var base_default = initLayout;
var pageModules = {
// 键 'dashboard' 对应一个函数,该函数调用 import() 返回一个 Promise
// esbuild 看到这个 import() 语法,就会自动将 dashboard.js 及其依赖打包成一个独立的 chunk 文件
- "dashboard": () => import("./dashboard-CJJWKYPR.js"),
- "keys": () => import("./keys-4GCIJ7HW.js"),
- "logs": () => import("./logs-AG4TD2DO.js")
+ "dashboard": () => import("./dashboard-XFUWX3IN.js"),
+ "keys": () => import("./keys-HRP4JR7B.js"),
+ "logs": () => import("./logs-OFCAHOEI.js")
// 'settings': () => import('./pages/settings.js'), // 未来启用 settings 页面
// 未来新增的页面,只需在这里添加一行映射,esbuild会自动处理
};
diff --git a/web/templates/logs.html b/web/templates/logs.html
index ce517e5..d735e83 100644
--- a/web/templates/logs.html
+++ b/web/templates/logs.html
@@ -41,16 +41,16 @@
-
+
- 已选择 0 / 100
+ 已选择 0 / 0
-
每页行数
-
+
每页行数
+
+
+
+
+
+
+
+
+
+
+
+
- 第 1 / 10 页
+ 第 1 / 1 页
-