Greasy Fork

来自缓存

Greasy Fork is available in English.

LDOH New API Helper

LDOH New API 助手(余额查询、自动签到、密钥管理、模型查询)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         LDOH New API Helper
// @namespace    jojojotarou.ldoh.newapi.helper
// @version      2.0.2
// @description  LDOH New API 助手(余额查询、自动签到、密钥管理、模型查询)
// @author       @JoJoJotarou
// @match        https://ldoh.105117.xyz/*
// @include      *
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        unsafeWindow
// @connect      *
// @run-at       document-idle
// @license      MIT
// ==/UserScript==
(() => {
  // src/config.js
  var CONFIG = {
    STORAGE_KEY: "ldoh_newapi_data",
    SETTINGS_KEY: "ldoh_newapi_settings",
    WHITELIST_KEY: "ldoh_site_whitelist",
    BLACKLIST_KEY: "ldoh_site_blacklist",
    BLACKLIST_REMOVED_KEY: "ldoh_site_blacklist_removed",
    CHECKIN_SKIP_KEY: "ldoh_checkin_skip",
    CHECKIN_SKIP_REMOVED_KEY: "ldoh_checkin_skip_removed",
    BLACKLIST: ["elysiver.h-e.top", "anthorpic.us.ci", "demo.voapi.top", "windhub.cc", "ai.qaq.al"],
    DEFAULT_CHECKIN_SKIP: /* @__PURE__ */ new Map([
      ["api.67.si", "CF Turnstile \u62E6\u622A"],
      ["runanytime.hxi.me", "CF Turnstile \u62E6\u622A"],
      ["anyrouter.top", "\u767B\u5F55\u81EA\u52A8\u7B7E\u5230"],
      ["x666.me", "\u7AD9\u5916\u7B7E\u5230"]
    ]),
    DEFAULT_INTERVAL: 60,
    DEFAULT_MAX_CONCURRENT: 15,
    DEFAULT_MAX_BACKGROUND: 10,
    QUOTA_CONVERSION_RATE: 5e5,
    PORTAL_HOST: "ldoh.105117.xyz",
    REQUEST_TIMEOUT: 1e4,
    DEBOUNCE_DELAY: 800,
    LOGIN_CHECK_INTERVAL: 500,
    LOGIN_CHECK_MAX_ATTEMPTS: 10,
    ANIMATION_FAST_MS: 200,
    ANIMATION_NORMAL_MS: 300,
    DOM: {
      CARD_SELECTOR: ".rounded-xl.shadow.group.relative",
      HELPER_CONTAINER_CLASS: "ldoh-helper-container",
      STYLE_ID: "ldoh-helper-css"
    }
  };

  // src/logger.js
  var _print = (level, msg, color, bg, ...args) => console.log(
    `%c LDOH %c ${level.toUpperCase()} %c ${msg}`,
    "background: #6366f1; color: white; border-radius: 3px 0 0 3px; font-weight: bold; padding: 1px 4px",
    `background: ${bg}; color: ${color}; border-radius: 0 3px 3px 0; font-weight: bold; padding: 1px 4px`,
    "color: inherit; font-weight: normal",
    ...args
  );
  var _printDebug = (level, msg, color, bg, ...args) => console.debug(
    `%c LDOH %c ${level.toUpperCase()} %c ${msg}`,
    "background: #6366f1; color: white; border-radius: 3px 0 0 3px; font-weight: bold; padding: 1px 4px",
    `background: ${bg}; color: ${color}; border-radius: 0 3px 3px 0; font-weight: bold; padding: 1px 4px`,
    "color: inherit; font-weight: normal",
    ...args
  );
  var Log = {
    info: (msg, ...args) => _print("info", msg, "#fff", "#3b82f6", ...args),
    success: (msg, ...args) => _print("ok", msg, "#fff", "#10b981", ...args),
    warn: (msg, ...args) => _print("warn", msg, "#000", "#f59e0b", ...args),
    error: (msg, ...args) => _print("err", msg, "#fff", "#ef4444", ...args),
    debug: (msg, ...args) => _printDebug("debug", msg, "#fff", "#8b5cf6", ...args)
  };

  // src/utils/format.js
  function formatQuota(q) {
    if (q === void 0 || q === null || isNaN(q)) return "0.00";
    return (q / CONFIG.QUOTA_CONVERSION_RATE).toFixed(2);
  }
  function normalizeHost(host) {
    if (!host || typeof host !== "string") {
      Log.warn("normalizeHost \u6536\u5230\u65E0\u6548\u7684 host", host);
      return "";
    }
    return host.toLowerCase().split(":")[0];
  }
  function escapeHtml(str) {
    if (!str || typeof str !== "string") return "";
    const div = document.createElement("div");
    div.textContent = str;
    return div.innerHTML;
  }

  // src/utils/storage.js
  function _isInManagedList(n, builtinList, addedKey, removedKey) {
    const added = GM_getValue(addedKey, []);
    const removed = GM_getValue(removedKey, []);
    const inBuiltin = Array.isArray(builtinList) ? builtinList.includes(n) : builtinList instanceof Map ? builtinList.has(n) : Object.prototype.hasOwnProperty.call(builtinList, n);
    return inBuiltin && !removed.includes(n) || added.includes(n);
  }
  function _toggleManagedList(n, builtinList, addedKey, removedKey) {
    const inBuiltin = Array.isArray(builtinList) ? builtinList.includes(n) : builtinList instanceof Map ? builtinList.has(n) : Object.prototype.hasOwnProperty.call(builtinList, n);
    if (_isInManagedList(n, builtinList, addedKey, removedKey)) {
      if (inBuiltin) {
        const removed = GM_getValue(removedKey, []);
        if (!removed.includes(n)) {
          removed.push(n);
          GM_setValue(removedKey, removed);
        }
      } else {
        const added = GM_getValue(addedKey, []);
        const idx = added.indexOf(n);
        if (idx >= 0) {
          added.splice(idx, 1);
          GM_setValue(addedKey, added);
        }
      }
      return false;
    } else {
      const removed = GM_getValue(removedKey, []);
      const ridx = removed.indexOf(n);
      if (ridx >= 0) {
        removed.splice(ridx, 1);
        GM_setValue(removedKey, removed);
      } else {
        const added = GM_getValue(addedKey, []);
        if (!added.includes(n)) {
          added.push(n);
          GM_setValue(addedKey, added);
        }
      }
      return true;
    }
  }
  function saveSiteData(host, data) {
    try {
      const all = GM_getValue(CONFIG.STORAGE_KEY, {});
      const key = normalizeHost(host);
      all[key] = { ...all[key] || {}, ...data, ts: Date.now() };
      GM_setValue(CONFIG.STORAGE_KEY, all);
      Log.debug(`\u4FDD\u5B58\u7AD9\u70B9\u6570\u636E: ${key}`, data);
    } catch (e) {
      Log.error(`\u4FDD\u5B58\u7AD9\u70B9\u6570\u636E\u5931\u8D25: ${host}`, e);
    }
  }
  function getSiteData(host) {
    try {
      const all = GM_getValue(CONFIG.STORAGE_KEY, {});
      const key = normalizeHost(host);
      return all[key] || {};
    } catch (e) {
      Log.error(`\u83B7\u53D6\u7AD9\u70B9\u6570\u636E\u5931\u8D25: ${host}`, e);
      return {};
    }
  }
  function getAndSyncUserId(host) {
    try {
      const userStr = localStorage.getItem("user");
      if (!userStr) {
        Log.debug("localStorage \u4E2D\u672A\u627E\u5230 user \u6570\u636E");
        return null;
      }
      const user = JSON.parse(userStr);
      if (!user || typeof user !== "object" || !user.id) {
        Log.warn("user \u6570\u636E\u683C\u5F0F\u65E0\u6548\u6216\u7F3A\u5931 ID");
        return null;
      }
      const userId = String(user.id);
      const normalizedHost = normalizeHost(host);
      const siteData = getSiteData(normalizedHost);
      if (siteData.userId !== userId) {
        Log.info(`[\u8EAB\u4EFD\u540C\u6B65] \u4E3A ${normalizedHost} \u8BC6\u522B\u5230\u65B0\u7528\u6237 ID: ${userId}`);
        saveSiteData(normalizedHost, { userId });
      }
      return userId;
    } catch (e) {
      Log.error("\u540C\u6B65\u7528\u6237 ID \u5931\u8D25", e);
      return null;
    }
  }
  function isBlacklisted(host) {
    return _isInManagedList(
      normalizeHost(host),
      CONFIG.BLACKLIST,
      CONFIG.BLACKLIST_KEY,
      CONFIG.BLACKLIST_REMOVED_KEY
    );
  }
  function toggleBlacklist(host) {
    return _toggleManagedList(
      normalizeHost(host),
      CONFIG.BLACKLIST,
      CONFIG.BLACKLIST_KEY,
      CONFIG.BLACKLIST_REMOVED_KEY
    );
  }
  function getBuiltinCheckinSkipReason(host) {
    const n = normalizeHost(host);
    const list = CONFIG.DEFAULT_CHECKIN_SKIP;
    return list instanceof Map ? list.get(n) ?? null : list[n] ?? null;
  }
  function isCheckinSkipped(host) {
    const n = normalizeHost(host);
    if (getSiteData(n).checkinSupported === false) return true;
    return _isInManagedList(
      n,
      CONFIG.DEFAULT_CHECKIN_SKIP,
      CONFIG.CHECKIN_SKIP_KEY,
      CONFIG.CHECKIN_SKIP_REMOVED_KEY
    );
  }
  function toggleCheckinSkip(host) {
    return _toggleManagedList(
      normalizeHost(host),
      CONFIG.DEFAULT_CHECKIN_SKIP,
      CONFIG.CHECKIN_SKIP_KEY,
      CONFIG.CHECKIN_SKIP_REMOVED_KEY
    );
  }

  // src/styles.js
  var STYLES = `
  :root {
    --ldoh-primary: #6366f1;
    --ldoh-primary-hover: #4f46e5;
    --ldoh-success: #10b981;
    --ldoh-warning: #f59e0b;
    --ldoh-danger: #ef4444;
    --ldoh-text: #1e293b;
    --ldoh-text-light: #64748b;
    --ldoh-bg: #ffffff;
    --ldoh-card-bg: rgba(255, 255, 255, 0.85);
    --ldoh-border: #e2e8f0;
    --ldoh-radius: 12px;
    --ldoh-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.08), 0 2px 6px -1px rgba(0, 0, 0, 0.04);
  }

  .ldoh-helper-container {
    display: flex; align-items: center; gap: 4px; z-index: 10;
    pointer-events: auto; animation: ldoh-fade-in 0.3s ease-out;
  }
  @keyframes ldoh-fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }

  .ldoh-info-bar {
    display: flex; align-items: center; gap: 4px;
    font-size: 10px; font-weight: 600; color: inherit;
    white-space: nowrap;
  }

  .status-ok { background: var(--ldoh-success); }
  .status-none { background: #9ca3af; }

  .ldoh-btn {
    width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
    background: transparent; border-radius: 4px; border: none;
    cursor: pointer; color: inherit; transition: all 0.2s; flex-shrink: 0;
  }
  .ldoh-btn:hover { background: rgba(99, 102, 241, 0.1); color: var(--ldoh-primary); opacity: 1; transform: scale(1.1); }
  .ldoh-btn:active { transform: scale(0.95); }

  .ldoh-refresh-btn.loading svg { animation: ldoh-spin 0.8s linear infinite; }
  @keyframes ldoh-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

  /* Dialog */
  .ldh-overlay {
    position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4);
    z-index: 900; display: flex; justify-content: center; align-items: center;
    backdrop-filter: blur(6px); animation: ldoh-fade-in-blur 0.3s ease-out;
  }
  @keyframes ldoh-fade-in-blur { from { opacity: 0; backdrop-filter: blur(0); } to { opacity: 1; backdrop-filter: blur(6px); } }

  .ldh-dialog {
    background: #fff; width: min(720px, 94vw); max-height: 85vh;
    border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
    display: flex; flex-direction: column; overflow: hidden;
    border: 1px solid rgba(255, 255, 255, 0.2); font-size: 13px;
    transform-origin: center; animation: ldoh-zoom-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  @keyframes ldoh-zoom-in { from { transform: scale(0.9) translateY(20px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }

  .ldh-header {
    padding: 18px 24px; border-bottom: 1px solid var(--ldoh-border);
    display: flex; justify-content: space-between; align-items: center;
    background: linear-gradient(to right, #f8fafc, #ffffff);
  }
  .ldh-title { font-size: 16px; font-weight: 700; color: var(--ldoh-text); }
  .ldh-close {
    width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
    border-radius: 50%; color: var(--ldoh-text-light); cursor: pointer; transition: all 0.2s;
  }
  .ldh-close:hover { background: #f1f5f9; color: var(--ldoh-danger); transform: rotate(90deg); }

  .ldh-content { padding: 24px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 24px; scrollbar-width: thin; }
  .ldh-sec-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
  .ldh-sec-title { font-size: 14px; font-weight: 700; color: var(--ldoh-text); display: flex; align-items: center; gap: 6px; }
  .ldh-sec-badge { font-size: 11px; padding: 2px 8px; background: #f1f5f9; border-radius: 20px; color: var(--ldoh-text-light); }

  .ldh-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
  .ldh-grid-models { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
  .ldh-item {
    padding: 12px; border: 1px solid var(--ldoh-border); border-radius: var(--ldoh-radius);
    font-size: 13px; color: var(--ldoh-text); background: #fff; cursor: pointer;
    position: relative; transition: all 0.2s ease;
    display: flex; flex-direction: column; gap: 4px;
  }
  .ldh-item:hover { border-color: var(--ldoh-primary); background: #f5f3ff; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1); }
  .ldh-item:active { transform: translateY(0); }

  .ldh-item.active { border-color: var(--ldoh-primary); background: #f5f3ff; box-shadow: inset 0 0 0 1px var(--ldoh-primary); }

  .ldh-quota { color: var(--ldoh-warning); font-weight: 800; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }

  /* Toast */
  .ldoh-toast-container { position: fixed; top: 24px; right: 24px; z-index: 950; display: flex; flex-direction: column; gap: 12px; pointer-events: none; }
  .ldoh-toast {
    min-width: 300px; max-width: 450px; padding: 14px 18px; background: #fff; border-radius: 14px;
    box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
    display: flex; align-items: center; gap: 12px; font-size: 14px; font-weight: 600;
    pointer-events: auto; animation: ldoh-slide-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
    border-left: 5px solid var(--ldoh-primary);
  }
  @keyframes ldoh-slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }

  .ldoh-toast.success { border-left-color: var(--ldoh-success); }
  .ldoh-toast.error { border-left-color: var(--ldoh-danger); }
  .ldoh-toast.warning { border-left-color: var(--ldoh-warning); }

  .ldoh-toast-icon { width: 22px; height: 22px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
  .ldoh-toast.success .ldoh-toast-icon { background: #ecfdf5; color: var(--ldoh-success); }
  .ldoh-toast.error .ldoh-toast-icon { background: #fef2f2; color: var(--ldoh-danger); }
  .ldoh-toast.warning .ldoh-toast-icon { background: #fffbeb; color: var(--ldoh-warning); }
  .ldoh-toast.info .ldoh-toast-icon { background: #f0f9ff; color: var(--ldoh-primary); }

  .ldoh-toast-message { flex: 1; color: var(--ldoh-text); line-height: 1.5; }
  .ldoh-toast-close { width: 24px; height: 24px; flex-shrink: 0; cursor: pointer; color: var(--ldoh-text-light); display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: all 0.2s; }
  .ldoh-toast-close:hover { background: #f1f5f9; color: var(--ldoh-text); }

  /* ---- \u60AC\u6D6E\u9762\u677F FAB ---- */
  .ldoh-fab {
    position: fixed; right: 20px; bottom: 20px; width: 44px; height: 44px;
    border-radius: 50%; background: var(--ldoh-primary); color: white; border: none;
    cursor: pointer; z-index: 800; display: flex; align-items: center; justify-content: center;
    box-shadow: 0 4px 14px rgba(99, 102, 241, 0.45);
    transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  .ldoh-fab:hover { background: var(--ldoh-primary-hover); transform: scale(1.08); }
  .ldoh-fab-badge {
    position: absolute; top: -4px; right: -4px; background: var(--ldoh-danger); color: white;
    border-radius: 99px; font-size: 9px; font-weight: 700; min-width: 16px; height: 16px;
    display: flex; align-items: center; justify-content: center; padding: 0 3px; border: 2px solid white;
  }
  .ldoh-floating-panel {
    position: fixed; right: 20px; bottom: 76px; width: 500px; max-height: 62vh;
    background: #fff; border-radius: 16px; z-index: 799; flex-direction: column; overflow: hidden;
    box-shadow: 0 20px 40px -8px rgba(0,0,0,0.18), 0 4px 12px -2px rgba(0,0,0,0.08);
    border: 1px solid var(--ldoh-border); transform-origin: bottom right;
  }
  @keyframes ldoh-panel-in {
    from { opacity: 0; transform: scale(0.85) translateY(16px); }
    to { opacity: 1; transform: scale(1) translateY(0); }
  }
  .ldoh-panel-in { animation: ldoh-panel-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
  .ldoh-panel-hd {
    padding: 10px 14px; border-bottom: 1px solid var(--ldoh-border);
    display: flex; align-items: center; gap: 8px; flex-shrink: 0;
    background: linear-gradient(to right, #f8fafc, #fff);
  }
  .ldoh-panel-hd-title {
    flex: 1; font-size: 13px; font-weight: 700; color: var(--ldoh-text);
    display: flex; align-items: center; gap: 6px;
  }
  .ldoh-panel-hd-total { font-size: 11px; color: var(--ldoh-text-light); }
  .ldoh-panel-search {
    padding: 7px 12px 6px; border-bottom: 1px solid var(--ldoh-border); flex-shrink: 0; background: #fff;
  }
  .ldoh-panel-search-wrap { position: relative; }
  .ldoh-panel-search-icon {
    position: absolute; left: 8px; top: 50%; transform: translateY(-50%);
    opacity: 0.35; pointer-events: none;
  }
  .ldoh-panel-search-input {
    width: 100%; box-sizing: border-box; padding: 5px 8px 5px 28px;
    border: 1px solid var(--ldoh-border); border-radius: 6px; font-size: 12px;
    outline: none; background: #f8fafc; transition: border-color 0.2s, background 0.2s;
  }
  .ldoh-panel-search-input:focus { border-color: var(--ldoh-primary); background: #fff; }
  .ldoh-filter-bar { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
  .ldoh-filter-chip {
    font-size: 10px; padding: 2px 8px; border-radius: 10px; cursor: pointer;
    border: 1px solid var(--ldoh-border); background: #f8fafc; color: var(--ldoh-text-light);
    user-select: none; transition: all 0.15s; white-space: nowrap;
  }
  .ldoh-filter-chip:hover { border-color: #cbd5e1; color: var(--ldoh-text); }
  .ldoh-filter-chip.active { background: var(--ldoh-primary); color: #fff; border-color: var(--ldoh-primary); }
  .ldoh-panel-body { overflow-y: auto; flex: 1; scrollbar-width: thin; min-height: 200px; }
  .ldoh-panel-row {
    display: grid; grid-template-columns: 1fr 54px 64px 22px 22px 22px 22px 22px 22px;
    align-items: center; gap: 6px; padding: 7px 12px;
    border-bottom: 1px solid #f1f5f9; transition: background 0.15s; font-size: 12px;
  }
  .ldoh-panel-row:hover { background: #f8fafc; }
  .ldoh-panel-row:last-child { border-bottom: none; }
  .ldoh-panel-name {
    font-weight: 600; color: var(--ldoh-text);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    display: flex; flex-direction: column; gap: 1px; min-width: 0;
  }
  .ldoh-panel-name-main {
    font-weight: 600; color: var(--ldoh-text);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .ldoh-panel-name-host {
    font-size: 10px; font-weight: 400; color: var(--ldoh-text-light);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .ldoh-panel-checkin {
    font-size: 10px; padding: 2px 5px; border-radius: 8px; font-weight: 600; text-align: center;
  }
  .ldoh-panel-checkin.ok { background: #ecfdf5; color: #059669; }
  .ldoh-panel-checkin.no { background: #fffbeb; color: #d97706; }
  .ldoh-panel-checkin.na { background: #f1f5f9; color: var(--ldoh-text-light); }
  .ldoh-panel-balance {
    font-weight: 700; color: #d97706; font-family: ui-monospace, monospace;
    font-size: 12px; text-align: right; white-space: nowrap;
  }
  .ldoh-panel-empty { padding: 32px; text-align: center; color: var(--ldoh-text-light); font-size: 13px; }

  /* \u5220\u9664\u786E\u8BA4\u6C14\u6CE1 */
  .ldoh-confirm-pop {
    position: fixed; z-index: 1000;
    background: #fff; border: 1px solid var(--ldoh-border); border-radius: 10px;
    box-shadow: 0 6px 20px -4px rgba(0,0,0,0.15);
    padding: 8px 10px; display: flex; align-items: center; gap: 8px;
    font-size: 12px; font-weight: 600; color: var(--ldoh-text); white-space: nowrap;
    animation: ldoh-fade-in 0.15s ease-out;
  }
  .ldoh-confirm-pop::after {
    content: ""; position: absolute; bottom: -5px; right: 10px;
    width: 8px; height: 8px; background: #fff;
    border-right: 1px solid var(--ldoh-border); border-bottom: 1px solid var(--ldoh-border);
    transform: rotate(45deg);
  }
  .ldoh-pop-btn {
    padding: 3px 10px; border-radius: 6px; border: none; font-size: 11px;
    font-weight: 600; cursor: pointer; transition: all 0.15s;
  }
  .ldoh-pop-cancel { background: #f1f5f9; color: var(--ldoh-text); }
  .ldoh-pop-cancel:hover { background: #e2e8f0; }
  .ldoh-pop-confirm { background: var(--ldoh-danger); color: #fff; }
  .ldoh-pop-confirm:hover { background: #dc2626; }

  /* \u8BBE\u7F6E\u83DC\u5355 */
  .ldoh-settings-pop {
    position: fixed; z-index: 1000;
    background: #fff; border: 1px solid var(--ldoh-border); border-radius: 10px;
    box-shadow: 0 6px 20px -4px rgba(0,0,0,0.15);
    padding: 6px; width: 140px;
    animation: ldoh-fade-in 0.15s ease-out;
  }
  .ldoh-settings-item {
    width: 100%; border: none; background: transparent;
    padding: 8px 10px; border-radius: 8px; cursor: pointer; white-space: nowrap;
    font-size: 12px; font-weight: 600; color: var(--ldoh-text);
    text-align: left; transition: background 0.15s;
  }
  .ldoh-settings-item:hover { background: #f8fafc; }

  .ldoh-interval-pop {
    position: fixed; z-index: 1000;
    background: #fff; border: 1px solid var(--ldoh-border); border-radius: 10px;
    box-shadow: 0 6px 20px -4px rgba(0,0,0,0.15);
    padding: 10px; width: 220px;
    animation: ldoh-fade-in 0.15s ease-out;
  }
  .ldoh-interval-title {
    font-size: 12px; font-weight: 700; color: var(--ldoh-text);
    margin-bottom: 8px;
  }
  .ldoh-interval-hint {
    font-size: 11px; color: var(--ldoh-text-light);
    margin-top: 6px;
  }
  .ldoh-interval-input {
    width: 100%;
    border: 1px solid var(--ldoh-border);
    border-radius: 8px;
    padding: 7px 9px;
    font-size: 12px;
    outline: none;
    transition: border-color 0.15s, box-shadow 0.15s;
    box-sizing: border-box;
  }
  .ldoh-interval-input:focus {
    border-color: var(--ldoh-primary);
    box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
  }
  .ldoh-interval-actions {
    margin-top: 10px; display: flex; justify-content: flex-end; gap: 8px;
  }
`;

  // src/utils/misc.js
  function injectStyles() {
    const styleId = CONFIG.DOM.STYLE_ID;
    if (!document.getElementById(styleId)) {
      Log.debug("\u6CE8\u5165\u6837\u5F0F\u8868");
      const s = document.createElement("style");
      s.id = styleId;
      s.textContent = STYLES;
      document.head.appendChild(s);
    }
  }
  function copy(text) {
    try {
      GM_setClipboard(text);
      Log.debug(`\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F: ${text.substring(0, 20)}...`);
    } catch (e) {
      Log.error("\u590D\u5236\u5230\u526A\u8D34\u677F\u5931\u8D25", e);
    }
  }
  function debounce(func, delay) {
    let timer = null;
    return function(...args) {
      clearTimeout(timer);
      timer = setTimeout(() => func.apply(this, args), delay);
    };
  }
  async function isNewApiSite(retryCount = 5) {
    try {
      const host = window.location.hostname;
      const normalizedHost = normalizeHost(host);
      const whitelist = GM_getValue(CONFIG.WHITELIST_KEY, []);
      const inWhitelist = whitelist.includes(normalizedHost);
      if (!inWhitelist) {
        Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u4E0D\u5728 LDOH \u7AD9\u70B9\u767D\u540D\u5355\u4E2D\uFF0C\u8DF3\u8FC7`);
        return false;
      }
      if (retryCount > 0) {
        Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u5728 LDOH \u767D\u540D\u5355\u4E2D\uFF0C\u7EE7\u7EED\u68C0\u6D4B New API \u7279\u5F81`);
      }
      let hasUserData = !!localStorage.getItem("user");
      if (!hasUserData && retryCount > 0) {
        Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u6682\u65E0\u7528\u6237\u6570\u636E\uFF0C${retryCount === 1 ? "\u7ED3\u675F" : "\u7B49\u5F85"}\u91CD\u8BD5...`);
        await new Promise((resolve) => setTimeout(resolve, 500));
        return isNewApiSite(retryCount - 1);
      }
      if (hasUserData) {
        Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u68C0\u6D4B\u5230\u7528\u6237\u6570\u636E\uFF0C\u5224\u5B9A\u4E3A New API \u7AD9\u70B9`);
        return true;
      }
      Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u672A\u8BC6\u522B\u4E3A New API \u7AD9\u70B9`);
      return false;
    } catch (e) {
      Log.error("[\u7AD9\u70B9\u8BC6\u522B] \u68C0\u6D4B\u5931\u8D25", e);
      return false;
    }
  }
  async function waitForLogin() {
    Log.debug("[\u767B\u5F55\u68C0\u6D4B] \u5F00\u59CB\u7B49\u5F85\u7528\u6237\u767B\u5F55...");
    const host = window.location.hostname;
    for (let i = 0; i < CONFIG.LOGIN_CHECK_MAX_ATTEMPTS; i++) {
      const userId = getAndSyncUserId(host);
      if (userId) {
        Log.success(`[\u767B\u5F55\u68C0\u6D4B] \u68C0\u6D4B\u5230\u767B\u5F55\uFF0C\u7528\u6237 ID: ${userId}`);
        return userId;
      }
      await new Promise((resolve) => setTimeout(resolve, CONFIG.LOGIN_CHECK_INTERVAL));
    }
    Log.debug("[\u767B\u5F55\u68C0\u6D4B] \u8D85\u65F6\uFF0C\u672A\u68C0\u6D4B\u5230\u767B\u5F55");
    return null;
  }
  function watchLoginStatus(callback) {
    const host = window.location.hostname;
    window.addEventListener("storage", (e) => {
      if (e.key === "user" && e.newValue) {
        Log.debug("[\u767B\u5F55\u76D1\u542C] \u68C0\u6D4B\u5230 user \u6570\u636E\u53D8\u5316");
        const userId = getAndSyncUserId(host);
        if (userId) callback(userId);
      }
    });
    let lastUserId = getAndSyncUserId(host);
    setInterval(() => {
      const currentUserId = getAndSyncUserId(host);
      if (currentUserId && currentUserId !== lastUserId) {
        Log.debug("[\u767B\u5F55\u76D1\u542C] \u8F6E\u8BE2\u68C0\u6D4B\u5230\u767B\u5F55");
        lastUserId = currentUserId;
        callback(currentUserId);
      }
    }, CONFIG.LOGIN_CHECK_INTERVAL);
  }

  // src/ui/toast.js
  var _container = null;
  function _initContainer() {
    if (!_container) {
      _container = document.createElement("div");
      _container.className = "ldoh-toast-container";
      document.body.appendChild(_container);
    }
  }
  var ICONS = {
    success: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
    error: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
    warning: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
    info: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>'
  };
  function removeToast(toast) {
    if (!toast || !toast.parentNode) return;
    toast.style.animation = "ldoh-slide-in 0.3s ease-in reverse forwards";
    setTimeout(() => toast.remove(), 300);
  }
  function showToast(message, type = "info", duration = 3e3) {
    _initContainer();
    const toast = document.createElement("div");
    toast.className = `ldoh-toast ${type}`;
    toast.innerHTML = `
    <div class="ldoh-toast-icon">${ICONS[type] || ICONS.info}</div>
    <div class="ldoh-toast-message">${escapeHtml(message)}</div>
    <div class="ldoh-toast-close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></div>
  `;
    toast.querySelector(".ldoh-toast-close").onclick = () => removeToast(toast);
    _container.appendChild(toast);
    if (duration > 0) setTimeout(() => removeToast(toast), duration);
    return toast;
  }
  var Toast = {
    show: showToast,
    remove: removeToast,
    success: (msg, duration) => showToast(msg, "success", duration),
    error: (msg, duration) => showToast(msg, "error", duration),
    warning: (msg, duration) => showToast(msg, "warning", duration),
    info: (msg, duration) => showToast(msg, "info", duration)
  };

  // src/utils/date.js
  function getTodayString() {
    const now = /* @__PURE__ */ new Date();
    return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
  }
  function getCurrentMonthString() {
    return getTodayString().slice(0, 7);
  }
  function isCheckedInToday(siteData) {
    if (!siteData) return false;
    const today = getTodayString();
    return siteData.checkedInToday === true && (!siteData.lastCheckinDate || siteData.lastCheckinDate === today);
  }

  // src/api.js
  var _state = {
    waiters: [],
    activeRequests: 0,
    activeBackgroundRequests: 0
  };
  function _release(isInteractive) {
    _state.activeRequests--;
    if (!isInteractive) _state.activeBackgroundRequests--;
    const settings = GM_getValue(CONFIG.SETTINGS_KEY, {});
    const maxConcurrent = settings.maxConcurrent || CONFIG.DEFAULT_MAX_CONCURRENT;
    const maxBackground = settings.maxBackground || CONFIG.DEFAULT_MAX_BACKGROUND;
    let idx = _state.waiters.findIndex(
      (w) => w.isInteractive && _state.activeRequests < maxConcurrent
    );
    if (idx < 0) {
      idx = _state.waiters.findIndex(
        (w) => !w.isInteractive && _state.activeRequests < maxConcurrent && _state.activeBackgroundRequests < maxBackground
      );
    }
    if (idx >= 0) {
      const w = _state.waiters.splice(idx, 1)[0];
      _state.activeRequests++;
      if (!w.isInteractive) _state.activeBackgroundRequests++;
      w.resolve();
    }
  }
  async function _acquire(isInteractive) {
    const settings = GM_getValue(CONFIG.SETTINGS_KEY, {});
    const maxConcurrent = settings.maxConcurrent || CONFIG.DEFAULT_MAX_CONCURRENT;
    const maxBackground = settings.maxBackground || CONFIG.DEFAULT_MAX_BACKGROUND;
    const canRun = () => _state.activeRequests < maxConcurrent && (isInteractive || _state.activeBackgroundRequests < maxBackground);
    if (!canRun()) {
      await new Promise((resolve) => _state.waiters.push({ resolve, isInteractive }));
      return;
    }
    _state.activeRequests++;
    if (!isInteractive) _state.activeBackgroundRequests++;
  }
  async function request(method, host, path, token = null, userId = null, body = null, isInteractive = false, extraHeaders = {}) {
    await _acquire(isInteractive);
    Log.debug(
      `[\u8BF7\u6C42] ${method} ${host}${path} (\u5E76\u53D1: ${_state.activeRequests}, \u540E\u53F0: ${_state.activeBackgroundRequests}, \u4EA4\u4E92: ${isInteractive})`
    );
    try {
      const result = await new Promise((resolve, _reject) => {
        const requestConfig = {
          method,
          url: `https://${host}${path}`,
          headers: {
            "Content-Type": "application/json",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0",
            Referer: `https://${host}/`,
            ...token ? { Authorization: `Bearer ${token}` } : {},
            ...userId ? { "New-Api-User": userId } : {},
            ...extraHeaders
          },
          timeout: CONFIG.REQUEST_TIMEOUT,
          onload: (res) => {
            try {
              const data = JSON.parse(res.responseText);
              if (res.status >= 200 && res.status < 300) {
                Log.debug(`[\u54CD\u5E94\u6210\u529F] ${method} ${host}${path}`, data);
                resolve(data);
              } else {
                Log.warn(`[\u54CD\u5E94\u9519\u8BEF] ${method} ${host}${path} - \u72B6\u6001\u7801: ${res.status}`, data);
                resolve({ success: false, error: `HTTP ${res.status}`, data });
              }
            } catch (e) {
              Log.error(`[\u89E3\u6790\u5931\u8D25] ${method} ${host}${path}`, e);
              resolve({ success: false, error: "\u89E3\u6790\u54CD\u5E94\u5931\u8D25" });
            }
          },
          onerror: (err) => {
            Log.error(`[\u7F51\u7EDC\u9519\u8BEF] ${method} ${host}${path}`, err);
            resolve({ success: false, error: "\u7F51\u7EDC\u9519\u8BEF" });
          },
          ontimeout: () => {
            Log.warn(`[\u8BF7\u6C42\u8D85\u65F6] ${method} ${host}${path}`);
            resolve({ success: false, error: "\u8BF7\u6C42\u8D85\u65F6" });
          }
        };
        if (body) requestConfig.data = JSON.stringify(body);
        GM_xmlhttpRequest(requestConfig);
      });
      return result;
    } finally {
      _release(isInteractive);
    }
  }
  async function _probeQuota(host, token, userId) {
    Log.debug(`[\u83B7\u53D6\u7528\u6237\u4FE1\u606F] ${host}`);
    const res = await request("GET", host, "/api/user/self", token, userId);
    if (res.success && res.data) {
      Log.debug(`[\u7528\u6237\u4FE1\u606F] ${host} - \u989D\u5EA6: ${res.data.quota}`);
      return { success: true, quota: res.data.quota, error: null };
    }
    Log.error(`[\u7528\u6237\u4FE1\u606F\u83B7\u53D6\u5931\u8D25] ${host}`, res);
    return { success: false, quota: null, error: res?.error || "\u8BF7\u6C42\u5931\u8D25" };
  }
  async function _probeCheckinStatus(host, token, userId, currentData) {
    const monthStr = getCurrentMonthString();
    const todayStr = getTodayString();
    if (currentData.checkinSupported === false) {
      Log.debug(`[\u7B7E\u5230\u6570\u636E] ${host} - LDOH \u6807\u8BB0\u4E0D\u652F\u6301\u7B7E\u5230\uFF0C\u8DF3\u8FC7\u63A2\u6D4B`);
      return {
        success: true,
        error: null,
        data: {
          checkedInToday: null,
          checkinSupported: false,
          lastCheckinDate: currentData.lastCheckinDate
        }
      };
    }
    Log.debug(`[\u83B7\u53D6\u7B7E\u5230\u6570\u636E] ${host} - \u6708\u4EFD: ${monthStr}`);
    const res = await request("GET", host, `/api/user/checkin?month=${monthStr}`, token, userId);
    if (res.success && res.data) {
      let checkedInToday = !!res.data?.stats?.checked_in_today;
      if (host === "wzw.pp.ua") checkedInToday = !!res.data?.checked_in;
      const lastCheckinDate = checkedInToday ? todayStr : currentData.lastCheckinDate;
      Log.debug(`[\u7B7E\u5230\u6570\u636E] ${host} - \u5DF2\u7B7E\u5230: ${checkedInToday}`);
      return {
        success: true,
        error: null,
        data: { checkedInToday, checkinSupported: true, lastCheckinDate }
      };
    }
    Log.warn(`[\u7B7E\u5230\u6570\u636E\u83B7\u53D6\u5931\u8D25] ${host} - \u63A5\u53E3\u8BF7\u6C42\u5931\u8D25`, res);
    return {
      success: false,
      error: res?.error || "\u8BF7\u6C42\u5931\u8D25",
      data: {
        checkedInToday: null,
        checkinSupported: currentData.checkinSupported ?? true,
        lastCheckinDate: currentData.lastCheckinDate
      }
    };
  }
  async function updateSiteStatus(host, userId, force = false, strict = false) {
    let data = getSiteData(host);
    const settings = GM_getValue(CONFIG.SETTINGS_KEY, { interval: CONFIG.DEFAULT_INTERVAL });
    if (!force && data.ts && Date.now() - data.ts < settings.interval * 60 * 1e3) {
      Log.debug(
        `[\u8DF3\u8FC7\u66F4\u65B0] ${host} - \u8DDD\u79BB\u4E0A\u6B21\u66F4\u65B0 ${Math.round((Date.now() - data.ts) / 6e4)} \u5206\u949F`
      );
      return data;
    }
    Log.info(`[\u5F00\u59CB\u66F4\u65B0] ${host} (\u5F3A\u5236: ${force})`);
    if (!data.token) {
      Log.warn(`[\u8DF3\u8FC7\u66F4\u65B0] ${host} - token \u4E0D\u5B58\u5728`);
      return data;
    }
    const quotaResult = await _probeQuota(host, data.token, userId);
    if (!quotaResult.success) {
      if (quotaResult.error === "\u8BF7\u6C42\u8D85\u65F6") {
        throw new Error(`${host} \u8BF7\u6C42\u8D85\u65F6`);
      }
      if (strict) {
        throw new Error(`${host} \u63A5\u53E3\u8BF7\u6C42\u5931\u8D25`);
      }
    }
    if (quotaResult.success && quotaResult.quota != null) data.quota = quotaResult.quota;
    const checkinResult = await _probeCheckinStatus(host, data.token, userId, data);
    if (!checkinResult.success) {
      if (checkinResult.error === "\u8BF7\u6C42\u8D85\u65F6") {
        throw new Error(`${host} \u8BF7\u6C42\u8D85\u65F6`);
      }
      if (strict) {
        throw new Error(`${host} \u63A5\u53E3\u8BF7\u6C42\u5931\u8D25`);
      }
    }
    Object.assign(data, checkinResult.data);
    data.userId = userId;
    saveSiteData(host, data);
    const checkinLabel = data.checkinSupported ? data.checkedInToday ? "\u662F" : "\u5426" : "\u4E0D\u652F\u6301";
    Log.success(`[\u66F4\u65B0\u5B8C\u6210] ${host} - \u989D\u5EA6: $${formatQuota(data.quota)}, \u7B7E\u5230: ${checkinLabel}`);
    return data;
  }
  async function fetchToken(host, userId) {
    try {
      let res = await request("GET", host, "/api/user/token", null, userId);
      if (!res.success || !res.data) {
        const sessionVal = document.cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith("session="))?.slice("session=".length);
        if (sessionVal) {
          Log.debug(`[Token] ${host} - \u5C1D\u8BD5 session cookie \u91CD\u8BD5`);
          res = await request("GET", host, "/api/user/token", null, userId, null, false, {
            Cookie: `session=${sessionVal}`
          });
        }
      }
      if (res.success && res.data) {
        Log.success(`[Token] ${host} - \u83B7\u53D6\u6210\u529F`);
        return res.data;
      }
      Log.error(`[Token] ${host} - \u83B7\u53D6\u5931\u8D25`, res);
      return null;
    } catch (e) {
      Log.error(`[Token] ${host} - \u5F02\u5E38`, e);
      return null;
    }
  }
  async function fetchSelf(host, token, userId) {
    return request("GET", host, "/api/user/self", token, userId);
  }
  async function fetchKeys(host, token, userId, page = 0) {
    try {
      const res = await request(
        "GET",
        host,
        `/api/token/?p=${page}&size=1000`,
        token,
        userId,
        null,
        true
      );
      return res.success ? Array.isArray(res.data) ? res.data : res.data?.items || [] : [];
    } catch (e) {
      Log.error(`[fetchKeys] ${host}`, e);
      return [];
    }
  }
  async function createToken(host, token, userId, name, group) {
    try {
      Log.debug(`[\u521B\u5EFA\u5BC6\u94A5] ${host} - \u540D\u79F0: ${name}, \u5206\u7EC4: ${group}`);
      const res = await request(
        "POST",
        host,
        "/api/token/",
        token,
        userId,
        {
          remain_quota: 0,
          expired_time: -1,
          unlimited_quota: true,
          model_limits_enabled: false,
          model_limits: "",
          cross_group_retry: false,
          name,
          group,
          allow_ips: ""
        },
        true
      );
      if (res.success) Log.success(`[\u5BC6\u94A5\u521B\u5EFA\u6210\u529F] ${host}`);
      else Log.error(`[\u5BC6\u94A5\u521B\u5EFA\u5931\u8D25] ${host}`, res);
      return res;
    } catch (e) {
      Log.error(`[\u521B\u5EFA\u5BC6\u94A5\u5F02\u5E38] ${host}`, e);
      return { success: false, error: "\u521B\u5EFA\u5BC6\u94A5\u5F02\u5E38" };
    }
  }
  async function deleteToken(host, token, userId, tokenId) {
    try {
      Log.debug(`[\u5220\u9664\u5BC6\u94A5] ${host} - ID: ${tokenId}`);
      const res = await request("DELETE", host, `/api/token/${tokenId}`, token, userId, null, true);
      if (res.success) Log.success(`[\u5BC6\u94A5\u5220\u9664\u6210\u529F] ${host}`);
      else Log.error(`[\u5BC6\u94A5\u5220\u9664\u5931\u8D25] ${host}`, res);
      return res;
    } catch (e) {
      Log.error(`[\u5220\u9664\u5BC6\u94A5\u5F02\u5E38] ${host}`, e);
      return { success: false, error: "\u5220\u9664\u5BC6\u94A5\u5F02\u5E38" };
    }
  }
  async function checkin(host, token, userId) {
    try {
      Log.debug(`[\u7B7E\u5230] ${host}`);
      const res = await request("POST", host, "/api/user/checkin", token, userId, null, true);
      if (res.success) {
        const awarded = res.data?.quota_awarded || 0;
        Log.success(`[\u7B7E\u5230\u6210\u529F] ${host} - \u83B7\u5F97\u989D\u5EA6: ${formatQuota(awarded)}`);
      } else if (res.message && res.message.includes("\u5DF2\u7B7E\u5230")) {
        Log.success(`[\u5DF2\u7B7E\u5230] ${host} - \u4ECA\u65E5\u5DF2\u7B7E\u5230`);
        res.alreadyCheckedIn = true;
      } else Log.error(`[\u7B7E\u5230\u5931\u8D25] ${host}`, res);
      return res;
    } catch (e) {
      Log.error(`[\u7B7E\u5230\u5F02\u5E38] ${host}`, e);
      return { success: false, error: "\u7B7E\u5230\u5F02\u5E38" };
    }
  }
  async function fetchDetails(host, token, userId) {
    try {
      Log.debug(`[\u83B7\u53D6\u8BE6\u60C5] ${host}`);
      const [pricingRes, keys] = await Promise.all([
        request("GET", host, "/api/pricing", token, userId, null, true),
        fetchKeys(host, token, userId)
      ]);
      const models = pricingRes.success ? pricingRes.data : [];
      return { models, keys };
    } catch (e) {
      Log.error(`[\u83B7\u53D6\u8BE6\u60C5\u5F02\u5E38] ${host}`, e);
      return { models: [], keys: [] };
    }
  }
  async function fetchGroups(host, token, userId) {
    try {
      Log.debug(`[\u83B7\u53D6\u5206\u7EC4\u5217\u8868] ${host}`);
      const res = await request("GET", host, "/api/user/self/groups", token, userId, null, true);
      if (res.success && res.data) return res.data;
      Log.warn(`[\u5206\u7EC4\u5217\u8868\u83B7\u53D6\u5931\u8D25] ${host}`, res);
      return {};
    } catch (e) {
      Log.error(`[\u83B7\u53D6\u5206\u7EC4\u5217\u8868\u5F02\u5E38] ${host}`, e);
      return {};
    }
  }
  var API = {
    request,
    updateSiteStatus,
    fetchToken,
    fetchSelf,
    fetchKeys,
    createToken,
    deleteToken,
    checkin,
    fetchDetails,
    fetchGroups
  };

  // src/ui/overlay.js
  function createOverlay(html) {
    injectStyles();
    const ov = document.createElement("div");
    ov.className = "ldh-overlay";
    ov.innerHTML = `<div class="ldh-dialog">${html}</div>`;
    ov.onclick = (e) => {
      if (e.target !== ov) return;
      ov.querySelector(".ldh-dialog").style.animation = `ldoh-zoom-in ${CONFIG.ANIMATION_FAST_MS}ms ease-in reverse forwards`;
      ov.style.animation = `ldoh-fade-in-blur ${CONFIG.ANIMATION_FAST_MS}ms ease-in reverse forwards`;
      setTimeout(() => ov.remove(), CONFIG.ANIMATION_FAST_MS);
    };
    document.body.appendChild(ov);
    return ov;
  }

  // src/ui/base.js
  var UI = {
    /**
     * 创建一个通用元素
     */
    element(tag, options = {}) {
      const el = document.createElement(tag);
      if (options.className) el.className = options.className;
      if (options.id) el.id = options.id;
      if (options.style) {
        if (typeof options.style === "string") {
          el.style.cssText = options.style;
        } else {
          Object.assign(el.style, options.style);
        }
      }
      if (options.innerHTML) el.innerHTML = options.innerHTML;
      if (options.textContent) el.textContent = options.textContent;
      if (options.title) el.title = options.title;
      if (options.dataset) {
        for (const [key, val] of Object.entries(options.dataset)) {
          el.dataset[key] = val;
        }
      }
      if (options.onClick) el.onclick = options.onClick;
      if (options.onMouseOver) el.onmouseover = options.onMouseOver;
      if (options.onMouseOut) el.onmouseout = options.onMouseOut;
      if (options.children) {
        options.children.forEach((child) => {
          if (child) el.appendChild(child);
        });
      }
      return el;
    },
    /**
     * 创建一个容器 (div)
     */
    div(options = {}) {
      return this.element("div", options);
    },
    /**
     * 创建一个 span
     */
    span(options = {}) {
      return this.element("span", options);
    },
    /**
     * 创建一个按钮
     */
    button(options = {}) {
      const el = this.element("button", options);
      if (options.type) el.type = options.type;
      if (options.disabled) el.disabled = options.disabled;
      return el;
    },
    /**
     * 创建一个输入框
     */
    input(options = {}) {
      const el = this.element("input", options);
      el.type = options.type || "text";
      if (options.placeholder) el.placeholder = options.placeholder;
      if (options.value) el.value = options.value;
      if (options.min) el.min = options.min;
      if (options.step) el.step = options.step;
      if (options.max) el.max = options.max;
      if (options.onFocus) el.onfocus = options.onFocus;
      if (options.onBlur) el.onblur = options.onBlur;
      if (options.onInput) el.oninput = options.onInput;
      if (options.onKeyDown) el.addEventListener("keydown", options.onKeyDown);
      return el;
    },
    /**
     * 创建 SVG 图标
     */
    icon(svgContent, options = {}) {
      const wrapper = this.element("div", options);
      wrapper.innerHTML = svgContent;
      return wrapper.firstElementChild || wrapper;
    },
    /**
     * 预定义的一些常用 SVG 字符串
     */
    ICONS: {
      REFRESH: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/></svg>',
      DETAILS: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg>',
      CLOSE: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
      CLOSE_LG: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
      TRASH: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
      PANEL: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>',
      EYE: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
      CHECKIN: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><polyline points="9 16 11 18 15 14"/></svg>',
      CHECKIN_OFF: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="7" y1="13" x2="17" y2="21"/><line x1="17" y1="13" x2="7" y2="21"/></svg>',
      LOCATE: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>'
    }
  };

  // src/ui/dialog.js
  var _detailLoadingHosts = /* @__PURE__ */ new Set();
  function _closeDetailDialog() {
    const ov = document.querySelector(".ldh-overlay");
    if (!ov) return;
    const dialog = ov.querySelector(".ldh-dialog");
    if (dialog) {
      dialog.style.animation = `ldoh-zoom-in ${CONFIG.ANIMATION_FAST_MS}ms ease-in reverse forwards`;
    }
    ov.style.animation = `ldoh-fade-in-blur ${CONFIG.ANIMATION_FAST_MS}ms ease-in reverse forwards`;
    setTimeout(() => ov.remove(), CONFIG.ANIMATION_FAST_MS);
  }
  function _buildDetailHeader(host) {
    return UI.div({
      className: "ldh-header",
      children: [
        UI.div({ className: "ldh-title", textContent: host }),
        UI.div({
          className: "ldh-close",
          innerHTML: UI.ICONS.CLOSE_LG,
          onClick: _closeDetailDialog
        })
      ]
    });
  }
  function _renderKeySection(host, data, keys, modelItems, modelsBadge, modelArray) {
    const container = UI.div({ className: "ldh-sec-wrapper" });
    const keysBadge = UI.span({ className: "ldh-sec-badge", textContent: keys.length });
    const keysGrid = UI.div({ className: "ldh-grid" });
    const refreshKeys = (list) => {
      keysGrid.innerHTML = "";
      if (list.length) {
        list.forEach(
          (k) => keysGrid.appendChild(
            _buildKeyItem(k, host, data, keysGrid, modelItems, modelsBadge, modelArray)
          )
        );
      } else {
        keysGrid.appendChild(
          UI.div({
            style: "grid-column:1/-1;text-align:center;padding:20px;color:var(--ldoh-text-light);",
            textContent: "\u6682\u65E0\u53EF\u7528\u5BC6\u94A5"
          })
        );
      }
    };
    const { createForm, createKeyBtn } = _buildCreateKeyForm(host, data, async () => {
      const newKeys = await API.fetchKeys(host, data.token, data.userId, 1);
      refreshKeys(newKeys);
      keysBadge.textContent = newKeys.length;
    });
    container.appendChild(
      UI.div({
        className: "ldh-sec-header",
        children: [
          UI.div({
            className: "ldh-sec-title",
            children: [UI.span({ textContent: "\u{1F511} \u5BC6\u94A5\u5217\u8868" }), keysBadge]
          }),
          createKeyBtn
        ]
      })
    );
    container.appendChild(createForm);
    container.appendChild(keysGrid);
    refreshKeys(keys);
    return container;
  }
  function _renderModelSection(models, modelItems, modelsBadge) {
    const container = UI.div({ className: "ldh-sec-wrapper" });
    container.appendChild(
      UI.div({
        className: "ldh-sec-header",
        children: [
          UI.div({
            className: "ldh-sec-title",
            children: [UI.span({ textContent: "\u{1F916} \u6A21\u578B\u5217\u8868" }), modelsBadge]
          })
        ]
      })
    );
    const modelsGrid = UI.div({ className: "ldh-grid-models" });
    if (models.length) {
      const fmtPrice = (v) => parseFloat(v.toFixed(6)).toString();
      models.forEach((m) => {
        const modelName = m.model_name || m;
        let priceHtml = "";
        if (typeof m.quota_type === "number") {
          priceHtml = m.quota_type === 1 ? `<div style="font-size:10px;font-weight:600;color:#64748b">$${fmtPrice(m.model_price)} /\u6B21</div>` : `<div style="font-size:10px;font-weight:600;color:#64748b">\u8F93\u5165: $${fmtPrice(m.model_ratio * 2)}/M \xB7 \u8F93\u51FA: $${fmtPrice(m.model_ratio * (m.completion_ratio || 1) * 2)}/M</div>`;
        }
        const item = UI.div({
          className: "ldh-item",
          dataset: { modelName, modelGroups: JSON.stringify(m.enable_groups || []) },
          innerHTML: `<div style="font-weight:600;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(modelName)}</div>${priceHtml}<div style="font-size:9px;color:var(--ldoh-text-light)">\u70B9\u51FB\u590D\u5236</div>`,
          onClick: () => {
            copy(modelName);
            Toast.success("\u5DF2\u590D\u5236\u6A21\u578B\u540D");
          }
        });
        modelsGrid.appendChild(item);
        modelItems.push(item);
      });
    } else {
      modelsGrid.appendChild(
        UI.div({
          style: "grid-column:1/-1;text-align:center;padding:20px;color:var(--ldoh-text-light);",
          textContent: "\u6682\u65E0\u53EF\u7528\u6A21\u578B"
        })
      );
    }
    container.appendChild(modelsGrid);
    return container;
  }
  function _buildCreateKeyForm(host, data, onCreated) {
    const createKeyBtn = UI.button({
      style: "padding:4px 12px;background:var(--ldoh-primary);color:white;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all 0.2s;",
      textContent: "+ \u521B\u5EFA\u5BC6\u94A5",
      onMouseOver: (e) => e.target.style.background = "var(--ldoh-primary-hover)",
      onMouseOut: (e) => e.target.style.background = "var(--ldoh-primary)"
    });
    const createForm = UI.div({
      style: "display:none;padding:16px;background:#f8fafc;border:1px solid var(--ldoh-border);border-radius:var(--ldoh-radius);margin-bottom:12px;"
    });
    const nameInput = UI.input({
      placeholder: "\u8BF7\u8F93\u5165\u5BC6\u94A5\u540D\u79F0",
      style: "width:100%;padding:8px 10px;border:1px solid var(--ldoh-border);border-radius:6px;font-size:13px;outline:none;transition:all 0.2s;box-sizing:border-box;",
      onFocus: (e) => e.target.style.borderColor = "var(--ldoh-primary)",
      onBlur: (e) => e.target.style.borderColor = "var(--ldoh-border)"
    });
    const groupSelect = document.createElement("select");
    groupSelect.style.cssText = "width:100%;padding:8px 10px;border:1px solid var(--ldoh-border);border-radius:6px;font-size:13px;outline:none;cursor:pointer;background:white;box-sizing:border-box;";
    const submitBtn = UI.button({
      textContent: "\u521B\u5EFA",
      style: "padding:8px 16px;background:var(--ldoh-primary);color:white;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;",
      onClick: async () => {
        const name = nameInput.value.trim();
        if (!name) {
          Toast.warning("\u8BF7\u8F93\u5165\u5BC6\u94A5\u540D\u79F0");
          nameInput.focus();
          return;
        }
        submitBtn.disabled = true;
        submitBtn.textContent = "\u521B\u5EFA\u4E2D...";
        submitBtn.style.opacity = "0.6";
        try {
          const result = await API.createToken(
            host,
            data.token,
            data.userId,
            name,
            groupSelect.value
          );
          if (result.success) {
            Toast.success("\u5BC6\u94A5\u521B\u5EFA\u6210\u529F");
            createForm.style.display = "none";
            createKeyBtn.textContent = "+ \u521B\u5EFA\u5BC6\u94A5";
            nameInput.value = "";
            onCreated?.();
          } else {
            Toast.error(result.message || "\u5BC6\u94A5\u521B\u5EFA\u5931\u8D25");
          }
        } catch (e) {
          Log.error("\u521B\u5EFA\u5BC6\u94A5\u5931\u8D25", e);
          Toast.error("\u521B\u5EFA\u5BC6\u94A5\u5931\u8D25");
        } finally {
          submitBtn.disabled = false;
          submitBtn.textContent = "\u521B\u5EFA";
          submitBtn.style.opacity = "1";
        }
      }
    });
    const cancelBtn = UI.button({
      textContent: "\u53D6\u6D88",
      style: "padding:8px 16px;background:#e2e8f0;color:var(--ldoh-text);border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;",
      onClick: () => {
        createForm.style.display = "none";
        createKeyBtn.textContent = "+ \u521B\u5EFA\u5BC6\u94A5";
        nameInput.value = "";
      }
    });
    createForm.appendChild(
      UI.div({
        style: "display:grid;grid-template-columns:1fr 1fr auto;gap:12px;align-items:end;",
        children: [
          UI.div({
            children: [
              UI.div({
                style: "font-size:12px;font-weight:600;margin-bottom:6px;",
                textContent: "\u5BC6\u94A5\u540D\u79F0"
              }),
              nameInput
            ]
          }),
          UI.div({
            children: [
              UI.div({
                style: "font-size:12px;font-weight:600;margin-bottom:6px;",
                textContent: "\u9009\u62E9\u5206\u7EC4"
              }),
              groupSelect
            ]
          }),
          UI.div({ style: "display:flex;gap:8px;", children: [cancelBtn, submitBtn] })
        ]
      })
    );
    createKeyBtn.onclick = async () => {
      if (createForm.style.display === "none") {
        createKeyBtn.disabled = true;
        createKeyBtn.textContent = "\u52A0\u8F7D\u4E2D...";
        try {
          const groups = await API.fetchGroups(host, data.token, data.userId);
          groupSelect.innerHTML = "";
          Object.entries(groups).forEach(([gName, gInfo]) => {
            const opt = document.createElement("option");
            opt.value = gName;
            opt.textContent = `${gName} - ${gInfo.desc} (\u500D\u7387: ${gInfo.ratio})`;
            groupSelect.appendChild(opt);
          });
          createForm.style.display = "block";
          createKeyBtn.textContent = "\u6536\u8D77\u8868\u5355";
          setTimeout(() => nameInput.focus(), 100);
        } catch (e) {
          Log.error("\u83B7\u53D6\u5206\u7EC4\u5217\u8868\u5931\u8D25", e);
          Toast.error("\u83B7\u53D6\u5206\u7EC4\u5217\u8868\u5931\u8D25");
        } finally {
          createKeyBtn.disabled = false;
        }
      } else {
        createForm.style.display = "none";
        createKeyBtn.textContent = "+ \u521B\u5EFA\u5BC6\u94A5";
        nameInput.value = "";
      }
    };
    return { createForm, createKeyBtn };
  }
  function _buildKeyItem(k, host, data, keysGrid, modelItems, modelsBadge, modelArray) {
    const item = UI.div({
      className: "ldh-item ldh-key-item",
      dataset: { group: k.group || "", key: `sk-${k.key}` },
      style: "position: relative;",
      innerHTML: `
      <div style="font-weight:700;color:var(--ldoh-text)">${escapeHtml(k.name || "\u672A\u547D\u540D")}</div>
      ${k.group ? `<div style="font-size:10px;color:var(--ldoh-primary);font-weight:600">Group: ${escapeHtml(k.group)}</div>` : ""}
      <div style="font-size:10px;color:var(--ldoh-text-light);font-family:monospace;overflow:hidden;text-overflow:ellipsis">sk-${k.key.substring(0, 16)}...</div>
    `
    });
    const deleteBtn = UI.div({
      className: "ldh-delete-btn",
      innerHTML: UI.ICONS.TRASH,
      style: "position:absolute;top:8px;right:8px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;cursor:pointer;opacity:0;transition:all 0.2s;color:var(--ldoh-danger);",
      title: "\u5220\u9664\u5BC6\u94A5",
      onMouseOver: (e) => e.target.closest(".ldh-delete-btn").style.background = "rgba(239,68,68,0.1)",
      onMouseOut: (e) => e.target.closest(".ldh-delete-btn").style.background = "transparent",
      onClick: (e) => {
        e.stopPropagation();
        if (!window.confirm(`\u5220\u9664\u5BC6\u94A5 "${k.name || "\u672A\u547D\u540D"}"\uFF1F`)) return;
        API.deleteToken(host, data.token, data.userId, k.id).then((res) => {
          if (res.success) {
            Toast.success("\u5BC6\u94A5\u5220\u9664\u6210\u529F");
            item.remove();
          } else Toast.error(res.message || "\u5BC6\u94A5\u5220\u9664\u5931\u8D25");
        });
      }
    });
    item.appendChild(deleteBtn);
    item.onmouseenter = () => deleteBtn.style.opacity = "1";
    item.onmouseleave = () => deleteBtn.style.opacity = "0";
    item.onclick = (e) => {
      if (e.target.closest(".ldh-delete-btn")) return;
      const isAlreadyActive = item.classList.contains("active");
      keysGrid.querySelectorAll(".ldh-item").forEach((el) => el.classList.remove("active"));
      let selectedGroup = null;
      if (!isAlreadyActive) {
        item.classList.add("active");
        selectedGroup = item.dataset.group;
        copy(item.dataset.key);
        Toast.success(`\u5DF2\u9009\u4E2D\u5206\u7EC4 ${selectedGroup || "\u9ED8\u8BA4"} \u5E76\u590D\u5236\u5BC6\u94A5`);
      } else {
        copy(item.dataset.key);
        Toast.success("\u5DF2\u590D\u5236\u5BC6\u94A5");
      }
      let visibleCount = 0;
      modelItems.forEach((mi) => {
        let isVisible = true;
        if (selectedGroup) {
          try {
            isVisible = JSON.parse(mi.dataset.modelGroups || "[]").includes(selectedGroup);
          } catch (_err) {
            isVisible = mi.dataset.modelName.toLowerCase().includes(selectedGroup.toLowerCase());
          }
        }
        mi.style.display = isVisible ? "" : "none";
        if (isVisible) visibleCount++;
      });
      modelsBadge.textContent = selectedGroup ? `${visibleCount}/${modelArray.length}` : modelArray.length;
    };
    return item;
  }
  async function showDetailsDialog(host, data) {
    if (_detailLoadingHosts.has(host)) return;
    _detailLoadingHosts.add(host);
    let loadingOverlay = null;
    try {
      loadingOverlay = createOverlay(
        '<div class="ldh-header"><div class="ldh-title">\u6B63\u5728\u83B7\u53D6\u5BC6\u94A5\u548C\u6A21\u578B...</div></div><div class="ldh-content" style="align-items:center;justify-content:center;min-height:200px"><div class="ldoh-refresh-btn loading">' + UI.ICONS.REFRESH + "</div></div>"
      );
      const details = await API.fetchDetails(host, data.token, data.userId);
      loadingOverlay.remove();
      const { models, keys } = details;
      const modelArray = models?.data && Array.isArray(models.data) ? models.data : Array.isArray(models) ? models : [];
      const dialog = UI.div({ className: "ldh-dialog", children: [_buildDetailHeader(host)] });
      const content = UI.div({ className: "ldh-content" });
      const modelItems = [];
      const modelsBadge = UI.span({ className: "ldh-sec-badge", textContent: modelArray.length });
      content.appendChild(
        _renderKeySection(host, data, keys || [], modelItems, modelsBadge, modelArray)
      );
      content.appendChild(_renderModelSection(modelArray, modelItems, modelsBadge));
      dialog.appendChild(content);
      const overlay = createOverlay("");
      overlay.querySelector(".ldh-dialog").replaceWith(dialog);
    } catch (e) {
      if (loadingOverlay?.parentNode) loadingOverlay.remove();
      Log.error(`[\u8BE6\u60C5\u5931\u8D25] ${host}`, e);
      Toast.error("\u83B7\u53D6\u8BE6\u60C5\u5931\u8D25");
    } finally {
      _detailLoadingHosts.delete(host);
    }
  }

  // src/utils/bus.js
  var EventBus = {
    _listeners: /* @__PURE__ */ new Map(),
    on(event, callback) {
      if (!this._listeners.has(event)) {
        this._listeners.set(event, /* @__PURE__ */ new Set());
      }
      this._listeners.get(event).add(callback);
      return () => this.off(event, callback);
    },
    off(event, callback) {
      if (this._listeners.has(event)) {
        this._listeners.get(event).delete(callback);
      }
    },
    emit(event, ...args) {
      if (this._listeners.has(event)) {
        for (const callback of this._listeners.get(event)) {
          try {
            callback(...args);
          } catch (e) {
            console.error(`[EventBus] Error in listener for event ${event}:`, e);
          }
        }
      }
    },
    clear(event) {
      if (event) {
        this._listeners.delete(event);
      } else {
        this._listeners.clear();
      }
    }
  };
  var UI_EVENTS = {
    SHOW_DETAILS: "site:show_details",
    GLOBAL_REFRESH: "ui:global_refresh",
    // 全局刷新信号
    DATA_CHANGED: "data:changed"
  };

  // src/services/site.js
  var SiteService = {
    /**
     * 刷新全部站点数据。
     */
    async refreshAll() {
      const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
      const sites = Object.entries(allData).filter(
        ([host, data]) => data.userId && data.token && !isBlacklisted(host)
      );
      const siteCount = sites.length;
      if (siteCount === 0) {
        Toast.info("\u6CA1\u6709\u7AD9\u70B9\u6570\u636E\u9700\u8981\u5237\u65B0");
        return;
      }
      const progressToast = Toast.show(`\u6B63\u5728\u5237\u65B0\u7AD9\u70B9 0/${siteCount}...`, "info", 0);
      let completedCount = 0;
      let successCount = 0;
      let failedCount = 0;
      const promises = sites.map(async ([host, data]) => {
        try {
          const fresh = await API.updateSiteStatus(host, data.userId, true);
          EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: fresh, renderable: true });
          successCount++;
        } catch (e) {
          Log.error(`[SiteRefresh] \u5237\u65B0\u7AD9\u70B9\u5931\u8D25: ${host}`, e);
          failedCount++;
        }
        completedCount++;
        const messageEl = progressToast.querySelector(".ldoh-toast-message");
        if (messageEl) {
          messageEl.textContent = `\u6B63\u5728\u5237\u65B0\u7AD9\u70B9 ${completedCount}/${siteCount}...`;
        }
      });
      await Promise.all(promises);
      Toast.remove(progressToast);
      if (failedCount > 0) {
        Toast.warning(`\u5237\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${successCount} \u4E2A\uFF0C\u5931\u8D25 ${failedCount} \u4E2A`, 4e3);
      } else {
        Toast.success(`\u5237\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${successCount} \u4E2A\uFF0C\u5931\u8D25 0 \u4E2A`, 3e3);
      }
      EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
    },
    /**
     * 一键签到符合条件的站点。
     * @param {boolean} showConfirm
     */
    async checkinEligibleSites(showConfirm = true) {
      const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
      const today = getTodayString();
      const sites = Object.entries(allData).filter(([host, data]) => {
        if (!data.userId || !data.token || data.checkinSupported === false) return false;
        if (isCheckinSkipped(host)) return false;
        const lastCheckinDate = data.lastCheckinDate || "1970-01-01";
        return lastCheckinDate !== today || data.checkedInToday === false;
      });
      if (sites.length === 0) {
        Toast.info("\u6240\u6709\u7AD9\u70B9\u4ECA\u5929\u90FD\u5DF2\u7B7E\u5230");
        return;
      }
      if (showConfirm) {
        const siteList = sites.map(([host, data]) => {
          const lastCheckin = data.lastCheckinDate || "\u4ECE\u672A";
          return `  \u2022 ${host} (\u4E0A\u6B21: ${lastCheckin})`;
        }).join("\n");
        const confirmed = window.confirm(
          `\u{1F381} \u5C06\u5BF9\u4EE5\u4E0B ${sites.length} \u4E2A\u7AD9\u70B9\u8FDB\u884C\u81EA\u52A8\u7B7E\u5230\uFF1A

${siteList}

\u6CE8\u610F\uFF1A\u90E8\u5206\u7AD9\u70B9\u53EF\u80FD\u6709 CF \u6821\u9A8C\uFF0C\u7B7E\u5230\u53EF\u80FD\u5931\u8D25\u6216\u8D85\u65F6\uFF0810\u79D2\uFF09

\u662F\u5426\u7EE7\u7EED\uFF1F`
        );
        if (!confirmed) return;
      }
      const progressToast = Toast.show(`\u6B63\u5728\u7B7E\u5230 0/${sites.length}...`, "info", 0);
      const siteResults = /* @__PURE__ */ new Map();
      let completedCount = 0;
      const failedSites = [];
      const checkinSite = async (host, data, updateProgress = true) => {
        try {
          const result = await API.checkin(host, data.token, data.userId);
          if (updateProgress) {
            completedCount++;
            const messageEl = progressToast.querySelector(".ldoh-toast-message");
            if (messageEl) {
              messageEl.textContent = `\u6B63\u5728\u7B7E\u5230 ${completedCount}/${sites.length}...`;
            }
          }
          if (result.success || result.alreadyCheckedIn) {
            siteResults.set(host, result.success ? "success" : "already");
            const siteData = getSiteData(host);
            siteData.lastCheckinDate = today;
            siteData.checkedInToday = true;
            if (result.success && result.data?.quota_awarded) {
              siteData.quota = (siteData.quota || 0) + result.data.quota_awarded;
            }
            saveSiteData(host, siteData);
            EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: siteData, renderable: true });
            return true;
          }
          if (result.error === "\u7B7E\u5230\u8D85\u65F6\uFF0815\u79D2\uFF09") {
            siteResults.set(host, "timeout");
            return false;
          }
          siteResults.set(host, "fail");
          return false;
        } catch (e) {
          Log.error(`[AutoCheckin] \u7B7E\u5230\u7AD9\u70B9\u5931\u8D25: ${host}`, e);
          siteResults.set(host, "fail");
          if (updateProgress) {
            completedCount++;
            const messageEl = progressToast.querySelector(".ldoh-toast-message");
            if (messageEl) {
              messageEl.textContent = `\u6B63\u5728\u7B7E\u5230 ${completedCount}/${sites.length}...`;
            }
          }
          return false;
        }
      };
      await Promise.all(
        sites.map(async ([host, data]) => {
          const success = await checkinSite(host, data);
          if (!success) failedSites.push([host, data]);
        })
      );
      const maxRetries = 2;
      for (let retry = 1; retry <= maxRetries && failedSites.length > 0; retry++) {
        const messageEl = progressToast.querySelector(".ldoh-toast-message");
        if (messageEl) {
          messageEl.textContent = `\u7B2C ${retry} \u6B21\u91CD\u8BD5 ${failedSites.length} \u4E2A\u5931\u8D25\u7AD9\u70B9...`;
        }
        const retrySites = [...failedSites];
        failedSites.length = 0;
        await Promise.all(
          retrySites.map(async ([host, data]) => {
            const success = await checkinSite(host, data, false);
            if (!success) failedSites.push([host, data]);
          })
        );
        completedCount = sites.length - failedSites.length;
        const progressMessageEl = progressToast.querySelector(".ldoh-toast-message");
        if (progressMessageEl) {
          progressMessageEl.textContent = `\u6B63\u5728\u7B7E\u5230 ${completedCount}/${sites.length}...`;
        }
      }
      Toast.remove(progressToast);
      let successCount = 0;
      let alreadyCheckedCount = 0;
      for (const status of siteResults.values()) {
        if (status === "success") successCount++;
        else if (status === "already") alreadyCheckedCount++;
      }
      if (successCount > 0 || alreadyCheckedCount > 0) {
        Toast.success(`\u7B7E\u5230\u5B8C\u6210\uFF01\u6210\u529F ${successCount} \u4E2A\uFF0C\u5DF2\u7B7E\u5230 ${alreadyCheckedCount} \u4E2A`, 5e3);
      } else {
        Toast.warning("\u7B7E\u5230\u5B8C\u6210\uFF0C\u4F46\u6CA1\u6709\u6210\u529F\u7684\u7AD9\u70B9", 5e3);
      }
      EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
    },
    /**
     * 批量刷新过期站点。
     */
    async refreshStaleSitesBatch() {
      const settings = GM_getValue(CONFIG.SETTINGS_KEY, {});
      const intervalMs = (settings.interval || CONFIG.DEFAULT_INTERVAL) * 60 * 1e3;
      const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
      const now = Date.now();
      const staleSites = Object.entries(allData).filter(([host, siteData]) => {
        return siteData.userId && siteData.token && !isBlacklisted(host) && siteData.ts && now - siteData.ts >= intervalMs;
      });
      if (!staleSites.length) return;
      let hasAnySuccessfulUpdate = false;
      await Promise.allSettled(
        staleSites.map(async ([host, siteData]) => {
          try {
            const fresh = await API.updateSiteStatus(host, siteData.userId, false);
            EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: fresh, renderable: true });
            hasAnySuccessfulUpdate = true;
          } catch (e) {
            Log.error(`[\u81EA\u52A8\u5237\u65B0] ${host}`, e);
          }
        })
      );
      if (hasAnySuccessfulUpdate) {
        EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
      }
    },
    /**
     * 单个站点刷新
     */
    async refreshSite(host, siteData, showToast2 = true) {
      try {
        const fresh = await API.updateSiteStatus(host, siteData.userId, true, true);
        EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: fresh, renderable: true });
        if (showToast2) Toast.success(`${host} \u5DF2\u66F4\u65B0`);
        return fresh;
      } catch (err) {
        Log.error(`[\u7AD9\u70B9\u5237\u65B0] ${host}`, err);
        if (showToast2)
          Toast.error(String(err?.message || "").includes("\u8BF7\u6C42\u8D85\u65F6") ? "\u5237\u65B0\u8D85\u65F6" : "\u5237\u65B0\u5931\u8D25");
        throw err;
      }
    },
    /**
     * 删除站点缓存数据,并通知 UI 移除对应元素
     */
    async deleteSiteData(host) {
      const normalizedHost = normalizeHost(host);
      const all = GM_getValue(CONFIG.STORAGE_KEY, {});
      if (all[normalizedHost]) {
        delete all[normalizedHost];
        GM_setValue(CONFIG.STORAGE_KEY, all);
        EventBus.emit(UI_EVENTS.DATA_CHANGED, {
          host: normalizedHost,
          next: null,
          renderable: false
        });
        EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
        return true;
      }
      return false;
    }
  };

  // src/ui/panel.js
  function _findCardByHost(host) {
    const target = normalizeHost(host);
    const container = document.querySelector(
      `.${CONFIG.DOM.HELPER_CONTAINER_CLASS}[data-host="${target}"]`
    );
    return container ? container.closest(CONFIG.DOM.CARD_SELECTOR) : null;
  }
  var FloatingPanel = {
    _fab: null,
    _panel: null,
    _isOpen: false,
    _searchQuery: "",
    _checkinFilter: "",
    _checkinRunning: false,
    _refreshAllRunning: false,
    _confirmPop: null,
    _confirmOutsideHandler: null,
    _settingsPop: null,
    _settingsOutsideHandler: null,
    _intervalPop: null,
    _intervalOutsideHandler: null,
    _concurrencyPop: null,
    _concurrencyOutsideHandler: null,
    _pendingRefresh: false,
    _refreshTimer: null,
    _rendering: false,
    init() {
      if (document.getElementById("ldoh-fab")) return;
      injectStyles();
      const fab = UI.button({
        id: "ldoh-fab",
        className: "ldoh-fab",
        title: "\u7AD9\u70B9\u603B\u89C8",
        innerHTML: `
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
          <rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
        </svg>
        <span class="ldoh-fab-badge" id="ldoh-fab-badge" style="display:none">0</span>
      `,
        onClick: (e) => {
          e.stopPropagation();
          this.toggle();
        }
      });
      document.body.appendChild(fab);
      this._fab = fab;
      const panel = UI.div({
        id: "ldoh-floating-panel",
        className: "ldoh-floating-panel",
        style: "display: none;"
      });
      document.body.appendChild(panel);
      this._panel = panel;
      document.addEventListener("click", (e) => {
        const isOnPopover = this._confirmPop && this._confirmPop.contains(e.target) || this._settingsPop && this._settingsPop.contains(e.target) || this._intervalPop && this._intervalPop.contains(e.target) || this._concurrencyPop && this._concurrencyPop.contains(e.target);
        if (this._isOpen && !panel.contains(e.target) && !fab.contains(e.target) && !isOnPopover) {
          this.close();
        }
      });
      EventBus.on(UI_EVENTS.DATA_CHANGED, (delta) => this.applyDelta(delta));
      EventBus.on(UI_EVENTS.GLOBAL_REFRESH, () => this.refresh());
      this._updateBadge();
      Log.debug("[FloatingPanel] \u521D\u59CB\u5316\u5B8C\u6210");
    },
    toggle() {
      this._isOpen ? this.close() : this.open();
    },
    open() {
      this._isOpen = true;
      this._panel.style.display = "flex";
      this._panel.classList.add("ldoh-panel-in");
      setTimeout(() => this._panel.classList.remove("ldoh-panel-in"), CONFIG.ANIMATION_NORMAL_MS);
      this.render();
    },
    close() {
      this._isOpen = false;
      this._searchQuery = "";
      this._removeIntervalPopover();
      this._removeSettingsMenu();
      this._removeConfirmPopover();
      this._removeConcurrencyPopover();
      this._panel.style.display = "none";
    },
    refresh() {
      this._updateBadge();
      if (!this._isOpen) return;
      if (this._confirmPop || this._settingsPop || this._intervalPop || this._concurrencyPop) {
        this._pendingRefresh = true;
        return;
      }
      clearTimeout(this._refreshTimer);
      this._refreshTimer = setTimeout(() => this.render(), 150);
    },
    _getCheckinMeta(siteData) {
      if (siteData.checkinSupported !== false) {
        if (isCheckedInToday(siteData)) {
          return { checkinClass: "ok", checkinText: "\u5DF2\u7B7E\u5230" };
        }
        if (siteData.checkedInToday === false || siteData.lastCheckinDate) {
          return { checkinClass: "no", checkinText: "\u672A\u7B7E\u5230" };
        }
      }
      return { checkinClass: "na", checkinText: "\u2500" };
    },
    _applyRowVisibility(row) {
      const matchSearch = !this._searchQuery || row.dataset.searchKey.includes(this._searchQuery);
      const matchFilter = !this._checkinFilter || row.dataset.checkinStatus === this._checkinFilter;
      row.style.display = matchSearch && matchFilter ? "" : "none";
    },
    /**
     * 局部更新面板单行
     */
    updateSiteRow(host, siteData) {
      if (!this._isOpen || !this._panel) return false;
      const normalizedHost = normalizeHost(host);
      const row = this._panel.querySelector(`.ldoh-panel-row[data-host="${normalizedHost}"]`);
      if (!row) return false;
      const balanceEl = row.querySelector(".ldoh-panel-balance-col");
      if (balanceEl) balanceEl.textContent = `$${formatQuota(siteData.quota)}`;
      const { checkinClass, checkinText } = this._getCheckinMeta(siteData);
      const checkinEl = row.querySelector(".ldoh-panel-checkin-col");
      if (checkinEl) {
        checkinEl.className = `ldoh-panel-checkin ldoh-panel-checkin-col ${checkinClass}`;
        checkinEl.textContent = checkinText;
      }
      row.dataset.checkinStatus = checkinClass;
      this._applyRowVisibility(row);
      return true;
    },
    applyDelta(delta) {
      this._updateBadge();
      if (!this._isOpen) return { handled: false, needRefresh: false };
      if (!delta?.renderable) return { handled: false, needRefresh: true };
      const updated = this.updateSiteRow(delta.host, delta.next || {});
      return { handled: updated, needRefresh: !updated };
    },
    refreshBadgeOnly() {
      this._updateBadge();
    },
    _updateBadge() {
      const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
      const count = Object.values(allData).filter((d) => d.userId || d.quota != null).length;
      const badge = document.getElementById("ldoh-fab-badge");
      if (badge) {
        badge.textContent = count;
        badge.style.display = count > 0 ? "flex" : "none";
      }
    },
    _showSettingsMenu(anchorEl) {
      if (!anchorEl) return;
      this._removeIntervalPopover();
      this._removeConfirmPopover();
      this._removeSettingsMenu();
      const actions = [
        { label: "\u8BBE\u7F6E\u66F4\u65B0\u95F4\u9694", handler: (triggerEl) => this._showIntervalPopover(triggerEl) },
        { label: "\u8BBE\u7F6E\u5E76\u53D1\u6570", handler: (triggerEl) => this._showConcurrencyPopover(triggerEl) },
        {
          label: "\u91CD\u7F6E\u68C0\u6D4B\u9ED1\u540D\u5355",
          handler: (triggerEl) => {
            this._showConfirmPopover(triggerEl, "\u786E\u8BA4\u91CD\u7F6E\u68C0\u6D4B\u9ED1\u540D\u5355\uFF1F", () => {
              GM_setValue(CONFIG.BLACKLIST_KEY, []);
              GM_setValue(CONFIG.BLACKLIST_REMOVED_KEY, []);
              Toast.success("\u68C0\u6D4B\u9ED1\u540D\u5355\u5DF2\u91CD\u7F6E");
              this.refresh();
            });
          }
        },
        {
          label: "\u91CD\u7F6E\u7B7E\u5230\u9ED1\u540D\u5355",
          handler: (triggerEl) => {
            this._showConfirmPopover(triggerEl, "\u786E\u8BA4\u91CD\u7F6E\u7B7E\u5230\u9ED1\u540D\u5355\uFF1F", () => {
              GM_setValue(CONFIG.CHECKIN_SKIP_KEY, []);
              GM_setValue(CONFIG.CHECKIN_SKIP_REMOVED_KEY, []);
              Toast.success("\u7B7E\u5230\u9ED1\u540D\u5355\u5DF2\u91CD\u7F6E");
              this.refresh();
            });
          }
        },
        {
          label: "\u6E05\u7406\u7F13\u5B58",
          danger: true,
          handler: (triggerEl) => {
            const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
            if (Object.keys(allData).length === 0) {
              Toast.info("\u7F13\u5B58\u5DF2\u7ECF\u662F\u7A7A\u7684");
              return;
            }
            this._showConfirmPopover(triggerEl, `\u786E\u8BA4\u6E05\u9664\u7F13\u5B58\uFF1F`, () => {
              GM_setValue(CONFIG.STORAGE_KEY, {});
              Toast.success("\u7F13\u5B58\u5DF2\u6E05\u7406\uFF0C\u9875\u9762\u5C06\u5237\u65B0", 2e3);
              setTimeout(() => location.reload(), 2e3);
            });
          }
        },
        { label: "\u4F7F\u7528\u8BF4\u660E", handler: () => this._showHelpDialog() }
      ];
      const pop = UI.div({ id: "ldoh-settings-pop", className: "ldoh-settings-pop" });
      actions.forEach(({ label, handler, danger }) => {
        pop.appendChild(
          UI.button({
            className: "ldoh-settings-item",
            textContent: label,
            style: danger ? "color: var(--ldoh-danger, #ef4444)" : "",
            onClick: (e) => {
              e.stopPropagation();
              this._removeSettingsMenu();
              handler(anchorEl);
            }
          })
        );
      });
      const rect = anchorEl.getBoundingClientRect();
      Object.assign(pop.style, {
        top: `${rect.bottom + 6}px`,
        right: `${window.innerWidth - rect.right}px`
      });
      document.body.appendChild(pop);
      this._settingsPop = pop;
      this._settingsOutsideHandler = (e) => {
        if (!pop.contains(e.target) && !anchorEl.contains(e.target)) this._removeSettingsMenu();
      };
      setTimeout(() => document.addEventListener("click", this._settingsOutsideHandler), 0);
    },
    _showHelpDialog() {
      const html = `
      <div style="padding:24px">
      <h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:var(--ldoh-text)">\u4F7F\u7528\u8BF4\u660E</h2>
      <div style="font-size:13px;color:var(--ldoh-text-muted);line-height:1.7;max-height:60vh;overflow-y:auto;padding-right:4px">
        <p><strong>LDOH New API Helper</strong><br>\u81EA\u52A8\u540C\u6B65\u7AD9\u70B9\u989D\u5EA6\u4E0E\u7B7E\u5230\u72B6\u6001\u3002</p>
      </div>
      <div style="text-align:right;margin-top:16px">
        <button type="button" class="ldh-dialog-close" style="padding:6px 20px;border-radius:6px;background:var(--ldoh-surface2);color:var(--ldoh-text);border:none;cursor:pointer;font-size:13px">\u5173\u95ED</button>
      </div>
      </div>`;
      const ov = createOverlay(html);
      const closeBtn = ov.querySelector(".ldh-dialog-close");
      if (closeBtn)
        closeBtn.onclick = () => {
          ov.style.opacity = "0";
          setTimeout(() => ov.remove(), 200);
        };
    },
    _showIntervalPopover(anchorEl) {
      if (!anchorEl) return;
      this._removeConfirmPopover();
      this._removeSettingsMenu();
      this._removeConcurrencyPopover();
      this._removeIntervalPopover();
      const current = GM_getValue(CONFIG.SETTINGS_KEY, {
        interval: CONFIG.DEFAULT_INTERVAL
      }).interval;
      const inputEl = UI.input({
        type: "number",
        min: "5",
        step: "1",
        value: current,
        className: "ldoh-interval-input"
      });
      const pop = UI.div({
        id: "ldoh-interval-pop",
        className: "ldoh-interval-pop",
        children: [
          UI.div({ className: "ldoh-interval-title", textContent: "\u8BBE\u7F6E\u66F4\u65B0\u95F4\u9694" }),
          inputEl,
          UI.div({ className: "ldoh-interval-hint", textContent: "\u5355\u4F4D\uFF1A\u5206\u949F\uFF0C\u6700\u5C0F\u503C 5 \u5206\u949F" }),
          UI.div({
            className: "ldoh-interval-actions",
            children: [
              UI.button({
                className: "ldoh-pop-btn ldoh-pop-cancel",
                textContent: "\u53D6\u6D88",
                onClick: (e) => {
                  e.stopPropagation();
                  this._removeIntervalPopover();
                }
              }),
              UI.button({
                className: "ldoh-pop-btn ldoh-pop-confirm",
                textContent: "\u4FDD\u5B58",
                onClick: (e) => {
                  e.stopPropagation();
                  const val = parseInt(inputEl.value, 10);
                  if (isNaN(val) || val < 5) {
                    Toast.error("\u65E0\u6548\u7684\u95F4\u9694\u503C");
                    return;
                  }
                  GM_setValue(CONFIG.SETTINGS_KEY, {
                    ...GM_getValue(CONFIG.SETTINGS_KEY, {}),
                    interval: val
                  });
                  Toast.success(`\u5DF2\u66F4\u65B0\u4E3A ${val} \u5206\u949F`);
                  this._removeIntervalPopover();
                }
              })
            ]
          })
        ]
      });
      const rect = anchorEl.getBoundingClientRect();
      Object.assign(pop.style, {
        top: `${rect.bottom + 6}px`,
        right: `${window.innerWidth - rect.right}px`
      });
      document.body.appendChild(pop);
      this._intervalPop = pop;
      pop.addEventListener("click", (e) => e.stopPropagation());
      setTimeout(() => inputEl.focus(), 0);
      this._intervalOutsideHandler = (e) => {
        if (!pop.contains(e.target) && !anchorEl.contains(e.target)) this._removeIntervalPopover();
      };
      setTimeout(() => document.addEventListener("click", this._intervalOutsideHandler), 0);
    },
    _removeConfirmPopover() {
      if (this._confirmOutsideHandler)
        document.removeEventListener("click", this._confirmOutsideHandler);
      this._confirmOutsideHandler = null;
      if (this._confirmPop) {
        this._confirmPop.remove();
        this._confirmPop = null;
      }
      if (!this._rendering) this._flushPendingRefresh();
    },
    _showConfirmPopover(anchorEl, text, onConfirm) {
      if (!anchorEl) return;
      this._removeIntervalPopover();
      this._removeSettingsMenu();
      this._removeConfirmPopover();
      this._removeConcurrencyPopover();
      const pop = UI.div({
        id: "ldoh-confirm-pop",
        className: "ldoh-confirm-pop",
        children: [
          UI.span({ style: "white-space:pre-line", textContent: text }),
          UI.button({
            className: "ldoh-pop-btn ldoh-pop-cancel",
            textContent: "\u53D6\u6D88",
            onClick: (e) => {
              e.stopPropagation();
              this._removeConfirmPopover();
            }
          }),
          UI.button({
            className: "ldoh-pop-btn ldoh-pop-confirm",
            textContent: "\u786E\u8BA4",
            onClick: (e) => {
              e.stopPropagation();
              this._removeConfirmPopover();
              onConfirm?.();
            }
          })
        ]
      });
      const rect = anchorEl.getBoundingClientRect();
      Object.assign(pop.style, {
        top: `${rect.top - 48}px`,
        right: `${window.innerWidth - rect.right}px`
      });
      document.body.appendChild(pop);
      this._confirmPop = pop;
      this._confirmOutsideHandler = (e) => {
        if (!pop.contains(e.target) && !anchorEl.contains(e.target)) this._removeConfirmPopover();
      };
      setTimeout(() => document.addEventListener("click", this._confirmOutsideHandler), 0);
    },
    _removeIntervalPopover() {
      if (this._intervalOutsideHandler)
        document.removeEventListener("click", this._intervalOutsideHandler);
      this._intervalOutsideHandler = null;
      if (this._intervalPop) {
        this._intervalPop.remove();
        this._intervalPop = null;
      }
      if (!this._rendering) this._flushPendingRefresh();
    },
    _removeConcurrencyPopover() {
      if (this._concurrencyOutsideHandler)
        document.removeEventListener("click", this._concurrencyOutsideHandler);
      this._concurrencyOutsideHandler = null;
      if (this._concurrencyPop) {
        this._concurrencyPop.remove();
        this._concurrencyPop = null;
      }
      if (!this._rendering) this._flushPendingRefresh();
    },
    _showConcurrencyPopover(anchorEl) {
      if (!anchorEl) return;
      this._removeConfirmPopover();
      this._removeSettingsMenu();
      this._removeIntervalPopover();
      this._removeConcurrencyPopover();
      const s = GM_getValue(CONFIG.SETTINGS_KEY, {});
      const curConcurrent = s.maxConcurrent || CONFIG.DEFAULT_MAX_CONCURRENT;
      const curBackground = s.maxBackground || CONFIG.DEFAULT_MAX_BACKGROUND;
      const totalInput = UI.input({
        type: "number",
        min: "1",
        max: "50",
        value: curConcurrent,
        className: "ldoh-interval-input"
      });
      const bgInput = UI.input({
        type: "number",
        min: "1",
        max: "50",
        value: curBackground,
        className: "ldoh-interval-input"
      });
      const pop = UI.div({
        id: "ldoh-concurrency-pop",
        className: "ldoh-interval-pop",
        style: "width:240px;",
        children: [
          UI.div({ className: "ldoh-interval-title", textContent: "\u8BBE\u7F6E\u5E76\u53D1\u6570" }),
          UI.div({
            children: [
              UI.div({ style: "font-size:11px;margin-bottom:4px;", textContent: "\u603B\u5E76\u53D1\u6570" }),
              totalInput
            ]
          }),
          UI.div({
            children: [
              UI.div({ style: "font-size:11px;margin-bottom:4px;", textContent: "\u540E\u53F0\u5E76\u53D1\u6570" }),
              bgInput
            ]
          }),
          UI.div({ className: "ldoh-interval-hint", textContent: "\u540E\u53F0\u5E76\u53D1\u5E94 \u2264 \u603B\u5E76\u53D1" }),
          UI.div({
            className: "ldoh-interval-actions",
            children: [
              UI.button({
                className: "ldoh-pop-btn ldoh-pop-cancel",
                textContent: "\u53D6\u6D88",
                onClick: (e) => {
                  e.stopPropagation();
                  this._removeConcurrencyPopover();
                }
              }),
              UI.button({
                className: "ldoh-pop-btn ldoh-pop-confirm",
                textContent: "\u4FDD\u5B58",
                onClick: (e) => {
                  e.stopPropagation();
                  const total = parseInt(totalInput.value, 10);
                  const bg = parseInt(bgInput.value, 10);
                  if (bg > total) {
                    Toast.error("\u540E\u53F0\u5E76\u53D1\u4E0D\u80FD\u5927\u4E8E\u603B\u5E76\u53D1");
                    return;
                  }
                  GM_setValue(CONFIG.SETTINGS_KEY, {
                    ...GM_getValue(CONFIG.SETTINGS_KEY, {}),
                    maxConcurrent: total,
                    maxBackground: bg
                  });
                  Toast.success("\u5E76\u53D1\u8BBE\u7F6E\u5DF2\u66F4\u65B0");
                  this._removeConcurrencyPopover();
                }
              })
            ]
          })
        ]
      });
      const rect = anchorEl.getBoundingClientRect();
      Object.assign(pop.style, {
        top: `${rect.bottom + 6}px`,
        right: `${window.innerWidth - rect.right}px`
      });
      document.body.appendChild(pop);
      this._concurrencyPop = pop;
      pop.addEventListener("click", (e) => e.stopPropagation());
      setTimeout(() => totalInput.focus(), 0);
      this._concurrencyOutsideHandler = (e) => {
        if (!pop.contains(e.target) && !anchorEl.contains(e.target)) this._removeConcurrencyPopover();
      };
      setTimeout(() => document.addEventListener("click", this._concurrencyOutsideHandler), 0);
    },
    _removeSettingsMenu() {
      if (this._settingsOutsideHandler)
        document.removeEventListener("click", this._settingsOutsideHandler);
      this._settingsOutsideHandler = null;
      if (this._settingsPop) {
        this._settingsPop.remove();
        this._settingsPop = null;
      }
      if (!this._rendering) this._flushPendingRefresh();
    },
    _flushPendingRefresh() {
      if (this._pendingRefresh && this._isOpen && !this._confirmPop && !this._settingsPop && !this._intervalPop && !this._concurrencyPop) {
        this._pendingRefresh = false;
        this.render();
      }
    },
    render() {
      if (!this._panel) return;
      this._rendering = true;
      const existingBody = this._panel.querySelector(".ldoh-panel-body");
      const savedScroll = existingBody ? existingBody.scrollTop : 0;
      this._removeIntervalPopover();
      this._removeSettingsMenu();
      this._removeConfirmPopover();
      this._removeConcurrencyPopover();
      const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
      const sorted = Object.entries(allData).filter(([, d]) => d.userId || d.quota != null).sort(([, a], [, b]) => (b.quota || 0) - (a.quota || 0));
      const totalBalance = sorted.reduce((sum, [, d]) => sum + (d.quota || 0), 0);
      this._panel.innerHTML = "";
      this._panel.appendChild(this._buildHeader(sorted, totalBalance));
      const { searchBar, bindSearch } = this._buildSearchBar();
      this._panel.appendChild(searchBar);
      const body = this._buildBody(sorted);
      this._panel.appendChild(body);
      bindSearch(body);
      body.scrollTop = savedScroll;
      this._rendering = false;
    },
    _buildHeader(sorted, totalBalance) {
      const hd = UI.div({
        className: "ldoh-panel-hd",
        innerHTML: `
        <div class="ldoh-panel-hd-title">${UI.ICONS.PANEL} \u7AD9\u70B9\u603B\u89C8 <span class="ldh-sec-badge">${sorted.length}</span></div>
        <div class="ldoh-panel-hd-total">\u5408\u8BA1 <strong style="color:#d97706">$${formatQuota(totalBalance)}</strong></div>
      `
      });
      const refreshAllBtn = UI.div({
        className: `ldoh-btn ldoh-refresh-btn ${this._refreshAllRunning ? "loading" : ""}`,
        title: "\u5237\u65B0\u6240\u6709\u7AD9\u70B9\u6570\u636E",
        innerHTML: UI.ICONS.REFRESH,
        onClick: (e) => {
          e.stopPropagation();
          if (this._refreshAllRunning || refreshAllBtn.classList.contains("loading")) return;
          this._showConfirmPopover(refreshAllBtn, "\u786E\u8BA4\u5237\u65B0\u5168\u90E8\uFF1F", async () => {
            this._refreshAllRunning = true;
            refreshAllBtn.classList.add("loading");
            try {
              await SiteService.refreshAll();
            } finally {
              this._refreshAllRunning = false;
              this.refresh();
            }
          });
        }
      });
      const checkinBtn = UI.div({
        className: `ldoh-btn ldoh-refresh-btn ${this._checkinRunning ? "loading" : ""}`,
        title: "\u4E00\u952E\u7B7E\u5230",
        innerHTML: UI.ICONS.CHECKIN,
        onClick: (e) => {
          e.stopPropagation();
          if (this._checkinRunning || checkinBtn.classList.contains("loading")) return;
          this._showConfirmPopover(checkinBtn, "\u786E\u8BA4\u81EA\u52A8\u7B7E\u5230\uFF1F", async () => {
            this._checkinRunning = true;
            checkinBtn.classList.add("loading");
            try {
              await SiteService.checkinEligibleSites(false);
            } finally {
              this._checkinRunning = false;
              this.refresh();
            }
          });
        }
      });
      const settingsBtn = UI.div({
        className: "ldoh-btn",
        title: "\u8BBE\u7F6E",
        innerHTML: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 .99-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06-.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51.99H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
        onClick: (e) => {
          e.stopPropagation();
          this._showSettingsMenu(settingsBtn);
        }
      });
      hd.appendChild(refreshAllBtn);
      hd.appendChild(checkinBtn);
      hd.appendChild(settingsBtn);
      hd.appendChild(
        UI.div({
          className: "ldoh-btn",
          title: "\u5173\u95ED",
          innerHTML: UI.ICONS.CLOSE,
          onClick: () => this.close()
        })
      );
      return hd;
    },
    _buildSearchBar() {
      const searchBar = UI.div({
        className: "ldoh-panel-search",
        innerHTML: `
        <div class="ldoh-panel-search-wrap">
          <svg class="ldoh-panel-search-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
          <input class="ldoh-panel-search-input" placeholder="\u641C\u7D22\u7AD9\u70B9..." value="${escapeHtml(this._searchQuery)}">
        </div>
        <div class="ldoh-filter-bar">
          <span class="ldoh-filter-chip ${this._checkinFilter === "" ? "active" : ""}" data-filter="">\u5168\u90E8</span>
          <span class="ldoh-filter-chip ${this._checkinFilter === "ok" ? "active" : ""}" data-filter="ok">\u5DF2\u7B7E\u5230</span>
          <span class="ldoh-filter-chip ${this._checkinFilter === "no" ? "active" : ""}" data-filter="no">\u672A\u7B7E\u5230</span>
          <span class="ldoh-filter-chip ${this._checkinFilter === "na" ? "active" : ""}" data-filter="na">\u4E0D\u652F\u6301/\u65E0\u6CD5\u68C0\u6D4B\u7B7E\u5230</span>
        </div>
      `
      });
      const bindSearch = (body) => {
        const input = searchBar.querySelector(".ldoh-panel-search-input");
        input.oninput = () => {
          clearTimeout(this._searchTimer);
          this._searchTimer = setTimeout(() => {
            this._searchQuery = input.value.toLowerCase().trim();
            body.querySelectorAll(".ldoh-panel-row").forEach((row) => this._applyRowVisibility(row));
          }, 200);
        };
        searchBar.querySelectorAll(".ldoh-filter-chip").forEach((chip) => {
          chip.onclick = () => {
            this._checkinFilter = chip.dataset.filter;
            searchBar.querySelectorAll(".ldoh-filter-chip").forEach((c) => c.classList.toggle("active", c.dataset.filter === this._checkinFilter));
            body.querySelectorAll(".ldoh-panel-row").forEach((row) => this._applyRowVisibility(row));
          };
        });
      };
      return { searchBar, bindSearch };
    },
    _buildBody(sorted) {
      const body = UI.div({ className: "ldoh-panel-body" });
      if (!sorted.length) {
        body.appendChild(UI.div({ className: "ldoh-panel-empty", textContent: "\u6682\u65E0\u7AD9\u70B9\u6570\u636E" }));
      } else {
        sorted.forEach(([host, siteData]) => body.appendChild(this._buildSiteRow(host, siteData)));
      }
      return body;
    },
    _buildSiteRow(host, siteData) {
      const isBlk = isBlacklisted(host);
      const { checkinClass, checkinText } = this._getCheckinMeta(siteData);
      const row = UI.div({
        className: "ldoh-panel-row",
        dataset: {
          host: normalizeHost(host),
          searchKey: `${siteData.siteName || ""} ${host}`.toLowerCase(),
          checkinStatus: checkinClass
        }
      });
      row.appendChild(
        UI.div({
          className: "ldoh-panel-name",
          innerHTML: `<span class="ldoh-panel-name-main">${siteData.siteName || host}</span><span class="ldoh-panel-name-host">${host}</span>`
        })
      );
      row.appendChild(
        UI.div({
          className: `ldoh-panel-checkin ldoh-panel-checkin-col ${checkinClass}`,
          textContent: checkinText
        })
      );
      row.appendChild(
        UI.div({
          className: "ldoh-panel-balance ldoh-panel-balance-col",
          textContent: `$${formatQuota(siteData.quota)}`
        })
      );
      if (!isBlk) {
        row.appendChild(
          UI.div({
            className: "ldoh-btn ldoh-details-btn",
            title: "\u5BC6\u94A5\u4E0E\u6A21\u578B\u8BE6\u60C5",
            innerHTML: UI.ICONS.DETAILS,
            onClick: async (e) => {
              e.stopPropagation();
              const btn = e.currentTarget;
              if (btn.classList.contains("loading")) return;
              btn.classList.add("loading");
              try {
                await showDetailsDialog(host, siteData);
              } finally {
                btn.classList.remove("loading");
              }
            }
          })
        );
      } else {
        row.appendChild(UI.div({ style: "width:22px" }));
      }
      if (!isBlk) {
        const refreshBtn = UI.div({
          className: "ldoh-btn ldoh-refresh-btn",
          title: "\u5237\u65B0\u6570\u636E",
          innerHTML: UI.ICONS.REFRESH,
          onClick: async (e) => {
            if (refreshBtn.classList.contains("loading")) return;
            refreshBtn.classList.add("loading");
            try {
              await SiteService.refreshSite(host, siteData);
            } finally {
              refreshBtn.classList.remove("loading");
            }
          }
        });
        row.appendChild(refreshBtn);
      } else {
        row.appendChild(UI.div({ style: "width:22px" }));
      }
      row.appendChild(
        UI.div({
          className: "ldoh-btn",
          title: "\u5B9A\u4F4D\u5361\u7247",
          innerHTML: UI.ICONS.LOCATE,
          onClick: () => {
            const card = _findCardByHost(host);
            if (card) {
              card.scrollIntoView({ behavior: "smooth", block: "center" });
              card.style.outline = "2px solid var(--ldoh-primary)";
              card.style.outlineOffset = "2px";
              setTimeout(() => {
                card.style.outline = "";
                card.style.outlineOffset = "";
              }, 2e3);
            } else {
              Toast.warning(`\u672A\u627E\u5230 ${host} \u7684\u5361\u7247`);
            }
          }
        })
      );
      const blkBtn = UI.div({
        className: "ldoh-btn",
        title: isBlk ? "\u88AB\u52A8\u76D1\u63A7\uFF08\u70B9\u51FB\u6062\u590D\u4E3B\u52A8\u68C0\u6D4B\uFF09" : "\u4E3B\u52A8\u68C0\u6D4B\uFF08\u70B9\u51FB\u5207\u6362\u4E3A\u88AB\u52A8\u76D1\u63A7\uFF09",
        style: `color: ${isBlk ? "#9ca3af" : "var(--ldoh-success)"}`,
        innerHTML: UI.ICONS.EYE,
        onClick: (e) => {
          e.stopPropagation();
          this._showConfirmPopover(
            blkBtn,
            isBlk ? "\u6062\u590D\u4E3B\u52A8\u68C0\u6D4B\uFF1F" : "\u52A0\u5165\u9ED1\u540D\u5355\uFF08\u88AB\u52A8\u76D1\u63A7\uFF09\uFF1F",
            () => {
              toggleBlacklist(host);
              Toast.success(`${host} \u72B6\u6001\u5DF2\u66F4\u65B0`);
              this.refresh();
            }
          );
        }
      });
      row.appendChild(blkBtn);
      const isSkipped = isCheckinSkipped(host);
      const noSupport = siteData.checkinSupported === false;
      const skipBtn = UI.div({
        className: "ldoh-btn",
        title: noSupport ? "\u4E0D\u652F\u6301\u7B7E\u5230" : isSkipped ? "\u5DF2\u8DF3\u8FC7\u7B7E\u5230\uFF08\u70B9\u51FB\u6062\u590D\uFF09" : "\u81EA\u52A8\u7B7E\u5230\u4E2D\uFF08\u70B9\u51FB\u8DF3\u8FC7\uFF09",
        style: `color: ${noSupport || isSkipped || isBlk ? "#9ca3af" : "var(--ldoh-success)"}; cursor: ${noSupport || isBlk ? "default" : "pointer"}`,
        innerHTML: noSupport ? UI.ICONS.CHECKIN_OFF : UI.ICONS.CHECKIN,
        onClick: (e) => {
          if (noSupport || isBlk) return;
          const reason = getBuiltinCheckinSkipReason(host);
          this._showConfirmPopover(
            skipBtn,
            isSkipped ? `\u6062\u590D\u81EA\u52A8\u7B7E\u5230\uFF1F${reason ? `
(\u539F\u56E0: ${reason})` : ""}` : "\u8DF3\u8FC7\u81EA\u52A8\u7B7E\u5230\uFF1F",
            () => {
              toggleCheckinSkip(host);
              Toast.success(`${host} \u7B7E\u5230\u7B56\u7565\u5DF2\u66F4\u65B0`);
              this.refresh();
            }
          );
        }
      });
      row.appendChild(skipBtn);
      const delBtn = UI.div({
        className: "ldoh-btn",
        title: "\u5220\u9664\u7F13\u5B58\u6570\u636E",
        style: "color: var(--ldoh-danger)",
        innerHTML: UI.ICONS.TRASH,
        onMouseOver: (e) => e.currentTarget.style.background = "rgba(239, 68, 68, 0.1)",
        onMouseOut: (e) => e.currentTarget.style.background = "transparent",
        onClick: (e) => {
          e.stopPropagation();
          this._showConfirmPopover(delBtn, "\u786E\u8BA4\u5F7B\u5E95\u5220\u9664\u8BE5\u7AD9\u70B9\u7F13\u5B58\uFF1F", () => {
            SiteService.deleteSiteData(host);
            Toast.success(`\u5DF2\u5220\u9664 ${host} \u7F13\u5B58`);
          });
        }
      });
      row.appendChild(delBtn);
      this._applyRowVisibility(row);
      return row;
    }
  };

  // src/ui/card.js
  var CardView = {
    _observer: null,
    /**
     * 初始化:开启扫描、注册观察器、订阅事件
     */
    init() {
      injectStyles();
      const scheduleRescan = debounce(() => this.rescan(), CONFIG.DEBOUNCE_DELAY);
      EventBus.on(UI_EVENTS.DATA_CHANGED, (delta) => {
        if (delta.renderable) {
          if (!this.updateByHost(delta.host, delta.next)) {
            scheduleRescan();
          }
        } else {
          this.removeByHost(delta.host);
        }
      });
      EventBus.on(UI_EVENTS.GLOBAL_REFRESH, () => scheduleRescan());
      this.rescan();
      this._observer = new MutationObserver((mutations) => {
        const hasCardChanges = mutations.some(
          (m) => [...m.addedNodes, ...m.removedNodes].some(
            (node) => node instanceof Element && (node.matches?.(CONFIG.DOM.CARD_SELECTOR) || node.querySelector?.(CONFIG.DOM.CARD_SELECTOR))
          )
        );
        if (hasCardChanges) scheduleRescan();
      });
      this._observer.observe(document.body, { childList: true, subtree: true });
      Log.debug("[CardView] \u521D\u59CB\u5316\u5B8C\u6210\uFF0C\u5DF2\u5F00\u542F\u81EA\u52A8\u76D1\u63A7");
    },
    /**
     * 扫描全页卡片并挂载 UI
     */
    rescan() {
      const cards = Array.from(document.querySelectorAll(CONFIG.DOM.CARD_SELECTOR));
      if (cards.length === 0) return;
      cards.forEach((card) => {
        const links = Array.from(card.querySelectorAll("a"));
        const siteLink = links.find((a) => a.href.startsWith("http") && !a.href.includes("linux.do")) || links[0];
        if (!siteLink) return;
        try {
          const host = normalizeHost(new URL(siteLink.href).hostname);
          const container = this.ensureHostAnchor(card, host);
          const siteData = getSiteData(host);
          if (siteData.userId || siteData.quota != null) {
            this.render(card, host, siteData);
          } else if (container) {
            container.innerHTML = "";
            container.style.display = "none";
          }
        } catch (_e) {
        }
      });
    },
    /**
     * 快速更新所有匹配 host 的卡片内容
     */
    updateByHost(host, data) {
      const target = normalizeHost(host);
      const containers = document.querySelectorAll(
        `.${CONFIG.DOM.HELPER_CONTAINER_CLASS}[data-host="${target}"]`
      );
      if (containers.length === 0) return false;
      containers.forEach((container) => {
        container.style.display = "";
        this.renderContent(container, host, data);
      });
      return true;
    },
    /**
     * 移除匹配 host 的辅助 UI
     */
    removeByHost(host) {
      const target = normalizeHost(host);
      const containers = document.querySelectorAll(
        `.${CONFIG.DOM.HELPER_CONTAINER_CLASS}[data-host="${target}"]`
      );
      containers.forEach((c) => c.remove());
      return containers.length > 0;
    },
    /**
     * 渲染/挂载卡片助手 UI
     */
    render(card, host, data) {
      const container = this.ensureHostAnchor(card, host);
      container.style.display = "";
      this.renderContent(container, host, data);
    },
    /**
     * 确保卡片存在可定位锚点,并写入 data-host
     */
    ensureHostAnchor(card, host) {
      const targetHost = normalizeHost(host);
      let container = card.querySelector(`.${CONFIG.DOM.HELPER_CONTAINER_CLASS}`);
      if (!container) {
        container = UI.div({
          className: CONFIG.DOM.HELPER_CONTAINER_CLASS,
          dataset: { host: targetHost }
        });
        const ut = Array.from(card.querySelectorAll("div")).find(
          (el) => el.textContent.includes("\u66F4\u65B0\u65F6\u95F4") && (el.children.length === 0 || el.querySelector(`.${CONFIG.DOM.HELPER_CONTAINER_CLASS}`))
        );
        if (ut) {
          Object.assign(ut.style, {
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
            gap: "8px"
          });
          if (ut.children.length === 0) {
            const text = UI.span({ textContent: ut.textContent.trim() });
            ut.textContent = "";
            ut.appendChild(text);
          }
          ut.appendChild(container);
        } else {
          Object.assign(container.style, {
            position: "absolute",
            bottom: "8px",
            right: "8px"
          });
          card.appendChild(container);
        }
      }
      container.dataset.host = targetHost;
      return container;
    },
    /**
     * 纯内容渲染逻辑
     */
    renderContent(container, host, data) {
      container.innerHTML = "";
      const isOk = isCheckedInToday(data);
      const hasCheckin = data.checkinSupported !== false && (isOk || data.checkedInToday === false || data.lastCheckinDate);
      container.appendChild(
        UI.div({
          className: "ldoh-info-bar",
          children: [
            UI.span({
              style: { color: "#d97706" },
              textContent: `$${formatQuota(data.quota)}`
            }),
            hasCheckin ? UI.span({ style: { opacity: "0.5" }, textContent: "|" }) : null,
            hasCheckin ? UI.span({
              style: {
                color: isOk ? "var(--ldoh-success)" : "var(--ldoh-warning)"
              },
              textContent: isOk ? "\u5DF2\u7B7E\u5230" : "\u672A\u7B7E\u5230"
            }) : null
          ]
        })
      );
      if (!isBlacklisted(host)) {
        container.appendChild(
          UI.div({
            className: "ldoh-btn ldoh-refresh-btn",
            title: "\u5237\u65B0\u6570\u636E",
            innerHTML: UI.ICONS.REFRESH,
            onClick: async (e) => {
              e.preventDefault();
              e.stopPropagation();
              const btn = e.currentTarget;
              if (btn.classList.contains("loading")) return;
              btn.classList.add("loading");
              try {
                await SiteService.refreshSite(host, data);
              } finally {
                btn.classList.remove("loading");
              }
            }
          })
        );
        container.appendChild(
          UI.div({
            className: "ldoh-btn ldoh-details-btn",
            title: "\u8BE6\u60C5",
            innerHTML: UI.ICONS.DETAILS,
            onClick: async (e) => {
              e.preventDefault();
              e.stopPropagation();
              const btn = e.currentTarget;
              if (btn.classList.contains("loading")) return;
              btn.classList.add("loading");
              try {
                await showDetailsDialog(host, data);
              } finally {
                btn.classList.remove("loading");
              }
            }
          })
        );
      }
    },
    destroy() {
      if (this._observer) {
        this._observer.disconnect();
        this._observer = null;
      }
    }
  };

  // src/ui/sync.js
  function toObject(value) {
    return value && typeof value === "object" && !Array.isArray(value) ? value : {};
  }
  function toComparable(data) {
    if (!data || typeof data !== "object") return null;
    return {
      userId: data.userId ?? null,
      quota: data.quota ?? null,
      checkedInToday: data.checkedInToday ?? null,
      lastCheckinDate: data.lastCheckinDate ?? null,
      checkinSupported: data.checkinSupported ?? null,
      siteName: data.siteName ?? null
    };
  }
  function isRenderable(data) {
    return !!(data && (data.userId || data.quota != null));
  }
  function computeStorageDiff(oldValue, newValue) {
    const oldData = toObject(oldValue);
    const nextData = toObject(newValue);
    const allHosts = /* @__PURE__ */ new Set([...Object.keys(oldData), ...Object.keys(nextData)]);
    const deltas = [];
    allHosts.forEach((rawHost) => {
      const host = normalizeHost(rawHost);
      if (!host) return;
      const prev = oldData[rawHost] || null;
      const next = nextData[rawHost] || null;
      const prevComparable = toComparable(prev);
      const nextComparable = toComparable(next);
      const changed = JSON.stringify(prevComparable) !== JSON.stringify(nextComparable);
      if (!changed) return;
      deltas.push({
        host,
        prev,
        next,
        changed,
        added: !prev && !!next,
        removed: !!prev && !next,
        renderable: isRenderable(next)
      });
    });
    return deltas;
  }
  function attachStorageSync({ storageKey, remoteOnly = true } = {}) {
    if (!storageKey) throw new Error("attachStorageSync: storageKey is required");
    return GM_addValueChangeListener(storageKey, (_name, oldValue, newValue, remote) => {
      if (remoteOnly && !remote) return;
      const deltas = computeStorageDiff(oldValue, newValue);
      if (!deltas.length) return;
      deltas.forEach((delta) => {
        EventBus.emit(UI_EVENTS.DATA_CHANGED, delta);
      });
      EventBus.emit(UI_EVENTS.PANEL_REFRESH);
    });
  }

  // src/hooks.js
  function _processSitesResponse(sites) {
    const entries = [];
    sites.forEach((site) => {
      try {
        const host = normalizeHost(new URL(site.apiBaseUrl).hostname);
        if (!host) return;
        entries.push({
          host,
          name: site.name || host,
          supportsCheckin: site.supportsCheckin === true
        });
      } catch (_e) {
      }
    });
    if (!entries.length) return;
    GM_setValue(
      CONFIG.WHITELIST_KEY,
      entries.map((e) => e.host)
    );
    const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
    let changedCount = 0;
    entries.forEach(({ host, name, supportsCheckin }) => {
      const cur = allData[host] || {};
      if (cur.siteName !== name || cur.checkinSupported !== supportsCheckin) {
        allData[host] = { ...cur, siteName: name, checkinSupported: supportsCheckin };
        changedCount++;
      }
    });
    if (changedCount > 0) {
      GM_setValue(CONFIG.STORAGE_KEY, allData);
      EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
    }
    Log.debug(`[\u7AD9\u70B9\u76D1\u63A7] \u7AD9\u70B9\u5217\u8868\u5DF2\u540C\u6B65: ${entries.length} \u4E2A`);
  }
  var Hooks = {
    /**
     * 在 LDOH 门户 hook fetch,拦截 GET /api/sites 响应。
     */
    installPortalSitesFetchHook() {
      try {
        const _origFetch = unsafeWindow.fetch;
        unsafeWindow.fetch = new Proxy(_origFetch, {
          apply(target, thisArg, args) {
            const [input, init] = args;
            const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
            const method = (init?.method ?? "GET").toUpperCase();
            const result = Reflect.apply(target, thisArg, args);
            if (method === "GET" && url.includes("/api/sites") && !url.includes("mode=runaway")) {
              result.then(async (res) => {
                try {
                  const data = await res.clone().json();
                  if (Array.isArray(data.sites)) _processSitesResponse(data.sites);
                } catch (_e) {
                }
              }).catch(() => {
              });
            }
            return result;
          }
        });
        Log.debug("[\u7AD9\u70B9\u76D1\u63A7] /api/sites hook \u5DF2\u542F\u52A8");
      } catch (e) {
        Log.warn("[\u7AD9\u70B9\u76D1\u63A7] installPortalSitesFetchHook \u5931\u8D25", e);
      }
    },
    /**
     * 在公益站页面 hook XHR,监控用户手动签到。
     */
    installCheckinXhrHook() {
      try {
        const XHR = unsafeWindow.XMLHttpRequest;
        if (XHR.prototype.__ldoh_hooked) return;
        XHR.prototype.__ldoh_hooked = true;
        const _open = XHR.prototype.open;
        const _send = XHR.prototype.send;
        XHR.prototype.open = function(method, url, ...rest) {
          this._ldoh_method = method;
          this._ldoh_url = url;
          return _open.apply(this, [method, url, ...rest]);
        };
        XHR.prototype.send = function(_body) {
          if (this._ldoh_method?.toUpperCase() === "POST" && typeof this._ldoh_url === "string" && this._ldoh_url.includes("/api/user/checkin")) {
            this.addEventListener("load", function() {
              try {
                const res = JSON.parse(this.responseText);
                if (res.success) {
                  const host = normalizeHost(window.location.hostname);
                  const siteData = getSiteData(host);
                  siteData.checkedInToday = true;
                  siteData.lastCheckinDate = getTodayString();
                  if (res.data?.quota_awarded) {
                    siteData.quota = (siteData.quota || 0) + res.data.quota_awarded;
                  }
                  saveSiteData(host, siteData);
                  EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: siteData, renderable: true });
                  Log.success(`[\u7B7E\u5230\u76D1\u63A7] ${host} - \u7B7E\u5230\u6210\u529F`);
                }
              } catch (e) {
                Log.debug("[\u7B7E\u5230\u76D1\u63A7] \u89E3\u6790\u5931\u8D25", e);
              }
            });
          }
          return _send.apply(this, arguments);
        };
        Log.debug("[\u7B7E\u5230\u76D1\u63A7] XHR hook \u5DF2\u542F\u52A8");
      } catch (e) {
        Log.warn("[\u7B7E\u5230\u76D1\u63A7] XHR hook \u5931\u8D25", e);
      }
    },
    /**
     * 薄荷公益站(up.x666.me)签到监控
     */
    installX666CheckinHook() {
      try {
        if (unsafeWindow.__ldoh_x666_fetch_hooked) return;
        unsafeWindow.__ldoh_x666_fetch_hooked = true;
        const _origFetch = unsafeWindow.fetch;
        unsafeWindow.fetch = new Proxy(_origFetch, {
          apply(target, thisArg, args) {
            const [input, init] = args;
            const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
            const method = (init?.method ?? "GET").toUpperCase();
            const result = Reflect.apply(target, thisArg, args);
            if (method === "POST" && url.includes("/api/checkin/spin")) {
              result.then(async (res) => {
                try {
                  const data = await res.clone().json();
                  if (data?.success) {
                    const host = "x666.me";
                    const siteData = getSiteData(host);
                    siteData.checkedInToday = true;
                    siteData.lastCheckinDate = getTodayString();
                    if (typeof data.new_balance === "number") siteData.quota = data.new_balance;
                    saveSiteData(host, siteData);
                    EventBus.emit(UI_EVENTS.DATA_CHANGED, {
                      host,
                      next: siteData,
                      renderable: true
                    });
                    Log.success(`[\u7B7E\u5230\u76D1\u63A7] ${host} - \u7B7E\u5230\u6210\u529F`);
                  }
                } catch (_e) {
                }
              }).catch(() => {
              });
            }
            return result;
          }
        });
        Log.debug("[\u7B7E\u5230\u76D1\u63A7] x666 hook \u5DF2\u542F\u52A8");
      } catch (e) {
        Log.warn("[\u7B7E\u5230\u76D1\u63A7] x666 hook \u5931\u8D25", e);
      }
    },
    /**
     * runanytime 福利转盘监控
     */
    installRunanytimeWheelHook() {
      try {
        if (unsafeWindow.__ldoh_runanytime_wheel_fetch_hooked) return;
        unsafeWindow.__ldoh_runanytime_wheel_fetch_hooked = true;
        const _origFetch = unsafeWindow.fetch;
        unsafeWindow.fetch = new Proxy(_origFetch, {
          apply(target, thisArg, args) {
            const [input, init] = args;
            const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
            const method = (init?.method ?? "GET").toUpperCase();
            const result = Reflect.apply(target, thisArg, args);
            if (method === "POST" && url.includes("/api/wheel")) {
              result.then(async (res) => {
                try {
                  const data = await res.clone().json();
                  if (data?.success !== true) return;
                  const host = "runanytime.hxi.me";
                  const siteData = getSiteData(host);
                  if (!siteData.token || !siteData.userId) return;
                  const selfRes = await API.fetchSelf(host, siteData.token, siteData.userId);
                  if (selfRes.success && selfRes.data?.quota != null) {
                    siteData.quota = selfRes.data.quota;
                    saveSiteData(host, siteData);
                    EventBus.emit(UI_EVENTS.DATA_CHANGED, {
                      host,
                      next: siteData,
                      renderable: true
                    });
                    Log.success(`[\u798F\u5229\u8F6C\u76D8] ${host} - \u4F59\u989D\u5DF2\u540C\u6B65`);
                  }
                } catch (_e) {
                }
              }).catch(() => {
              });
            }
            return result;
          }
        });
        Log.debug("[\u798F\u5229\u8F6C\u76D8] runanytime hook \u5DF2\u542F\u52A8");
      } catch (e) {
        Log.warn("[\u798F\u5229\u8F6C\u76D8] runanytime hook \u5931\u8D25", e);
      }
    },
    /**
     * hook XHR,监听 GET /api/user/self,被动同步余额
     */
    installSelfProfileXhrHook() {
      try {
        const XHR = unsafeWindow.XMLHttpRequest;
        if (XHR.prototype.__ldoh_self_hooked) return;
        XHR.prototype.__ldoh_self_hooked = true;
        const _open = XHR.prototype.open;
        XHR.prototype.open = function(method, url, ...rest) {
          this._ldoh_self_method = method;
          this._ldoh_self_url = url;
          return _open.apply(this, [method, url, ...rest]);
        };
        const _send = XHR.prototype.send;
        XHR.prototype.send = function(_body) {
          if (this._ldoh_self_method?.toUpperCase() === "GET" && typeof this._ldoh_self_url === "string" && this._ldoh_self_url.includes("/api/user/self")) {
            this.addEventListener("load", function() {
              try {
                const res = JSON.parse(this.responseText);
                if (res.success && res.data?.quota != null) {
                  const host = normalizeHost(window.location.hostname);
                  const siteData = getSiteData(host);
                  siteData.quota = res.data.quota;
                  saveSiteData(host, siteData);
                  EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: siteData, renderable: true });
                  Log.success(`[\u4F59\u989D\u76D1\u63A7] ${host} - \u4F59\u989D\u5DF2\u66F4\u65B0`);
                }
              } catch (_e) {
              }
            });
          }
          return _send.apply(this, arguments);
        };
        Log.debug("[\u4F59\u989D\u76D1\u63A7] XHR hook \u5DF2\u542F\u52A8");
      } catch (e) {
        Log.warn("[\u4F59\u989D\u76D1\u63A7] XHR hook \u5931\u8D25", e);
      }
    },
    /**
     * hook XHR,监听 POST /api/user/topup,兑换码成功后更新余额。
     */
    installTopupXhrHook() {
      try {
        const XHR = unsafeWindow.XMLHttpRequest;
        if (XHR.prototype.__ldoh_topup_hooked) return;
        XHR.prototype.__ldoh_topup_hooked = true;
        const _open = XHR.prototype.open;
        XHR.prototype.open = function(method, url, ...rest) {
          this._ldoh_topup_method = method;
          this._ldoh_topup_url = url;
          return _open.apply(this, [method, url, ...rest]);
        };
        const _send = XHR.prototype.send;
        XHR.prototype.send = function(_body) {
          if (this._ldoh_topup_method?.toUpperCase() === "POST" && typeof this._ldoh_topup_url === "string" && this._ldoh_topup_url.includes("/api/user/topup")) {
            this.addEventListener("load", function() {
              try {
                const res = JSON.parse(this.responseText);
                if (res.success && res.data > 0) {
                  const host = normalizeHost(window.location.hostname);
                  const siteData = getSiteData(host);
                  API.fetchSelf(host, siteData.token, siteData.userId).then((selfRes) => {
                    if (selfRes.success && selfRes.data?.quota != null) {
                      siteData.quota = selfRes.data.quota;
                      saveSiteData(host, siteData);
                      EventBus.emit(UI_EVENTS.DATA_CHANGED, {
                        host,
                        next: siteData,
                        renderable: true
                      });
                      Log.success(`[\u5151\u6362\u7801] ${host} - \u4F59\u989D\u5DF2\u66F4\u65B0`);
                    }
                  }).catch(() => {
                  });
                }
              } catch (_e) {
              }
            });
          }
          return _send.apply(this, arguments);
        };
        Log.debug("[\u5151\u6362\u7801] XHR hook \u5DF2\u542F\u52A8");
      } catch (e) {
        Log.warn("[\u5151\u6362\u7801] XHR hook \u5931\u8D25", e);
      }
    }
  };

  // src/main.js
  if (window.top === window.self && !window.__LDOH_HELPER_RUNNING__) {
    window.__LDOH_HELPER_RUNNING__ = true;
    "use strict";
    let storageChangeListenerId = null;
    let staleDataRefreshTimer = null;
    async function ensureSiteIdentityAndSync(host) {
      const syncStatus = async (uid) => {
        const normalizedHost = normalizeHost(host);
        const siteData = getSiteData(normalizedHost);
        if (!siteData.token) {
          const token = await API.fetchToken(normalizedHost, uid);
          if (token) {
            siteData.token = token;
            saveSiteData(normalizedHost, siteData);
          }
        }
        await SiteService.refreshSite(host, siteData, false);
      };
      let userId = getAndSyncUserId(host);
      if (userId) {
        syncStatus(userId).catch(() => {
        });
      } else {
        userId = await waitForLogin() || await new Promise((resolve) => watchLoginStatus(resolve));
        if (userId) {
          Toast.success("\u8BC6\u522B\u5230\u767B\u5F55\uFF0C\u6B63\u5728\u540C\u6B65...");
          userId = getAndSyncUserId(host);
          syncStatus(userId).catch(() => {
          });
        }
      }
    }
    async function init() {
      try {
        const host = window.location.hostname;
        const isPortal = host === CONFIG.PORTAL_HOST;
        EventBus.on(UI_EVENTS.SHOW_DETAILS, (host2, data) => showDetailsDialog(host2, data));
        if (isPortal) {
          Log.info("\u73AF\u5883: LDOH \u95E8\u6237");
          CardView.init();
          FloatingPanel.init();
          Hooks.installPortalSitesFetchHook();
          storageChangeListenerId = attachStorageSync({ storageKey: CONFIG.STORAGE_KEY });
          SiteService.refreshStaleSitesBatch();
          staleDataRefreshTimer = setInterval(() => SiteService.refreshStaleSitesBatch(), 6e4);
        } else {
          Log.info(`\u73AF\u5883: \u7AD9\u70B9 ${host}`);
          if (host === "up.x666.me") return Hooks.installX666CheckinHook();
          if (host === "fuli.hxi.me") return Hooks.installRunanytimeWheelHook();
          if (await isNewApiSite()) {
            Hooks.installCheckinXhrHook();
            Hooks.installSelfProfileXhrHook();
            Hooks.installTopupXhrHook();
            if (!isBlacklisted(normalizeHost(host))) await ensureSiteIdentityAndSync(host);
          }
        }
      } catch (e) {
        Log.error("\u521D\u59CB\u5316\u5931\u8D25", e);
      }
    }
    window.addEventListener("beforeunload", () => {
      CardView.destroy();
      if (storageChangeListenerId) GM_removeValueChangeListener(storageChangeListenerId);
      if (staleDataRefreshTimer) clearInterval(staleDataRefreshTimer);
    });
    init();
  }
})();