// ==UserScript==
// @name 百度贴吧一键签到
// @namespace https://blog.qitongtingyu.online/
// @version 1.5
// @description 一键签到所有关注的贴吧,按钮实时显示已签到/未签到数量
// @author 原作:栖桐听雨;
// @icon https://tb3.bdstatic.com/public/icon/favicon-v2.ico
// @match *://tieba.baidu.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect tieba.baidu.com
// @connect c.tieba.baidu.com
// @require https://unpkg.com/crypto-js@4.1.1/core.js
// @require https://unpkg.com/crypto-js@4.1.1/md5.js
// @license MIT
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const colors = {
primary: '#a18276', primaryDark: '#8a6f64', primaryLight: '#b89a8f',
accent: '#f4b886', bgPrimary: '#fefdfb', bgHighlight: 'rgba(244, 184, 134, 0.3)',
textPrimary: '#5c4a42', textWhite: '#ffffff', border: 'rgba(92, 74, 66, 0.12)',
success: '#7a9e7e', error: '#c97b7b', info: '#a18276',
shadowPrimary: 'rgba(161, 130, 118, 0.35)', shadowPrimaryHover: 'rgba(161, 130, 118, 0.45)',
shadowPrimaryActive: 'rgba(161, 130, 118, 0.3)'
};
const styles = `
body > #tiebaAutoSignBtn {
all: initial;
}
body > #tiebaAutoSignBtn {
position: fixed;
bottom: 5px;
right: 5px;
z-index: 99999;
padding: 1px 1px;
background: linear-gradient(135deg, ${colors.primary} 0%, ${colors.primaryDark} 100%);
color: ${colors.textWhite};
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
letter-spacing: 0.5px;
box-shadow: 0 6px 2px ${colors.shadowPrimary};
transition: transform 0.15s cubic-bezier(0.2, 0.8, 0.2, 1),
box-shadow 0.15s cubic-bezier(0.2, 0.8, 0.2, 1),
background 0.2s ease;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
outline: none;
display: block;
width: 120px;
text-align: center;
line-height: 1.5;
}
body > #tiebaAutoSignBtn:hover {
background: linear-gradient(135deg, ${colors.primaryLight} 0%, ${colors.primary} 100%);
box-shadow: 0 8px 28px ${colors.shadowPrimaryHover};
transform: translateY(-2px);
}
body > #tiebaAutoSignBtn:active {
transform: translateY(0) scale(0.98);
box-shadow: 0 4px 12px ${colors.shadowPrimaryActive};
}
body > #tiebaAutoSignBtn:disabled {
background: linear-gradient(135deg, #a89a90 0%, #8a7268 100%);
cursor: not-allowed;
transform: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
body > #tiebaAutoSignBtn .sign-count {
font-size: 13px;
opacity: 0.9;
margin-top: 2px;
display: block;
}
@media (max-width: 480px) {
body > #tiebaAutoSignBtn {
padding: 12px 20px;
font-size: 14px;
bottom: 16px;
right: 16px;
border-radius: 24px;
min-width: 140px;
}
body > #tiebaAutoSignBtn .sign-count {
font-size: 12px;
}
}
@media (orientation: landscape) and (max-height: 500px) {
body > #tiebaAutoSignBtn {
bottom: 12px;
padding: 10px 18px;
font-size: 13px;
min-width: 140px;
}
}
`;
const CACHE_KEY = 'tieba_sign_cache_v2';
const FAKE_VERSION = '9.7.8.0';
function makeFakeParams(obj) {
return Object.assign({
_client_type: 4,
_client_version: FAKE_VERSION,
_phone_imei: '0'.repeat(15),
model: 'HUAWEI P40',
net_type: 1,
stErrorNums: 1,
stMethod: 1,
stMode: 1,
stSize: 320,
stTime: 117,
stTimesNum: 1,
timestamp: Date.now()
}, obj);
}
function sign(payload) {
const sortKeys = Object.keys(payload).sort();
let str = sortKeys.reduce((acc, key) => acc += `${key}=${payload[key]}`, '');
str += 'tiebaclient!!!';
return CryptoJS.MD5(str).toString();
}
function getTodayCache() {
const today = new Date();
const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const cache = JSON.parse(GM_getValue(CACHE_KEY, '{}'));
if (cache.date !== dateStr) {
GM_setValue(CACHE_KEY, JSON.stringify({ date: dateStr, signed: {} }));
return new Map();
}
return new Map(Object.entries(cache.signed || {}));
}
function saveToCache(forumName) {
const today = new Date();
const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const cache = JSON.parse(GM_getValue(CACHE_KEY, '{}'));
if (cache.date !== dateStr) {
cache.date = dateStr;
cache.signed = {};
}
cache.signed[forumName] = true;
GM_setValue(CACHE_KEY, JSON.stringify(cache));
}
function createStyles() {
const style = document.createElement('style');
style.textContent = styles;
document.head.appendChild(style);
}
function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768;
}
function showErrorPrompt(message) {
const defaultTieba = 'redstone_machinery_communication';
const input = prompt(`${message}\n\n已为你填充默认贴吧:${defaultTieba}\n如需添加其他贴吧,请用英文逗号分隔`, defaultTieba);
return input ? input.split(',').map(name => name.trim()).filter(name => name).map(name => ({ forum_name: name, is_sign: 0 })) : [];
}
async function getFollowedTiebaWithSignStatus() {
const forumMap = new Map();
const addForums = (forums) => {
forums.forEach(f => {
const name = f.forum_name?.trim() || f.name?.trim();
const id = f.forum_id?.toString() || f.id?.toString();
const isSign = f.is_sign === 1 || f.is_sign === '1' || f.is_sign === true ? 1 : 0;
if (name) {
const key = id || name;
const existing = forumMap.get(key);
if (existing) {
if (isSign) existing.is_sign = 1;
} else {
forumMap.set(key, { forum_name: name, is_sign: isSign });
}
}
});
};
const fromNewmoindex = async () => {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://tieba.baidu.com/mo/q/newmoindex',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': document.cookie,
'User-Agent': navigator.userAgent
},
timeout: 20000,
onload: r => {
try {
const data = JSON.parse(r.responseText || '{}');
if (data.data?.like_forum && Array.isArray(data.data.like_forum)) {
addForums(data.data.like_forum);
}
} catch { }
resolve();
},
onerror: () => resolve(),
ontimeout: () => resolve()
});
});
};
const fromAppApi = async () => {
const appCommonHeader = {
'User-agent': `bdtb for Android ${FAKE_VERSION}`,
Accept: '',
'Content-Type': 'application/x-www-form-urlencoded'
};
let page = 1;
let hasMore = true;
while (hasMore && page <= 50) {
const baseParams = makeFakeParams({
BDUSS: document.cookie.match(/BDUSS=([^;]+)/)?.[1] || '',
page_no: page,
page_size: 200
});
const params = { ...baseParams, sign: sign(baseParams) };
const dataStr = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&');
const response = await new Promise(resolve => {
GM_xmlhttpRequest({
method: 'POST',
url: 'https://c.tieba.baidu.com/c/f/forum/like',
headers: { ...appCommonHeader, 'Cookie': document.cookie },
data: dataStr,
timeout: 20000,
onload: r => {
try {
resolve(JSON.parse(r.responseText || '{}'));
} catch {
resolve({});
}
},
onerror: () => resolve({}),
ontimeout: () => resolve({})
});
});
if (response.forum_list) {
const forums = [];
if (response.forum_list.non_gconforum) {
const list = Array.isArray(response.forum_list.non_gconforum) ? response.forum_list.non_gconforum : [];
forums.push(...list);
}
if (response.forum_list.gconforum) {
const list = Array.isArray(response.forum_list.gconforum) ? response.forum_list.gconforum : [];
forums.push(...list);
}
if (Array.isArray(response.forum_list)) {
forums.push(...response.forum_list);
}
addForums(forums);
}
hasMore = response.has_more === '1' || response.has_more === 1;
page++;
await new Promise(r => setTimeout(r, 200));
}
};
await fromNewmoindex();
await fromAppApi();
const tiebaList = [...forumMap.values()];
return tiebaList.length > 0 ? tiebaList : showErrorPrompt('自动提取贴吧列表失败');
}
async function getTBS() {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://tieba.baidu.com/dc/common/tbs',
headers: { 'Cookie': document.cookie },
timeout: 10000,
onload: r => {
try { resolve(JSON.parse(r.responseText).tbs || ''); }
catch { resolve(''); }
},
onerror: () => resolve('')
});
});
}
async function signSingleTieba(name) {
return new Promise(async resolve => {
const tbs = await getTBS();
const data = `ie=utf-8&kw=${encodeURIComponent(name)}${tbs ? `&tbs=${tbs}` : ''}`;
GM_xmlhttpRequest({
method: 'POST',
url: 'https://tieba.baidu.com/sign/add',
data: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Referer': `https://tieba.baidu.com/f?kw=${encodeURIComponent(name)}`,
'Cookie': document.cookie
},
timeout: 15000,
onload: r => {
try {
const res = JSON.parse(r.responseText);
switch (res.no) {
case 0: resolve({ status: 'success', msg: `${name}:签到成功\n连续签到:${res.data?.uinfo?.cont_sign_num || 0}天 | 总签到:${res.data?.uinfo?.total_sign_num || 0}次\n本吧排名:${res.data?.finfo?.current_rank_info?.sign_count || 0}` }); break;
case 1101: resolve({ status: 'existed', msg: `${name}:今日已签到` }); break;
case 100002: resolve({ status: 'fail', msg: `${name}:未登录/登录过期` }); break;
case 34004: resolve({ status: 'vcode', msg: `${name}:失败(需要验证码)` }); break;
default: {
const errorMsg = res.error || '未知错误';
if (errorMsg.includes('vcode')) {
resolve({ status: 'vcode', msg: `${name}:失败(need vcode)` });
} else {
resolve({ status: 'fail', msg: `${name}:失败(${errorMsg})` });
}
} break;
}
} catch { resolve({ status: 'fail', msg: `${name}:解析失败` }); }
},
onerror: () => resolve({ status: 'fail', msg: `${name}:网络错误` }),
ontimeout: () => resolve({ status: 'fail', msg: `${name}:请求超时` })
});
});
}
let currentStats = { signed: 0, unsigned: 0, total: 0 };
let isSigning = false;
let refreshTimer = null;
async function updateButtonStats(force = false) {
if (isSigning && !force) return;
try {
const list = await getFollowedTiebaWithSignStatus();
const signed = list.filter(t => t.is_sign === 1).length;
const unsigned = list.filter(t => t.is_sign !== 1).length;
currentStats = { signed, unsigned, total: list.length };
updateButtonText();
} catch { }
}
function updateButtonText() {
const btn = document.getElementById('tiebaAutoSignBtn');
if (!btn || isSigning) return;
btn.innerHTML = `贴吧一键签到已签到 ${currentStats.signed} 未签到 ${currentStats.unsigned}`;
}
async function autoSign() {
isSigning = true;
const btn = document.getElementById('tiebaAutoSignBtn');
if (btn) {
btn.disabled = true;
btn.innerHTML = `贴吧一键签到获取列表中...`;
}
try {
const tiebaList = await getFollowedTiebaWithSignStatus();
if (tiebaList.length === 0) {
if (btn) btn.innerHTML = `贴吧一键签到未获取到贴吧`;
setTimeout(() => { isSigning = false; updateButtonStats(true); }, 2000);
return;
}
const cache = getTodayCache();
const toSign = [];
const skipped = [];
const failedList = [];
const vcodeList = [];
for (const t of tiebaList) {
if (cache.has(t.forum_name) || t.is_sign === 1) {
skipped.push({ status: 'existed', msg: `${t.forum_name}:今日已签到` });
} else {
toSign.push(t);
}
}
let [success, existed, fail, contSign] = [0, skipped.length, 0, 0];
if (toSign.length === 0) {
if (btn) btn.innerHTML = `贴吧一键签到全部已签到`;
setTimeout(() => { isSigning = false; updateButtonStats(true); }, 2000);
return;
}
const baseDelay = 300;
const batchSize = 5;
const restInterval = 100;
for (let i = 0; i < toSign.length; i++) {
if (btn) {
btn.innerHTML = `贴吧一键签到签到中 ${i + 1}/${toSign.length}`;
}
const res = await signSingleTieba(toSign[i].forum_name);
if (res.status === 'success') {
success++;
saveToCache(toSign[i].forum_name);
const match = res.msg.match(/连续签到:(\d+)天/);
if (match) contSign += parseInt(match[1]);
} else if (res.status === 'existed') {
existed++;
saveToCache(toSign[i].forum_name);
} else if (res.status === 'vcode') {
fail++;
vcodeList.push(toSign[i]);
} else {
fail++;
failedList.push(toSign[i]);
}
if (i !== toSign.length - 1) {
const randomOffset = Math.floor(Math.random() * 200) - 100;
await new Promise(r => setTimeout(r, Math.max(0, baseDelay + randomOffset)));
}
if ((i + 1) % batchSize === 0 && i !== toSign.length - 1) {
const batchRandom = Math.floor(Math.random() * 1000);
await new Promise(r => setTimeout(r, 1500 + batchRandom));
}
if ((i + 1) % restInterval === 0 && i !== toSign.length - 1) {
const restRandom = Math.floor(Math.random() * 2000);
if (btn) {
btn.innerHTML = `贴吧一键签到休息中 ${i + 1}/${toSign.length}`;
}
await new Promise(r => setTimeout(r, 4500 + restRandom));
}
}
if (vcodeList.length > 0) {
const maxVcodeRetries = 3;
let currentVcodeList = [...vcodeList];
for (let retryRound = 0; retryRound < maxVcodeRetries && currentVcodeList.length > 0; retryRound++) {
if (btn) {
btn.innerHTML = `贴吧一键签到验证码重试 ${retryRound + 1}/3`;
}
await new Promise(r => setTimeout(r, 10000));
const vcodeBaseDelay = 2000;
const retryResults = [];
for (let j = 0; j < currentVcodeList.length; j++) {
const name = currentVcodeList[j].forum_name;
const res = await signSingleTieba(name);
if (res.status === 'success') {
success++;
fail--;
saveToCache(name);
const match = res.msg.match(/连续签到:(\d+)天/);
if (match) contSign += parseInt(match[1]);
} else if (res.status === 'existed') {
existed++;
fail--;
saveToCache(name);
} else {
retryResults.push(currentVcodeList[j]);
}
if (j !== currentVcodeList.length - 1) {
const randomOffset = Math.floor(Math.random() * 1000);
await new Promise(r => setTimeout(r, vcodeBaseDelay + randomOffset));
}
}
currentVcodeList = retryResults;
}
}
if (failedList.length > 0) {
const maxFailRetries = 3;
let currentFailedList = [...failedList];
for (let retryRound = 0; retryRound < maxFailRetries && currentFailedList.length > 0; retryRound++) {
if (btn) {
btn.innerHTML = `贴吧一键签到失败重试 ${retryRound + 1}/3`;
}
await new Promise(r => setTimeout(r, 5000));
const retryResults = [];
for (let k = 0; k < currentFailedList.length; k++) {
const name = currentFailedList[k].forum_name;
const res = await signSingleTieba(name);
if (res.status === 'success') {
success++;
fail--;
saveToCache(name);
const match = res.msg.match(/连续签到:(\d+)天/);
if (match) contSign += parseInt(match[1]);
} else if (res.status === 'existed') {
existed++;
fail--;
saveToCache(name);
} else {
retryResults.push(currentFailedList[k]);
}
if (k !== currentFailedList.length - 1) {
await new Promise(r => setTimeout(r, 2000 + Math.floor(Math.random() * 1000)));
}
}
currentFailedList = retryResults;
}
}
if (btn) {
btn.innerHTML = `贴吧一键签到完成 成功${success} 失败${fail}`;
}
} catch {
if (btn) {
btn.innerHTML = `贴吧一键签到签到出错`;
}
}
setTimeout(() => {
isSigning = false;
updateButtonStats(true);
}, 3000);
}
function createSignButton() {
if (document.getElementById('tiebaAutoSignBtn')) return;
const btn = document.createElement('button');
btn.id = 'tiebaAutoSignBtn';
btn.innerHTML = `贴吧一键签到加载中...`;
btn.addEventListener('click', async () => {
if (btn.disabled) return;
await autoSign();
});
document.body.appendChild(btn);
}
function init() {
createStyles();
setTimeout(() => {
createSignButton();
updateButtonStats();
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(() => updateButtonStats(), 60000);
}, isMobile() ? 3000 : 2000);
}
document.readyState === 'complete' ? init() : window.addEventListener('load', init);
})();