您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
LINUXDO ReadBoost是一个LINUXDO刷取已读帖量脚本,理论上支持所有Discourse论坛
// ==UserScript== // @name LINUXDO ReadBoost // @namespace linux.do_ReadBoost // @match https://linux.do/t/topic/* // @grant GM_setValue // @grant GM_getValue // @version 1.4 // @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== const hasAgreed = GM_getValue("hasAgreed", false) if (!hasAgreed) { const userInput = prompt("[ LINUXDO ReadBoost ]\n检测到这是你第一次使用LINUXDO ReadBoost,使用前你必须知晓:使用该第三方脚本可能会导致包括并不限于账号被限制、被封禁的潜在风险,脚本不对出现的任何风险负责,这是一个开源脚本,你可以自由审核其中的内容,如果你同意以上内容,请输入“明白”") if (userInput !== "明白") { alert("您未同意风险提示,脚本已停止运行。") throw new Error("未同意风险提示") } GM_setValue("hasAgreed", true) } // 初始化 const headerButtons = document.querySelector(".header-buttons") const topicID = window.location.pathname.split("/")[3] const repliesInfo = document.querySelector("div[class=timeline-replies]").textContent.trim() const [currentPosition, totalReplies] = repliesInfo.split("/").map(part => parseInt(part.trim(), 10)) const csrfToken = document.querySelector("meta[name=csrf-token]").getAttribute("content") console.log("LINUXDO ReadBoost 已加载") console.log(`帖子ID:${topicID}`) console.log(`当前位置:${currentPosition}`) console.log(`总回复:${totalReplies}`) // 默认参数 const DEFAULT_CONFIG = { baseDelay: 2500, randomDelayRange: 800, minReqSize: 8, maxReqSize: 20, minReadTime: 800, maxReadTime: 3000, autoStart: false } let config = { ...DEFAULT_CONFIG, ...getStoredConfig() } // 设置按钮和状态UI const settingsButton = createButton("设置", "settingsButton", "btn-icon-text") const statusLabel = createStatusLabel("LINUXDO ReadBoost待命中") headerButtons.appendChild(statusLabel) headerButtons.appendChild(settingsButton) // 绑定设置按钮事件 settingsButton.addEventListener("click", showSettingsUI) // 自启动处理 if (config.autoStart) { startReading(topicID, totalReplies) } 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) } } /** * 按钮封装 */ function createButton(label, id, extraClass = "") { const outerSpan = document.createElement("span") outerSpan.className = "auth-buttons" const button = document.createElement("button") button.className = `btn btn-small ${extraClass}` button.id = id const span = document.createElement("span") span.className = "d-button-label" span.textContent = label button.appendChild(span) outerSpan.appendChild(button) return outerSpan } /** * 状态标签封装 */ function createStatusLabel(initialText) { const labelSpan = document.createElement("span") labelSpan.id = "statusLabel" labelSpan.style.marginLeft = "10px" labelSpan.style.marginRight = "10px" labelSpan.textContent = initialText return labelSpan } /** * 更新状态标签内容 */ function updateStatus(text, color = "#555") { const statusLabel = document.getElementById("statusLabel") if (statusLabel) { statusLabel.textContent = text statusLabel.style.color = color } } /** * 显示设置UI界面 */ function showSettingsUI() { const settingsDiv = document.createElement("div") settingsDiv.style.position = "fixed" settingsDiv.style.top = "50%" settingsDiv.style.left = "50%" settingsDiv.style.transform = "translate(-50%, -50%)" settingsDiv.style.padding = "20px" settingsDiv.style.border = "1px solid #ccc" settingsDiv.style.borderRadius = "10px" settingsDiv.style.zIndex = "1000" settingsDiv.style.backgroundColor = "var(--secondary)"; settingsDiv.style.color = "var(--primary)"; settingsDiv.style.boxShadow = "0 4px 14px rgba(0, 0, 0, 0.3)"; const autoStartChecked = config.autoStart ? "checked" : "" const settingsHtml = ` <h3>设置参数</h3> <label>基础延迟(ms): <input id="baseDelay" type="number" value="${config.baseDelay}"></label><br> <label>随机延迟范围(ms): <input id="randomDelayRange" type="number" value="${config.randomDelayRange}"></label><br> <label>最小每次请求阅读量: <input id="minReqSize" type="number" value="${config.minReqSize}"></label><br> <label>最大每次请求阅读量: <input id="maxReqSize" type="number" value="${config.maxReqSize}"></label><br> <label>最小阅读时间(ms): <input id="minReadTime" type="number" value="${config.minReadTime}"></label><br> <label>最大阅读时间(ms): <input id="maxReadTime" type="number" value="${config.maxReadTime}"></label><br> <label><input type="checkbox" id="advancedMode"> 高级设置(解锁参数选项)</label><br> <label><input type="checkbox" id="autoStart" ${autoStartChecked}> 自动运行</label><br><br> <button class="btn btn-small" id="startManually" > <span class="d-button-label">手动开始</span> </button> <button class="btn btn-small" id="saveSettings" > <span class="d-button-label">保存</span> </button> <button class="btn btn-small" id="closeSettings"> <span class="d-button-label">关闭</span> </button> <button class="btn btn-small" id="resetDefaults"> <span class="d-button-label">恢复默认值</span> </button> ` settingsDiv.innerHTML = settingsHtml document.body.appendChild(settingsDiv) // 手动开始按钮 document.getElementById("startManually").addEventListener("click", () => { settingsDiv.remove() startReading(topicID, totalReplies) }) // 保存设置 document.getElementById("saveSettings").addEventListener("click", () => { config.baseDelay = parseInt(document.getElementById("baseDelay").value, 10) config.randomDelayRange = parseInt(document.getElementById("randomDelayRange").value, 10) config.minReqSize = parseInt(document.getElementById("minReqSize").value, 10) config.maxReqSize = parseInt(document.getElementById("maxReqSize").value, 10) config.minReadTime = parseInt(document.getElementById("minReadTime").value, 10) config.maxReadTime = parseInt(document.getElementById("maxReadTime").value, 10) config.autoStart = document.getElementById("autoStart").checked // 持久化保存设置 GM_setValue("baseDelay", config.baseDelay) GM_setValue("randomDelayRange", config.randomDelayRange) GM_setValue("minReqSize", config.minReqSize) GM_setValue("maxReqSize", config.maxReqSize) GM_setValue("minReadTime", config.minReadTime) GM_setValue("maxReadTime", config.maxReadTime) GM_setValue("autoStart", config.autoStart) alert("设置已保存!") location.reload() }) document.getElementById("resetDefaults").addEventListener("click", () => { // 重置为默认配置 config = { ...DEFAULT_CONFIG } // 保存默认配置到存储 GM_setValue("baseDelay", DEFAULT_CONFIG.baseDelay) GM_setValue("randomDelayRange", DEFAULT_CONFIG.randomDelayRange) GM_setValue("minReqSize", DEFAULT_CONFIG.minReqSize) GM_setValue("maxReqSize", DEFAULT_CONFIG.maxReqSize) GM_setValue("minReadTime", DEFAULT_CONFIG.minReadTime) GM_setValue("maxReadTime", DEFAULT_CONFIG.maxReadTime) GM_setValue("autoStart", DEFAULT_CONFIG.autoStart) alert("已恢复默认设置!") location.reload() }) /** * 切换输入框状态,在默认状态下禁用 */ function toggleSettingsInputs(enabled) { const inputs = [ "baseDelay", "randomDelayRange", "minReqSize", "maxReqSize", "minReadTime", "maxReadTime" ] inputs.forEach(inputId => { const inputElement = document.getElementById(inputId) if (inputElement) { inputElement.disabled = !enabled } }) } toggleSettingsInputs(false) // 启用高级设置告警弹窗 document.getElementById("advancedMode").addEventListener("change", (event) => { if (event.target.checked) { const userInput = prompt("[ LINUXDO ReadBoost ]\n如果你不知道你在修改什么,那么不建议开启高级设置,随意修改可能会提高脚本崩溃、账号被禁等风险的可能!请输入 '明白' 确认继续开启高级设置:") if (userInput !== "明白") { alert("您未确认风险,高级设置未启用。") event.target.checked = false return } // 启用所有输入框 toggleSettingsInputs(true) } else { // 禁用所有输入框 toggleSettingsInputs(false) } }) // 关闭设置UI document.getElementById("closeSettings").addEventListener("click", () => { settingsDiv.remove() }) } /** * 开始刷取已读帖子 * @param {string} topicId 主题ID * @param {number} totalReplies 总回复数 */ async function startReading(topicId, totalReplies) { console.log("启动阅读处理...") const baseRequestDelay = config.baseDelay const randomDelayRange = config.randomDelayRange const minBatchReplyCount = config.minReqSize const maxBatchReplyCount = config.maxReqSize const minReadTime = config.minReadTime const maxReadTime = config.maxReadTime // 随机数生成 function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min } // 发起读帖请求 async function sendBatch(startId, endId, retryCount = 3) { const params = createBatchParams(startId, endId) try { const response = await fetch("https://linux.do/topics/timings", { headers: { "accept": "*/*", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "discourse-background": "true", "discourse-logged-in": "true", "discourse-present": "true", "priority": "u=1, i", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "x-csrf-token": csrfToken, "x-requested-with": "XMLHttpRequest", "x-silence-logger": "true" }, referrer: `https://linux.do/`, body: params.toString(), method: "POST", mode: "cors", credentials: "include" }) if (!response.ok) { throw new Error(`HTTP请求失败,状态码:${response.status}`) } console.log(`成功处理回复 ${startId} - ${endId}`) updateStatus(`成功处理回复 ${startId} - ${endId}`, "green") } catch (e) { console.error(`处理回复 ${startId} - ${endId} 失败: `, e) if (retryCount > 0) { console.log(`重试处理回复 ${startId} - ${endId},剩余重试次数:${retryCount}`) updateStatus(`重试处理回复 ${startId} - ${endId},剩余重试次数:${retryCount}`, "orange") // 等待一段时间再重试 const retryDelay = 2000 // 重试间隔时间(毫秒) await new Promise(r => setTimeout(r, retryDelay)) await sendBatch(startId, endId, retryCount - 1) } else { console.error(`处理回复 ${startId} - ${endId} 失败,自动跳过`) updateStatus(`处理回复 ${startId} - ${endId} ,自动跳过`, "red") } } const delay = baseRequestDelay + getRandomInt(0, randomDelayRange) await new Promise(r => setTimeout(r, delay)) } // 生成请求body参数 function createBatchParams(startId, endId) { const params = new URLSearchParams() for (let i = startId; i <= endId; i++) { params.append(`timings[${i}]`, getRandomInt(minReadTime, maxReadTime).toString()) } const topicTime = getRandomInt(minReadTime * (endId - startId + 1), maxReadTime * (endId - startId + 1)).toString() params.append('topic_time', topicTime) params.append('topic_id', topicId) return params } // 批量阅读处理 for (let i = 1; i <= totalReplies;) { const batchSize = getRandomInt(minBatchReplyCount, maxBatchReplyCount) const startId = i const endId = Math.min(i + batchSize - 1, totalReplies) await sendBatch(startId, endId) i = endId + 1 } updateStatus(`所有回复处理完成`, "green") console.log('所有回复处理完成') }