Greasy Fork

来自缓存

Greasy Fork is available in English.

百度贴吧自动签到助手

🚀 百度贴吧自动签到脚本:基于移动端接口,支持一键签到 + 智能补签。内置现代化暗黑风 UI 面板,可视化日志,安全稳定。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         百度贴吧自动签到助手
// @namespace    https://aiotieba.cc
// @version      0.1.0
// @description  🚀 百度贴吧自动签到脚本:基于移动端接口,支持一键签到 + 智能补签。内置现代化暗黑风 UI 面板,可视化日志,安全稳定。
// @match        https://tieba.baidu.com/*
// @match        https://tiebac.baidu.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_log
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @connect      tieba.baidu.com
// @connect      tiebac.baidu.com
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(() => {
    "use strict";

    const APP_HOST = "https://tiebac.baidu.com";
    const WEB_HOST = "https://tieba.baidu.com";
    const CLIENT_VERSION = "12.64.1.1";
    const SIGN_SUFFIX = "tiebaclient!!!";
    const STORAGE_KEY = "tieba_autosign_bduss";
    const MAX_RN = 200;
    const SLEEP_MS = 600;

    const state = {
        running: false,
        logBuffer: [],
        dom: {},
    };

    function setStatus(text, type = "info") {
        if (state.dom.status) {
            state.dom.status.textContent = text;
            state.dom.status.dataset.type = type;
        }
    }

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

    function toUtf8Bytes(str) {
        if (typeof TextEncoder !== "undefined") {
            return new TextEncoder().encode(str);
        }
        const utf8 = unescape(encodeURIComponent(str));
        const arr = new Uint8Array(utf8.length);
        for (let i = 0; i < utf8.length; i++) arr[i] = utf8.charCodeAt(i);
        return arr;
    }

    function leftRotate(x, c) {
        return (x << c) | (x >>> (32 - c));
    }

    function md5(input) {
        const bytes = toUtf8Bytes(input);
        const originalBitLength = bytes.length * 8;
        const withPadding = new Uint8Array(((bytes.length + 9 + 63) >> 6) << 6);
        withPadding.set(bytes);
        withPadding[bytes.length] = 0x80;
        const dv = new DataView(withPadding.buffer);
        dv.setUint32(withPadding.length - 8, originalBitLength, true);

        let a = 0x67452301;
        let b = 0xefcdab89;
        let c = 0x98badcfe;
        let d = 0x10325476;

        const k = new Uint32Array(64);
        for (let i = 0; i < 64; i++) {
            k[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32);
        }

        const r = [
            7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
            5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
            4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
            6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
        ];

        for (let offset = 0; offset < withPadding.length; offset += 64) {
            const chunk = new Uint32Array(16);
            for (let i = 0; i < 16; i++) {
                chunk[i] = dv.getUint32(offset + i * 4, true);
            }

            let A = a;
            let B = b;
            let C = c;
            let D = d;

            for (let i = 0; i < 64; i++) {
                let F, g;
                if (i < 16) {
                    F = (B & C) | (~B & D);
                    g = i;
                } else if (i < 32) {
                    F = (D & B) | (~D & C);
                    g = (5 * i + 1) % 16;
                } else if (i < 48) {
                    F = B ^ C ^ D;
                    g = (3 * i + 5) % 16;
                } else {
                    F = C ^ (B | ~D);
                    g = (7 * i) % 16;
                }
                const tmp = D;
                D = C;
                C = B;
                const sum = (A + F + k[i] + chunk[g]) >>> 0;
                B = (B + leftRotate(sum, r[i])) >>> 0;
                A = tmp;
            }

            a = (a + A) >>> 0;
            b = (b + B) >>> 0;
            c = (c + C) >>> 0;
            d = (d + D) >>> 0;
        }

        const digest = new Uint8Array(16);
        const words = [a, b, c, d];
        for (let i = 0; i < 4; i++) {
            const w = words[i];
            digest[i * 4] = w & 0xff;
            digest[i * 4 + 1] = (w >>> 8) & 0xff;
            digest[i * 4 + 2] = (w >>> 16) & 0xff;
            digest[i * 4 + 3] = (w >>> 24) & 0xff;
        }
        return Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
    }

    function signParams(pairs) {
        const body = pairs.map(([k, v]) => `${k}=${v}`).join("") + SIGN_SUFFIX;
        return md5(body);
    }

    function gmRequest(method, url, data, { headers = {}, cookie = "" } = {}) {
        return new Promise((resolve, reject) => {
            const opts = {
                method,
                url,
                data,
                headers: {
                    Accept: "application/json, text/plain, */*",
                    ...headers,
                },
                onload: (resp) => resolve(resp),
                onerror: () => reject(new Error("Network error")),
                ontimeout: () => reject(new Error("Request timeout")),
                timeout: 15000,
            };
            if (cookie) {
                opts.headers.Cookie = cookie;
            }
            GM_xmlhttpRequest(opts);
        });
    }

    function truncate(str, max = 500) {
        if (typeof str !== "string") return "";
        return str.length > max ? `${str.slice(0, max)}...(${str.length} chars)` : str;
    }

    function parseJsonSafe(resp, ctx) {
        try {
            return JSON.parse(resp.responseText);
        } catch (e) {
            throw new Error(`${ctx}:返回内容非 JSON,status=${resp.status},片段=${truncate(resp.responseText)}`);
        }
    }

    function log(...args) {
        const msg = args.map(String).join(" ");
        state.logBuffer.push(msg);
        if (state.logBuffer.length > 500) state.logBuffer.shift();
        GM_log(msg);
        if (state.dom.logArea) {
            state.dom.logArea.value = state.logBuffer.join("\n");
            state.dom.logArea.scrollTop = state.dom.logArea.scrollHeight;
        }
    }

    async function getTbs(bduss) {
        log("获取 tbs...");
        const resp = await gmRequest("GET", `${WEB_HOST}/dc/common/tbs`, null, {
            cookie: `BDUSS=${bduss};`,
        });
        const data = parseJsonSafe(resp, "获取 tbs");
        if (!data || data.is_login !== 1 || !data.tbs) {
            throw new Error("获取 tbs 失败,可能 BDUSS 无效或未登录。");
        }
        log("tbs 获取成功");
        return data.tbs;
    }

    async function multiSign(bduss) {
        log("尝试网页一键签到...");
        const form = new URLSearchParams();
        form.append("_client_version", CLIENT_VERSION);
        form.append("subapp_type", "hybrid");
        const resp = await gmRequest("POST", `${WEB_HOST}/c/c/forum/msign`, form.toString(), {
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                "Subapp-Type": "hybrid",
            },
            cookie: `BDUSS=${bduss};`,
        });
        log(`一键签到响应状态:${resp.status}, length=${resp.responseText?.length ?? 0}`);
        const data = parseJsonSafe(resp, "一键签到");
        if (Number(data.error_code || 0) !== 0) {
            throw new Error(`一键签到失败:${data.error_msg || resp.status},raw=${truncate(resp.responseText)}`);
        }
        if (Number(data.error?.errno || 0) !== 0) {
            const errno = Number(data.error.errno);
            const msg = data.error.errmsg || data.error.usermsg || "未知错误";
            // 340011: 签得太快;340016: 频控
            if (errno === 340011 || errno === 340016) {
                log(`一键签到被限速(errno=${errno}, msg=${msg}),将跳过一键改为逐吧签到`);
                return { ok: false, errno, msg };
            }
            throw new Error(`一键签到失败:${msg} (errno=${errno}),raw=${truncate(resp.responseText)}`);
        }
        log("网页一键签到完成");
        return { ok: true };
    }

    async function fetchFollowForums(bduss, tbs) {
        const unsigned = [];
        for (let page = 1; page < 9999; page++) {
            const form = new URLSearchParams();
            form.append("tbs", tbs);
            form.append("sort_type", "3");
            form.append("call_from", "3");
            form.append("page_no", String(page));
            form.append("res_num", String(MAX_RN));
            const resp = await gmRequest("POST", `${WEB_HOST}/c/f/forum/forumGuide`, form.toString(), {
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Subapp-Type": "hybrid",
                },
                cookie: `BDUSS=${bduss};`,
            });
            const data = parseJsonSafe(resp, "获取关注列表");
            if (data.error_code && Number(data.error_code) !== 0) {
                throw new Error(`获取关注列表失败:${data.error_msg || data.error_code},raw=${truncate(resp.responseText)}`);
            }
            const forums = data.like_forum || [];
            for (const forum of forums) {
                if (!forum.is_sign) {
                    unsigned.push(forum.forum_name);
                }
            }
            const hasMore = data.like_forum_has_more;
            log(`关注列表第 ${page} 页,新增未签 ${forums.filter((f) => !f.is_sign).length}`);
            if (!hasMore) break;
            await sleep(200);
        }
        log(`共发现未签到贴吧 ${unsigned.length} 个`);
        return unsigned;
    }

    async function signOne(bduss, tbs, fname) {
        const pairs = [
            ["BDUSS", bduss],
            ["_client_version", CLIENT_VERSION],
            ["kw", fname],
            ["tbs", tbs],
        ];
        const sign = signParams(pairs);
        const form = new URLSearchParams();
        for (const [k, v] of pairs) form.append(k, v);
        form.append("sign", sign);
        const resp = await gmRequest("POST", `${APP_HOST}/c/c/forum/sign`, form.toString(), {
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
        });
        const data = parseJsonSafe(resp, `签到 ${fname}`);
        if (Number(data.error_code || 0) !== 0) {
            const err = data.error_msg || data.error_code;
            throw new Error(`${err},raw=${truncate(resp.responseText)}`);
        }
        if (Number(data.user_info?.sign_bonus_point || 0) === 0) {
            throw new Error("sign_bonus_point 为 0(重复签到或频率限制)");
        }
    }

    async function run() {
        if (state.running) return;
        const bduss = state.dom.bdussInput.value.trim();
        if (!bduss) {
            log("缺少 BDUSS,请先填写后点击保存");
            setStatus("请填写 BDUSS", "warn");
            return;
        }
        GM_setValue(STORAGE_KEY, bduss);
        state.running = true;
        state.dom.startBtn.disabled = true;
        state.dom.saveBtn.disabled = true;
        state.logBuffer.length = 0;
        log("开始签到流程");
        setStatus("运行中...", "info");
        try {
            const tbs = await getTbs(bduss);
            const msignRes = await multiSign(bduss);
            const unsigned = await fetchFollowForums(bduss, tbs);
            let success = 0;
            let skipped = 0;
            let failed = 0;
            for (const fname of unsigned) {
                await sleep(SLEEP_MS);
                try {
                    await signOne(bduss, tbs, fname);
                    success++;
                    log(`✓ 签到成功:${fname}`);
                } catch (err) {
                    const msg = String(err?.message || err);
                    if (msg.includes("160002") || msg.includes("重复") || msg.includes("sign_bonus_point 为 0")) {
                        skipped++;
                        log(`⟳ 已签到 / 频控:${fname} -> ${msg}`);
                    } else {
                        failed++;
                        log(`✗ 失败:${fname} -> ${msg}`);
                    }
                }
            }
            log(`完成。成功 ${success},已签/跳过 ${skipped},失败 ${failed}`);
            setStatus(`完成:成功${success} 跳过${skipped} 失败${failed}`, failed === 0 ? "success" : "warn");
        } catch (err) {
            log("流程中断:", err?.message || err);
            setStatus(`失败:${err?.message || err}`, "error");
        } finally {
            state.running = false;
            state.dom.startBtn.disabled = false;
            state.dom.saveBtn.disabled = false;
        }
    }

    function togglePanel() {
        const panel = state.dom.panel;
        if (!panel) return;
        const hidden = panel.dataset.hidden === "1";
        panel.dataset.hidden = hidden ? "0" : "1";
        panel.style.transform = "translateY(0)";
    }

    function buildUI() {
        const panel = document.createElement("div");
        panel.id = "aiotieba-autosign-panel";
        panel.dataset.hidden = "1";
        panel.tabIndex = 0;
        panel.innerHTML = `
            <div class="aia-header">
                <span class="aia-title">贴吧自动签到</span>
                <div class="aia-actions">
                    <button type="button" id="aia-save">保存</button>
                    <button type="button" id="aia-start">开始</button>
                    <button type="button" id="aia-toggle">折叠</button>
                </div>
            </div>
            <div class="aia-body">
                <div class="aia-status" id="aia-status" data-type="idle">待机</div>
                <label class="aia-row">
                    <span>BDUSS</span>
                    <input id="aia-bduss" type="password" placeholder="粘贴 BDUSS" autocomplete="off" />
                </label>
                <textarea id="aia-log" readonly placeholder="日志输出..."></textarea>
            </div>
            <div class="aia-dot" title="点击展开签到面板"></div>
        `;

        const style = `
            #aiotieba-autosign-panel {
                position: fixed;
                right: 12px;
                bottom: 16px;
                width: min(360px, 90vw);
                background: #0b172a;
                color: #e8f0ff;
                border-radius: 12px;
                box-shadow: 0 10px 30px rgba(0,0,0,0.35);
                font-family: "Segoe UI", "PingFang SC", sans-serif;
                font-size: 13px;
                z-index: 2147483647;
                overflow: hidden;
                transition: transform 0.28s ease;
            }
            #aiotieba-autosign-panel[data-hidden="1"] {
                width: 36px;
                height: 36px;
                padding: 0;
                border-radius: 50%;
                background: #1b3566;
                box-shadow: 0 8px 24px rgba(0,0,0,0.35);
            }
            #aiotieba-autosign-panel[data-hidden="1"] .aia-header,
            #aiotieba-autosign-panel[data-hidden="1"] .aia-body {
                display: none;
            }
            #aiotieba-autosign-panel .aia-dot {
                display: none;
                width: 18px;
                height: 18px;
                border-radius: 50%;
                background: #1f6feb;
                margin: 9px;
                box-shadow: 0 0 0 3px rgba(31,111,235,0.25), 0 4px 10px rgba(0,0,0,0.35);
                cursor: pointer;
            }
            #aiotieba-autosign-panel[data-hidden="1"] .aia-dot {
                display: block;
            }
            #aiotieba-autosign-panel .aia-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 8px 10px;
                background: linear-gradient(135deg, #1b3566, #122441);
            }
            #aiotieba-autosign-panel .aia-title {
                font-weight: 700;
                letter-spacing: 0.3px;
            }
            #aiotieba-autosign-panel .aia-actions button {
                margin-left: 6px;
                background: #1f6feb;
                color: #fff;
                border: none;
                border-radius: 6px;
                padding: 6px 10px;
                cursor: pointer;
            }
            #aiotieba-autosign-panel .aia-actions button:disabled {
                opacity: 0.5;
                cursor: not-allowed;
            }
            #aiotieba-autosign-panel .aia-body {
                padding: 10px;
            }
            #aiotieba-autosign-panel .aia-row {
                display: flex;
                align-items: center;
                gap: 8px;
                margin-bottom: 8px;
            }
            #aiotieba-autosign-panel .aia-status {
                display: inline-block;
                margin-bottom: 6px;
                padding: 4px 8px;
                border-radius: 8px;
                font-size: 12px;
                background: #203659;
                color: #dbe7ff;
            }
            #aiotieba-autosign-panel .aia-status[data-type="success"] { background: #1d6337; }
            #aiotieba-autosign-panel .aia-status[data-type="warn"] { background: #7a4b0f; }
            #aiotieba-autosign-panel .aia-status[data-type="error"] { background: #7a1b1b; }
            #aiotieba-autosign-panel input#aia-bduss {
                flex: 1;
                padding: 6px 8px;
                border: 1px solid #2b3f66;
                border-radius: 6px;
                background: #0f1f3b;
                color: #e8f0ff;
            }
            #aiotieba-autosign-panel textarea#aia-log {
                width: 100%;
                height: 180px;
                resize: vertical;
                border: 1px solid #2b3f66;
                border-radius: 8px;
                background: #0f1f3b;
                color: #dbe7ff;
                padding: 8px;
                box-sizing: border-box;
            }
            #aiotieba-autosign-panel button#aia-toggle {
                background: #25395e;
            }
        `;
        GM_addStyle(style);

        document.body.appendChild(panel);

        state.dom = {
            panel,
            bdussInput: panel.querySelector("#aia-bduss"),
            logArea: panel.querySelector("#aia-log"),
            saveBtn: panel.querySelector("#aia-save"),
            startBtn: panel.querySelector("#aia-start"),
            toggleBtn: panel.querySelector("#aia-toggle"),
            dot: panel.querySelector(".aia-dot"),
            status: panel.querySelector("#aia-status"),
        };

        const saved = GM_getValue(STORAGE_KEY, "");
        if (saved) {
            state.dom.bdussInput.value = saved;
            log("已加载保存的 BDUSS");
        }
        setStatus("待机", "idle");

        state.dom.saveBtn.addEventListener("click", () => {
            const val = state.dom.bdussInput.value.trim();
            GM_setValue(STORAGE_KEY, val);
            log("BDUSS 已保存");
        });
        state.dom.startBtn.addEventListener("click", () => run());
        state.dom.toggleBtn.addEventListener("click", togglePanel);
        state.dom.dot.addEventListener("click", togglePanel);
        panel.addEventListener("keydown", (e) => {
            if (e.key === "Enter" || e.key === " ") {
                e.preventDefault();
                togglePanel();
            }
        });
    }

    function registerMenu() {
        GM_registerMenuCommand("打开/折叠签到面板", togglePanel);
        GM_registerMenuCommand("开始签到", run);
    }

    buildUI();
    registerMenu();
})();