// ==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 // @downloadURL https://update.greasyfork.icu/scripts/565844/LDOH%20New%20API%20Helper.user.js // @updateURL https://update.greasyfork.icu/scripts/565844/LDOH%20New%20API%20Helper.meta.js // ==/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: '', error: '', warning: '', info: '' }; 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 = `
${ICONS[type] || ICONS.info}
${escapeHtml(message)}
`; 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 = `
${html}
`; 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: '', DETAILS: '', CLOSE: '', CLOSE_LG: '', TRASH: '', PANEL: '', EYE: '', CHECKIN: '', CHECKIN_OFF: '', LOCATE: '' } }; // 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 ? `
$${fmtPrice(m.model_price)} /\u6B21
` : `
\u8F93\u5165: $${fmtPrice(m.model_ratio * 2)}/M \xB7 \u8F93\u51FA: $${fmtPrice(m.model_ratio * (m.completion_ratio || 1) * 2)}/M
`; } const item = UI.div({ className: "ldh-item", dataset: { modelName, modelGroups: JSON.stringify(m.enable_groups || []) }, innerHTML: `
${escapeHtml(modelName)}
${priceHtml}
\u70B9\u51FB\u590D\u5236
`, 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: `
${escapeHtml(k.name || "\u672A\u547D\u540D")}
${k.group ? `
Group: ${escapeHtml(k.group)}
` : ""}
sk-${k.key.substring(0, 16)}...
` }); 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( '
\u6B63\u5728\u83B7\u53D6\u5BC6\u94A5\u548C\u6A21\u578B...
' + UI.ICONS.REFRESH + "
" ); 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: ` `, 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 = `

\u4F7F\u7528\u8BF4\u660E

LDOH New API Helper
\u81EA\u52A8\u540C\u6B65\u7AD9\u70B9\u989D\u5EA6\u4E0E\u7B7E\u5230\u72B6\u6001\u3002

`; 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: `
${UI.ICONS.PANEL} \u7AD9\u70B9\u603B\u89C8 ${sorted.length}
\u5408\u8BA1 $${formatQuota(totalBalance)}
` }); 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: ``, 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: `
\u5168\u90E8 \u5DF2\u7B7E\u5230 \u672A\u7B7E\u5230 \u4E0D\u652F\u6301/\u65E0\u6CD5\u68C0\u6D4B\u7B7E\u5230
` }); 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: `${siteData.siteName || host}${host}` }) ); 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(); } })();