# 唠嗑巨作:寻找深海潜水员

80hou 2026-08-16 22:28 1

前言:


^-^


如有违规处,麻烦移动到无意义 ^-^


NodeSeek 关注列表「最后发言时间」查询与排序助手



^-^


💡 开发初衷与痛点

一句话简介:一键查询关注/粉丝列表用户的最后回帖时间,支持按“最久未发言”或“最近活跃”排序,内置防刷队列与本地缓存,杜绝接口滥用。


在论坛里互关了不少聊得来的朋友,时间一长总想看看大家最近都在聊什么、还有谁在活跃。但每当打开关注列表时,就会遇到很尴尬的体验:


信息维度单一:卡片上只显示“注册天数”、“主题数”和“评论数”,完全看不出该用户最近一次说话是什么时候;


人多查起来费劲:关注的人一多,想知道谁很久没冒泡、谁“失联/退坑”了,必须手动一个个点进个人主页去翻动态,极其繁琐耗时。


为了能快速找到常在线的好友唠嗑、顺便看看谁成了“深海潜水员”,便写了这个轻量级的油猴脚本。


核心功能

🕒 最后发言时间透视:在每张卡片下方一键获取最后一次评论的相对时间(如 10 分钟前、2 天前);


💬 悬停预览与直达:鼠标悬停即可预览最新回复的帖子标题与楼层内容,点击直接在新标签页跳转到该评论楼层;


顶栏一键批量查询:顶栏标签旁嵌入了批量按钮,支持按设定的安全间隔自动依次排队查询全页;


📊 多列智能排序:


最久未发言:正序排列,快速找出最长时间没发言、处于潜水状态的朋友;


️ 最近活跃:倒序排列,优先展示刚刚/近期活跃的用户;


️ 默认顺序:一键恢复原始列表顺序。


🛡️ 架构设计与安全审计

为了保证脚本在提升体验的同时,绝对不给论坛服务器造成压力、不滥用 API、不触发风控,代码在底层设计上做了严格的审计与防护:


单任务串行队列(严格限速防刷):


采用单线程异步任务队列管理所有网络请求,每次查询强制保持 250ms 冷却间隔,杜绝高并发请求,避免触发 Cloudflare 的限流拦截(HTTP 429)。


30 分钟持久化本地缓存:


查询结果自动写入 localStorage(有效期 30 分钟)。在此期间,无论在“我的关注”和“我的粉丝”之间如何切换、翻页或刷新页面,数据全部本地秒级回显,不消耗任何 API 请求。


严格路由准入守卫(URL 白名单):


仅在 /fans?type=follow 与 /fans?type=fans 页面初始化功能模块,在其他任意页面自动休眠且不注入多余 DOM。


瀑布流多列自适应均衡分发:


排序重排逻辑完全基于纯前端 DOM 时间戳运算,采用轮流分发算法,适配多列瀑布流布局,避免传统排序导致的单列挤压与空白 Bug。



【油猴脚本】NodeSeek 关注与粉丝列表 - 最后发言时间与智能排序
// ==UserScript==
// @name NodeSeek 关注与粉丝列表 - 最后发言时间与智能排序
// @namespace https://www.nodeseek.com/
// @version 1.0.0
// @description 在 NodeSeek 关注与粉丝列表中按需查询用户最后发言时间,统一中文相对时间展示,支持正序(最久未发言)与倒序(最近活跃)多列均衡排序,带持久化缓存与单任务防刷队列
// @author Gemini
// @match *://www.nodeseek.com/fans*
// @match *://nodeseek.com/fans*
// @grant none
// @run-at document-idle
// ==/UserScript==

(function () {
'use strict';

// =========================================================================
// 1. 基础配置模块 (Configuration)
// =========================================================================
const CONFIG = {
// 请求间隔时间(毫秒):控制队列中每个用户查询的间隔,防止被风控限流
REQUEST_INTERVAL_MS: 250,
// 缓存有效期(分钟):在该时间内切换标签或刷新页面,直接读取本地数据
CACHE_EXPIRE_MINUTES: 30,
// 本地存储键名前缀
STORAGE_KEY_PREFIX: 'ns_user_comment_cache_zh_'
};

// =========================================================================
// 2. 路由准入控制模块 (Route Guard)
// =========================================================================
function isTargetRoute() {
if (window.location.pathname !== '/fans') {
return false;
}
const params = new URLSearchParams(window.location.search);
const tabType = params.get('type');
return tabType === 'follow' || tabType === 'fans';
}

// =========================================================================
// 3. 样式注入模块 (Style Injection)
// =========================================================================
function injectStyles() {
if (document.getElementById('ns-comment-time-style')) {
return;
}
const style = document.createElement('style');
style.id = 'ns-comment-time-style';
style.innerHTML = `
/* 单个卡片查询按钮及结果展示条 */
.ns-time-trigger-btn {
display: flex;
align-items: center;
justify-content: center;
margin: 6px 10px 8px 10px;
padding: 4px 8px;
background: #f8fafc;
border: 1px dashed #94a3b8;
border-radius: 6px;
font-size: 12px;
color: #475569;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
user-select: none;
box-sizing: border-box;
}
.ns-time-trigger-btn:hover {
background: #f1f5f9;
border-color: #64748b;
color: #0f172a;
}
.ns-time-trigger-btn.loading {
background: #fffbeb;
border-color: #f59e0b;
border-style: solid;
color: #b45309;
cursor: wait;
}
.ns-time-trigger-btn.success {
background: #ecfdf5;
border-color: #10b981;
border-style: solid;
color: #065f46;
cursor: pointer;
}
.ns-time-trigger-btn.empty {
background: #f3f4f6;
border-color: #d1d5db;
border-style: solid;
color: #6b7280;
}
.ns-time-trigger-btn.error {
background: #fef2f2;
border-color: #ef4444;
border-style: solid;
color: #b91c1c;
}

/* 顶部工具栏容器 */
.ns-toolbar-group {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}

/* 通用顶栏按钮样式 */
.ns-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px 12px;
font-size: 12px;
font-weight: 500;
border-radius: 16px;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
text-decoration: none;
white-space: nowrap;
height: 28px;
box-sizing: border-box;
line-height: 1;
}

/* 一键查询按钮 */
.ns-batch-btn {
background: #10b981;
color: #ffffff !important;
box-shadow: 0 2px 6px rgba(16, 185, 129, 0.25);
}
.ns-batch-btn:hover {
background: #059669;
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.35);
}
.ns-batch-btn.running {
background: #f59e0b;
box-shadow: 0 2px 6px rgba(245, 158, 11, 0.25);
cursor: wait;
}
.ns-batch-btn.done {
background: #6b7280;
box-shadow: none;
cursor: default;
}

/* 排序按钮 */
.ns-sort-btn {
background: #ffffff;
color: #374151 !important;
border: 1px solid #d1d5db;
}
.ns-sort-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.ns-sort-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #ffffff !important;
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.25);
}
`;
document.head.appendChild(style);
}

// =========================================================================
// 4. 存储与请求队列控制模块 (Storage & Request Queue)
// =========================================================================
const StorageManager = {
// 读取缓存数据,过期自动清理
get(uid) {
try {
const raw = localStorage.getItem(CONFIG.STORAGE_KEY_PREFIX + uid);
if (!raw) return null;
const record = JSON.parse(raw);
const expireMs = CONFIG.CACHE_EXPIRE_MINUTES * 60 * 1000;
if (Date.now() - record.timestamp < expireMs) {
return record.data;
}
localStorage.removeItem(CONFIG.STORAGE_KEY_PREFIX + uid);
} catch (e) {
// 忽略异常
}
return null;
},
// 写入缓存并附带生成时间戳
set(uid, data) {
try {
const record = {
timestamp: Date.now(),
data: data
};
localStorage.setItem(CONFIG.STORAGE_KEY_PREFIX + uid, JSON.stringify(record));
} catch (e) {
// 捕获存储空间满异常
}
}
};

// 单任务异步队列控制器
const TaskQueue = {
tasks: [],
running: false,
activeUids: new Set(),

// 添加任务到执行队列
add(uid, taskFn) {
if (this.activeUids.has(uid)) {
return;
}
this.activeUids.add(uid);
this.tasks.push({ uid, taskFn });
this.run();
},

// 串行执行任务
async run() {
if (this.running || this.tasks.length === 0) {
return;
}
this.running = true;
const current = this.tasks.shift();

try {
await current.taskFn();
} catch (err) {
console.error('[NodeSeek-Helper] 任务执行异常:', err);
} finally {
this.activeUids.delete(current.uid);
}

setTimeout(() => {
this.running = false;
this.run();
}, CONFIG.REQUEST_INTERVAL_MS);
}
};

// =========================================================================
// 5. 时间解析与中文标准化模块 (Time Parser)
// =========================================================================
const TimeParser = {
// 将各类中英文相对时间、复合时间或绝对日期解析为绝对时间戳(毫秒数)
parseToTimestamp(rawStr) {
if (!rawStr) return 0;
const str = String(rawStr).trim().toLowerCase();

if (str === '刚刚' || str.includes('just now') || str === 'now') {
return Date.now();
}
if (str === '暂无发言' || str === '获取失败') {
return 0;
}

// 1. 尝试直接按标准时间戳或标准日期格式解析
const direct = Date.parse(str);
if (!isNaN(direct) && direct > 0) {
return direct;
}

// 2. 匹配中英文相对时间并计算毫秒偏移
let offsetMs = 0;
let matched = false;

// 年: 1年 / 1y / 1 year / 1 years
const yMatch = str.match(/(\d+)\s*(?:年|y(?:ear)?s?)/);
if (yMatch) {
offsetMs += parseInt(yMatch[1], 10) * 365 * 86400 * 1000;
matched = true;
}

// 月: 1个月 / 1月 / 1mo / 1 month / 1 months
const monMatch = str.match(/(\d+)\s*(?:个月|月|mo(?:nth)?s?)/);
if (monMatch) {
offsetMs += parseInt(monMatch[1], 10) * 30 * 86400 * 1000;
matched = true;
}

// 周: 1周 / 1星期 / 1w / 1 week / 1 weeks
const wMatch = str.match(/(\d+)\s*(?:周|星期|w(?:eek)?s?)/);
if (wMatch) {
offsetMs += parseInt(wMatch[1], 10) * 7 * 86400 * 1000;
matched = true;
}

// 天: 1天 / 1d / 1 day / 1day / 1 days
const dMatch = str.match(/(\d+)\s*(?:天|日|d(?:ay)?s?)/);
if (dMatch) {
offsetMs += parseInt(dMatch[1], 10) * 86400 * 1000;
matched = true;
}

// 小时: 4小时 / 4h / 4 hour / 4 hours
const hMatch = str.match(/(\d+)\s*(?:小时|时|h(?:our)?s?)/);
if (hMatch) {
offsetMs += parseInt(hMatch[1], 10) * 3600 * 1000;
matched = true;
}

// 分钟: 11分钟 / 11分 / 11m / 11min / 11 minute / 11 minutes
const mMatch = str.match(/(\d+)\s*(?:分钟|分|m(?:in)?(?:ute)?s?)/);
if (mMatch) {
offsetMs += parseInt(mMatch[1], 10) * 60 * 1000;
matched = true;
}

// 秒: 30秒 / 30s / 30 sec / 30 second / 30 seconds
const sMatch = str.match(/(\d+)\s*(?:秒|s(?:ec)?(?:ond)?s?)/);
if (sMatch) {
offsetMs += parseInt(sMatch[1], 10) * 1000;
matched = true;
}

if (matched) {
return Date.now() - offsetMs;
}

return 0;
},

// 将时间戳统一转换为规范友好的中文相对时间
formatToChinese(timestampMs) {
if (!timestampMs || timestampMs <= 0) {
return '暂无发言';
}

const diffSeconds = Math.floor((Date.now() - timestampMs) / 1000);
if (diffSeconds < 0 || diffSeconds < 60) {
return '刚刚';
}
if (diffSeconds < 3600) {
return `${Math.floor(diffSeconds / 60)} 分钟前`;
}
if (diffSeconds < 86400) {
return `${Math.floor(diffSeconds / 3600)} 小时前`;
}
if (diffSeconds < 86400 * 30) {
return `${Math.floor(diffSeconds / 86400)} 天前`;
}
if (diffSeconds < 86400 * 365) {
return `${Math.floor(diffSeconds / (86400 * 30))} 个月前`;
}
return `${Math.floor(diffSeconds / (86400 * 365))} 年前`;
},

// 从帖子页面提取楼层时间
extractFromHtml(html, floorId, uid) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');

function getTimeTextFromContainer(el) {
if (!el) return null;
const timeEl = el.querySelector('time, [datetime], [class*="time"], [class*="date"]');
if (timeEl) {
const val = timeEl.getAttribute('datetime') || timeEl.getAttribute('title') || timeEl.textContent.trim();
if (val) return val;
}
const text = el.innerText || el.textContent || '';
const match = text.match(/(\d+\s*(?:年|月|周|天|日|小时|时|分|秒|y|mo|w|d|h|m|s|year|month|week|day|hour|min|minute|sec|second)s?(?:\s*ago|\s*前)?|\d{4}[-/]\d{2}[-/]\d{2}(?:\s+\d{2}:\d{2})?)/i);
if (match) return match[1];
return null;
}

// 1. 楼层编号精确匹配
if (floorId !== undefined && floorId !== null) {
const nodes = doc.querySelectorAll('a, span, div, b');
for (const node of nodes) {
const text = node.textContent.trim();
if (text === `#${floorId}` || text === `# ${floorId}` || text === `${floorId}#`) {
const itemBlock = node.closest('.comment-item, [class*="comment"], .post-item, tr, li, div[class*="item"]');
const rawTime = getTimeTextFromContainer(itemBlock);
if (rawTime) return rawTime;
}
}
}

// 2. 头像 UID 匹配
if (uid) {
const userAvatar = doc.querySelector(`img[src*="/avatar/${uid}"]`);
if (userAvatar) {
const itemBlock = userAvatar.closest('.comment-item, [class*="comment"], .post-item, tr, li, div[class*="item"]');
const rawTime = getTimeTextFromContainer(itemBlock);
if (rawTime) return rawTime;
}
}

// 3. 通用时间标签兜底
const anyTime = doc.querySelector('time, [datetime]');
if (anyTime) {
const val = anyTime.getAttribute('datetime') || anyTime.getAttribute('title') || anyTime.textContent.trim();
if (val) return val;
}

return null;
}
};

// =========================================================================
// 6. 网络通信模块 (Network Service)
// =========================================================================
const ApiService = {
async fetchUserLastComment(uid) {
try {
// 第一步:拉取最近一条评论元数据
const commentRes = await fetch(`/api/content/list-comments?uid=${uid}&page=1`, {
headers: { 'Accept': 'application/json, text/plain, */*' },
credentials: 'include'
});
if (!commentRes.ok) throw new Error(`HTTP ${commentRes.status}`);

const data = await commentRes.json();
if (!data || !data.success || !Array.isArray(data.comments)) {
return { timeStr: '获取失败', timestampMs: 0, title: '', link: `/space/${uid}#/comments` };
}
if (data.comments.length === 0) {
return { timeStr: '暂无发言', timestampMs: 0, title: '', link: `/space/${uid}#/comments` };
}

const firstComment = data.comments[0];
const postId = firstComment.post_id;
const floorId = firstComment.floor_id || 0;
const postTitle = firstComment.title || '';
const commentText = firstComment.text || '';

const pageIndex = floorId > 0 ? Math.ceil(floorId / 20) : 1;
const postUrl = `/post-${postId}-${pageIndex}`;

// 第二步:拉取帖子详情页提取该楼层的真实时间
const postRes = await fetch(postUrl, { credentials: 'include' });
if (postRes.ok) {
const postHtml = await postRes.text();
const rawTimeStr = TimeParser.extractFromHtml(postHtml, floorId, uid);
const timestampMs = rawTimeStr ? TimeParser.parseToTimestamp(rawTimeStr) : 0;
const chineseTimeStr = timestampMs > 0 ? TimeParser.formatToChinese(timestampMs) : (rawTimeStr || '近期发言');

return {
timeStr: chineseTimeStr,
timestampMs: timestampMs,
title: postTitle,
text: commentText,
floorId: floorId,
link: `${postUrl}#${floorId}`
};
}

return {
timeStr: '近期发言',
timestampMs: Date.now() - 86400000,
title: postTitle,
text: commentText,
floorId: floorId,
link: postUrl
};
} catch (err) {
console.error(`[NodeSeek-Helper] UID ${uid} 查询失败:`, err);
}
return { timeStr: '获取失败', timestampMs: 0, title: '', link: `/space/${uid}#/comments` };
}
};

// =========================================================================
// 7. 多列均衡排序与 DOM 重排模块 (Sort & Layout Manager)
// =========================================================================
let isDomSorting = false;

const SortManager = {
// 获取所有卡片及其所属的列容器结构
getLayoutInfo() {
const btns = Array.from(document.querySelectorAll('.ns-time-trigger-btn'));
const cards = [];

btns.forEach(btn => {
let card = btn.parentElement;
while (card && card !== document.body) {
if (card.querySelector('button') || card.innerText.includes('评论数') || card.innerText.includes('转账')) {
cards.push(card);
break;
}
card = card.parentElement;
}
});

// 获取全部列容器(去重并保留原始布局顺序)
const columnSet = new Set();
cards.forEach(c => {
if (c.parentElement) {
columnSet.add(c.parentElement);
}
});
const columns = Array.from(columnSet);

return { cards, columns };
},

// 执行多列自适应均衡排序
applySort(mode) {
const { cards, columns } = SortManager.getLayoutInfo();
if (cards.length === 0 || columns.length === 0) return;

isDomSorting = true;

const sortedCards = [...cards].sort((a, b) => {
if (mode === 'default') {
const idxA = parseInt(a.dataset.nsOriginalIndex || '0', 10);
const idxB = parseInt(b.dataset.nsOriginalIndex || '0', 10);
return idxA - idxB;
}

const uidA = a.querySelector('.ns-time-trigger-btn')?.dataset.uid;
const uidB = b.querySelector('.ns-time-trigger-btn')?.dataset.uid;

const cacheA = uidA ? StorageManager.get(uidA) : null;
const cacheB = uidB ? StorageManager.get(uidB) : null;

const timeA = cacheA ? (cacheA.timestampMs || TimeParser.parseToTimestamp(cacheA.timeStr)) : -1;
const timeB = cacheB ? (cacheB.timestampMs || TimeParser.parseToTimestamp(cacheB.timeStr)) : -1;

// 未查询的卡片统一排在末尾
if (timeA === -1 && timeB === -1) return 0;
if (timeA === -1) return 1;
if (timeB === -1) return -1;

// 正序 (asc): 最久未发言在前 (时间戳小在前)
if (mode === 'asc') {
return timeA - timeB;
}
// 倒序 (desc): 最近活跃在前 (时间戳大在前)
if (mode === 'desc') {
return timeB - timeA;
}
return 0;
});

// 按照列容器依次均匀轮流分发(保证 4 列瀑布流高度平衡)
if (columns.length > 1) {
sortedCards.forEach((card, index) => {
const targetCol = columns[index % columns.length];
targetCol.appendChild(card);
});
} else {
const singleContainer = columns[0];
sortedCards.forEach(card => singleContainer.appendChild(card));
}

setTimeout(() => {
isDomSorting = false;
}, 100);

SortManager.updateButtonsUi(mode);
},

// 更新排序按钮高亮状态
updateButtonsUi(mode) {
const ascBtn = document.getElementById('ns-sort-asc-btn');
const descBtn = document.getElementById('ns-sort-desc-btn');
const defBtn = document.getElementById('ns-sort-def-btn');

if (ascBtn) ascBtn.classList.toggle('active', mode === 'asc');
if (descBtn) descBtn.classList.toggle('active', mode === 'desc');
if (defBtn) defBtn.classList.toggle('active', mode === 'default');
}
};

// =========================================================================
// 8. 视图渲染与交互模块 (UI & Interaction)
// =========================================================================
const UiManager = {
renderResult(btn, result) {
const { timeStr, title, text, floorId, link } = result;

if (link) {
btn.href = link;
btn.target = '_blank';
}

if (title || text) {
let tip = '';
if (title) tip += `帖子: 《${title}》\n`;
if (floorId !== undefined) tip += `楼层: #${floorId}\n`;
if (text) tip += `内容: ${text.slice(0, 45)}${text.length > 45 ? '...' : ''}\n`;
tip += `点击直接跳转至该楼层`;
btn.title = tip;
}

btn.classList.remove('loading');
if (!timeStr || timeStr === '暂无发言') {
btn.className = 'ns-time-trigger-btn empty';
btn.innerHTML = `💬 <b>最后发言:</b>&nbsp;暂无发言`;
} else if (timeStr === '获取失败') {
btn.className = 'ns-time-trigger-btn error';
btn.innerHTML = `⚠️ <b>最后发言:</b>&nbsp;获取失败 (点击重试)`;
btn.dataset.queried = 'false';
} else {
btn.className = 'ns-time-trigger-btn success';
btn.innerHTML = `💬 <b>最后发言:</b>&nbsp;<b>${timeStr}</b>`;
}
},

triggerQuery(btn, onFinished) {
const uid = btn.dataset.uid;
if (!uid || btn.dataset.queried === 'true') {
if (onFinished) onFinished();
return;
}

const cachedData = StorageManager.get(uid);
if (cachedData) {
btn.dataset.queried = 'true';
UiManager.renderResult(btn, cachedData);
if (onFinished) onFinished();
return;
}

btn.dataset.queried = 'true';
btn.className = 'ns-time-trigger-btn loading';
btn.innerHTML = `🕒 正在查询...`;

TaskQueue.add(uid, async () => {
const result = await ApiService.fetchUserLastComment(uid);
if (result.timeStr !== '获取失败') {
StorageManager.set(uid, result);
}
UiManager.renderResult(btn, result);
if (onFinished) onFinished();
});
},

mountCard(img, index) {
const match = img.src.match(/\/avatar\/(\d+)/i);
if (!match) return;
const uid = match[1];

let card = img.parentElement;
while (card && card !== document.body) {
if (card.querySelector('button') || card.innerText.includes('评论数') || card.innerText.includes('转账')) {
break;
}
card = card.parentElement;
}

if (!card || card === document.body) return;

if (!card.dataset.nsOriginalIndex) {
card.dataset.nsOriginalIndex = String(index);
}

if (card.dataset.nsBtnMounted) return;
card.dataset.nsBtnMounted = 'true';

const triggerBtn = document.createElement('a');
triggerBtn.className = 'ns-time-trigger-btn';
triggerBtn.dataset.uid = uid;
triggerBtn.dataset.queried = 'false';
triggerBtn.title = '点击查询该用户最后发言时间';
triggerBtn.innerHTML = `🔍 查最后发言`;

triggerBtn.addEventListener('click', (e) => {
if (triggerBtn.dataset.queried !== 'true') {
e.preventDefault();
UiManager.triggerQuery(triggerBtn);
}
});

const btnGroup = card.querySelector('button')?.parentElement || card.lastElementChild;
if (btnGroup && btnGroup.parentElement === card) {
card.insertBefore(triggerBtn, btnGroup);
} else {
card.appendChild(triggerBtn);
}

const cachedData = StorageManager.get(uid);
if (cachedData) {
triggerBtn.dataset.queried = 'true';
UiManager.renderResult(triggerBtn, cachedData);
}
},

mountTopToolbar() {
if (document.getElementById('ns-top-toolbar-container')) return;

const allTextNodes = Array.from(document.querySelectorAll('a, span, button, div'));
const tabTextEl = allTextNodes.find(el =>
el.children.length === 0 && (el.textContent.includes('我的关注') || el.textContent.includes('我的粉丝'))
);
if (!tabTextEl) return;

let capsule = tabTextEl.parentElement;
while (capsule && capsule !== document.body) {
if (capsule.children.length >= 2 && capsule.children.length <= 4 &&
capsule.innerText.includes('我的关注') && capsule.innerText.includes('我的粉丝')) {
break;
}
capsule = capsule.parentElement;
}
if (!capsule || capsule === document.body) return;

const toolbar = document.createElement('div');
toolbar.id = 'ns-top-toolbar-container';
toolbar.className = 'ns-toolbar-group';

// 1. 一键查询按钮
const batchBtn = document.createElement('a');
batchBtn.id = 'ns-top-batch-btn';
batchBtn.className = 'ns-action-btn ns-batch-btn';
batchBtn.innerHTML = `⚡ <span>一键查询本页发言</span>`;
batchBtn.title = '依次排队查询当前页面所有用户的最后发言时间';

let isRunning = false;
batchBtn.addEventListener('click', (e) => {
e.preventDefault();
if (isRunning) return;

const unqueriedBtns = Array.from(document.querySelectorAll('.ns-time-trigger-btn[data-queried="false"]'));
if (unqueriedBtns.length === 0) {
batchBtn.className = 'ns-action-btn ns-batch-btn done';
batchBtn.innerHTML = `✔ <span>本页已全部查完</span>`;
setTimeout(() => {
batchBtn.className = 'ns-action-btn ns-batch-btn';
batchBtn.innerHTML = `⚡ <span>一键查询本页发言</span>`;
}, 3000);
return;
}

isRunning = true;
batchBtn.className = 'ns-action-btn ns-batch-btn running';

let completedCount = 0;
const totalCount = unqueriedBtns.length;
batchBtn.innerHTML = `⚡ <span>查询中 (${completedCount}/${totalCount})...</span>`;

unqueriedBtns.forEach(btn => {
UiManager.triggerQuery(btn, () => {
completedCount++;
if (completedCount < totalCount) {
batchBtn.innerHTML = `⚡ <span>查询中 (${completedCount}/${totalCount})...</span>`;
} else {
isRunning = false;
batchBtn.className = 'ns-action-btn ns-batch-btn done';
batchBtn.innerHTML = `✔ <span>本页已全部查完</span>`;
setTimeout(() => {
batchBtn.className = 'ns-action-btn ns-batch-btn';
batchBtn.innerHTML = `⚡ <span>一键查询本页发言</span>`;
}, 3000);
}
});
});
});

// 2. 最久发言排序按钮
const sortAscBtn = document.createElement('a');
sortAscBtn.id = 'ns-sort-asc-btn';
sortAscBtn.className = 'ns-action-btn ns-sort-btn';
sortAscBtn.innerHTML = `⏳ <span>最久未发言</span>`;
sortAscBtn.title = '按最后发言时间正序排序,最久未发言的用户排在最前';
sortAscBtn.addEventListener('click', (e) => {
e.preventDefault();
SortManager.applySort('asc');
});

// 3. 最近活跃排序按钮
const sortDescBtn = document.createElement('a');
sortDescBtn.id = 'ns-sort-desc-btn';
sortDescBtn.className = 'ns-action-btn ns-sort-btn';
sortDescBtn.innerHTML = `⏱️ <span>最近活跃</span>`;
sortDescBtn.title = '按最后发言时间倒序排序,最近发言的用户排在最前';
sortDescBtn.addEventListener('click', (e) => {
e.preventDefault();
SortManager.applySort('desc');
});

// 4. 恢复默认排序按钮
const sortDefBtn = document.createElement('a');
sortDefBtn.id = 'ns-sort-def-btn';
sortDefBtn.className = 'ns-action-btn ns-sort-btn';
sortDefBtn.innerHTML = `↩️ <span>默认</span>`;
sortDefBtn.title = '恢复为页面原始顺序';
sortDefBtn.addEventListener('click', (e) => {
e.preventDefault();
SortManager.applySort('default');
});

toolbar.appendChild(batchBtn);
toolbar.appendChild(sortAscBtn);
toolbar.appendChild(sortDescBtn);
toolbar.appendChild(sortDefBtn);

const parent = capsule.parentElement;
if (parent && !parent.classList.contains('ns-tab-wrapper')) {
const wrapper = document.createElement('div');
wrapper.className = 'ns-tab-wrapper';
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
wrapper.style.justifyContent = 'center';
wrapper.style.gap = '12px';
wrapper.style.margin = '10px 0';
wrapper.style.flexWrap = 'wrap';

capsule.insertAdjacentElement('beforebegin', wrapper);
wrapper.appendChild(capsule);
wrapper.appendChild(toolbar);
}
}
};

// =========================================================================
// 9. 运行时初始化与监听 (Runtime & Lifecycle)
// =========================================================================
function run() {
if (!isTargetRoute()) {
const topToolbar = document.getElementById('ns-top-toolbar-container');
if (topToolbar) topToolbar.remove();
return;
}

if (isDomSorting) return;

injectStyles();
UiManager.mountTopToolbar();

const avatars = document.querySelectorAll('img[src*="/avatar/"]');
avatars.forEach((img, index) => UiManager.mountCard(img, index));
}

setTimeout(run, 300);

let debounceTimer = null;
const observer = new MutationObserver(() => {
if (isDomSorting) return;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(run, 250);
});
observer.observe(document.body, { childList: true, subtree: true });
})();


🤝 交流与反馈


脚本代码已做模块化重构与语法规范审查(符合 ESLint 无报错标准)。


最新回复 (17)
  • JohnLeo 08-16 22:33
    1

    你不如把之前没做完的项目全接手都做了吧 ^-^

  • 80hou 楼主 08-16 22:36
    2

    @JohnLeo #1


    那不行,哈基米只能干这种简单的活。


    我这开局30仙晶还没通关呢。 ^-^



    准备在拼一把解锁全部表情。 ^-^

  • JohnLeo 08-16 22:38
    3

    @80hou #2 那就把项目拆开一个一个让3.7做,3.7的小项目让我很满意 ^-^

  • 80hou 楼主 08-16 22:50
    4

    @JohnLeo #3


    3.7Flash确实比3.6Flash强了很多。 ^-^


    不会一直流口水找骂了。


    3.1Pro还是一坨..文心一言都比它强。 ^-^

  • 80hou 楼主 08-16 22:55
    5

    @JohnLeo #3


    看了下,已经有20天没打开过项目了。 ^-^


  • JohnLeo 08-16 22:57
    6

    @80hou #4 3.1Pro我用都懒得用,生怕给自己气出高血压,那是同一个对话聊2句都能丢失上下文的货色 ^-^

    只有在ASK Gemini 我之前才会用一下,强制搜索,但凡Gemini自己会主动联网搜索都不至于被人嘲讽美国豆包 ^-^

  • 80hou 楼主 08-16 23:08
    7

    @JohnLeo #6


    你这还能聊2句..


    3.1Pro完全就是垃圾桶里找垃圾.


    之前微软免费的GPT5.4都完爆3.1Pro


    哈基米就是扶不起的阿斗.


    拥有领先的技术,天崩开局.跟百度坐一桌.


  • JohnLeo 08-16 23:11
    8

    @80hou #7 你用的是web版吧,我很少用,之前吹的时候我也没觉得web版多好,都是antigravity ^-^

  • 80hou 楼主 08-16 23:20
    9

    @JohnLeo #8


    反重力都不想吐槽。登录几次封我几次。 ^-^


    解封弄了我快一个月。


    平时也就用用codex写写项目。哈基米也就吃瓜时候生成下总结图。


    一样的问题,微软免费的都比哈基米强。 ^-^



  • JohnLeo 08-16 23:22
    10

    @80hou #9 那你有点惨,除了我买的羊毛号,所有号都能登录,最离谱的是,我羊毛号虽然登录不了桌面antigravity但能登录antigravity CLI ^-^

  • 80hou 楼主 08-16 23:26
    11

    @JohnLeo #8


    Google很容易让人代入诺基亚的那种决策层。


    太注重商业,为了商业,可以为他人摇旗呐喊 ^-^


    非要特立独行,走不同寻常之路。


    苦心经营,憋了了大招。 ^-^


    就像历史长河里的暴风影音。上市后忘了初心。

  • 80hou 楼主 08-16 23:30
    12

    @JohnLeo #10


    使用门槛太高了,我矮个子跨不过这个门槛 ^-^

  • JohnLeo 08-16 23:31
    13

    @80hou #11 目前对他的Pro还有期待,他解决了幻觉和上下文问题的话,肯定是强到离谱,不知道为什么可以拥有这么好的资源数据还能这么拉 ^-^

  • 80hou 楼主 08-16 23:39
    14

    @JohnLeo #13


    搞技术研究的人你无法想象他们的执着。


    困在技术领域的牢笼里,柴油酱醋油盐不进


    ^-^

  • clawoo 08-17 00:04
    15

    ^-^

  • iceberg 08-17 00:25
    16

    ^-^

  • GeekAnt 08-17 02:20
    17

    @80hou #7


    ^-^

* 帖子来源NodeSeek
返回