// ==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('所有回复处理完成')
}