// ==UserScript== // @name 贴吧一键签到R+ // @namespace https://blog.qitongtingyu.online/ // @version 1.0 // @description 一键签到所有关注的贴吧,按钮实时显示已签到/未签到数量,支持悬浮菜单(设置/贴吧/版本);列表三源并行抓取+详情缓存加速,显示等级与经验,吧名可点击在新标签页打开 // @author Zcy1340,原作者:栖桐听雨 // @match *://tieba.baidu.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @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 SCRIPT_VERSION = '1.0'; 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)' }; /* ===== HTML 转义,防止吧名含特殊字符时破坏弹窗 DOM ===== */ function escapeHTML(str) { return String(str).replace(/[&<>"']/g, s => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[s])); } 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; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; } 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; } } /* ===== 悬浮按钮面板 ===== */ body > #tiebaAutoSignPopover { all: initial; position: fixed; z-index: 99998; display: flex; flex-direction: column; gap: 6px; opacity: 0; visibility: hidden; pointer-events: none; /* 隐藏时禁止点击 */ transform: translateY(8px); transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; } body > #tiebaAutoSignPopover.visible { opacity: 1; visibility: visible; pointer-events: auto; /* 显示时允许点击 */ transform: translateY(0); } #tiebaAutoSignPopover .pop-btn { all: initial; display: block; width: 120px; box-sizing: border-box; text-align: center; padding: 8px 0; font-size: 14px; letter-spacing: 4px; text-indent: 4px; color: ${colors.textWhite}; background: linear-gradient(135deg, ${colors.primaryLight} 0%, ${colors.primary} 100%); border: none; border-radius: 8px; cursor: pointer; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; box-shadow: 0 4px 12px ${colors.shadowPrimary}; transition: background 0.15s ease, transform 0.1s ease; -webkit-tap-highlight-color: transparent; touch-action: manipulation; } #tiebaAutoSignPopover .pop-btn:hover { background: linear-gradient(135deg, ${colors.primary} 0%, ${colors.primaryDark} 100%); transform: translateY(-1px); } #tiebaAutoSignPopover .pop-btn:active { transform: scale(0.97); } /* ===== 弹窗 ===== */ #tiebaSignModalOverlay { position: fixed; inset: 0; background: rgba(92, 74, 66, 0.35); z-index: 100000; display: flex; align-items: center; justify-content: center; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; } #tiebaSignModal { background: ${colors.bgPrimary}; border-radius: 12px; width: 340px; max-width: calc(100vw - 40px); max-height: 70vh; overflow: auto; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25); padding: 16px 18px; color: ${colors.textPrimary}; box-sizing: border-box; } /* ===== v2.3:宽度自适应内容(贴吧列表用),随最长一行自动加宽 ===== */ #tiebaSignModal.auto-width { width: max-content; min-width: 320px; } #tiebaSignModal .modal-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: 600; margin-bottom: 12px; } #tiebaSignModal .modal-close { border: none; background: transparent; font-size: 16px; cursor: pointer; color: ${colors.textPrimary}; padding: 2px 6px; line-height: 1; } #tiebaSignModal .modal-body { font-size: 14px; line-height: 1.6; } #tiebaSignModal .form-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; gap: 10px; font-size: 14px; } #tiebaSignModal input[type=number] { width: 100px; padding: 5px 8px; border: 1px solid ${colors.border}; border-radius: 6px; font-size: 13px; color: ${colors.textPrimary}; box-sizing: border-box; outline: none; } #tiebaSignModal input[type=number]:focus { border-color: ${colors.primary}; } #tiebaSignModal .modal-btn { width: 100%; padding: 9px 0; border: none; border-radius: 8px; background: linear-gradient(135deg, ${colors.primary} 0%, ${colors.primaryDark} 100%); color: #fff; cursor: pointer; font-size: 14px; letter-spacing: 2px; } #tiebaSignModal .modal-btn:active { opacity: 0.85; } /* ===== v2.3:贴吧列表项——单行布局(吧名 + 等级/经验 + 签到状态),压缩总高度 ===== */ #tiebaSignModal .tieba-item { display: flex; align-items: center; gap: 10px; padding: 4px 8px; border: 1px solid ${colors.border}; border-radius: 4px; margin-bottom: 4px; font-size: 13px; white-space: nowrap; } #tiebaSignModal .tieba-item:last-child { margin-bottom: 0; } /* 吧名超链接:点击在新标签页打开对应贴吧;超宽时省略号截断 */ #tiebaSignModal a.tieba-name-link { font-weight: 500; color: ${colors.primary}; text-decoration: none; cursor: pointer; overflow: hidden; text-overflow: ellipsis; min-width: 3em; } #tiebaSignModal a.tieba-name-link:hover { color: ${colors.primaryDark}; text-decoration: underline; } #tiebaSignModal a.tieba-name-link:active { opacity: 0.75; } #tiebaSignModal .tieba-details { font-size: 12px; color: ${colors.info}; } #tiebaSignModal .tieba-sign { margin-left: auto; } `; const CACHE_KEY = 'tieba_sign_cache_v2'; const SETTINGS_KEY = 'tieba_sign_settings_v1'; /* ===== v2.3 新增:贴吧详情缓存(等级/经验变化缓慢,用于加速列表打开) ===== */ const DETAIL_CACHE_KEY = 'tieba_detail_cache_v1'; const DETAIL_CACHE_TTL = 10 * 60 * 1000; /* 缓存有效期:10 分钟 */ const FAKE_VERSION = '9.7.8.0'; function getSettings() { return Object.assign( { batchSize: 5, baseDelay: 300, restInterval: 100 }, JSON.parse(GM_getValue(SETTINGS_KEY, '{}')) ); } 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)); } /* ===== v2.3 新增:详情缓存读写/失效 ===== */ function getDetailCache() { try { const cache = JSON.parse(GM_getValue(DETAIL_CACHE_KEY, '{}')); if (cache && cache.time && (Date.now() - cache.time) < DETAIL_CACHE_TTL && Array.isArray(cache.list) && cache.list.length > 0) { return cache.list; } } catch (e) { } return null; } function saveDetailCache(list) { try { GM_setValue(DETAIL_CACHE_KEY, JSON.stringify({ time: Date.now(), list })); } catch (e) { } } function clearDetailCache() { try { if (typeof GM_deleteValue === 'function') GM_deleteValue(DETAIL_CACHE_KEY); else GM_setValue(DETAIL_CACHE_KEY, '{}'); } catch (e) { } } 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 })) : []; } /* ===== v2.3:列表获取——三数据源并行 + 详情缓存 + 轻量模式;已移除头衔相关内容,仅提取等级数值与经验 fetchDetail=false 轻量模式(仅取签到状态);silent=true 静默模式(失败不弹输入框,用于后台预热) ===== */ async function getFollowedTiebaWithSignStatus(fetchDetail = true, silent = false) { const forumMap = new Map(); const addForums = (forums) => { forums.forEach(f => { if (!f) return; const name = (f.forum_name ?? f.name ?? '').toString().trim(); if (!name) return; const isSign = (f.is_sign === 1 || f.is_sign === '1' || f.is_sign === true) ? 1 : 0; /* 头衔等级数值(1~18):优先取 level_id,兼容纯数字型 level 字段 */ let levelId = null; const lidRaw = (f.level_id !== undefined && f.level_id !== null && f.level_id !== '') ? f.level_id : null; if (lidRaw !== null) { const s = String(lidRaw).trim(); const n = parseInt(s, 10); if (!isNaN(n) && String(n) === s && n >= 1 && n <= 18) levelId = n; } if (levelId === null && f.level !== undefined && f.level !== null && String(f.level).trim() !== '') { const s = String(f.level).trim(); const n = parseInt(s, 10); if (!isNaN(n) && String(n) === s && n >= 1 && n <= 18) levelId = n; } /* 当前经验:兼容 cur_score / curScore / exp */ let curScore = null; const cs = f.cur_score ?? f.curScore ?? f.exp ?? null; if (cs !== null && cs !== '') { const n = Number(cs); curScore = isNaN(n) ? cs : n; } /* 升级所需经验:兼容 levelup_score / levelupScore / upgrade_score / upgradeScore */ let upgradeScore = null; const us = f.levelup_score ?? f.levelupScore ?? f.upgrade_score ?? f.upgradeScore ?? null; if (us !== null && us !== '') { const n = Number(us); upgradeScore = isNaN(n) ? us : n; } /* 统一以吧名为唯一键:多数据源正确合并,避免同一贴吧重复出现 */ const key = name; const existing = forumMap.get(key); if (existing) { if (isSign) existing.is_sign = 1; /* 命中已存在贴吧时合并补全等级/经验(而非丢弃) */ const e = existing.extra || {}; if (levelId && !e.levelId) e.levelId = levelId; if (curScore && !e.curScore) e.curScore = curScore; if (upgradeScore && !e.upgradeScore) e.upgradeScore = upgradeScore; existing.extra = e; } else { forumMap.set(key, { forum_name: name, is_sign: isSign, extra: { levelId: levelId || 0, curScore: curScore || 0, upgradeScore: upgradeScore || 0 } }); } }); }; /* 数据源1:移动端首页接口(关注列表 + 签到状态,单请求,最快) */ 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: 10000, 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() }); }); }; /* 数据源2:贴吧客户端接口(等级/经验等详细数据) */ 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++; /* v2.3:分页间隔 200ms -> 50ms,降低整体抓取延迟 */ await new Promise(r => setTimeout(r, 50)); } }; /* 数据源3:PC 端"我关注的吧"页面,兜底补全等级数值与经验 */ const fromMylike = async () => { const parsePage = (html) => { const records = []; try { const doc = new DOMParser().parseFromString(html, 'text/html'); doc.querySelectorAll('table tr').forEach(tr => { const cells = tr.querySelectorAll('td'); if (cells.length < 3) return; const link = cells[0].querySelector('a[href]'); if (!link) return; let tName = (link.textContent || '').trim(); if (!tName) { const m = (link.getAttribute('href') || '').match(/[?&]kw=([^&]+)/); if (m) { try { tName = decodeURIComponent(m[1]); } catch (e) { tName = m[1]; } } } if (!tName) return; const levelNum = parseInt((cells[1].textContent || '').replace(/[^\d]/g, ''), 10); const scoreNum = parseInt((cells[2].textContent || '').replace(/[^\d]/g, ''), 10); const rec = { forum_name: tName }; if (!isNaN(levelNum) && levelNum >= 1 && levelNum <= 18) rec.level_id = levelNum; if (!isNaN(scoreNum) && scoreNum > 0) rec.cur_score = scoreNum; if (rec.level_id !== undefined || rec.cur_score !== undefined) records.push(rec); }); } catch (e) { } return records; }; let pn = 1; while (pn <= 100) { const html = await new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: `https://tieba.baidu.com/f/like/mylike?pn=${pn}`, headers: { 'Cookie': document.cookie }, timeout: 15000, onload: r => resolve(r.responseText || ''), onerror: () => resolve(''), ontimeout: () => resolve('') }); }); if (!html) break; const records = parsePage(html); if (!records.length) break; addForums(records); pn++; /* v2.3:分页间隔 300ms -> 100ms */ await new Promise(r => setTimeout(r, 100)); } }; /* ===== 轻量模式:仅取签到状态(悬浮按钮定时刷新用) ===== */ if (!fetchDetail) { await fromNewmoindex(); if (forumMap.size > 0) return [...forumMap.values()]; await fromAppApi(); return [...forumMap.values()]; } /* ===== 完整模式 ===== */ /* 快速路径:详情缓存有效时,仅拉取一次签到状态并与缓存合并,实现秒开 */ const cachedDetails = getDetailCache(); if (cachedDetails && cachedDetails.length > 0) { await fromNewmoindex(); if (forumMap.size === 0) await fromAppApi(); if (forumMap.size > 0) { const detailMap = new Map(cachedDetails.filter(d => d && d.forum_name).map(d => [d.forum_name, d])); return [...forumMap.values()].map(t => { const d = detailMap.get(t.forum_name); const e = t.extra || {}; if (d) { return { forum_name: t.forum_name, is_sign: t.is_sign, extra: { levelId: e.levelId || d.levelId || 0, curScore: e.curScore || d.curScore || 0, upgradeScore: e.upgradeScore || d.upgradeScore || 0 } }; } return t; /* 新关注的贴吧(缓存中无):显示快速源已有数据 */ }); } /* 快速数据源均失败:回退完整抓取 */ } /* 慢速路径:三数据源并行抓取(原先串行延迟=三者之和,现在≈最慢一个) */ await Promise.all([fromNewmoindex(), fromAppApi(), fromMylike()]); const tiebaList = [...forumMap.values()]; if (tiebaList.length > 0) { /* 抓取成功后写入详情缓存,供下次秒开 */ saveDetailCache(tiebaList.map(t => ({ forum_name: t.forum_name, levelId: t.extra?.levelId || 0, curScore: t.extra?.curScore || 0, upgradeScore: t.extra?.upgradeScore || 0 }))); return tiebaList; } return silent ? [] : 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}次` }); 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}:失败` }); } 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 popHoverTimer = null; let popHideTimer = null; let popLeaveTimer = null; function scheduleHidePopover() { clearTimeout(popLeaveTimer); popLeaveTimer = setTimeout(() => { const btn = document.getElementById('tiebaAutoSignBtn'); const pop = document.getElementById('tiebaAutoSignPopover'); const onBtn = btn && (btn.matches(':hover') || document.activeElement === btn); const onPop = pop && (pop.matches(':hover') || document.activeElement === pop); if (!onBtn && !onPop) hidePopover(); }, 300); } function positionPopover() { const btn = document.getElementById('tiebaAutoSignBtn'); const pop = document.getElementById('tiebaAutoSignPopover'); if (!btn || !pop) return; const rect = btn.getBoundingClientRect(); pop.style.right = (window.innerWidth - rect.right) + 'px'; pop.style.bottom = (window.innerHeight - rect.top + 8) + 'px'; } function showPopover() { const pop = document.getElementById('tiebaAutoSignPopover'); if (!pop) return; positionPopover(); pop.classList.add('visible'); clearTimeout(popHideTimer); popHideTimer = setTimeout(hidePopover, 5000); } function hidePopover() { const pop = document.getElementById('tiebaAutoSignPopover'); if (!pop) return; pop.classList.remove('visible'); clearTimeout(popHideTimer); clearTimeout(popLeaveTimer); } function createPopover() { if (document.getElementById('tiebaAutoSignPopover')) return; const pop = document.createElement('div'); pop.id = 'tiebaAutoSignPopover'; pop.innerHTML = ` `; pop.querySelector('[data-action="settings"]').addEventListener('click', () => { hidePopover(); openSettingsModal(); }); pop.querySelector('[data-action="tieba"]').addEventListener('click', () => { hidePopover(); openTiebaModal(); }); pop.querySelector('[data-action="version"]').addEventListener('click', () => { hidePopover(); openVersionModal(); }); pop.addEventListener('mouseenter', () => { clearTimeout(popHideTimer); clearTimeout(popLeaveTimer); }); pop.addEventListener('mouseleave', scheduleHidePopover); document.body.appendChild(pop); } function bindHoverEvents(btn) { btn.addEventListener('mouseenter', () => { clearTimeout(popHoverTimer); clearTimeout(popLeaveTimer); popHoverTimer = setTimeout(showPopover, 150); }); btn.addEventListener('mouseleave', () => { clearTimeout(popHoverTimer); scheduleHidePopover(); }); btn.addEventListener('click', () => { clearTimeout(popHoverTimer); hidePopover(); }); window.addEventListener('resize', () => { const pop = document.getElementById('tiebaAutoSignPopover'); if (pop && pop.classList.contains('visible')) positionPopover(); }); } /* ===== v2.3:showModal 新增 opts.autoWidth 选项(贴吧列表宽度自适应内容) ===== */ function showModal(title, bodyHTML, onReady, opts = {}) { closeModal(); const overlay = document.createElement('div'); overlay.id = 'tiebaSignModalOverlay'; overlay.innerHTML = `
`; if (opts.autoWidth) overlay.querySelector('#tiebaSignModal').classList.add('auto-width'); overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(); }); overlay.querySelector('.modal-close').addEventListener('click', closeModal); document.body.appendChild(overlay); if (onReady) onReady(overlay); } function closeModal() { document.getElementById('tiebaSignModalOverlay')?.remove(); } function openSettingsModal() { const s = getSettings(); showModal('设置', `
每批签到数量
签到间隔 (ms)
休息间隔 (个)
`, (overlay) => { overlay.querySelector('#signSetSave').addEventListener('click', () => { const batchSize = Math.max(1, parseInt(overlay.querySelector('#signSetBatch').value) || 5); const baseDelay = Math.max(0, parseInt(overlay.querySelector('#signSetDelay').value) || 300); const restInterval = Math.max(0, parseInt(overlay.querySelector('#signSetRest').value) || 100); GM_setValue(SETTINGS_KEY, JSON.stringify({ batchSize, baseDelay, restInterval })); closeModal(); }); }); } /* ===== v2.3:贴吧列表弹窗——单行布局(吧名 | 等级/经验 | 签到状态),已移除头衔;宽度自适应 ===== */ async function openTiebaModal() { showModal('贴吧', '
正在获取贴吧列表...
'); try { const list = await getFollowedTiebaWithSignStatus(); if (!list.length) { showModal('贴吧', '
未获取到关注的贴吧
'); return; } const items = list.map(t => { const extra = t.extra || {}; const levelId = extra.levelId || 0; const levelText = levelId ? `${levelId}级` : '-'; const cur = (extra.curScore !== undefined && extra.curScore !== null) ? extra.curScore : 0; const need = (extra.upgradeScore !== undefined && extra.upgradeScore !== null) ? extra.upgradeScore : 0; const expText = need > 0 ? `${cur}/${need}` : `${cur}`; const signText = t.is_sign === 1 ? '已签到' : '未签到'; const signColor = t.is_sign === 1 ? colors.success : colors.error; /* 吧名超链接:点击在新标签页打开对应贴吧 */ const tiebaUrl = `https://tieba.baidu.com/f?kw=${encodeURIComponent(t.forum_name)}`; return `
${escapeHTML(t.forum_name)} 等级:${levelText} | 经验:${expText} ${signText}
`; }).join(''); showModal('贴吧', `
共关注 ${list.length} 个贴吧(点击吧名在新标签页打开)
${items}`, null, { autoWidth: true }); } catch { showModal('贴吧', '
获取贴吧列表失败
'); } } function openVersionModal() { showModal('版本', `
百度贴吧一键签到
版本:v${SCRIPT_VERSION}
更新:https://scriptcat.org/zh-CN/users/211553
协议:MIT License
v2.3 更新:列表移除头衔,仅显示等级与经验;三数据源并行抓取 + 详情缓存 + 后台预热,大幅降低打开延迟;吧名、等级、经验单行显示,弹窗宽度随内容自适应加宽。
`); } /* ==================== 签到主逻辑 ==================== */ 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(false); 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; let detailDirty = false; /* 本次是否有签到成功(经验值已变化,需使详情缓存失效以刷新) */ 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 settings = getSettings(); const baseDelay = settings.baseDelay; const batchSize = settings.batchSize; const restInterval = settings.restInterval; 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++; detailDirty = true; 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 (restInterval > 0 && (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--; detailDirty = true; 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--; detailDirty = true; 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; /* 签到产生经验变化后使详情缓存失效,下次打开列表将重新抓取最新数据 */ if (detailDirty) clearDetailCache(); 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(); createPopover(); bindHoverEvents(document.getElementById('tiebaAutoSignBtn')); updateButtonStats(); /* v2.3 新增:后台静默预热详情缓存,首次打开"贴吧"列表也基本秒开(失败不弹窗) */ setTimeout(() => { if (!getDetailCache()) { getFollowedTiebaWithSignStatus(true, true).catch(() => { }); } }, 2000); if (refreshTimer) clearInterval(refreshTimer); refreshTimer = setInterval(() => updateButtonStats(), 60000); }, isMobile() ? 3000 : 2000); } document.readyState === 'complete' ? init() : window.addEventListener('load', init); })();