您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动折叠 Discourse 论坛中的无意义回复
当前为
// ==UserScript== // @name Discourse 无意义回复折叠器 // @namespace http://tampermonkey.net/ // @version 0.1 // @description 自动折叠 Discourse 论坛中的无意义回复 // @author 量子咸鱼K // @match *://linux.do/t/topic/* // @grant GM_log // @run-at document-end // @license MIT // ==/UserScript== // 立即执行的日志,确认脚本加载 console.log('[折叠器] 脚本已加载'); (function() { 'use strict'; // 添加日志函数 const DEBUG = { enabled: true, log: function(type, message, data = null) { if (!this.enabled) return; const timestamp = new Date().toISOString().split('T')[1]; console.log(`[折叠器][${timestamp}][${type}] ${message}`, data ? data : ''); } }; // 立即执行的测试日志 DEBUG.log('测试', '日志系统初始化成功'); // 配置项 const CONFIG = { // 判定为无意义回复的最大字符数 MAX_CHARS: 30, // 连续显示的最大回复数 MAX_VISIBLE_REPLIES: 8, // 用于判定无意义回复的关键词和正则表达式 MEANINGLESS_PATTERNS: [ // 基础表情和重复字符 /^.*[。.…~~]+$/, // 省略号 /^.*[哈嘿呵h]{2,}$/i, // 笑声 /^.*[6666]{2,}$/, // 666 /^.*[??!!.。]{2,}$/, // 连续的标点符号 /^.*[::][++]1[::]$/, // :+1: /^.*(\s*:[\w-]+:\s*){1,}$/, // 纯表情符号 // 单字重复 /^.*(.)\1{1,}$/, // 任何字符重复 // 感谢类 /^.*[谢感]谢?(分享|大佬|楼主|老铁|老哥|佬友?|大神|博主)?[!!~~。.]*$/, /^.*感恩|感动|感激[!!~~。.]*$/, /^.*(thank|thanks|thx|tks)[!!~~。.]*$/i, // 支持类 /^.*(支持|顶|赞|好评|mark占?位?|收藏|马克|签到|打卡|学习|关注|收藏了|路过|前来|学习了)[!!~~。.]*$/i, /^.*(\+1|1\+|加1|[➕+]1)[!!~~。.]*$/, /^.*先赞后看[!!~~。.]*$/, /^.*已阅[!!~~。.]*$/, /^.*非常好用[!!~~。.]*$/, /^.*好用[,,]?爱用[!!~~。.]*$/, /^.*爱用[,,]?喜欢[!!~~。.]*$/, /^.*火钳威武[!!~~。.]*$/, // 称赞类 /^.*(好|棒|强|厉害|可以|不错|牛|帅|赞|妙|秒|绝|狠|太强|很强|太棒|很棒|牛逼|nb|可以的)[!!~~。.]*$/i, /^.*(nice|good|perfect|awesome|ok+)[!!~~。.]*$/i, /^.*[牛nb]{1,}[bB呀啊哇plus]{0,5}$/, // 牛b,nbbb,牛逼plus等 /^.*牛啊?皇[!!~~。.]*$/, // 楼层相关 /^.*[第前后大小]?[1-9一二三四五六七八九十百千]{1,}[楼层名]?[!!~~。.]*$/, /^.*(前排|沙发|板凳|地板)[!!~~。.]*$/, /^.*[大小]?后排[!!~~。.]*$/, /^.*排队[!!~~。.]*$/, /^.*[前后][排队][!!~~。.]*$/, // 佬相关 /^.*(佬|大佬|巨佬|巨巨|大神)[!!~~。.]*$/, /^.*佬(的)?分享[!!~~。.]*$/, /^.*始皇(大佬|陛下|老师|[vV][1-9])?[!!~~。.]*$/, /^.*吾皇[万岁]{2,}$/, /^.*伟大[~~]*[,,]?无需多[盐言][!!~~。.]*$/, // 其他常见短语 /^.*(顶上去|顶上来|顶一下|帮顶|支持一下|学习了|学到了|受益了|get|学习打卡)[!!~~。.]*$/i, /^.*(看看|路过|潜水|冒泡|打卡|签到|留念|留名)[!!~~。.]*$/, /^.*[1-9一二三四五六七八九十]\s*[份分]到手[!!~~。.]*$/, /^.*别说话[!!~~。.]*$/, /^.*前排[!!~~。.]*爽[~~]*$/, /^.*前排[!!~~。.]*始皇[牛nb逼]{1,}[!!~~。.]*(破音)$/, // 表情符号组合 /^.*(:[++]1:\s*){1,}$/, // 连续的 :+1: 表情 /^.*[::][^\s]{1,10}[::](\s*[::][^\s]{1,10}[::])*$/, // 任意表情符号组合 // Custom "来了","太强","哈哈哈","红红火火","牛啊","好好好","重生了","来啦","cy","插眼","mark","Mark","tql","始皇" ] }; // 判断是否为无意义回复 function isMeaninglessReply(content) { const cleanContent = content.replace(/\s+/g, ''); if (cleanContent.length <= CONFIG.MAX_CHARS) { const matchedPattern = CONFIG.MEANINGLESS_PATTERNS.find(pattern => { if (pattern instanceof RegExp) { return pattern.test(cleanContent); } else { return cleanContent.toLowerCase().includes(pattern.toLowerCase()); } }); if (matchedPattern) { DEBUG.log('检测', `发现无意义回复: "${content}" (匹配模式: ${matchedPattern})`); return true; } } return false; } // 创建折叠后的回复元素 function createFoldedReply(post) { try { const userInfo = post.querySelector('.topic-meta-data'); if (!userInfo) { DEBUG.log('错误', '未找到用户信息区域'); return null; } const username = userInfo.querySelector('.username'); const postNumber = userInfo.querySelector('.post-number, .linuxfloor'); const cookedContent = post.querySelector('.cooked'); let author; if (!username || !cookedContent) { author = userInfo.querySelector('.full-name').childNodes[0].getAttribute('data-user-card'); //console.log(username,cookedContent,userInfo,author); }else{ author = username.textContent; } const content = cookedContent.textContent.trim(); const number = postNumber ? postNumber.textContent : ''; DEBUG.log('创建', `创建折叠元素: #${number} ${author}`); const foldedDiv = document.createElement('div'); foldedDiv.className = 'folded-reply'; foldedDiv.innerHTML = ` ${number ? `<span class="folded-post-number">${number}</span>` : ''} <span class="folded-author">${author}</span>: <span class="folded-content">${content}</span> `; foldedDiv.style.cssText = ` padding: 5px 15px; margin: 5px 0; background-color: var(--primary-very-low); border-radius: 4px; font-size: 0.9em; cursor: pointer; display: flex; align-items: center; gap: 8px; `; foldedDiv.addEventListener('click', () => { DEBUG.log('点击', `展开回复: #${number}`); post.style.display = ''; foldedDiv.style.display = 'none'; }); return foldedDiv; } catch (error) { DEBUG.log('错误', '创建折叠元素失败', error); return null; } } // 处理连续的无意义回复 function handleConsecutiveMeaninglessReplies(replies) { let currentIndex = 0; let consecutiveGroups = []; let currentGroup = []; // 首先找出所有连续的回复组 for (let i = 0; i < replies.length; i++) { if (currentGroup.length === 0) { currentGroup.push(replies[i]); } else { const lastPost = currentGroup[currentGroup.length - 1].post; const currentPost = replies[i].post; // 检查是否连续(通过比较帖子编号) const lastNumber = parseInt(lastPost.querySelector('.post-number, .linuxfloor')?.textContent?.replace(/[^0-9]/g, '')); const currentNumber = parseInt(currentPost.querySelector('.post-number, .linuxfloor')?.textContent?.replace(/[^0-9]/g, '')); if (lastNumber && currentNumber && currentNumber === lastNumber + 1) { currentGroup.push(replies[i]); } else { if (currentGroup.length > CONFIG.MAX_VISIBLE_REPLIES) { consecutiveGroups.push([...currentGroup]); } currentGroup = [replies[i]]; } } } // 处理最后一组 if (currentGroup.length > CONFIG.MAX_VISIBLE_REPLIES) { consecutiveGroups.push(currentGroup); } // 处理每一组连续回复 consecutiveGroups.forEach(group => { DEBUG.log('处理', `发现连续回复组: 数量=${group.length}`); // 显示前 MAX_VISIBLE_REPLIES 个回复 for (let i = 0; i < CONFIG.MAX_VISIBLE_REPLIES; i++) { if (group[i]) { group[i].foldedReply.style.display = ''; } } // 隐藏剩余的回复 for (let i = CONFIG.MAX_VISIBLE_REPLIES; i < group.length; i++) { group[i].foldedReply.style.display = 'none'; } // 创建省略号元素 const ellipsis = document.createElement('div'); ellipsis.className = 'replies-ellipsis'; ellipsis.innerHTML = ` <span>还有 ${group.length - CONFIG.MAX_VISIBLE_REPLIES} 条类似回复</span> <span class="show-more">点击展开</span> `; ellipsis.style.cssText = ` text-align: center; padding: 8px; color: var(--primary-medium); cursor: pointer; margin: 5px 0; background-color: var(--primary-very-low); border-radius: 4px; font-size: 0.9em; `; // 插入省略号到最后一个可见回复之后 const lastVisibleReply = group[CONFIG.MAX_VISIBLE_REPLIES - 1].foldedReply; if (lastVisibleReply) { lastVisibleReply.parentNode.insertBefore(ellipsis, lastVisibleReply.nextSibling); DEBUG.log('插入', '插入省略号元素'); } // 点击省略号时展开所有回复 ellipsis.addEventListener('click', () => { DEBUG.log('展开', '展开连续回复'); for (let i = CONFIG.MAX_VISIBLE_REPLIES; i < group.length; i++) { group[i].foldedReply.style.display = ''; } ellipsis.style.display = 'none'; }); }); } // 主函数 function foldMeaninglessReplies() { DEBUG.log('执行', '开始处理帖子'); // 移除已存在的折叠元素 document.querySelectorAll('.folded-reply, .replies-ellipsis').forEach(el => el.remove()); const posts = Array.from(document.querySelectorAll('.post-stream article.boxed.onscreen-post')).slice(1); DEBUG.log('统计', `找到 ${posts.length} 个回复帖子`); const meaninglessReplies = []; posts.forEach(post => { try { const content = post.querySelector('.cooked')?.textContent.trim(); if (!content) { DEBUG.log('跳过', '帖子内容为空'); return; } if (isMeaninglessReply(content)) { const foldedReply = createFoldedReply(post); if (foldedReply) { post.parentNode.insertBefore(foldedReply, post); post.style.display = 'none'; meaninglessReplies.push({post, foldedReply}); } } } catch (error) { DEBUG.log('错误', '处理帖子时发生错误', error); } }); DEBUG.log('统计', `本次共折叠 ${meaninglessReplies.length} 个回复`); if (meaninglessReplies.length > 0) { handleConsecutiveMeaninglessReplies(meaninglessReplies); } } // 添加样式 const style = document.createElement('style'); style.textContent = ` .folded-reply { transition: background-color 0.2s; } .folded-reply:hover { background-color: var(--primary-low); } .folded-post-number { color: var(--primary-medium); font-size: 0.8em; min-width: 2em; } .folded-author { font-weight: bold; color: var(--primary-high); } .folded-content { color: var(--primary-medium); } .replies-ellipsis .show-more { color: var(--tertiary); margin-left: 5px; } .replies-ellipsis:hover { background-color: var(--primary-low); } `; document.head.appendChild(style); // 使用防抖函数来避免频繁触发 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 检查页面是否已完全加载 function isPageFullyLoaded() { // 检查 Discourse 应用是否已加载 if (typeof define !== 'function' || typeof require !== 'function') { DEBUG.log('加载检查', 'AMD 模块系统未加载'); return false; } // 检查 post-stream 组件是否已加载 const postStream = document.querySelector('#post-stream'); if (!postStream) { DEBUG.log('加载检查', 'post-stream 元素未找到'); return false; } // 检查是否有加载状态 const loadingPosts = postStream.querySelector('.loading-container, .timeline-loading, .loading-onebox'); if (loadingPosts) { DEBUG.log('加载检查', '帖子正在加载中'); return false; } // 检查是否有可见的帖子 const visiblePosts = postStream.querySelectorAll('article.topic-post:not(.placeholder)'); if (visiblePosts.length === 0) { DEBUG.log('加载检查', '没有可见的帖子'); return false; } DEBUG.log('加载检查', `页面加载完成 (可见帖子数: ${visiblePosts.length})`); return true; } // 等待 Discourse 应用加载 function waitForDiscourse() { return new Promise((resolve) => { const maxAttempts = 200; // 增加等待时间到 20 秒 let attempts = 0; function check() { attempts++; // 检查 Discourse 应用是否已加载 const appLoaded = typeof define === 'function' && typeof require === 'function'; const postStreamLoaded = document.querySelector('#post-stream article.topic-post'); const loadingIndicator = document.querySelector('#post-stream .loading-container'); // 检查 TopicController 是否已初始化 const topicControllerLoaded = window.require && (() => { try { const container = window.require('discourse/app').default.__container__; const controller = container.lookup('controller:topic'); return controller && controller.model && controller.model.postStream; } catch (e) { return false; } })(); if (appLoaded && postStreamLoaded && !loadingIndicator && topicControllerLoaded) { // 额外等待一小段时间,确保内容完全加载 setTimeout(() => { DEBUG.log('等待', 'Discourse 应用已加载,帖子已就绪'); resolve(); }, 1000); return; } if (attempts >= maxAttempts) { DEBUG.log('等待', '等待超时,将在路由变化时重试'); resolve(); return; } setTimeout(check, 100); } // 如果页面已经加载完成,立即开始检查 if (document.readyState === 'complete') { check(); } else { // 否则等待页面加载完成 window.addEventListener('load', check); } }); } // 监听路由变化 function setupRouteObserver() { let lastUrl = location.href; let isProcessing = false; // 创建一个 MutationObserver 来监视 URL 变化 const observer = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; if (isProcessing) return; isProcessing = true; DEBUG.log('路由', '检测到页面 URL 变化'); // 等待新页面加载完成 setTimeout(() => { if (window.requestIdleCallback) { requestIdleCallback(() => { DEBUG.log('执行', '页面变化后开始折叠'); foldMeaninglessReplies(); isProcessing = false; }); } else { setTimeout(() => { DEBUG.log('执行', '页面变化后开始折叠'); foldMeaninglessReplies(); isProcessing = false; }, 1000); } }, 1000); } }); observer.observe(document, { subtree: true, childList: true }); // 监听 popstate 事件(浏览器前进/后退) window.addEventListener('popstate', () => { if (isProcessing) return; isProcessing = true; DEBUG.log('路由', '检测到 popstate 事件'); waitForDiscourse().then(() => { if (window.requestIdleCallback) { requestIdleCallback(() => { DEBUG.log('执行', 'popstate 后开始折叠'); foldMeaninglessReplies(); isProcessing = false; }); } else { setTimeout(() => { DEBUG.log('执行', 'popstate 后开始折叠'); foldMeaninglessReplies(); isProcessing = false; }, 1000); } }); }); } // 设置定时器 function setupAutoFold() { DEBUG.log('定时', '启动自动折叠定时器'); // 创建定时器 const timer = setInterval(() => { const postStream = document.querySelector('#post-stream'); if (!postStream) return; const loadingContainer = document.querySelector('#post-stream .loading-container'); if (loadingContainer) return; DEBUG.log('定时', '执行定时折叠检查'); foldMeaninglessReplies(); }, 5000); // 在页面卸载时清除定时器 window.addEventListener('unload', () => { clearInterval(timer); }); return timer; } // 初始化函数 async function initialize() { try { DEBUG.log('初始化', '脚本开始运行'); // 等待 Discourse 应用加载 // await waitForDiscourse(); // 设置路由观察器 setupRouteObserver(); // 设置自动折叠定时器 const timer = setupAutoFold(); // 使用 requestIdleCallback 在浏览器空闲时执行折叠操作 if (window.requestIdleCallback) { requestIdleCallback(() => { DEBUG.log('执行', '开始初始折叠'); foldMeaninglessReplies(); // 设置 MutationObserver setupObserver(); }); } else { // 如果不支持 requestIdleCallback,则延迟执行 setTimeout(() => { DEBUG.log('执行', '开始初始折叠'); foldMeaninglessReplies(); // 设置 MutationObserver setupObserver(); }, 1000); } } catch (error) { DEBUG.log('错误', '初始化失败', error); console.error('折叠脚本初始化失败:', error); setTimeout(initialize, 5000); } } // 设置 MutationObserver function setupObserver() { const postStream = document.querySelector('#post-stream'); if (!postStream) return; DEBUG.log('监听', '开始监听帖子流变化'); const observer = new MutationObserver(debounce((mutations) => { const hasNewPosts = mutations.some(mutation => { return Array.from(mutation.addedNodes).some(node => node.nodeType === 1 && ( node.classList?.contains('topic-post') || node.querySelector?.('.topic-post') ) ); }); const loadingContainer = document.querySelector('#post-stream .loading-container'); if (hasNewPosts && !loadingContainer) { // 等待一小段时间确保新帖子完全加载 setTimeout(() => { if (!document.querySelector('#post-stream .loading-container')) { DEBUG.log('观察器', '发现新帖子,开始处理'); foldMeaninglessReplies(); } }, 500); } }, 200)); observer.observe(postStream, { childList: true, subtree: true }); // 监听滚动事件 window.addEventListener('scroll', debounce(() => { const loadingContainer = document.querySelector('#post-stream .loading-container'); if (loadingContainer) return; const posts = postStream.querySelectorAll('article.topic-post:not(.placeholder)'); const lastPost = posts[posts.length - 1]; if (!lastPost) return; const rect = lastPost.getBoundingClientRect(); if (rect.bottom <= window.innerHeight * 2) { // 等待一小段时间确保新帖子加载完成 setTimeout(() => { if (!document.querySelector('#post-stream .loading-container')) { DEBUG.log('滚动', '接近底部,检查新帖子'); foldMeaninglessReplies(); } }, 500); } }, 200), { passive: true }); } // 启动脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1000)); } else { setTimeout(initialize, 1000); } })();