Greasy Fork is available in English.
LDOH New API 助手(余额查询、自动签到、密钥管理、模型查询)
// ==UserScript==
// @name LDOH New API Helper
// @namespace jojojotarou.ldoh.newapi.helper
// @version 2.0.2
// @description LDOH New API 助手(余额查询、自动签到、密钥管理、模型查询)
// @author @JoJoJotarou
// @match https://ldoh.105117.xyz/*
// @include *
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_addValueChangeListener
// @grant GM_removeValueChangeListener
// @grant unsafeWindow
// @connect *
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
// src/config.js
var CONFIG = {
STORAGE_KEY: "ldoh_newapi_data",
SETTINGS_KEY: "ldoh_newapi_settings",
WHITELIST_KEY: "ldoh_site_whitelist",
BLACKLIST_KEY: "ldoh_site_blacklist",
BLACKLIST_REMOVED_KEY: "ldoh_site_blacklist_removed",
CHECKIN_SKIP_KEY: "ldoh_checkin_skip",
CHECKIN_SKIP_REMOVED_KEY: "ldoh_checkin_skip_removed",
BLACKLIST: ["elysiver.h-e.top", "anthorpic.us.ci", "demo.voapi.top", "windhub.cc", "ai.qaq.al"],
DEFAULT_CHECKIN_SKIP: /* @__PURE__ */ new Map([
["api.67.si", "CF Turnstile \u62E6\u622A"],
["runanytime.hxi.me", "CF Turnstile \u62E6\u622A"],
["anyrouter.top", "\u767B\u5F55\u81EA\u52A8\u7B7E\u5230"],
["x666.me", "\u7AD9\u5916\u7B7E\u5230"]
]),
DEFAULT_INTERVAL: 60,
DEFAULT_MAX_CONCURRENT: 15,
DEFAULT_MAX_BACKGROUND: 10,
QUOTA_CONVERSION_RATE: 5e5,
PORTAL_HOST: "ldoh.105117.xyz",
REQUEST_TIMEOUT: 1e4,
DEBOUNCE_DELAY: 800,
LOGIN_CHECK_INTERVAL: 500,
LOGIN_CHECK_MAX_ATTEMPTS: 10,
ANIMATION_FAST_MS: 200,
ANIMATION_NORMAL_MS: 300,
DOM: {
CARD_SELECTOR: ".rounded-xl.shadow.group.relative",
HELPER_CONTAINER_CLASS: "ldoh-helper-container",
STYLE_ID: "ldoh-helper-css"
}
};
// src/logger.js
var _print = (level, msg, color, bg, ...args) => console.log(
`%c LDOH %c ${level.toUpperCase()} %c ${msg}`,
"background: #6366f1; color: white; border-radius: 3px 0 0 3px; font-weight: bold; padding: 1px 4px",
`background: ${bg}; color: ${color}; border-radius: 0 3px 3px 0; font-weight: bold; padding: 1px 4px`,
"color: inherit; font-weight: normal",
...args
);
var _printDebug = (level, msg, color, bg, ...args) => console.debug(
`%c LDOH %c ${level.toUpperCase()} %c ${msg}`,
"background: #6366f1; color: white; border-radius: 3px 0 0 3px; font-weight: bold; padding: 1px 4px",
`background: ${bg}; color: ${color}; border-radius: 0 3px 3px 0; font-weight: bold; padding: 1px 4px`,
"color: inherit; font-weight: normal",
...args
);
var Log = {
info: (msg, ...args) => _print("info", msg, "#fff", "#3b82f6", ...args),
success: (msg, ...args) => _print("ok", msg, "#fff", "#10b981", ...args),
warn: (msg, ...args) => _print("warn", msg, "#000", "#f59e0b", ...args),
error: (msg, ...args) => _print("err", msg, "#fff", "#ef4444", ...args),
debug: (msg, ...args) => _printDebug("debug", msg, "#fff", "#8b5cf6", ...args)
};
// src/utils/format.js
function formatQuota(q) {
if (q === void 0 || q === null || isNaN(q)) return "0.00";
return (q / CONFIG.QUOTA_CONVERSION_RATE).toFixed(2);
}
function normalizeHost(host) {
if (!host || typeof host !== "string") {
Log.warn("normalizeHost \u6536\u5230\u65E0\u6548\u7684 host", host);
return "";
}
return host.toLowerCase().split(":")[0];
}
function escapeHtml(str) {
if (!str || typeof str !== "string") return "";
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// src/utils/storage.js
function _isInManagedList(n, builtinList, addedKey, removedKey) {
const added = GM_getValue(addedKey, []);
const removed = GM_getValue(removedKey, []);
const inBuiltin = Array.isArray(builtinList) ? builtinList.includes(n) : builtinList instanceof Map ? builtinList.has(n) : Object.prototype.hasOwnProperty.call(builtinList, n);
return inBuiltin && !removed.includes(n) || added.includes(n);
}
function _toggleManagedList(n, builtinList, addedKey, removedKey) {
const inBuiltin = Array.isArray(builtinList) ? builtinList.includes(n) : builtinList instanceof Map ? builtinList.has(n) : Object.prototype.hasOwnProperty.call(builtinList, n);
if (_isInManagedList(n, builtinList, addedKey, removedKey)) {
if (inBuiltin) {
const removed = GM_getValue(removedKey, []);
if (!removed.includes(n)) {
removed.push(n);
GM_setValue(removedKey, removed);
}
} else {
const added = GM_getValue(addedKey, []);
const idx = added.indexOf(n);
if (idx >= 0) {
added.splice(idx, 1);
GM_setValue(addedKey, added);
}
}
return false;
} else {
const removed = GM_getValue(removedKey, []);
const ridx = removed.indexOf(n);
if (ridx >= 0) {
removed.splice(ridx, 1);
GM_setValue(removedKey, removed);
} else {
const added = GM_getValue(addedKey, []);
if (!added.includes(n)) {
added.push(n);
GM_setValue(addedKey, added);
}
}
return true;
}
}
function saveSiteData(host, data) {
try {
const all = GM_getValue(CONFIG.STORAGE_KEY, {});
const key = normalizeHost(host);
all[key] = { ...all[key] || {}, ...data, ts: Date.now() };
GM_setValue(CONFIG.STORAGE_KEY, all);
Log.debug(`\u4FDD\u5B58\u7AD9\u70B9\u6570\u636E: ${key}`, data);
} catch (e) {
Log.error(`\u4FDD\u5B58\u7AD9\u70B9\u6570\u636E\u5931\u8D25: ${host}`, e);
}
}
function getSiteData(host) {
try {
const all = GM_getValue(CONFIG.STORAGE_KEY, {});
const key = normalizeHost(host);
return all[key] || {};
} catch (e) {
Log.error(`\u83B7\u53D6\u7AD9\u70B9\u6570\u636E\u5931\u8D25: ${host}`, e);
return {};
}
}
function getAndSyncUserId(host) {
try {
const userStr = localStorage.getItem("user");
if (!userStr) {
Log.debug("localStorage \u4E2D\u672A\u627E\u5230 user \u6570\u636E");
return null;
}
const user = JSON.parse(userStr);
if (!user || typeof user !== "object" || !user.id) {
Log.warn("user \u6570\u636E\u683C\u5F0F\u65E0\u6548\u6216\u7F3A\u5931 ID");
return null;
}
const userId = String(user.id);
const normalizedHost = normalizeHost(host);
const siteData = getSiteData(normalizedHost);
if (siteData.userId !== userId) {
Log.info(`[\u8EAB\u4EFD\u540C\u6B65] \u4E3A ${normalizedHost} \u8BC6\u522B\u5230\u65B0\u7528\u6237 ID: ${userId}`);
saveSiteData(normalizedHost, { userId });
}
return userId;
} catch (e) {
Log.error("\u540C\u6B65\u7528\u6237 ID \u5931\u8D25", e);
return null;
}
}
function isBlacklisted(host) {
return _isInManagedList(
normalizeHost(host),
CONFIG.BLACKLIST,
CONFIG.BLACKLIST_KEY,
CONFIG.BLACKLIST_REMOVED_KEY
);
}
function toggleBlacklist(host) {
return _toggleManagedList(
normalizeHost(host),
CONFIG.BLACKLIST,
CONFIG.BLACKLIST_KEY,
CONFIG.BLACKLIST_REMOVED_KEY
);
}
function getBuiltinCheckinSkipReason(host) {
const n = normalizeHost(host);
const list = CONFIG.DEFAULT_CHECKIN_SKIP;
return list instanceof Map ? list.get(n) ?? null : list[n] ?? null;
}
function isCheckinSkipped(host) {
const n = normalizeHost(host);
if (getSiteData(n).checkinSupported === false) return true;
return _isInManagedList(
n,
CONFIG.DEFAULT_CHECKIN_SKIP,
CONFIG.CHECKIN_SKIP_KEY,
CONFIG.CHECKIN_SKIP_REMOVED_KEY
);
}
function toggleCheckinSkip(host) {
return _toggleManagedList(
normalizeHost(host),
CONFIG.DEFAULT_CHECKIN_SKIP,
CONFIG.CHECKIN_SKIP_KEY,
CONFIG.CHECKIN_SKIP_REMOVED_KEY
);
}
// src/styles.js
var STYLES = `
:root {
--ldoh-primary: #6366f1;
--ldoh-primary-hover: #4f46e5;
--ldoh-success: #10b981;
--ldoh-warning: #f59e0b;
--ldoh-danger: #ef4444;
--ldoh-text: #1e293b;
--ldoh-text-light: #64748b;
--ldoh-bg: #ffffff;
--ldoh-card-bg: rgba(255, 255, 255, 0.85);
--ldoh-border: #e2e8f0;
--ldoh-radius: 12px;
--ldoh-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.08), 0 2px 6px -1px rgba(0, 0, 0, 0.04);
}
.ldoh-helper-container {
display: flex; align-items: center; gap: 4px; z-index: 10;
pointer-events: auto; animation: ldoh-fade-in 0.3s ease-out;
}
@keyframes ldoh-fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.ldoh-info-bar {
display: flex; align-items: center; gap: 4px;
font-size: 10px; font-weight: 600; color: inherit;
white-space: nowrap;
}
.status-ok { background: var(--ldoh-success); }
.status-none { background: #9ca3af; }
.ldoh-btn {
width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
background: transparent; border-radius: 4px; border: none;
cursor: pointer; color: inherit; transition: all 0.2s; flex-shrink: 0;
}
.ldoh-btn:hover { background: rgba(99, 102, 241, 0.1); color: var(--ldoh-primary); opacity: 1; transform: scale(1.1); }
.ldoh-btn:active { transform: scale(0.95); }
.ldoh-refresh-btn.loading svg { animation: ldoh-spin 0.8s linear infinite; }
@keyframes ldoh-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* Dialog */
.ldh-overlay {
position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4);
z-index: 900; display: flex; justify-content: center; align-items: center;
backdrop-filter: blur(6px); animation: ldoh-fade-in-blur 0.3s ease-out;
}
@keyframes ldoh-fade-in-blur { from { opacity: 0; backdrop-filter: blur(0); } to { opacity: 1; backdrop-filter: blur(6px); } }
.ldh-dialog {
background: #fff; width: min(720px, 94vw); max-height: 85vh;
border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
display: flex; flex-direction: column; overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.2); font-size: 13px;
transform-origin: center; animation: ldoh-zoom-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes ldoh-zoom-in { from { transform: scale(0.9) translateY(20px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }
.ldh-header {
padding: 18px 24px; border-bottom: 1px solid var(--ldoh-border);
display: flex; justify-content: space-between; align-items: center;
background: linear-gradient(to right, #f8fafc, #ffffff);
}
.ldh-title { font-size: 16px; font-weight: 700; color: var(--ldoh-text); }
.ldh-close {
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
border-radius: 50%; color: var(--ldoh-text-light); cursor: pointer; transition: all 0.2s;
}
.ldh-close:hover { background: #f1f5f9; color: var(--ldoh-danger); transform: rotate(90deg); }
.ldh-content { padding: 24px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 24px; scrollbar-width: thin; }
.ldh-sec-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.ldh-sec-title { font-size: 14px; font-weight: 700; color: var(--ldoh-text); display: flex; align-items: center; gap: 6px; }
.ldh-sec-badge { font-size: 11px; padding: 2px 8px; background: #f1f5f9; border-radius: 20px; color: var(--ldoh-text-light); }
.ldh-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.ldh-grid-models { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.ldh-item {
padding: 12px; border: 1px solid var(--ldoh-border); border-radius: var(--ldoh-radius);
font-size: 13px; color: var(--ldoh-text); background: #fff; cursor: pointer;
position: relative; transition: all 0.2s ease;
display: flex; flex-direction: column; gap: 4px;
}
.ldh-item:hover { border-color: var(--ldoh-primary); background: #f5f3ff; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1); }
.ldh-item:active { transform: translateY(0); }
.ldh-item.active { border-color: var(--ldoh-primary); background: #f5f3ff; box-shadow: inset 0 0 0 1px var(--ldoh-primary); }
.ldh-quota { color: var(--ldoh-warning); font-weight: 800; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
/* Toast */
.ldoh-toast-container { position: fixed; top: 24px; right: 24px; z-index: 950; display: flex; flex-direction: column; gap: 12px; pointer-events: none; }
.ldoh-toast {
min-width: 300px; max-width: 450px; padding: 14px 18px; background: #fff; border-radius: 14px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
display: flex; align-items: center; gap: 12px; font-size: 14px; font-weight: 600;
pointer-events: auto; animation: ldoh-slide-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
border-left: 5px solid var(--ldoh-primary);
}
@keyframes ldoh-slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
.ldoh-toast.success { border-left-color: var(--ldoh-success); }
.ldoh-toast.error { border-left-color: var(--ldoh-danger); }
.ldoh-toast.warning { border-left-color: var(--ldoh-warning); }
.ldoh-toast-icon { width: 22px; height: 22px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
.ldoh-toast.success .ldoh-toast-icon { background: #ecfdf5; color: var(--ldoh-success); }
.ldoh-toast.error .ldoh-toast-icon { background: #fef2f2; color: var(--ldoh-danger); }
.ldoh-toast.warning .ldoh-toast-icon { background: #fffbeb; color: var(--ldoh-warning); }
.ldoh-toast.info .ldoh-toast-icon { background: #f0f9ff; color: var(--ldoh-primary); }
.ldoh-toast-message { flex: 1; color: var(--ldoh-text); line-height: 1.5; }
.ldoh-toast-close { width: 24px; height: 24px; flex-shrink: 0; cursor: pointer; color: var(--ldoh-text-light); display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: all 0.2s; }
.ldoh-toast-close:hover { background: #f1f5f9; color: var(--ldoh-text); }
/* ---- \u60AC\u6D6E\u9762\u677F FAB ---- */
.ldoh-fab {
position: fixed; right: 20px; bottom: 20px; width: 44px; height: 44px;
border-radius: 50%; background: var(--ldoh-primary); color: white; border: none;
cursor: pointer; z-index: 800; display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.45);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.ldoh-fab:hover { background: var(--ldoh-primary-hover); transform: scale(1.08); }
.ldoh-fab-badge {
position: absolute; top: -4px; right: -4px; background: var(--ldoh-danger); color: white;
border-radius: 99px; font-size: 9px; font-weight: 700; min-width: 16px; height: 16px;
display: flex; align-items: center; justify-content: center; padding: 0 3px; border: 2px solid white;
}
.ldoh-floating-panel {
position: fixed; right: 20px; bottom: 76px; width: 500px; max-height: 62vh;
background: #fff; border-radius: 16px; z-index: 799; flex-direction: column; overflow: hidden;
box-shadow: 0 20px 40px -8px rgba(0,0,0,0.18), 0 4px 12px -2px rgba(0,0,0,0.08);
border: 1px solid var(--ldoh-border); transform-origin: bottom right;
}
@keyframes ldoh-panel-in {
from { opacity: 0; transform: scale(0.85) translateY(16px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.ldoh-panel-in { animation: ldoh-panel-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
.ldoh-panel-hd {
padding: 10px 14px; border-bottom: 1px solid var(--ldoh-border);
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
background: linear-gradient(to right, #f8fafc, #fff);
}
.ldoh-panel-hd-title {
flex: 1; font-size: 13px; font-weight: 700; color: var(--ldoh-text);
display: flex; align-items: center; gap: 6px;
}
.ldoh-panel-hd-total { font-size: 11px; color: var(--ldoh-text-light); }
.ldoh-panel-search {
padding: 7px 12px 6px; border-bottom: 1px solid var(--ldoh-border); flex-shrink: 0; background: #fff;
}
.ldoh-panel-search-wrap { position: relative; }
.ldoh-panel-search-icon {
position: absolute; left: 8px; top: 50%; transform: translateY(-50%);
opacity: 0.35; pointer-events: none;
}
.ldoh-panel-search-input {
width: 100%; box-sizing: border-box; padding: 5px 8px 5px 28px;
border: 1px solid var(--ldoh-border); border-radius: 6px; font-size: 12px;
outline: none; background: #f8fafc; transition: border-color 0.2s, background 0.2s;
}
.ldoh-panel-search-input:focus { border-color: var(--ldoh-primary); background: #fff; }
.ldoh-filter-bar { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
.ldoh-filter-chip {
font-size: 10px; padding: 2px 8px; border-radius: 10px; cursor: pointer;
border: 1px solid var(--ldoh-border); background: #f8fafc; color: var(--ldoh-text-light);
user-select: none; transition: all 0.15s; white-space: nowrap;
}
.ldoh-filter-chip:hover { border-color: #cbd5e1; color: var(--ldoh-text); }
.ldoh-filter-chip.active { background: var(--ldoh-primary); color: #fff; border-color: var(--ldoh-primary); }
.ldoh-panel-body { overflow-y: auto; flex: 1; scrollbar-width: thin; min-height: 200px; }
.ldoh-panel-row {
display: grid; grid-template-columns: 1fr 54px 64px 22px 22px 22px 22px 22px 22px;
align-items: center; gap: 6px; padding: 7px 12px;
border-bottom: 1px solid #f1f5f9; transition: background 0.15s; font-size: 12px;
}
.ldoh-panel-row:hover { background: #f8fafc; }
.ldoh-panel-row:last-child { border-bottom: none; }
.ldoh-panel-name {
font-weight: 600; color: var(--ldoh-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
display: flex; flex-direction: column; gap: 1px; min-width: 0;
}
.ldoh-panel-name-main {
font-weight: 600; color: var(--ldoh-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ldoh-panel-name-host {
font-size: 10px; font-weight: 400; color: var(--ldoh-text-light);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ldoh-panel-checkin {
font-size: 10px; padding: 2px 5px; border-radius: 8px; font-weight: 600; text-align: center;
}
.ldoh-panel-checkin.ok { background: #ecfdf5; color: #059669; }
.ldoh-panel-checkin.no { background: #fffbeb; color: #d97706; }
.ldoh-panel-checkin.na { background: #f1f5f9; color: var(--ldoh-text-light); }
.ldoh-panel-balance {
font-weight: 700; color: #d97706; font-family: ui-monospace, monospace;
font-size: 12px; text-align: right; white-space: nowrap;
}
.ldoh-panel-empty { padding: 32px; text-align: center; color: var(--ldoh-text-light); font-size: 13px; }
/* \u5220\u9664\u786E\u8BA4\u6C14\u6CE1 */
.ldoh-confirm-pop {
position: fixed; z-index: 1000;
background: #fff; border: 1px solid var(--ldoh-border); border-radius: 10px;
box-shadow: 0 6px 20px -4px rgba(0,0,0,0.15);
padding: 8px 10px; display: flex; align-items: center; gap: 8px;
font-size: 12px; font-weight: 600; color: var(--ldoh-text); white-space: nowrap;
animation: ldoh-fade-in 0.15s ease-out;
}
.ldoh-confirm-pop::after {
content: ""; position: absolute; bottom: -5px; right: 10px;
width: 8px; height: 8px; background: #fff;
border-right: 1px solid var(--ldoh-border); border-bottom: 1px solid var(--ldoh-border);
transform: rotate(45deg);
}
.ldoh-pop-btn {
padding: 3px 10px; border-radius: 6px; border: none; font-size: 11px;
font-weight: 600; cursor: pointer; transition: all 0.15s;
}
.ldoh-pop-cancel { background: #f1f5f9; color: var(--ldoh-text); }
.ldoh-pop-cancel:hover { background: #e2e8f0; }
.ldoh-pop-confirm { background: var(--ldoh-danger); color: #fff; }
.ldoh-pop-confirm:hover { background: #dc2626; }
/* \u8BBE\u7F6E\u83DC\u5355 */
.ldoh-settings-pop {
position: fixed; z-index: 1000;
background: #fff; border: 1px solid var(--ldoh-border); border-radius: 10px;
box-shadow: 0 6px 20px -4px rgba(0,0,0,0.15);
padding: 6px; width: 140px;
animation: ldoh-fade-in 0.15s ease-out;
}
.ldoh-settings-item {
width: 100%; border: none; background: transparent;
padding: 8px 10px; border-radius: 8px; cursor: pointer; white-space: nowrap;
font-size: 12px; font-weight: 600; color: var(--ldoh-text);
text-align: left; transition: background 0.15s;
}
.ldoh-settings-item:hover { background: #f8fafc; }
.ldoh-interval-pop {
position: fixed; z-index: 1000;
background: #fff; border: 1px solid var(--ldoh-border); border-radius: 10px;
box-shadow: 0 6px 20px -4px rgba(0,0,0,0.15);
padding: 10px; width: 220px;
animation: ldoh-fade-in 0.15s ease-out;
}
.ldoh-interval-title {
font-size: 12px; font-weight: 700; color: var(--ldoh-text);
margin-bottom: 8px;
}
.ldoh-interval-hint {
font-size: 11px; color: var(--ldoh-text-light);
margin-top: 6px;
}
.ldoh-interval-input {
width: 100%;
border: 1px solid var(--ldoh-border);
border-radius: 8px;
padding: 7px 9px;
font-size: 12px;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
box-sizing: border-box;
}
.ldoh-interval-input:focus {
border-color: var(--ldoh-primary);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
}
.ldoh-interval-actions {
margin-top: 10px; display: flex; justify-content: flex-end; gap: 8px;
}
`;
// src/utils/misc.js
function injectStyles() {
const styleId = CONFIG.DOM.STYLE_ID;
if (!document.getElementById(styleId)) {
Log.debug("\u6CE8\u5165\u6837\u5F0F\u8868");
const s = document.createElement("style");
s.id = styleId;
s.textContent = STYLES;
document.head.appendChild(s);
}
}
function copy(text) {
try {
GM_setClipboard(text);
Log.debug(`\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F: ${text.substring(0, 20)}...`);
} catch (e) {
Log.error("\u590D\u5236\u5230\u526A\u8D34\u677F\u5931\u8D25", e);
}
}
function debounce(func, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
async function isNewApiSite(retryCount = 5) {
try {
const host = window.location.hostname;
const normalizedHost = normalizeHost(host);
const whitelist = GM_getValue(CONFIG.WHITELIST_KEY, []);
const inWhitelist = whitelist.includes(normalizedHost);
if (!inWhitelist) {
Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u4E0D\u5728 LDOH \u7AD9\u70B9\u767D\u540D\u5355\u4E2D\uFF0C\u8DF3\u8FC7`);
return false;
}
if (retryCount > 0) {
Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u5728 LDOH \u767D\u540D\u5355\u4E2D\uFF0C\u7EE7\u7EED\u68C0\u6D4B New API \u7279\u5F81`);
}
let hasUserData = !!localStorage.getItem("user");
if (!hasUserData && retryCount > 0) {
Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u6682\u65E0\u7528\u6237\u6570\u636E\uFF0C${retryCount === 1 ? "\u7ED3\u675F" : "\u7B49\u5F85"}\u91CD\u8BD5...`);
await new Promise((resolve) => setTimeout(resolve, 500));
return isNewApiSite(retryCount - 1);
}
if (hasUserData) {
Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u68C0\u6D4B\u5230\u7528\u6237\u6570\u636E\uFF0C\u5224\u5B9A\u4E3A New API \u7AD9\u70B9`);
return true;
}
Log.debug(`[\u7AD9\u70B9\u8BC6\u522B] ${host} - \u672A\u8BC6\u522B\u4E3A New API \u7AD9\u70B9`);
return false;
} catch (e) {
Log.error("[\u7AD9\u70B9\u8BC6\u522B] \u68C0\u6D4B\u5931\u8D25", e);
return false;
}
}
async function waitForLogin() {
Log.debug("[\u767B\u5F55\u68C0\u6D4B] \u5F00\u59CB\u7B49\u5F85\u7528\u6237\u767B\u5F55...");
const host = window.location.hostname;
for (let i = 0; i < CONFIG.LOGIN_CHECK_MAX_ATTEMPTS; i++) {
const userId = getAndSyncUserId(host);
if (userId) {
Log.success(`[\u767B\u5F55\u68C0\u6D4B] \u68C0\u6D4B\u5230\u767B\u5F55\uFF0C\u7528\u6237 ID: ${userId}`);
return userId;
}
await new Promise((resolve) => setTimeout(resolve, CONFIG.LOGIN_CHECK_INTERVAL));
}
Log.debug("[\u767B\u5F55\u68C0\u6D4B] \u8D85\u65F6\uFF0C\u672A\u68C0\u6D4B\u5230\u767B\u5F55");
return null;
}
function watchLoginStatus(callback) {
const host = window.location.hostname;
window.addEventListener("storage", (e) => {
if (e.key === "user" && e.newValue) {
Log.debug("[\u767B\u5F55\u76D1\u542C] \u68C0\u6D4B\u5230 user \u6570\u636E\u53D8\u5316");
const userId = getAndSyncUserId(host);
if (userId) callback(userId);
}
});
let lastUserId = getAndSyncUserId(host);
setInterval(() => {
const currentUserId = getAndSyncUserId(host);
if (currentUserId && currentUserId !== lastUserId) {
Log.debug("[\u767B\u5F55\u76D1\u542C] \u8F6E\u8BE2\u68C0\u6D4B\u5230\u767B\u5F55");
lastUserId = currentUserId;
callback(currentUserId);
}
}, CONFIG.LOGIN_CHECK_INTERVAL);
}
// src/ui/toast.js
var _container = null;
function _initContainer() {
if (!_container) {
_container = document.createElement("div");
_container.className = "ldoh-toast-container";
document.body.appendChild(_container);
}
}
var ICONS = {
success: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
error: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
warning: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
info: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>'
};
function removeToast(toast) {
if (!toast || !toast.parentNode) return;
toast.style.animation = "ldoh-slide-in 0.3s ease-in reverse forwards";
setTimeout(() => toast.remove(), 300);
}
function showToast(message, type = "info", duration = 3e3) {
_initContainer();
const toast = document.createElement("div");
toast.className = `ldoh-toast ${type}`;
toast.innerHTML = `
<div class="ldoh-toast-icon">${ICONS[type] || ICONS.info}</div>
<div class="ldoh-toast-message">${escapeHtml(message)}</div>
<div class="ldoh-toast-close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></div>
`;
toast.querySelector(".ldoh-toast-close").onclick = () => removeToast(toast);
_container.appendChild(toast);
if (duration > 0) setTimeout(() => removeToast(toast), duration);
return toast;
}
var Toast = {
show: showToast,
remove: removeToast,
success: (msg, duration) => showToast(msg, "success", duration),
error: (msg, duration) => showToast(msg, "error", duration),
warning: (msg, duration) => showToast(msg, "warning", duration),
info: (msg, duration) => showToast(msg, "info", duration)
};
// src/utils/date.js
function getTodayString() {
const now = /* @__PURE__ */ new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
}
function getCurrentMonthString() {
return getTodayString().slice(0, 7);
}
function isCheckedInToday(siteData) {
if (!siteData) return false;
const today = getTodayString();
return siteData.checkedInToday === true && (!siteData.lastCheckinDate || siteData.lastCheckinDate === today);
}
// src/api.js
var _state = {
waiters: [],
activeRequests: 0,
activeBackgroundRequests: 0
};
function _release(isInteractive) {
_state.activeRequests--;
if (!isInteractive) _state.activeBackgroundRequests--;
const settings = GM_getValue(CONFIG.SETTINGS_KEY, {});
const maxConcurrent = settings.maxConcurrent || CONFIG.DEFAULT_MAX_CONCURRENT;
const maxBackground = settings.maxBackground || CONFIG.DEFAULT_MAX_BACKGROUND;
let idx = _state.waiters.findIndex(
(w) => w.isInteractive && _state.activeRequests < maxConcurrent
);
if (idx < 0) {
idx = _state.waiters.findIndex(
(w) => !w.isInteractive && _state.activeRequests < maxConcurrent && _state.activeBackgroundRequests < maxBackground
);
}
if (idx >= 0) {
const w = _state.waiters.splice(idx, 1)[0];
_state.activeRequests++;
if (!w.isInteractive) _state.activeBackgroundRequests++;
w.resolve();
}
}
async function _acquire(isInteractive) {
const settings = GM_getValue(CONFIG.SETTINGS_KEY, {});
const maxConcurrent = settings.maxConcurrent || CONFIG.DEFAULT_MAX_CONCURRENT;
const maxBackground = settings.maxBackground || CONFIG.DEFAULT_MAX_BACKGROUND;
const canRun = () => _state.activeRequests < maxConcurrent && (isInteractive || _state.activeBackgroundRequests < maxBackground);
if (!canRun()) {
await new Promise((resolve) => _state.waiters.push({ resolve, isInteractive }));
return;
}
_state.activeRequests++;
if (!isInteractive) _state.activeBackgroundRequests++;
}
async function request(method, host, path, token = null, userId = null, body = null, isInteractive = false, extraHeaders = {}) {
await _acquire(isInteractive);
Log.debug(
`[\u8BF7\u6C42] ${method} ${host}${path} (\u5E76\u53D1: ${_state.activeRequests}, \u540E\u53F0: ${_state.activeBackgroundRequests}, \u4EA4\u4E92: ${isInteractive})`
);
try {
const result = await new Promise((resolve, _reject) => {
const requestConfig = {
method,
url: `https://${host}${path}`,
headers: {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0",
Referer: `https://${host}/`,
...token ? { Authorization: `Bearer ${token}` } : {},
...userId ? { "New-Api-User": userId } : {},
...extraHeaders
},
timeout: CONFIG.REQUEST_TIMEOUT,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (res.status >= 200 && res.status < 300) {
Log.debug(`[\u54CD\u5E94\u6210\u529F] ${method} ${host}${path}`, data);
resolve(data);
} else {
Log.warn(`[\u54CD\u5E94\u9519\u8BEF] ${method} ${host}${path} - \u72B6\u6001\u7801: ${res.status}`, data);
resolve({ success: false, error: `HTTP ${res.status}`, data });
}
} catch (e) {
Log.error(`[\u89E3\u6790\u5931\u8D25] ${method} ${host}${path}`, e);
resolve({ success: false, error: "\u89E3\u6790\u54CD\u5E94\u5931\u8D25" });
}
},
onerror: (err) => {
Log.error(`[\u7F51\u7EDC\u9519\u8BEF] ${method} ${host}${path}`, err);
resolve({ success: false, error: "\u7F51\u7EDC\u9519\u8BEF" });
},
ontimeout: () => {
Log.warn(`[\u8BF7\u6C42\u8D85\u65F6] ${method} ${host}${path}`);
resolve({ success: false, error: "\u8BF7\u6C42\u8D85\u65F6" });
}
};
if (body) requestConfig.data = JSON.stringify(body);
GM_xmlhttpRequest(requestConfig);
});
return result;
} finally {
_release(isInteractive);
}
}
async function _probeQuota(host, token, userId) {
Log.debug(`[\u83B7\u53D6\u7528\u6237\u4FE1\u606F] ${host}`);
const res = await request("GET", host, "/api/user/self", token, userId);
if (res.success && res.data) {
Log.debug(`[\u7528\u6237\u4FE1\u606F] ${host} - \u989D\u5EA6: ${res.data.quota}`);
return { success: true, quota: res.data.quota, error: null };
}
Log.error(`[\u7528\u6237\u4FE1\u606F\u83B7\u53D6\u5931\u8D25] ${host}`, res);
return { success: false, quota: null, error: res?.error || "\u8BF7\u6C42\u5931\u8D25" };
}
async function _probeCheckinStatus(host, token, userId, currentData) {
const monthStr = getCurrentMonthString();
const todayStr = getTodayString();
if (currentData.checkinSupported === false) {
Log.debug(`[\u7B7E\u5230\u6570\u636E] ${host} - LDOH \u6807\u8BB0\u4E0D\u652F\u6301\u7B7E\u5230\uFF0C\u8DF3\u8FC7\u63A2\u6D4B`);
return {
success: true,
error: null,
data: {
checkedInToday: null,
checkinSupported: false,
lastCheckinDate: currentData.lastCheckinDate
}
};
}
Log.debug(`[\u83B7\u53D6\u7B7E\u5230\u6570\u636E] ${host} - \u6708\u4EFD: ${monthStr}`);
const res = await request("GET", host, `/api/user/checkin?month=${monthStr}`, token, userId);
if (res.success && res.data) {
let checkedInToday = !!res.data?.stats?.checked_in_today;
if (host === "wzw.pp.ua") checkedInToday = !!res.data?.checked_in;
const lastCheckinDate = checkedInToday ? todayStr : currentData.lastCheckinDate;
Log.debug(`[\u7B7E\u5230\u6570\u636E] ${host} - \u5DF2\u7B7E\u5230: ${checkedInToday}`);
return {
success: true,
error: null,
data: { checkedInToday, checkinSupported: true, lastCheckinDate }
};
}
Log.warn(`[\u7B7E\u5230\u6570\u636E\u83B7\u53D6\u5931\u8D25] ${host} - \u63A5\u53E3\u8BF7\u6C42\u5931\u8D25`, res);
return {
success: false,
error: res?.error || "\u8BF7\u6C42\u5931\u8D25",
data: {
checkedInToday: null,
checkinSupported: currentData.checkinSupported ?? true,
lastCheckinDate: currentData.lastCheckinDate
}
};
}
async function updateSiteStatus(host, userId, force = false, strict = false) {
let data = getSiteData(host);
const settings = GM_getValue(CONFIG.SETTINGS_KEY, { interval: CONFIG.DEFAULT_INTERVAL });
if (!force && data.ts && Date.now() - data.ts < settings.interval * 60 * 1e3) {
Log.debug(
`[\u8DF3\u8FC7\u66F4\u65B0] ${host} - \u8DDD\u79BB\u4E0A\u6B21\u66F4\u65B0 ${Math.round((Date.now() - data.ts) / 6e4)} \u5206\u949F`
);
return data;
}
Log.info(`[\u5F00\u59CB\u66F4\u65B0] ${host} (\u5F3A\u5236: ${force})`);
if (!data.token) {
Log.warn(`[\u8DF3\u8FC7\u66F4\u65B0] ${host} - token \u4E0D\u5B58\u5728`);
return data;
}
const quotaResult = await _probeQuota(host, data.token, userId);
if (!quotaResult.success) {
if (quotaResult.error === "\u8BF7\u6C42\u8D85\u65F6") {
throw new Error(`${host} \u8BF7\u6C42\u8D85\u65F6`);
}
if (strict) {
throw new Error(`${host} \u63A5\u53E3\u8BF7\u6C42\u5931\u8D25`);
}
}
if (quotaResult.success && quotaResult.quota != null) data.quota = quotaResult.quota;
const checkinResult = await _probeCheckinStatus(host, data.token, userId, data);
if (!checkinResult.success) {
if (checkinResult.error === "\u8BF7\u6C42\u8D85\u65F6") {
throw new Error(`${host} \u8BF7\u6C42\u8D85\u65F6`);
}
if (strict) {
throw new Error(`${host} \u63A5\u53E3\u8BF7\u6C42\u5931\u8D25`);
}
}
Object.assign(data, checkinResult.data);
data.userId = userId;
saveSiteData(host, data);
const checkinLabel = data.checkinSupported ? data.checkedInToday ? "\u662F" : "\u5426" : "\u4E0D\u652F\u6301";
Log.success(`[\u66F4\u65B0\u5B8C\u6210] ${host} - \u989D\u5EA6: $${formatQuota(data.quota)}, \u7B7E\u5230: ${checkinLabel}`);
return data;
}
async function fetchToken(host, userId) {
try {
let res = await request("GET", host, "/api/user/token", null, userId);
if (!res.success || !res.data) {
const sessionVal = document.cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith("session="))?.slice("session=".length);
if (sessionVal) {
Log.debug(`[Token] ${host} - \u5C1D\u8BD5 session cookie \u91CD\u8BD5`);
res = await request("GET", host, "/api/user/token", null, userId, null, false, {
Cookie: `session=${sessionVal}`
});
}
}
if (res.success && res.data) {
Log.success(`[Token] ${host} - \u83B7\u53D6\u6210\u529F`);
return res.data;
}
Log.error(`[Token] ${host} - \u83B7\u53D6\u5931\u8D25`, res);
return null;
} catch (e) {
Log.error(`[Token] ${host} - \u5F02\u5E38`, e);
return null;
}
}
async function fetchSelf(host, token, userId) {
return request("GET", host, "/api/user/self", token, userId);
}
async function fetchKeys(host, token, userId, page = 0) {
try {
const res = await request(
"GET",
host,
`/api/token/?p=${page}&size=1000`,
token,
userId,
null,
true
);
return res.success ? Array.isArray(res.data) ? res.data : res.data?.items || [] : [];
} catch (e) {
Log.error(`[fetchKeys] ${host}`, e);
return [];
}
}
async function createToken(host, token, userId, name, group) {
try {
Log.debug(`[\u521B\u5EFA\u5BC6\u94A5] ${host} - \u540D\u79F0: ${name}, \u5206\u7EC4: ${group}`);
const res = await request(
"POST",
host,
"/api/token/",
token,
userId,
{
remain_quota: 0,
expired_time: -1,
unlimited_quota: true,
model_limits_enabled: false,
model_limits: "",
cross_group_retry: false,
name,
group,
allow_ips: ""
},
true
);
if (res.success) Log.success(`[\u5BC6\u94A5\u521B\u5EFA\u6210\u529F] ${host}`);
else Log.error(`[\u5BC6\u94A5\u521B\u5EFA\u5931\u8D25] ${host}`, res);
return res;
} catch (e) {
Log.error(`[\u521B\u5EFA\u5BC6\u94A5\u5F02\u5E38] ${host}`, e);
return { success: false, error: "\u521B\u5EFA\u5BC6\u94A5\u5F02\u5E38" };
}
}
async function deleteToken(host, token, userId, tokenId) {
try {
Log.debug(`[\u5220\u9664\u5BC6\u94A5] ${host} - ID: ${tokenId}`);
const res = await request("DELETE", host, `/api/token/${tokenId}`, token, userId, null, true);
if (res.success) Log.success(`[\u5BC6\u94A5\u5220\u9664\u6210\u529F] ${host}`);
else Log.error(`[\u5BC6\u94A5\u5220\u9664\u5931\u8D25] ${host}`, res);
return res;
} catch (e) {
Log.error(`[\u5220\u9664\u5BC6\u94A5\u5F02\u5E38] ${host}`, e);
return { success: false, error: "\u5220\u9664\u5BC6\u94A5\u5F02\u5E38" };
}
}
async function checkin(host, token, userId) {
try {
Log.debug(`[\u7B7E\u5230] ${host}`);
const res = await request("POST", host, "/api/user/checkin", token, userId, null, true);
if (res.success) {
const awarded = res.data?.quota_awarded || 0;
Log.success(`[\u7B7E\u5230\u6210\u529F] ${host} - \u83B7\u5F97\u989D\u5EA6: ${formatQuota(awarded)}`);
} else if (res.message && res.message.includes("\u5DF2\u7B7E\u5230")) {
Log.success(`[\u5DF2\u7B7E\u5230] ${host} - \u4ECA\u65E5\u5DF2\u7B7E\u5230`);
res.alreadyCheckedIn = true;
} else Log.error(`[\u7B7E\u5230\u5931\u8D25] ${host}`, res);
return res;
} catch (e) {
Log.error(`[\u7B7E\u5230\u5F02\u5E38] ${host}`, e);
return { success: false, error: "\u7B7E\u5230\u5F02\u5E38" };
}
}
async function fetchDetails(host, token, userId) {
try {
Log.debug(`[\u83B7\u53D6\u8BE6\u60C5] ${host}`);
const [pricingRes, keys] = await Promise.all([
request("GET", host, "/api/pricing", token, userId, null, true),
fetchKeys(host, token, userId)
]);
const models = pricingRes.success ? pricingRes.data : [];
return { models, keys };
} catch (e) {
Log.error(`[\u83B7\u53D6\u8BE6\u60C5\u5F02\u5E38] ${host}`, e);
return { models: [], keys: [] };
}
}
async function fetchGroups(host, token, userId) {
try {
Log.debug(`[\u83B7\u53D6\u5206\u7EC4\u5217\u8868] ${host}`);
const res = await request("GET", host, "/api/user/self/groups", token, userId, null, true);
if (res.success && res.data) return res.data;
Log.warn(`[\u5206\u7EC4\u5217\u8868\u83B7\u53D6\u5931\u8D25] ${host}`, res);
return {};
} catch (e) {
Log.error(`[\u83B7\u53D6\u5206\u7EC4\u5217\u8868\u5F02\u5E38] ${host}`, e);
return {};
}
}
var API = {
request,
updateSiteStatus,
fetchToken,
fetchSelf,
fetchKeys,
createToken,
deleteToken,
checkin,
fetchDetails,
fetchGroups
};
// src/ui/overlay.js
function createOverlay(html) {
injectStyles();
const ov = document.createElement("div");
ov.className = "ldh-overlay";
ov.innerHTML = `<div class="ldh-dialog">${html}</div>`;
ov.onclick = (e) => {
if (e.target !== ov) return;
ov.querySelector(".ldh-dialog").style.animation = `ldoh-zoom-in ${CONFIG.ANIMATION_FAST_MS}ms ease-in reverse forwards`;
ov.style.animation = `ldoh-fade-in-blur ${CONFIG.ANIMATION_FAST_MS}ms ease-in reverse forwards`;
setTimeout(() => ov.remove(), CONFIG.ANIMATION_FAST_MS);
};
document.body.appendChild(ov);
return ov;
}
// src/ui/base.js
var UI = {
/**
* 创建一个通用元素
*/
element(tag, options = {}) {
const el = document.createElement(tag);
if (options.className) el.className = options.className;
if (options.id) el.id = options.id;
if (options.style) {
if (typeof options.style === "string") {
el.style.cssText = options.style;
} else {
Object.assign(el.style, options.style);
}
}
if (options.innerHTML) el.innerHTML = options.innerHTML;
if (options.textContent) el.textContent = options.textContent;
if (options.title) el.title = options.title;
if (options.dataset) {
for (const [key, val] of Object.entries(options.dataset)) {
el.dataset[key] = val;
}
}
if (options.onClick) el.onclick = options.onClick;
if (options.onMouseOver) el.onmouseover = options.onMouseOver;
if (options.onMouseOut) el.onmouseout = options.onMouseOut;
if (options.children) {
options.children.forEach((child) => {
if (child) el.appendChild(child);
});
}
return el;
},
/**
* 创建一个容器 (div)
*/
div(options = {}) {
return this.element("div", options);
},
/**
* 创建一个 span
*/
span(options = {}) {
return this.element("span", options);
},
/**
* 创建一个按钮
*/
button(options = {}) {
const el = this.element("button", options);
if (options.type) el.type = options.type;
if (options.disabled) el.disabled = options.disabled;
return el;
},
/**
* 创建一个输入框
*/
input(options = {}) {
const el = this.element("input", options);
el.type = options.type || "text";
if (options.placeholder) el.placeholder = options.placeholder;
if (options.value) el.value = options.value;
if (options.min) el.min = options.min;
if (options.step) el.step = options.step;
if (options.max) el.max = options.max;
if (options.onFocus) el.onfocus = options.onFocus;
if (options.onBlur) el.onblur = options.onBlur;
if (options.onInput) el.oninput = options.onInput;
if (options.onKeyDown) el.addEventListener("keydown", options.onKeyDown);
return el;
},
/**
* 创建 SVG 图标
*/
icon(svgContent, options = {}) {
const wrapper = this.element("div", options);
wrapper.innerHTML = svgContent;
return wrapper.firstElementChild || wrapper;
},
/**
* 预定义的一些常用 SVG 字符串
*/
ICONS: {
REFRESH: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/></svg>',
DETAILS: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg>',
CLOSE: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
CLOSE_LG: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
TRASH: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
PANEL: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>',
EYE: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
CHECKIN: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><polyline points="9 16 11 18 15 14"/></svg>',
CHECKIN_OFF: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="7" y1="13" x2="17" y2="21"/><line x1="17" y1="13" x2="7" y2="21"/></svg>',
LOCATE: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>'
}
};
// src/ui/dialog.js
var _detailLoadingHosts = /* @__PURE__ */ new Set();
function _closeDetailDialog() {
const ov = document.querySelector(".ldh-overlay");
if (!ov) return;
const dialog = ov.querySelector(".ldh-dialog");
if (dialog) {
dialog.style.animation = `ldoh-zoom-in ${CONFIG.ANIMATION_FAST_MS}ms ease-in reverse forwards`;
}
ov.style.animation = `ldoh-fade-in-blur ${CONFIG.ANIMATION_FAST_MS}ms ease-in reverse forwards`;
setTimeout(() => ov.remove(), CONFIG.ANIMATION_FAST_MS);
}
function _buildDetailHeader(host) {
return UI.div({
className: "ldh-header",
children: [
UI.div({ className: "ldh-title", textContent: host }),
UI.div({
className: "ldh-close",
innerHTML: UI.ICONS.CLOSE_LG,
onClick: _closeDetailDialog
})
]
});
}
function _renderKeySection(host, data, keys, modelItems, modelsBadge, modelArray) {
const container = UI.div({ className: "ldh-sec-wrapper" });
const keysBadge = UI.span({ className: "ldh-sec-badge", textContent: keys.length });
const keysGrid = UI.div({ className: "ldh-grid" });
const refreshKeys = (list) => {
keysGrid.innerHTML = "";
if (list.length) {
list.forEach(
(k) => keysGrid.appendChild(
_buildKeyItem(k, host, data, keysGrid, modelItems, modelsBadge, modelArray)
)
);
} else {
keysGrid.appendChild(
UI.div({
style: "grid-column:1/-1;text-align:center;padding:20px;color:var(--ldoh-text-light);",
textContent: "\u6682\u65E0\u53EF\u7528\u5BC6\u94A5"
})
);
}
};
const { createForm, createKeyBtn } = _buildCreateKeyForm(host, data, async () => {
const newKeys = await API.fetchKeys(host, data.token, data.userId, 1);
refreshKeys(newKeys);
keysBadge.textContent = newKeys.length;
});
container.appendChild(
UI.div({
className: "ldh-sec-header",
children: [
UI.div({
className: "ldh-sec-title",
children: [UI.span({ textContent: "\u{1F511} \u5BC6\u94A5\u5217\u8868" }), keysBadge]
}),
createKeyBtn
]
})
);
container.appendChild(createForm);
container.appendChild(keysGrid);
refreshKeys(keys);
return container;
}
function _renderModelSection(models, modelItems, modelsBadge) {
const container = UI.div({ className: "ldh-sec-wrapper" });
container.appendChild(
UI.div({
className: "ldh-sec-header",
children: [
UI.div({
className: "ldh-sec-title",
children: [UI.span({ textContent: "\u{1F916} \u6A21\u578B\u5217\u8868" }), modelsBadge]
})
]
})
);
const modelsGrid = UI.div({ className: "ldh-grid-models" });
if (models.length) {
const fmtPrice = (v) => parseFloat(v.toFixed(6)).toString();
models.forEach((m) => {
const modelName = m.model_name || m;
let priceHtml = "";
if (typeof m.quota_type === "number") {
priceHtml = m.quota_type === 1 ? `<div style="font-size:10px;font-weight:600;color:#64748b">$${fmtPrice(m.model_price)} /\u6B21</div>` : `<div style="font-size:10px;font-weight:600;color:#64748b">\u8F93\u5165: $${fmtPrice(m.model_ratio * 2)}/M \xB7 \u8F93\u51FA: $${fmtPrice(m.model_ratio * (m.completion_ratio || 1) * 2)}/M</div>`;
}
const item = UI.div({
className: "ldh-item",
dataset: { modelName, modelGroups: JSON.stringify(m.enable_groups || []) },
innerHTML: `<div style="font-weight:600;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(modelName)}</div>${priceHtml}<div style="font-size:9px;color:var(--ldoh-text-light)">\u70B9\u51FB\u590D\u5236</div>`,
onClick: () => {
copy(modelName);
Toast.success("\u5DF2\u590D\u5236\u6A21\u578B\u540D");
}
});
modelsGrid.appendChild(item);
modelItems.push(item);
});
} else {
modelsGrid.appendChild(
UI.div({
style: "grid-column:1/-1;text-align:center;padding:20px;color:var(--ldoh-text-light);",
textContent: "\u6682\u65E0\u53EF\u7528\u6A21\u578B"
})
);
}
container.appendChild(modelsGrid);
return container;
}
function _buildCreateKeyForm(host, data, onCreated) {
const createKeyBtn = UI.button({
style: "padding:4px 12px;background:var(--ldoh-primary);color:white;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all 0.2s;",
textContent: "+ \u521B\u5EFA\u5BC6\u94A5",
onMouseOver: (e) => e.target.style.background = "var(--ldoh-primary-hover)",
onMouseOut: (e) => e.target.style.background = "var(--ldoh-primary)"
});
const createForm = UI.div({
style: "display:none;padding:16px;background:#f8fafc;border:1px solid var(--ldoh-border);border-radius:var(--ldoh-radius);margin-bottom:12px;"
});
const nameInput = UI.input({
placeholder: "\u8BF7\u8F93\u5165\u5BC6\u94A5\u540D\u79F0",
style: "width:100%;padding:8px 10px;border:1px solid var(--ldoh-border);border-radius:6px;font-size:13px;outline:none;transition:all 0.2s;box-sizing:border-box;",
onFocus: (e) => e.target.style.borderColor = "var(--ldoh-primary)",
onBlur: (e) => e.target.style.borderColor = "var(--ldoh-border)"
});
const groupSelect = document.createElement("select");
groupSelect.style.cssText = "width:100%;padding:8px 10px;border:1px solid var(--ldoh-border);border-radius:6px;font-size:13px;outline:none;cursor:pointer;background:white;box-sizing:border-box;";
const submitBtn = UI.button({
textContent: "\u521B\u5EFA",
style: "padding:8px 16px;background:var(--ldoh-primary);color:white;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;",
onClick: async () => {
const name = nameInput.value.trim();
if (!name) {
Toast.warning("\u8BF7\u8F93\u5165\u5BC6\u94A5\u540D\u79F0");
nameInput.focus();
return;
}
submitBtn.disabled = true;
submitBtn.textContent = "\u521B\u5EFA\u4E2D...";
submitBtn.style.opacity = "0.6";
try {
const result = await API.createToken(
host,
data.token,
data.userId,
name,
groupSelect.value
);
if (result.success) {
Toast.success("\u5BC6\u94A5\u521B\u5EFA\u6210\u529F");
createForm.style.display = "none";
createKeyBtn.textContent = "+ \u521B\u5EFA\u5BC6\u94A5";
nameInput.value = "";
onCreated?.();
} else {
Toast.error(result.message || "\u5BC6\u94A5\u521B\u5EFA\u5931\u8D25");
}
} catch (e) {
Log.error("\u521B\u5EFA\u5BC6\u94A5\u5931\u8D25", e);
Toast.error("\u521B\u5EFA\u5BC6\u94A5\u5931\u8D25");
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "\u521B\u5EFA";
submitBtn.style.opacity = "1";
}
}
});
const cancelBtn = UI.button({
textContent: "\u53D6\u6D88",
style: "padding:8px 16px;background:#e2e8f0;color:var(--ldoh-text);border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;",
onClick: () => {
createForm.style.display = "none";
createKeyBtn.textContent = "+ \u521B\u5EFA\u5BC6\u94A5";
nameInput.value = "";
}
});
createForm.appendChild(
UI.div({
style: "display:grid;grid-template-columns:1fr 1fr auto;gap:12px;align-items:end;",
children: [
UI.div({
children: [
UI.div({
style: "font-size:12px;font-weight:600;margin-bottom:6px;",
textContent: "\u5BC6\u94A5\u540D\u79F0"
}),
nameInput
]
}),
UI.div({
children: [
UI.div({
style: "font-size:12px;font-weight:600;margin-bottom:6px;",
textContent: "\u9009\u62E9\u5206\u7EC4"
}),
groupSelect
]
}),
UI.div({ style: "display:flex;gap:8px;", children: [cancelBtn, submitBtn] })
]
})
);
createKeyBtn.onclick = async () => {
if (createForm.style.display === "none") {
createKeyBtn.disabled = true;
createKeyBtn.textContent = "\u52A0\u8F7D\u4E2D...";
try {
const groups = await API.fetchGroups(host, data.token, data.userId);
groupSelect.innerHTML = "";
Object.entries(groups).forEach(([gName, gInfo]) => {
const opt = document.createElement("option");
opt.value = gName;
opt.textContent = `${gName} - ${gInfo.desc} (\u500D\u7387: ${gInfo.ratio})`;
groupSelect.appendChild(opt);
});
createForm.style.display = "block";
createKeyBtn.textContent = "\u6536\u8D77\u8868\u5355";
setTimeout(() => nameInput.focus(), 100);
} catch (e) {
Log.error("\u83B7\u53D6\u5206\u7EC4\u5217\u8868\u5931\u8D25", e);
Toast.error("\u83B7\u53D6\u5206\u7EC4\u5217\u8868\u5931\u8D25");
} finally {
createKeyBtn.disabled = false;
}
} else {
createForm.style.display = "none";
createKeyBtn.textContent = "+ \u521B\u5EFA\u5BC6\u94A5";
nameInput.value = "";
}
};
return { createForm, createKeyBtn };
}
function _buildKeyItem(k, host, data, keysGrid, modelItems, modelsBadge, modelArray) {
const item = UI.div({
className: "ldh-item ldh-key-item",
dataset: { group: k.group || "", key: `sk-${k.key}` },
style: "position: relative;",
innerHTML: `
<div style="font-weight:700;color:var(--ldoh-text)">${escapeHtml(k.name || "\u672A\u547D\u540D")}</div>
${k.group ? `<div style="font-size:10px;color:var(--ldoh-primary);font-weight:600">Group: ${escapeHtml(k.group)}</div>` : ""}
<div style="font-size:10px;color:var(--ldoh-text-light);font-family:monospace;overflow:hidden;text-overflow:ellipsis">sk-${k.key.substring(0, 16)}...</div>
`
});
const deleteBtn = UI.div({
className: "ldh-delete-btn",
innerHTML: UI.ICONS.TRASH,
style: "position:absolute;top:8px;right:8px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;cursor:pointer;opacity:0;transition:all 0.2s;color:var(--ldoh-danger);",
title: "\u5220\u9664\u5BC6\u94A5",
onMouseOver: (e) => e.target.closest(".ldh-delete-btn").style.background = "rgba(239,68,68,0.1)",
onMouseOut: (e) => e.target.closest(".ldh-delete-btn").style.background = "transparent",
onClick: (e) => {
e.stopPropagation();
if (!window.confirm(`\u5220\u9664\u5BC6\u94A5 "${k.name || "\u672A\u547D\u540D"}"\uFF1F`)) return;
API.deleteToken(host, data.token, data.userId, k.id).then((res) => {
if (res.success) {
Toast.success("\u5BC6\u94A5\u5220\u9664\u6210\u529F");
item.remove();
} else Toast.error(res.message || "\u5BC6\u94A5\u5220\u9664\u5931\u8D25");
});
}
});
item.appendChild(deleteBtn);
item.onmouseenter = () => deleteBtn.style.opacity = "1";
item.onmouseleave = () => deleteBtn.style.opacity = "0";
item.onclick = (e) => {
if (e.target.closest(".ldh-delete-btn")) return;
const isAlreadyActive = item.classList.contains("active");
keysGrid.querySelectorAll(".ldh-item").forEach((el) => el.classList.remove("active"));
let selectedGroup = null;
if (!isAlreadyActive) {
item.classList.add("active");
selectedGroup = item.dataset.group;
copy(item.dataset.key);
Toast.success(`\u5DF2\u9009\u4E2D\u5206\u7EC4 ${selectedGroup || "\u9ED8\u8BA4"} \u5E76\u590D\u5236\u5BC6\u94A5`);
} else {
copy(item.dataset.key);
Toast.success("\u5DF2\u590D\u5236\u5BC6\u94A5");
}
let visibleCount = 0;
modelItems.forEach((mi) => {
let isVisible = true;
if (selectedGroup) {
try {
isVisible = JSON.parse(mi.dataset.modelGroups || "[]").includes(selectedGroup);
} catch (_err) {
isVisible = mi.dataset.modelName.toLowerCase().includes(selectedGroup.toLowerCase());
}
}
mi.style.display = isVisible ? "" : "none";
if (isVisible) visibleCount++;
});
modelsBadge.textContent = selectedGroup ? `${visibleCount}/${modelArray.length}` : modelArray.length;
};
return item;
}
async function showDetailsDialog(host, data) {
if (_detailLoadingHosts.has(host)) return;
_detailLoadingHosts.add(host);
let loadingOverlay = null;
try {
loadingOverlay = createOverlay(
'<div class="ldh-header"><div class="ldh-title">\u6B63\u5728\u83B7\u53D6\u5BC6\u94A5\u548C\u6A21\u578B...</div></div><div class="ldh-content" style="align-items:center;justify-content:center;min-height:200px"><div class="ldoh-refresh-btn loading">' + UI.ICONS.REFRESH + "</div></div>"
);
const details = await API.fetchDetails(host, data.token, data.userId);
loadingOverlay.remove();
const { models, keys } = details;
const modelArray = models?.data && Array.isArray(models.data) ? models.data : Array.isArray(models) ? models : [];
const dialog = UI.div({ className: "ldh-dialog", children: [_buildDetailHeader(host)] });
const content = UI.div({ className: "ldh-content" });
const modelItems = [];
const modelsBadge = UI.span({ className: "ldh-sec-badge", textContent: modelArray.length });
content.appendChild(
_renderKeySection(host, data, keys || [], modelItems, modelsBadge, modelArray)
);
content.appendChild(_renderModelSection(modelArray, modelItems, modelsBadge));
dialog.appendChild(content);
const overlay = createOverlay("");
overlay.querySelector(".ldh-dialog").replaceWith(dialog);
} catch (e) {
if (loadingOverlay?.parentNode) loadingOverlay.remove();
Log.error(`[\u8BE6\u60C5\u5931\u8D25] ${host}`, e);
Toast.error("\u83B7\u53D6\u8BE6\u60C5\u5931\u8D25");
} finally {
_detailLoadingHosts.delete(host);
}
}
// src/utils/bus.js
var EventBus = {
_listeners: /* @__PURE__ */ new Map(),
on(event, callback) {
if (!this._listeners.has(event)) {
this._listeners.set(event, /* @__PURE__ */ new Set());
}
this._listeners.get(event).add(callback);
return () => this.off(event, callback);
},
off(event, callback) {
if (this._listeners.has(event)) {
this._listeners.get(event).delete(callback);
}
},
emit(event, ...args) {
if (this._listeners.has(event)) {
for (const callback of this._listeners.get(event)) {
try {
callback(...args);
} catch (e) {
console.error(`[EventBus] Error in listener for event ${event}:`, e);
}
}
}
},
clear(event) {
if (event) {
this._listeners.delete(event);
} else {
this._listeners.clear();
}
}
};
var UI_EVENTS = {
SHOW_DETAILS: "site:show_details",
GLOBAL_REFRESH: "ui:global_refresh",
// 全局刷新信号
DATA_CHANGED: "data:changed"
};
// src/services/site.js
var SiteService = {
/**
* 刷新全部站点数据。
*/
async refreshAll() {
const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
const sites = Object.entries(allData).filter(
([host, data]) => data.userId && data.token && !isBlacklisted(host)
);
const siteCount = sites.length;
if (siteCount === 0) {
Toast.info("\u6CA1\u6709\u7AD9\u70B9\u6570\u636E\u9700\u8981\u5237\u65B0");
return;
}
const progressToast = Toast.show(`\u6B63\u5728\u5237\u65B0\u7AD9\u70B9 0/${siteCount}...`, "info", 0);
let completedCount = 0;
let successCount = 0;
let failedCount = 0;
const promises = sites.map(async ([host, data]) => {
try {
const fresh = await API.updateSiteStatus(host, data.userId, true);
EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: fresh, renderable: true });
successCount++;
} catch (e) {
Log.error(`[SiteRefresh] \u5237\u65B0\u7AD9\u70B9\u5931\u8D25: ${host}`, e);
failedCount++;
}
completedCount++;
const messageEl = progressToast.querySelector(".ldoh-toast-message");
if (messageEl) {
messageEl.textContent = `\u6B63\u5728\u5237\u65B0\u7AD9\u70B9 ${completedCount}/${siteCount}...`;
}
});
await Promise.all(promises);
Toast.remove(progressToast);
if (failedCount > 0) {
Toast.warning(`\u5237\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${successCount} \u4E2A\uFF0C\u5931\u8D25 ${failedCount} \u4E2A`, 4e3);
} else {
Toast.success(`\u5237\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${successCount} \u4E2A\uFF0C\u5931\u8D25 0 \u4E2A`, 3e3);
}
EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
},
/**
* 一键签到符合条件的站点。
* @param {boolean} showConfirm
*/
async checkinEligibleSites(showConfirm = true) {
const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
const today = getTodayString();
const sites = Object.entries(allData).filter(([host, data]) => {
if (!data.userId || !data.token || data.checkinSupported === false) return false;
if (isCheckinSkipped(host)) return false;
const lastCheckinDate = data.lastCheckinDate || "1970-01-01";
return lastCheckinDate !== today || data.checkedInToday === false;
});
if (sites.length === 0) {
Toast.info("\u6240\u6709\u7AD9\u70B9\u4ECA\u5929\u90FD\u5DF2\u7B7E\u5230");
return;
}
if (showConfirm) {
const siteList = sites.map(([host, data]) => {
const lastCheckin = data.lastCheckinDate || "\u4ECE\u672A";
return ` \u2022 ${host} (\u4E0A\u6B21: ${lastCheckin})`;
}).join("\n");
const confirmed = window.confirm(
`\u{1F381} \u5C06\u5BF9\u4EE5\u4E0B ${sites.length} \u4E2A\u7AD9\u70B9\u8FDB\u884C\u81EA\u52A8\u7B7E\u5230\uFF1A
${siteList}
\u6CE8\u610F\uFF1A\u90E8\u5206\u7AD9\u70B9\u53EF\u80FD\u6709 CF \u6821\u9A8C\uFF0C\u7B7E\u5230\u53EF\u80FD\u5931\u8D25\u6216\u8D85\u65F6\uFF0810\u79D2\uFF09
\u662F\u5426\u7EE7\u7EED\uFF1F`
);
if (!confirmed) return;
}
const progressToast = Toast.show(`\u6B63\u5728\u7B7E\u5230 0/${sites.length}...`, "info", 0);
const siteResults = /* @__PURE__ */ new Map();
let completedCount = 0;
const failedSites = [];
const checkinSite = async (host, data, updateProgress = true) => {
try {
const result = await API.checkin(host, data.token, data.userId);
if (updateProgress) {
completedCount++;
const messageEl = progressToast.querySelector(".ldoh-toast-message");
if (messageEl) {
messageEl.textContent = `\u6B63\u5728\u7B7E\u5230 ${completedCount}/${sites.length}...`;
}
}
if (result.success || result.alreadyCheckedIn) {
siteResults.set(host, result.success ? "success" : "already");
const siteData = getSiteData(host);
siteData.lastCheckinDate = today;
siteData.checkedInToday = true;
if (result.success && result.data?.quota_awarded) {
siteData.quota = (siteData.quota || 0) + result.data.quota_awarded;
}
saveSiteData(host, siteData);
EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: siteData, renderable: true });
return true;
}
if (result.error === "\u7B7E\u5230\u8D85\u65F6\uFF0815\u79D2\uFF09") {
siteResults.set(host, "timeout");
return false;
}
siteResults.set(host, "fail");
return false;
} catch (e) {
Log.error(`[AutoCheckin] \u7B7E\u5230\u7AD9\u70B9\u5931\u8D25: ${host}`, e);
siteResults.set(host, "fail");
if (updateProgress) {
completedCount++;
const messageEl = progressToast.querySelector(".ldoh-toast-message");
if (messageEl) {
messageEl.textContent = `\u6B63\u5728\u7B7E\u5230 ${completedCount}/${sites.length}...`;
}
}
return false;
}
};
await Promise.all(
sites.map(async ([host, data]) => {
const success = await checkinSite(host, data);
if (!success) failedSites.push([host, data]);
})
);
const maxRetries = 2;
for (let retry = 1; retry <= maxRetries && failedSites.length > 0; retry++) {
const messageEl = progressToast.querySelector(".ldoh-toast-message");
if (messageEl) {
messageEl.textContent = `\u7B2C ${retry} \u6B21\u91CD\u8BD5 ${failedSites.length} \u4E2A\u5931\u8D25\u7AD9\u70B9...`;
}
const retrySites = [...failedSites];
failedSites.length = 0;
await Promise.all(
retrySites.map(async ([host, data]) => {
const success = await checkinSite(host, data, false);
if (!success) failedSites.push([host, data]);
})
);
completedCount = sites.length - failedSites.length;
const progressMessageEl = progressToast.querySelector(".ldoh-toast-message");
if (progressMessageEl) {
progressMessageEl.textContent = `\u6B63\u5728\u7B7E\u5230 ${completedCount}/${sites.length}...`;
}
}
Toast.remove(progressToast);
let successCount = 0;
let alreadyCheckedCount = 0;
for (const status of siteResults.values()) {
if (status === "success") successCount++;
else if (status === "already") alreadyCheckedCount++;
}
if (successCount > 0 || alreadyCheckedCount > 0) {
Toast.success(`\u7B7E\u5230\u5B8C\u6210\uFF01\u6210\u529F ${successCount} \u4E2A\uFF0C\u5DF2\u7B7E\u5230 ${alreadyCheckedCount} \u4E2A`, 5e3);
} else {
Toast.warning("\u7B7E\u5230\u5B8C\u6210\uFF0C\u4F46\u6CA1\u6709\u6210\u529F\u7684\u7AD9\u70B9", 5e3);
}
EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
},
/**
* 批量刷新过期站点。
*/
async refreshStaleSitesBatch() {
const settings = GM_getValue(CONFIG.SETTINGS_KEY, {});
const intervalMs = (settings.interval || CONFIG.DEFAULT_INTERVAL) * 60 * 1e3;
const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
const now = Date.now();
const staleSites = Object.entries(allData).filter(([host, siteData]) => {
return siteData.userId && siteData.token && !isBlacklisted(host) && siteData.ts && now - siteData.ts >= intervalMs;
});
if (!staleSites.length) return;
let hasAnySuccessfulUpdate = false;
await Promise.allSettled(
staleSites.map(async ([host, siteData]) => {
try {
const fresh = await API.updateSiteStatus(host, siteData.userId, false);
EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: fresh, renderable: true });
hasAnySuccessfulUpdate = true;
} catch (e) {
Log.error(`[\u81EA\u52A8\u5237\u65B0] ${host}`, e);
}
})
);
if (hasAnySuccessfulUpdate) {
EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
}
},
/**
* 单个站点刷新
*/
async refreshSite(host, siteData, showToast2 = true) {
try {
const fresh = await API.updateSiteStatus(host, siteData.userId, true, true);
EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: fresh, renderable: true });
if (showToast2) Toast.success(`${host} \u5DF2\u66F4\u65B0`);
return fresh;
} catch (err) {
Log.error(`[\u7AD9\u70B9\u5237\u65B0] ${host}`, err);
if (showToast2)
Toast.error(String(err?.message || "").includes("\u8BF7\u6C42\u8D85\u65F6") ? "\u5237\u65B0\u8D85\u65F6" : "\u5237\u65B0\u5931\u8D25");
throw err;
}
},
/**
* 删除站点缓存数据,并通知 UI 移除对应元素
*/
async deleteSiteData(host) {
const normalizedHost = normalizeHost(host);
const all = GM_getValue(CONFIG.STORAGE_KEY, {});
if (all[normalizedHost]) {
delete all[normalizedHost];
GM_setValue(CONFIG.STORAGE_KEY, all);
EventBus.emit(UI_EVENTS.DATA_CHANGED, {
host: normalizedHost,
next: null,
renderable: false
});
EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
return true;
}
return false;
}
};
// src/ui/panel.js
function _findCardByHost(host) {
const target = normalizeHost(host);
const container = document.querySelector(
`.${CONFIG.DOM.HELPER_CONTAINER_CLASS}[data-host="${target}"]`
);
return container ? container.closest(CONFIG.DOM.CARD_SELECTOR) : null;
}
var FloatingPanel = {
_fab: null,
_panel: null,
_isOpen: false,
_searchQuery: "",
_checkinFilter: "",
_checkinRunning: false,
_refreshAllRunning: false,
_confirmPop: null,
_confirmOutsideHandler: null,
_settingsPop: null,
_settingsOutsideHandler: null,
_intervalPop: null,
_intervalOutsideHandler: null,
_concurrencyPop: null,
_concurrencyOutsideHandler: null,
_pendingRefresh: false,
_refreshTimer: null,
_rendering: false,
init() {
if (document.getElementById("ldoh-fab")) return;
injectStyles();
const fab = UI.button({
id: "ldoh-fab",
className: "ldoh-fab",
title: "\u7AD9\u70B9\u603B\u89C8",
innerHTML: `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
</svg>
<span class="ldoh-fab-badge" id="ldoh-fab-badge" style="display:none">0</span>
`,
onClick: (e) => {
e.stopPropagation();
this.toggle();
}
});
document.body.appendChild(fab);
this._fab = fab;
const panel = UI.div({
id: "ldoh-floating-panel",
className: "ldoh-floating-panel",
style: "display: none;"
});
document.body.appendChild(panel);
this._panel = panel;
document.addEventListener("click", (e) => {
const isOnPopover = this._confirmPop && this._confirmPop.contains(e.target) || this._settingsPop && this._settingsPop.contains(e.target) || this._intervalPop && this._intervalPop.contains(e.target) || this._concurrencyPop && this._concurrencyPop.contains(e.target);
if (this._isOpen && !panel.contains(e.target) && !fab.contains(e.target) && !isOnPopover) {
this.close();
}
});
EventBus.on(UI_EVENTS.DATA_CHANGED, (delta) => this.applyDelta(delta));
EventBus.on(UI_EVENTS.GLOBAL_REFRESH, () => this.refresh());
this._updateBadge();
Log.debug("[FloatingPanel] \u521D\u59CB\u5316\u5B8C\u6210");
},
toggle() {
this._isOpen ? this.close() : this.open();
},
open() {
this._isOpen = true;
this._panel.style.display = "flex";
this._panel.classList.add("ldoh-panel-in");
setTimeout(() => this._panel.classList.remove("ldoh-panel-in"), CONFIG.ANIMATION_NORMAL_MS);
this.render();
},
close() {
this._isOpen = false;
this._searchQuery = "";
this._removeIntervalPopover();
this._removeSettingsMenu();
this._removeConfirmPopover();
this._removeConcurrencyPopover();
this._panel.style.display = "none";
},
refresh() {
this._updateBadge();
if (!this._isOpen) return;
if (this._confirmPop || this._settingsPop || this._intervalPop || this._concurrencyPop) {
this._pendingRefresh = true;
return;
}
clearTimeout(this._refreshTimer);
this._refreshTimer = setTimeout(() => this.render(), 150);
},
_getCheckinMeta(siteData) {
if (siteData.checkinSupported !== false) {
if (isCheckedInToday(siteData)) {
return { checkinClass: "ok", checkinText: "\u5DF2\u7B7E\u5230" };
}
if (siteData.checkedInToday === false || siteData.lastCheckinDate) {
return { checkinClass: "no", checkinText: "\u672A\u7B7E\u5230" };
}
}
return { checkinClass: "na", checkinText: "\u2500" };
},
_applyRowVisibility(row) {
const matchSearch = !this._searchQuery || row.dataset.searchKey.includes(this._searchQuery);
const matchFilter = !this._checkinFilter || row.dataset.checkinStatus === this._checkinFilter;
row.style.display = matchSearch && matchFilter ? "" : "none";
},
/**
* 局部更新面板单行
*/
updateSiteRow(host, siteData) {
if (!this._isOpen || !this._panel) return false;
const normalizedHost = normalizeHost(host);
const row = this._panel.querySelector(`.ldoh-panel-row[data-host="${normalizedHost}"]`);
if (!row) return false;
const balanceEl = row.querySelector(".ldoh-panel-balance-col");
if (balanceEl) balanceEl.textContent = `$${formatQuota(siteData.quota)}`;
const { checkinClass, checkinText } = this._getCheckinMeta(siteData);
const checkinEl = row.querySelector(".ldoh-panel-checkin-col");
if (checkinEl) {
checkinEl.className = `ldoh-panel-checkin ldoh-panel-checkin-col ${checkinClass}`;
checkinEl.textContent = checkinText;
}
row.dataset.checkinStatus = checkinClass;
this._applyRowVisibility(row);
return true;
},
applyDelta(delta) {
this._updateBadge();
if (!this._isOpen) return { handled: false, needRefresh: false };
if (!delta?.renderable) return { handled: false, needRefresh: true };
const updated = this.updateSiteRow(delta.host, delta.next || {});
return { handled: updated, needRefresh: !updated };
},
refreshBadgeOnly() {
this._updateBadge();
},
_updateBadge() {
const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
const count = Object.values(allData).filter((d) => d.userId || d.quota != null).length;
const badge = document.getElementById("ldoh-fab-badge");
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? "flex" : "none";
}
},
_showSettingsMenu(anchorEl) {
if (!anchorEl) return;
this._removeIntervalPopover();
this._removeConfirmPopover();
this._removeSettingsMenu();
const actions = [
{ label: "\u8BBE\u7F6E\u66F4\u65B0\u95F4\u9694", handler: (triggerEl) => this._showIntervalPopover(triggerEl) },
{ label: "\u8BBE\u7F6E\u5E76\u53D1\u6570", handler: (triggerEl) => this._showConcurrencyPopover(triggerEl) },
{
label: "\u91CD\u7F6E\u68C0\u6D4B\u9ED1\u540D\u5355",
handler: (triggerEl) => {
this._showConfirmPopover(triggerEl, "\u786E\u8BA4\u91CD\u7F6E\u68C0\u6D4B\u9ED1\u540D\u5355\uFF1F", () => {
GM_setValue(CONFIG.BLACKLIST_KEY, []);
GM_setValue(CONFIG.BLACKLIST_REMOVED_KEY, []);
Toast.success("\u68C0\u6D4B\u9ED1\u540D\u5355\u5DF2\u91CD\u7F6E");
this.refresh();
});
}
},
{
label: "\u91CD\u7F6E\u7B7E\u5230\u9ED1\u540D\u5355",
handler: (triggerEl) => {
this._showConfirmPopover(triggerEl, "\u786E\u8BA4\u91CD\u7F6E\u7B7E\u5230\u9ED1\u540D\u5355\uFF1F", () => {
GM_setValue(CONFIG.CHECKIN_SKIP_KEY, []);
GM_setValue(CONFIG.CHECKIN_SKIP_REMOVED_KEY, []);
Toast.success("\u7B7E\u5230\u9ED1\u540D\u5355\u5DF2\u91CD\u7F6E");
this.refresh();
});
}
},
{
label: "\u6E05\u7406\u7F13\u5B58",
danger: true,
handler: (triggerEl) => {
const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
if (Object.keys(allData).length === 0) {
Toast.info("\u7F13\u5B58\u5DF2\u7ECF\u662F\u7A7A\u7684");
return;
}
this._showConfirmPopover(triggerEl, `\u786E\u8BA4\u6E05\u9664\u7F13\u5B58\uFF1F`, () => {
GM_setValue(CONFIG.STORAGE_KEY, {});
Toast.success("\u7F13\u5B58\u5DF2\u6E05\u7406\uFF0C\u9875\u9762\u5C06\u5237\u65B0", 2e3);
setTimeout(() => location.reload(), 2e3);
});
}
},
{ label: "\u4F7F\u7528\u8BF4\u660E", handler: () => this._showHelpDialog() }
];
const pop = UI.div({ id: "ldoh-settings-pop", className: "ldoh-settings-pop" });
actions.forEach(({ label, handler, danger }) => {
pop.appendChild(
UI.button({
className: "ldoh-settings-item",
textContent: label,
style: danger ? "color: var(--ldoh-danger, #ef4444)" : "",
onClick: (e) => {
e.stopPropagation();
this._removeSettingsMenu();
handler(anchorEl);
}
})
);
});
const rect = anchorEl.getBoundingClientRect();
Object.assign(pop.style, {
top: `${rect.bottom + 6}px`,
right: `${window.innerWidth - rect.right}px`
});
document.body.appendChild(pop);
this._settingsPop = pop;
this._settingsOutsideHandler = (e) => {
if (!pop.contains(e.target) && !anchorEl.contains(e.target)) this._removeSettingsMenu();
};
setTimeout(() => document.addEventListener("click", this._settingsOutsideHandler), 0);
},
_showHelpDialog() {
const html = `
<div style="padding:24px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:var(--ldoh-text)">\u4F7F\u7528\u8BF4\u660E</h2>
<div style="font-size:13px;color:var(--ldoh-text-muted);line-height:1.7;max-height:60vh;overflow-y:auto;padding-right:4px">
<p><strong>LDOH New API Helper</strong><br>\u81EA\u52A8\u540C\u6B65\u7AD9\u70B9\u989D\u5EA6\u4E0E\u7B7E\u5230\u72B6\u6001\u3002</p>
</div>
<div style="text-align:right;margin-top:16px">
<button type="button" class="ldh-dialog-close" style="padding:6px 20px;border-radius:6px;background:var(--ldoh-surface2);color:var(--ldoh-text);border:none;cursor:pointer;font-size:13px">\u5173\u95ED</button>
</div>
</div>`;
const ov = createOverlay(html);
const closeBtn = ov.querySelector(".ldh-dialog-close");
if (closeBtn)
closeBtn.onclick = () => {
ov.style.opacity = "0";
setTimeout(() => ov.remove(), 200);
};
},
_showIntervalPopover(anchorEl) {
if (!anchorEl) return;
this._removeConfirmPopover();
this._removeSettingsMenu();
this._removeConcurrencyPopover();
this._removeIntervalPopover();
const current = GM_getValue(CONFIG.SETTINGS_KEY, {
interval: CONFIG.DEFAULT_INTERVAL
}).interval;
const inputEl = UI.input({
type: "number",
min: "5",
step: "1",
value: current,
className: "ldoh-interval-input"
});
const pop = UI.div({
id: "ldoh-interval-pop",
className: "ldoh-interval-pop",
children: [
UI.div({ className: "ldoh-interval-title", textContent: "\u8BBE\u7F6E\u66F4\u65B0\u95F4\u9694" }),
inputEl,
UI.div({ className: "ldoh-interval-hint", textContent: "\u5355\u4F4D\uFF1A\u5206\u949F\uFF0C\u6700\u5C0F\u503C 5 \u5206\u949F" }),
UI.div({
className: "ldoh-interval-actions",
children: [
UI.button({
className: "ldoh-pop-btn ldoh-pop-cancel",
textContent: "\u53D6\u6D88",
onClick: (e) => {
e.stopPropagation();
this._removeIntervalPopover();
}
}),
UI.button({
className: "ldoh-pop-btn ldoh-pop-confirm",
textContent: "\u4FDD\u5B58",
onClick: (e) => {
e.stopPropagation();
const val = parseInt(inputEl.value, 10);
if (isNaN(val) || val < 5) {
Toast.error("\u65E0\u6548\u7684\u95F4\u9694\u503C");
return;
}
GM_setValue(CONFIG.SETTINGS_KEY, {
...GM_getValue(CONFIG.SETTINGS_KEY, {}),
interval: val
});
Toast.success(`\u5DF2\u66F4\u65B0\u4E3A ${val} \u5206\u949F`);
this._removeIntervalPopover();
}
})
]
})
]
});
const rect = anchorEl.getBoundingClientRect();
Object.assign(pop.style, {
top: `${rect.bottom + 6}px`,
right: `${window.innerWidth - rect.right}px`
});
document.body.appendChild(pop);
this._intervalPop = pop;
pop.addEventListener("click", (e) => e.stopPropagation());
setTimeout(() => inputEl.focus(), 0);
this._intervalOutsideHandler = (e) => {
if (!pop.contains(e.target) && !anchorEl.contains(e.target)) this._removeIntervalPopover();
};
setTimeout(() => document.addEventListener("click", this._intervalOutsideHandler), 0);
},
_removeConfirmPopover() {
if (this._confirmOutsideHandler)
document.removeEventListener("click", this._confirmOutsideHandler);
this._confirmOutsideHandler = null;
if (this._confirmPop) {
this._confirmPop.remove();
this._confirmPop = null;
}
if (!this._rendering) this._flushPendingRefresh();
},
_showConfirmPopover(anchorEl, text, onConfirm) {
if (!anchorEl) return;
this._removeIntervalPopover();
this._removeSettingsMenu();
this._removeConfirmPopover();
this._removeConcurrencyPopover();
const pop = UI.div({
id: "ldoh-confirm-pop",
className: "ldoh-confirm-pop",
children: [
UI.span({ style: "white-space:pre-line", textContent: text }),
UI.button({
className: "ldoh-pop-btn ldoh-pop-cancel",
textContent: "\u53D6\u6D88",
onClick: (e) => {
e.stopPropagation();
this._removeConfirmPopover();
}
}),
UI.button({
className: "ldoh-pop-btn ldoh-pop-confirm",
textContent: "\u786E\u8BA4",
onClick: (e) => {
e.stopPropagation();
this._removeConfirmPopover();
onConfirm?.();
}
})
]
});
const rect = anchorEl.getBoundingClientRect();
Object.assign(pop.style, {
top: `${rect.top - 48}px`,
right: `${window.innerWidth - rect.right}px`
});
document.body.appendChild(pop);
this._confirmPop = pop;
this._confirmOutsideHandler = (e) => {
if (!pop.contains(e.target) && !anchorEl.contains(e.target)) this._removeConfirmPopover();
};
setTimeout(() => document.addEventListener("click", this._confirmOutsideHandler), 0);
},
_removeIntervalPopover() {
if (this._intervalOutsideHandler)
document.removeEventListener("click", this._intervalOutsideHandler);
this._intervalOutsideHandler = null;
if (this._intervalPop) {
this._intervalPop.remove();
this._intervalPop = null;
}
if (!this._rendering) this._flushPendingRefresh();
},
_removeConcurrencyPopover() {
if (this._concurrencyOutsideHandler)
document.removeEventListener("click", this._concurrencyOutsideHandler);
this._concurrencyOutsideHandler = null;
if (this._concurrencyPop) {
this._concurrencyPop.remove();
this._concurrencyPop = null;
}
if (!this._rendering) this._flushPendingRefresh();
},
_showConcurrencyPopover(anchorEl) {
if (!anchorEl) return;
this._removeConfirmPopover();
this._removeSettingsMenu();
this._removeIntervalPopover();
this._removeConcurrencyPopover();
const s = GM_getValue(CONFIG.SETTINGS_KEY, {});
const curConcurrent = s.maxConcurrent || CONFIG.DEFAULT_MAX_CONCURRENT;
const curBackground = s.maxBackground || CONFIG.DEFAULT_MAX_BACKGROUND;
const totalInput = UI.input({
type: "number",
min: "1",
max: "50",
value: curConcurrent,
className: "ldoh-interval-input"
});
const bgInput = UI.input({
type: "number",
min: "1",
max: "50",
value: curBackground,
className: "ldoh-interval-input"
});
const pop = UI.div({
id: "ldoh-concurrency-pop",
className: "ldoh-interval-pop",
style: "width:240px;",
children: [
UI.div({ className: "ldoh-interval-title", textContent: "\u8BBE\u7F6E\u5E76\u53D1\u6570" }),
UI.div({
children: [
UI.div({ style: "font-size:11px;margin-bottom:4px;", textContent: "\u603B\u5E76\u53D1\u6570" }),
totalInput
]
}),
UI.div({
children: [
UI.div({ style: "font-size:11px;margin-bottom:4px;", textContent: "\u540E\u53F0\u5E76\u53D1\u6570" }),
bgInput
]
}),
UI.div({ className: "ldoh-interval-hint", textContent: "\u540E\u53F0\u5E76\u53D1\u5E94 \u2264 \u603B\u5E76\u53D1" }),
UI.div({
className: "ldoh-interval-actions",
children: [
UI.button({
className: "ldoh-pop-btn ldoh-pop-cancel",
textContent: "\u53D6\u6D88",
onClick: (e) => {
e.stopPropagation();
this._removeConcurrencyPopover();
}
}),
UI.button({
className: "ldoh-pop-btn ldoh-pop-confirm",
textContent: "\u4FDD\u5B58",
onClick: (e) => {
e.stopPropagation();
const total = parseInt(totalInput.value, 10);
const bg = parseInt(bgInput.value, 10);
if (bg > total) {
Toast.error("\u540E\u53F0\u5E76\u53D1\u4E0D\u80FD\u5927\u4E8E\u603B\u5E76\u53D1");
return;
}
GM_setValue(CONFIG.SETTINGS_KEY, {
...GM_getValue(CONFIG.SETTINGS_KEY, {}),
maxConcurrent: total,
maxBackground: bg
});
Toast.success("\u5E76\u53D1\u8BBE\u7F6E\u5DF2\u66F4\u65B0");
this._removeConcurrencyPopover();
}
})
]
})
]
});
const rect = anchorEl.getBoundingClientRect();
Object.assign(pop.style, {
top: `${rect.bottom + 6}px`,
right: `${window.innerWidth - rect.right}px`
});
document.body.appendChild(pop);
this._concurrencyPop = pop;
pop.addEventListener("click", (e) => e.stopPropagation());
setTimeout(() => totalInput.focus(), 0);
this._concurrencyOutsideHandler = (e) => {
if (!pop.contains(e.target) && !anchorEl.contains(e.target)) this._removeConcurrencyPopover();
};
setTimeout(() => document.addEventListener("click", this._concurrencyOutsideHandler), 0);
},
_removeSettingsMenu() {
if (this._settingsOutsideHandler)
document.removeEventListener("click", this._settingsOutsideHandler);
this._settingsOutsideHandler = null;
if (this._settingsPop) {
this._settingsPop.remove();
this._settingsPop = null;
}
if (!this._rendering) this._flushPendingRefresh();
},
_flushPendingRefresh() {
if (this._pendingRefresh && this._isOpen && !this._confirmPop && !this._settingsPop && !this._intervalPop && !this._concurrencyPop) {
this._pendingRefresh = false;
this.render();
}
},
render() {
if (!this._panel) return;
this._rendering = true;
const existingBody = this._panel.querySelector(".ldoh-panel-body");
const savedScroll = existingBody ? existingBody.scrollTop : 0;
this._removeIntervalPopover();
this._removeSettingsMenu();
this._removeConfirmPopover();
this._removeConcurrencyPopover();
const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
const sorted = Object.entries(allData).filter(([, d]) => d.userId || d.quota != null).sort(([, a], [, b]) => (b.quota || 0) - (a.quota || 0));
const totalBalance = sorted.reduce((sum, [, d]) => sum + (d.quota || 0), 0);
this._panel.innerHTML = "";
this._panel.appendChild(this._buildHeader(sorted, totalBalance));
const { searchBar, bindSearch } = this._buildSearchBar();
this._panel.appendChild(searchBar);
const body = this._buildBody(sorted);
this._panel.appendChild(body);
bindSearch(body);
body.scrollTop = savedScroll;
this._rendering = false;
},
_buildHeader(sorted, totalBalance) {
const hd = UI.div({
className: "ldoh-panel-hd",
innerHTML: `
<div class="ldoh-panel-hd-title">${UI.ICONS.PANEL} \u7AD9\u70B9\u603B\u89C8 <span class="ldh-sec-badge">${sorted.length}</span></div>
<div class="ldoh-panel-hd-total">\u5408\u8BA1 <strong style="color:#d97706">$${formatQuota(totalBalance)}</strong></div>
`
});
const refreshAllBtn = UI.div({
className: `ldoh-btn ldoh-refresh-btn ${this._refreshAllRunning ? "loading" : ""}`,
title: "\u5237\u65B0\u6240\u6709\u7AD9\u70B9\u6570\u636E",
innerHTML: UI.ICONS.REFRESH,
onClick: (e) => {
e.stopPropagation();
if (this._refreshAllRunning || refreshAllBtn.classList.contains("loading")) return;
this._showConfirmPopover(refreshAllBtn, "\u786E\u8BA4\u5237\u65B0\u5168\u90E8\uFF1F", async () => {
this._refreshAllRunning = true;
refreshAllBtn.classList.add("loading");
try {
await SiteService.refreshAll();
} finally {
this._refreshAllRunning = false;
this.refresh();
}
});
}
});
const checkinBtn = UI.div({
className: `ldoh-btn ldoh-refresh-btn ${this._checkinRunning ? "loading" : ""}`,
title: "\u4E00\u952E\u7B7E\u5230",
innerHTML: UI.ICONS.CHECKIN,
onClick: (e) => {
e.stopPropagation();
if (this._checkinRunning || checkinBtn.classList.contains("loading")) return;
this._showConfirmPopover(checkinBtn, "\u786E\u8BA4\u81EA\u52A8\u7B7E\u5230\uFF1F", async () => {
this._checkinRunning = true;
checkinBtn.classList.add("loading");
try {
await SiteService.checkinEligibleSites(false);
} finally {
this._checkinRunning = false;
this.refresh();
}
});
}
});
const settingsBtn = UI.div({
className: "ldoh-btn",
title: "\u8BBE\u7F6E",
innerHTML: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 .99-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06-.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51.99H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
onClick: (e) => {
e.stopPropagation();
this._showSettingsMenu(settingsBtn);
}
});
hd.appendChild(refreshAllBtn);
hd.appendChild(checkinBtn);
hd.appendChild(settingsBtn);
hd.appendChild(
UI.div({
className: "ldoh-btn",
title: "\u5173\u95ED",
innerHTML: UI.ICONS.CLOSE,
onClick: () => this.close()
})
);
return hd;
},
_buildSearchBar() {
const searchBar = UI.div({
className: "ldoh-panel-search",
innerHTML: `
<div class="ldoh-panel-search-wrap">
<svg class="ldoh-panel-search-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input class="ldoh-panel-search-input" placeholder="\u641C\u7D22\u7AD9\u70B9..." value="${escapeHtml(this._searchQuery)}">
</div>
<div class="ldoh-filter-bar">
<span class="ldoh-filter-chip ${this._checkinFilter === "" ? "active" : ""}" data-filter="">\u5168\u90E8</span>
<span class="ldoh-filter-chip ${this._checkinFilter === "ok" ? "active" : ""}" data-filter="ok">\u5DF2\u7B7E\u5230</span>
<span class="ldoh-filter-chip ${this._checkinFilter === "no" ? "active" : ""}" data-filter="no">\u672A\u7B7E\u5230</span>
<span class="ldoh-filter-chip ${this._checkinFilter === "na" ? "active" : ""}" data-filter="na">\u4E0D\u652F\u6301/\u65E0\u6CD5\u68C0\u6D4B\u7B7E\u5230</span>
</div>
`
});
const bindSearch = (body) => {
const input = searchBar.querySelector(".ldoh-panel-search-input");
input.oninput = () => {
clearTimeout(this._searchTimer);
this._searchTimer = setTimeout(() => {
this._searchQuery = input.value.toLowerCase().trim();
body.querySelectorAll(".ldoh-panel-row").forEach((row) => this._applyRowVisibility(row));
}, 200);
};
searchBar.querySelectorAll(".ldoh-filter-chip").forEach((chip) => {
chip.onclick = () => {
this._checkinFilter = chip.dataset.filter;
searchBar.querySelectorAll(".ldoh-filter-chip").forEach((c) => c.classList.toggle("active", c.dataset.filter === this._checkinFilter));
body.querySelectorAll(".ldoh-panel-row").forEach((row) => this._applyRowVisibility(row));
};
});
};
return { searchBar, bindSearch };
},
_buildBody(sorted) {
const body = UI.div({ className: "ldoh-panel-body" });
if (!sorted.length) {
body.appendChild(UI.div({ className: "ldoh-panel-empty", textContent: "\u6682\u65E0\u7AD9\u70B9\u6570\u636E" }));
} else {
sorted.forEach(([host, siteData]) => body.appendChild(this._buildSiteRow(host, siteData)));
}
return body;
},
_buildSiteRow(host, siteData) {
const isBlk = isBlacklisted(host);
const { checkinClass, checkinText } = this._getCheckinMeta(siteData);
const row = UI.div({
className: "ldoh-panel-row",
dataset: {
host: normalizeHost(host),
searchKey: `${siteData.siteName || ""} ${host}`.toLowerCase(),
checkinStatus: checkinClass
}
});
row.appendChild(
UI.div({
className: "ldoh-panel-name",
innerHTML: `<span class="ldoh-panel-name-main">${siteData.siteName || host}</span><span class="ldoh-panel-name-host">${host}</span>`
})
);
row.appendChild(
UI.div({
className: `ldoh-panel-checkin ldoh-panel-checkin-col ${checkinClass}`,
textContent: checkinText
})
);
row.appendChild(
UI.div({
className: "ldoh-panel-balance ldoh-panel-balance-col",
textContent: `$${formatQuota(siteData.quota)}`
})
);
if (!isBlk) {
row.appendChild(
UI.div({
className: "ldoh-btn ldoh-details-btn",
title: "\u5BC6\u94A5\u4E0E\u6A21\u578B\u8BE6\u60C5",
innerHTML: UI.ICONS.DETAILS,
onClick: async (e) => {
e.stopPropagation();
const btn = e.currentTarget;
if (btn.classList.contains("loading")) return;
btn.classList.add("loading");
try {
await showDetailsDialog(host, siteData);
} finally {
btn.classList.remove("loading");
}
}
})
);
} else {
row.appendChild(UI.div({ style: "width:22px" }));
}
if (!isBlk) {
const refreshBtn = UI.div({
className: "ldoh-btn ldoh-refresh-btn",
title: "\u5237\u65B0\u6570\u636E",
innerHTML: UI.ICONS.REFRESH,
onClick: async (e) => {
if (refreshBtn.classList.contains("loading")) return;
refreshBtn.classList.add("loading");
try {
await SiteService.refreshSite(host, siteData);
} finally {
refreshBtn.classList.remove("loading");
}
}
});
row.appendChild(refreshBtn);
} else {
row.appendChild(UI.div({ style: "width:22px" }));
}
row.appendChild(
UI.div({
className: "ldoh-btn",
title: "\u5B9A\u4F4D\u5361\u7247",
innerHTML: UI.ICONS.LOCATE,
onClick: () => {
const card = _findCardByHost(host);
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "center" });
card.style.outline = "2px solid var(--ldoh-primary)";
card.style.outlineOffset = "2px";
setTimeout(() => {
card.style.outline = "";
card.style.outlineOffset = "";
}, 2e3);
} else {
Toast.warning(`\u672A\u627E\u5230 ${host} \u7684\u5361\u7247`);
}
}
})
);
const blkBtn = UI.div({
className: "ldoh-btn",
title: isBlk ? "\u88AB\u52A8\u76D1\u63A7\uFF08\u70B9\u51FB\u6062\u590D\u4E3B\u52A8\u68C0\u6D4B\uFF09" : "\u4E3B\u52A8\u68C0\u6D4B\uFF08\u70B9\u51FB\u5207\u6362\u4E3A\u88AB\u52A8\u76D1\u63A7\uFF09",
style: `color: ${isBlk ? "#9ca3af" : "var(--ldoh-success)"}`,
innerHTML: UI.ICONS.EYE,
onClick: (e) => {
e.stopPropagation();
this._showConfirmPopover(
blkBtn,
isBlk ? "\u6062\u590D\u4E3B\u52A8\u68C0\u6D4B\uFF1F" : "\u52A0\u5165\u9ED1\u540D\u5355\uFF08\u88AB\u52A8\u76D1\u63A7\uFF09\uFF1F",
() => {
toggleBlacklist(host);
Toast.success(`${host} \u72B6\u6001\u5DF2\u66F4\u65B0`);
this.refresh();
}
);
}
});
row.appendChild(blkBtn);
const isSkipped = isCheckinSkipped(host);
const noSupport = siteData.checkinSupported === false;
const skipBtn = UI.div({
className: "ldoh-btn",
title: noSupport ? "\u4E0D\u652F\u6301\u7B7E\u5230" : isSkipped ? "\u5DF2\u8DF3\u8FC7\u7B7E\u5230\uFF08\u70B9\u51FB\u6062\u590D\uFF09" : "\u81EA\u52A8\u7B7E\u5230\u4E2D\uFF08\u70B9\u51FB\u8DF3\u8FC7\uFF09",
style: `color: ${noSupport || isSkipped || isBlk ? "#9ca3af" : "var(--ldoh-success)"}; cursor: ${noSupport || isBlk ? "default" : "pointer"}`,
innerHTML: noSupport ? UI.ICONS.CHECKIN_OFF : UI.ICONS.CHECKIN,
onClick: (e) => {
if (noSupport || isBlk) return;
const reason = getBuiltinCheckinSkipReason(host);
this._showConfirmPopover(
skipBtn,
isSkipped ? `\u6062\u590D\u81EA\u52A8\u7B7E\u5230\uFF1F${reason ? `
(\u539F\u56E0: ${reason})` : ""}` : "\u8DF3\u8FC7\u81EA\u52A8\u7B7E\u5230\uFF1F",
() => {
toggleCheckinSkip(host);
Toast.success(`${host} \u7B7E\u5230\u7B56\u7565\u5DF2\u66F4\u65B0`);
this.refresh();
}
);
}
});
row.appendChild(skipBtn);
const delBtn = UI.div({
className: "ldoh-btn",
title: "\u5220\u9664\u7F13\u5B58\u6570\u636E",
style: "color: var(--ldoh-danger)",
innerHTML: UI.ICONS.TRASH,
onMouseOver: (e) => e.currentTarget.style.background = "rgba(239, 68, 68, 0.1)",
onMouseOut: (e) => e.currentTarget.style.background = "transparent",
onClick: (e) => {
e.stopPropagation();
this._showConfirmPopover(delBtn, "\u786E\u8BA4\u5F7B\u5E95\u5220\u9664\u8BE5\u7AD9\u70B9\u7F13\u5B58\uFF1F", () => {
SiteService.deleteSiteData(host);
Toast.success(`\u5DF2\u5220\u9664 ${host} \u7F13\u5B58`);
});
}
});
row.appendChild(delBtn);
this._applyRowVisibility(row);
return row;
}
};
// src/ui/card.js
var CardView = {
_observer: null,
/**
* 初始化:开启扫描、注册观察器、订阅事件
*/
init() {
injectStyles();
const scheduleRescan = debounce(() => this.rescan(), CONFIG.DEBOUNCE_DELAY);
EventBus.on(UI_EVENTS.DATA_CHANGED, (delta) => {
if (delta.renderable) {
if (!this.updateByHost(delta.host, delta.next)) {
scheduleRescan();
}
} else {
this.removeByHost(delta.host);
}
});
EventBus.on(UI_EVENTS.GLOBAL_REFRESH, () => scheduleRescan());
this.rescan();
this._observer = new MutationObserver((mutations) => {
const hasCardChanges = mutations.some(
(m) => [...m.addedNodes, ...m.removedNodes].some(
(node) => node instanceof Element && (node.matches?.(CONFIG.DOM.CARD_SELECTOR) || node.querySelector?.(CONFIG.DOM.CARD_SELECTOR))
)
);
if (hasCardChanges) scheduleRescan();
});
this._observer.observe(document.body, { childList: true, subtree: true });
Log.debug("[CardView] \u521D\u59CB\u5316\u5B8C\u6210\uFF0C\u5DF2\u5F00\u542F\u81EA\u52A8\u76D1\u63A7");
},
/**
* 扫描全页卡片并挂载 UI
*/
rescan() {
const cards = Array.from(document.querySelectorAll(CONFIG.DOM.CARD_SELECTOR));
if (cards.length === 0) return;
cards.forEach((card) => {
const links = Array.from(card.querySelectorAll("a"));
const siteLink = links.find((a) => a.href.startsWith("http") && !a.href.includes("linux.do")) || links[0];
if (!siteLink) return;
try {
const host = normalizeHost(new URL(siteLink.href).hostname);
const container = this.ensureHostAnchor(card, host);
const siteData = getSiteData(host);
if (siteData.userId || siteData.quota != null) {
this.render(card, host, siteData);
} else if (container) {
container.innerHTML = "";
container.style.display = "none";
}
} catch (_e) {
}
});
},
/**
* 快速更新所有匹配 host 的卡片内容
*/
updateByHost(host, data) {
const target = normalizeHost(host);
const containers = document.querySelectorAll(
`.${CONFIG.DOM.HELPER_CONTAINER_CLASS}[data-host="${target}"]`
);
if (containers.length === 0) return false;
containers.forEach((container) => {
container.style.display = "";
this.renderContent(container, host, data);
});
return true;
},
/**
* 移除匹配 host 的辅助 UI
*/
removeByHost(host) {
const target = normalizeHost(host);
const containers = document.querySelectorAll(
`.${CONFIG.DOM.HELPER_CONTAINER_CLASS}[data-host="${target}"]`
);
containers.forEach((c) => c.remove());
return containers.length > 0;
},
/**
* 渲染/挂载卡片助手 UI
*/
render(card, host, data) {
const container = this.ensureHostAnchor(card, host);
container.style.display = "";
this.renderContent(container, host, data);
},
/**
* 确保卡片存在可定位锚点,并写入 data-host
*/
ensureHostAnchor(card, host) {
const targetHost = normalizeHost(host);
let container = card.querySelector(`.${CONFIG.DOM.HELPER_CONTAINER_CLASS}`);
if (!container) {
container = UI.div({
className: CONFIG.DOM.HELPER_CONTAINER_CLASS,
dataset: { host: targetHost }
});
const ut = Array.from(card.querySelectorAll("div")).find(
(el) => el.textContent.includes("\u66F4\u65B0\u65F6\u95F4") && (el.children.length === 0 || el.querySelector(`.${CONFIG.DOM.HELPER_CONTAINER_CLASS}`))
);
if (ut) {
Object.assign(ut.style, {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "8px"
});
if (ut.children.length === 0) {
const text = UI.span({ textContent: ut.textContent.trim() });
ut.textContent = "";
ut.appendChild(text);
}
ut.appendChild(container);
} else {
Object.assign(container.style, {
position: "absolute",
bottom: "8px",
right: "8px"
});
card.appendChild(container);
}
}
container.dataset.host = targetHost;
return container;
},
/**
* 纯内容渲染逻辑
*/
renderContent(container, host, data) {
container.innerHTML = "";
const isOk = isCheckedInToday(data);
const hasCheckin = data.checkinSupported !== false && (isOk || data.checkedInToday === false || data.lastCheckinDate);
container.appendChild(
UI.div({
className: "ldoh-info-bar",
children: [
UI.span({
style: { color: "#d97706" },
textContent: `$${formatQuota(data.quota)}`
}),
hasCheckin ? UI.span({ style: { opacity: "0.5" }, textContent: "|" }) : null,
hasCheckin ? UI.span({
style: {
color: isOk ? "var(--ldoh-success)" : "var(--ldoh-warning)"
},
textContent: isOk ? "\u5DF2\u7B7E\u5230" : "\u672A\u7B7E\u5230"
}) : null
]
})
);
if (!isBlacklisted(host)) {
container.appendChild(
UI.div({
className: "ldoh-btn ldoh-refresh-btn",
title: "\u5237\u65B0\u6570\u636E",
innerHTML: UI.ICONS.REFRESH,
onClick: async (e) => {
e.preventDefault();
e.stopPropagation();
const btn = e.currentTarget;
if (btn.classList.contains("loading")) return;
btn.classList.add("loading");
try {
await SiteService.refreshSite(host, data);
} finally {
btn.classList.remove("loading");
}
}
})
);
container.appendChild(
UI.div({
className: "ldoh-btn ldoh-details-btn",
title: "\u8BE6\u60C5",
innerHTML: UI.ICONS.DETAILS,
onClick: async (e) => {
e.preventDefault();
e.stopPropagation();
const btn = e.currentTarget;
if (btn.classList.contains("loading")) return;
btn.classList.add("loading");
try {
await showDetailsDialog(host, data);
} finally {
btn.classList.remove("loading");
}
}
})
);
}
},
destroy() {
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
}
};
// src/ui/sync.js
function toObject(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function toComparable(data) {
if (!data || typeof data !== "object") return null;
return {
userId: data.userId ?? null,
quota: data.quota ?? null,
checkedInToday: data.checkedInToday ?? null,
lastCheckinDate: data.lastCheckinDate ?? null,
checkinSupported: data.checkinSupported ?? null,
siteName: data.siteName ?? null
};
}
function isRenderable(data) {
return !!(data && (data.userId || data.quota != null));
}
function computeStorageDiff(oldValue, newValue) {
const oldData = toObject(oldValue);
const nextData = toObject(newValue);
const allHosts = /* @__PURE__ */ new Set([...Object.keys(oldData), ...Object.keys(nextData)]);
const deltas = [];
allHosts.forEach((rawHost) => {
const host = normalizeHost(rawHost);
if (!host) return;
const prev = oldData[rawHost] || null;
const next = nextData[rawHost] || null;
const prevComparable = toComparable(prev);
const nextComparable = toComparable(next);
const changed = JSON.stringify(prevComparable) !== JSON.stringify(nextComparable);
if (!changed) return;
deltas.push({
host,
prev,
next,
changed,
added: !prev && !!next,
removed: !!prev && !next,
renderable: isRenderable(next)
});
});
return deltas;
}
function attachStorageSync({ storageKey, remoteOnly = true } = {}) {
if (!storageKey) throw new Error("attachStorageSync: storageKey is required");
return GM_addValueChangeListener(storageKey, (_name, oldValue, newValue, remote) => {
if (remoteOnly && !remote) return;
const deltas = computeStorageDiff(oldValue, newValue);
if (!deltas.length) return;
deltas.forEach((delta) => {
EventBus.emit(UI_EVENTS.DATA_CHANGED, delta);
});
EventBus.emit(UI_EVENTS.PANEL_REFRESH);
});
}
// src/hooks.js
function _processSitesResponse(sites) {
const entries = [];
sites.forEach((site) => {
try {
const host = normalizeHost(new URL(site.apiBaseUrl).hostname);
if (!host) return;
entries.push({
host,
name: site.name || host,
supportsCheckin: site.supportsCheckin === true
});
} catch (_e) {
}
});
if (!entries.length) return;
GM_setValue(
CONFIG.WHITELIST_KEY,
entries.map((e) => e.host)
);
const allData = GM_getValue(CONFIG.STORAGE_KEY, {});
let changedCount = 0;
entries.forEach(({ host, name, supportsCheckin }) => {
const cur = allData[host] || {};
if (cur.siteName !== name || cur.checkinSupported !== supportsCheckin) {
allData[host] = { ...cur, siteName: name, checkinSupported: supportsCheckin };
changedCount++;
}
});
if (changedCount > 0) {
GM_setValue(CONFIG.STORAGE_KEY, allData);
EventBus.emit(UI_EVENTS.GLOBAL_REFRESH);
}
Log.debug(`[\u7AD9\u70B9\u76D1\u63A7] \u7AD9\u70B9\u5217\u8868\u5DF2\u540C\u6B65: ${entries.length} \u4E2A`);
}
var Hooks = {
/**
* 在 LDOH 门户 hook fetch,拦截 GET /api/sites 响应。
*/
installPortalSitesFetchHook() {
try {
const _origFetch = unsafeWindow.fetch;
unsafeWindow.fetch = new Proxy(_origFetch, {
apply(target, thisArg, args) {
const [input, init] = args;
const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
const method = (init?.method ?? "GET").toUpperCase();
const result = Reflect.apply(target, thisArg, args);
if (method === "GET" && url.includes("/api/sites") && !url.includes("mode=runaway")) {
result.then(async (res) => {
try {
const data = await res.clone().json();
if (Array.isArray(data.sites)) _processSitesResponse(data.sites);
} catch (_e) {
}
}).catch(() => {
});
}
return result;
}
});
Log.debug("[\u7AD9\u70B9\u76D1\u63A7] /api/sites hook \u5DF2\u542F\u52A8");
} catch (e) {
Log.warn("[\u7AD9\u70B9\u76D1\u63A7] installPortalSitesFetchHook \u5931\u8D25", e);
}
},
/**
* 在公益站页面 hook XHR,监控用户手动签到。
*/
installCheckinXhrHook() {
try {
const XHR = unsafeWindow.XMLHttpRequest;
if (XHR.prototype.__ldoh_hooked) return;
XHR.prototype.__ldoh_hooked = true;
const _open = XHR.prototype.open;
const _send = XHR.prototype.send;
XHR.prototype.open = function(method, url, ...rest) {
this._ldoh_method = method;
this._ldoh_url = url;
return _open.apply(this, [method, url, ...rest]);
};
XHR.prototype.send = function(_body) {
if (this._ldoh_method?.toUpperCase() === "POST" && typeof this._ldoh_url === "string" && this._ldoh_url.includes("/api/user/checkin")) {
this.addEventListener("load", function() {
try {
const res = JSON.parse(this.responseText);
if (res.success) {
const host = normalizeHost(window.location.hostname);
const siteData = getSiteData(host);
siteData.checkedInToday = true;
siteData.lastCheckinDate = getTodayString();
if (res.data?.quota_awarded) {
siteData.quota = (siteData.quota || 0) + res.data.quota_awarded;
}
saveSiteData(host, siteData);
EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: siteData, renderable: true });
Log.success(`[\u7B7E\u5230\u76D1\u63A7] ${host} - \u7B7E\u5230\u6210\u529F`);
}
} catch (e) {
Log.debug("[\u7B7E\u5230\u76D1\u63A7] \u89E3\u6790\u5931\u8D25", e);
}
});
}
return _send.apply(this, arguments);
};
Log.debug("[\u7B7E\u5230\u76D1\u63A7] XHR hook \u5DF2\u542F\u52A8");
} catch (e) {
Log.warn("[\u7B7E\u5230\u76D1\u63A7] XHR hook \u5931\u8D25", e);
}
},
/**
* 薄荷公益站(up.x666.me)签到监控
*/
installX666CheckinHook() {
try {
if (unsafeWindow.__ldoh_x666_fetch_hooked) return;
unsafeWindow.__ldoh_x666_fetch_hooked = true;
const _origFetch = unsafeWindow.fetch;
unsafeWindow.fetch = new Proxy(_origFetch, {
apply(target, thisArg, args) {
const [input, init] = args;
const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
const method = (init?.method ?? "GET").toUpperCase();
const result = Reflect.apply(target, thisArg, args);
if (method === "POST" && url.includes("/api/checkin/spin")) {
result.then(async (res) => {
try {
const data = await res.clone().json();
if (data?.success) {
const host = "x666.me";
const siteData = getSiteData(host);
siteData.checkedInToday = true;
siteData.lastCheckinDate = getTodayString();
if (typeof data.new_balance === "number") siteData.quota = data.new_balance;
saveSiteData(host, siteData);
EventBus.emit(UI_EVENTS.DATA_CHANGED, {
host,
next: siteData,
renderable: true
});
Log.success(`[\u7B7E\u5230\u76D1\u63A7] ${host} - \u7B7E\u5230\u6210\u529F`);
}
} catch (_e) {
}
}).catch(() => {
});
}
return result;
}
});
Log.debug("[\u7B7E\u5230\u76D1\u63A7] x666 hook \u5DF2\u542F\u52A8");
} catch (e) {
Log.warn("[\u7B7E\u5230\u76D1\u63A7] x666 hook \u5931\u8D25", e);
}
},
/**
* runanytime 福利转盘监控
*/
installRunanytimeWheelHook() {
try {
if (unsafeWindow.__ldoh_runanytime_wheel_fetch_hooked) return;
unsafeWindow.__ldoh_runanytime_wheel_fetch_hooked = true;
const _origFetch = unsafeWindow.fetch;
unsafeWindow.fetch = new Proxy(_origFetch, {
apply(target, thisArg, args) {
const [input, init] = args;
const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
const method = (init?.method ?? "GET").toUpperCase();
const result = Reflect.apply(target, thisArg, args);
if (method === "POST" && url.includes("/api/wheel")) {
result.then(async (res) => {
try {
const data = await res.clone().json();
if (data?.success !== true) return;
const host = "runanytime.hxi.me";
const siteData = getSiteData(host);
if (!siteData.token || !siteData.userId) return;
const selfRes = await API.fetchSelf(host, siteData.token, siteData.userId);
if (selfRes.success && selfRes.data?.quota != null) {
siteData.quota = selfRes.data.quota;
saveSiteData(host, siteData);
EventBus.emit(UI_EVENTS.DATA_CHANGED, {
host,
next: siteData,
renderable: true
});
Log.success(`[\u798F\u5229\u8F6C\u76D8] ${host} - \u4F59\u989D\u5DF2\u540C\u6B65`);
}
} catch (_e) {
}
}).catch(() => {
});
}
return result;
}
});
Log.debug("[\u798F\u5229\u8F6C\u76D8] runanytime hook \u5DF2\u542F\u52A8");
} catch (e) {
Log.warn("[\u798F\u5229\u8F6C\u76D8] runanytime hook \u5931\u8D25", e);
}
},
/**
* hook XHR,监听 GET /api/user/self,被动同步余额
*/
installSelfProfileXhrHook() {
try {
const XHR = unsafeWindow.XMLHttpRequest;
if (XHR.prototype.__ldoh_self_hooked) return;
XHR.prototype.__ldoh_self_hooked = true;
const _open = XHR.prototype.open;
XHR.prototype.open = function(method, url, ...rest) {
this._ldoh_self_method = method;
this._ldoh_self_url = url;
return _open.apply(this, [method, url, ...rest]);
};
const _send = XHR.prototype.send;
XHR.prototype.send = function(_body) {
if (this._ldoh_self_method?.toUpperCase() === "GET" && typeof this._ldoh_self_url === "string" && this._ldoh_self_url.includes("/api/user/self")) {
this.addEventListener("load", function() {
try {
const res = JSON.parse(this.responseText);
if (res.success && res.data?.quota != null) {
const host = normalizeHost(window.location.hostname);
const siteData = getSiteData(host);
siteData.quota = res.data.quota;
saveSiteData(host, siteData);
EventBus.emit(UI_EVENTS.DATA_CHANGED, { host, next: siteData, renderable: true });
Log.success(`[\u4F59\u989D\u76D1\u63A7] ${host} - \u4F59\u989D\u5DF2\u66F4\u65B0`);
}
} catch (_e) {
}
});
}
return _send.apply(this, arguments);
};
Log.debug("[\u4F59\u989D\u76D1\u63A7] XHR hook \u5DF2\u542F\u52A8");
} catch (e) {
Log.warn("[\u4F59\u989D\u76D1\u63A7] XHR hook \u5931\u8D25", e);
}
},
/**
* hook XHR,监听 POST /api/user/topup,兑换码成功后更新余额。
*/
installTopupXhrHook() {
try {
const XHR = unsafeWindow.XMLHttpRequest;
if (XHR.prototype.__ldoh_topup_hooked) return;
XHR.prototype.__ldoh_topup_hooked = true;
const _open = XHR.prototype.open;
XHR.prototype.open = function(method, url, ...rest) {
this._ldoh_topup_method = method;
this._ldoh_topup_url = url;
return _open.apply(this, [method, url, ...rest]);
};
const _send = XHR.prototype.send;
XHR.prototype.send = function(_body) {
if (this._ldoh_topup_method?.toUpperCase() === "POST" && typeof this._ldoh_topup_url === "string" && this._ldoh_topup_url.includes("/api/user/topup")) {
this.addEventListener("load", function() {
try {
const res = JSON.parse(this.responseText);
if (res.success && res.data > 0) {
const host = normalizeHost(window.location.hostname);
const siteData = getSiteData(host);
API.fetchSelf(host, siteData.token, siteData.userId).then((selfRes) => {
if (selfRes.success && selfRes.data?.quota != null) {
siteData.quota = selfRes.data.quota;
saveSiteData(host, siteData);
EventBus.emit(UI_EVENTS.DATA_CHANGED, {
host,
next: siteData,
renderable: true
});
Log.success(`[\u5151\u6362\u7801] ${host} - \u4F59\u989D\u5DF2\u66F4\u65B0`);
}
}).catch(() => {
});
}
} catch (_e) {
}
});
}
return _send.apply(this, arguments);
};
Log.debug("[\u5151\u6362\u7801] XHR hook \u5DF2\u542F\u52A8");
} catch (e) {
Log.warn("[\u5151\u6362\u7801] XHR hook \u5931\u8D25", e);
}
}
};
// src/main.js
if (window.top === window.self && !window.__LDOH_HELPER_RUNNING__) {
window.__LDOH_HELPER_RUNNING__ = true;
"use strict";
let storageChangeListenerId = null;
let staleDataRefreshTimer = null;
async function ensureSiteIdentityAndSync(host) {
const syncStatus = async (uid) => {
const normalizedHost = normalizeHost(host);
const siteData = getSiteData(normalizedHost);
if (!siteData.token) {
const token = await API.fetchToken(normalizedHost, uid);
if (token) {
siteData.token = token;
saveSiteData(normalizedHost, siteData);
}
}
await SiteService.refreshSite(host, siteData, false);
};
let userId = getAndSyncUserId(host);
if (userId) {
syncStatus(userId).catch(() => {
});
} else {
userId = await waitForLogin() || await new Promise((resolve) => watchLoginStatus(resolve));
if (userId) {
Toast.success("\u8BC6\u522B\u5230\u767B\u5F55\uFF0C\u6B63\u5728\u540C\u6B65...");
userId = getAndSyncUserId(host);
syncStatus(userId).catch(() => {
});
}
}
}
async function init() {
try {
const host = window.location.hostname;
const isPortal = host === CONFIG.PORTAL_HOST;
EventBus.on(UI_EVENTS.SHOW_DETAILS, (host2, data) => showDetailsDialog(host2, data));
if (isPortal) {
Log.info("\u73AF\u5883: LDOH \u95E8\u6237");
CardView.init();
FloatingPanel.init();
Hooks.installPortalSitesFetchHook();
storageChangeListenerId = attachStorageSync({ storageKey: CONFIG.STORAGE_KEY });
SiteService.refreshStaleSitesBatch();
staleDataRefreshTimer = setInterval(() => SiteService.refreshStaleSitesBatch(), 6e4);
} else {
Log.info(`\u73AF\u5883: \u7AD9\u70B9 ${host}`);
if (host === "up.x666.me") return Hooks.installX666CheckinHook();
if (host === "fuli.hxi.me") return Hooks.installRunanytimeWheelHook();
if (await isNewApiSite()) {
Hooks.installCheckinXhrHook();
Hooks.installSelfProfileXhrHook();
Hooks.installTopupXhrHook();
if (!isBlacklisted(normalizeHost(host))) await ensureSiteIdentityAndSync(host);
}
}
} catch (e) {
Log.error("\u521D\u59CB\u5316\u5931\u8D25", e);
}
}
window.addEventListener("beforeunload", () => {
CardView.destroy();
if (storageChangeListenerId) GM_removeValueChangeListener(storageChangeListenerId);
if (staleDataRefreshTimer) clearInterval(staleDataRefreshTimer);
});
init();
}
})();