// ==UserScript==
// @name NODELOC ReadBoost
// @namespace nodeloc.cc_ReadBoost
// @match https://nodeloc.cc/t/topic/*
// @grant GM_setValue
// @grant GM_getValue
// @version 1.5
// @author Sunwuyuan
// @description NODELOC ReadBoost是一个NODELOC刷取已读帖量脚本,理论上支持所有Discourse论坛
// @description:zh-TW NODELOC ReadBoost是一個NODELOC刷取已讀帖量腳本,理論上支持所有Discourse論壇
// @description:en NODELOC ReadBoost is a script for NODELOC 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此程序由 adodo 为 LINUX DO 社区开发,你可以在 https://linux.do/t/topic/283062 了解(设置页面包含链接)。\n[ LINUXDO ReadBoost ]\n此程序由 Sunwuyuan 移植,你可以在 https://github.com/Sunwuyuan/NODELOC_ReadBoost 查看(设置页面包含链接)。\n[ NODELOC ReadBoost ]\n检测到这是你第一次使用NODELOC 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("NODELOC 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: true,
};
let config = { ...DEFAULT_CONFIG, ...getStoredConfig() };
// 设置按钮和状态UI
const settingsButton = createButton("设置", "settingsButton", "btn-icon-text");
const statusLabel = createStatusLabel("NODELOC 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" onclick="window.open('https://github.com/Sunwuyuan/NODELOC_ReadBoost', '_blank')">
<span class="d-button-label">开源于 GitHub</span>
</button>
<button class="btn btn-small" onclick="window.open('https://linux.do/t/topic/283062', '_blank')">
<span class="d-button-label">原作者帖子</span>
</button>
<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(
"[ NODELOC 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://nodeloc.cc/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://nodeloc.cc/`,
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("所有回复处理完成");
}