// ==UserScript==
// @name 薄荷签到助手
// @namespace https://linux.do/
// @version 0.1.5
// @description linux.do 提示薄荷签到、抽奖强化与炫酷提示
// @match https://linux.do/*
// @match https://qd.x666.me/*
// @match https://x666.me/*
// @require https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/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-start
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/557970/%E8%96%84%E8%8D%B7%E7%AD%BE%E5%88%B0%E5%8A%A9%E6%89%8B.user.js
// @updateURL https://update.greasyfork.icu/scripts/557970/%E8%96%84%E8%8D%B7%E7%AD%BE%E5%88%B0%E5%8A%A9%E6%89%8B.meta.js
// ==/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;
},
// 等待 DOM 加载完成
domReady: () => {
return new Promise(resolve => {
if (document.readyState !== 'loading') resolve();
else document.addEventListener('DOMContentLoaded', resolve);
});
},
// 通过 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);
});
}
};
/**
* ==========================================================================
* 简易日志
* ==========================================================================
*/
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 {
// 使用 location.origin 替代 '*',提高安全性
window.postMessage({ type, payload, source: 'bohe-bridge' }, location.origin);
} 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;
user-select: none;
touch-action: none;
}
.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);
cursor: grabbing;
}
.float-btn img {
width: 28px;
height: 28px;
border-radius: 50%;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.4);
pointer-events: none;
}
.float-btn span {
max-width: 0;
opacity: 0;
overflow: hidden;
white-space: nowrap;
transition: all 0.3s ease;
pointer-events: none;
}
.float-btn:hover span {
max-width: 100px;
opacity: 1;
margin-left: 8px;
}
/* 拖拽提示 */
.drag-hint {
position: absolute;
top: -38px;
left: 50%;
transform: translateX(-50%) translateY(8px);
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
z-index: 1000000;
}
.drag-hint::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: rgba(0, 0, 0, 0.85) transparent transparent transparent;
}
.float-btn[data-is-default="true"]:hover .drag-hint {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* 烟花弹窗文案 */
.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);
// 初始位置检测 (默认右侧 120px)
if (pos.side === 'right' && Math.abs(pos.top - 120) < 1) {
btn.dataset.isDefault = 'true';
}
btn.innerHTML = `
薄荷签到
可拖拽
`;
this.enableDrag(btn, pos, onPosChange, onClick);
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, onClick) {
let lastPos = this.applyFloatPosition(btn, initialPos);
let startX = 0;
let startY = 0;
let startTime = 0;
const onPointerDown = (e) => {
if (e.button !== 0) return; // 仅左键
btn.setPointerCapture(e.pointerId);
startX = e.clientX;
startY = e.clientY;
startTime = Date.now();
const rect = btn.getBoundingClientRect();
const offsetX = e.clientX - rect.left;
const offsetY = e.clientY - rect.top;
const move = (ev) => {
// 移动超过 5px 才视为拖拽开始,避免手抖
if (!btn.classList.contains('dragging') &&
(Math.abs(ev.clientX - startX) > 5 || Math.abs(ev.clientY - startY) > 5)) {
btn.classList.add('dragging');
}
if (btn.classList.contains('dragging')) {
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');
}
};
const up = (ev) => {
const endTime = Date.now();
const dist = Math.sqrt(Math.pow(ev.clientX - startX, 2) + Math.pow(ev.clientY - startY, 2));
const isDrag = btn.classList.contains('dragging') || dist > 5;
btn.classList.remove('dragging');
btn.releasePointerCapture(e.pointerId);
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
if (isDrag) {
// 拖拽结束:吸附边缘并保存位置
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 });
// 更新是否在默认位置的状态
if (finalPos.side === 'right' && Math.abs(finalPos.top - 120) < 1) {
btn.dataset.isDefault = 'true';
} else {
delete btn.dataset.isDefault;
}
lastPos = finalPos;
onPosChange(finalPos);
} else {
// 点击判定:距离小且时间短,或者单纯未触发拖拽
onClick(ev);
}
};
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 站内集成
* ==========================================================================
*/
async function initLinux() {
// 等待 DOM 准备好再渲染 UI
await UTILS.domReady();
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 check = () => {
const latest = GM_getValue(CONFIG.storage.autoCheckDate, '');
const todayNow = UTILS.todayStr();
if (latest === todayNow) return;
GM_setValue(CONFIG.storage.autoCheckDate, todayNow);
spawnBackgroundTab();
};
if (UTILS.isAfterTargetHour(8)) {
check();
} else {
const delay = Math.max(0, UTILS.nextTargetTimeMs(8) - Date.now());
setTimeout(check, delay);
}
}
function spawnBackgroundTab() {
// 替代 iframe:使用 GM_openInTab 后台打开,确保 Cookie 共享更稳定
// 目标页面 (initQd) 检测到 from=bohe-auto 后会自动关闭
GM_openInTab(CONFIG.urls.check, { active: false, insert: true });
}
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)
* ==========================================================================
*/
async function initQd() {
// 1. 优先注入网络拦截器(无需等待 DOM)
setupNetworkInterceptor();
// 2. 等待 DOM 处理 UI
await UTILS.domReady();
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);
}
// 3. 监听消息
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(), 1000); // 稍作延迟确保数据写入
}
});
tweakResultModal();
tweakSpinBtn();
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 = () => {
// 简单模拟 UTC+8 日期字符串
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() }
}, location.origin);
}).catch(() => {});
// 修改显示配额 (UI效果)
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')) {
let realData = null;
try { realData = await res.clone().json(); } catch (_) {}
if (isTestMode && !realData?.data?.cdk) {
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;
};
const script = document.createElement('script');
script.textContent = `(${interceptorFn.toString()})('${CONFIG.events.canSpin}', ${IS_TEST_MODE});`;
// documentElement 即使在 document-start 时也存在
(document.head || document.documentElement).appendChild(script);
script.remove();
}
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.documentElement, { childList: true, subtree: true, attributes: true });
window.addEventListener('beforeunload', () => observer.disconnect(), { 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;
});
};
const observer = new MutationObserver(() => {
const btn = document.querySelector('#resultModal .close-button');
if (btn && !btn.dataset.patched) patchButton(btn);
});
observer.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function observeCdk() {
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 });
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}).catch(() => {});
}
/**
* ==========================================================================
* 模块:充值页(x666.me)
* ==========================================================================
*/
async function initX666() {
const path = location.pathname;
// 1. 登录回跳检测
GM_addValueChangeListener(CONFIG.storage.loginSuccess, (_, __, val) => {
if (val && (Date.now() - val < 10000)) {
handleRedirectToTopup();
}
});
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;
};
if (!checkAndClose()) setInterval(checkAndClose, 500);
}
if (path.includes('/console/topup')) {
injectTopupInterceptor();
// 确保 DOM 准备好后再填充
await UTILS.domReady();
autofill();
return;
}
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点' }
}, location.origin);
};
const shouldNotify = (data, status) => {
if (status && status >= 400) return false;
if (data && typeof data === 'object' && data.success === false) return false;
return true;
};
// XHR Interception
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 Interception
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)) notify();
} catch (_) {}
}
return res;
});
}
});
window.fetch = wrappedFetch;
window[PATCH_KEY] = true;
};
const script = document.createElement('script');
script.textContent = `(${interceptorFn.toString()})('${CONFIG.events.topupDone}');`;
(document.head || document.documentElement).appendChild(script);
script.remove();
Bridge.on(CONFIG.events.topupDone, (payload) => {
GM_setValue(CONFIG.storage.topupFinish, { time: UTILS.now(), message: payload.message });
GM_setValue(CONFIG.storage.latestCdk, '');
setTimeout(() => window.close(), 800);
});
}
async function autofill() {
const params = new URLSearchParams(location.search);
const cdk = params.get('cdk') || GM_getValue(CONFIG.storage.latestCdk, '');
if (!cdk) return;
try {
const input = await UTILS.waitFor('#redemptionCode');
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 }));
// 查找按钮:优先兄弟节点,其次父级兄弟节点
let btn = null;
try {
// 尝试在父容器附近寻找按钮,增加容错
const parent = input.parentElement?.parentElement || document.body;
btn = await UTILS.waitFor('.semi-button-primary', parent, 3000);
} catch (e) {
console.warn('[Bohe] Button lookup timeout');
}
if (btn) {
// 稍微延迟一下,显得更自然,也给 React 状态更新留出时间
setTimeout(() => btn.click(), 300);
}
} catch (e) {
console.error('[Bohe] Autofill failed', e);
}
}
/**
* ==========================================================================
* 共享特效
* ==========================================================================
*/
async function triggerFireworks(text) {
if (typeof confetti === 'undefined') return;
// UI 必须就绪
if (!ui) {
await UTILS.domReady();
if (!ui) ui = new BoheUI();
}
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);
})();
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) {
console.error('[Bohe] Init error:', err);
}
})();