(() => {
  // frontend/js/components/slidingTabs.js
  var SlidingTabs = class {
    /**
     * @param {HTMLElement} containerElement - The main container element with the `data-sliding-tabs-container` attribute.
     */
    constructor(containerElement) {
      this.container = containerElement;
      this.indicator = this.container.querySelector("[data-tab-indicator]");
      this.tabs = this.container.querySelectorAll("[data-tab-item]");
      this.activeTab = this.container.querySelector(".tab-active");
      if (!this.indicator || this.tabs.length === 0) {
        console.error("SlidingTabs component is missing required elements (indicator or items).", this.container);
        return;
      }
      this.init();
    }
    init() {
      if (this.activeTab) {
        setTimeout(() => this.updateIndicator(this.activeTab), 50);
      }
      this.bindEvents();
    }
    updateIndicator(targetTab) {
      if (!targetTab) return;
      const containerRect = this.container.getBoundingClientRect();
      const targetRect = targetTab.getBoundingClientRect();
      const left = targetRect.left - containerRect.left;
      const width = targetRect.width;
      this.indicator.style.left = `${left}px`;
      this.indicator.style.width = `${width}px`;
    }
    bindEvents() {
      this.tabs.forEach((tab) => {
        tab.addEventListener("click", (e) => {
          if (this.activeTab) {
            this.activeTab.classList.remove("tab-active");
          }
          tab.classList.add("tab-active");
          this.activeTab = tab;
          this.updateIndicator(this.activeTab);
        });
        tab.addEventListener("mouseenter", () => {
          this.updateIndicator(tab);
        });
      });
      this.container.addEventListener("mouseleave", () => {
        this.updateIndicator(this.activeTab);
      });
    }
  };
  document.addEventListener("DOMContentLoaded", () => {
    const allTabContainers = document.querySelectorAll("[data-sliding-tabs-container]");
    allTabContainers.forEach((container) => {
      new SlidingTabs(container);
    });
  });

  // frontend/js/components/customSelect.js
  var CustomSelect = class _CustomSelect {
    constructor(container) {
      this.container = container;
      this.trigger = this.container.querySelector(".custom-select-trigger");
      this.panel = this.container.querySelector(".custom-select-panel");
      if (!this.trigger || !this.panel) {
        console.warn("CustomSelect cannot initialize: missing .custom-select-trigger or .custom-select-panel.", this.container);
        return;
      }
      this.nativeSelect = this.container.querySelector("select");
      this.triggerText = this.trigger.querySelector("span");
      this.template = this.panel.querySelector(".custom-select-option-template");
      if (typeof _CustomSelect.openInstance === "undefined") {
        _CustomSelect.openInstance = null;
        _CustomSelect.initGlobalListener();
      }
      if (this.nativeSelect) {
        this.generateOptions();
        this.updateTriggerText();
      }
      this.bindEvents();
    }
    static initGlobalListener() {
      document.addEventListener("click", (event) => {
        if (_CustomSelect.openInstance && !_CustomSelect.openInstance.container.contains(event.target)) {
          _CustomSelect.openInstance.close();
        }
      });
    }
    generateOptions() {
      this.panel.querySelectorAll(":scope > *:not(.custom-select-option-template)").forEach((child) => child.remove());
      Array.from(this.nativeSelect.options).forEach((option) => {
        let item;
        if (this.template) {
          item = this.template.cloneNode(true);
          item.classList.remove("custom-select-option-template");
          item.removeAttribute("hidden");
        } else {
          item = document.createElement("a");
          item.href = "#";
          item.className = "block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-600";
        }
        item.classList.add("custom-select-option");
        item.textContent = option.textContent;
        item.dataset.value = option.value;
        if (option.selected) {
          item.classList.add("is-selected");
        }
        this.panel.appendChild(item);
      });
    }
    bindEvents() {
      this.trigger.addEventListener("click", (event) => {
        if (this.trigger.classList.contains("is-disabled")) {
          return;
        }
        event.stopPropagation();
        if (_CustomSelect.openInstance && _CustomSelect.openInstance !== this) {
          _CustomSelect.openInstance.close();
        }
        this.toggle();
      });
      if (this.nativeSelect) {
        this.panel.addEventListener("click", (event) => {
          event.preventDefault();
          const option = event.target.closest(".custom-select-option");
          if (option) {
            this.selectOption(option);
          }
        });
      }
    }
    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.panel.querySelectorAll(".custom-select-option").forEach((el) => el.classList.remove("is-selected"));
      optionEl.classList.add("is-selected");
      this.close();
    }
    updateTriggerText() {
      if (!this.nativeSelect || !this.triggerText) return;
      const selectedOption = this.nativeSelect.options[this.nativeSelect.selectedIndex];
      if (selectedOption) {
        this.triggerText.textContent = selectedOption.textContent;
      }
    }
    toggle() {
      this.panel.classList.toggle("hidden");
      if (this.panel.classList.contains("hidden")) {
        if (_CustomSelect.openInstance === this) {
          _CustomSelect.openInstance = null;
        }
      } else {
        _CustomSelect.openInstance = this;
      }
    }
    open() {
      this.panel.classList.remove("hidden");
      _CustomSelect.openInstance = this;
    }
    close() {
      this.panel.classList.add("hidden");
      if (_CustomSelect.openInstance === this) {
        _CustomSelect.openInstance = null;
      }
    }
  };

  // frontend/js/components/ui.js
  var ModalManager = class {
    /**
     * Shows a generic modal by its ID.
     * @param {string} modalId The ID of the modal element to show.
     */
    show(modalId) {
      const modal = document.getElementById(modalId);
      if (modal) {
        modal.classList.remove("hidden");
      } else {
        console.error(`Modal with ID "${modalId}" not found.`);
      }
    }
    /**
     * Hides a generic modal by its ID.
     * @param {string} modalId The ID of the modal element to hide.
     */
    hide(modalId) {
      const modal = document.getElementById(modalId);
      if (modal) {
        modal.classList.add("hidden");
      } else {
        console.error(`Modal with ID "${modalId}" not found.`);
      }
    }
    /**
     * Shows a confirmation dialog. This is a versatile method for 'Are you sure?' style prompts.
     * It dynamically sets the title, message, and confirm action for a generic confirmation modal.
     * @param {object} options - The options for the confirmation modal.
     * @param {string} options.modalId - The ID of the confirmation modal element (e.g., 'resetModal', 'deleteConfirmModal').
     * @param {string} options.title - The title to display in the modal header.
     * @param {string} options.message - The message to display in the modal body. Can contain HTML.
     * @param {function} options.onConfirm - The callback function to execute when the confirm button is clicked.
     * @param {boolean} [options.disableConfirm=false] - Whether the confirm button should be initially disabled.
     */
    showConfirm({ modalId, title, message, onConfirm, disableConfirm = false }) {
      const modalElement = document.getElementById(modalId);
      if (!modalElement) {
        console.error(`Confirmation modal with ID "${modalId}" not found.`);
        return;
      }
      const titleElement = modalElement.querySelector('[id$="ModalTitle"]');
      const messageElement = modalElement.querySelector('[id$="ModalMessage"]');
      const confirmButton = modalElement.querySelector('[id^="confirm"]');
      if (!titleElement || !messageElement || !confirmButton) {
        console.error(`Modal "${modalId}" is missing required child elements (title, message, or confirm button).`);
        return;
      }
      titleElement.textContent = title;
      messageElement.innerHTML = message;
      confirmButton.disabled = disableConfirm;
      const newConfirmButton = confirmButton.cloneNode(true);
      confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
      newConfirmButton.onclick = () => onConfirm();
      this.show(modalId);
    }
    /**
     * Shows a result modal to indicate the outcome of an operation (success or failure).
     * @param {boolean} success - If true, displays a success icon and title; otherwise, shows failure indicators.
     * @param {string|Node} message - The message to display. Can be a simple string or a complex DOM Node for rich content.
     * @param {boolean} [autoReload=false] - If true, the page will automatically reload when the modal is closed.
     */
    showResult(success, message, autoReload = false) {
      const modalElement = document.getElementById("resultModal");
      if (!modalElement) {
        console.error("Result modal with ID 'resultModal' not found.");
        return;
      }
      const titleElement = document.getElementById("resultModalTitle");
      const messageElement = document.getElementById("resultModalMessage");
      const iconElement = document.getElementById("resultIcon");
      const confirmButton = document.getElementById("resultModalConfirmBtn");
      if (!titleElement || !messageElement || !iconElement || !confirmButton) {
        console.error("Result modal is missing required child elements.");
        return;
      }
      titleElement.textContent = success ? "\u64CD\u4F5C\u6210\u529F" : "\u64CD\u4F5C\u5931\u8D25";
      if (success) {
        iconElement.innerHTML = '<i class="fas fa-check-circle text-success-500"></i>';
        iconElement.className = "text-6xl mb-3 text-success-500";
      } else {
        iconElement.innerHTML = '<i class="fas fa-times-circle text-danger-500"></i>';
        iconElement.className = "text-6xl mb-3 text-danger-500";
      }
      messageElement.innerHTML = "";
      if (typeof message === "string") {
        const messageDiv = document.createElement("div");
        messageDiv.innerText = message;
        messageElement.appendChild(messageDiv);
      } else if (message instanceof Node) {
        messageElement.appendChild(message);
      } else {
        const messageDiv = document.createElement("div");
        messageDiv.innerText = String(message);
        messageElement.appendChild(messageDiv);
      }
      confirmButton.onclick = () => this.closeResult(autoReload);
      this.show("resultModal");
    }
    /**
     * Closes the result modal.
     * @param {boolean} [reload=false] - If true, reloads the page after closing the modal.
     */
    closeResult(reload = false) {
      this.hide("resultModal");
      if (reload) {
        location.reload();
      }
    }
    /**
     * Shows and initializes the progress modal for long-running operations.
     * @param {string} title - The title to display for the progress modal.
     */
    showProgress(title) {
      const modal = document.getElementById("progressModal");
      if (!modal) {
        console.error("Progress modal with ID 'progressModal' not found.");
        return;
      }
      const titleElement = document.getElementById("progressModalTitle");
      const statusText = document.getElementById("progressStatusText");
      const progressBar = document.getElementById("progressBar");
      const progressPercentage = document.getElementById("progressPercentage");
      const progressLog = document.getElementById("progressLog");
      const closeButton = document.getElementById("progressModalCloseBtn");
      const closeIcon = document.getElementById("closeProgressModalBtn");
      if (!titleElement || !statusText || !progressBar || !progressPercentage || !progressLog || !closeButton || !closeIcon) {
        console.error("Progress modal is missing required child elements.");
        return;
      }
      titleElement.textContent = title;
      statusText.textContent = "\u51C6\u5907\u5F00\u59CB...";
      progressBar.style.width = "0%";
      progressPercentage.textContent = "0%";
      progressLog.innerHTML = "";
      closeButton.disabled = true;
      closeIcon.disabled = true;
      this.show("progressModal");
    }
    /**
     * Updates the progress bar and status text within the progress modal.
     * @param {number} processed - The number of items that have been processed.
     * @param {number} total - The total number of items to process.
     * @param {string} status - The current status message to display.
     */
    updateProgress(processed, total, status) {
      const modal = document.getElementById("progressModal");
      if (!modal || modal.classList.contains("hidden")) return;
      const progressBar = document.getElementById("progressBar");
      const progressPercentage = document.getElementById("progressPercentage");
      const statusText = document.getElementById("progressStatusText");
      const closeButton = document.getElementById("progressModalCloseBtn");
      const closeIcon = document.getElementById("closeProgressModalBtn");
      const percentage = total > 0 ? Math.round(processed / total * 100) : 0;
      progressBar.style.width = `${percentage}%`;
      progressPercentage.textContent = `${percentage}%`;
      statusText.textContent = status;
      if (processed === total) {
        closeButton.disabled = false;
        closeIcon.disabled = false;
      }
    }
    /**
     * Adds a log entry to the progress modal's log area.
     * @param {string} message - The log message to append.
     * @param {boolean} [isError=false] - If true, styles the log entry as an error.
     */
    addProgressLog(message, isError = false) {
      const progressLog = document.getElementById("progressLog");
      if (!progressLog) return;
      const logEntry = document.createElement("div");
      logEntry.textContent = message;
      logEntry.className = isError ? "text-danger-600" : "text-gray-700";
      progressLog.appendChild(logEntry);
      progressLog.scrollTop = progressLog.scrollHeight;
    }
    /**
     * Closes the progress modal.
     * @param {boolean} [reload=false] - If true, reloads the page after closing.
     */
    closeProgress(reload = false) {
      this.hide("progressModal");
      if (reload) {
        location.reload();
      }
    }
  };
  var UIPatterns = class {
    /**
     * Animates numerical values in elements from 0 to their target number.
     * The target number is read from the element's text content.
     * @param {string} selector - The CSS selector for the elements to animate (e.g., '.stat-value').
     * @param {number} [duration=1500] - The duration of the animation in milliseconds.
     */
    animateCounters(selector = ".stat-value", duration = 1500) {
      const statValues = document.querySelectorAll(selector);
      statValues.forEach((valueElement) => {
        const finalValue = parseInt(valueElement.textContent, 10);
        if (isNaN(finalValue)) return;
        if (!valueElement.dataset.originalValue) {
          valueElement.dataset.originalValue = valueElement.textContent;
        }
        let startValue = 0;
        const startTime = performance.now();
        const updateCounter = (currentTime) => {
          const elapsedTime = currentTime - startTime;
          if (elapsedTime < duration) {
            const progress = elapsedTime / duration;
            const easeOutValue = 1 - Math.pow(1 - progress, 3);
            const currentValue = Math.floor(easeOutValue * finalValue);
            valueElement.textContent = currentValue;
            requestAnimationFrame(updateCounter);
          } else {
            valueElement.textContent = valueElement.dataset.originalValue;
          }
        };
        requestAnimationFrame(updateCounter);
      });
    }
    /**
     * Toggles the visibility of a content section with a smooth height animation.
     * It expects a specific HTML structure where the header and content are within a common parent (e.g., a card).
     * The content element should have a `collapsed` class when hidden.
     * @param {HTMLElement} header - The header element that was clicked to trigger the toggle.
     */
    toggleSection(header) {
      const card = header.closest(".stats-card");
      if (!card) return;
      const content = card.querySelector(".key-content");
      const toggleIcon = header.querySelector(".toggle-icon");
      if (!content || !toggleIcon) {
        console.error("Toggle section failed: Content or icon element not found.", { header });
        return;
      }
      const isCollapsed = content.classList.contains("collapsed");
      toggleIcon.classList.toggle("collapsed", !isCollapsed);
      if (isCollapsed) {
        content.classList.remove("collapsed");
        content.style.maxHeight = null;
        content.style.opacity = null;
        content.style.paddingTop = null;
        content.style.paddingBottom = null;
        content.style.overflow = "hidden";
        requestAnimationFrame(() => {
          const targetHeight = content.scrollHeight;
          content.style.maxHeight = `${targetHeight}px`;
          content.style.opacity = "1";
          content.style.paddingTop = "1rem";
          content.style.paddingBottom = "1rem";
          content.addEventListener("transitionend", function onExpansionEnd() {
            content.removeEventListener("transitionend", onExpansionEnd);
            if (!content.classList.contains("collapsed")) {
              content.style.maxHeight = "";
              content.style.overflow = "visible";
            }
          }, { once: true });
        });
      } else {
        const currentHeight = content.scrollHeight;
        content.style.maxHeight = `${currentHeight}px`;
        content.style.overflow = "hidden";
        requestAnimationFrame(() => {
          content.style.maxHeight = "0px";
          content.style.opacity = "0";
          content.style.paddingTop = "0";
          content.style.paddingBottom = "0";
          content.classList.add("collapsed");
        });
      }
    }
  };
  var modalManager = new ModalManager();
  var uiPatterns = new UIPatterns();

  // frontend/js/components/taskCenter.js
  var TaskCenterManager = class {
    constructor() {
      this.tasks = [];
      this.activePolls = /* @__PURE__ */ new Map();
      this.heartbeatInterval = null;
      this.MINIMUM_TASK_DISPLAY_TIME_MS = 800;
      this.hasUnreadCompletedTasks = false;
      this.isAnimating = false;
      this.countdownTimer = null;
      this.trigger = document.getElementById("task-hub-trigger");
      this.panel = document.getElementById("task-hub-panel");
      this.countdownBar = document.getElementById("task-hub-countdown-bar");
      this.countdownRing = document.getElementById("task-hub-countdown-ring");
      this.indicator = document.getElementById("task-hub-indicator");
      this.clearBtn = document.getElementById("task-hub-clear-btn");
      this.taskListContainer = document.getElementById("task-list-container");
      this.emptyState = document.getElementById("task-list-empty");
    }
    // [THE FINAL, DEFINITIVE VERSION]
    init() {
      if (!this.trigger || !this.panel) {
        console.warn("Task Center UI core elements not found. Initialization skipped.");
        return;
      }
      this.trigger.addEventListener("click", (event) => {
        event.stopPropagation();
        if (this.isAnimating) return;
        if (this.panel.classList.contains("hidden")) {
          this._handleUserInteraction();
          this.openPanel();
        } else {
          this.closePanel();
        }
      });
      document.addEventListener("click", (event) => {
        if (!this.panel.classList.contains("hidden") && !this.isAnimating && !this.panel.contains(event.target) && !this.trigger.contains(event.target)) {
          this.closePanel();
        }
      });
      this.trigger.addEventListener("mouseenter", this._stopCountdown.bind(this));
      this.panel.addEventListener("mouseenter", this._handleUserInteraction.bind(this));
      const handleMouseLeave = () => {
        if (!this.panel.classList.contains("hidden")) {
          this._startCountdown();
        }
      };
      this.panel.addEventListener("mouseleave", handleMouseLeave);
      this.trigger.addEventListener("mouseleave", handleMouseLeave);
      this.clearBtn?.addEventListener("click", this.clearCompletedTasks.bind(this));
      this.taskListContainer.addEventListener("click", (event) => {
        const toggleHeader = event.target.closest("[data-task-toggle]");
        if (!toggleHeader) return;
        this._handleUserInteraction();
        const taskItem = toggleHeader.closest(".task-list-item");
        const content = taskItem.querySelector("[data-task-content]");
        if (!content) return;
        const isCollapsed = content.classList.contains("collapsed");
        toggleHeader.classList.toggle("expanded", isCollapsed);
        if (isCollapsed) {
          content.classList.remove("collapsed");
          content.style.maxHeight = `${content.scrollHeight}px`;
          content.style.opacity = "1";
          content.addEventListener("transitionend", () => {
            if (!content.classList.contains("collapsed")) content.style.maxHeight = "none";
          }, { once: true });
        } else {
          content.style.maxHeight = `${content.scrollHeight}px`;
          requestAnimationFrame(() => {
            content.style.maxHeight = "0px";
            content.style.opacity = "0";
            content.classList.add("collapsed");
          });
        }
      });
      this._render();
      this._startHeartbeat();
      console.log("Task Center UI Initialized [Multi-Task Heartbeat Polling Architecture - IGNITED].");
    }
    async startTask(taskDefinition) {
      try {
        const initialTaskData = await taskDefinition.start();
        if (!initialTaskData || !initialTaskData.id) throw new Error("Task definition did not return a valid initial task object.");
        const newTask = {
          id: initialTaskData.id,
          definition: taskDefinition,
          data: initialTaskData,
          timestamp: /* @__PURE__ */ new Date(),
          startTime: Date.now()
        };
        if (!initialTaskData.is_running) {
          console.log(`[TaskCenter] Task ${newTask.id} completed synchronously. Skipping poll.`);
          taskDefinition.renderToastNarrative(newTask.data, {}, toastManager);
          this.tasks.unshift(newTask);
          this._render();
          this._handleTaskCompletion(newTask);
          return;
        }
        this.tasks.unshift(newTask);
        this.activePolls.set(newTask.id, newTask);
        this._render();
        this.openPanel();
        taskDefinition.renderToastNarrative(newTask.data, {}, toastManager);
        this._updateIndicatorState();
      } catch (error) {
        console.error("Failed to start task:", error);
        toastManager.show(`\u4EFB\u52A1\u542F\u52A8\u5931\u8D25: ${error.message}`, "error");
      }
    }
    _startHeartbeat() {
      if (this.heartbeatInterval) return;
      this.heartbeatInterval = setInterval(this._tick.bind(this), 1500);
    }
    _stopHeartbeat() {
      if (this.heartbeatInterval) {
        clearInterval(this.heartbeatInterval);
        this.heartbeatInterval = null;
      }
    }
    async _tick() {
      if (this.activePolls.size === 0) {
        return;
      }
      for (const taskId of [...this.activePolls.keys()]) {
        const task = this.activePolls.get(taskId);
        if (!task) continue;
        try {
          const response = await task.definition.poll(taskId);
          if (!response.success || !response.data) throw new Error(response.message || "Polling failed");
          const oldData = { ...task.data };
          task.data = response.data;
          this._updateTaskItemInHistory(task.id, task.data);
          task.definition.renderToastNarrative(task.data, oldData, toastManager);
          if (!task.data.is_running) {
            this._handleTaskCompletion(task);
          }
        } catch (error) {
          console.error(`Polling for task ${taskId} failed:`, error);
          task.data.error = error.message;
          this._updateTaskItemInHistory(task.id, task.data);
          this._handleTaskCompletion(task);
        }
      }
    }
    _handleTaskCompletion(task) {
      this.activePolls.delete(task.id);
      this._updateIndicatorState();
      const toastId = `task-${task.id}`;
      const finalize = async () => {
        await toastManager.dismiss(toastId, !task.data.error);
        this._updateTaskItemInDom(task);
        this.hasUnreadCompletedTasks = true;
        this._updateIndicatorState();
        if (task.data.error) {
          if (task.definition.onError) task.definition.onError(task.data);
        } else {
          if (task.definition.onSuccess) task.definition.onSuccess(task.data);
        }
      };
      const elapsedTime = Date.now() - task.startTime;
      const remainingTime = this.MINIMUM_TASK_DISPLAY_TIME_MS - elapsedTime;
      if (remainingTime > 0) {
        setTimeout(finalize, remainingTime);
      } else {
        finalize();
      }
    }
    // [REFACTORED for robustness]
    _updateIndicatorState() {
      const hasRunningTasks = this.activePolls.size > 0;
      const shouldBeVisible = hasRunningTasks || this.hasUnreadCompletedTasks;
      this.indicator.classList.toggle("hidden", !shouldBeVisible);
    }
    // [REFACTORED for robustness]
    clearCompletedTasks() {
      this.tasks = this.tasks.filter((task) => this.activePolls.has(task.id));
      this.hasUnreadCompletedTasks = false;
      this._render();
    }
    // [NEW SAFETY METHOD]
    _updateTaskItemInHistory(taskId, newData) {
      const taskInHistory = this.tasks.find((t) => t.id === taskId);
      if (taskInHistory) {
        taskInHistory.data = newData;
      }
    }
    // --- 渲染与DOM操作 ---
    _render() {
      this.taskListContainer.innerHTML = this.tasks.map((task) => this._createTaskItemHtml(task)).join("");
      const hasTasks = this.tasks.length > 0;
      this.taskListContainer.classList.toggle("hidden", !hasTasks);
      this.emptyState.classList.toggle("hidden", hasTasks);
      this._updateIndicatorState();
    }
    _createTaskItemHtml(task) {
      const innerHtml = task.definition.renderTaskCenterItem(task.data, task.timestamp, this._formatTimeAgo);
      return `<div class="task-list-item" data-task-id="${task.id}">${innerHtml}</div>`;
    }
    _updateTaskItemInDom(task) {
      const item = this.taskListContainer.querySelector(`[data-task-id="${task.id}"]`);
      if (item) {
        item.innerHTML = task.definition.renderTaskCenterItem(task.data, task.timestamp, this._formatTimeAgo);
      }
    }
    // --- 核心面板开关逻辑 ---
    openPanel() {
      if (this.isAnimating || !this.panel.classList.contains("hidden")) return;
      this.isAnimating = true;
      this.panel.classList.remove("hidden");
      this.panel.classList.add("animate-panel-in");
      setTimeout(() => {
        this.panel.classList.remove("animate-panel-in");
        this.isAnimating = false;
        this._startCountdown();
      }, 150);
    }
    closePanel() {
      if (this.isAnimating || this.panel.classList.contains("hidden")) return;
      this._stopCountdown();
      this.isAnimating = true;
      this.panel.classList.add("animate-panel-out");
      setTimeout(() => {
        this.panel.classList.remove("animate-panel-out");
        this.panel.classList.add("hidden");
        this.isAnimating = false;
      }, 50);
    }
    // --- [新增] 倒计时管理方法 ---
    /**
     * 启动或重启倒计时和进度条动画
     * @private
     */
    _startCountdown() {
      this._stopCountdown();
      this.countdownBar.classList.add("w-full", "duration-[4950ms]");
      this.countdownRing.style.transition = "none";
      this.countdownRing.style.strokeDashoffset = "72.26";
      void this.countdownRing.offsetHeight;
      this.countdownRing.style.transition = "stroke-dashoffset 4.95s linear";
      this.countdownRing.style.strokeDashoffset = "0";
      this.countdownTimer = setTimeout(() => {
        this.closePanel();
      }, 4950);
    }
    /**
     * 停止倒计时并重置进度条
     * @private
     */
    _stopCountdown() {
      if (this.countdownTimer) {
        clearTimeout(this.countdownTimer);
        this.countdownTimer = null;
      }
      this.countdownBar.classList.remove("w-full");
      this.countdownRing.style.transition = "none";
      this.countdownRing.style.strokeDashoffset = "72.26";
    }
    // [NEW] A central handler for any action that confirms the user has seen the panel.
    _handleUserInteraction() {
      this._stopCountdown();
      if (this.hasUnreadCompletedTasks) {
        this.hasUnreadCompletedTasks = false;
        this._updateIndicatorState();
      }
    }
    _formatTimeAgo(date) {
      if (!date) return "";
      const seconds = Math.floor((/* @__PURE__ */ new Date() - new Date(date)) / 1e3);
      if (seconds < 2) return "\u521A\u521A";
      if (seconds < 60) return `${seconds}\u79D2\u524D`;
      const minutes = Math.floor(seconds / 60);
      if (minutes < 60) return `${minutes}\u5206\u949F\u524D`;
      const hours = Math.floor(minutes / 60);
      if (hours < 24) return `${hours}\u5C0F\u65F6\u524D`;
      const days = Math.floor(hours / 24);
      return `${days}\u5929\u524D`;
    }
  };
  var ToastManager = class {
    constructor() {
      this.container = document.getElementById("toast-container");
      if (!this.container) {
        this.container = document.createElement("div");
        this.container.id = "toast-container";
        this.container.className = "fixed bottom-4 right-4 z-[100] w-full max-w-sm space-y-3";
        document.body.appendChild(this.container);
      }
      this.activeToasts = /* @__PURE__ */ new Map();
    }
    /**
     * 显示一个 Toast 通知
     * @param {string} message - The message to display.
     * @param {string} [type='info'] - 'info', 'success', or 'error'.
     * @param {number} [duration=4000] - Duration in milliseconds.
     */
    show(message, type = "info", duration = 4e3) {
      const toastElement = this._createToastHtml(message, type);
      this.container.appendChild(toastElement);
      requestAnimationFrame(() => {
        toastElement.classList.remove("opacity-0", "translate-y-2");
        toastElement.classList.add("opacity-100", "translate-y-0");
      });
      setTimeout(() => {
        toastElement.classList.remove("opacity-100", "translate-y-0");
        toastElement.classList.add("opacity-0", "translate-y-2");
        toastElement.addEventListener("transitionend", () => toastElement.remove(), { once: true });
      }, duration);
    }
    // [NEW] 创建或更新一个带进度条的Toast
    showProgressToast(toastId, title, message, progress) {
      if (this.activeToasts.has(toastId)) {
        const toastElement = this.activeToasts.get(toastId);
        const messageEl = toastElement.querySelector(".toast-message");
        const progressBar = toastElement.querySelector(".toast-progress-bar");
        messageEl.textContent = `${message} - ${Math.round(progress)}%`;
        anime({
          targets: progressBar,
          width: `${progress}%`,
          duration: 400,
          easing: "easeOutQuad"
        });
      } else {
        const toastElement = this._createProgressToastHtml(toastId, title, message, progress);
        this.container.appendChild(toastElement);
        this.activeToasts.set(toastId, toastElement);
        requestAnimationFrame(() => {
          toastElement.classList.remove("opacity-0", "translate-x-full");
          toastElement.classList.add("opacity-100", "translate-x-0");
        });
      }
    }
    // [NEW] 移除一个进度Toast
    dismiss(toastId, success = null) {
      return new Promise((resolve) => {
        if (!this.activeToasts.has(toastId)) {
          resolve();
          return;
        }
        const toastElement = this.activeToasts.get(toastId);
        const performFadeOut = () => {
          toastElement.classList.remove("opacity-100", "translate-x-0");
          toastElement.classList.add("opacity-0", "translate-x-full");
          toastElement.addEventListener("transitionend", () => {
            toastElement.remove();
            this.activeToasts.delete(toastId);
            resolve();
          }, { once: true });
        };
        if (success === null) {
          performFadeOut();
        } else {
          const iconContainer = toastElement.querySelector(".toast-icon");
          const messageEl = toastElement.querySelector(".toast-message");
          if (success) {
            const progressBar = toastElement.querySelector(".toast-progress-bar");
            messageEl.textContent = "\u5DF2\u5B8C\u6210";
            anime({
              targets: progressBar,
              width: "100%",
              duration: 300,
              easing: "easeOutQuad",
              complete: () => {
                iconContainer.innerHTML = `<i class="fas fa-check-circle text-white"></i>`;
                iconContainer.className = `toast-icon bg-green-500`;
                setTimeout(performFadeOut, 900);
              }
            });
          } else {
            iconContainer.innerHTML = `<i class="fas fa-times-circle text-white"></i>`;
            iconContainer.className = `toast-icon bg-red-500`;
            messageEl.textContent = "\u5931\u8D25";
            setTimeout(performFadeOut, 1200);
          }
        }
      });
    }
    _createToastHtml(message, type) {
      const icons = {
        info: { class: "bg-blue-500", icon: "fa-info-circle" },
        success: { class: "bg-green-500", icon: "fa-check-circle" },
        error: { class: "bg-red-500", icon: "fa-exclamation-triangle" }
      };
      const typeInfo = icons[type] || icons.info;
      const toast = document.createElement("div");
      toast.className = "toast-item opacity-0 translate-y-2 transition-all duration-300 ease-out";
      toast.innerHTML = `
            <div class="toast-icon ${typeInfo.class}">
                <i class="fas ${typeInfo.icon}"></i>
            </div>
            <div class="toast-content">
                <p class="toast-title">${this._capitalizeFirstLetter(type)}</p>
                <p class="toast-message">${message}</p>
            </div>
        `;
      return toast;
    }
    // [NEW] 创建带进度条Toast的HTML结构
    _createProgressToastHtml(toastId, title, message, progress) {
      const toast = document.createElement("div");
      toast.className = "toast-item opacity-0 translate-x-full transition-all duration-300 ease-out";
      toast.dataset.toastId = toastId;
      toast.innerHTML = `
            <div class="toast-icon bg-blue-500">
                <i class="fas fa-spinner animate-spin"></i>
            </div>
            <div class="toast-content">
                <p class="toast-title">${title}</p>
                <p class="toast-message">${message} - ${Math.round(progress)}%</p>
                <div class="w-full bg-slate-200 dark:bg-zinc-700 rounded-full h-1 mt-1.5 overflow-hidden">
                    <div class="toast-progress-bar bg-blue-500 h-1 rounded-full" style="width: ${progress}%;"></div>
                </div>
            </div>
        `;
      return toast;
    }
    _capitalizeFirstLetter(string) {
      return string.charAt(0).toUpperCase() + string.slice(1);
    }
  };
  var taskCenterManager = new TaskCenterManager();
  var toastManager = new ToastManager();

  // frontend/js/pages/dashboard.js
  function init() {
    console.log("[Modern Frontend] Dashboard module loaded. Future logic will execute here.");
  }

  // frontend/js/components/tagInput.js
  var TagInput = class {
    constructor(container, options = {}) {
      if (!container) {
        console.error("TagInput container not found.");
        return;
      }
      this.container = container;
      this.input = container.querySelector(".tag-input-new");
      this.tags = [];
      this.options = {
        validator: /.+/,
        validationMessage: "\u8F93\u5165\u683C\u5F0F\u65E0\u6548",
        ...options
      };
      this.copyBtn = document.createElement("button");
      this.copyBtn.className = "tag-copy-btn";
      this.copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
      this.copyBtn.title = "\u590D\u5236\u6240\u6709";
      this.container.appendChild(this.copyBtn);
      this._initEventListeners();
    }
    _initEventListeners() {
      this.container.addEventListener("click", (e) => {
        if (e.target.closest(".tag-delete")) {
          this._removeTag(e.target.closest(".tag-item"));
        }
      });
      if (this.input) {
        this.input.addEventListener("keydown", (e) => {
          if (e.key === "Enter" || e.key === "," || e.key === " ") {
            e.preventDefault();
            const value = this.input.value.trim();
            if (value) {
              this._addTag(value);
              this.input.value = "";
            }
          }
        });
        this.input.addEventListener("blur", () => {
          const value = this.input.value.trim();
          if (value) {
            this._addTag(value);
            this.input.value = "";
          }
        });
      }
      this.copyBtn.addEventListener("click", this._handleCopyAll.bind(this));
    }
    _addTag(raw_value) {
      const value = raw_value.toLowerCase();
      if (!this.options.validator.test(value)) {
        console.warn(`Tag validation failed for value: "${value}". Rule: ${this.options.validator}`);
        this.input.placeholder = this.options.validationMessage;
        this.input.classList.add("input-error");
        setTimeout(() => {
          this.input.classList.remove("input-error");
          this.input.placeholder = "\u6DFB\u52A0...";
        }, 2e3);
        return;
      }
      if (this.tags.includes(value)) return;
      this.tags.push(value);
      const tagEl = document.createElement("span");
      tagEl.className = "tag-item";
      tagEl.innerHTML = `<span class="tag-text">${value}</span><button class="tag-delete">&times;</button>`;
      this.container.insertBefore(tagEl, this.input);
    }
    // 处理复制逻辑的专用方法
    _handleCopyAll() {
      const tagsString = this.tags.join(",");
      if (!tagsString) {
        this.copyBtn.innerHTML = "<span>\u65E0\u5185\u5BB9!</span>";
        this.copyBtn.classList.add("none");
        setTimeout(() => {
          this.copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
          this.copyBtn.classList.remove("copied");
        }, 1500);
        return;
      }
      navigator.clipboard.writeText(tagsString).then(() => {
        this.copyBtn.innerHTML = "<span>\u5DF2\u590D\u5236!</span>";
        this.copyBtn.classList.add("copied");
        setTimeout(() => {
          this.copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
          this.copyBtn.classList.remove("copied");
        }, 2e3);
      }).catch((err) => {
        console.error("Could not copy text: ", err);
        this.copyBtn.innerHTML = "<span>\u5931\u8D25!</span>";
        setTimeout(() => {
          this.copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
        }, 2e3);
      });
    }
    _removeTag(tagEl) {
      const value = tagEl.querySelector(".tag-text").textContent;
      this.tags = this.tags.filter((t) => t !== value);
      tagEl.remove();
    }
    getValues() {
      return this.tags;
    }
    setValues(values) {
      this.container.querySelectorAll(".tag-item").forEach((el) => el.remove());
      this.tags = [];
      if (Array.isArray(values)) {
        values.filter((value) => value).forEach((value) => this._addTag(value));
      }
    }
  };

  // frontend/js/pages/keys/requestSettingsModal.js
  var RequestSettingsModal = class {
    constructor({ onSave }) {
      this.modalId = "request-settings-modal";
      this.modal = document.getElementById(this.modalId);
      this.onSave = onSave;
      if (!this.modal) {
        throw new Error(`Modal with id "${this.modalId}" not found.`);
      }
      this.elements = {
        saveBtn: document.getElementById("request-settings-save-btn"),
        customHeadersContainer: document.getElementById("CUSTOM_HEADERS_container"),
        addCustomHeaderBtn: document.getElementById("addCustomHeaderBtn"),
        streamOptimizerEnabled: document.getElementById("STREAM_OPTIMIZER_ENABLED"),
        streamingSettingsPanel: document.getElementById("streaming-settings-panel"),
        streamMinDelay: document.getElementById("STREAM_MIN_DELAY"),
        streamMaxDelay: document.getElementById("STREAM_MAX_DELAY"),
        streamShortTextThresh: document.getElementById("STREAM_SHORT_TEXT_THRESHOLD"),
        streamLongTextThresh: document.getElementById("STREAM_LONG_TEXT_THRESHOLD"),
        streamChunkSize: document.getElementById("STREAM_CHUNK_SIZE"),
        fakeStreamEnabled: document.getElementById("FAKE_STREAM_ENABLED"),
        fakeStreamInterval: document.getElementById("FAKE_STREAM_EMPTY_DATA_INTERVAL_SECONDS"),
        toolsCodeExecutionEnabled: document.getElementById("TOOLS_CODE_EXECUTION_ENABLED"),
        urlContextEnabled: document.getElementById("URL_CONTEXT_ENABLED"),
        showSearchLink: document.getElementById("SHOW_SEARCH_LINK"),
        showThinkingProcess: document.getElementById("SHOW_THINKING_PROCESS"),
        safetySettingsContainer: document.getElementById("SAFETY_SETTINGS_container"),
        addSafetySettingBtn: document.getElementById("addSafetySettingBtn"),
        configOverrides: document.getElementById("group-config-overrides")
      };
      this._initEventListeners();
    }
    // --- 公共 API ---
    /**
     * 打開模態框並填充數據
     * @param {object} data - 用於填充表單的數據
     */
    open(data) {
      this._populateForm(data);
      modalManager.show(this.modalId);
    }
    /**
     * 關閉模態框
     */
    close() {
      modalManager.hide(this.modalId);
    }
    // --- 內部事件與邏輯 ---
    _initEventListeners() {
      this.modal.addEventListener("click", (e) => {
        const removeBtn = e.target.closest(".remove-btn");
        if (removeBtn) {
          removeBtn.parentElement.remove();
        }
      });
      if (this.elements.addCustomHeaderBtn) {
        this.elements.addCustomHeaderBtn.addEventListener("click", () => this.addCustomHeaderItem());
      }
      if (this.elements.addSafetySettingBtn) {
        this.elements.addSafetySettingBtn.addEventListener("click", () => this.addSafetySettingItem());
      }
      if (this.elements.saveBtn) {
        this.elements.saveBtn.addEventListener("click", this._handleSave.bind(this));
      }
      if (this.elements.streamOptimizerEnabled) {
        this.elements.streamOptimizerEnabled.addEventListener("change", (e) => {
          this._toggleStreamingPanel(e.target.checked);
        });
      }
      const closeAction = () => {
        modalManager.hide(this.modalId);
      };
      const closeTriggers = this.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
      closeTriggers.forEach((trigger) => {
        trigger.addEventListener("click", closeAction);
      });
      this.modal.addEventListener("click", (event) => {
        if (event.target === this.modal) {
          closeAction();
        }
      });
    }
    async _handleSave() {
      const data = this._collectFormData();
      if (this.onSave) {
        try {
          if (this.elements.saveBtn) {
            this.elements.saveBtn.disabled = true;
            this.elements.saveBtn.textContent = "Saving...";
          }
          await this.onSave(data);
          this.close();
        } catch (error) {
          console.error("Failed to save request settings:", error);
          alert(`\u4FDD\u5B58\u5931\u6557: ${error.message}`);
        } finally {
          if (this.elements.saveBtn) {
            this.elements.saveBtn.disabled = false;
            this.elements.saveBtn.textContent = "Save Changes";
          }
        }
      }
    }
    // --- 所有表單處理輔助方法 ---
    _populateForm(data = {}) {
      const isStreamOptimizerEnabled = !!data.stream_optimizer_enabled;
      this._setToggle(this.elements.streamOptimizerEnabled, isStreamOptimizerEnabled);
      this._toggleStreamingPanel(isStreamOptimizerEnabled);
      this._setValue(this.elements.streamMinDelay, data.stream_min_delay);
      this._setValue(this.elements.streamMaxDelay, data.stream_max_delay);
      this._setValue(this.elements.streamShortTextThresh, data.stream_short_text_threshold);
      this._setValue(this.elements.streamLongTextThresh, data.stream_long_text_threshold);
      this._setValue(this.elements.streamChunkSize, data.stream_chunk_size);
      this._setToggle(this.elements.fakeStreamEnabled, data.fake_stream_enabled);
      this._setValue(this.elements.fakeStreamInterval, data.fake_stream_empty_data_interval_seconds);
      this._setToggle(this.elements.toolsCodeExecutionEnabled, data.tools_code_execution_enabled);
      this._setToggle(this.elements.urlContextEnabled, data.url_context_enabled);
      this._setToggle(this.elements.showSearchLink, data.show_search_link);
      this._setToggle(this.elements.showThinkingProcess, data.show_thinking_process);
      this._setValue(this.elements.configOverrides, data.config_overrides);
      this._populateKVItems(this.elements.customHeadersContainer, data.custom_headers, this.addCustomHeaderItem.bind(this));
      this._clearContainer(this.elements.safetySettingsContainer);
      if (data.safety_settings && typeof data.safety_settings === "object") {
        for (const [key, value] of Object.entries(data.safety_settings)) {
          this.addSafetySettingItem(key, value);
        }
      }
    }
    /**
    * Collects all data from the form fields and returns it as an object.
    * @returns {object} The collected request configuration data.
    */
    collectFormData() {
      return {
        // Simple Toggles & Inputs
        stream_optimizer_enabled: this.elements.streamOptimizerEnabled.checked,
        stream_min_delay: parseInt(this.elements.streamMinDelay.value, 10),
        stream_max_delay: parseInt(this.elements.streamMaxDelay.value, 10),
        stream_short_text_threshold: parseInt(this.elements.streamShortTextThresh.value, 10),
        stream_long_text_threshold: parseInt(this.elements.streamLongTextThresh.value, 10),
        stream_chunk_size: parseInt(this.elements.streamChunkSize.value, 10),
        fake_stream_enabled: this.elements.fakeStreamEnabled.checked,
        fake_stream_empty_data_interval_seconds: parseInt(this.elements.fakeStreamInterval.value, 10),
        tools_code_execution_enabled: this.elements.toolsCodeExecutionEnabled.checked,
        url_context_enabled: this.elements.urlContextEnabled.checked,
        show_search_link: this.elements.showSearchLink.checked,
        show_thinking_process: this.elements.showThinkingProcess.checked,
        config_overrides: this.elements.configOverrides.value,
        // Dynamic & Complex Fields
        custom_headers: this._collectKVItems(this.elements.customHeadersContainer),
        safety_settings: this._collectSafetySettings(this.elements.safetySettingsContainer)
        // TODO: Collect from Tag Inputs
        // image_models: this.imageModelsInput.getValues(),
      };
    }
    //  控制流式面板显示/隐藏的辅助函数
    _toggleStreamingPanel(is_enabled) {
      if (this.elements.streamingSettingsPanel) {
        if (is_enabled) {
          this.elements.streamingSettingsPanel.classList.remove("hidden");
        } else {
          this.elements.streamingSettingsPanel.classList.add("hidden");
        }
      }
    }
    /**
     * Adds a new key-value pair item for Custom Headers.
     * @param {string} [key=''] - The initial key.
     * @param {string} [value=''] - The initial value.
     */
    addCustomHeaderItem(key = "", value = "") {
      const container = this.elements.customHeadersContainer;
      const item = document.createElement("div");
      item.className = "dynamic-kv-item";
      item.innerHTML = `
            <input type="text" class="modal-input text-xs bg-zinc-100 dark:bg-zinc-700/50" placeholder="Header Name" value="${key}">
            <input type="text" class="modal-input text-xs" placeholder="Header Value" value="${value}">
            <button type="button" class="remove-btn text-zinc-400 hover:text-red-500 transition-colors"><i class="fas fa-trash-alt"></i></button>
        `;
      container.appendChild(item);
    }
    /**
     * Adds a new item for Safety Settings.
     * @param {string} [category=''] - The initial category.
     * @param {string} [threshold=''] - The initial threshold.
     */
    addSafetySettingItem(category = "", threshold = "") {
      const container = this.elements.safetySettingsContainer;
      const item = document.createElement("div");
      item.className = "safety-setting-item flex items-center gap-x-2";
      const harmCategories = [
        "HARM_CATEGORY_HARASSMENT",
        "HARM_CATEGORY_HATE_SPEECH",
        "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "HARM_CATEGORY_DANGEROUS_CONTENT",
        "HARM_CATEGORY_DANGEROUS_CONTENT",
        "HARM_CATEGORY_CIVIC_INTEGRITY"
      ];
      const harmThresholds = [
        "BLOCK_OFF",
        "BLOCK_NONE",
        "BLOCK_LOW_AND_ABOVE",
        "BLOCK_MEDIUM_AND_ABOVE",
        "BLOCK_ONLY_HIGH"
      ];
      const categorySelect = document.createElement("select");
      categorySelect.className = "modal-input flex-grow";
      harmCategories.forEach((cat) => {
        const option = new Option(cat.replace("HARM_CATEGORY_", ""), cat);
        if (cat === category) option.selected = true;
        categorySelect.add(option);
      });
      const thresholdSelect = document.createElement("select");
      thresholdSelect.className = "modal-input w-48";
      harmThresholds.forEach((thr) => {
        const option = new Option(thr.replace("BLOCK_", "").replace("_AND_ABOVE", "+"), thr);
        if (thr === threshold) option.selected = true;
        thresholdSelect.add(option);
      });
      const removeButton = document.createElement("button");
      removeButton.type = "button";
      removeButton.className = "remove-btn text-zinc-400 hover:text-red-500 transition-colors";
      removeButton.innerHTML = `<i class="fas fa-trash-alt"></i>`;
      item.appendChild(categorySelect);
      item.appendChild(thresholdSelect);
      item.appendChild(removeButton);
      container.appendChild(item);
    }
    // --- Private Helper Methods for Form Handling ---
    _setValue(element, value) {
      if (element && value !== null && value !== void 0) {
        element.value = value;
      }
    }
    _setToggle(element, value) {
      if (element) {
        element.checked = !!value;
      }
    }
    _clearContainer(container) {
      if (container) {
        const firstChild = container.firstElementChild;
        const isTemplate = firstChild && (firstChild.tagName === "TEMPLATE" || firstChild.id === "kv-item-header");
        let child = isTemplate ? firstChild.nextElementSibling : container.firstElementChild;
        while (child) {
          const next = child.nextElementSibling;
          child.remove();
          child = next;
        }
      }
    }
    _populateKVItems(container, items, addItemFn) {
      this._clearContainer(container);
      if (items && typeof items === "object") {
        for (const [key, value] of Object.entries(items)) {
          addItemFn(key, value);
        }
      }
    }
    _collectKVItems(container) {
      const items = {};
      container.querySelectorAll(".dynamic-kv-item").forEach((item) => {
        const keyEl = item.querySelector(".dynamic-kv-key");
        const valueEl = item.querySelector(".dynamic-kv-value");
        if (keyEl && valueEl && keyEl.value) {
          items[keyEl.value] = valueEl.value;
        }
      });
      return items;
    }
    _collectSafetySettings(container) {
      const items = {};
      container.querySelectorAll(".safety-setting-item").forEach((item) => {
        const categorySelect = item.querySelector("select:first-child");
        const thresholdSelect = item.querySelector("select:last-of-type");
        if (categorySelect && thresholdSelect && categorySelect.value) {
          items[categorySelect.value] = thresholdSelect.value;
        }
      });
      return items;
    }
  };

  // frontend/js/services/api.js
  var APIClientError = class extends Error {
    constructor(message, status, code, rawMessageFromServer) {
      super(message);
      this.name = "APIClientError";
      this.status = status;
      this.code = code;
      this.rawMessageFromServer = rawMessageFromServer;
    }
  };
  var apiPromiseCache = /* @__PURE__ */ new Map();
  async function apiFetch(url, options = {}) {
    const isGetRequest = !options.method || options.method.toUpperCase() === "GET";
    const cacheKey = isGetRequest && !options.noCache ? url : null;
    if (cacheKey && apiPromiseCache.has(cacheKey)) {
      return apiPromiseCache.get(cacheKey);
    }
    const token = localStorage.getItem("bearerToken");
    const headers = {
      "Content-Type": "application/json",
      ...options.headers
    };
    if (token) {
      headers["Authorization"] = `Bearer ${token}`;
    }
    const requestPromise = (async () => {
      try {
        const response = await fetch(url, { ...options, headers });
        if (response.status === 401) {
          if (cacheKey) apiPromiseCache.delete(cacheKey);
          localStorage.removeItem("bearerToken");
          if (window.location.pathname !== "/login") {
            window.location.href = "/login?error=\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u3002";
          }
          throw new APIClientError("Unauthorized", 401, "UNAUTHORIZED", "Session expired or token is invalid.");
        }
        if (!response.ok) {
          let errorData = null;
          let rawMessage = "";
          try {
            rawMessage = await response.text();
            if (rawMessage) {
              errorData = JSON.parse(rawMessage);
            }
          } catch (e) {
            errorData = { error: { code: "UNKNOWN_FORMAT", message: rawMessage || response.statusText } };
          }
          const code = errorData?.error?.code || "UNKNOWN_ERROR";
          const messageFromServer = errorData?.error?.message || rawMessage || "No message provided by server.";
          const error = new APIClientError(
            `API request failed: ${response.status}`,
            response.status,
            code,
            messageFromServer
          );
          throw error;
        }
        return response;
      } catch (error) {
        if (cacheKey) apiPromiseCache.delete(cacheKey);
        throw error;
      }
    })();
    if (cacheKey) {
      apiPromiseCache.set(cacheKey, requestPromise);
    }
    return requestPromise;
  }
  async function apiFetchJson(url, options = {}) {
    try {
      const response = await apiFetch(url, options);
      const clonedResponse = response.clone();
      const jsonData = await clonedResponse.json();
      return jsonData;
    } catch (error) {
      throw error;
    }
  }

  // frontend/js/components/apiKeyManager.js
  var ApiKeyManager = class {
    constructor() {
    }
    // [新增] 开始一个向指定分组添加Keys的异步任务
    /**
     * Starts a task to add multiple API keys to a specific group.
     * @param {number} groupId - The ID of the group.
     * @param {string} keysText - A string of keys, separated by newlines.
     * @returns {Promise<object>} A promise that resolves to the initial task status object.
     */
    async addKeysToGroup(groupId, keysText, validate) {
      const payload = {
        key_group_id: groupId,
        keys: keysText,
        validate_on_import: validate
      };
      const response = await apiFetch(`/admin/keygroups/${groupId}/apikeys/bulk`, {
        method: "POST",
        body: JSON.stringify(payload),
        noCache: true
      });
      return response.json();
    }
    // [新增] 查询一个指定任务的当前状态
    /**
     * Gets the current status of a background task.
     * @param {string} taskId - The ID of the task.
     * @returns {Promise<object>} A promise that resolves to the task status object.
     */
    getTaskStatus(taskId, options = {}) {
      return apiFetchJson(`/admin/tasks/${taskId}`, options);
    }
    /**
     * Fetches a paginated and filtered list of keys.
     * @param {string} type - The type of keys to fetch ('valid' or 'invalid').
     * @param {number} [page=1] - The page number to retrieve.
     * @param {number} [limit=10] - The number of keys per page.
     * @param {string} [searchTerm=''] - A search term to filter keys.
     * @param {number|null} [failCountThreshold=null] - A threshold for filtering by failure count.
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async fetchKeys(type, page = 1, limit = 10, searchTerm = "", failCountThreshold = null) {
      const params = new URLSearchParams({
        page,
        limit,
        status: type
      });
      if (searchTerm) params.append("search", searchTerm);
      if (failCountThreshold !== null) params.append("fail_count_threshold", failCountThreshold);
      return await apiFetch(`/api/keys?${params.toString()}`);
    }
    /**
    * Starts a task to unlink multiple API keys from a specific group.
    * @param {number} groupId - The ID of the group.
    * @param {string} keysText - A string of keys, separated by newlines.
    * @returns {Promise<object>} A promise that resolves to the initial task status object.
    */
    async unlinkKeysFromGroup(groupId, keysInput) {
      let keysAsText;
      if (Array.isArray(keysInput)) {
        keysAsText = keysInput.join("\n");
      } else {
        keysAsText = keysInput;
      }
      const payload = {
        key_group_id: groupId,
        keys: keysAsText
      };
      const response = await apiFetch(`/admin/keygroups/${groupId}/apikeys/bulk`, {
        method: "DELETE",
        body: JSON.stringify(payload),
        noCache: true
      });
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({ message: response.statusText }));
        throw new Error(errorData.message || `Request failed with status ${response.status}`);
      }
      return response.json();
    }
    /**
     * 更新一个Key在特定分组中的状态 (e.g., 'ACTIVE', 'DISABLED').
     * @param {number} groupId - The ID of the group.
     * @param {number} keyId - The ID of the API key (api_keys.id).
     * @param {string} newStatus - The new operational status ('ACTIVE', 'DISABLED', etc.).
     * @returns {Promise<object>} A promise that resolves to the updated mapping object.
     */
    async updateKeyStatusInGroup(groupId, keyId, newStatus) {
      const endpoint = `/admin/keygroups/${groupId}/apikeys/${keyId}`;
      const payload = { status: newStatus };
      return await apiFetchJson(endpoint, {
        method: "PUT",
        body: JSON.stringify(payload),
        noCache: true
      });
    }
    /**
     * [MODIFIED] Fetches a paginated and filtered list of API key details for a specific group.
     * @param {number} groupId - The ID of the group.
     * @param {object} [params={}] - An object containing pagination and filter parameters.
     * @param {number} [params.page=1] - The page number to fetch.
     * @param {number} [params.limit=20] - The number of items per page.
     * @param {string} [params.status] - An optional status to filter the keys by.
     * @returns {Promise<object>} A promise that resolves to a pagination object.
     */
    async getKeysForGroup(groupId, params = {}) {
      const query = new URLSearchParams({
        page: params.page || 1,
        // Default to page 1 if not provided
        limit: params.limit || 20
        // Default to 20 per page if not provided
      });
      if (params.status) {
        query.append("status", params.status);
      }
      if (params.keyword && params.keyword.trim() !== "") {
        query.append("keyword", params.keyword.trim());
      }
      const url = `/admin/keygroups/${groupId}/apikeys?${query.toString()}`;
      const responseData = await apiFetchJson(url, { noCache: true });
      if (!responseData.success || typeof responseData.data !== "object" || !Array.isArray(responseData.data.items)) {
        throw new Error(responseData.message || "Failed to fetch paginated keys for the group.");
      }
      return responseData.data;
    }
    /**
     * 启动一个重新验证一个或多个Key的异步任务。
     * @param {number} groupId - The ID of the group context for validation.
     * @param {string[]} keyValues - An array of API key strings to revalidate.
     * @returns {Promise<object>} A promise that resolves to the initial task status object.
     */
    async revalidateKeys(groupId, keyValues) {
      const payload = {
        keys: keyValues.join("\n")
      };
      const url = `/admin/keygroups/${groupId}/apikeys/test`;
      const responseData = await apiFetchJson(url, {
        method: "POST",
        body: JSON.stringify(payload),
        noCache: true
      });
      if (!responseData.success || !responseData.data) {
        throw new Error(responseData.message || "Failed to start revalidation task.");
      }
      return responseData.data;
    }
    /**
     * Starts a generic bulk action task for an entire group based on filters.
     * This single function replaces the need for separate cleanup, revalidate, and restore functions.
     * @param {number} groupId The group ID.
     * @param {object} payload The body of the request, defining the action and filters.
     * @returns {Promise<object>} The initial task response with a task_id.
     */
    async startGroupBulkActionTask(groupId, payload) {
      const url = `/admin/keygroups/${groupId}/bulk-actions`;
      const responseData = await apiFetchJson(url, {
        method: "POST",
        body: JSON.stringify(payload)
      });
      if (!responseData.success || !responseData.data) {
        throw new Error(responseData.message || "\u672A\u80FD\u542F\u52A8\u5206\u7EC4\u6279\u91CF\u4EFB\u52A1\u3002");
      }
      return responseData.data;
    }
    /**
     * [NEW] Fetches all keys for a group, filtered by status, for export purposes using the dedicated export API.
     * @param {number} groupId The ID of the group.
     * @param {string[]} statuses An array of statuses to filter by (e.g., ['active', 'cooldown']). Use ['all'] for everything.
     * @returns {Promise<string[]>} A promise that resolves to an array of API key strings.
     */
    async exportKeysForGroup(groupId, statuses = ["all"]) {
      const params = new URLSearchParams();
      statuses.forEach((status) => params.append("status", status));
      const url = `/admin/keygroups/${groupId}/apikeys/export?${params.toString()}`;
      const responseData = await apiFetchJson(url, { noCache: true });
      if (!responseData.success || !Array.isArray(responseData.data)) {
        throw new Error(responseData.message || "\u672A\u80FD\u83B7\u53D6\u7528\u4E8E\u5BFC\u51FA\u7684Key\u5217\u8868\u3002");
      }
      return responseData.data;
    }
    /**  ！！！以下为GB预置函数，未做对齐
     * Verifies a single API key.
     * @param {string} key - The API key to verify.
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async verifyKey(key) {
      return await apiFetch(`/gemini/v1beta/verify-key/${key}`, { method: "POST" });
    }
    /**
     * Verifies a batch of selected API keys.
     * @param {string[]} keys - An array of API keys to verify.
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async verifySelectedKeys(keys) {
      return await apiFetch(`/gemini/v1beta/verify-selected-keys`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ keys })
      });
    }
    /**
     * Resets the failure count for a single API key.
     * @param {string} key - The API key whose failure count is to be reset.
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async resetFailCount(key) {
      return await apiFetch(`/gemini/v1beta/reset-fail-count/${key}`, { method: "POST" });
    }
    /**
     * Resets the failure count for a batch of selected API keys.
     * @param {string[]} keys - An array of API keys to reset.
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async resetSelectedFailCounts(keys) {
      return await apiFetch(`/gemini/v1beta/reset-selected-fail-counts`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ keys })
      });
    }
    /**
     * Deletes a single API key.
     * @param {string} key - The API key to delete.
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async deleteKey(key) {
      return await apiFetch(`/api/config/keys/${key}`, { method: "DELETE" });
    }
    /**
     * Deletes a batch of selected API keys.
     * @param {string[]} keys - An array of API keys to delete.
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async deleteSelectedKeys(keys) {
      return await apiFetch("/api/config/keys/delete-selected", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ keys })
      });
    }
    /**
     * Fetches all keys, both valid and invalid.
     * @returns {Promise<object>} A promise that resolves to an object containing 'valid_keys' and 'invalid_keys' arrays.
     */
    async fetchAllKeys() {
      return await apiFetch("/api/keys/all");
    }
    /**
     * Fetches usage details for a specific key over the last 24 hours.
     * @param {string} key - The API key to get details for.
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async getKeyUsageDetails(key) {
      return await apiFetch(`/api/key-usage-details/${key}`);
    }
    /**
     * Fetches API call statistics for a given period.
     * @param {string} period - The time period for the stats (e.g., '1m', '1h', '24h').
     * @returns {Promise<object>} A promise that resolves to the API response data.
     */
    async getStatsDetails(period) {
      return await apiFetch(`/api/stats/details?period=${period}`);
    }
  };
  var apiKeyManager = new ApiKeyManager();

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

  // frontend/js/pages/keys/addApiModal.js
  var AddApiModal = class {
    constructor({ onImportSuccess }) {
      this.modalId = "add-api-modal";
      this.onImportSuccess = onImportSuccess;
      this.activeGroupId = null;
      this.elements = {
        modal: document.getElementById(this.modalId),
        title: document.getElementById("add-api-modal-title"),
        inputView: document.getElementById("add-api-input-view"),
        textarea: document.getElementById("api-add-textarea"),
        importBtn: document.getElementById("add-api-import-btn"),
        validateCheckbox: document.getElementById("validate-on-import-checkbox")
      };
      if (!this.elements.modal) {
        throw new Error(`Modal with id "${this.modalId}" not found.`);
      }
      this._initEventListeners();
    }
    open(activeGroupId) {
      if (!activeGroupId) {
        console.error("Cannot open AddApiModal: activeGroupId is required.");
        return;
      }
      this.activeGroupId = activeGroupId;
      this._reset();
      modalManager.show(this.modalId);
    }
    _initEventListeners() {
      this.elements.importBtn?.addEventListener("click", this._handleSubmit.bind(this));
      const closeAction = () => {
        this._reset();
        modalManager.hide(this.modalId);
      };
      const closeTriggers = this.elements.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
      closeTriggers.forEach((trigger) => trigger.addEventListener("click", closeAction));
      this.elements.modal.addEventListener("click", (event) => {
        if (event.target === this.elements.modal) closeAction();
      });
    }
    async _handleSubmit(event) {
      event.preventDefault();
      const cleanedKeys = this._parseAndCleanKeys(this.elements.textarea.value);
      if (cleanedKeys.length === 0) {
        alert("\u6CA1\u6709\u68C0\u6D4B\u5230\u6709\u6548\u7684API Keys\u3002");
        return;
      }
      this.elements.importBtn.disabled = true;
      this.elements.importBtn.innerHTML = `<i class="fas fa-spinner fa-spin mr-2"></i>\u6B63\u5728\u542F\u52A8...`;
      const addKeysTask = {
        start: async () => {
          const shouldValidate = this.elements.validateCheckbox.checked;
          const response = await apiKeyManager.addKeysToGroup(this.activeGroupId, cleanedKeys.join("\n"), shouldValidate);
          if (!response.success || !response.data) throw new Error(response.message || "\u542F\u52A8\u5BFC\u5165\u4EFB\u52A1\u5931\u8D25\u3002");
          return response.data;
        },
        poll: async (taskId) => {
          return await apiKeyManager.getTaskStatus(taskId, { noCache: true });
        },
        renderTaskCenterItem: (data, timestamp, formatTimeAgo) => {
          const timeAgo = formatTimeAgo(timestamp);
          let contentHtml = "";
          if (!data.is_running && !data.error) {
            const result = data.result || {};
            const newlyLinked = result.newly_linked_count || 0;
            const alreadyLinked = result.already_linked_count || 0;
            const summaryTitle = `\u6279\u91CF\u94FE\u63A5 ${newlyLinked} Key\uFF0C\u5DF2\u8DF3\u8FC7 ${alreadyLinked}`;
            contentHtml = `
                        <div class="task-item-main">
                            <div class="task-item-icon-summary text-green-500"><i class="fas fa-check-circle"></i></div>
                            <div class="task-item-content flex-grow">
                                <div class="flex justify-between items-center cursor-pointer" data-task-toggle>
                                    <p class="task-item-title">${summaryTitle}</p>
                                    <i class="fas fa-chevron-down task-toggle-icon"></i>
                                </div>
                                <div class="task-details-content collapsed" data-task-content>
                                    <div class="task-details-body space-y-1">
                                        <p class="flex justify-between"><span>\u6709\u6548\u8F93\u5165:</span> <span class="font-semibold">${data.total}</span></p>
                                        <p class="flex justify-between"><span>\u5206\u7EC4\u4E2D\u5DF2\u5B58\u5728 (\u8DF3\u8FC7):</span> <span class="font-semibold">${alreadyLinked}</span></p>
                                        <p class="flex justify-between font-bold"><span>\u65B0\u589E\u94FE\u63A5:</span> <span>${newlyLinked}</span></p>
                                    </div>
                                </div>
                            </div>
                        </div>
                    `;
          } else if (!data.is_running && data.error) {
            contentHtml = `
                        <div class="task-item-main">
                            <div class="task-item-icon-summary text-red-500"><i class="fas fa-times-circle"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">\u6279\u91CF\u6DFB\u52A0\u5931\u8D25</p>
                                <p class="task-item-status text-red-500 truncate" title="${data.error || "\u672A\u77E5\u9519\u8BEF"}">
                                    ${data.error || "\u672A\u77E5\u9519\u8BEF"}
                                </p>
                            </div>
                        </div>`;
          } else {
            contentHtml = `
                        <div class="task-item-main gap-3">
                            <div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">\u6279\u91CF\u6DFB\u52A0 ${data.total} \u4E2AAPI Key</p>
                                <p class="task-item-status">\u8FD0\u884C\u4E2D...</p>
                            </div>
                        </div>`;
          }
          return `${contentHtml}<div class="task-item-timestamp">${timeAgo}</div>`;
        },
        renderToastNarrative: (data, oldData, toastManager2) => {
          const toastId = `task-${data.id}`;
          const progress = data.total > 0 ? data.processed / data.total * 100 : 0;
          toastManager2.showProgressToast(toastId, `\u6279\u91CF\u6DFB\u52A0Key`, "\u5904\u7406\u4E2D", progress);
        },
        // This now ONLY shows the FINAL summary toast, after everything else is done.
        onSuccess: (data) => {
          if (this.onImportSuccess) this.onImportSuccess();
          const newlyLinked = data.result?.newly_linked_count || 0;
          toastManager.show(`\u4EFB\u52A1\u5B8C\u6210\uFF01\u6210\u529F\u94FE\u63A5 ${newlyLinked} \u4E2AKey\u3002`, "success");
        },
        // This is the final error handler.
        onError: (data) => {
          toastManager.show(`\u4EFB\u52A1\u5931\u8D25: ${data.error || "\u672A\u77E5\u9519\u8BEF"}`, "error");
        }
      };
      taskCenterManager.startTask(addKeysTask);
      modalManager.hide(this.modalId);
      this._reset();
    }
    _reset() {
      this.elements.title.textContent = "\u6279\u91CF\u6DFB\u52A0 API Keys";
      this.elements.inputView.classList.remove("hidden");
      this.elements.textarea.value = "";
      this.elements.textarea.disabled = false;
      this.elements.importBtn.disabled = false;
      this.elements.importBtn.innerHTML = "\u5BFC\u5165";
    }
    _parseAndCleanKeys(text) {
      const keys = text.replace(/[,;]/g, " ").split(/[\s\n]+/);
      const cleanedKeys = keys.map((key) => key.trim()).filter((key) => isValidApiKeyFormat(key));
      return [...new Set(cleanedKeys)];
    }
  };

  // frontend/js/pages/keys/deleteApiModal.js
  var DeleteApiModal = class {
    constructor({ onDeleteSuccess }) {
      this.modalId = "delete-api-modal";
      this.onDeleteSuccess = onDeleteSuccess;
      this.activeGroupId = null;
      this.elements = {
        modal: document.getElementById(this.modalId),
        textarea: document.getElementById("api-delete-textarea"),
        deleteBtn: document.getElementById(this.modalId).querySelector(".modal-btn-danger")
      };
      if (!this.elements.modal) {
        throw new Error(`Modal with id "${this.modalId}" not found.`);
      }
      this._initEventListeners();
    }
    open(activeGroupId) {
      if (!activeGroupId) {
        console.error("Cannot open DeleteApiModal: activeGroupId is required.");
        return;
      }
      this.activeGroupId = activeGroupId;
      this._reset();
      modalManager.show(this.modalId);
    }
    _initEventListeners() {
      this.elements.deleteBtn?.addEventListener("click", this._handleSubmit.bind(this));
      const closeAction = () => {
        this._reset();
        modalManager.hide(this.modalId);
      };
      const closeTriggers = this.elements.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
      closeTriggers.forEach((trigger) => trigger.addEventListener("click", closeAction));
      this.elements.modal.addEventListener("click", (event) => {
        if (event.target === this.elements.modal) closeAction();
      });
    }
    async _handleSubmit(event) {
      event.preventDefault();
      const cleanedKeys = this._parseAndCleanKeys(this.elements.textarea.value);
      if (cleanedKeys.length === 0) {
        alert("\u6CA1\u6709\u68C0\u6D4B\u5230\u6709\u6548\u7684API Keys\u3002");
        return;
      }
      this.elements.deleteBtn.disabled = true;
      this.elements.deleteBtn.innerHTML = `<i class="fas fa-spinner fa-spin mr-2"></i>\u6B63\u5728\u542F\u52A8...`;
      const deleteKeysTask = {
        start: async () => {
          const response = await apiKeyManager.unlinkKeysFromGroup(this.activeGroupId, cleanedKeys.join("\n"));
          if (!response.success || !response.data) throw new Error(response.message || "\u542F\u52A8\u89E3\u7ED1\u4EFB\u52A1\u5931\u8D25\u3002");
          return response.data;
        },
        poll: async (taskId) => {
          return await apiKeyManager.getTaskStatus(taskId, { noCache: true });
        },
        renderTaskCenterItem: (data, timestamp, formatTimeAgo) => {
          const timeAgo = formatTimeAgo(timestamp);
          let contentHtml = "";
          if (!data.is_running && !data.error) {
            const result = data.result || {};
            const unlinked = result.unlinked_count || 0;
            const deleted = result.hard_deleted_count || 0;
            const notFound = result.not_found_count || 0;
            const totalInput = data.total;
            const summaryTitle = `\u89E3\u7ED1 ${unlinked} Key\uFF0C\u6E05\u7406 ${deleted}`;
            contentHtml = `
                        <div class="task-item-main">
                            <div class="task-item-icon-summary text-green-500"><i class="fas fa-check-circle"></i></div>
                            <div class="task-item-content flex-grow">
                                <div class="flex justify-between items-center cursor-pointer" data-task-toggle>
                                    <p class="task-item-title">${summaryTitle}</p>
                                    <i class="fas fa-chevron-down task-toggle-icon"></i>
                                </div>
                                <div class="task-details-content collapsed" data-task-content>
                                    <div class="task-details-body space-y-1">
                                        <p class="flex justify-between"><span>\u6709\u6548\u8F93\u5165:</span> <span class="font-semibold">${totalInput}</span></p>
                                        <p class="flex justify-between"><span>\u672A\u5728\u5206\u7EC4\u4E2D\u627E\u5230:</span> <span class="font-semibold">${notFound}</span></p>
                                        <p class="flex justify-between"><span>\u4ECE\u5206\u7EC4\u4E2D\u89E3\u7ED1:</span> <span class="font-semibold">${unlinked}</span></p>
                                        <p class="flex justify-between font-bold"><span>\u5F7B\u5E95\u6E05\u7406\u5B64\u7ACBKey:</span> <span>${deleted}</span></p>
                                    </div>
                                </div>
                            </div>
                        </div>
                    `;
          } else if (!data.is_running && data.error) {
            contentHtml = `
                        <div class="task-item-main">
                            <div class="task-item-icon-summary text-red-500"><i class="fas fa-times-circle"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">\u6279\u91CF\u5220\u9664\u5931\u8D25</p>
                                <p class="task-item-status text-red-500 truncate" title="${data.error || "\u672A\u77E5\u9519\u8BEF"}">
                                    ${data.error || "\u672A\u77E5\u9519\u8BEF"}
                                </p>
                            </div>
                        </div>`;
          } else {
            contentHtml = `
                        <div class="task-item-main gap-3">
                            <div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">\u6279\u91CF\u5220\u9664 ${data.total} \u4E2AAPI Key</p>
                                <p class="task-item-status">\u8FD0\u884C\u4E2D...</p>
                            </div>
                        </div>`;
          }
          return `${contentHtml}<div class="task-item-timestamp">${timeAgo}</div>`;
        },
        // he Toast is now solely responsible for showing real-time progress.
        renderToastNarrative: (data, oldData, toastManager2) => {
          const toastId = `task-${data.id}`;
          const progress = data.total > 0 ? data.processed / data.total * 100 : 0;
          toastManager2.showProgressToast(toastId, `\u6279\u91CF\u5220\u9664Key`, "\u5904\u7406\u4E2D", progress);
        },
        // This now ONLY shows the FINAL summary toast, after everything else is done.
        onSuccess: (data) => {
          if (this.onDeleteSuccess) this.onDeleteSuccess();
          const newlyLinked = data.result?.newly_linked_count || 0;
          toastManager.show(`\u4EFB\u52A1\u5B8C\u6210\uFF01\u6210\u529F\u5220\u9664 ${newlyLinked} \u4E2AKey\u3002`, "success");
        },
        // This is the final error handler.
        onError: (data) => {
          toastManager.show(`\u4EFB\u52A1\u5931\u8D25: ${data.error || "\u672A\u77E5\u9519\u8BEF"}`, "error");
        }
      };
      taskCenterManager.startTask(deleteKeysTask);
      modalManager.hide(this.modalId);
      this._reset();
    }
    _reset() {
      this.elements.textarea.value = "";
      this.elements.deleteBtn.disabled = false;
      this.elements.deleteBtn.innerHTML = "\u5220\u9664";
    }
    _parseAndCleanKeys(text) {
      const keys = text.replace(/[,;]/g, " ").split(/[\s\n]+/);
      const cleanedKeys = keys.map((key) => key.trim()).filter((key) => isValidApiKeyFormat(key));
      return [...new Set(cleanedKeys)];
    }
  };

  // frontend/js/pages/keys/keyGroupModal.js
  var MAX_GROUP_NAME_LENGTH = 32;
  var KeyGroupModal = class {
    constructor({ onSave, tagInputInstances }) {
      this.modalId = "keygroup-modal";
      this.onSave = onSave;
      this.tagInputs = tagInputInstances;
      this.editingGroupId = null;
      const modal = document.getElementById(this.modalId);
      if (!modal) {
        throw new Error(`Modal with id "${this.modalId}" not found.`);
      }
      this.elements = {
        modal,
        title: document.getElementById("modal-title"),
        saveBtn: document.getElementById("modal-save-btn"),
        // 表单字段
        nameInput: document.getElementById("group-name"),
        nameHelper: document.getElementById("group-name-helper"),
        displayNameInput: document.getElementById("group-display-name"),
        descriptionInput: document.getElementById("group-description"),
        strategySelect: document.getElementById("group-strategy"),
        maxRetriesInput: document.getElementById("group-max-retries"),
        failureThresholdInput: document.getElementById("group-key-blacklist-threshold"),
        enableProxyToggle: document.getElementById("group-enable-proxy"),
        enableSmartGatewayToggle: document.getElementById("group-enable-smart-gateway"),
        // 自动验证设置
        enableKeyCheckToggle: document.getElementById("group-enable-key-check"),
        keyCheckSettingsPanel: document.getElementById("key-check-settings"),
        keyCheckModelInput: document.getElementById("group-key-check-model"),
        keyCheckIntervalInput: document.getElementById("group-key-check-interval-minutes"),
        keyCheckConcurrencyInput: document.getElementById("group-key-check-concurrency"),
        keyCooldownInput: document.getElementById("group-key-cooldown-minutes"),
        keyCheckEndpointInput: document.getElementById("group-key-check-endpoint")
      };
      this._initEventListeners();
    }
    open(groupData = null) {
      this._populateForm(groupData);
      modalManager.show(this.modalId);
    }
    close() {
      modalManager.hide(this.modalId);
    }
    _initEventListeners() {
      if (this.elements.saveBtn) {
        this.elements.saveBtn.addEventListener("click", this._handleSave.bind(this));
      }
      if (this.elements.nameInput) {
        this.elements.nameInput.addEventListener("input", this._sanitizeGroupName.bind(this));
      }
      if (this.elements.enableKeyCheckToggle) {
        this.elements.enableKeyCheckToggle.addEventListener("change", (e) => {
          this.elements.keyCheckSettingsPanel.classList.toggle("hidden", !e.target.checked);
        });
      }
      const closeAction = () => this.close();
      const closeTriggers = this.elements.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
      closeTriggers.forEach((trigger) => trigger.addEventListener("click", closeAction));
      this.elements.modal.addEventListener("click", (event) => {
        if (event.target === this.elements.modal) closeAction();
      });
    }
    // 实时净化 group name 的哨兵函数
    _sanitizeGroupName(event) {
      const input = event.target;
      let value = input.value;
      value = value.toLowerCase();
      value = value.replace(/[^a-z0-9-]/g, "");
      if (value.length > MAX_GROUP_NAME_LENGTH) {
        value = value.substring(0, MAX_GROUP_NAME_LENGTH);
      }
      if (input.value !== value) {
        input.value = value;
      }
    }
    async _handleSave() {
      this._sanitizeGroupName({ target: this.elements.nameInput });
      const data = this._collectFormData();
      if (!data.name || !data.display_name) {
        alert("\u5206\u7EC4\u540D\u79F0\u548C\u663E\u793A\u540D\u79F0\u662F\u5FC5\u586B\u9879\u3002");
        return;
      }
      const groupNameRegex = /^[a-z0-9-]+$/;
      if (!groupNameRegex.test(data.name) || data.name.length > MAX_GROUP_NAME_LENGTH) {
        alert("\u5206\u7EC4\u540D\u79F0\u683C\u5F0F\u65E0\u6548\u3002\u4EC5\u9650\u4F7F\u7528\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26(-)\uFF0C\u4E14\u957F\u5EA6\u4E0D\u8D85\u8FC732\u4E2A\u5B57\u7B26\u3002");
        return;
      }
      if (this.onSave) {
        this.elements.saveBtn.disabled = true;
        this.elements.saveBtn.textContent = "\u4FDD\u5B58\u4E2D...";
        try {
          await this.onSave(data);
          this.close();
        } catch (error) {
          console.error("Failed to save key group:", error);
        } finally {
          this.elements.saveBtn.disabled = false;
          this.elements.saveBtn.textContent = "\u4FDD\u5B58";
        }
      }
    }
    _populateForm(data) {
      if (data) {
        this.editingGroupId = data.id;
        this.elements.title.textContent = "\u7F16\u8F91 Key Group";
        this.elements.nameInput.value = data.name || "";
        this.elements.nameInput.disabled = false;
        this.elements.displayNameInput.value = data.display_name || "";
        this.elements.descriptionInput.value = data.description || "";
        this.elements.strategySelect.value = data.polling_strategy || "random";
        this.elements.enableProxyToggle.checked = data.enable_proxy || false;
        const settings = data.settings && data.settings.SettingsJSON ? data.settings.SettingsJSON : {};
        this.elements.maxRetriesInput.value = settings.max_retries ?? "";
        this.elements.failureThresholdInput.value = settings.key_blacklist_threshold ?? "";
        this.elements.enableSmartGatewayToggle.checked = settings.enable_smart_gateway || false;
        const isKeyCheckEnabled = settings.enable_key_check || false;
        this.elements.enableKeyCheckToggle.checked = isKeyCheckEnabled;
        this.elements.keyCheckSettingsPanel.classList.toggle("hidden", !isKeyCheckEnabled);
        this.elements.keyCheckModelInput.value = settings.key_check_model || "";
        this.elements.keyCheckIntervalInput.value = settings.key_check_interval_minutes ?? "";
        this.elements.keyCheckConcurrencyInput.value = settings.key_check_concurrency ?? "";
        this.elements.keyCooldownInput.value = settings.key_cooldown_minutes ?? "";
        this.elements.keyCheckEndpointInput.value = settings.key_check_endpoint || "";
        this.tagInputs.models.setValues(data.allowed_models || []);
        this.tagInputs.upstreams.setValues(data.allowed_upstreams || []);
        this.tagInputs.tokens.setValues(data.allowed_tokens || []);
      } else {
        this.editingGroupId = null;
        this.elements.title.textContent = "\u521B\u5EFA\u65B0\u7684 Key Group";
        this._resetForm();
      }
    }
    _collectFormData() {
      const parseIntOrNull = (value) => {
        const trimmed = value.trim();
        return trimmed === "" ? null : parseInt(trimmed, 10);
      };
      const formData = {
        name: this.elements.nameInput.value.trim(),
        display_name: this.elements.displayNameInput.value.trim(),
        description: this.elements.descriptionInput.value.trim(),
        polling_strategy: this.elements.strategySelect.value,
        max_retries: parseIntOrNull(this.elements.maxRetriesInput.value),
        key_blacklist_threshold: parseIntOrNull(this.elements.failureThresholdInput.value),
        enable_proxy: this.elements.enableProxyToggle.checked,
        enable_smart_gateway: this.elements.enableSmartGatewayToggle.checked,
        enable_key_check: this.elements.enableKeyCheckToggle.checked,
        key_check_model: this.elements.keyCheckModelInput.value.trim() || null,
        key_check_interval_minutes: parseIntOrNull(this.elements.keyCheckIntervalInput.value),
        key_check_concurrency: parseIntOrNull(this.elements.keyCheckConcurrencyInput.value),
        key_cooldown_minutes: parseIntOrNull(this.elements.keyCooldownInput.value),
        key_check_endpoint: this.elements.keyCheckEndpointInput.value.trim() || null,
        allowed_models: this.tagInputs.models.getValues(),
        allowed_upstreams: this.tagInputs.upstreams.getValues(),
        allowed_tokens: this.tagInputs.tokens.getValues()
      };
      if (this.editingGroupId) {
        formData.id = this.editingGroupId;
      }
      return formData;
    }
    /**
     * [核心修正] 完整且健壮的表单重置方法
     */
    _resetForm() {
      this.elements.nameInput.value = "";
      this.elements.nameInput.disabled = false;
      this.elements.displayNameInput.value = "";
      this.elements.descriptionInput.value = "";
      this.elements.strategySelect.value = "random";
      this.elements.maxRetriesInput.value = "";
      this.elements.failureThresholdInput.value = "";
      this.elements.enableProxyToggle.checked = false;
      this.elements.enableSmartGatewayToggle.checked = false;
      this.elements.enableKeyCheckToggle.checked = false;
      this.elements.keyCheckSettingsPanel.classList.add("hidden");
      this.elements.keyCheckModelInput.value = "";
      this.elements.keyCheckIntervalInput.value = "";
      this.elements.keyCheckConcurrencyInput.value = "";
      this.elements.keyCooldownInput.value = "";
      this.elements.keyCheckEndpointInput.value = "";
      this.tagInputs.models.setValues([]);
      this.tagInputs.upstreams.setValues([]);
      this.tagInputs.tokens.setValues([]);
    }
  };

  // frontend/js/pages/keys/cloneGroupModal.js
  var CloneGroupModal = class {
    constructor({ onCloneSuccess }) {
      this.modalId = "clone-group-modal";
      this.onCloneSuccess = onCloneSuccess;
      this.activeGroup = null;
      this.elements = {
        modal: document.getElementById(this.modalId),
        title: document.getElementById("clone-group-modal-title"),
        confirmBtn: document.getElementById("clone-group-confirm-btn")
      };
      if (!this.elements.modal) {
        console.error(`Modal with id "${this.modalId}" not found. Ensure the HTML is in your document.`);
        return;
      }
      this._initEventListeners();
    }
    open(group) {
      if (!group || !group.id) {
        console.error("Cannot open CloneGroupModal: a group object with an ID is required.");
        return;
      }
      this.activeGroup = group;
      this.elements.title.innerHTML = `\u786E\u8BA4\u514B\u9686\u5206\u7EC4 <code class="text-base font-semibold text-blue-500">${group.display_name}</code>`;
      this._reset();
      modalManager.show(this.modalId);
    }
    _initEventListeners() {
      this.elements.confirmBtn?.addEventListener("click", this._handleSubmit.bind(this));
      const closeAction = () => {
        this._reset();
        modalManager.hide(this.modalId);
      };
      const closeTriggers = this.elements.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
      closeTriggers.forEach((trigger) => trigger.addEventListener("click", closeAction));
      this.elements.modal.addEventListener("click", (event) => {
        if (event.target === this.elements.modal) closeAction();
      });
    }
    async _handleSubmit() {
      if (!this.activeGroup) return;
      this.elements.confirmBtn.disabled = true;
      this.elements.confirmBtn.innerHTML = `<i class="fas fa-spinner fa-spin mr-2"></i>\u514B\u9686\u4E2D...`;
      try {
        const endpoint = `/admin/keygroups/${this.activeGroup.id}/clone`;
        const response = await apiFetch(endpoint, {
          method: "POST",
          noCache: true
        });
        const result = await response.json();
        if (result.success && result.data) {
          toastManager.show(`\u5206\u7EC4 '${this.activeGroup.display_name}' \u5DF2\u6210\u529F\u514B\u9686\u3002`, "success");
          if (this.onCloneSuccess) {
            this.onCloneSuccess(result.data);
          }
          modalManager.hide(this.modalId);
        } else {
          throw new Error(result.error?.message || result.message || "\u514B\u9686\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
        }
      } catch (error) {
        toastManager.show(`\u514B\u9686\u5931\u8D25: ${error.message}`, "error");
      } finally {
        this._reset();
      }
    }
    _reset() {
      if (this.elements.confirmBtn) {
        this.elements.confirmBtn.disabled = false;
        this.elements.confirmBtn.innerHTML = "\u786E\u8BA4\u514B\u9686";
      }
    }
  };

  // frontend/js/pages/keys/deleteGroupModal.js
  var DeleteGroupModal = class {
    constructor({ onDeleteSuccess }) {
      this.modalId = "delete-group-modal";
      this.onDeleteSuccess = onDeleteSuccess;
      this.activeGroup = null;
      this.elements = {
        modal: document.getElementById(this.modalId),
        title: document.getElementById("delete-group-modal-title"),
        confirmInput: document.getElementById("delete-group-confirm-input"),
        confirmBtn: document.getElementById("delete-group-confirm-btn")
      };
      if (!this.elements.modal) {
        throw new Error(`Modal with id "${this.modalId}" not found.`);
      }
      this._initEventListeners();
    }
    open(group) {
      if (!group || !group.id) {
        console.error("Cannot open DeleteGroupModal: group object with id is required.");
        return;
      }
      this.activeGroup = group;
      this.elements.title.innerHTML = `\u786E\u8BA4\u5220\u9664\u5206\u7EC4 <code class="text-base font-semibold text-red-500">${group.display_name}</code>`;
      this._reset();
      modalManager.show(this.modalId);
    }
    _initEventListeners() {
      this.elements.confirmBtn?.addEventListener("click", this._handleSubmit.bind(this));
      this.elements.confirmInput?.addEventListener("input", () => {
        const isConfirmed = this.elements.confirmInput.value.trim() === "\u5220\u9664";
        this.elements.confirmBtn.disabled = !isConfirmed;
      });
      const closeAction = () => {
        this._reset();
        modalManager.hide(this.modalId);
      };
      const closeTriggers = this.elements.modal.querySelectorAll(`[data-modal-close="${this.modalId}"]`);
      closeTriggers.forEach((trigger) => trigger.addEventListener("click", closeAction));
      this.elements.modal.addEventListener("click", (event) => {
        if (event.target === this.elements.modal) closeAction();
      });
    }
    async _handleSubmit() {
      if (!this.activeGroup) return;
      this.elements.confirmBtn.disabled = true;
      this.elements.confirmBtn.innerHTML = `<i class="fas fa-spinner fa-spin mr-2"></i>\u5220\u9664\u4E2D...`;
      try {
        const endpoint = `/admin/keygroups/${this.activeGroup.id}`;
        const response = await apiFetch(endpoint, {
          method: "DELETE",
          noCache: true
          // Ensure a fresh request
        });
        const result = await response.json();
        if (result.success) {
          toastManager.show(`\u5206\u7EC4 '${this.activeGroup.display_name}' \u5DF2\u6210\u529F\u5220\u9664\u3002`, "success");
          if (this.onDeleteSuccess) {
            this.onDeleteSuccess(this.activeGroup.id);
          }
          modalManager.hide(this.modalId);
        } else {
          throw new Error(result.error?.message || result.message || "\u5220\u9664\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
        }
      } catch (error) {
        toastManager.show(`\u5220\u9664\u5931\u8D25: ${error.message}`, "error");
      } finally {
        this._reset();
      }
    }
    _reset() {
      if (this.elements.confirmInput) this.elements.confirmInput.value = "";
      if (this.elements.confirmBtn) {
        this.elements.confirmBtn.disabled = true;
        this.elements.confirmBtn.innerHTML = "\u786E\u8BA4\u5220\u9664";
      }
    }
  };

  // frontend/js/services/errorHandler.js
  var ERROR_MESSAGES2 = {
    "STATE_CONFLICT_MASTER_REVOKED": "\u64CD\u4F5C\u5931\u8D25\uFF1A\u65E0\u6CD5\u6FC0\u6D3B\u4E00\u4E2A\u5DF2\u88AB\u6C38\u4E45\u540A\u9500\uFF08Revoked\uFF09\u7684Key\u3002",
    "NOT_FOUND": "\u64CD\u4F5C\u5931\u8D25\uFF1A\u76EE\u6807\u8D44\u6E90\u4E0D\u5B58\u5728\u6216\u5DF2\u4ECE\u672C\u7EC4\u79FB\u9664\u3002\u5217\u8868\u5C06\u81EA\u52A8\u5237\u65B0\u3002",
    "NO_KEYS_MATCH_FILTER": "\u6CA1\u6709\u627E\u5230\u4EFB\u4F55\u7B26\u5408\u5F53\u524D\u8FC7\u6EE4\u6761\u4EF6\u7684Key\u53EF\u4F9B\u64CD\u4F5C\u3002",
    // You can add many more specific codes here as your application grows.
    "DEFAULT": "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u8054\u7CFB\u7BA1\u7406\u5458\u3002"
  };
  function handleApiError(error, toastManager2, options = {}) {
    const prefix = options.prefix || "";
    const errorCode = error?.code || "DEFAULT";
    const displayMessage = ERROR_MESSAGES2[errorCode] || error.rawMessageFromServer || error.message || ERROR_MESSAGES2["DEFAULT"];
    toastManager2.show(`${prefix}${displayMessage}`, "error");
  }

  // frontend/js/pages/keys/apiKeyList.js
  var ApiKeyList = class {
    /**
     * @param {HTMLElement} container - The DOM element that will contain the API key list.
     */
    constructor(container) {
      if (!container) {
        throw new Error("ApiKeyListManager requires a valid container element.");
      }
      this.elements = {
        container,
        // This is the scrollable list container now, e.g., #api-list-container
        gridContainer: null,
        // Will be dynamically created inside container
        paginationContainer: document.querySelector(".pagination-controls"),
        // Find the pagination container
        itemsPerPageSelect: document.querySelector(".items-per-page-select"),
        // Find the dropdown
        selectAllCheckbox: document.getElementById("select-all"),
        batchActionButton: document.querySelector(".batch-action-btn"),
        // The trigger button
        batchActionPanel: document.querySelector(".batch-action-panel"),
        // The dropdown panel
        statusFilterSelects: document.querySelectorAll(".status-filter-select"),
        desktopSearchInput: document.getElementById("desktop-search-input"),
        mobileSearchBtn: document.getElementById("mobile-search-btn"),
        desktopQuickActionsPanel: document.getElementById("desktop-quick-actions-panel"),
        mobileQuickActionsPanel: document.getElementById("mobile-quick-actions-panel"),
        desktopMultifunctionPanel: document.getElementById("desktop-multifunction-panel"),
        mobileMultifunctionPanel: document.getElementById("mobile-multifunction-panel")
      };
      this.state = {
        currentKeys: [],
        // Now holds only the keys for the current page
        selectedKeyIds: /* @__PURE__ */ new Set(),
        isApiKeysLoading: false,
        activeGroupId: null,
        activeGroupName: "",
        currentPage: 1,
        itemsPerPage: 20,
        // Default value, will be updated from select
        totalItems: 0,
        totalPages: 1,
        filterStatus: "all",
        searchText: ""
      };
      this.debouncedSearch = debounce(() => {
        this.state.currentPage = 1;
        this.loadApiKeys(this.state.activeGroupId, true);
      }, 300);
      this.boundListeners = {
        handleContainerClick: this._handleContainerClick.bind(this),
        handlePaginationClick: this.handlePaginationClick.bind(this),
        handleItemsPerPageChange: this._handleItemsPerPageChange.bind(this),
        handleSelectAllChange: this._handleSelectAllChange.bind(this),
        handleStatusFilterChange: this._handleStatusFilterChange.bind(this),
        handleBatchActionClick: this._handleBatchActionClick.bind(this),
        handleDocumentClickForMenuClose: this._handleDocumentClickForMenuClose.bind(this),
        handleSearchInput: this._handleSearchInput.bind(this),
        handleSearchEnter: this._handleSearchEnter.bind(this),
        showMobileSearchModal: this._showMobileSearchModal.bind(this),
        handleGlobalClick: this._handleGlobalClick.bind(this)
      };
    }
    init() {
      if (!this.elements.container) return;
      this.elements.container.addEventListener("click", this.boundListeners.handleContainerClick);
      this.elements.paginationContainer?.addEventListener("click", this.boundListeners.handlePaginationClick);
      this.elements.selectAllCheckbox?.addEventListener("change", this.boundListeners.handleSelectAllChange);
      this.elements.batchActionPanel?.addEventListener("click", this.boundListeners.handleBatchActionClick);
      this.elements.desktopSearchInput?.addEventListener("input", this.boundListeners.handleSearchInput);
      this.elements.desktopSearchInput?.addEventListener("keydown", this.boundListeners.handleSearchEnter);
      this.elements.mobileSearchBtn?.addEventListener("click", this.boundListeners.showMobileSearchModal);
      document.addEventListener("click", this.boundListeners.handleDocumentClickForMenuClose);
      document.body.addEventListener("click", this.boundListeners.handleGlobalClick);
      const itemsPerPageSelect = this.elements.itemsPerPageSelect?.querySelector("select");
      if (itemsPerPageSelect) {
        itemsPerPageSelect.addEventListener("change", this.boundListeners.handleItemsPerPageChange);
        this.state.itemsPerPage = parseInt(itemsPerPageSelect.value, 10);
      }
      this.elements.statusFilterSelects.forEach((selectContainer) => {
        const actualSelect = selectContainer.querySelector("select");
        actualSelect?.addEventListener("change", this.boundListeners.handleStatusFilterChange);
      });
      this._renderMultifunctionMenu();
      this._renderQuickActionsMenu();
    }
    destroy() {
      console.log("Destroying ApiKeyList instance and cleaning up listeners.");
      this.elements.container.removeEventListener("click", this.boundListeners.handleContainerClick);
      this.elements.paginationContainer?.removeEventListener("click", this.boundListeners.handlePaginationClick);
      this.elements.selectAllCheckbox?.removeEventListener("change", this.boundListeners.handleSelectAllChange);
      this.elements.batchActionPanel?.removeEventListener("click", this.boundListeners.handleBatchActionClick);
      this.elements.desktopSearchInput?.removeEventListener("input", this.boundListeners.handleSearchInput);
      this.elements.desktopSearchInput?.removeEventListener("keydown", this.boundListeners.handleSearchEnter);
      this.elements.mobileSearchBtn?.removeEventListener("click", this.boundListeners.showMobileSearchModal);
      document.removeEventListener("click", this.boundListeners.handleDocumentClickForMenuClose);
      document.body.removeEventListener("click", this.boundListeners.handleGlobalClick);
      const itemsPerPageSelect = this.elements.itemsPerPageSelect?.querySelector("select");
      if (itemsPerPageSelect) {
        itemsPerPageSelect.removeEventListener("change", this.boundListeners.handleItemsPerPageChange);
      }
      this.elements.statusFilterSelects.forEach((selectContainer) => {
        const actualSelect = selectContainer.querySelector("select");
        actualSelect?.removeEventListener("change", this.boundListeners.handleStatusFilterChange);
      });
      this.debouncedSearch.cancel?.();
      this.elements.container.innerHTML = "";
    }
    _handleItemsPerPageChange(e) {
      this.state.itemsPerPage = parseInt(e.target.value, 10);
      this.state.currentPage = 1;
      this.loadApiKeys(this.state.activeGroupId, true);
    }
    _handleStatusFilterChange(e) {
      this.state.filterStatus = e.target.value;
      this.state.currentPage = 1;
      this.loadApiKeys(this.state.activeGroupId, true);
    }
    _handleDocumentClickForMenuClose(event) {
      if (!event.target.closest(".api-card")) {
        this._closeAllActionMenus();
      }
    }
    _handleGlobalClick(event) {
      this._handleQuickActionClick(event);
      this._handleMultifunctionMenuClick(event);
      this._handleDropdownToggle(event);
    }
    /**
     * Updates the active group context for the manager.
     * @param {number} groupId The new active group ID.
     */
    setActiveGroup(groupId, groupName) {
      this.state.activeGroupId = groupId;
      this.state.activeGroupName = groupName || "";
      this.state.currentPage = 1;
    }
    /**
     * Fetches and renders API keys for the specified group.
     * @param {number} groupId - The ID of the group to load keys for.
     * @param {boolean} [force=false] - If true, bypasses the cache and fetches from the server.
     */
    async loadApiKeys(groupId, force = false) {
      this.state.selectedKeyIds.clear();
      if (!groupId) {
        this.state.currentKeys = [];
        this.render();
        return;
      }
      this.state.isApiKeysLoading = true;
      this.render();
      try {
        const { currentPage, itemsPerPage, filterStatus, searchText } = this.state;
        const params = {
          page: currentPage,
          limit: itemsPerPage
        };
        if (filterStatus !== "all") {
          params.status = filterStatus;
        }
        if (searchText && searchText.trim() !== "") {
          params.keyword = searchText.trim();
        }
        const pageData = await apiKeyManager.getKeysForGroup(groupId, params);
        this.state.currentKeys = pageData.items;
        this.state.totalItems = pageData.total;
        this.state.totalPages = pageData.pages;
        this.state.currentPage = pageData.page;
      } catch (error) {
        toastManager.show(`\u52A0\u8F7DAPI Keys\u5931\u8D25: ${error.message || "\u672A\u77E5\u9519\u8BEF"}`, "error");
        this.state.currentKeys = [];
        this.state.totalItems = 0;
        this.state.totalPages = 1;
      } finally {
        this.state.isApiKeysLoading = false;
        this.render();
      }
    }
    /**
     * Renders the list of API keys based on the current state.
     */
    render() {
      if (this.state.isApiKeysLoading && this.state.currentKeys.length === 0) {
        this.elements.container.innerHTML = '<div class="flex items-center justify-center h-full text-zinc-500"><p>\u6B63\u5728\u52A0\u8F7D API Keys...</p></div>';
        if (this.elements.paginationContainer) this.elements.paginationContainer.innerHTML = "";
        return;
      }
      if (!this.state.activeGroupId) {
        this.elements.container.innerHTML = '<div class="flex items-center justify-center h-full text-zinc-500"><p>\u8BF7\u5148\u5728\u5DE6\u4FA7\u9009\u62E9\u4E00\u4E2A\u5206\u7EC4</p></div>';
        if (this.elements.paginationContainer) this.elements.paginationContainer.innerHTML = "";
        return;
      }
      if (this.state.currentKeys.length === 0) {
        this.elements.container.innerHTML = '<div class="flex items-center justify-center h-full text-zinc-500"><p>\u8BE5\u5206\u7EC4\u4E0B\u8FD8\u6CA1\u6709 API Key\u3002</p></div>';
        if (this.elements.paginationContainer) this.elements.paginationContainer.innerHTML = "";
        return;
      }
      const listHtml = this.state.currentKeys.map((apiKey) => this._createApiKeyCardHtml(apiKey)).join("");
      this.elements.container.innerHTML = `<div class="grid grid-cols-1 md:grid-cols-2 gap-3">${listHtml}</div>`;
      this.elements.gridContainer = this.elements.container.firstChild;
      if (this.elements.paginationContainer) {
        this.elements.paginationContainer.innerHTML = this._createPaginationHtml();
      }
      this._updateAllStatusIndicators();
      this._syncCardCheckboxes();
      this._syncSelectionUI();
    }
    // [NEW] Handles clicks on pagination buttons
    handlePaginationClick(event) {
      const button = event.target.closest("button[data-page]");
      if (!button || button.disabled) return;
      const newPage = parseInt(button.dataset.page, 10);
      if (newPage !== this.state.currentPage) {
        this.state.currentPage = newPage;
        this.loadApiKeys(this.state.activeGroupId, true);
      }
    }
    // [NEW] Generates the HTML for the pagination controls
    _createPaginationHtml() {
      const { currentPage, totalPages } = this.state;
      if (totalPages < 1) return "";
      const baseButtonClasses = "pagination-button px-3 py-1 rounded text-sm transition-colors duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed";
      const activeClasses = "bg-zinc-500 text-white font-semibold";
      const inactiveClasses = "hover:bg-zinc-200 dark:hover:bg-zinc-700";
      let html = "";
      const prevDisabled = currentPage <= 1 ? "disabled" : "";
      const nextDisabled = currentPage >= totalPages ? "disabled" : "";
      html += `<button class="${baseButtonClasses} ${inactiveClasses}" data-page="${currentPage - 1}" ${prevDisabled}>
                    <i class="fas fa-chevron-left"></i>
                </button>`;
      const pagesToShow = this._getPaginationPages(currentPage, totalPages);
      pagesToShow.forEach((page) => {
        if (page === "...") {
          html += `<span class="px-3 py-1 text-zinc-400 dark:text-zinc-500 text-sm">...</span>`;
        } else {
          const pageClasses = page === currentPage ? activeClasses : inactiveClasses;
          html += `<button class="${baseButtonClasses} ${pageClasses}" data-page="${page}">${page}</button>`;
        }
      });
      html += `<button class="${baseButtonClasses} ${inactiveClasses}" data-page="${currentPage + 1}" ${nextDisabled}>
                    <i class="fas fa-chevron-right"></i>
                </button>`;
      return html;
    }
    // [NEW] Helper to determine which page numbers to show in pagination (e.g., 1 ... 5 6 7 ... 12)
    _getPaginationPages(current, total, width = 2) {
      if (total <= width * 2 + 3) {
        return Array.from({ length: total }, (_, i) => i + 1);
      }
      const pages = [1];
      if (current > width + 2) pages.push("...");
      for (let i = Math.max(2, current - width); i <= Math.min(total - 1, current + width); i++) {
        pages.push(i);
      }
      if (current < total - width - 1) pages.push("...");
      pages.push(total);
      return pages;
    }
    /**
     * Handles all actions originating from within an API key card.
     * @param {Event} event - The click event.
     */
    async handleCardAction(event) {
      const button = event.target.closest("button[data-action]");
      if (!button) return;
      const action = button.dataset.action;
      const card = button.closest(".api-card");
      if (!card) return;
      if (action === "toggle-menu") {
        const menu = card.querySelector('[data-menu="actions"]');
        if (menu) {
          const isMenuOpen = !menu.classList.contains("hidden");
          this._closeAllActionMenus(card);
          if (!isMenuOpen) {
            menu.classList.remove("hidden");
          }
        }
        return;
      }
      this._closeAllActionMenus();
      const keyId = parseInt(card.dataset.keyId, 10);
      const groupId = this.state.activeGroupId;
      const menuToClose = card.querySelector('[data-menu="actions"]');
      if (menuToClose && !menuToClose.classList.contains("hidden")) {
        this._closeAllActionMenus();
      }
      const apiKeyData = this.state.currentKeys.find((key) => key.id === keyId);
      if (!apiKeyData) {
        toastManager.show("\u9519\u8BEF: \u627E\u4E0D\u5230\u8BE5Key\u7684\u6570\u636E\u3002\u53EF\u80FD\u662F\u5217\u8868\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u5C1D\u8BD5\u5237\u65B0\u3002", "error");
        return;
      }
      const fullApiKey = apiKeyData.api_key;
      switch (action) {
        case "toggle-visibility":
        case "copy-key":
          this._handleLocalCardActions(action, button, card, fullApiKey);
          break;
        case "set-status": {
          const newStatus = button.dataset.newStatus;
          if (!newStatus) return;
          try {
            await apiKeyManager.updateKeyStatusInGroup(groupId, keyId, newStatus);
            toastManager.show(`Key \u72B6\u6001\u5DF2\u6210\u529F\u66F4\u65B0\u4E3A ${newStatus}`, "success");
          } catch (error) {
            handleApiError(error, toastManager);
          } finally {
            await this.loadApiKeys(groupId, true);
          }
          break;
        }
        case "revalidate": {
          const revalidateTask = this._createRevalidateTaskDefinition(groupId, fullApiKey);
          taskCenterManager.startTask(revalidateTask);
          break;
        }
        case "delete-key": {
          const result = await Swal.fire({
            target: "#main-content-wrapper",
            width: "20rem",
            backdrop: `rgba(0,0,0,0.5)`,
            heightAuto: false,
            customClass: {
              popup: `swal2-custom-style ${document.documentElement.classList.contains("dark") ? "swal2-dark" : ""}`
            },
            title: "\u786E\u8BA4\u5220\u9664",
            html: `\u786E\u5B9A\u8981\u4ECE <b>\u5F53\u524D\u5206\u7EC4</b> \u4E2D\u79FB\u9664\u8FD9\u4E2AKey\u5417\uFF1F`,
            //icon: 'warning',
            showCancelButton: true,
            confirmButtonText: "\u786E\u8BA4",
            cancelButtonText: "\u53D6\u6D88",
            reverseButtons: true,
            confirmButtonColor: "#d33",
            cancelButtonColor: "#6b7280",
            focusConfirm: false,
            focusCancel: false
          });
          if (!result.isConfirmed) {
            return;
          }
          try {
            const unlinkResult = await apiKeyManager.unlinkKeysFromGroup(groupId, [fullApiKey]);
            if (!unlinkResult.success) {
              throw new Error(unlinkResult.message || "\u540E\u7AEF\u672A\u80FD\u79FB\u9664Key\u3002");
            }
            toastManager.show(`\u6210\u529F\u79FB\u9664 1 \u4E2AKey\u3002`, "success");
            await this.loadApiKeys(groupId, true);
          } catch (error) {
            const errorMessage = error && error.message ? error.message : ERROR_MESSAGES["DEFAULT"];
            toastManager.show(`\u79FB\u9664Key\u5931\u8D25: ${errorMessage}`, "error");
            await this.loadApiKeys(groupId, true);
          }
          break;
        }
      }
    }
    // --- Private Helper Methods (copied from original file) ---
    _createRevalidateTaskDefinition(groupId, fullApiKey) {
      return {
        start: () => apiKeyManager.revalidateKeys(groupId, [fullApiKey]),
        poll: (taskId) => apiKeyManager.getTaskStatus(taskId, { noCache: true }),
        onSuccess: (data) => {
          toastManager.show(`Key\u9A8C\u8BC1\u5B8C\u6210`, "success");
          this.loadApiKeys(groupId, true);
        },
        onError: (data) => {
          toastManager.show(`\u9A8C\u8BC1\u4EFB\u52A1\u5931\u8D25: ${data.error || "\u672A\u77E5\u9519\u8BEF"}`, "error");
        },
        renderToastNarrative: (data, oldData, toastManager2) => {
          const toastId = `task-${data.id}`;
          const progress = data.total > 0 ? data.processed / data.total * 100 : 0;
          toastManager2.showProgressToast(toastId, `\u6B63\u5728\u9A8C\u8BC1Key`, "\u5904\u7406\u4E2D", progress);
        },
        renderTaskCenterItem: (data, timestamp, formatTimeAgo) => {
          const timeAgo = formatTimeAgo(timestamp);
          const maskedKey = escapeHTML(`${fullApiKey.substring(0, 4)}...${fullApiKey.substring(fullApiKey.length - 4)}`);
          let contentHtml = "";
          const isDone = !data.is_running;
          if (isDone) {
            if (data.error) {
              const safeError = escapeHTML(data.error);
              contentHtml = `
                            <div class="task-item-main">
                                <div class="task-item-icon-summary text-red-500"><i class="fas fa-exclamation-triangle"></i></div>
                                <div class="task-item-content flex-grow">
                                    <p class="task-item-title">\u9A8C\u8BC1\u4EFB\u52A1\u51FA\u9519: ${maskedKey}</p>
                                    <p class="task-item-status text-red-500 truncate" title="${safeError}">${safeError}</p>
                                </div>
                            </div>`;
            } else {
              const result = data.result?.results?.[0];
              const isSuccess = result?.status === "valid";
              const iconClass = isSuccess ? "text-green-500 fas fa-check-circle" : "text-red-500 fas fa-times-circle";
              const title = isSuccess ? "\u9A8C\u8BC1\u6210\u529F" : "\u9A8C\u8BC1\u5931\u8D25";
              const safeMessage = escapeHTML(result?.message || "\u6CA1\u6709\u8BE6\u7EC6\u4FE1\u606F\u3002");
              contentHtml = `
                            <div class="task-item-main">
                                <div class="task-item-icon-summary"><i class="${iconClass}"></i></div>
                                <div class="task-item-content flex-grow">
                                    <p class="task-item-title">${title}: ${maskedKey}</p>
                                    <p class="task-item-status truncate" title="${safeMessage}">${safeMessage}</p>
                                </div>
                            </div>
                        `;
            }
          } else {
            contentHtml = `
                        <div class="task-item-main gap-3">
                            <div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">\u6B63\u5728\u9A8C\u8BC1: ${maskedKey}</p>
                                <p class="task-item-status">\u8FD0\u884C\u4E2D... (${data.processed}/${data.total})</p>
                            </div>
                        </div>`;
          }
          return `${contentHtml}<div class="task-item-timestamp">${timeAgo}</div>`;
        }
      };
    }
    /**
    * Creates the HTML for a single API key card, adapting to the flat APIKeyDetails structure.
    * @param {object} item - The APIKeyDetails object from the API.
    * @returns {string} The HTML string for the card.
    */
    _createApiKeyCardHtml(item) {
      if (!item || !item.api_key) return "";
      const maskedKey = escapeHTML(`${item.api_key.substring(0, 4)}......${item.api_key.substring(item.api_key.length - 4)}`);
      const status = escapeHTML(item.status);
      const errorCount = escapeHTML(item.consecutive_error_count);
      const keyId = escapeHTML(item.id);
      const mappingId = escapeHTML(`${item.api_key_id}-${item.key_group_id}`);
      const setActiveAction = `data-action="set-status" data-new-status="ACTIVE"`;
      const revalidateAction = `data-action="revalidate"`;
      const disableAction = `data-action="set-status" data-new-status="DISABLED"`;
      const deleteAction = `data-action="delete-key"`;
      return `
            <div class="api-card group relative flex items-center gap-x-3 rounded-lg p-3 bg-white dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700/60" 
                data-status="${status}" 
                data-key-id="${keyId}" 
                data-mapping-id="${mappingId}">
                <input type="checkbox" class="api-key-checkbox h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 shrink-0">
                <span data-status-indicator class="w-2 h-2 rounded-full shrink-0"></span>
                <div class="flex-grow min-w-0">
                    <p class="font-mono text-xs font-semibold truncate">${maskedKey}</p>
                    <p class="text-xs text-zinc-400 mt-1">\u5931\u8D25: ${errorCount} \u6B21</p>
                </div>
                <!-- [DESKTOP ONLY] \u5FEB\u901F\u64CD\u4F5C\u6309\u94AE - \u5728\u79FB\u52A8\u7AEF(<lg)\u9690\u85CF -->
                <div class="hidden lg:flex items-center gap-x-2 text-zinc-400 text-xs z-10">
                    <button class="hover:text-blue-500" data-action="toggle-visibility" title="\u67E5\u770B\u5B8C\u6574Key"><i class="fas fa-eye"></i></button>
                    <button class="hover:text-blue-500" data-action="copy-key" title="\u590D\u5236Key"><i class="fas fa-copy"></i></button>
                </div>
                <!-- [DESKTOP ONLY] Hover Menu - \u5728\u79FB\u52A8\u7AEF(<lg)\u9690\u85CF -->
                <div class="hidden lg:flex absolute right-14 top-1/2 -translate-y-1/2 items-center bg-zinc-200 dark:bg-zinc-700 rounded-full shadow-md opacity-0 lg:group-hover:opacity-100 transition-opacity duration-200 z-20">
                    <button class="px-2 py-1 hover:text-green-500" ${setActiveAction} title="\u8BBE\u4E3A\u53EF\u7528"><i class="fas fa-check-circle"></i></button>
                    <button class="px-2 py-1 hover:text-blue-500" ${revalidateAction} title="\u91CD\u65B0\u9A8C\u8BC1"><i class="fas fa-sync-alt"></i></button>
                    <button class="px-2 py-1 hover:text-yellow-500" ${disableAction} title="\u7981\u7528"><i class="fas fa-ban"></i></button>
                    <button class="px-2 py-1 hover:text-red-500" ${deleteAction} title="\u4ECE\u5206\u7EC4\u4E2D\u79FB\u9664"><i class="fas fa-trash-alt"></i></button>
                </div>
                <!-- [MOBILE ONLY] Kebab Menu and Dropdown - \u5728\u684C\u9762\u7AEF(>=lg)\u9690\u85CF -->
                <div class="relative lg:hidden">
                    <button data-action="toggle-menu" class="flex items-center justify-center h-8 w-8 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-500" title="\u66F4\u591A\u64CD\u4F5C">
                        <i class="fas fa-ellipsis-v"></i>
                    </button>
                    <div data-menu="actions" class="absolute right-0 mt-2 w-48 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-md shadow-lg z-30 hidden">
                        <!-- [NEW] "\u67E5\u770B"\u548C"\u590D\u5236"\u64CD\u4F5C\u5DF2\u79FB\u5165\u79FB\u52A8\u7AEF\u83DC\u5355 -->
                        <button class="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-x-3" data-action="toggle-visibility"><i class="fas fa-eye w-4"></i> \u67E5\u770B/\u9690\u85CF Key</button>
                        <button class="w-full text-left px-4 py-2 text-sm text-zinc-700 dark:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-x-3" data-action="copy-key"><i class="fas fa-copy w-4"></i> \u590D\u5236 Key</button>
                        <div class="border-t border-zinc-200 dark:border-zinc-700 my-1"></div>
                        <button class="w-full text-left px-4 py-2 text-sm text-green-600 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-x-3" ${setActiveAction}><i class="fas fa-check-circle w-4"></i> \u8BBE\u4E3A\u53EF\u7528</button>
                        <button class="w-full text-left px-4 py-2 text-sm text-blue-600 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-x-3" ${revalidateAction}><i class="fas fa-sync-alt w-4"></i> \u91CD\u65B0\u9A8C\u8BC1</button>
                        <button class="w-full text-left px-4 py-2 text-sm text-yellow-600 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-x-3" ${disableAction}><i class="fas fa-ban w-4"></i> \u7981\u7528</button>
                        <div class="border-t border-zinc-200 dark:border-zinc-700 my-1"></div>
                        <button class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-zinc-100 dark:hover:bg-zinc-700 flex items-center gap-x-3" ${deleteAction}><i class="fas fa-trash-alt w-4"></i> \u4ECE\u5206\u7EC4\u4E2D\u79FB\u9664</button>
                    </div>
                </div>
            </div>
        `;
    }
    _handleLocalCardActions(action, button, card, fullApiKey) {
      switch (action) {
        case "toggle-visibility": {
          const safeApiKey = escapeHTML(fullApiKey);
          Swal.fire({
            target: "#main-content-wrapper",
            width: "24rem",
            // 适配移动端宽度
            backdrop: `rgba(0,0,0,0.5)`,
            heightAuto: false,
            customClass: {
              popup: `swal2-custom-style rounded-xl ${document.documentElement.classList.contains("dark") ? "swal2-dark" : ""}`,
              htmlContainer: "m-0 text-left"
              // 移除默认边距
            },
            showConfirmButton: false,
            showCloseButton: false,
            html: `
                            <div class="mt-2 flex items-center justify-between rounded-md bg-zinc-100 dark:bg-zinc-700 p-3 font-mono text-sm text-zinc-800 dark:text-zinc-200">
                                <code class="break-all">${safeApiKey}</code>
                                <button id="swal-copy-key-btn" class="ml-4 p-2 rounded-md hover:bg-zinc-200 dark:hover:bg-zinc-600 text-zinc-500 dark:text-zinc-300" title="\u590D\u5236">
                                    <i class="far fa-copy"></i>
                                </button>
                            </div>
                        `,
            didOpen: (modal) => {
              const copyBtn = modal.querySelector("#swal-copy-key-btn");
              if (copyBtn) {
                copyBtn.addEventListener("click", () => {
                  navigator.clipboard.writeText(fullApiKey).then(() => {
                    toastManager.show("API Key \u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\u3002", "success");
                    copyBtn.innerHTML = '<i class="fas fa-check text-green-500"></i>';
                    setTimeout(() => {
                      copyBtn.innerHTML = '<i class="far fa-copy"></i>';
                    }, 1500);
                  }).catch((err) => {
                    toastManager.show(`\u590D\u5236\u5931\u8D25: ${err.message}`, "error");
                  });
                });
              }
            }
          });
          break;
        }
        case "copy-key": {
          if (!fullApiKey) {
            toastManager.show("\u65E0\u6CD5\u627E\u5230\u5B8C\u6574\u7684Key\u7528\u4E8E\u590D\u5236\u3002", "error");
            break;
          }
          navigator.clipboard.writeText(fullApiKey).then(() => {
            toastManager.show("API Key \u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\u3002", "success");
          }).catch((err) => {
            toastManager.show(`\u590D\u5236\u5931\u8D25: ${err.message}`, "error");
          });
          break;
        }
      }
    }
    _updateAllStatusIndicators() {
      const allCards = this.elements.container.querySelectorAll(".api-card[data-status]");
      allCards.forEach((card) => this._updateApiKeyStatusIndicator(card));
    }
    _updateApiKeyStatusIndicator(cardElement) {
      const status = cardElement.dataset.status;
      if (!status) return;
      const indicator = cardElement.querySelector("[data-status-indicator]");
      if (!indicator) return;
      const statusColors = {
        "ACTIVE": "bg-green-500",
        "PENDING": "bg-gray-400",
        "COOLDOWN": "bg-yellow-500",
        "DISABLED": "bg-orange-500",
        "BANNED": "bg-red-500"
      };
      Object.values(statusColors).forEach((colorClass) => indicator.classList.remove(colorClass));
      if (statusColors[status]) {
        indicator.classList.add(statusColors[status]);
      }
    }
    /**
     * [NEW HELPER] Closes all action menus, optionally ignoring one card.
     * @param {HTMLElement} [ignoreCard=null] - The card whose menu should not be closed.
     */
    _closeAllActionMenus(ignoreCard = null) {
      this.elements.container.querySelectorAll(".api-card").forEach((card) => {
        if (card === ignoreCard) return;
        const menu = card.querySelector('[data-menu="actions"]');
        if (menu) {
          menu.classList.add("hidden");
        }
      });
    }
    /**
     * [NEW] A central click handler for the entire list container.
     * It delegates clicks on checkboxes or action buttons to specific handlers.
     */
    _handleContainerClick(event) {
      const target = event.target;
      if (target.matches(".api-key-checkbox")) {
        this._handleSelectionChange(target);
        return;
      }
      const actionButton = target.closest("button[data-action]");
      if (actionButton) {
        this.handleCardAction(event);
        return;
      }
    }
    /**
     * [NEW] Handles a click on an individual API key's checkbox.
     * @param {HTMLInputElement} checkbox - The checkbox element that was clicked.
     */
    _handleSelectionChange(checkbox) {
      const card = checkbox.closest(".api-card");
      if (!card) return;
      const keyId = parseInt(card.dataset.keyId, 10);
      if (isNaN(keyId)) return;
      if (checkbox.checked) {
        this.state.selectedKeyIds.add(keyId);
      } else {
        this.state.selectedKeyIds.delete(keyId);
      }
      this._syncSelectionUI();
    }
    /**
     * [NEW] Synchronizes the UI elements based on the current selection state.
     * This includes the "Select All" checkbox and batch action buttons.
     */
    _syncSelectionUI() {
      if (!this.elements.selectAllCheckbox || !this.elements.batchActionButton) return;
      const selectedCount = this.state.selectedKeyIds.size;
      const visibleKeysCount = this.state.currentKeys.length;
      if (selectedCount === 0) {
        this.elements.selectAllCheckbox.checked = false;
        this.elements.selectAllCheckbox.indeterminate = false;
      } else if (selectedCount < visibleKeysCount) {
        this.elements.selectAllCheckbox.checked = false;
        this.elements.selectAllCheckbox.indeterminate = true;
      } else if (selectedCount === visibleKeysCount && visibleKeysCount > 0) {
        this.elements.selectAllCheckbox.checked = true;
        this.elements.selectAllCheckbox.indeterminate = false;
      }
      const isDisabled = selectedCount === 0;
      if (isDisabled) {
        this.elements.batchActionButton.classList.add("is-disabled");
      } else {
        this.elements.batchActionButton.classList.remove("is-disabled");
      }
      this.elements.batchActionButton.style.pointerEvents = isDisabled ? "none" : "auto";
      this.elements.batchActionButton.style.opacity = isDisabled ? "0.5" : "1";
      const counter = this.elements.batchActionButton.querySelector("span");
      if (counter) {
        counter.textContent = isDisabled ? "\u6279\u91CF\u64CD\u4F5C" : `\u5DF2\u9009 ${selectedCount} \u9879`;
      }
    }
    /**
     * [NEW] Handles the change event of the main "Select All" checkbox.
     * @param {Event} event - The change event object.
     */
    _handleSelectAllChange(event) {
      const isChecked = event.target.checked;
      this.state.currentKeys.forEach((key) => {
        if (isChecked) {
          this.state.selectedKeyIds.add(key.id);
        } else {
          this.state.selectedKeyIds.delete(key.id);
        }
      });
      this._syncCardCheckboxes();
      this._syncSelectionUI();
    }
    /**
     * [NEW] Ensures that the checked status of each individual card's checkbox
     * matches the selection state stored in `this.state.selectedKeyIds`.
     */
    _syncCardCheckboxes() {
      if (!this.elements.gridContainer) return;
      const checkboxes = this.elements.gridContainer.querySelectorAll(".api-key-checkbox");
      checkboxes.forEach((checkbox) => {
        const card = checkbox.closest(".api-card");
        if (card) {
          const keyId = parseInt(card.dataset.keyId, 10);
          if (!isNaN(keyId)) {
            checkbox.checked = this.state.selectedKeyIds.has(keyId);
          }
        }
      });
    }
    /**
     * [NEW] Handles clicks within the batch action dropdown panel.
     * Uses event delegation to determine which action was triggered.
     * This version is adapted for the final HTML structure.
     * @param {Event} event - The click event.
     */
    _handleBatchActionClick(event) {
      const button = event.target.closest("button[data-batch-action]");
      if (!button) return;
      event.preventDefault();
      const customSelectContainer = button.closest(".custom-select");
      if (customSelectContainer) {
        const panel = customSelectContainer.querySelector(".custom-select-panel");
        if (panel) panel.classList.add("hidden");
      }
      const action = button.dataset.batchAction;
      const selectedIds = Array.from(this.state.selectedKeyIds);
      if (selectedIds.length === 0) {
        toastManager.show("\u6CA1\u6709\u9009\u4E2D\u4EFB\u4F55Key\u3002", "warning");
        return;
      }
      switch (action) {
        case "copy-to-clipboard":
          this._batchCopyToClipboard(selectedIds);
          break;
        case "set-status-active":
          this._batchSetStatus("ACTIVE", selectedIds);
          break;
        case "set-status-disabled":
          this._batchSetStatus("DISABLED", selectedIds);
          break;
        case "revalidate":
          this._batchRevalidate(selectedIds);
          break;
        case "delete":
          this._batchDelete(selectedIds);
          break;
        default:
          console.warn(`Unknown batch action: ${action}`);
      }
    }
    /**
     * [NEW] Helper for batch updating the status of selected keys.
     * @param {string} newStatus - The new status ('ACTIVE' or 'DISABLED').
     * @param {number[]} keyIds - An array of selected key IDs.
     */
    async _batchSetStatus(newStatus, keyIds) {
      const groupId = this.state.activeGroupId;
      const actionText = newStatus === "ACTIVE" ? "\u542F\u7528" : "\u7981\u7528";
      toastManager.show(`\u6B63\u5728\u6279\u91CF${actionText} ${keyIds.length} \u4E2AKey...`, "info", 3e3);
      try {
        const promises = keyIds.map((id) => apiKeyManager.updateKeyStatusInGroup(groupId, id, newStatus));
        const results = await Promise.allSettled(promises);
        const fulfilledCount = results.filter((r) => r.status === "fulfilled").length;
        const rejectedCount = results.length - fulfilledCount;
        let toastMessage = `\u6279\u91CF${actionText}\u64CD\u4F5C\u5B8C\u6210\u3002`;
        let toastType = "success";
        if (rejectedCount > 0) {
          toastMessage = `\u64CD\u4F5C\u5B8C\u6210: ${fulfilledCount} \u4E2A\u6210\u529F\uFF0C${rejectedCount} \u4E2A\u5931\u8D25\uFF08\u53EF\u80FD\u7531\u4E8EKey\u72B6\u6001\u9650\u5236\uFF09\u3002\u5217\u8868\u5DF2\u66F4\u65B0\u3002`;
          toastType = fulfilledCount > 0 ? "warning" : "error";
        }
        toastManager.show(toastMessage, toastType);
      } catch (error) {
        toastManager.show(`\u6279\u91CF${actionText}\u65F6\u53D1\u751F\u7F51\u7EDC\u9519\u8BEF: ${error.message || "\u672A\u77E5\u9519\u8BEF"}`, "error");
      } finally {
        await this.loadApiKeys(groupId, true);
      }
    }
    /**
     * [NEW] Helper for batch revalidating selected keys.
     * @param {number[]} keyIds - An array of selected key IDs.
     */
    _batchRevalidate(keyIds) {
      const groupId = this.state.activeGroupId;
      const currentKeysMap = new Map(this.state.currentKeys.map((key) => [key.id, key.api_key]));
      const keysToRevalidate = keyIds.map((id) => currentKeysMap.get(id)).filter(Boolean);
      if (keysToRevalidate.length === 0) {
        toastManager.show("\u627E\u4E0D\u5230\u5339\u914D\u7684Key\u8FDB\u884C\u9A8C\u8BC1\u3002\u8BF7\u5237\u65B0\u5217\u8868\u540E\u91CD\u8BD5\u3002", "error");
        return;
      }
      const revalidateTask = {
        start: () => apiKeyManager.revalidateKeys(groupId, keysToRevalidate),
        poll: (taskId) => apiKeyManager.getTaskStatus(taskId, { noCache: true }),
        onSuccess: (data) => {
          toastManager.show(`\u6279\u91CF\u9A8C\u8BC1\u5B8C\u6210\u3002`, "success");
          this.loadApiKeys(groupId, true);
        },
        onError: (data) => {
          toastManager.show(`\u6279\u91CF\u9A8C\u8BC1\u4EFB\u52A1\u5931\u8D25: ${data.error || "\u672A\u77E5\u9519\u8BEF"}`, "error");
        },
        renderToastNarrative: (data, oldData, toastManager2) => {
          const toastId = `task-${data.id}`;
          const progress = data.total > 0 ? data.processed / data.total * 100 : 0;
          toastManager2.showProgressToast(toastId, `\u6B63\u5728\u6279\u91CF\u9A8C\u8BC1Key`, `\u5904\u7406\u4E2D (${data.processed}/${data.total})`, progress);
        },
        // [MODIFIED] This is the core fix. A new, detailed renderer for the task center.
        renderTaskCenterItem: (data, timestamp, formatTimeAgo) => {
          const timeAgo = formatTimeAgo(timestamp);
          let contentHtml = "";
          if (data.is_running) {
            contentHtml = `
                        <div class="task-item-main gap-3">
                            <div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">\u6279\u91CF\u9A8C\u8BC1 ${data.total} \u4E2AKey</p>
                                <p class="task-item-status">\u8FD0\u884C\u4E2D... (${data.processed}/${data.total})</p>
                            </div>
                        </div>`;
          } else {
            if (data.error) {
              contentHtml = `
                            <div class="task-item-main">
                                <div class="task-item-icon-summary text-red-500"><i class="fas fa-exclamation-triangle"></i></div>
                                <div class="task-item-content flex-grow">
                                    <p class="task-item-title">\u6279\u91CF\u9A8C\u8BC1\u4EFB\u52A1\u51FA\u9519</p>
                                    <p class="task-item-status text-red-500 truncate" title="${data.error}">${data.error}</p>
                                </div>
                            </div>`;
            } else {
              const results = data.result?.results || [];
              const validCount = results.filter((r) => r.status === "valid").length;
              const invalidCount = results.length - validCount;
              const summaryTitle = `\u9A8C\u8BC1\u5B8C\u6210: ${validCount}\u4E2A\u6709\u6548, ${invalidCount}\u4E2A\u65E0\u6548`;
              const overallIconClass = invalidCount > 0 ? "text-yellow-500 fas fa-exclamation-triangle" : "text-green-500 fas fa-check-circle";
              const detailsHtml = results.map((result) => {
                const maskedKey = escapeHTML(`${result.key.substring(0, 4)}...${result.key.substring(result.key.length - 4)}`);
                const safeMessage = escapeHTML(result.message);
                if (result.status === "valid") {
                  return `
                                    <div class="flex items-start text-xs">
                                        <i class="fas fa-check-circle text-green-500 mt-0.5 mr-2"></i>
                                        <div class="flex-grow">
                                            <p class="font-mono">${maskedKey}</p>
                                            <p class="text-zinc-400">${safeMessage}</p>
                                        </div>
                                    </div>`;
                } else {
                  return `
                                    <div class="flex items-start text-xs">
                                        <i class="fas fa-times-circle text-red-500 mt-0.5 mr-2"></i>
                                        <div class="flex-grow">
                                            <p class="font-mono">${maskedKey}</p>
                                            <p class="text-zinc-400">${safeMessage}</p>
                                        </div>
                                    </div>`;
                }
              }).join("");
              contentHtml = `
                            <div class="task-item-main">
                                <div class="task-item-icon-summary"><i class="${overallIconClass}"></i></div>
                                <div class="task-item-content flex-grow">
                                    <div class="flex justify-between items-center cursor-pointer" data-task-toggle>
                                        <p class="task-item-title">${summaryTitle}</p>
                                        <i class="fas fa-chevron-down task-toggle-icon"></i>
                                    </div>
                                    <div class="task-details-content collapsed" data-task-content>
                                        <div class="task-details-body space-y-2 mt-2">
                                            ${detailsHtml}
                                        </div>
                                    </div>
                                </div>
                            </div>`;
            }
          }
          return `${contentHtml}<div class="task-item-timestamp">${timeAgo}</div>`;
        }
      };
      taskCenterManager.startTask(revalidateTask);
    }
    /**
     * [NEW] Helper for batch deleting (unlinking) selected keys from the group.
     * @param {number[]} keyIds - An array of selected key IDs.
     */
    async _batchDelete(keyIds) {
      const groupId = this.state.activeGroupId;
      const selectedCount = keyIds.length;
      const result = await Swal.fire({
        target: "#main-content-wrapper",
        width: "20rem",
        backdrop: `rgba(0,0,0,0.5)`,
        heightAuto: false,
        customClass: {
          popup: `swal2-custom-style ${document.documentElement.classList.contains("dark") ? "swal2-dark" : ""}`
        },
        title: "\u786E\u8BA4\u6279\u91CF\u79FB\u9664",
        html: `\u786E\u5B9A\u8981\u4ECE <b>\u5F53\u524D\u5206\u7EC4</b> \u4E2D\u79FB\u9664\u9009\u4E2D\u7684 <b>${selectedCount}</b> \u4E2AKey\u5417\uFF1F`,
        showCancelButton: true,
        confirmButtonText: "\u786E\u8BA4",
        cancelButtonText: "\u53D6\u6D88",
        reverseButtons: true,
        confirmButtonColor: "#d33",
        cancelButtonColor: "#6b7280"
      });
      if (!result.isConfirmed) return;
      const keysToDelete = this.state.currentKeys.filter((key) => keyIds.includes(key.id)).map((key) => key.api_key);
      if (keysToDelete.length === 0) {
        toastManager.show("\u627E\u4E0D\u5230\u5339\u914D\u7684Key\u8FDB\u884C\u79FB\u9664\u3002\u8BF7\u5237\u65B0\u5217\u8868\u540E\u91CD\u8BD5\u3002", "error");
        return;
      }
      try {
        const unlinkResult = await apiKeyManager.unlinkKeysFromGroup(groupId, keysToDelete);
        if (!unlinkResult.success) {
          throw new Error(unlinkResult.message || "\u540E\u7AEF\u672A\u80FD\u79FB\u9664Keys\u3002");
        }
        toastManager.show(`\u6210\u529F\u79FB\u9664 ${keysToDelete.length} \u4E2AKey\u3002`, "success");
        await this.loadApiKeys(groupId, true);
      } catch (error) {
        const errorMessage = error && error.message ? error.message : "\u672A\u77E5\u9519\u8BEF";
        toastManager.show(`\u6279\u91CF\u79FB\u9664Key\u5931\u8D25: ${errorMessage}`, "error");
        await this.loadApiKeys(groupId, true);
      }
    }
    /**
     * [NEW] Helper for copying all selected API keys to the clipboard.
     * @param {number[]} keyIds - An array of selected key IDs.
     */
    _batchCopyToClipboard(keyIds) {
      const currentKeysMap = new Map(this.state.currentKeys.map((key) => [key.id, key.api_key]));
      const keysToCopy = keyIds.map((id) => currentKeysMap.get(id)).filter(Boolean);
      if (keysToCopy.length === 0) {
        toastManager.show("\u6CA1\u6709\u627E\u5230\u53EF\u590D\u5236\u7684Key\u3002\u5217\u8868\u53EF\u80FD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u5237\u65B0\u3002", "warning");
        return;
      }
      const textToCopy = keysToCopy.join("\n");
      navigator.clipboard.writeText(textToCopy).then(() => {
        toastManager.show(`\u6210\u529F\u590D\u5236 ${keysToCopy.length} \u4E2AKey\u5230\u526A\u8D34\u677F\u3002`, "success");
      }).catch((err) => {
        console.error("Failed to copy keys to clipboard:", err);
        toastManager.show(`\u590D\u5236\u5931\u8D25: ${err.message}`, "error");
      });
    }
    /** [NEW] Shows the mobile search modal using SweetAlert2. */
    _showMobileSearchModal() {
      Swal.fire({
        target: "#main-content-wrapper",
        width: "24rem",
        backdrop: `rgba(0,0,0,0.5)`,
        heightAuto: false,
        customClass: {
          popup: `swal2-custom-style rounded-xl ${document.documentElement.classList.contains("dark") ? "swal2-dark" : ""}`,
          htmlContainer: "m-0"
        },
        showConfirmButton: false,
        // We don't need a confirm button
        showCloseButton: false,
        // A close button is user-friendly
        html: `
                <div class="flex items-center justify-between rounded-md bg-zinc-100 dark:bg-zinc-700 p-3 mt-2 font-mono text-sm text-zinc-800 dark:text-zinc-200">
                    <input type="text" id="swal-search-input" placeholder="\u641C\u7D22 API Key..." class="w-full focus:outline-none focus:ring-0 rounded-lg dark:placeholder-zinc-400" 
                           autocomplete="off">
                </div>
            `,
        didOpen: (modal) => {
          const input = modal.querySelector("#swal-search-input");
          if (!input) return;
          input.value = this.state.searchText;
          input.focus();
          input.addEventListener("input", this._handleSearchInput.bind(this));
          input.addEventListener("keydown", (event) => {
            this._handleSearchEnter.bind(this)(event);
            if (event.key === "Enter") {
              event.preventDefault();
              Swal.close();
            }
          });
        }
      });
    }
    /** [NEW] Central handler for any search input event. */
    _handleSearchInput(event) {
      this._updateSearchStateAndSyncInputs(event.target.value);
      this.debouncedSearch();
    }
    /** Synchronizes search text across UI state and inputs. */
    _updateSearchStateAndSyncInputs(value) {
      this.state.searchText = value;
      if (this.elements.desktopSearchInput && document.activeElement !== this.elements.desktopSearchInput) {
        this.elements.desktopSearchInput.value = value;
      }
    }
    /** [MODIFIED] Handles 'Enter' key press for immediate search. Bug fixed. */
    _handleSearchEnter(event) {
      if (event.key === "Enter") {
        event.preventDefault();
        this.debouncedSearch.cancel?.();
        this.state.currentPage = 1;
        this.loadApiKeys(this.state.activeGroupId, true);
      }
    }
    /** Hides the mobile search overlay */
    _hideMobileSearch() {
      this.elements.mobileSearchOverlay?.classList.add("hidden");
    }
    /**
    * A private helper that returns the complete quick actions configuration.
    * This is the single, authoritative source for all quick action menu data.
    * @returns {Array<Object>} The configuration array for quick actions.
    */
    _getQuickActionsConfiguration() {
      return [
        { action: "revalidate-invalid", text: "\u9A8C\u8BC1\u6240\u6709\u65E0\u6548Key", icon: "fa-rocket text-blue-500", requiresConfirm: false },
        { action: "revalidate-all", text: "\u9A8C\u8BC1\u6240\u6709Key", icon: "fa-sync-alt text-blue-500", requiresConfirm: true, confirmText: "\u6B64\u64CD\u4F5C\u5C06\u5BF9\u5F53\u524D\u5206\u7EC4\u4E0B\u7684 **\u6240\u6709Key** \u53D1\u8D77\u4E00\u6B21API\u8BF7\u6C42\u4EE5\u9A8C\u8BC1\u5176\u6709\u6548\u6027\uFF0C\u53EF\u80FD\u4F1A\u6D88\u8017\u5927\u91CF\u989D\u5EA6\u3002\u786E\u5B9A\u8981\u7EE7\u7EED\u5417\uFF1F" },
        { action: "restore-cooldown", text: "\u6062\u590D\u6240\u6709\u51B7\u5374\u4E2DKey", icon: "fa-undo text-green-500", requiresConfirm: false },
        { type: "divider" },
        { action: "cleanup-banned", text: "\u5220\u9664\u6240\u6709\u5931\u6548Key", icon: "fa-trash-alt", danger: true, requiresConfirm: true, confirmText: "\u786E\u5B9A\u8981\u4ECE\u5F53\u524D\u5206\u7EC4\u4E2D\u79FB\u9664 **\u6240\u6709** \u72B6\u6001\u4E3A `BANNED` \u7684Key\u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002" }
      ];
    }
    /**
     * Renders the content of the quick actions dropdown menu into both desktop and mobile panels.
     */
    _renderQuickActionsMenu() {
      const actions = this._getQuickActionsConfiguration();
      const menuHtml = actions.map((item) => {
        if (item.type === "divider") {
          return '<div class="menu-divider"></div>';
        }
        return `
                <button data-quick-action="${item.action}" class="menu-item ${item.danger ? "menu-item-danger" : ""} whitespace-nowrap">
                    <i class="fas ${item.icon} menu-item-icon"></i>
                    <span>${item.text}</span>
                </button>
            `;
      }).join("");
      const menuWrapper = `<div class="py-1">${menuHtml}</div>`;
      if (this.elements.desktopQuickActionsPanel) {
        this.elements.desktopQuickActionsPanel.innerHTML = menuWrapper;
      }
      if (this.elements.mobileQuickActionsPanel) {
        this.elements.mobileQuickActionsPanel.innerHTML = menuWrapper;
      }
    }
    /**
     * [NEW] Handles clicks on any quick action button.
     * @param {Event} event The click event.
     */
    async _handleQuickActionClick(event) {
      const button = event.target.closest("button[data-quick-action]");
      if (!button) return;
      event.preventDefault();
      const action = button.dataset.quickAction;
      const groupId = this.state.activeGroupId;
      if (!groupId) {
        toastManager.show("\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u5206\u7EC4\u3002", "warning");
        return;
      }
      const actionConfig = this._getQuickActionConfig(action);
      if (actionConfig && actionConfig.requiresConfirm) {
        const result = await Swal.fire({
          target: "#main-content-wrapper",
          title: "\u8BF7\u786E\u8BA4\u64CD\u4F5C",
          html: actionConfig.confirmText,
          icon: "warning",
          showCancelButton: true,
          confirmButtonText: "\u786E\u8BA4\u6267\u884C",
          cancelButtonText: "\u53D6\u6D88",
          reverseButtons: true,
          confirmButtonColor: "#d33",
          customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains("dark") ? "swal2-dark" : ""}` }
        });
        if (!result.isConfirmed) return;
      }
      this._executeQuickAction(action, groupId);
    }
    /** [NEW] Helper to retrieve action configuration. */
    _getQuickActionConfig(action) {
      const actions = this._getQuickActionsConfiguration();
      return actions.find((a) => a.action === action);
    }
    /**
     * [MODIFIED] Executes the quick action by building the correct payload for the unified bulk-actions endpoint.
     * @param {string} action The action to execute.
     * @param {number} groupId The current group ID.
     */
    async _executeQuickAction(action, groupId) {
      let taskDefinition;
      let payload;
      let taskTitle;
      switch (action) {
        case "revalidate-invalid":
          taskTitle = "\u9A8C\u8BC1\u5206\u7EC4\u65E0\u6548Key";
          payload = {
            action: "revalidate",
            filter: {
              // Backend should interpret 'invalid' as this set of statuses
              status: ["disabled", "cooldown", "banned"]
            }
          };
          break;
        case "revalidate-all":
          taskTitle = "\u9A8C\u8BC1\u5206\u7EC4\u6240\u6709Key";
          payload = {
            action: "revalidate",
            filter: { status: ["all"] }
          };
          break;
        case "restore-cooldown":
          taskTitle = "\u6062\u590D\u51B7\u5374\u4E2DKey";
          payload = {
            action: "set_status",
            new_status: "active",
            // The target status
            filter: { status: ["cooldown"] }
          };
          break;
        case "cleanup-banned":
          taskTitle = "\u6E05\u7406\u5931\u6548Key";
          payload = {
            action: "delete",
            filter: { status: ["banned"] }
          };
          break;
        default:
          toastManager.show(`\u672A\u77E5\u7684\u5FEB\u901F\u5904\u7F6E\u64CD\u4F5C: ${action}`, "error");
          return;
      }
      try {
        const response = await apiKeyManager.startGroupBulkActionTask(groupId, payload);
        if (response && response.id) {
          const startPromise = Promise.resolve(response);
          const taskDefinition2 = this._createGroupTaskDefinition(taskTitle, startPromise, groupId, action);
          taskCenterManager.startTask(taskDefinition2);
        } else {
          if (response && response.result && response.result.message) {
            toastManager.show(response.result.message, "info");
          } else {
            toastManager.show("\u64CD\u4F5C\u5DF2\u5B8C\u6210\uFF0C\u4F46\u65E0\u4EFB\u4F55\u9879\u76EE\u88AB\u66F4\u6539\u3002", "info");
          }
        }
      } catch (error) {
        handleApiError(error, toastManager, { prefix: `${taskTitle}\u65F6\u53D1\u751F\u610F\u5916\u9519\u8BEF: ` });
      }
    }
    /**
     * [NEW] Generic task definition factory for group-level operations.
     */
    _createGroupTaskDefinition(title, startPromise, groupId, action) {
      return {
        start: () => startPromise,
        poll: (taskId) => apiKeyManager.getTaskStatus(taskId, { noCache: true }),
        onSuccess: (data) => {
          toastManager.show(`${title}\u4EFB\u52A1\u5B8C\u6210\u3002`, "success");
          this.loadApiKeys(groupId, true);
        },
        onError: (data) => {
          const displayMessage = escapeHTML(data.error || "\u4EFB\u52A1\u6267\u884C\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF");
          toastManager.show(`${title}\u4EFB\u52A1\u5931\u8D25: ${displayMessage}`, "error");
        },
        renderToastNarrative: (data, oldData, toastManager2) => {
          const toastId = `task-${data.id}`;
          const progress = data.total > 0 ? data.processed / data.total * 100 : 0;
          toastManager2.showProgressToast(toastId, title, `\u5904\u7406\u4E2D (${data.processed}/${data.total})`, progress);
        },
        renderTaskCenterItem: (data, timestamp, formatTimeAgo) => {
          const timeAgo = formatTimeAgo(timestamp);
          let contentHtml = "";
          if (data.is_running) {
            contentHtml = `
                        <div class="task-item-main gap-3">
                            <div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">${title}</p>
                                <p class="task-item-status">\u8FD0\u884C\u4E2D... (${data.processed}/${data.total})</p>
                            </div>
                        </div>`;
          } else if (data.error) {
            const safeError = escapeHTML(data.error);
            contentHtml = `
                        <div class="task-item-main">
                            <div class="task-item-icon-summary text-red-500"><i class="fas fa-exclamation-triangle"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">${title}\u4EFB\u52A1\u51FA\u9519</p>
                                <p class="task-item-status text-red-500 truncate" title="${safeError}">${safeError}</p>
                            </div>
                        </div>`;
          } else {
            let summary = "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u3002";
            const result = data.result || {};
            const iconClass = "fas fa-check-circle text-green-500";
            switch (action) {
              case "cleanup-banned":
                summary = `\u6210\u529F\u6E05\u7406 ${result.unlinked_count || 0} \u4E2A\u5931\u6548Key\u3002`;
                break;
              case "restore-cooldown":
                if (result.skipped_count > 0) {
                  summary = `\u5B8C\u6210: ${result.updated_count || 0} \u4E2A\u5DF2\u6062\u590D, ${result.skipped_count} \u4E2A\u88AB\u8DF3\u8FC7\u3002`;
                } else {
                  summary = `\u6210\u529F\u6062\u590D ${result.updated_count || 0} \u4E2A\u51B7\u5374\u4E2DKey\u3002`;
                }
                break;
              case "revalidate-invalid":
              case "revalidate-all":
                const results = result.results || [];
                const validCount = results.filter((r) => r.status === "valid").length;
                const invalidCount = results.length - validCount;
                summary = `\u9A8C\u8BC1\u5B8C\u6210: ${validCount}\u4E2A\u6709\u6548, ${invalidCount}\u4E2A\u65E0\u6548\u3002`;
                break;
              default:
                summary = `\u5904\u7406\u4E86 ${data.processed} \u4E2AKey\u3002`;
            }
            const safeSummary = escapeHTML(summary);
            contentHtml = `
                        <div class="task-item-main">
                            <div class="task-item-icon-summary"><i class="${iconClass}"></i></div>
                            <div class="task-item-content flex-grow">
                                <p class="task-item-title">${title}</p>
                                <p class="task-item-status truncate" title="${safeSummary}">${safeSummary}</p>
                            </div>
                        </div>`;
          }
          return `${contentHtml}<div class="task-item-timestamp">${timeAgo}</div>`;
        }
      };
    }
    /**
     * [NEW] A generic handler to open/close any custom dropdown menu.
     * It uses data attributes to link triggers to panels.
     * @param {Event} event The click event.
     */
    _handleDropdownToggle(event) {
      const toggleButton = event.target.closest('[data-toggle="custom-select"]');
      if (!toggleButton) {
        this._closeAllDropdowns();
        return;
      }
      const targetSelector = toggleButton.dataset.target;
      const targetPanel = document.querySelector(targetSelector);
      if (!targetPanel) return;
      const isPanelOpen = !targetPanel.classList.contains("hidden");
      this._closeAllDropdowns(targetPanel);
      if (!isPanelOpen) {
        targetPanel.classList.remove("hidden");
      }
    }
    /**
     * [NEW] A helper utility to close all custom dropdown panels.
     * @param {HTMLElement} [excludePanel=null] An optional panel to exclude from closing.
     */
    _closeAllDropdowns(excludePanel = null) {
      const allPanels = document.querySelectorAll(".custom-select-panel");
      allPanels.forEach((panel) => {
        if (panel !== excludePanel) {
          panel.classList.add("hidden");
        }
      });
    }
    /**
     * [NEW] Renders the content of the multifunction (export) dropdown menu.
     */
    _renderMultifunctionMenu() {
      const actions = [
        { action: "export-all", text: "\u5BFC\u51FA\u6240\u6709Key", icon: "fa-file-export" },
        { action: "export-valid", text: "\u5BFC\u51FA\u6709\u6548Key", icon: "fa-file-alt" },
        { action: "export-invalid", text: "\u5BFC\u51FA\u65E0\u6548Key", icon: "fa-file-excel" }
      ];
      const menuHtml = actions.map((item) => {
        return `
                <button data-multifunction-action="${item.action}" class="menu-item whitespace-nowrap">
                    <i class="fas ${item.icon} menu-item-icon"></i>
                    <span>${item.text}</span>
                </button>
            `;
      }).join("");
      const menuWrapper = `<div class="py-1">${menuHtml}</div>`;
      if (this.elements.desktopMultifunctionPanel) {
        this.elements.desktopMultifunctionPanel.innerHTML = menuWrapper;
      }
      if (this.elements.mobileMultifunctionPanel) {
        this.elements.mobileMultifunctionPanel.innerHTML = menuWrapper;
      }
    }
    /**
     * [NEW] Handles clicks on any multifunction menu button.
     * @param {Event} event The click event.
     */
    async _handleMultifunctionMenuClick(event) {
      const button = event.target.closest("button[data-multifunction-action]");
      if (!button) return;
      event.preventDefault();
      this._closeAllDropdowns();
      const action = button.dataset.multifunctionAction;
      const groupId = this.state.activeGroupId;
      if (!groupId) {
        toastManager.show("\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u5206\u7EC4\u3002", "warning");
        return;
      }
      let statuses = [];
      let filename = `${this.state.activeGroupName}_keys.txt`;
      switch (action) {
        case "export-all":
          statuses = ["all"];
          filename = `${this.state.activeGroupName}_all_keys.txt`;
          break;
        case "export-valid":
          statuses = ["active", "cooldown"];
          filename = `${this.state.activeGroupName}_valid_keys.txt`;
          break;
        case "export-invalid":
          statuses = ["disabled", "banned"];
          filename = `${this.state.activeGroupName}_invalid_keys.txt`;
          break;
        default:
          return;
      }
      toastManager.show("\u6B63\u5728\u51C6\u5907\u5BFC\u51FA\u6570\u636E...", "info");
      try {
        const keys = await apiKeyManager.exportKeysForGroup(groupId, statuses);
        if (keys.length === 0) {
          toastManager.show("\u6CA1\u6709\u627E\u5230\u7B26\u5408\u6761\u4EF6\u7684Key\u53EF\u4F9B\u5BFC\u51FA\u3002", "warning");
          return;
        }
        this._triggerTextFileDownload(keys.join("\n"), filename);
        toastManager.show(`\u6210\u529F\u5BFC\u51FA ${keys.length} \u4E2AKey\u3002`, "success");
      } catch (error) {
        toastManager.show(`\u5BFC\u51FA\u5931\u8D25: ${error.message}`, "error");
      }
    }
    /**
     * [NEW] A utility function to trigger a text file download in the browser.
     * @param {string} content The content of the file.
     * @param {string} filename The desired name of the file.
     */
    _triggerTextFileDownload(content, filename) {
      const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }
  };
  var apiKeyList_default = ApiKeyList;

  // frontend/js/pages/keys/index.js
  var KeyGroupsPage = class {
    constructor() {
      this.state = {
        groups: [],
        groupDetailsCache: {},
        activeGroupId: null,
        isLoading: true
      };
      this.debouncedSaveOrder = debounce(this.saveGroupOrder.bind(this), 1500);
      this.elements = {
        dashboardTitle: document.querySelector("#group-dashboard h2"),
        dashboardControls: document.querySelector("#group-dashboard .flex.items-center.gap-x-3"),
        apiListContainer: document.getElementById("api-list-container"),
        groupListCollapsible: document.getElementById("group-list-collapsible"),
        desktopGroupContainer: document.querySelector("#desktop-group-cards-list .card-list-content"),
        mobileGroupContainer: document.getElementById("mobile-group-cards-list"),
        addGroupBtnContainer: document.getElementById("add-group-btn-container"),
        groupMenuToggle: document.getElementById("group-menu-toggle"),
        mobileActiveGroupDisplay: document.querySelector(".mobile-group-selector > div")
      };
      this.initialized = this.elements.desktopGroupContainer !== null && this.elements.apiListContainer !== null;
      if (this.initialized) {
        this.apiKeyList = new apiKeyList_default(this.elements.apiListContainer);
      }
      const allowedModelsInput = new TagInput(document.getElementById("allowed-models-container"), {
        validator: /^[a-z0-9\.-]+$/,
        validationMessage: "\u65E0\u6548\u7684\u6A21\u578B\u683C\u5F0F"
      });
      const allowedUpstreamsInput = new TagInput(document.getElementById("allowed-upstreams-container"), {
        validator: /^(https?:\/\/)?[\w\.-]+\.[a-z]{2,}(\/[\w\.-]*)*\/?$/i,
        validationMessage: "\u65E0\u6548\u7684 URL \u683C\u5F0F"
      });
      const allowedTokensInput = new TagInput(document.getElementById("allowed-tokens-container"), {
        validator: /.+/,
        validationMessage: "\u4EE4\u724C\u4E0D\u80FD\u4E3A\u7A7A"
      });
      this.keyGroupModal = new KeyGroupModal({
        onSave: this.handleSaveGroup.bind(this),
        tagInputInstances: {
          models: allowedModelsInput,
          upstreams: allowedUpstreamsInput,
          tokens: allowedTokensInput
        }
      });
      this.deleteGroupModal = new DeleteGroupModal({
        onDeleteSuccess: (deletedGroupId) => {
          if (this.state.activeGroupId === deletedGroupId) {
            this.state.activeGroupId = null;
            this.apiKeyList.loadApiKeys(null);
          }
          this.loadKeyGroups(true);
        }
      });
      this.addApiModal = new AddApiModal({
        onImportSuccess: () => this.apiKeyList.loadApiKeys(this.state.activeGroupId, true)
      });
      this.cloneGroupModal = new CloneGroupModal({
        onCloneSuccess: (clonedGroup) => {
          if (clonedGroup && clonedGroup.id) {
            this.state.activeGroupId = clonedGroup.id;
          }
          this.loadKeyGroups(true);
        }
      });
      this.deleteApiModal = new DeleteApiModal({
        onDeleteSuccess: () => this.apiKeyList.loadApiKeys(this.state.activeGroupId, true)
      });
      this.requestSettingsModal = new RequestSettingsModal({
        onSave: this.handleSaveRequestSettings.bind(this)
      });
      this.activeTooltip = null;
    }
    async init() {
      if (!this.initialized) {
        console.error("KeyGroupsPage: Could not initialize. Essential container elements like 'desktopGroupContainer' or 'apiListContainer' are missing from the DOM.");
        return;
      }
      this.initEventListeners();
      if (this.apiKeyList) {
        this.apiKeyList.init();
      }
      await this.loadKeyGroups();
    }
    initEventListeners() {
      document.body.addEventListener("click", (event) => {
        const addGroupBtn = event.target.closest(".add-group-btn");
        const addApiBtn = event.target.closest("#add-api-btn");
        const deleteApiBtn = event.target.closest("#delete-api-btn");
        if (addGroupBtn) this.keyGroupModal.open();
        if (addApiBtn) this.addApiModal.open(this.state.activeGroupId);
        if (deleteApiBtn) this.deleteApiModal.open(this.state.activeGroupId);
      });
      this.elements.dashboardControls?.addEventListener("click", (event) => {
        const button = event.target.closest("button[data-action]");
        if (!button) return;
        const action = button.dataset.action;
        const activeGroup = this.state.groups.find((g) => g.id === this.state.activeGroupId);
        switch (action) {
          case "edit-group":
            if (activeGroup) {
              this.openEditGroupModal(activeGroup.id);
            } else {
              alert("\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u5206\u7EC4\u8FDB\u884C\u7F16\u8F91\u3002");
            }
            break;
          case "open-settings":
            this.openRequestSettingsModal();
            break;
          case "clone-group":
            if (activeGroup) {
              this.cloneGroupModal.open(activeGroup);
            } else {
              alert("\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u5206\u7EC4\u8FDB\u884C\u514B\u9686\u3002");
            }
            break;
          case "delete-group":
            console.log("Delete action triggered for group:", this.state.activeGroupId);
            this.deleteGroupModal.open(activeGroup);
            break;
        }
      });
      this.elements.groupListCollapsible?.addEventListener("click", (event) => {
        this.handleGroupCardClick(event);
      });
      this.elements.groupMenuToggle?.addEventListener("click", (event) => {
        event.stopPropagation();
        const menu = this.elements.groupListCollapsible;
        if (!menu) return;
        menu.classList.toggle("hidden");
        setTimeout(() => {
          menu.classList.toggle("mobile-group-menu-active");
        }, 0);
      });
      document.addEventListener("click", (event) => {
        const menu = this.elements.groupListCollapsible;
        const toggle = this.elements.groupMenuToggle;
        if (menu && menu.classList.contains("mobile-group-menu-active") && !menu.contains(event.target) && !toggle.contains(event.target)) {
          this._closeMobileMenu();
        }
      });
      this.initCustomSelects();
      this.initTooltips();
      this.initDragAndDrop();
      this._initBatchActions();
    }
    // 4. 数据获取与渲染逻辑 
    async loadKeyGroups(force = false) {
      this.state.isLoading = true;
      try {
        const responseData = await apiFetchJson("/admin/keygroups", { noCache: force });
        if (responseData && responseData.success && Array.isArray(responseData.data)) {
          this.state.groups = responseData.data;
        } else {
          console.error("API response format is incorrect:", responseData);
          this.state.groups = [];
        }
        if (this.state.groups.length > 0 && !this.state.activeGroupId) {
          this.state.activeGroupId = this.state.groups[0].id;
        }
        this.renderGroupList();
        if (this.state.activeGroupId) {
          this.updateDashboard();
        }
        this.updateAllHealthIndicators();
      } catch (error) {
        console.error("Failed to load or parse key groups:", error);
        this.state.groups = [];
        this.renderGroupList();
        this.updateDashboard();
      } finally {
        this.state.isLoading = false;
      }
      if (this.state.activeGroupId) {
        this.updateDashboard();
      } else {
        this.apiKeyList.loadApiKeys(null);
      }
    }
    /**
     * Helper function to determine health indicator CSS classes based on success rate.
     * @param {number} rate - The success rate (0-100).
     * @returns {{ring: string, dot: string}} - The CSS classes for the ring and dot.
     */
    _getHealthIndicatorClasses(rate) {
      if (rate >= 50) return { ring: "bg-green-500/20", dot: "bg-green-500" };
      if (rate >= 30) return { ring: "bg-yellow-500/20", dot: "bg-yellow-500" };
      if (rate >= 10) return { ring: "bg-orange-500/20", dot: "bg-orange-500" };
      return { ring: "bg-red-500/20", dot: "bg-red-500" };
    }
    /**
     * Renders the list of group cards based on the current state.
     */
    renderGroupList() {
      if (!this.state.groups) return;
      const desktopListHtml = this.state.groups.map((group) => {
        const isActive = group.id === this.state.activeGroupId;
        const cardClass = isActive ? "group-card-active" : "group-card-inactive";
        const successRate = 100;
        const healthClasses = this._getHealthIndicatorClasses(successRate);
        const channelTag = this._getChannelTypeTag(group.channel_type || "Local");
        const customTags = this._getCustomTags(group.custom_tags);
        return `
                <div class="${cardClass}" data-group-id="${group.id}" data-success-rate="${successRate}">
                    <div class="flex items-center gap-x-3">
                        <div data-health-indicator class="health-indicator-ring ${healthClasses.ring}">
                            <div data-health-dot class="health-indicator-dot ${healthClasses.dot}"></div>
                        </div>
                        <div class="flex-grow">
                            <!-- [\u6700\u7EC8\u5E03\u5C40] 1. \u540D\u79F0 -> 2. \u63CF\u8FF0 -> 3. \u6807\u7B7E -->
                            <h3 class="font-semibold text-sm">${group.display_name}</h3>
                            <p class="card-sub-text my-1.5">${group.description || "No description available"}</p>
                            <div class="flex items-center gap-x-1.5 flex-wrap">
                                ${channelTag}
                                ${customTags}
                            </div>
                        </div>
                    </div>
                </div>`;
      }).join("");
      if (this.elements.desktopGroupContainer) {
        this.elements.desktopGroupContainer.innerHTML = desktopListHtml;
        if (this.elements.addGroupBtnContainer) {
          this.elements.desktopGroupContainer.parentElement.appendChild(this.elements.addGroupBtnContainer);
        }
      }
      const mobileListHtml = this.state.groups.map((group) => {
        const isActive = group.id === this.state.activeGroupId;
        const cardClass = isActive ? "group-card-active" : "group-card-inactive";
        return `
                <div class="${cardClass}" data-group-id="${group.id}">
                    <h3 class="font-semibold text-sm">${group.display_name})</h3>
                    <p class="card-sub-text my-1.5">${group.description || "No description available"}</p>
                </div>`;
      }).join("");
      if (this.elements.mobileGroupContainer) {
        this.elements.mobileGroupContainer.innerHTML = mobileListHtml;
      }
    }
    // 事件处理器和UI更新函数，现在完全由 state 驱动
    handleGroupCardClick(event) {
      const clickedCard = event.target.closest("[data-group-id]");
      if (!clickedCard) return;
      const groupId = parseInt(clickedCard.dataset.groupId, 10);
      if (this.state.activeGroupId !== groupId) {
        this.state.activeGroupId = groupId;
        this.renderGroupList();
        this.updateDashboard();
      }
      if (window.innerWidth < 1024) {
        this._closeMobileMenu();
      }
    }
    // [NEW HELPER METHOD] Centralizes the logic for closing the mobile menu.
    _closeMobileMenu() {
      const menu = this.elements.groupListCollapsible;
      if (!menu) return;
      menu.classList.remove("mobile-group-menu-active");
      menu.classList.add("hidden");
    }
    updateDashboard() {
      const activeGroup = this.state.groups.find((g) => g.id === this.state.activeGroupId);
      if (activeGroup) {
        if (this.elements.dashboardTitle) {
          this.elements.dashboardTitle.textContent = `${activeGroup.display_name}`;
        }
        if (this.elements.mobileActiveGroupDisplay) {
          this.elements.mobileActiveGroupDisplay.innerHTML = `
                    <h3 class="font-semibold text-sm">${activeGroup.display_name}</h3>
                    <p class="card-sub-text">\u5F53\u524D\u9009\u62E9</p>`;
        }
        this.apiKeyList.setActiveGroup(activeGroup.id, activeGroup.display_name);
        this.apiKeyList.loadApiKeys(activeGroup.id);
      } else {
        if (this.elements.dashboardTitle) this.elements.dashboardTitle.textContent = "No Group Selected";
        if (this.elements.mobileActiveGroupDisplay) this.elements.mobileActiveGroupDisplay.innerHTML = `<h3 class="font-semibold text-sm">\u8BF7\u9009\u62E9\u4E00\u4E2A\u5206\u7EC4</h3>`;
        this.apiKeyList.loadApiKeys(null);
      }
    }
    /**
     * Handles the saving of a key group with modern toast notifications.
     * @param {object} groupData The data collected from the KeyGroupModal.
     */
    async handleSaveGroup(groupData) {
      const isEditing = !!groupData.id;
      const endpoint = isEditing ? `/admin/keygroups/${groupData.id}` : "/admin/keygroups";
      const method = isEditing ? "PUT" : "POST";
      console.log(`[CONTROLLER] ${isEditing ? "Updating" : "Creating"} group...`, { endpoint, method, data: groupData });
      try {
        const response = await apiFetch(endpoint, {
          method,
          body: JSON.stringify(groupData),
          noCache: true
        });
        const result = await response.json();
        if (!result.success) {
          throw new Error(result.message || "An unknown error occurred on the server.");
        }
        if (isEditing) {
          console.log(`[CACHE INVALIDATION] Deleting cached details for group ${groupData.id}.`);
          delete this.state.groupDetailsCache[groupData.id];
        }
        if (!isEditing && result.data && result.data.id) {
          this.state.activeGroupId = result.data.id;
        }
        toastManager.show(`\u5206\u7EC4 "${groupData.display_name}" \u5DF2\u6210\u529F\u4FDD\u5B58\u3002`, "success");
        await this.loadKeyGroups(true);
      } catch (error) {
        console.error(`Failed to save group:`, error.message);
        toastManager.show(`\u4FDD\u5B58\u5931\u8D25: ${error.message}`, "error");
        throw error;
      }
    }
    /**
     * Opens the KeyGroupModal for editing, utilizing a cache-then-fetch strategy.
     * @param {number} groupId The ID of the group to edit.
     */
    async openEditGroupModal(groupId) {
      if (this.state.groupDetailsCache[groupId]) {
        console.log(`[CACHE HIT] Using cached details for group ${groupId}.`);
        this.keyGroupModal.open(this.state.groupDetailsCache[groupId]);
        return;
      }
      console.log(`[CACHE MISS] Fetching details for group ${groupId}.`);
      try {
        const endpoint = `/admin/keygroups/${groupId}`;
        const responseData = await apiFetchJson(endpoint, { noCache: true });
        if (responseData && responseData.success) {
          const groupDetails = responseData.data;
          this.state.groupDetailsCache[groupId] = groupDetails;
          this.keyGroupModal.open(groupDetails);
        } else {
          throw new Error(responseData.message || "Failed to load group details.");
        }
      } catch (error) {
        console.error(`Failed to fetch details for group ${groupId}:`, error);
        alert(`\u65E0\u6CD5\u52A0\u8F7D\u5206\u7EC4\u8BE6\u60C5: ${error.message}`);
      }
    }
    async openRequestSettingsModal() {
      if (!this.state.activeGroupId) {
        modalManager.showResult(false, "\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u5206\u7EC4\u3002");
        return;
      }
      console.log(`Opening request settings for group ID: ${this.state.activeGroupId}`);
      const data = {};
      this.requestSettingsModal.open(data);
    }
    /**
     * @param {object} data The data collected from the RequestSettingsModal.
     */
    async handleSaveRequestSettings(data) {
      if (!this.state.activeGroupId) {
        throw new Error("No active group selected.");
      }
      console.log(`[CONTROLLER] Saving request settings for group ${this.state.activeGroupId}:`, data);
      return Promise.resolve();
    }
    initCustomSelects() {
      const customSelects = document.querySelectorAll(".custom-select");
      customSelects.forEach((select) => new CustomSelect(select));
    }
    _initBatchActions() {
    }
    /**
     * Sends the new group UI order to the backend API.
     * @param {Array<object>} orderData - An array of objects, e.g., [{id: 1, order: 0}, {id: 2, order: 1}]
     */
    async saveGroupOrder(orderData) {
      console.log("Debounced save triggered. Sending UI order to API:", orderData);
      try {
        const response = await apiFetch("/admin/keygroups/order", {
          method: "PUT",
          body: JSON.stringify(orderData),
          noCache: true
        });
        const result = await response.json();
        if (!result.success) {
          throw new Error(result.message || "Failed to save UI order on the server.");
        }
        console.log("UI order saved successfully.");
      } catch (error) {
        console.error("Failed to save new group UI order:", error);
        this.loadKeyGroups();
      }
    }
    /**
     * Initializes drag-and-drop functionality for the group list.
     */
    initDragAndDrop() {
      if (typeof Sortable === "undefined") {
        console.error("SortableJS is not loaded.");
        return;
      }
      const container = this.elements.desktopGroupContainer;
      if (!container) return;
      new Sortable(container, {
        animation: 150,
        ghostClass: "sortable-ghost",
        dragClass: "sortable-drag",
        filter: "#add-group-btn-container",
        onEnd: (evt) => {
          const groupCards = Array.from(container.querySelectorAll("[data-group-id]"));
          const orderedState = groupCards.map((card) => {
            const cardId = parseInt(card.dataset.groupId, 10);
            return this.state.groups.find((group) => group.id === cardId);
          }).filter(Boolean);
          if (orderedState.length !== this.state.groups.length) {
            console.error("Drag-and-drop failed: Could not map all DOM elements to state. Aborting.");
            return;
          }
          this.state.groups = orderedState;
          const payload = this.state.groups.map((group, index) => ({
            id: group.id,
            order: index
          }));
          this.debouncedSaveOrder(payload);
        }
      });
    }
    /**
     * Helper function to generate a styled HTML tag for the channel type.
     * @param {string} type - The channel type string (e.g., 'OpenAI', 'Azure').
     * @returns {string} - The generated HTML span element.
     */
    _getChannelTypeTag(type) {
      if (!type) return "";
      const styles = {
        "OpenAI": "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
        "Azure": "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
        "Claude": "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
        "Gemini": "bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-300",
        "Local": "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
      };
      const baseClass = "inline-block text-xs font-medium px-2 py-0.5 rounded-md";
      const tagClass = styles[type] || styles["Local"];
      return `<span class="${baseClass} ${tagClass}">${type}</span>`;
    }
    /**
     * Generates styled HTML for custom tags with deterministically assigned colors.
     * @param {string[]} tags - An array of custom tag strings.
     * @returns {string} - The generated HTML for all custom tags.
     */
    _getCustomTags(tags) {
      if (!tags || !Array.isArray(tags) || tags.length === 0) {
        return "";
      }
      const colorPalette = [
        "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
        "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
        "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300",
        "bg-cyan-100 text-cyan-800 dark:bg-cyan-900 dark:text-cyan-300",
        "bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-300",
        "bg-lime-100 text-lime-800 dark:bg-lime-900 dark:text-lime-300"
      ];
      const baseClass = "inline-block text-xs font-medium px-2 py-0.5 rounded-md";
      return tags.map((tag) => {
        let hash = 0;
        for (let i = 0; i < tag.length; i++) {
          hash += tag.charCodeAt(i);
        }
        const colorClass = colorPalette[hash % colorPalette.length];
        return `<span class="${baseClass} ${colorClass}">${tag}</span>`;
      }).join("");
    }
    _updateHealthIndicator(cardElement) {
      const rate = parseFloat(cardElement.dataset.successRate);
      if (isNaN(rate)) return;
      const indicator = cardElement.querySelector("[data-health-indicator]");
      const dot = cardElement.querySelector("[data-health-dot]");
      if (!indicator || !dot) return;
      const colors = {
        green: ["bg-green-500/20", "bg-green-500"],
        yellow: ["bg-yellow-500/20", "bg-yellow-500"],
        orange: ["bg-orange-500/20", "bg-orange-500"],
        red: ["bg-red-500/20", "bg-red-500"]
      };
      Object.values(colors).forEach(([bgClass, dotClass]) => {
        indicator.classList.remove(bgClass);
        dot.classList.remove(dotClass);
      });
      let newColor;
      if (rate >= 50) newColor = colors.green;
      else if (rate >= 25) newColor = colors.yellow;
      else if (rate >= 10) newColor = colors.orange;
      else newColor = colors.red;
      indicator.classList.add(newColor[0]);
      dot.classList.add(newColor[1]);
    }
    updateAllHealthIndicators() {
      if (!this.elements.groupListCollapsible) return;
      const allCards = this.elements.groupListCollapsible.querySelectorAll("[data-success-rate]");
      allCards.forEach((card) => this._updateHealthIndicator(card));
    }
    initTooltips() {
      const tooltipIcons = document.querySelectorAll(".tooltip-icon");
      tooltipIcons.forEach((icon) => {
        icon.addEventListener("mouseenter", (e) => this.showTooltip(e));
        icon.addEventListener("mouseleave", () => this.hideTooltip());
      });
    }
    showTooltip(e) {
      this.hideTooltip();
      const target = e.currentTarget;
      const text = target.dataset.tooltipText;
      if (!text) return;
      const tooltip = document.createElement("div");
      tooltip.className = "global-tooltip";
      tooltip.textContent = text;
      document.body.appendChild(tooltip);
      this.activeTooltip = tooltip;
      const targetRect = target.getBoundingClientRect();
      const tooltipRect = tooltip.getBoundingClientRect();
      let top = targetRect.top - tooltipRect.height - 8;
      let left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
      if (top < 0) top = targetRect.bottom + 8;
      if (left < 0) left = 8;
      if (left + tooltipRect.width > window.innerWidth) {
        left = window.innerWidth - tooltipRect.width - 8;
      }
      tooltip.style.top = `${top}px`;
      tooltip.style.left = `${left}px`;
    }
    hideTooltip() {
      if (this.activeTooltip) {
        this.activeTooltip.remove();
        this.activeTooltip = null;
      }
    }
  };
  function init2() {
    console.log("[Modern Frontend] Keys page controller loaded.");
    const page = new KeyGroupsPage();
    page.init();
  }

  // frontend/js/main.js
  var pageModules = {
    "dashboard": init,
    //'settings': initSettings,
    //'error-logs': initErrorLogs,
    "keys": init2
    //keys是page_handler中对该页面pageID的定义简称，我们沿用即可
    // 未来新增的页面，只需在这里添加一行映射
  };
  document.addEventListener("DOMContentLoaded", () => {
    const allTabContainers = document.querySelectorAll("[data-sliding-tabs-container]");
    allTabContainers.forEach((container) => new SlidingTabs(container));
    const allSelectContainers = document.querySelectorAll("[data-custom-select-container]");
    allSelectContainers.forEach((container) => new CustomSelect(container));
    taskCenterManager.init();
    const pageContainer = document.querySelector("[data-page-id]");
    if (pageContainer) {
      const pageId = pageContainer.dataset.pageId;
      if (pageId && pageModules[pageId]) {
        pageModules[pageId]();
      }
    }
  });
  window.modalManager = modalManager;
  window.taskCenterManager = taskCenterManager;
  window.toastManager = toastManager;
  window.uiPatterns = uiPatterns;
})();
