LINUXDO ReadBoost

LINUXDO ReadBoost是一個LINUXDO刷取已讀帖量腳本,理論上支持所有Discourse論壇

  1. // ==UserScript==
  2. // @name LINUXDO ReadBoost
  3. // @namespace linux.do_ReadBoost
  4. // @match https://linux.do/*
  5. // @grant GM_setValue
  6. // @grant GM_getValue
  7. // @grant GM_registerMenuCommand
  8. // @version 2.2
  9. // @author Do
  10. // @description LINUXDO ReadBoost是一个LINUXDO刷取已读帖量脚本,理论上支持所有Discourse论坛
  11. // @description:zh-TW LINUXDO ReadBoost是一個LINUXDO刷取已讀帖量腳本,理論上支持所有Discourse論壇
  12. // @description:en LINUXDO ReadBoost is a script for LINUXDO to boost the number of read posts. It theoretically supports all Discourse forums.
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict'
  17. const hasAgreed = GM_getValue("hasAgreed", false)
  18. if (!hasAgreed) {
  19. const userInput = prompt("[ LINUXDO ReadBoost ]\n检测到这是你第一次使用LINUXDO ReadBoost,使用前你必须知晓:使用该第三方脚本可能会导致包括并不限于账号被限制、被封禁的潜在风险,脚本不对出现的任何风险负责,这是一个开源脚本,你可以自由审核其中的内容,如果你同意以上内容,请输入'明白'")
  20. if (userInput !== "明白") {
  21. alert("您未同意风险提示,脚本已停止运行。")
  22. return
  23. }
  24. GM_setValue("hasAgreed", true)
  25. }
  26.  
  27. // 默认参数
  28. const DEFAULT_CONFIG = {
  29. baseDelay: 2500,
  30. randomDelayRange: 800,
  31. minReqSize: 8,
  32. maxReqSize: 20,
  33. minReadTime: 800,
  34. maxReadTime: 3000,
  35. autoStart: false,
  36. startFromCurrent: false
  37. }
  38.  
  39. let config = { ...DEFAULT_CONFIG, ...getStoredConfig() }
  40. let isRunning = false
  41. let shouldStop = false
  42. let statusLabel = null
  43. let initTimeout = null
  44. function isTopicPage() {
  45. return /^https:\/\/linux\.do\/t\/[^/]+\/\d+/.test(window.location.href)
  46. }
  47.  
  48. function getPageInfo() {
  49. if (!isTopicPage()) {
  50. throw new Error("不在帖子页面")
  51. }
  52. const topicID = window.location.pathname.split("/")[3]
  53. const repliesElement = document.querySelector("div[class=timeline-replies]")
  54. const csrfElement = document.querySelector("meta[name=csrf-token]")
  55.  
  56. if (!repliesElement || !csrfElement) {
  57. throw new Error("无法获取页面信息,请确认在正确的帖子页面")
  58. }
  59.  
  60. const repliesInfo = repliesElement.textContent.trim()
  61. const [currentPosition, totalReplies] = repliesInfo.split("/").map(part => parseInt(part.trim(), 10))
  62. const csrfToken = csrfElement.getAttribute("content")
  63.  
  64. return { topicID, currentPosition, totalReplies, csrfToken }
  65. }
  66.  
  67.  
  68. function getStoredConfig() {
  69. return {
  70. baseDelay: GM_getValue("baseDelay", DEFAULT_CONFIG.baseDelay),
  71. randomDelayRange: GM_getValue("randomDelayRange", DEFAULT_CONFIG.randomDelayRange),
  72. minReqSize: GM_getValue("minReqSize", DEFAULT_CONFIG.minReqSize),
  73. maxReqSize: GM_getValue("maxReqSize", DEFAULT_CONFIG.maxReqSize),
  74. minReadTime: GM_getValue("minReadTime", DEFAULT_CONFIG.minReadTime),
  75. maxReadTime: GM_getValue("maxReadTime", DEFAULT_CONFIG.maxReadTime),
  76. autoStart: GM_getValue("autoStart", DEFAULT_CONFIG.autoStart),
  77. startFromCurrent: GM_getValue("startFromCurrent", DEFAULT_CONFIG.startFromCurrent)
  78. }
  79. }
  80.  
  81. function saveConfig(newConfig) {
  82. Object.keys(newConfig).forEach(key => {
  83. GM_setValue(key, newConfig[key])
  84. config[key] = newConfig[key]
  85. })
  86. location.reload()
  87. }
  88.  
  89. function createStatusLabel() {
  90. // 移除已存在的状态标签
  91. const existingLabel = document.getElementById("readBoostStatusLabel")
  92. if (existingLabel) {
  93. existingLabel.remove()
  94. }
  95.  
  96. const headerButtons = document.querySelector(".header-buttons")
  97. if (!headerButtons) return null
  98.  
  99. const labelSpan = document.createElement("span")
  100. labelSpan.id = "readBoostStatusLabel"
  101. labelSpan.style.cssText = `
  102. margin-left: 10px;
  103. margin-right: 10px;
  104. padding: 5px 10px;
  105. border-radius: 4px;
  106. background: var(--primary-low);
  107. color: var(--primary);
  108. font-size: 12px;
  109. font-weight: bold;
  110. cursor: pointer;
  111. `
  112. labelSpan.textContent = "ReadBoost"+" ⚙️"
  113. labelSpan.addEventListener("click", showSettingsUI)
  114.  
  115. headerButtons.appendChild(labelSpan)
  116. return labelSpan
  117. }
  118.  
  119. // 更新状态
  120. function updateStatus(text, type = "info") {
  121. if (!statusLabel) return
  122.  
  123. const colors = {
  124. info: "var(--primary)",
  125. success: "#2e8b57",
  126. warning: "#ff8c00",
  127. error: "#dc3545",
  128. running: "#007bff"
  129. }
  130.  
  131. statusLabel.textContent = text + " ⚙️"
  132. statusLabel.style.color = colors[type] || colors.info
  133. }
  134.  
  135. function showSettingsUI() {
  136. const settingsDiv = document.createElement("div")
  137. settingsDiv.style.cssText = `
  138. position: fixed;
  139. top: 50%;
  140. left: 50%;
  141. transform: translate(-50%, -50%);
  142. padding: 25px;
  143. border-radius: 12px;
  144. z-index: 10000;
  145. background: var(--secondary);
  146. color: var(--primary);
  147. box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  148. border: 1px solid var(--primary-low);
  149. min-width: 400px;
  150. max-width: 500px;
  151. `
  152.  
  153. const autoStartChecked = config.autoStart ? "checked" : ""
  154. const startFromCurrentChecked = config.startFromCurrent ? "checked" : ""
  155. settingsDiv.innerHTML = `
  156. <h3 style="margin-top: 0; color: var(--primary); text-align: center;">ReadBoost 设置</h3>
  157. <div style="display: grid; gap: 15px;">
  158. <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
  159. <label style="display: flex; flex-direction: column; gap: 5px;">
  160. <span>基础延迟(ms):</span>
  161. <input id="baseDelay" type="number" value="${config.baseDelay}"
  162. style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
  163. </label>
  164. <label style="display: flex; flex-direction: column; gap: 5px;">
  165. <span>随机延迟范围(ms):</span>
  166. <input id="randomDelayRange" type="number" value="${config.randomDelayRange}"
  167. style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
  168. </label>
  169. </div>
  170. <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
  171. <label style="display: flex; flex-direction: column; gap: 5px;">
  172. <span>最小每次请求量:</span>
  173. <input id="minReqSize" type="number" value="${config.minReqSize}"
  174. style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
  175. </label>
  176. <label style="display: flex; flex-direction: column; gap: 5px;">
  177. <span>最大每次请求量:</span>
  178. <input id="maxReqSize" type="number" value="${config.maxReqSize}"
  179. style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
  180. </label>
  181. </div>
  182. <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
  183. <label style="display: flex; flex-direction: column; gap: 5px;">
  184. <span>最小阅读时间(ms):</span>
  185. <input id="minReadTime" type="number" value="${config.minReadTime}"
  186. style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
  187. </label>
  188. <label style="display: flex; flex-direction: column; gap: 5px;">
  189. <span>最大阅读时间(ms):</span>
  190. <input id="maxReadTime" type="number" value="${config.maxReadTime}"
  191. style="padding: 8px; border: 1px solid var(--primary-low); border-radius: 4px; background: var(--secondary);">
  192. </label>
  193. </div>
  194. <div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
  195. <label style="display: flex; align-items: center; gap: 8px;">
  196. <input type="checkbox" id="advancedMode" style="transform: scale(1.2);">
  197. <span>高级设置模式</span>
  198. </label>
  199. <label style="display: flex; align-items: center; gap: 8px;">
  200. <input type="checkbox" id="autoStart" ${autoStartChecked} style="transform: scale(1.2);">
  201. <span>自动运行</span>
  202. </label>
  203. <label style="display: flex; align-items: center; gap: 8px;">
  204. <input type="checkbox" id="startFromCurrent" ${startFromCurrentChecked} style="transform: scale(1.2);">
  205. <span>从当前浏览位置开始</span>
  206. </label>
  207. </div>
  208. <div style="display: flex; gap: 10px; justify-content: center; margin-top: 10px;">
  209. <button id="saveSettings" style="padding: 10px 20px; border: none; border-radius: 6px; background: #007bff; color: white; cursor: pointer;">保存设置</button>
  210. <button id="resetDefaults" style="padding: 10px 20px; border: none; border-radius: 6px; background: #6c757d; color: white; cursor: pointer;">重置默认</button>
  211. <button id="closeSettings" style="padding: 10px 20px; border: none; border-radius: 6px; background: #dc3545; color: white; cursor: pointer;">关闭</button>
  212. </div>
  213. </div>
  214. `
  215.  
  216. document.body.appendChild(settingsDiv)
  217.  
  218. toggleAdvancedInputs(false)
  219.  
  220. document.getElementById("advancedMode").addEventListener("change", (e) => {
  221. if (e.target.checked) {
  222. const confirmed = confirm("高级设置可能增加账号风险,确定要启用吗?")
  223. if (!confirmed) {
  224. e.target.checked = false
  225. return
  226. }
  227. }
  228. toggleAdvancedInputs(e.target.checked)
  229. })
  230.  
  231. document.getElementById("saveSettings").addEventListener("click", () => {
  232. const newConfig = {
  233. baseDelay: parseInt(document.getElementById("baseDelay").value, 10),
  234. randomDelayRange: parseInt(document.getElementById("randomDelayRange").value, 10),
  235. minReqSize: parseInt(document.getElementById("minReqSize").value, 10),
  236. maxReqSize: parseInt(document.getElementById("maxReqSize").value, 10),
  237. minReadTime: parseInt(document.getElementById("minReadTime").value, 10),
  238. maxReadTime: parseInt(document.getElementById("maxReadTime").value, 10),
  239. autoStart: document.getElementById("autoStart").checked,
  240. startFromCurrent: document.getElementById("startFromCurrent").checked
  241. }
  242.  
  243. saveConfig(newConfig)
  244. settingsDiv.remove()
  245. updateStatus("设置已保存", "success")
  246.  
  247. })
  248.  
  249. document.getElementById("resetDefaults").addEventListener("click", () => {
  250. if (confirm("确定要重置为默认设置吗?")) {
  251. saveConfig(DEFAULT_CONFIG)
  252. settingsDiv.remove()
  253. updateStatus("已重置为默认设置", "info")
  254. }
  255. })
  256.  
  257. document.getElementById("closeSettings").addEventListener("click", () => {
  258. settingsDiv.remove()
  259. })
  260.  
  261. function toggleAdvancedInputs(enabled) {
  262. const inputs = ["baseDelay", "randomDelayRange", "minReqSize", "maxReqSize", "minReadTime", "maxReadTime"]
  263. inputs.forEach(id => {
  264. const input = document.getElementById(id)
  265. if (input) {
  266. input.disabled = !enabled
  267. input.style.opacity = enabled ? "1" : "0.6"
  268. }
  269. })
  270. }
  271. }
  272.  
  273. async function startReading() {
  274. if (isRunning) {
  275. updateStatus("脚本正在运行中...", "warning")
  276. return
  277. }
  278.  
  279. try {
  280. const pageInfo = getPageInfo()
  281. isRunning = true
  282. shouldStop = false
  283.  
  284. updateStatus("正在启动...", "running")
  285.  
  286. await processReading(pageInfo)
  287.  
  288. updateStatus("处理完成", "success")
  289. } catch (error) {
  290. console.error("执行错误:", error)
  291. if (error.message === "用户停止执行") {
  292. updateStatus("ReadBoost", "info")
  293. } else {
  294. updateStatus("执行失败: " + error.message, "error")
  295. }
  296. } finally {
  297. isRunning = false
  298. }
  299. }
  300.  
  301.  
  302. function stopReading() {
  303. shouldStop = true
  304. updateStatus("正在停止...", "warning")
  305. }
  306.  
  307. // 处理阅读逻辑
  308. async function processReading(pageInfo) {
  309. const { topicID, currentPosition, totalReplies, csrfToken } = pageInfo
  310. const startPosition = config.startFromCurrent ? currentPosition : 1
  311.  
  312. console.log(`开始处理,起始位置: ${startPosition}, 总回复: ${totalReplies}`)
  313.  
  314. function getRandomInt(min, max) {
  315. return Math.floor(Math.random() * (max - min + 1)) + min
  316. }
  317.  
  318. async function sendBatch(startId, endId, retryCount = 3) {
  319. // 停止检查
  320. if (shouldStop) throw new Error("用户停止执行")
  321.  
  322. const params = new URLSearchParams()
  323.  
  324. for (let i = startId; i <= endId; i++) {
  325. params.append(`timings[${i}]`, getRandomInt(config.minReadTime, config.maxReadTime).toString())
  326. }
  327.  
  328. const topicTime = getRandomInt(
  329. config.minReadTime * (endId - startId + 1),
  330. config.maxReadTime * (endId - startId + 1)
  331. ).toString()
  332.  
  333. params.append('topic_time', topicTime)
  334. params.append('topic_id', topicID)
  335.  
  336. try {
  337. const response = await fetch("https://linux.do/topics/timings", {
  338. method: "POST",
  339. headers: {
  340. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  341. "X-CSRF-Token": csrfToken,
  342. "X-Requested-With": "XMLHttpRequest"
  343. },
  344. body: params,
  345. credentials: "include"
  346. })
  347.  
  348. if (!response.ok) {
  349. throw new Error(`HTTP ${response.status}`)
  350. }
  351.  
  352. // 再次检查是否应该停止
  353. if (shouldStop) throw new Error("用户停止执行")
  354.  
  355. updateStatus(`处理回复 ${startId}-${endId} (${Math.round((endId / totalReplies) * 100)}%)`, "running")
  356.  
  357. } catch (error) {
  358. if (shouldStop) throw error // 如果是停止信号,直接抛出
  359.  
  360. if (retryCount > 0) {
  361. updateStatus(`重试 ${startId}-${endId} (剩余${retryCount}次)`, "warning")
  362. await new Promise(r => setTimeout(r, 2000))
  363. return await sendBatch(startId, endId, retryCount - 1)
  364. }
  365. throw error
  366. }
  367.  
  368. // 延迟期间也检查停止信号
  369. const delay = config.baseDelay + getRandomInt(0, config.randomDelayRange)
  370. for (let i = 0; i < delay; i += 100) {
  371. if (shouldStop) throw new Error("用户停止执行")
  372. await new Promise(r => setTimeout(r, Math.min(100, delay - i)))
  373. }
  374. }
  375.  
  376. // 批量处理
  377. for (let i = startPosition; i <= totalReplies;) {
  378. if (shouldStop) break
  379.  
  380. const batchSize = getRandomInt(config.minReqSize, config.maxReqSize)
  381. const startId = i
  382. const endId = Math.min(i + batchSize - 1, totalReplies)
  383.  
  384. await sendBatch(startId, endId)
  385. i = endId + 1
  386. }
  387. }
  388.  
  389. // 注册(不可用)菜单命令
  390. GM_registerMenuCommand("🚀 开始执行", startReading)
  391. GM_registerMenuCommand("⏹️ 停止执行", stopReading)
  392. GM_registerMenuCommand("⚙️ 设置", showSettingsUI)
  393.  
  394. function init() {
  395. statusLabel = createStatusLabel()
  396. // 强制停止之前的任务
  397. shouldStop = true
  398.  
  399. // 等待当前任务停止后再继续
  400. if (isRunning) {
  401. setTimeout(init, 1000)
  402. return
  403. }
  404.  
  405. // 重置状态
  406. isRunning = false
  407. shouldStop = false
  408.  
  409. // 清除之前的超时
  410. if (initTimeout) {
  411. clearTimeout(initTimeout)
  412. }
  413. if (!isTopicPage()) return
  414.  
  415. try {
  416. const pageInfo = getPageInfo()
  417. console.log("LINUXDO ReadBoost 已加载")
  418. console.log(`帖子ID: ${pageInfo.topicID}, 总回复: ${pageInfo.totalReplies}`)
  419.  
  420. statusLabel = createStatusLabel()
  421.  
  422.  
  423. if (config.autoStart) {
  424. initTimeout = setTimeout(startReading, 1000)
  425. }
  426.  
  427. } catch (error) {
  428. console.error("初始化失败:", error)
  429. initTimeout = setTimeout(init, 1000)
  430. }
  431. }
  432. // 监听 URL 变化
  433. function setupRouteListener() {
  434. let lastUrl = location.href
  435. const originalPushState = history.pushState
  436. history.pushState = function () {
  437. originalPushState.apply(history, arguments)
  438. if (location.href !== lastUrl) {
  439. lastUrl = location.href
  440. setTimeout(init, 500)
  441. }
  442. }
  443. }
  444. if (document.readyState === 'loading') {
  445. document.addEventListener('DOMContentLoaded', () => {
  446. init()
  447. setupRouteListener()
  448. })
  449. } else {
  450. init()
  451. setupRouteListener()
  452. }
  453. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址