// ==UserScript== // @name linux.do 消灭蓝点 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description try to take over the world! // @author Yous // @match https://linux.do/* // @exclude https://linux.do/*.json // @icon https://cdn.linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png // @require https://cdn.jsdelivr.net/npm/layui@2.9.14/dist/layui.min.js // @resource layuiCSS https://cdn.jsdelivr.net/npm/layui@2.9.14/dist/css/layui.min.css // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_setValue // @grant GM_getValue // ==/UserScript== /* global layui */ (function () { 'use strict'; // 加载外部 CSS let layuiCSS = GM_getResourceText("layuiCSS"); // 替换 CSS 中的相对路径 layuiCSS = layuiCSS.replace(/url\(\s*['"]?(\.\.\/font\/[^'"\s]+)['"]?\s*\)/g, function (match, url) { // 假设字体文件位于一个可以访问的绝对路径 return match.replace(url, 'https://cdn.jsdelivr.net/npm/layui@2.9.14/dist/font/' + url.split('/').pop()); }); GM_addStyle(layuiCSS); // ------ config ------ let CONFIG = { enableBrowseAssist: true, // 开启助手 enableWindowPeriodRead: true, // 开启空闲阅读 readAllPostsInTopic: false, // 是否阅读主题所有帖子,false 从最后的内容开始看 singlePostsReading: 1000, // 单次阅读帖子数,控制 timings 请求 body 行为 maxRetryTimes: 5, // 最大重试次数 windowPeriodTopicUrls: ['https://linux.do/new.json', 'https://linux.do/unread.json'], // 空窗期获取帖子 url getCsrfTokenFromHtml: false, // 是否从 html 中获取 csrf token maxLogLineNum: 100, // 日志最大条数 }; // ------ css ------ let uiWidth = "32rem"; // ui 宽度 let uiQueueHeight = "150px"; // ui 任务队列高度 let uiLogHeight = "300px"; // ui 日志高度 let uiTagFontSize = "0.75rem"; // ui 标签字体大小 let uiQueueFontSize = "0.75rem"; // ui 队列字体大小 let uiLogFontSize = "0.75rem"; // ui 日志字体大小 // ------ logic ------ let lastTaskTime = new Date("1970-01-01T00:00:00"); let statData = { // 维护统计 totalSuccess: 0, totalFail: 0, totalReadingTime: 0, }; let taskQueue = []; // 维护任务队列 let windowPeriodTopics = []; // 空闲随机阅读的帖子列表,[[, <阅读楼层数>]] let excludeTopic = []; // 将5分钟内阅读过的 topic 排除 let logs = []; // 维护日志 let globalCsrfToken = null; // 维护 csrf token let nativeXMLHttpRequestOpen; // 维护原生 XMLHttpRequest 的 open 方法 let nativeXMLHttpRequestSend; // 维护原生 XMLHttpRequest 的 send 方法 let dialogElement = document.createElement("div"); // 维护 UI let queueListElement = document.createElement("ul"); let logListElement = document.createElement("ul"); const isNativeFunction = (func) => { if (typeof func !== "function") { return false; } // 获取函数的字符串表示形式 const funcString = func.toString(); // 检查字符串是否包含 "[native code]" return funcString.includes("[native code]"); }; const ensureNativeMethods = (func) => { if (isNativeFunction(func)) { return func; } else { throw new Error(`${func.name} is not native`); } }; // 随机整数 const getRandomInt = (start, end) => { return Math.floor(Math.random() * (end - start + 1)) + start; }; const matchUrl = (url, pattern) => { return pattern.test(url); }; const isTopicUrl = (url) => { const patternTopic = /^\/t\/[0-9]+\.json($|\?)/; const patternPost = /^\/t\/[0-9]+\/[0-9]+\.json($|\?)/; return matchUrl(url, patternPost) || matchUrl(url, patternTopic); }; const isTimingsUrl = (url) => { const patternTimings = "/topics/timings"; return url === patternTimings; }; const isPollUrl = (url) => { const patternPoll = /^\/message-bus\/[a-z0-9]+\/poll/; return matchUrl(url, patternPoll); }; const formatDate = (d) => { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`; }; // 获取预加载数据 const getPreloadedData = () => { const preloadedDataElement = document.querySelector("#data-preloaded"); if (!preloadedDataElement) { throw new Error("Preloaded data element not found"); } const preloadedData = preloadedDataElement.getAttribute("data-preloaded"); return JSON.parse(preloadedData); }; // 获取用户名 const getUsername = () => { const preloadedData = getPreloadedData(); const preloadedCurrentUserData = JSON.parse(preloadedData.currentUser); return preloadedCurrentUserData.username; }; // 获取 csrf token const getCsrfToken = async () => { if (globalCsrfToken) { return globalCsrfToken; } if (CONFIG.getCsrfTokenFromHtml) { const csrfTokenElement = document.querySelector('meta[name="csrf-token"]'); if (!csrfTokenElement) { throw new Error("CSRF token element not found"); } globalCsrfToken = csrfTokenElement.getAttribute("content") return globalCsrfToken; } else { const csrfRes = await fetch("https://linux.do/session/csrf", { headers: { "x-csrf-token": "undefined", "x-requested-with": "XMLHttpRequest", }, body: null, method: "GET", mode: "cors", credentials: "include", }) .then((res) => res.json()) .catch((err) => { }); if (!csrfRes || !csrfRes.csrf) { throw new Error("CSRF token not fetch"); } globalCsrfToken = csrfRes.csrf return globalCsrfToken; } }; // 定义一个函数,用于每隔1-2秒处理1000个阅读 const handleReadingPosts = async ( task, topicId, numbers, csrfToken, maxReadPosts = CONFIG.singlePostsReading, retryTimes = 0 ) => { // 如果列表为空 或 超过最大限制 跳出 while while (numbers.length > 0 && retryTimes <= CONFIG.maxRetryTimes) { // 取出前 1000 个字符串,如果不足 1000 个,则取出所有剩余的字符串 let toProcess = numbers.slice(0, maxReadPosts); toProcess = toProcess[0] === 0 ? toProcess.slice(1) : toProcess; // 受后端限制,生成随机整数 randTime,范围在 60 秒到 61秒之间 const randTime = getRandomInt(60000, 61000); // 使用模板生成新的字符串列表 const newStrings = toProcess.map((num) => `timings%5B${num}%5D=${randTime}`); // 使用"&"连接字符串 const resultString = [...newStrings, `topic_time=${randTime}`, `topic_id=${topicId}`].join("&"); // 生成随机整数 睡眠时间,范围在 2000ms 到 3000ms 之间 await new Promise(resolve => setTimeout(resolve, getRandomInt(2000, 3000))); try { // 请求 timing const res = await fetch("https://linux.do/topics/timings", { headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "x-csrf-token": csrfToken, "x-requested-with": "XMLHttpRequest", }, body: resultString, method: "POST", mode: "cors", credentials: "include", }); if (res.status === 200) { addLog("success", `已完成话题[${topicId}]${toProcess[0]}至${toProcess[toProcess.length - 1]}层话题阅读`); numbers = numbers.slice(maxReadPosts); retryTimes = 0; await new Promise(resolve => setTimeout(resolve, getRandomInt(1000, 2000))); } else if (res.status >= 400 && res.status < 600) { addLog("warning", `阅读话题[${topicId}]出现错误(${res.status})!正在重试……`); task.status = "retrying"; updateDialogQueue(); retryTimes++; await new Promise(resolve => setTimeout(resolve, getRandomInt(3000, 5000))); } else { throw new Error(`Unexpected status: ${res.status}`); } } catch (err) { console.error(err); retryTimes++; addLog("error", `阅读话题[${topicId}]发生未知错误: ${err.message}`); await new Promise(resolve => setTimeout(resolve, getRandomInt(3000, 5000))); } } if (retryTimes > CONFIG.maxRetryTimes) { return { topicId, error: true, detail: "超过最大重试次数" }; } return { topicId, error: false, detail: "已完成阅读" }; }; // 配置 对话框 const settingDialog = () => { let layer = layui.layer; let form = layui.form; let $ = layui.$; layer.open({ type: 1, title: '设置', shade: false, area: ['34rem', '100%'], offset: 'r', anim: 'slideLeft', move: false, id: 'settingDialog-layer', content: $("#settingDialog"), }); // 初始化界面值 form.val('settingDialog-filter', { "enableBrowseAssist": CONFIG.enableBrowseAssist, "enableWindowPeriodRead": CONFIG.enableWindowPeriodRead, "getCsrfTokenFromHtml": CONFIG.getCsrfTokenFromHtml, "readAllPostsInTopic": CONFIG.readAllPostsInTopic, "windowPeriodTopicUrls": CONFIG.windowPeriodTopicUrls.join(","), "singlePostsReading": CONFIG.singlePostsReading, "maxRetryTimes": CONFIG.maxRetryTimes, "maxLogLineNum": CONFIG.maxLogLineNum, }); // 保存设置事件 form.on('submit(saveSetting)', function (data) { // 获取表单字段值 let field = data.field; let enableBrowseAssist = field?.enableBrowseAssist && field?.enableBrowseAssist === "1" ? true : false; // 助手开关是否被操作 let flag = enableBrowseAssist != CONFIG.enableBrowseAssist ? true : false; CONFIG.enableBrowseAssist = enableBrowseAssist; CONFIG.enableWindowPeriodRead = field?.enableWindowPeriodRead && field?.enableWindowPeriodRead === "1" ? true : false; CONFIG.getCsrfTokenFromHtml = field?.getCsrfTokenFromHtml && field?.getCsrfTokenFromHtml === "1" ? true : false; CONFIG.readAllPostsInTopic = field?.readAllPostsInTopic && field?.readAllPostsInTopic === "1" ? true : false; CONFIG.windowPeriodTopicUrls = field?.windowPeriodTopicUrls && field?.windowPeriodTopicUrls.length > 0 ? field?.windowPeriodTopicUrls.split(",") : []; CONFIG.singlePostsReading = field?.singlePostsReading ? field?.singlePostsReading : 1000; CONFIG.maxRetryTimes = field?.maxRetryTimes ? field?.maxRetryTimes : 5; CONFIG.maxLogLineNum = field?.maxLogLineNum ? field?.maxLogLineNum : 100; let username = getUsername(); GM_setValue(`${username}_eliminate_blue_dot_config`, JSON.stringify(CONFIG)); layer.msg('保存成功', { icon: 1, offset: 'rt', anim: 'slideLeft' }, function () { layer.closeLast('page'); addLog("success", "配置变更完成"); if (flag) { if (CONFIG.enableBrowseAssist) { helperStart(); } else { helperStop(); } } }); return false; // 阻止默认 form 跳转 }); }; // 创建对话框 const createDialog = () => { const dialog = document.createElement("div"); dialog.id = "task-dialog"; dialog.className = "d-modal__container"; dialog.style.cssText = `position: fixed; bottom: 4rem; right: 1rem; width: ${uiWidth}; border-radius: 0.5rem; display: none; z-index: 1000;`; const header = document.createElement("div"); header.className = "d-modal__header"; header.style.cssText = ""; header.innerHTML = '

Task Queue & Logs

'; const switchButton = document.createElement("button"); switchButton.className = "btn no-text btn-icon btn-flat"; switchButton.style = "flex: 0;"; switchButton.title = "设置"; switchButton.type = "button"; switchButton.innerHTML = ``; switchButton.onclick = () => { settingDialog(); }; header.appendChild(switchButton); const headerCloseButton = document.createElement("button"); headerCloseButton.className = "btn no-text btn-icon btn-flat modal-close"; headerCloseButton.style = "flex: 0;"; headerCloseButton.title = "关闭"; headerCloseButton.type = "button"; headerCloseButton.innerHTML = ''; headerCloseButton.onclick = () => { dialog.style.display = dialog.style.display === "none" ? "block" : "none"; }; header.appendChild(headerCloseButton); const content = document.createElement("div"); content.className = "d-modal__body"; content.style.cssText = "padding: 0.5rem; display: flex; flex-direction: column; height: calc(80vh - 4rem); overflow: hidden;"; const queueContainer = document.createElement("section"); queueContainer.id = "queue-container"; queueContainer.style.cssText = "flex: 1; margin-bottom: 1rem; width: 100%;"; queueContainer.innerHTML = "

Queue

"; const queueList = document.createElement("ul"); queueList.style.cssText = `height: ${uiQueueHeight}; overflow-y: auto; --scrollbarBg: transparent; --scrollbarThumbBg: var(--primary-low); --scrollbarWidth: 0.5em; scrollbar-color: rgba(0, 0, 0, 0.3) var(--scrollbarBg); margin: 1em 0 0 0.25em;`; queueContainer.appendChild(queueList); const logContainer = document.createElement("section"); logContainer.id = "log-container"; logContainer.style.cssText = "flex: 1; margin-bottom: 0; width: 100%;"; logContainer.innerHTML = "

Logs

"; const logList = document.createElement("ul"); logList.style.cssText = `height: ${uiLogHeight}; overflow-y: auto; --scrollbarBg: transparent; --scrollbarThumbBg: var(--primary-low); --scrollbarWidth: 0.5em; scrollbar-color: rgba(0, 0, 0, 0.3) var(--scrollbarBg); margin-left: 0.25em;`; logContainer.appendChild(logList); content.appendChild(queueContainer); content.appendChild(logContainer); const toggleButton = document.createElement("button"); toggleButton.title = "疯狂阅读"; toggleButton.className = "btn btn-default no-text btn-icon"; toggleButton.style.cssText = "z-index: 1001; position: fixed; bottom: 1rem; right: 1rem;"; toggleButton.innerHTML = ''; toggleButton.onclick = () => { dialog.style.display = dialog.style.display === "none" ? "block" : "none"; }; document.body.appendChild(toggleButton); dialog.appendChild(header); dialog.appendChild(content); document.body.appendChild(dialog); // 设置界面 let settingHtml = ``; layui.$("body").append(settingHtml); return { dialog, queueList, logList }; }; // 创建 tag 样式 const createTagStyle = (tagType) => { // 基础样式 const baseStatusStyle = `font-size: ${uiTagFontSize}; line-height: 1rem; padding-top: .25rem; padding-bottom: .25rem; padding-left: .5rem; padding-right: .5rem; border-radius: .25rem;`; let statusStyle; switch (tagType) { case "warning": case "pending": statusStyle = `${baseStatusStyle} color: rgba(146,64,14,1); background-color: rgba(253,230,138,1);`; break; case "info": case "processing": statusStyle = `${baseStatusStyle} color: rgba(30,64,175,1); background-color: rgba(191,219,254,1);`; break; case "success": case "completed": statusStyle = `${baseStatusStyle} color: rgba(6,95,70,1); background-color: rgba(167,243,208,1);`; break; case "error": case "retrying": case "failed": statusStyle = `${baseStatusStyle} color: rgba(160,0,1,1); background-color: rgba(255,209,209,1);`; break; default: statusStyle = `${baseStatusStyle} color: rgba(60,60,60,1); background-color: rgba(120, 120, 120,0.5);`; break; } return statusStyle; }; // 更新对话框内容 const updateDialogLog = () => { logListElement.innerHTML = logs .map((log) => { const eachStatusStyle = createTagStyle(log.level); return `
  • ${log.time} - ${log.message}${log.level}
  • `; }) .reverse() .join(""); // logListElement.scrollTop = logListElement.scrollHeight; }; const updateDialogQueue = () => { const statRow = `
  • 待执行数:${taskQueue.length}成功数:${statData.totalSuccess}失败数:${statData.totalFail}未读帖子:${windowPeriodTopics.length}阅读时间:${Math.round(statData.totalReadingTime / 60).toFixed(0)}分
  • `; queueListElement.innerHTML = statRow + taskQueue .map((task) => { const eachStatusStyle = createTagStyle(task.status); return `
  • [${task.actionType}] ${task.topicId}${task.status}
  • `; }) .join(""); // queueListElement.scrollTop = queueListElement.scrollHeight; }; // 添加日志 const addLog = (level, message) => { if (logs.length >= CONFIG.maxLogLineNum) logs.shift(); logs.push({ time: formatDate(new Date()), level, message }); updateDialogLog(); }; // 向队列中加入任务的方法 const addTask = (topicId, numbers, csrfToken, maxReadPosts, actionType) => { if (CONFIG.enableBrowseAssist) { // 检查队列中是否已存在相同的 topicId const isDuplicate = taskQueue.some(task => task.topicId === topicId) || excludeTopic.some(e => e.topicId === topicId); if (!isDuplicate) { taskQueue.push({ topicId, numbers, csrfToken, maxReadPosts, actionType, status: "pending", }); addLog("info", `任务已添加,目前队列长度:${taskQueue.length}`); updateDialogQueue(); // 如果这是队列中的第一个任务,立即开始处理 if (taskQueue.length === 1) { processQueue(); } } } }; const addInitTask = async () => { if (windowPeriodTopics.length > 0) { addLog("info", "空闲期任务开始执行"); const csrfToken = await getCsrfToken(); let indicesToRemove = []; for (const [index, windowPeriodTopicSelected] of windowPeriodTopics.entries()) { if (index > 29) { // 每次往任务队列中 最多加30个任务 break; } indicesToRemove.push(index); let windowPeriodTopicId = windowPeriodTopicSelected[0]; let windowPeriodTopicNums = Array.from( { length: windowPeriodTopicSelected[1] }, (_, i) => i + 1 ); addTask( windowPeriodTopicId, windowPeriodTopicNums, csrfToken, CONFIG.singlePostsReading, "无限阅读" ); } // 移除已经加入任务列表的未读 topic windowPeriodTopics = windowPeriodTopics.filter((_, index) => !indicesToRemove.includes(index)); if (windowPeriodTopics.length == 0) { await addWindowPeriodTopics(); } } }; // 处理队列中任务的方法 const processQueue = async () => { while (taskQueue.length > 0) { const task = taskQueue[0]; addLog("info", `正在阅读:${task.topicId}`); task.status = "processing"; updateDialogQueue(); try { const readingRes = await handleReadingPosts( task, task.topicId, task.numbers, task.csrfToken, task.maxReadPosts ); const finishTime = new Date(); const timeDiff = (finishTime - lastTaskTime) / 1000; if (readingRes.error) { statData.totalFail += 1; task.status = "failed"; addLog("error", readingRes.detail); } else { statData.totalReadingTime += Math.min(timeDiff, 60); statData.totalSuccess += 1; lastTaskTime = finishTime; task.status = "completed"; addLog("success", `任务已完成:${task.topicId}`); } } catch (err) { console.error(err); statData.totalFail += 1; task.status = "failed"; addLog("error", `处理任务时发生错误:${err.message}`); } taskQueue.shift(); updateDialogQueue(); await new Promise(resolve => setTimeout(resolve, 2000)); } }; const handleProcessTopic = async (request) => { // console.log("[LINUXDO NEXT] 这是一条帖子信息 Topic"); try { const topicData = JSON.parse(request.response); const csrfToken = await getCsrfToken(); const highestPostNumber = topicData.highest_post_number; let lastReadPostNumber; if (CONFIG.readAllPostsInTopic) { lastReadPostNumber = 1; // 强制读所有 } else { lastReadPostNumber = topicData.last_read_post_number; // 读最近的 } // 创建包含 lastReadPostNumber 到 highestPostNumber 的整数列表 let postNumbers = Array.from( { length: highestPostNumber - lastReadPostNumber + 1 }, (_, i) => i + lastReadPostNumber ); addTask( topicData.id, [...postNumbers], csrfToken, CONFIG.singlePostsReading, "主动出击" ); } catch (err) { console.log(request.response); console.log(err); addLog("error", "未知错误,请查看控制台!"); } }; const isBlankString = (str) => { return !str || str.trim() === ""; }; const extractPollString = (str) => { if (typeof str === "string") { const response = str.trim(); if (isBlankString(response)) { return []; } if (response.includes("\n|")) { const pollDataAll = response.split("\n|").map((pollData) => { if (isBlankString(pollData)) { return []; } try { return JSON.parse(pollData); } catch (err) { console.log(err); console.log(pollData); return []; } }); return pollDataAll; } else { try { return JSON.parse(response); } catch (err) { console.log(err); console.log(response); return []; } } } else { console.log(str); return []; } }; const handleProcessPoll = (request) => { console.log("[LinuxDo Assist] 这是一条拉取消息 poll"); console.log(typeof request.response); if (!!request.response) { const extractedData = extractPollString(request.response); console.log(extractedData); } else { addLog("error", "哦豁,Poll(扑)了个空"); } }; const clearExcludeTopicArray = () => { if (excludeTopic.length > 0) { // 5分钟之前的时间戳 let fiveMinutesAgo = Math.floor(Date.now() / 1000) - 5 * 60; excludeTopic = excludeTopic.filter(item => item.readTime > fiveMinutesAgo); addLog('success', '清理5分钟之前阅读过的 Topic'); } }; const addWindowPeriodTopics = async () => { if (CONFIG.enableBrowseAssist && CONFIG.enableWindowPeriodRead) { let page = 0; let maxPage = 9; // 最多获取10页未读列表 if (CONFIG.windowPeriodTopicUrls.length === 0) { CONFIG.windowPeriodTopicUrls.push(`https://linux.do/unread.json`); } const csrfToken = await getCsrfToken(); for (let topicUrl of CONFIG.windowPeriodTopicUrls) { addLog('info', `开始获取[${topicUrl}]的帖子`); let unreadTopic = await fetchUnreadTopic(csrfToken, topicUrl, page); handleUnreadTopic(unreadTopic); while (true) { page++; if (unreadTopic.more_topics_url) { unreadTopic = await fetchUnreadTopic(csrfToken, topicUrl, page); handleUnreadTopic(unreadTopic); // 生成随机整数 randSleepTime,范围在 1000ms 到 1500ms 之间 const randSleepTime = getRandomInt(1000, 1500); // 睡眠时间 await new Promise((resolve) => setTimeout(resolve, randSleepTime)); } else { break; } if (page >= maxPage) { // 若未读列表大于10页则仅获取10页 break; } } addLog('info', `获取[${topicUrl}]的帖子完成`); } addLog('info', `将未读Topic加入空闲阅读的列表,数量[${windowPeriodTopics.length}]`); if (windowPeriodTopics.length === 0) { const randSleepTime = getRandomInt(60000, 65000); addLog('info', `睡眠 ${Math.floor(randSleepTime / 1000)} s 后重新获取未读Topic`); // 睡眠时间, 范围在 60000ms 到 65000ms 之间 await new Promise((resolve) => setTimeout(resolve, randSleepTime)); if (CONFIG.enableBrowseAssist && CONFIG.enableWindowPeriodRead && windowPeriodTopics.length === 0) { addWindowPeriodTopics(); } } updateDialogQueue(); } }; const handleUnreadTopic = (unreadTopic) => { for (let item of unreadTopic.topics) { // if (item.unread_posts > 0) { windowPeriodTopics.push([item.id, item.highest_post_number]); // } } }; const fetchUnreadTopic = async (csrfToken, topicUrl, page) => { let response = await fetchGetJson({ url: `${topicUrl}?page=${page}`, csrfToken, }); if (response.status === 200) { let jsonData = response.detail; return jsonData.topic_list || {}; } else { throw new Error(`Unexpected status: ${response.status}, message: ${response.detail}`); } }; /** * 基础 get 请求 * * @param {{url: string; csrfToken?: string; otherHeaders?: any;}} props 请求参数 * @returns {Promise<{status: number; error: boolean; detail: any[]}>} */ const fetchGetJson = async (props) => { let retryTimes = 0; while (retryTimes <= CONFIG.maxRetryTimes) { try { const response = await fetch(props.url, { headers: { ...(props.csrfToken && { "x-csrf-token": props.csrfToken, }), ...(props.otherHeaders && { ...props.otherHeaders }), }, body: null, method: props.method || 'GET', mode: "cors", credentials: "include", }); if (response.status >= 200 && response.status < 300) { const data = await response.json(); return { status: response.status, error: false, detail: data, }; } else if (response.status >= 400 && response.status < 600) { // 在 2-5 秒之间随机等待 await new Promise(resolve => setTimeout(resolve, getRandomInt(2000, 5000))); retryTimes++; console.warn(`fetchGetJson: ${genErrMsg(response.status)}`); continue; } else { return { status: response.status, error: true, detail: "未知错误" }; } } catch (error) { // 在 2-5 秒之间随机等待 await new Promise(resolve => setTimeout(resolve, getRandomInt(2000, 5000))); retryTimes++; continue; } } if (retryTimes > CONFIG.maxRetryTimes) { return { status: 500, error: true, detail: "超过最大重试次数" }; } }; const genErrMsg = (statusCode) => { switch (statusCode) { case 403: return '无权限查看'; case 429: return '频率限制'; case 500: return '服务器内部错误'; case 503: return '服务器尚未处于可以接受请求的状态'; default: return '请求失败'; } }; function enableXMLHttpRequestHooks() { XMLHttpRequest.prototype.open = function (method, url, async, user, password) { // 拦截open this._custom_storage = { method, url }; return nativeXMLHttpRequestOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (data) { this.addEventListener( "readystatechange", function () { // 拦截 response if (this.readyState === 4) { // 判断 topic if ( isTopicUrl(this._custom_storage.url) && this._custom_storage.method === "GET" ) { handleProcessTopic(this); } // // 判断 timings // if ( // isTimingsUrl(this._custom_storage.url) && // this._custom_storage.method === "POST" // ) { // console.log("[LINUXDO NEXT] 这是一条阅读计时 timings"); // console.log(this); // } // 判断 poll // if ( // isPollUrl(this._custom_storage.url) && // this._custom_storage.method === "POST" // ) { // handleProcessPoll(this); // } } }, false ); return nativeXMLHttpRequestSend.apply(this, arguments); }; } function disableXMLHttpRequestHooks() { XMLHttpRequest.prototype.open = nativeXMLHttpRequestOpen; XMLHttpRequest.prototype.send = nativeXMLHttpRequestSend; } const helperStart = () => { enableXMLHttpRequestHooks(); addLog('success', CONFIG.enableBrowseAssist ? '助手已开启' : '助手已关闭'); addLog('success', CONFIG.enableWindowPeriodRead ? '空闲阅读已开启' : '空闲阅读已关闭'); addWindowPeriodTopics(); }; const helperStop = () => { disableXMLHttpRequestHooks(); if (taskQueue.length > 1) { addLog('warning', '正在删除多余任务,仅保留最后进行的任务'); taskQueue = taskQueue.slice(0, 1); } if (windowPeriodTopics.length > 0) { addLog('warning', '正在删除空闲阅读列表中的任务'); windowPeriodTopics = []; } addLog('error', '助手已停止'); } const init = () => { nativeXMLHttpRequestOpen = ensureNativeMethods( XMLHttpRequest.prototype.open ); nativeXMLHttpRequestSend = ensureNativeMethods( XMLHttpRequest.prototype.send ); const uiElements = createDialog(); dialogElement = uiElements.dialog; queueListElement = uiElements.queueList; logListElement = uiElements.logList; // 获取缓存中的配置 let username = getUsername(); let configCache = GM_getValue(`${username}_eliminate_blue_dot_config`, ""); if (configCache.length > 0) { CONFIG = JSON.parse(configCache); } // 初始化 空闲任务, 定时执行 // 生成随机整数 randSleepTime,范围在 10000ms 到 15000ms 之间 const randSleepTime = getRandomInt(10000, 15000); setInterval(() => { if (CONFIG.enableBrowseAssist && CONFIG.enableWindowPeriodRead && taskQueue.length == 0) { addInitTask(); } }, randSleepTime); // 定时清理 排除topic 数组, 每5分钟调用一次 setInterval(clearExcludeTopicArray, 5 * 60 * 1000); } // 初始化 init(); // 开始 helperStart(); })();