A7urT6s9dmV3pR8
10-03 14:09
45 楼
@NeiNei #9 发布于2026/10/3 10:57:59
@lianssss9 #4 脚本发下
// ==UserScript==
// @name ✅ NodeSeek 💡 高亮 & ❌ 屏蔽
// @namespace https://github.com/you/nodeseek-hotkey
// @version 260907
// @description 高亮和屏蔽 nodeseek.com 论坛帖子标题及用户回复
// @author Claude 首发于 260907
// @match https://www.nodeseek.com/*
// @icon https://www.nodeseek.com/static/image/favicon/favicon-32x32.png
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const config = {
// ❌ 屏蔽关键词(不区分大小写)- 空格分隔
blockKeywords: 'Vmiss DeepSeek 鹈鹕 国模 意见稿',
// ✅ 高亮关键词(不区分大小写)- 空格分隔
highlightKeywords: '免费 白嫖 羊毛 福利 限免 开源 开放 公益 赠送 送出 赠出 邀请码',
highlightColor: '#2ea44f',
// ❌ 屏蔽用户黑名单(不区分大小写)- 空格分隔
blockUsers: '',
// ✅ 高亮用户名单(不区分大小写)- 空格分隔
highlightUsers: '',
highlightUserColor: '#FFD700',
// ❌ 屏蔽正则
blockRegexPatterns: [
/第.{0,3}期/i,
/招.{0,4}(人|员|募)/i,
/(问|请问).{0,10}(哪|怎|如何|能不能|可以吗)/i,
/分享.{0,5}(经验|心得|体验|感受)/i,
],
};
// ── 解析关键词 ──
function parseKws(str) {
return str.trim().split(/\s+/).filter(k => k.length > 0);
}
const highlightKwArr = parseKws(config.highlightKeywords);
const blockKwArr = parseKws(config.blockKeywords);
const blockUsersArr = parseKws(config.blockUsers).map(u => u.toLowerCase());
const highlightUsersArr = parseKws(config.highlightUsers).map(u => u.toLowerCase());
// ── 注入样式 ──
function injectStyles() {
const id = 'ns-filter-style';
document.getElementById(id)?.remove();
const style = document.createElement('style');
style.id = id;
style.textContent = `
.ns-user-hl {
background-color: ${config.highlightUserColor} !important;
color: #333 !important;
padding: 1px 4px !important;
border-radius: 4px !important;
font-weight: bold !important;
}
`;
document.head.appendChild(style);
}
injectStyles();
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', injectStyles);
window.addEventListener('load', injectStyles);
// ── 判断函数 ──
function shouldBlock(title) {
const low = title.toLowerCase();
if (blockKwArr.some(k => low.includes(k.toLowerCase()))) return true;
return config.blockRegexPatterns.some(r => r.test(title));
}
function shouldHighlight(title) {
const low = title.toLowerCase();
return highlightKwArr.some(k => low.includes(k.toLowerCase()));
}
function isBlockedUser(name) {
if (!name) return false;
return blockUsersArr.includes(name.toLowerCase());
}
function isHighlightUser(name) {
if (!name) return false;
return highlightUsersArr.includes(name.toLowerCase());
}
// ── 关键词高亮(标题内嵌 span)──
function highlightKwInTitle(el, text) {
const escaped = highlightKwArr.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const regex = new RegExp(`(${escaped.join('|')})`, 'gi');
el.innerHTML = text.replace(regex, match =>
`<span style="background:${config.highlightColor};color:#fff;padding:1px 3px;border-radius:3px;font-weight:bold">${match}</span>`
);
}
// ── 处理列表帖子行 ──
// NodeSeek DOM:
// li.post-list-item
// .post-title > a ← 标题链接
// .avatar-normal[data-uid] ← 发帖人头像(data-uid)
// .avatar-normal img[title]← 发帖人名(部分版本)
function processRow(row) {
if (row.dataset.nsFiltered) return;
row.dataset.nsFiltered = '1';
const titleLink = row.querySelector('.post-title > a');
if (!titleLink) return;
const title = titleLink.textContent.trim();
// 取发帖人:优先用头像 data-uid 旁边的用户信息,
// NodeSeek 列表的作者名藏在 .post-meta .author a 或 .avatar img[title]
const authorEl =
row.querySelector('.post-meta .author a') ||
row.querySelector('.post-meta a[href*="/space/"]') ||
row.querySelector('.nsk-post-meta a[href*="/space/"]');
const author = authorEl ? authorEl.textContent.trim() : '';
// 1. 屏蔽用户
if (author && isBlockedUser(author)) {
row.style.display = 'none'; return;
}
// 2. 屏蔽关键词
if (shouldBlock(title)) {
row.style.display = 'none'; return;
}
// 3. 高亮用户(整行标题金色底)
if (author && isHighlightUser(author)) {
titleLink.classList.add('ns-user-hl'); return;
}
// 4. 高亮关键词
if (shouldHighlight(title)) {
highlightKwInTitle(titleLink, title);
}
}
// ── 处理帖子详情页回复楼层 ──
// NodeSeek 帖子页 DOM:
// .content-item
// .nsk-content-meta-info .author-info > a ← 楼层作者
function processReply(item) {
if (item.dataset.nsFiltered) return;
// 跳过1楼(帖主)
const floorNum = item.dataset.index || item.dataset.floor;
if (floorNum === '0' || floorNum === '1') { item.dataset.nsFiltered = '1'; return; }
const authorEl = item.querySelector('.nsk-content-meta-info .author-info > a') ||
item.querySelector('.author-info > a');
const author = authorEl ? authorEl.textContent.trim() : '';
item.dataset.nsFiltered = '1';
if (author && isBlockedUser(author)) {
item.remove();
}
}
// ── 批量处理 ──
function processAll() {
document.querySelectorAll('li.post-list-item').forEach(processRow);
document.querySelectorAll('.content-item').forEach(processReply);
}
processAll();
// ── MutationObserver 监听动态加载 ──
const observer = new MutationObserver(mutations => {
let needList = false, needReply = false;
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.matches?.('li.post-list-item') || node.querySelector?.('li.post-list-item')) needList = true;
if (node.matches?.('.content-item') || node.querySelector?.('.content-item')) needReply = true;
}
if (m.type === 'childList') {
const p = m.target.closest?.('li.post-list-item');
if (p) needList = true;
const r = m.target.closest?.('.content-item');
if (r) needReply = true;
}
}
if (needList) document.querySelectorAll('li.post-list-item').forEach(processRow);
if (needReply) document.querySelectorAll('.content-item').forEach(processReply);
});
observer.observe(document.body, {
childList: true, subtree: true
});
// ── 滚动(下拉无限加载)──
let scrollT;
window.addEventListener('scroll', () => {
clearTimeout(scrollT);
scrollT = setTimeout(processAll, 300);
}, { passive: true });
// ── SPA 路由切换 ──
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(processAll, 500);
}
}).observe(document.body, { childList: true, subtree: true });
console.log('[NS-Filter v1.0] 已启动 | 高亮词:', highlightKwArr.length, '| 屏蔽词:', blockKwArr.length);
})();