MaidCheckin Pro

Silent auto check-in for bbs.bt.sb (silent API call, no popups)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         MaidCheckin Pro
// @name:zh-CN   女仆论坛自动签到
// @namespace    http://tampermonkey.net/
// @version      7.2
// @description  Silent auto check-in for bbs.bt.sb (silent API call, no popups)
// @description:zh-CN  自动完成 bbs.bt.sb 每日签到(静默 API 调用,无弹窗)
// @author       mochu
// @match        *://bbs.bt.sb/*
// @icon         https://bbs.bt.sb/uploads/icon/icon-99224e973faa6f7d.png
// @grant        none
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ═══════════════════════════════════════════
    //  配 置 区
    // ═══════════════════════════════════════════
    const CONFIG = {
        apiUrl: 'https://bbs.bt.sb/api/check-in',   // 签到 API 地址
        pollMs: 3000,        // 轮询间隔(毫秒)
        debug: false         // 开启后在 console 输出详细日志
    };

    // ═══════════════════════════════════════════
    //  工 具 函 数
    // ═══════════════════════════════════════════
    const TAG = '[MaidCheckin Pro]';
    const log = (...args) => console.log(TAG, ...args);
    const dbg = (...args) => CONFIG.debug && console.log(TAG, '[DBG]', ...args);

    function todayKey() {
        const d = new Date();
        return `nv_checkin_${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
    }

    function showToast(msg, color = '#10B981') {
        const el = document.createElement('div');
        Object.assign(el.style, {
            position: 'fixed', top: '16px', right: '16px', zIndex: '99999',
            padding: '12px 22px', borderRadius: '10px',
            background: color, color: '#fff',
            fontSize: '14px', fontWeight: '600',
            boxShadow: '0 4px 16px rgba(0,0,0,.18)',
            opacity: '0', transform: 'translateY(-12px)',
            transition: 'all .35s ease', pointerEvents: 'none'
        });
        el.textContent = msg;
        document.body.appendChild(el);
        requestAnimationFrame(() => {
            el.style.opacity = '1';
            el.style.transform = 'translateY(0)';
        });
        setTimeout(() => {
            el.style.opacity = '0';
            el.style.transform = 'translateY(-12px)';
            setTimeout(() => el.remove(), 400);
        }, 3500);
    }

    /** 检测用户是否已登录 */
    function isLoggedIn() {
        const loginBtn = document.querySelector('a[href="/login"]');
        if (loginBtn && loginBtn.offsetParent !== null) {
            return false;
        }
        const avatar = document.querySelector('[class*="avatar"], [class*="Avatar"], img[alt*="avatar"]');
        if (avatar) return true;
        return !loginBtn;
    }

    // ═══════════════════════════════════════════
    //  核 心:直 接 调 API 签 到
    // ═══════════════════════════════════════════
    let isRunning = false;

    async function tryCheckin() {
        if (isRunning) return;
        if (localStorage.getItem(todayKey())) {
            dbg('今日已标记完成,跳过');
            return;
        }
        if (!isLoggedIn()) {
            dbg('未检测到登录状态,跳过');
            return;
        }

        isRunning = true;
        try {
            log('📡 正在调用签到 API...');
            const resp = await fetch(CONFIG.apiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                credentials: 'include',     // 携带 cookie(登录凭证)
                body: JSON.stringify({ action: 'check-in' })
            });

            if (resp.ok) {
                const data = await resp.json().catch(() => ({}));
                const payload = data?.data || data;   // 适配 apiSuccess 包装
                localStorage.setItem(todayKey(), '1');

                if (payload.alreadyCheckedIn) {
                    log('📌 今日已签到');
                    showToast('📌 今日已签到', '#3B82F6');
                } else {
                    const reward = payload.points || payload.reward || '';
                    const streak = payload.currentStreak ? `连续${payload.currentStreak}天` : '';
                    log(`✅ 签到成功!+${reward}`, streak, payload);
                    showToast(`✅ 签到成功${reward ? `(+${reward})` : ''}${streak ? ` ${streak}` : ''}`, '#10B981');
                }
            } else if (resp.status === 401 || resp.status === 403) {
                dbg('未登录或无权限,等待下次重试');
            } else {
                log('⚠️ 签到请求异常:', resp.status, resp.statusText);
            }
        } catch (err) {
            log('❌ 签到请求失败:', err.message);
        } finally {
            isRunning = false;
        }
    }

    // ═══════════════════════════════════════════
    //  SPA 路 由 监 听 + 轮 询
    // ═══════════════════════════════════════════
    function startWatcher() {
        // 启动后延迟 2 秒首次尝试
        setTimeout(tryCheckin, 2000);

        // 持续轮询直到当日签到成功
        const watcher = setInterval(() => {
            if (localStorage.getItem(todayKey())) {
                dbg('今日签到已完成,停止轮询');
                clearInterval(watcher);
                return;
            }
            tryCheckin();
        }, CONFIG.pollMs);

        // 监听 SPA 路由变化(Next.js pushState)
        let lastUrl = location.href;
        const urlObserver = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                dbg('SPA 路由跳转:', lastUrl);
                setTimeout(tryCheckin, 1500);
            }
        });
        urlObserver.observe(document.body, { childList: true, subtree: true });

        // 监听浏览器前进/后退
        window.addEventListener('popstate', () => {
            dbg('popstate 导航');
            setTimeout(tryCheckin, 1500);
        });
    }

    // ═══════════════════════════════════════════
    //  入 口
    // ═══════════════════════════════════════════
    if (document.readyState === 'complete') {
        startWatcher();
    } else {
        window.addEventListener('load', startWatcher);
    }
})();