原脚本:NS 管理记录快捷查看
修改后版本:
// ==UserScript==
// @name NS 管理记录快捷查看
// @namespace ns-seek-overlay
// @version 0.3.6
// @description 在每层 comment-menu 增加“⚖️ 管理记录”按钮,点击查看该用户管理记录
// @match https://www.nodeseek.com/post-*
// @grant none
// @license MIT
// @downloadURL https://update.greasyfork.org/scripts/567915/NS%20%E7%AE%A1%E7%90%86%E8%AE%B0%E5%BD%95%E5%BF%AB%E6%8D%B7%E6%9F%A5%E7%9C%8B.user.js
// @updateURL https://update.greasyfork.org/scripts/567915/NS%20%E7%AE%A1%E7%90%86%E8%AE%B0%E5%BD%95%E5%BF%AB%E6%8D%B7%E6%9F%A5%E7%9C%8B.meta.js
// ==/UserScript==
(function () {
'use strict';
const API_BASES = [
'https://api.xxboxx.de',
'http://127.0.0.1:18081',
];
function esc(s) {
return String(s ?? '').replace(/</g, '<').replace(/>/g, '>');
}
function label(row) {
const p = row.action_points_delta;
if (typeof p === 'number') {
if (p > 0) return '🎉 奖励';
if (p < 0) return '⚠️ 处罚';
}
return '📝 管理';
}
// ---- 时区修正 ----
// 接口返回的 actions_text / raw_line 里夹带的时间是按 UTC 生成的
// en-US 格式(例如 "8/12/2026, 7:49:34 AM"),与北京时间正好差 8 小时。
// 这里把这种时间子串找出来,当作 UTC 解析后转换成北京时间重新拼回文本,
// 只影响显示,不改动接口原始数据。
const US_DATETIME_RE = /(\d{1,2})\/(\d{1,2})\/(\d{4}),\s*(\d{1,2}):(\d{2}):(\d{2})\s*(AM|PM)/gi;
function parseAsUTC(m) {
// m: [full, month, day, year, hour, minute, second, ap]
let [, month, day, year, hour, minute, second, ap] = m;
hour = parseInt(hour, 10);
if (/PM/i.test(ap) && hour !== 12) hour += 12;
if (/AM/i.test(ap) && hour === 12) hour = 0;
const ms = Date.UTC(+year, +month - 1, +day, hour, +minute, +second);
return new Date(ms);
}
function toBeijingStr(date) {
return date.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour12: false,
});
}
function fixTimezoneInText(text) {
if (!text) return text;
return text.replace(US_DATETIME_RE, (...args) => {
const m = args.slice(0, 8); // full match + 7 个捕获组
const d = parseAsUTC(m);
if (isNaN(d.getTime())) return m[0];
return toBeijingStr(d) + '(北京时间)';
});
}
function createModal() {
let wrap = document.getElementById('ns-seek-modal-wrap');
if (wrap) return wrap;
wrap = document.createElement('div');
wrap.id = 'ns-seek-modal-wrap';
wrap.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:99999;display:none;';
wrap.innerHTML = `
<div id="ns-seek-panel" style="max-width:760px;margin:6vh auto;background:#fff !important;color:#111 !important;border-radius:10px;padding:14px 16px;max-height:85vh;overflow:auto;box-shadow:0 12px 40px rgba(0,0,0,.25)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;color:#111 !important;">
<strong id="ns-seek-title" style="color:#111 !important;">管理记录</strong>
<button id="ns-seek-close" style="border:1px solid #ddd;border-radius:6px;padding:2px 8px;cursor:pointer;background:#fff !important;color:#111 !important;">关闭</button>
</div>
<div id="ns-seek-content" style="font-size:14px;line-height:1.6;color:#111 !important;"></div>
</div>`;
document.body.appendChild(wrap);
wrap.querySelector('#ns-seek-close').onclick = () => (wrap.style.display = 'none');
wrap.addEventListener('click', (e) => {
if (e.target === wrap) wrap.style.display = 'none';
});
return wrap;
}
function renderRows(username, rows) {
if (!rows || !rows.length) return `<div>未找到 <b>${esc(username)}</b> 的管理记录。</div>`;
return rows
.map(
(r) => `
<div style="padding:8px 0;border-bottom:1px solid #eee;color:#111 !important;">
<div style="color:#111 !important;"><b style="color:#111 !important;">${label(r)}</b> #${r.record_id}</div>
<div style="color:#111 !important;">原因:${esc(r.reason_text || '-')}</div>
<div style="color:#111 !important;">处理:${esc(fixTimezoneInText(r.actions_text) || '-')}</div>
<div style="color:#111 !important;">链接:${r.post_url ? `<a href="${r.post_url}" target="_blank" style="color:#0969da !important;">${esc(r.post_url)}</a>` : '-'}</div>
</div>`
)
.join('');
}
function detectViewer() {
const CACHE_KEY = 'ns_seek_viewer_cache';
// 精准:顶部菜单当前用户(你提供的 DOM)
const exact = document.querySelector('.menu a.Username[href*="/space/"]');
if (exact) {
const t = (exact.textContent || '').trim();
const m = (exact.getAttribute('href') || '').match(/\/space\/(\d+)/);
const v = { username: t, uid: m ? m[1] : '' };
try { localStorage.setItem(CACHE_KEY, JSON.stringify(v)); } catch (_) {}
return v;
}
// 回退:导航区域中包含设置/登出图标附近的 /space/ 链接
const nav = document.querySelector('header, .header, .navbar, .topbar, .nav, .site-header, body');
const links = nav ? Array.from(nav.querySelectorAll('a[href*="/space/"]')) : [];
for (const a of links) {
const scope = (a.closest('.menu, .topbar, header, .navbar') || a.parentElement || a);
const near = ((scope && scope.innerText) || '').toLowerCase();
const t = (a.textContent || '').trim();
if (!t) continue;
if (/设置|登出|退出|signout|logout/.test(near)) {
const m = (a.getAttribute('href') || '').match(/\/space\/(\d+)/);
const v = { username: t, uid: m ? m[1] : '' };
try { localStorage.setItem(CACHE_KEY, JSON.stringify(v)); } catch (_) {}
return v;
}
}
// 最后回退:用上次缓存,避免偶发识别不到导致管理员被限流
try {
const c = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}');
if (c && (c.username || c.uid)) return { username: c.username || '', uid: c.uid || '' };
} catch (_) {}
return { username: '', uid: '' };
}
function findUserLinkFromMenu(menu) {
const floor = menu.closest('.post-list-content, .post, .post-item, article, .topic-content') || menu.parentElement;
if (!floor) return null;
// 按出现顺序取本层第一个看起来像用户名的链接
const links = Array.from(floor.querySelectorAll('a[href*="/space/"]'));
for (const a of links) {
const t = (a.textContent || '').trim();
if (!t) continue;
if (/^(等级|鸡腿|星辰|回复|主题帖|评论数|粉丝|收藏|私信|引用|回复|编辑|置顶)$/i.test(t)) continue;
// 允许纯数字用户名,但排除过短数字(常见计数)
if (/^\d+$/.test(t) && t.length <= 2) continue;
// 用户名允许中文/字母/数字/下划线/中划线/点
if (!/^[\p{L}\p{N}_\-.]+$/u.test(t)) continue;
return a;
}
return null;
}
async function openFor(username) {
const wrap = createModal();
wrap.style.display = 'block';
wrap.querySelector('#ns-seek-title').textContent = `${username} 的管理记录`;
const content = wrap.querySelector('#ns-seek-content');
content.innerHTML = '加载中...';
const viewer = detectViewer();
let lastErr = null;
for (const base of API_BASES) {
try {
const url = `${base}/api/seek?username=${encodeURIComponent(username)}&limit=100000&viewer=${encodeURIComponent(
viewer.username
)}&viewer_uid=${encodeURIComponent(viewer.uid)}`;
const resp = await fetch(url, { cache: 'no-store' });
const data = await resp.json();
if (!data.ok) {
if (data.error === 'rate_limited') {
content.innerHTML = `查询过快:请稍后再试(约 ${data.retry_after || 60}s)`;
return;
}
throw new Error(data.error || 'query failed');
}
content.innerHTML = renderRows(username, data.rows);
return;
} catch (e) {
lastErr = e;
}
}
content.innerHTML = `查询失败:${String(lastErr)}(可能是 API 不可达)`;
}
function injectButtons() {
const menus = Array.from(document.querySelectorAll('.comment-menu'));
menus.forEach((menu) => {
if (menu.dataset.nsSeekBound === '1') return;
menu.dataset.nsSeekBound = '1';
const userLink = findUserLinkFromMenu(menu);
if (!userLink) return;
if (userLink.dataset.nsSeekBtn === '1') return;
userLink.dataset.nsSeekBtn = '1';
const username = (userLink.textContent || '').trim();
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = '⚖️ 管理记录';
btn.style.cssText = [
'margin-left:6px',
'padding:1px 6px',
'font-size:12px',
'line-height:18px',
'border:1px solid #d0d7de',
'border-radius:10px',
'background:#f6f8fa',
'color:#24292f',
'cursor:pointer',
'vertical-align:middle'
].join(';');
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
openFor(username);
});
userLink.insertAdjacentElement('afterend', btn);
});
}
injectButtons();
new MutationObserver(() => injectButtons()).observe(document.body, { childList: true, subtree: true });
})();
修改思路:加一个fixTimezoneInText()函数,用正则抓出文本里M/D/YYYY, H:MM:SS AM/PM这种美式时间格式,设置UTC时间解析,再转成时间重新拼回文本里。只处理actions_text这一处显示,而不返回接口的原始数据。
// 抓取来的时间字符串是按 UTC 生成的 en-US 格式(如 "8/12/2026, 7:49:34 AM"),
// 与北京时间正好差 8 小时。这里把这类时间子串找出来,按 UTC 解析后加 8 小时,
// 再重新格式化为北京时间显示,不改动其余文字。
const US_DATETIME_RE = /(\d{1,2})\/(\d{1,2})\/(\d{4}),\s*(\d{1,2}):(\d{2}):(\d{2})\s*(AM|PM)/gi;
function parseAsUTC(m) {
let [, month, day, year, hour, minute, second, ap] = m;
hour = parseInt(hour, 10);
if (/PM/i.test(ap) && hour !== 12) hour += 12;
if (/AM/i.test(ap) && hour === 12) hour = 0;
const ms = Date.UTC(+year, +month - 1, +day, hour, +minute, +second);
return new Date(ms);
}
function toBeijingStr(date) {
return date.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour12: false,
});
}
function fixTimezoneInText(text) {
if (!text) return text;
return text.replace(US_DATETIME_RE, (...args) => {
const m = args.slice(0, 8); // full match + 7 分组
const d = parseAsUTC(m);
if (isNaN(d.getTime())) return m[0];
return toBeijingStr(d) + '(北京时间)';
});
}
然后把renderRows显示actions_text的那一行,从:
<div style="color:#111 !important;">处理:${esc(r.actions_text || '-')}</div>
改成:
<div style="color:#111 !important;">处理:${esc(fixTimezoneInText(r.actions_text) || '-')}</div>