Greasy Fork

来自缓存

Greasy Fork is available in English.

薄荷签到助手

linux.do 提示薄荷签到、抽奖强化与炫酷提示

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         薄荷签到助手
// @namespace    https://linux.do/
// @version      0.1.3
// @description  linux.do 提示薄荷签到、抽奖强化与炫酷提示
// @match        https://linux.do/*
// @match        https://qd.x666.me/*
// @match        https://x666.me/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js
// @icon         https://i.111666.best/image/UQ3YrIrF59JZfaEFGJabrr.png
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// @grant        unsafeWindow
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  /**
   * ==========================================================================
   * 常量与基础配置
   * ==========================================================================
   */
  const CONFIG = {
    iconUrl: 'https://i.111666.best/image/UQ3YrIrF59JZfaEFGJabrr.png',
    urls: {
      check: 'https://qd.x666.me/?from=bohe-auto',
      popup: 'https://qd.x666.me/?from=bohe-popup',
      topup: 'https://x666.me/console/topup'
    },
    storage: {
      spinStatus: 'bohe-spin-status',
      latestCdk: 'bohe-latest-cdk',
      topupFinish: 'bohe-topup-finish',
      autoCheckDate: 'bohe-auto-check-date',
      loginSuccess: 'bohe-login-success',
      floatPos: 'bohe-float-pos',
      logs: 'bohe-logs'
    },
    events: {
      canSpin: 'bohe-event-can-spin',
      topupDone: 'bohe-event-topup-done'
    }
  };
  // 站点刷新以 UTC+8 的 8:00 为界
  const TARGET_TZ_OFFSET_HOURS = 8;
  // 测试模式,无限月读
  const IS_TEST_MODE = false;

  const UTILS = {
    // 按目标时区(UTC+8)计算日期,避免本地时区干扰
    todayStr: () => {
      const now = new Date();
      const diffMinutes = TARGET_TZ_OFFSET_HOURS * 60 + now.getTimezoneOffset();
      const shifted = new Date(now.getTime() + diffMinutes * 60000);
      return shifted.toISOString().slice(0, 10);
    },
    now: () => Date.now(),
    // 计算下一次目标时区小时的本地触发时间(默认 8:00)
    nextTargetTimeMs: (targetHour = 8) => {
      const now = new Date();
      const diffMinutes = TARGET_TZ_OFFSET_HOURS * 60 + now.getTimezoneOffset();
      const toTargetMs = (ts) => ts + diffMinutes * 60000;
      const fromTargetMs = (ts) => ts - diffMinutes * 60000;

      const nowTarget = new Date(toTargetMs(now.getTime()));
      const target = new Date(nowTarget);
      target.setHours(targetHour, 0, 1, 0);
      if (target <= nowTarget) target.setDate(target.getDate() + 1);

      return fromTargetMs(target.getTime());
    },
    isAfterTargetHour: (hour = 8) => {
      const now = new Date();
      const diffMinutes = TARGET_TZ_OFFSET_HOURS * 60 + now.getTimezoneOffset();
      const targetNow = new Date(now.getTime() + diffMinutes * 60000);
      return targetNow.getHours() >= hour;
    },
    // 通过 MutationObserver 监听节点变动,等待元素出现
    waitFor: (selector, root = document, timeout = 15000) => {
      return new Promise((resolve, reject) => {
        const existing = root.querySelector(selector);
        if (existing) return resolve(existing);

        const observer = new MutationObserver(() => {
          const el = root.querySelector(selector);
          if (el) {
            observer.disconnect();
            clearTimeout(timer);
            resolve(el);
          }
        });

        observer.observe(root, { childList: true, subtree: true });

        const timer = setTimeout(() => {
          observer.disconnect();
          reject(new Error(`Timeout waiting for ${selector}`));
        }, timeout);
      });
    }
  };

  /**
   * ==========================================================================
   * 简易日志:存 GM,提供菜单导出,自动清理 30 天前的记录
   * ==========================================================================
   */
  const Log = {
    retentionMs: 30 * 24 * 60 * 60 * 1000,
    maxItems: 200,
    add(entry) {
      try {
        const now = UTILS.now();
        const logs = GM_getValue(CONFIG.storage.logs, []);
        const fresh = logs.filter((i) => now - i.time <= this.retentionMs);
        fresh.push({ ...entry, time: now });
        GM_setValue(CONFIG.storage.logs, fresh.slice(-this.maxItems));
      } catch (e) {
        console.error('[Bohe] Log write failed:', e);
      }
    },
    error(label, err, extra = {}) {
      const detail = err instanceof Error ? { message: err.message, stack: err.stack } : { message: String(err) };
      this.add({ level: 'error', label, ...detail, extra });
    },
    info(label, extra = {}) {
      this.add({ level: 'info', label, extra });
    },
    exportLogs() {
      const logs = GM_getValue(CONFIG.storage.logs, []);
      const text = JSON.stringify(logs, null, 2);
      const blob = new Blob([text], { type: 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `bohe-logs-${Date.now()}.txt`;
      document.body.appendChild(a);
      a.click();
      setTimeout(() => {
        URL.revokeObjectURL(url);
        a.remove();
      }, 1000);
    }
  };
  try {
    GM_registerMenuCommand('导出薄荷日志', () => Log.exportLogs());
  } catch (_) {}

  /**
   * ==========================================================================
   * 站内跨页面消息桥(事件总线)
   * ==========================================================================
   */
  const Bridge = {
    allowedOrigins: ['https://linux.do', 'https://qd.x666.me', 'https://x666.me'],
    isAllowedOrigin(origin) {
      return this.allowedOrigins.includes(origin);
    },
    emit: (type, payload) => {
      try {
        window.postMessage({ type, payload, source: 'bohe-bridge' }, '*');
      } catch (e) {
        Log.error('Bridge emit failed', e);
      }
    },
    on: (type, callback) => {
      window.addEventListener('message', (event) => {
        if (!event.origin || !Bridge.isAllowedOrigin(event.origin)) return;
        if (event.data?.source === 'bohe-bridge' && event.data?.type === type) {
          callback(event.data.payload);
        }
      });
    }
  };

  /**
   * ==========================================================================
   * UI 系统(Shadow DOM 容器)
   * ==========================================================================
   */
  class BoheUI {
    constructor() {
      this.host = document.createElement('div');
      this.host.id = 'bohe-ui-host';
      this.host.style.cssText = 'position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none;';
      document.body.appendChild(this.host);
      this.shadow = this.host.attachShadow({ mode: 'closed' });
      this.injectStyles();
    }

    injectStyles() {
      const style = document.createElement('style');
      style.textContent = `
        :host { font-family: system-ui, -apple-system, sans-serif; } 
        
                /* 悬浮签到按钮 */
                .float-btn {
                  position: fixed;
                  top: 120px;
                  pointer-events: auto;
                  display: flex;
                  align-items: center;
                  gap: 0;
                  padding: 10px;
                  padding-left: 12px;
                  background: linear-gradient(135deg, #48d1a0, #3fb58c);
                  color: #fff;
                  cursor: pointer;
                  font-size: 14px;
                  font-weight: 600;
                  transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
                  opacity: 0;
                  z-index: 999999;
                }
                .float-btn.right {
                  right: 0;
                  border-radius: 30px 0 0 30px;
                  box-shadow: -2px 4px 15px rgba(63, 181, 140, 0.3);
                  transform: translateX(100%);
                }
                .float-btn.left {
                  left: 0;
                  border-radius: 0 30px 30px 0;
                  box-shadow: 2px 4px 15px rgba(63, 181, 140, 0.3);
                  transform: translateX(-100%);
                }
                .float-btn.visible {
                  opacity: 1;
                  transform: translateX(0);
                }
                .float-btn.right:hover {
                  padding-right: 16px;
                  border-radius: 30px;
                  transform: translateX(0);
                  box-shadow: 0 8px 25px rgba(63, 181, 140, 0.5);
                }
                .float-btn.left:hover {
                  padding-right: 16px;
                  border-radius: 30px;
                  transform: translateX(0);
                  box-shadow: 0 8px 25px rgba(63, 181, 140, 0.5);
                }
                .float-btn.dragging {
                  transition: none !important;
                  box-shadow: 0 8px 25px rgba(63, 181, 140, 0.5);
                }
                .float-btn img {
                  width: 28px;
                  height: 28px;
                  border-radius: 50%;
                  box-shadow: 0 0 8px rgba(255, 255, 255, 0.4);
                }
                .float-btn span {
                  max-width: 0;
                  opacity: 0;
                  overflow: hidden;
                  white-space: nowrap;
                  transition: all 0.3s ease;
                }
                .float-btn:hover span {
                  max-width: 100px;
                  opacity: 1;
                  margin-left: 8px;
                }
        /* 烟花弹窗文案 */
        .fw-overlay {
          position: fixed; inset: 0; 
          display: flex; align-items: center; justify-content: center;
          pointer-events: none;
        }
        .fw-text {
          font-size: 6rem; font-weight: 900;
          color: #48d1a0;
          text-shadow: 2px 2px 0px #2d8a68, 4px 4px 0px #1a5c43, 0 0 30px rgba(72, 209, 160, 0.8);
          display: flex; gap: 0.05em;
          filter: drop-shadow(0 0 8px rgba(255,255,255,0.5));
        }
        .fw-char {
          display: inline-block;
          position: relative;
          animation: jump 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
          opacity: 0;
        }
        .fw-char::after {
          content: '';
          position: absolute;
          top: 15%; left: 50%;
          width: 6px; height: 8px;
          background: #ff5e5e;
          transform-origin: top center;
          animation: sway 2s ease-in-out infinite alternate;
          border-radius: 50%;
          z-index: 1;
          opacity: 0.9;
          box-shadow: 8px 12px 0 -1px #f4d03f, -10px 15px 0 -1px #48d1a0;
        }
        .fw-char:nth-child(odd)::after { background: #48d1a0; width: 5px; height: 6px; animation-delay: 0.2s; box-shadow: 12px 10px 0 -1px #ff5e5e; }
        .fw-char:nth-child(3n)::after { background: #f4d03f; width: 7px; height: 9px; animation-delay: 0.4s; box-shadow: -12px 12px 0 -1px #3fb58c; }

        @keyframes jump {
          0% { opacity: 0; transform: translateY(80px) scale(0.3); }
          100% { opacity: 1; transform: translateY(0) scale(1); }
        }
        @keyframes sway {
          0% { transform: translateX(-50%) rotate(-25deg); }
          100% { transform: translateX(-50%) rotate(25deg); }
        }
      `;
      this.shadow.appendChild(style);
    }

    createFloatBtn(onClick, initialPos, onPosChange) {
      const btn = document.createElement('div');
      const pos = this.applyFloatPosition(btn, initialPos);
      btn.classList.add('float-btn', pos.side);
      btn.innerHTML = `
        <img src="${CONFIG.iconUrl}" alt="Bohe" />
        <span>薄荷签到</span>
      `;
      // 保护点击:拖拽后抑制一次点击
      const safeClick = (e) => {
        if (btn.__bohe_blockClick) {
          btn.__bohe_blockClick = false;
          e.preventDefault();
          e.stopPropagation();
          return;
        }
        onClick(e);
      };
      btn.addEventListener('click', safeClick);
      this.enableDrag(btn, pos, onPosChange);
      this.shadow.appendChild(btn);
      
      // 通过下一帧触发过渡,让按钮滑入
      requestAnimationFrame(() => btn.classList.add('visible'));
      
      return btn;
    }

    applyFloatPosition(btn, pos = {}) {
      const side = pos.side === 'left' ? 'left' : 'right';
      const rawTop = Number.isFinite(pos.top) ? pos.top : 120;
      const clampedTop = Math.min(Math.max(rawTop, 10), Math.max(20, window.innerHeight - 80));
      btn.classList.remove('left', 'right');
      btn.classList.add(side);
      btn.style.left = side === 'left' ? '0' : 'auto';
      btn.style.right = side === 'right' ? '0' : 'auto';
      btn.style.top = `${clampedTop}px`;
      return { side, top: clampedTop };
    }

    enableDrag(btn, initialPos, onPosChange = () => {}) {
      let lastPos = this.applyFloatPosition(btn, initialPos);
      let startX = 0;
      let startY = 0;
      let moved = false;
      const onPointerDown = (e) => {
        if (e.button !== 0) return;
        btn.classList.add('dragging');
        btn.setPointerCapture(e.pointerId);
        startX = e.clientX;
        startY = e.clientY;
        moved = false;
        const rect = btn.getBoundingClientRect();
        const offsetX = e.clientX - rect.left;
        const offsetY = e.clientY - rect.top;

        const move = (ev) => {
          const x = ev.clientX - offsetX;
          const y = ev.clientY - offsetY;
          const clampedTop = Math.min(Math.max(y, 10), window.innerHeight - btn.offsetHeight - 10);
          const clampedLeft = Math.min(Math.max(x, 0), window.innerWidth - btn.offsetWidth);
          btn.style.left = `${clampedLeft}px`;
          btn.style.right = 'auto';
          btn.style.top = `${clampedTop}px`;
          btn.classList.remove('left', 'right');
          if (!moved && (Math.abs(ev.clientX - startX) > 3 || Math.abs(ev.clientY - startY) > 3)) {
            moved = true;
          }
        };

        const up = (ev) => {
          btn.classList.remove('dragging');
          btn.releasePointerCapture(e.pointerId);
          window.removeEventListener('pointermove', move);
          window.removeEventListener('pointerup', up);
          const centerX = ev.clientX;
          const side = centerX < window.innerWidth / 2 ? 'left' : 'right';
          const finalPos = this.applyFloatPosition(btn, { side, top: parseFloat(btn.style.top) || lastPos.top });
          lastPos = finalPos;
          onPosChange(finalPos);
          if (moved) {
            btn.__bohe_blockClick = true;
            setTimeout(() => { btn.__bohe_blockClick = false; }, 50);
          }
        };

        window.addEventListener('pointermove', move);
        window.addEventListener('pointerup', up);
      };

      btn.addEventListener('pointerdown', onPointerDown);
    }

    showFireworksText(text) {
      const overlay = document.createElement('div');
      overlay.className = 'fw-overlay';
      const container = document.createElement('div');
      container.className = 'fw-text';
      overlay.appendChild(container);
      this.shadow.appendChild(overlay);

      [...text].forEach((char, i) => {
        const span = document.createElement('span');
        span.textContent = char;
        span.className = 'fw-char';
        span.style.animationDelay = `${i * 0.1}s`;
        container.appendChild(span);
      });

      // 延迟淡出并移除节点,避免残留
      setTimeout(() => {
        overlay.style.transition = 'opacity 0.5s ease';
        overlay.style.opacity = '0';
        setTimeout(() => overlay.remove(), 500);
      }, 4000);
    }
  }

  // 全局 UI 实例,供特效复用
  let ui = null;

  /**
   * ==========================================================================
   * 模块:linux.do 站内集成
   * ==========================================================================
   */
  function initLinux() {
    ui = new BoheUI();
    
    // 本地状态缓存:抽奖状态、浮窗窗口句柄、烟花时间戳
    const state = {
      spinStatus: GM_getValue(CONFIG.storage.spinStatus, null),
      floatPos: GM_getValue(CONFIG.storage.floatPos, { side: 'right', top: 120 }),
      floatBtn: null,
      popupWindow: null,
      topupHandledAt: 0,
    };

    // 1. 悬浮按钮:点击打开签到浮窗
    state.floatBtn = ui.createFloatBtn(
      () => openOverlay(state),
      state.floatPos,
      (pos) => {
        state.floatPos = pos;
        GM_setValue(CONFIG.storage.floatPos, pos);
      }
    );
    
    // 2. 同步抽奖状态,完成后隐藏按钮
    GM_addValueChangeListener(CONFIG.storage.spinStatus, (_, __, val) => {
      state.spinStatus = val;
      updateBtnVisibility(state);
    });
    updateBtnVisibility(state);

    // 3. 监听充值成功事件,触发烟花并关闭浮窗
    GM_addValueChangeListener(CONFIG.storage.topupFinish, (_, __, val) => {
      if (!val || val.time === state.topupHandledAt) return;
      state.topupHandledAt = val.time;
      closeOverlay(state);
      triggerFireworks(val.message || '恭喜佬薄荷签到获得10000点');
    });

    // 4. 自动检查调度器:后台拉起签到页同步抽奖状态,不主动提交签到
    setupAutoCheckScheduler();
    
    // 5. 搜索彩蛋:输入“薄荷”弹烟花
    setupSearchEgg();
  }

  // 根据今日抽奖状态控制悬浮按钮显隐
  function updateBtnVisibility(state) {
    if (!state.floatBtn) return;
    const s = state.spinStatus;
    const isDone = s && s.date === UTILS.todayStr() && s.canSpin === false && !IS_TEST_MODE;
    state.floatBtn.style.display = isDone ? 'none' : 'flex';
  }

  // 打开脚本专用浮窗,居中显示抽奖页面
  function openOverlay(state) {
    if (state.popupWindow && !state.popupWindow.closed) {
      state.popupWindow.focus();
      return;
    }
    const w = Math.min(520, window.screen.availWidth * 0.6);
    const h = Math.min(900, window.screen.availHeight * 0.86);
    const l = (window.screen.availWidth - w) / 2;
    const t = (window.screen.availHeight - h) / 2;
    
    state.popupWindow = window.open(
      CONFIG.urls.popup, 
      'bohe-popup',
      `width=${w},height=${h},left=${l},top=${t},resizable=yes,scrollbars=yes`
    );
  }

  // 关闭浮窗并重置句柄
  function closeOverlay(state) {
    if (state.popupWindow && !state.popupWindow.closed) state.popupWindow.close();
    state.popupWindow = null;
  }

  function setupAutoCheckScheduler() {
    const lastCheck = GM_getValue(CONFIG.storage.autoCheckDate, '');
    const today = UTILS.todayStr();
    if (lastCheck === today) return;

    const trigger = () => {
      const latest = GM_getValue(CONFIG.storage.autoCheckDate, '');
      const todayNow = UTILS.todayStr();
      if (latest === todayNow) return;
      GM_setValue(CONFIG.storage.autoCheckDate, todayNow);
      spawnCheckFrame();
    };

    if (UTILS.isAfterTargetHour(8)) {
      trigger();
    } else {
      const delay = Math.max(0, UTILS.nextTargetTimeMs(8) - Date.now());
      setTimeout(trigger, delay);
    }
  }

  function spawnCheckFrame() {
    // 在隐藏 iframe 中打开签到链接,避免干扰当前页面
    const iframe = document.createElement('iframe');
    iframe.src = CONFIG.urls.check;
    iframe.style.display = 'none';
    document.body.appendChild(iframe);
    
    // 超时后移除 iframe,防止泄漏
    setTimeout(() => iframe.remove(), 15000);
  }

  function setupSearchEgg() {
    let eggTriggered = false;
    // 全局监听搜索输入框,避免重复绑定
    document.body.addEventListener('input', (e) => {
      if (e.target && e.target.id === 'header-search-input') {
        const val = e.target.value.trim();
        if (val === '薄荷' && !eggTriggered) {
          eggTriggered = true;
          triggerFireworks('我爱薄荷佬');
          setTimeout(() => (eggTriggered = false), 8000);
        }
      }
    });
  }

  /**
   * ==========================================================================
   * 模块:抽奖页(qd.x666.me)
   * ==========================================================================
   */
  function initQd() {
    const params = new URLSearchParams(location.search);
    
    // 登录跳转时保留来源参数,便于识别脚本自动打开
    if (params.has('from')) {
        sessionStorage.setItem('bohe_from', params.get('from'));
    }
    const fromSource = params.get('from') || sessionStorage.getItem('bohe_from');

    // 仅脚本打开时隐藏榜单,减少遮挡
    if (fromSource) {
      const style = document.createElement('style');
      style.textContent = '#mainContent .ranking-panel{display:none !important;}';
      document.head.appendChild(style);
    }

    // 1. 监听抽奖可用状态并同步到主站
    Bridge.on(CONFIG.events.canSpin, (payload) => {
      const prev = GM_getValue(CONFIG.storage.spinStatus, {});
      GM_setValue(CONFIG.storage.spinStatus, {
        ...prev,
        canSpin: payload.canSpin,
        date: payload.date || UTILS.todayStr(),
        time: UTILS.now()
      });
      
      // 自动打开的窗口完成后自动关闭
      if (fromSource === 'bohe-auto') {
        setTimeout(() => window.close(), 500);
      }
    });

    // 2. 接管接口:同步状态并伪装抽奖奖励
    setupNetworkInterceptor();

    // 3. UI 调整:抽奖结果按钮改为“确定并自动兑换”
    tweakResultModal();
    tweakSpinBtn();
    // 4. 抓取 CDK,方便充值页自动填充
    observeCdk();
  }

  function setupNetworkInterceptor() {
    // 以函数字符串方式插入到页面上下文,便于拦截 fetch
    const interceptorFn = (eventName, isTestMode) => {
      const PATCH_KEY = '__bohe_fetch_patched__';
      if (window[PATCH_KEY]) return;
      const origFetch = window.fetch;
      const dateStr = () => {
        const now = new Date(Date.now() + 8 * 60 * 60 * 1000);
        return now.toISOString().slice(0, 10);
      };
      const wrapped = new Proxy(origFetch, {
        apply(target, thisArg, args) {
          return target.apply(thisArg, args).then(async (res) => {
            const url = (typeof args[0] === 'string' ? args[0] : args[0]?.url) || '';

            if (url.includes('/api/user/info')) {
              res.clone().json().then(data => {
                window.postMessage({
                  source: 'bohe-bridge',
                  type: eventName,
                  payload: { canSpin: data?.data?.can_spin, date: dateStr() }
                }, '*');
              }).catch(() => {});

              try {
                const data = await res.clone().json();
                if (data?.data?.user?.total_quota !== undefined) {
                  data.data.user.total_quota *= 88;
                }
                return new Response(JSON.stringify(data), {
                  status: res.status,
                  statusText: res.statusText,
                  headers: res.headers
                });
              } catch (_) {
                return res;
              }
            }

            if (url.includes('/api/lottery/spin')) {
              if (isTestMode) {
                return new Response(JSON.stringify({
                  success: true,
                  data: {
                    level: 1,
                    label: '10000次',
                    cdk: 'BOHE-TEST-CDK-10000',
                  }
                }), {
                  status: 200,
                  headers: { 'Content-Type': 'application/json' }
                });
              }
              try {
                const data = await res.clone().json();
                if (data && data.data && typeof data.data.level !== 'undefined') {
                  data.data.level = 1;
                  data.data.label = '10000次';
                  return new Response(JSON.stringify(data), {
                    status: res.status,
                    statusText: res.statusText,
                    headers: res.headers
                  });
                }
              } catch (_) {
                return res;
              }
            }

            return res;
          });
        }
      });
      window.fetch = wrapped;
      window[PATCH_KEY] = true;
    };

    // 注入脚本到页面作用域,确保能拦截原生 fetch
    const script = document.createElement('script');
    script.textContent = `(${interceptorFn.toString()})('${CONFIG.events.canSpin}', ${IS_TEST_MODE});`;
    document.documentElement.appendChild(script);
    script.remove();
  }

  // 强制去除spinButton禁用状态以便测试
  function tweakSpinBtn() {
    if (!IS_TEST_MODE) return;
    
    const update = () => {
      const btn = document.getElementById('spinButton');
      if (btn) {
        if (btn.disabled) btn.disabled = false;
        if (btn.textContent.includes('今日已抽奖')) {
          btn.textContent = '开始转动';
        }
      }
    };

    update();
    const observer = new MutationObserver(update);
    observer.observe(document.body, { 
      childList: true, 
      subtree: true, 
      attributes: true, 
      attributeFilter: ['disabled'] 
    });
    const stop = () => observer.disconnect();
    window.addEventListener('beforeunload', stop, { once: true });
  }

  function tweakResultModal() {
    const patchButton = (btn) => {
      btn.dataset.patched = '1';
      btn.textContent = '确定并自动兑换';
      
      // 克隆并替换按钮以清空原有事件
      const newBtn = btn.cloneNode(true);
      btn.parentNode.replaceChild(newBtn, btn);
      
      newBtn.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        
        // 隐藏弹窗
        const modal = document.getElementById('resultModal');
        if (modal) modal.style.display = 'none';
        
        // 读取抽奖码
        const cdkText = document.getElementById('resultCdk')?.textContent || '';
        const match = cdkText.match(/([A-Za-z0-9-]+)/);
        const cdk = match ? match[1] : '';
        
        if (cdk) GM_setValue(CONFIG.storage.latestCdk, cdk);
        
        // 跳转至充值页,带上抽奖码
        window.location.href = cdk 
          ? `${CONFIG.urls.topup}?cdk=${encodeURIComponent(cdk)}` 
          : CONFIG.urls.topup;
      });
    };

    // 使用 MutationObserver 监控结果弹窗出现,避免循环查找
    const observer = new MutationObserver(() => {
      const btn = document.querySelector('#resultModal .close-button');
      if (btn && !btn.dataset.patched) {
        patchButton(btn);
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
    const stop = () => observer.disconnect();
    setTimeout(stop, 300000);
    window.addEventListener('beforeunload', stop, { once: true });
  }

  function observeCdk() {
    // 监听 CDK 文本内容变化,及时保存
    const observer = new MutationObserver((mutations) => {
      for (const m of mutations) {
        if (m.target.id === 'resultCdk' || m.target.parentElement?.id === 'resultCdk') {
           const text = document.getElementById('resultCdk')?.textContent || '';
           const match = text.match(/([A-Za-z0-9-]+)/);
           if (match) GM_setValue(CONFIG.storage.latestCdk, match[1]);
        }
      }
    });
    
    UTILS.waitFor('#resultCdk').then(el => {
       observer.observe(el, { characterData: true, childList: true, subtree: true });
       const stop = () => observer.disconnect();
       window.addEventListener('beforeunload', stop, { once: true });
    }).catch((err) => {
      Log.error('Observe CDK failed', err);
    });
  }

  /**
   * ==========================================================================
   * 模块:充值页(x666.me)
   * ==========================================================================
   */
  function initX666() {
    const path = location.pathname;

    // 1. 监听登录成功时间戳,原始标签页感知后跳转充值
    GM_addValueChangeListener(CONFIG.storage.loginSuccess, (_, __, val) => {
         if (val && (Date.now() - val < 10000)) {
             handleRedirectToTopup();
         }
    });

    // 2. 处理登录回跳子窗口:轮询 token 页面后回传状态并自动关闭
    if (window.opener) {
        const checkAndClose = () => {
            // 仅在父窗口是脚本打开的弹窗时介入,跨域则跳过
            let isScriptPopup = false;
            try {
                isScriptPopup = window.opener.name === 'bohe-popup';
            } catch (e) {
            }

            if (!isScriptPopup) return false;

            if (location.pathname.includes('/console/token')) {
                 GM_setValue(CONFIG.storage.loginSuccess, Date.now());
                 setTimeout(() => window.close(), 300);
                 return true;
            }
            return false;
        };
        
        // 子窗口立即开始轮询,检测到 token 页后关闭自身
        if (!checkAndClose()) {
            setInterval(checkAndClose, 500);
        }
    }

    // 3. 充值页逻辑:自动填写并拦截成功回执
    if (path.includes('/console/topup')) {
        injectTopupInterceptor();
        autofill();
        return;
    }

    // 4. 控制台首页兜底跳转到充值页,避免停留在面板
    if ((path === '/console' || path === '/console/') && !window.opener) {
        handleRedirectToTopup();
    }
  }

  function handleRedirectToTopup() {
      const cdk = GM_getValue(CONFIG.storage.latestCdk, '');
      const target = cdk 
        ? `${CONFIG.urls.topup}?cdk=${encodeURIComponent(cdk)}` 
        : CONFIG.urls.topup;
      
      // 已在目标页则不再跳转,防止循环
      if (!location.href.includes(target)) {
          window.location.href = target;
      }
  }

  function injectTopupInterceptor() {
    // 插入到页面上下文,拦截充值接口并回传烟花提示
    const interceptorFn = (eventName) => {
      const PATCH_KEY = '__bohe_topup_patched__';
      if (window[PATCH_KEY]) return;
      const notify = () => {
        window.postMessage({ 
          source: 'bohe-bridge', 
          type: eventName, 
          payload: { message: '恭喜佬薄荷签到获得10000点' } 
        }, '*');
      };

      const shouldNotify = (data, status) => {
        if (status && status >= 400) return false;
        if (data && typeof data === 'object' && data.success === false) return false;
        return true;
      };

      // 劫持 XHR:拦截 /api/user/topup
      const origOpen = XMLHttpRequest.prototype.open;
      const origSend = XMLHttpRequest.prototype.send;
      XMLHttpRequest.prototype.open = function (method, url) {
        this._isTopup = url && url.includes('/api/user/topup');
        return origOpen.apply(this, arguments);
      };
      XMLHttpRequest.prototype.send = function () {
        if (this._isTopup) {
          this.addEventListener('loadend', () => {
            let parsed = null;
            try { parsed = JSON.parse(this.responseText || '{}'); } catch (_) {}
            if (shouldNotify(parsed, this.status)) {
              notify();
            }
          });
        }
        return origSend.apply(this, arguments);
      };
      
      // 劫持 fetch:同样监控充值接口
      const origFetch = window.fetch;
      const wrappedFetch = new Proxy(origFetch, {
        apply(target, thisArg, args) {
          return target.apply(thisArg, args).then(async (res) => {
            const url = (typeof args[0] === 'string' ? args[0] : args[0]?.url) || '';
            if (url.includes('/api/user/topup')) {
              if (!res.ok) return res;
              try {
                const data = await res.clone().json();
                if (!shouldNotify(data, res.status)) {
                  return res;
                }
              } catch (_) {
                // ignore parse error, treat as success
              }
              notify();
            }
            return res;
          });
        }
      });
      window.fetch = wrappedFetch;
      window[PATCH_KEY] = true;
    };

    const script = document.createElement('script');
    script.textContent = `(${interceptorFn.toString()})('${CONFIG.events.topupDone}');`;
    document.documentElement.appendChild(script);
    script.remove();

    Bridge.on(CONFIG.events.topupDone, (payload) => {
      GM_setValue(CONFIG.storage.topupFinish, {
        time: UTILS.now(),
        message: payload.message
      });
      // 清空已使用的 CDK
      GM_setValue(CONFIG.storage.latestCdk, '');
      // 延迟关闭当前页,给提示动画留时间
      setTimeout(() => window.close(), 800);
    });
  }

  function autofill() {
    const params = new URLSearchParams(location.search);
    const cdk = params.get('cdk') || GM_getValue(CONFIG.storage.latestCdk, '');
    if (!cdk) return;

    UTILS.waitFor('#redemptionCode').then(input => {
      // 兼容 React/Vue 的赋值方式,触发受控输入框更新
      const descriptor = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
      if (descriptor && descriptor.set) {
        descriptor.set.call(input, cdk);
      } else {
        input.value = cdk;
      }
      input.dispatchEvent(new Event('input', { bubbles: true }));
      
      // 延迟点击提交按钮
      setTimeout(() => {
        // 依次查找输入框旁边或父级旁边的提交按钮(semi-button-primary)
        let btn = input.nextElementSibling?.querySelector('.semi-button-primary');
        if (!btn && input.parentElement) {
          btn = input.parentElement.nextElementSibling?.querySelector('.semi-button-primary');
        }
        if (btn) btn.click();
      }, 500);
    });
  }

  /**
   * ==========================================================================
   * 共享特效
   * ==========================================================================
   */
  function triggerFireworks(text) {
    if (typeof confetti === 'undefined') return;
    
    // 1. 通过 canvas-confetti 撒花,混合叶片形状
    const colors = ['#ff4d4d', '#48d1a0', '#3fb58c', '#ffffff', '#f4d03f', '#ff5e5e', '#1a73e8', '#9c27b0', '#ff9800', '#00ffff', '#ff00ff'];
    let leaf = null;
    try { leaf = confetti.shapeFromText({ text: '🍃', scalar: 3 }); } catch (_) {}
    
    const end = Date.now() + 3000;
    (function frame() {
      const opts = {
        colors, 
        shapes: leaf ? [leaf, 'circle', 'square'] : ['circle', 'square'],
        scalar: 1.3, startVelocity: 70, zIndex: 2147483647 
      };
      
      confetti({ ...opts, particleCount: 7, angle: 55, spread: 90, origin: { x: 0, y: 0.65 } });
      confetti({ ...opts, particleCount: 7, angle: 125, spread: 90, origin: { x: 1, y: 0.65 } });
      
      if (Date.now() < end) requestAnimationFrame(frame);
    })();

    // 2. Shadow DOM 文案动画
    if (ui) ui.showFireworksText(text);
  }

  /**
   * ==========================================================================
   * 入口:按域名切换对应模块
   * ==========================================================================
   */
  const host = location.hostname;
  try {
    if (host.includes('linux.do')) initLinux();
    else if (host === 'qd.x666.me') initQd();
    else if (host === 'x666.me') initX666();
  } catch (err) {
    Log.error('Init error', err);
    console.error('[Bohe] Init error:', err);
  }

})();