Greasy Fork is available in English.
linux.do 提示薄荷签到、抽奖强化与炫酷提示
// ==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); } })();