您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
LINUXDO ReadBoost是一个LINUXDO刷取已读帖量脚本,理论上支持所有Discourse论坛
当前为
// ==UserScript== // @name LINUXDO ReadBoost Demo // @namespace linux.do_ReadBoost // @match https://linux.do/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @version 2.0 // @author Do // @description LINUXDO ReadBoost是一个LINUXDO刷取已读帖量脚本,理论上支持所有Discourse论坛 // @description:zh-TW LINUXDO ReadBoost是一個LINUXDO刷取已讀帖量腳本,理論上支持所有Discourse論壇 // @description:en LINUXDO ReadBoost is a script for LINUXDO to boost the number of read posts. It theoretically supports all Discourse forums. // ==/UserScript== (function () { 'use strict' const hasAgreed = GM_getValue("hasAgreed", false) if (!hasAgreed) { const userInput = prompt("[ LINUXDO ReadBoost ]\n检测到这是你第一次使用LINUXDO ReadBoost,使用前你必须知晓:使用该第三方脚本可能会导致包括并不限于账号被限制、被封禁的潜在风险,脚本不对出现的任何风险负责,这是一个开源脚本,你可以自由审核其中的内容,如果你同意以上内容,请输入'明白'") if (userInput !== "明白") { alert("您未同意风险提示,脚本已停止运行。") return } GM_setValue("hasAgreed", true) } // 默认参数 const DEFAULT_CONFIG = { baseDelay: 2500, randomDelayRange: 800, minReqSize: 8, maxReqSize: 20, minReadTime: 800, maxReadTime: 3000, autoStart: false, startFromCurrent: false } let config = { ...DEFAULT_CONFIG, ...getStoredConfig() } let isRunning = false let shouldStop = false let statusLabel = null let initTimeout = null function isTopicPage() { return /^https:\/\/linux\.do\/t\/[^/]+\/\d+/.test(window.location.href) } function getPageInfo() { if (!isTopicPage()) { throw new Error("不在帖子页面") } const topicID = window.location.pathname.split("/")[3] const repliesElement = document.querySelector("div[class=timeline-replies]") const csrfElement = document.querySelector("meta[name=csrf-token]") if (!repliesElement || !csrfElement) { throw new Error("无法获取页面信息,请确认在正确的帖子页面") } const repliesInfo = repliesElement.textContent.trim() const [currentPosition, totalReplies] = repliesInfo.split("/").map(part => parseInt(part.trim(), 10)) const csrfToken = csrfElement.getAttribute("content") return { topicID, currentPosition, totalReplies, csrfToken } } function getStoredConfig() { return { baseDelay: GM_getValue("baseDelay", DEFAULT_CONFIG.baseDelay), randomDelayRange: GM_getValue("randomDelayRange", DEFAULT_CONFIG.randomDelayRange), minReqSize: GM_getValue("minReqSize", DEFAULT_CONFIG.minReqSize), maxReqSize: GM_getValue("maxReqSize", DEFAULT_CONFIG.maxReqSize), minReadTime: GM_getValue("minReadTime", DEFAULT_CONFIG.minReadTime), maxReadTime: GM_getValue("maxReadTime", DEFAULT_CONFIG.maxReadTime), autoStart: GM_getValue("autoStart", DEFAULT_CONFIG.autoStart), startFromCurrent: GM_getValue("startFromCurrent", DEFAULT_CONFIG.startFromCurrent) } } function saveConfig(newConfig) { Object.keys(newConfig).forEach(key => { GM_setValue(key, newConfig[key]) config[key] = newConfig[key] }) location.reload() } function createStatusLabel() { // 移除已存在的状态标签 const existingLabel = document.getElementById("readBoostStatusLabel") if (existingLabel) { existingLabel.remove() } const headerButtons = document.querySelector(".header-buttons") if (!headerButtons) return null const labelSpan = document.createElement("span") labelSpan.id = "readBoostStatusLabel" labelSpan.style.cssText = ` margin-left: 10px; margin-right: 10px; padding: 5px 10px; border-radius: 4px; background: var(--primary-low); color: var(--primary); font-size: 12px; font-weight: bold; ` labelSpan.textContent = "ReadBoost" headerButtons.appendChild(labelSpan) return labelSpan } // 更新状态 function updateStatus(text, type = "info") { if (!statusLabel) return const colors = { info: "var(--primary)", success: "#2e8b57", warning: "#ff8c00", error: "#dc3545", running: "#007bff" } statusLabel.textContent = text statusLabel.style.color = colors[type] || colors.info } function showSettingsUI() { const settingsDiv = document.createElement("div") settingsDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 25px; border-radius: 12px; z-index: 10000; background: var(--secondary); color: var(--primary); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); border: 1px solid var(--primary-low); min-width: 400px; max-width: 500px; ` const autoStartChecked = config.autoStart ? "checked" : "" const startFromCurrentChecked = config.startFromCurrent ? "checked" : "" settingsDiv.innerHTML = ` <h3 style="margin-top: 0; color: var(--primary); text-align: center;">ReadBoost 设置</h3> <div style="display: grid; gap: 15px;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;"> <label style="display: flex; flex-direction: column; gap: 5px;"> <span>基础延迟(ms):</span> <input id="baseDelay" type="number" value="${config.baseDelay}" style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);"> </label> <label style="display: flex; flex-direction: column; gap: 5px;"> <span>随机延迟范围(ms):</span> <input id="randomDelayRange" type="number" value="${config.randomDelayRange}" style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);"> </label> </div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;"> <label style="display: flex; flex-direction: column; gap: 5px;"> <span>最小每次请求量:</span> <input id="minReqSize" type="number" value="${config.minReqSize}" style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);"> </label> <label style="display: flex; flex-direction: column; gap: 5px;"> <span>最大每次请求量:</span> <input id="maxReqSize" type="number" value="${config.maxReqSize}" style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);"> </label> </div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;"> <label style="display: flex; flex-direction: column; gap: 5px;"> <span>最小阅读时间(ms):</span> <input id="minReadTime" type="number" value="${config.minReadTime}" style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);"> </label> <label style="display: flex; flex-direction: column; gap: 5px;"> <span>最大阅读时间(ms):</span> <input id="maxReadTime" type="number" value="${config.maxReadTime}" style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);"> </label> </div> <div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;"> <label style="display: flex; align-items: center; gap: 8px;"> <input type="checkbox" id="advancedMode" style="transform: scale(1.2);"> <span>高级设置模式</span> </label> <label style="display: flex; align-items: center; gap: 8px;"> <input type="checkbox" id="autoStart" ${autoStartChecked} style="transform: scale(1.2);"> <span>自动运行</span> </label> <label style="display: flex; align-items: center; gap: 8px;"> <input type="checkbox" id="startFromCurrent" ${startFromCurrentChecked} style="transform: scale(1.2);"> <span>从当前浏览位置开始</span> </label> </div> <div style="display: flex; gap: 10px; justify-content: center; margin-top: 10px;"> <button id="saveSettings" style="padding: 10px 20px; border: none; border-radius: 6px; background: #007bff; color: white; cursor: pointer;">保存设置</button> <button id="resetDefaults" style="padding: 10px 20px; border: none; border-radius: 6px; background: #6c757d; color: white; cursor: pointer;">重置默认</button> <button id="closeSettings" style="padding: 10px 20px; border: none; border-radius: 6px; background: #dc3545; color: white; cursor: pointer;">关闭</button> </div> </div> ` document.body.appendChild(settingsDiv) toggleAdvancedInputs(false) document.getElementById("advancedMode").addEventListener("change", (e) => { if (e.target.checked) { const confirmed = confirm("高级设置可能增加账号风险,确定要启用吗?") if (!confirmed) { e.target.checked = false return } } toggleAdvancedInputs(e.target.checked) }) document.getElementById("saveSettings").addEventListener("click", () => { const newConfig = { baseDelay: parseInt(document.getElementById("baseDelay").value, 10), randomDelayRange: parseInt(document.getElementById("randomDelayRange").value, 10), minReqSize: parseInt(document.getElementById("minReqSize").value, 10), maxReqSize: parseInt(document.getElementById("maxReqSize").value, 10), minReadTime: parseInt(document.getElementById("minReadTime").value, 10), maxReadTime: parseInt(document.getElementById("maxReadTime").value, 10), autoStart: document.getElementById("autoStart").checked, startFromCurrent: document.getElementById("startFromCurrent").checked } saveConfig(newConfig) settingsDiv.remove() updateStatus("设置已保存", "success") }) document.getElementById("resetDefaults").addEventListener("click", () => { if (confirm("确定要重置为默认设置吗?")) { saveConfig(DEFAULT_CONFIG) settingsDiv.remove() updateStatus("已重置为默认设置", "info") } }) document.getElementById("closeSettings").addEventListener("click", () => { settingsDiv.remove() }) function toggleAdvancedInputs(enabled) { const inputs = ["baseDelay", "randomDelayRange", "minReqSize", "maxReqSize", "minReadTime", "maxReadTime"] inputs.forEach(id => { const input = document.getElementById(id) if (input) { input.disabled = !enabled input.style.opacity = enabled ? "1" : "0.6" } }) } } async function startReading() { if (isRunning) { updateStatus("脚本正在运行中...", "warning") return } try { const pageInfo = getPageInfo() isRunning = true shouldStop = false updateStatus("正在启动...", "running") await processReading(pageInfo) updateStatus("处理完成", "success") } catch (error) { console.error("执行错误:", error) if (error.message === "用户停止执行") { updateStatus("ReadBoost", "info") } else { updateStatus("执行失败: " + error.message, "error") } } finally { isRunning = false } } function stopReading() { shouldStop = true updateStatus("正在停止...", "warning") } // 处理阅读逻辑 async function processReading(pageInfo) { const { topicID, currentPosition, totalReplies, csrfToken } = pageInfo const startPosition = config.startFromCurrent ? currentPosition : 1 console.log(`开始处理,起始位置: ${startPosition}, 总回复: ${totalReplies}`) function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min } async function sendBatch(startId, endId, retryCount = 3) { // 停止检查 if (shouldStop) throw new Error("用户停止执行") const params = new URLSearchParams() for (let i = startId; i <= endId; i++) { params.append(`timings[${i}]`, getRandomInt(config.minReadTime, config.maxReadTime).toString()) } const topicTime = getRandomInt( config.minReadTime * (endId - startId + 1), config.maxReadTime * (endId - startId + 1) ).toString() params.append('topic_time', topicTime) params.append('topic_id', topicID) try { const response = await fetch("https://linux.do/topics/timings", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-CSRF-Token": csrfToken, "X-Requested-With": "XMLHttpRequest" }, body: params, credentials: "include" }) if (!response.ok) { throw new Error(`HTTP ${response.status}`) } // 再次检查是否应该停止 if (shouldStop) throw new Error("用户停止执行") updateStatus(`处理回复 ${startId}-${endId} (${Math.round((endId / totalReplies) * 100)}%)`, "running") } catch (error) { if (shouldStop) throw error // 如果是停止信号,直接抛出 if (retryCount > 0) { updateStatus(`重试 ${startId}-${endId} (剩余${retryCount}次)`, "warning") await new Promise(r => setTimeout(r, 2000)) return await sendBatch(startId, endId, retryCount - 1) } throw error } // 延迟期间也检查停止信号 const delay = config.baseDelay + getRandomInt(0, config.randomDelayRange) for (let i = 0; i < delay; i += 100) { if (shouldStop) throw new Error("用户停止执行") await new Promise(r => setTimeout(r, Math.min(100, delay - i))) } } // 批量处理 for (let i = startPosition; i <= totalReplies;) { if (shouldStop) break const batchSize = getRandomInt(config.minReqSize, config.maxReqSize) const startId = i const endId = Math.min(i + batchSize - 1, totalReplies) await sendBatch(startId, endId) i = endId + 1 } } // 注册(不可用)菜单命令 GM_registerMenuCommand("🚀 开始执行", startReading) GM_registerMenuCommand("⏹️ 停止执行", stopReading) GM_registerMenuCommand("⚙️ 设置", showSettingsUI) function init() { statusLabel = createStatusLabel() // 强制停止之前的任务 shouldStop = true // 等待当前任务停止后再继续 if (isRunning) { setTimeout(init, 1000) return } // 重置状态 isRunning = false shouldStop = false // 清除之前的超时 if (initTimeout) { clearTimeout(initTimeout) } if (!isTopicPage()) return try { const pageInfo = getPageInfo() console.log("LINUXDO ReadBoost 已加载") console.log(`帖子ID: ${pageInfo.topicID}, 总回复: ${pageInfo.totalReplies}`) statusLabel = createStatusLabel() if (config.autoStart) { initTimeout = setTimeout(startReading, 1000) } } catch (error) { console.error("初始化失败:", error) initTimeout = setTimeout(init, 1000) } } // 监听 URL 变化 function setupRouteListener() { let lastUrl = location.href const originalPushState = history.pushState history.pushState = function () { originalPushState.apply(history, arguments) if (location.href !== lastUrl) { lastUrl = location.href setTimeout(init, 500) } } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { init() setupRouteListener() }) } else { init() setupRouteListener() } })()