NodeSeek 增强助手
更新V2.0.1 高效简洁的 NodeSeek 论坛增强油猴脚本。
✅ 删除鸡腿排行榜 - 完全删除排行榜相关代码
✅ 仅索引标题 - 交易和抽奖只根据标题内容判断,不索引帖子内容
✅ 各显示5个 - 交易和抽奖各显示最新5条
✅ 已浏览高亮 - 点击过的帖子显示✓已看标记,背景变灰
自动保存30天浏览记录避免重复查看 ✅ 中奖提醒 - 自动追踪参与的抽奖
监控用户抽奖帖的评论每10分钟检查开奖状态中奖时弹出桌面通知 ✅ 开奖时间显示 - 从标题提取开奖时间
显示在抽奖帖下方(⏰图标)支持多种时间格式
安装方法
安装Tampermonkey浏览器扩展
点击安装脚本:nodeseek-auto-checkin.user.js
访问NodeSeek论坛即可自动生效
项目地址:https://github.com/weiruankeji2025/weiruan-nodeseek-EnhancedAssistant
ps:鸡腿目前没有数据,稍后优化试试
// ==UserScript==
// @name NodeSeek 增强助手
// @namespace https://github.com/weiruankeji2025/weiruan-nodeseek-Sign.in
// @version 2.0.1
// @description NodeSeek论坛增强:自动签到 + 交易监控 + 抽奖追踪 + 中奖提醒
// @author weiruankeji2025
// @match https://www.nodeseek.com/*
// @icon https://www.nodeseek.com/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// @grant GM_addStyle
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ==================== 配置 ====================
const CONFIG = {
API_URL: 'https://www.nodeseek.com/api/attendance',
TRADE_URL: 'https://www.nodeseek.com/categories/trade',
HOME_URL: 'https://www.nodeseek.com/',
STORAGE_KEY: 'ns_last_checkin',
VISITED_KEY: 'ns_visited_posts',
WIN_CHECK_KEY: 'ns_win_check',
RANDOM_MODE: true,
TRADE_COUNT: 5,
LOTTERY_COUNT: 5,
WIN_CHECK_INTERVAL: 10 * 60 * 1000 // 10分钟检查一次中奖
};
// ==================== 样式注入 ====================
GM_addStyle(`
.ns-sidebar {
position: fixed;
right: 10px;
top: 70px;
width: 220px;
max-height: calc(100vh - 90px);
overflow-y: auto;
z-index: 9998;
display: flex;
flex-direction: column;
gap: 8px;
scrollbar-width: thin;
}
.ns-sidebar::-webkit-scrollbar { width: 4px; }
.ns-sidebar::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }
.ns-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 6px rgba(0,0,0,0.1);
overflow: hidden;
font-size: 12px;
}
.ns-card-header {
padding: 8px 10px;
font-weight: 600;
font-size: 12px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
}
.ns-card-toggle { opacity: 0.7; font-size: 11px; }
.ns-card.collapsed .ns-card-body { display: none; }
.ns-card.trade .ns-card-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; }
.ns-card.lottery .ns-card-header { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: #fff; }
.ns-item {
padding: 6px 10px;
border-bottom: 1px solid #f0f0f0;
transition: background 0.15s;
}
.ns-item:last-child { border-bottom: none; }
.ns-item:hover { background: #f8f9fa; }
.ns-item a {
color: #333;
text-decoration: none;
display: flex;
flex-direction: column;
gap: 3px;
line-height: 1.3;
font-size: 11px;
}
.ns-item a:hover { color: #1890ff; }
/* 已浏览样式 */
.ns-item.visited { background: #f5f5f5; opacity: 0.7; }
.ns-item.visited a { color: #999; }
.ns-item.visited .ns-tag { opacity: 0.6; }
.ns-visited-mark { font-size: 9px; color: #52c41a; margin-left: 4px; }
.ns-item-row {
display: flex;
align-items: center;
gap: 5px;
}
.ns-tag {
flex-shrink: 0;
padding: 1px 4px;
font-size: 9px;
border-radius: 2px;
color: #fff;
font-weight: 500;
}
.ns-tag.sell { background: #ff7875; }
.ns-tag.buy { background: #40a9ff; }
.ns-tag.lottery { background: #73d13d; }
.ns-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 开奖时间样式 */
.ns-lottery-time {
font-size: 9px;
color: #fa8c16;
padding-left: 24px;
}
.ns-empty { text-align: center; padding: 15px 10px; color: #999; font-size: 11px; }
.ns-loading { color: #1890ff; }
@media (prefers-color-scheme: dark) {
.ns-card { background: #242424; box-shadow: 0 1px 6px rgba(0,0,0,0.3); }
.ns-item { border-color: #333; }
.ns-item:hover { background: #2d2d2d; }
.ns-item a { color: #e0e0e0; }
.ns-item.visited { background: #1a1a1a; }
.ns-item.visited a { color: #666; }
.ns-empty { color: #666; }
.ns-lottery-time { color: #d48806; }
}
@media (max-width: 1400px) { .ns-sidebar { display: none; } }
`);
// ==================== 工具函数 ====================
const getToday = () => new Date().toISOString().slice(0, 10);
const hasCheckedIn = () => GM_getValue(CONFIG.STORAGE_KEY) === getToday();
const notify = (title, text, onclick) => {
GM_notification({ title, text, timeout: 5000, onclick });
console.log(`[NS助手] ${title}: ${text}`);
};
const extractPostId = (url) => url?.match(/\/post-(\d+)/)?.[1];
const truncate = (str, len) => {
if (!str) return '';
str = str.trim();
return str.length > len ? str.slice(0, len) + '…' : str;
};
const escapeHtml = (str) => {
if (!str) return '';
return str.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
};
// ==================== 已浏览帖子管理 ====================
const getVisitedPosts = () => {
try {
return GM_getValue(CONFIG.VISITED_KEY) || {};
} catch {
return {};
}
};
const markAsVisited = (postId) => {
const visited = getVisitedPosts();
visited[postId] = Date.now();
// 只保留最近30天的记录
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
for (const id in visited) {
if (visited[id] < cutoff) delete visited[id];
}
GM_setValue(CONFIG.VISITED_KEY, visited);
};
const isVisited = (postId) => {
const visited = getVisitedPosts();
return !!visited[postId];
};
// ==================== 签到功能 ====================
const doCheckin = async () => {
if (hasCheckedIn()) return;
try {
const res = await fetch(CONFIG.API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
credentials: 'include',
body: `random=${CONFIG.RANDOM_MODE}`
});
const data = await res.json();
if (data.success) {
GM_setValue(CONFIG.STORAGE_KEY, getToday());
notify('签到成功', data.message || '获得鸡腿奖励!');
} else if (data.message?.includes('已完成') || data.message?.includes('已签到')) {
GM_setValue(CONFIG.STORAGE_KEY, getToday());
}
} catch (e) {
console.error('[NS助手] 签到异常:', e);
}
};
// ==================== 数据获取(仅标题) ====================
const fetchPageTitles = async (url) => {
try {
const res = await fetch(url, { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const posts = [];
const seen = new Set();
doc.querySelectorAll('a[href*="/post-"]').forEach(link => {
const href = link.getAttribute('href');
const postId = extractPostId(href);
const title = link.textContent?.trim();
if (!postId || !title || title.length < 3 || seen.has(postId)) return;
if (link.closest('.pagination, [class*="page"]')) return;
seen.add(postId);
posts.push({
id: postId,
title,
url: href.startsWith('http') ? href : `https://www.nodeseek.com${href}`
});
});
return posts;
} catch (e) {
console.error('[NS助手] 获取页面失败:', e);
return [];
}
};
// ==================== 交易帖获取 ====================
const fetchActiveTrades = async () => {
const posts = await fetchPageTitles(CONFIG.TRADE_URL);
const results = [];
for (const post of posts) {
if (results.length >= CONFIG.TRADE_COUNT) break;
// 排除版块公告和置顶帖
if (/版块规定|中介索引|防骗提示|骗子索引/i.test(post.title)) continue;
// 排除已完成交易
if (/已出|已收|已售|sold|closed/i.test(post.title)) continue;
const isBuy = /收|求|buy|购/i.test(post.title);
results.push({
id: post.id,
title: post.title,
url: post.url,
type: isBuy ? 'buy' : 'sell',
tag: isBuy ? '求购' : '出售',
visited: isVisited(post.id)
});
}
return results;
};
// ==================== 抽奖帖获取(含开奖时间) ====================
const extractLotteryTime = (title) => {
// 从标题提取开奖时间
const patterns = [
/(\d{1,2})[月\/\-.](\d{1,2})[日号]?\s*(\d{1,2})[时点::]?(\d{0,2})?/, // 12月20日 20:00
/(\d{1,2})[\/\-.](\d{1,2})\s+(\d{1,2}):(\d{2})/, // 12/20 20:00
/(\d{1,2})[时点]开奖/, // 20点开奖
/(\d+)\s*小时后/, // 24小时后
/今[天晚].*?(\d{1,2})[时点::]/, // 今晚8点
/明[天日].*?(\d{1,2})[时点::]/, // 明天20点
];
for (const pattern of patterns) {
const match = title.match(pattern);
if (match) {
// 简单返回匹配到的时间描述
return match[0];
}
}
return null;
};
const fetchActiveLotteries = async () => {
const posts = await fetchPageTitles(CONFIG.HOME_URL);
const results = [], seen = new Set();
for (const post of posts) {
if (results.length >= CONFIG.LOTTERY_COUNT || seen.has(post.id)) continue;
// 只根据标题判断是否是抽奖帖
if (!/抽奖|开奖|福利|免费送|白嫖|送\d+|🎁|🎉/i.test(post.title)) continue;
if (/已开奖|已结束|已完成|结束|开奖结果/i.test(post.title)) continue;
seen.add(post.id);
const lotteryTime = extractLotteryTime(post.title);
const cleanTitle = post.title
.replace(/[\[【((]?\s*(抽奖|开奖|福利)\s*[\]】))]?/gi, '')
.replace(/^\s*[::]\s*/, '')
.trim();
results.push({
id: post.id,
title: cleanTitle || post.title,
url: post.url,
tag: '抽奖',
lotteryTime,
visited: isVisited(post.id)
});
}
return results;
};
// ==================== 中奖检测 ====================
const getParticipatedLotteries = () => {
try {
return GM_getValue(CONFIG.WIN_CHECK_KEY) || {};
} catch {
return {};
}
};
const addParticipatedLottery = (postId, title) => {
const participated = getParticipatedLotteries();
if (!participated[postId]) {
participated[postId] = { title, addedAt: Date.now(), checked: false };
GM_setValue(CONFIG.WIN_CHECK_KEY, participated);
}
};
const checkWinStatus = async () => {
const participated = getParticipatedLotteries();
const postIds = Object.keys(participated).filter(id => !participated[id].won);
if (postIds.length === 0) return;
console.log(`[NS助手] 检查 ${postIds.length} 个抽奖帖的中奖状态...`);
for (const postId of postIds.slice(0, 5)) { // 每次最多检查5个
try {
const res = await fetch(`https://www.nodeseek.com/post-${postId}.html`, {
credentials: 'include'
});
if (!res.ok) continue;
const html = await res.text();
// 获取当前用户名
const usernameMatch = html.match(/data-username="([^"]+)"/);
if (!usernameMatch) continue;
const currentUser = usernameMatch[1];
// 检查是否中奖(在开奖结果中出现用户名)
const isEnded = /已开奖|开奖结果|中奖名单|恭喜.*中奖/i.test(html);
if (isEnded) {
const winPattern = new RegExp(`@${currentUser}|恭喜\\s*${currentUser}|中奖.*${currentUser}|${currentUser}.*中奖`, 'i');
const isWinner = winPattern.test(html);
participated[postId].checked = true;
participated[postId].ended = true;
if (isWinner) {
participated[postId].won = true;
const title = participated[postId].title || '未知抽奖';
notify('🎉 恭喜中奖!', `您在「${truncate(title, 20)}」中奖了!`, () => {
window.open(`https://www.nodeseek.com/post-${postId}.html`, '_blank');
});
}
}
GM_setValue(CONFIG.WIN_CHECK_KEY, participated);
// 延迟避免请求过快
await new Promise(r => setTimeout(r, 1000));
} catch (e) {
console.log(`[NS助手] 检查帖子 ${postId} 失败:`, e.message);
}
}
};
// 监控当前页面是否参与抽奖
const monitorLotteryParticipation = () => {
const postId = extractPostId(location.href);
if (!postId) return;
// 检查页面是否是抽奖帖
const pageTitle = document.title || '';
if (!/抽奖|开奖|福利|免费送/i.test(pageTitle)) return;
// 监控评论提交
const observer = new MutationObserver(() => {
const hasCommented = document.querySelector('.comment-list .comment-item');
if (hasCommented) {
addParticipatedLottery(postId, pageTitle.replace(/ - NodeSeek$/, ''));
console.log(`[NS助手] 已记录参与抽奖: ${postId}`);
}
});
const commentList = document.querySelector('.comment-list, .post-comments, [class*="comment"]');
if (commentList) {
observer.observe(commentList, { childList: true, subtree: true });
}
// 同时检查是否已经评论过
setTimeout(() => {
const currentUser = document.querySelector('[data-username]')?.getAttribute('data-username');
if (currentUser) {
const comments = document.querySelectorAll('.comment-item, [class*="comment"]');
comments.forEach(comment => {
if (comment.textContent?.includes(currentUser)) {
addParticipatedLottery(postId, pageTitle.replace(/ - NodeSeek$/, ''));
}
});
}
}, 2000);
};
// ==================== 侧边栏UI ====================
let sidebarInstance = null;
const createSidebar = () => {
document.querySelector('.ns-sidebar')?.remove();
const sidebar = document.createElement('div');
sidebar.className = 'ns-sidebar';
sidebar.innerHTML = `
<div class="ns-card trade">
<div class="ns-card-header">
<span>💰 最新交易</span>
<span class="ns-card-toggle">−</span>
</div>
<div class="ns-card-body"><div class="ns-empty ns-loading">加载中...</div></div>
</div>
<div class="ns-card lottery">
<div class="ns-card-header">
<span>🎁 最新抽奖</span>
<span class="ns-card-toggle">−</span>
</div>
<div class="ns-card-body"><div class="ns-empty ns-loading">加载中...</div></div>
</div>
`;
document.body.appendChild(sidebar);
sidebar.querySelectorAll('.ns-card-header').forEach(header => {
header.addEventListener('click', () => {
const card = header.closest('.ns-card');
const toggle = header.querySelector('.ns-card-toggle');
card.classList.toggle('collapsed');
toggle.textContent = card.classList.contains('collapsed') ? '+' : '−';
});
});
sidebarInstance = sidebar;
return sidebar;
};
const renderTradeCard = (card, items) => {
const body = card.querySelector('.ns-card-body');
if (!items?.length) {
body.innerHTML = '<div class="ns-empty">暂无交易信息</div>';
return;
}
body.innerHTML = items.map(item => `
<div class="ns-item ${item.visited ? 'visited' : ''}" data-post-id="${item.id}">
<a href="${escapeHtml(item.url)}" target="_blank" title="${escapeHtml(item.title)}">
<div class="ns-item-row">
<span class="ns-tag ${item.type}">${item.tag}</span>
<span class="ns-title">${escapeHtml(truncate(item.title, 18))}</span>
${item.visited ? '<span class="ns-visited-mark">✓已看</span>' : ''}
</div>
</a>
</div>
`).join('');
// 添加点击事件标记已浏览
body.querySelectorAll('.ns-item').forEach(el => {
el.addEventListener('click', () => {
const postId = el.getAttribute('data-post-id');
if (postId) {
markAsVisited(postId);
el.classList.add('visited');
if (!el.querySelector('.ns-visited-mark')) {
el.querySelector('.ns-item-row')?.insertAdjacentHTML('beforeend',
'<span class="ns-visited-mark">✓已看</span>');
}
}
});
});
};
const renderLotteryCard = (card, items) => {
const body = card.querySelector('.ns-card-body');
if (!items?.length) {
body.innerHTML = '<div class="ns-empty">暂无抽奖信息</div>';
return;
}
body.innerHTML = items.map(item => `
<div class="ns-item ${item.visited ? 'visited' : ''}" data-post-id="${item.id}">
<a href="${escapeHtml(item.url)}" target="_blank" title="${escapeHtml(item.title)}">
<div class="ns-item-row">
<span class="ns-tag lottery">${item.tag}</span>
<span class="ns-title">${escapeHtml(truncate(item.title, 18))}</span>
${item.visited ? '<span class="ns-visited-mark">✓已看</span>' : ''}
</div>
${item.lotteryTime ? `<div class="ns-lottery-time">⏰ ${escapeHtml(item.lotteryTime)}</div>` : ''}
</a>
</div>
`).join('');
// 添加点击事件标记已浏览
body.querySelectorAll('.ns-item').forEach(el => {
el.addEventListener('click', () => {
const postId = el.getAttribute('data-post-id');
if (postId) {
markAsVisited(postId);
el.classList.add('visited');
if (!el.querySelector('.ns-visited-mark')) {
el.querySelector('.ns-item-row')?.insertAdjacentHTML('beforeend',
'<span class="ns-visited-mark">✓已看</span>');
}
}
});
});
};
const loadSidebarData = async (sidebar) => {
const [trades, lotteries] = await Promise.all([
fetchActiveTrades(),
fetchActiveLotteries()
]);
renderTradeCard(sidebar.querySelector('.ns-card.trade'), trades);
renderLotteryCard(sidebar.querySelector('.ns-card.lottery'), lotteries);
};
// ==================== 初始化 ====================
const init = () => {
console.log('[NS助手] v2.0.0 初始化');
// 自动签到
setTimeout(doCheckin, 1500);
// 监控抽奖参与
monitorLotteryParticipation();
// 定期检查中奖
setTimeout(checkWinStatus, 5000);
setInterval(checkWinStatus, CONFIG.WIN_CHECK_INTERVAL);
// 列表页显示侧边栏
const isListPage = location.pathname === '/' ||
location.pathname.startsWith('/board') ||
location.pathname.startsWith('/categor');
if (isListPage) {
setTimeout(async () => {
const sidebar = createSidebar();
await loadSidebarData(sidebar);
}, 800);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
V2.0.1已更新
增加或删除以下内容
✅ 删除鸡腿排行榜 - 完全删除排行榜相关代码
✅ 仅索引标题 - 交易和抽奖只根据标题内容判断,不索引帖子内容
✅ 各显示5个 - 交易和抽奖各显示最新5条
✅ 已浏览高亮 - 点击过的帖子显示✓已看标记,背景变灰
自动保存30天浏览记录避免重复查看
✅ 中奖提醒 - 自动追踪参与的抽奖
监控用户抽奖帖的评论每10分钟检查开奖状态中奖时弹出桌面通知
✅ 开奖时间显示 - 从标题提取开奖时间
显示在抽奖帖下方(⏰图标)支持多种时间格式
方便了抽奖 要是中了有提醒就好了
收藏
@xikk #1 我试试糙一个
@cnmdnews #3
大佬牛B 能糙出来就糙福大家了
y

有bug
好东西一起分享👍
牛逼
@tyoo #5 哈哈,等改进
@tyoo #5 稍后把鸡腿排行榜去掉了,有点鸡肋
牛逼