使用方法同原帖:(修复429速率限制)一个可以展示用户发帖数、回帖数、注册天数、信任等级的油猴脚本 - 开发调优 - LINUX DO
感谢原作者的辛勤付出
// ==UserScript==
// @name idcflare.com 注册信息辅助
// @namespace http://tampermonkey.net/
// @version 1.3
// @description 在帖子详情页发帖人用户名旁展示注册天数、发帖数、回帖数与信任级别。
// @author
// @match https://idcflare.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const PROFILE_API_BUILDERS = [
(slug) => `https://idcflare.com/u/${encodeURIComponent(slug)}.json`,
(slug) => `https://idcflare.com/users/${encodeURIComponent(slug)}.json`,
];
const SUMMARY_API_BUILDERS = [
(slug) => `https://idcflare.com/u/${encodeURIComponent(slug)}/summary.json`,
(slug) => `https://idcflare.com/users/${encodeURIComponent(slug)}/summary.json`,
];
const TRUST_LEVEL_LABELS = {
0: 'Lv0 新手',
1: 'Lv1 入门',
2: 'Lv2 成员',
3: 'Lv3 常驻',
4: 'Lv4 领袖',
};
const TRUST_LEVEL_ALIAS = {
newuser: 0,
basic: 1,
member: 2,
regular: 3,
leader: 4,
};
const userInfoCache = new Map();
const pendingRequests = new Map();
const MARKER_CLASS = 'tm-register-days';
const BUTTON_CLASS = 'tm-register-days-button';
const PAGE_TOGGLE_CONTAINER_ID = 'tm-register-days-toggle-container';
const PAGE_TOGGLE_ID = 'tm-register-days-toggle';
const PAGE_TOGGLE_HIDE_ID = 'tm-register-days-toggle-hide';
const DATA_ATTR_USER = 'data-tm-user';
const DATA_ATTR_ORIGINAL_USER = 'data-tm-original-user';
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const COLLAPSE_STATE_STORAGE_KEY = 'tm-register-days-toggle-collapsed';
const CONTAINER_TOP_STORAGE_KEY = 'tm-register-days-toggle-top';
const CONTAINER_VERTICAL_MARGIN = 20;
const DRAG_ACTIVATION_THRESHOLD_PX = 4;
let enhanceScheduled = false;
let revealInProgress = false;
let pageToggleCollapsed = loadInitialCollapseState();
let storedContainerTop = loadInitialContainerTop();
let containerDragState = null;
let suppressClickAfterDrag = false;
const globalObserver = new MutationObserver(() => {
scheduleEnhance();
});
function scheduleEnhance() {
if (enhanceScheduled) {
return;
}
enhanceScheduled = true;
requestAnimationFrame(() => {
enhanceScheduled = false;
enhanceTopicPage();
});
}
function isTopicPage() {
return window.location.pathname.startsWith('/t/');
}
function loadInitialCollapseState() {
// 从本地存储恢复开关的隐藏状态,避免用户每次刷新后重置
if (!supportsLocalStorage()) {
return false;
}
try {
return window.localStorage.getItem(COLLAPSE_STATE_STORAGE_KEY) === '1';
} catch (error) {
console.debug('[idcflare.com 注册信息脚本] 读取按钮隐藏状态失败:', error);
return false;
}
}
function loadInitialContainerTop() {
// 从本地存储读取按钮纵向位置,保证拖动后刷新仍保留
if (!supportsLocalStorage()) {
return null;
}
try {
const rawValue = window.localStorage.getItem(CONTAINER_TOP_STORAGE_KEY);
if (rawValue === null) {
return null;
}
const parsed = Number.parseFloat(rawValue);
return Number.isFinite(parsed) ? parsed : null;
} catch (error) {
console.debug('[idcflare.com 注册信息脚本] 读取按钮位置失败:', error);
return null;
}
}
function supportsLocalStorage() {
// 兼容部分隐私模式或禁用存储的浏览器环境
try {
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
} catch (error) {
return false;
}
}
function persistCollapseState(collapsed) {
// 将隐藏状态写入本地存储,方便用户下次访问保持一致
if (!supportsLocalStorage()) {
return;
}
try {
window.localStorage.setItem(COLLAPSE_STATE_STORAGE_KEY, collapsed ? '1' : '0');
} catch (error) {
console.debug('[idcflare.com 注册信息脚本] 保存按钮隐藏状态失败:', error);
}
}
function persistContainerTop(position) {
// 持久化按钮的纵向位置,避免拖动后刷新失效
if (!supportsLocalStorage()) {
return;
}
try {
window.localStorage.setItem(CONTAINER_TOP_STORAGE_KEY, String(position));
} catch (error) {
console.debug('[idcflare.com 注册信息脚本] 保存按钮位置失败:', error);
}
}
function clampContainerTop(container, targetTop) {
// 避免按钮拖出屏幕之外,限定在顶部与底部之间
if (!(container instanceof HTMLElement)) {
return targetTop;
}
const docElement = document.documentElement;
const docClientHeight = docElement ? docElement.clientHeight : 0;
const viewportHeight = Math.max(window.innerHeight || 0, docClientHeight || 0);
const containerHeight = container.offsetHeight || 0;
if (viewportHeight <= 0) {
return targetTop;
}
const maxTop = Math.max(CONTAINER_VERTICAL_MARGIN, viewportHeight - containerHeight - CONTAINER_VERTICAL_MARGIN);
const bounded = Math.min(Math.max(targetTop, CONTAINER_VERTICAL_MARGIN), maxTop);
return Number.isFinite(bounded) ? bounded : targetTop;
}
function applyStoredContainerPosition(container) {
// 根据当前存储的 top 值更新容器位置
if (!(container instanceof HTMLElement)) {
return;
}
if (typeof storedContainerTop === 'number') {
const clamped = clampContainerTop(container, storedContainerTop);
storedContainerTop = clamped;
container.style.top = `${clamped}px`;
container.style.bottom = 'auto';
} else {
container.style.top = 'auto';
container.style.bottom = `${CONTAINER_VERTICAL_MARGIN}px`;
}
}
function handleViewportResize() {
// 浏览器窗口尺寸变化时重新修正按钮位置
const container = document.getElementById(PAGE_TOGGLE_CONTAINER_ID);
if (!(container instanceof HTMLElement)) {
return;
}
if (typeof storedContainerTop === 'number') {
const clamped = clampContainerTop(container, storedContainerTop);
storedContainerTop = clamped;
container.style.top = `${clamped}px`;
container.style.bottom = 'auto';
}
}
function attachContainerDragHandlers(container) {
// 仅在首次创建容器时挂载拖动相关事件
if (!(container instanceof HTMLElement)) {
return;
}
if (container.dataset.tmDragAttached === '1') {
return;
}
container.dataset.tmDragAttached = '1';
container.addEventListener('pointerdown', handleContainerPointerDown);
container.addEventListener('pointermove', handleContainerPointerMove);
container.addEventListener('pointerup', handleContainerPointerUp);
container.addEventListener('pointercancel', handleContainerPointerCancel);
container.addEventListener('click', handleContainerClickCapture, true);
}
function handleContainerPointerDown(event) {
// 记录指针初始位置,为拖动做好准备
if (!(event instanceof PointerEvent)) {
return;
}
if (event.pointerType === 'mouse' && event.button !== 0) {
return;
}
const container = document.getElementById(PAGE_TOGGLE_CONTAINER_ID);
if (!(container instanceof HTMLElement)) {
return;
}
const rect = container.getBoundingClientRect();
const initialTop = typeof storedContainerTop === 'number' ? storedContainerTop : rect.top;
containerDragState = {
pointerId: event.pointerId,
startClientY: event.clientY,
initialTop,
container,
isDragging: false,
originalTransition: container.style.transition,
};
}
function handleContainerPointerMove(event) {
// 拖动过程中更新按钮展示位置
if (!(event instanceof PointerEvent) || !containerDragState || event.pointerId !== containerDragState.pointerId) {
return;
}
const deltaY = event.clientY - containerDragState.startClientY;
if (!containerDragState.isDragging) {
if (Math.abs(deltaY) < DRAG_ACTIVATION_THRESHOLD_PX) {
return;
}
containerDragState.isDragging = true;
containerDragState.container.style.transition = 'none';
containerDragState.container.style.bottom = 'auto';
const currentRect = containerDragState.container.getBoundingClientRect();
const currentTop = currentRect.top;
containerDragState.container.style.top = `${currentTop}px`;
containerDragState.initialTop = currentTop;
storedContainerTop = currentTop;
try {
containerDragState.container.setPointerCapture(event.pointerId);
} catch (error) {
// 某些环境不支持 setPointerCapture,忽略即可
}
}
event.preventDefault();
const nextTop = clampContainerTop(containerDragState.container, containerDragState.initialTop + deltaY);
storedContainerTop = nextTop;
containerDragState.container.style.top = `${nextTop}px`;
}
function finishContainerDrag(cancelled) {
// 在指针结束或被取消时收尾拖动逻辑
if (!containerDragState) {
return;
}
const { container, pointerId, isDragging, originalTransition } = containerDragState;
if (typeof originalTransition === 'string') {
container.style.transition = originalTransition;
} else {
container.style.transition = '';
}
if (isDragging) {
const rect = container.getBoundingClientRect();
const clamped = clampContainerTop(container, rect.top);
storedContainerTop = clamped;
container.style.top = `${clamped}px`;
container.style.bottom = 'auto';
if (!cancelled) {
persistContainerTop(clamped);
suppressClickAfterDrag = true;
}
}
try {
if (container.hasPointerCapture(pointerId)) {
container.releasePointerCapture(pointerId);
}
} catch (error) {
// Safari 等浏览器可能抛出异常,忽略即可
}
containerDragState = null;
}
function handleContainerPointerUp(event) {
// 指针抬起时结束拖动
if (!(event instanceof PointerEvent) || !containerDragState || event.pointerId !== containerDragState.pointerId) {
return;
}
const wasDragging = containerDragState.isDragging;
finishContainerDrag(false);
if (wasDragging) {
event.preventDefault();
}
}
function handleContainerPointerCancel(event) {
// 指针被取消时归位状态
if (!(event instanceof PointerEvent) || !containerDragState || event.pointerId !== containerDragState.pointerId) {
return;
}
finishContainerDrag(true);
}
function handleContainerClickCapture(event) {
// 拖动后立即触发的点击需要被抑制,防止误触按钮
if (!suppressClickAfterDrag) {
return;
}
suppressClickAfterDrag = false;
event.stopPropagation();
event.preventDefault();
}
function setPageToggleCollapsed(nextState) {
// 切换隐藏状态并立即刷新界面样式
pageToggleCollapsed = Boolean(nextState);
persistCollapseState(pageToggleCollapsed);
applyCollapseState();
}
function handleCollapseToggleClick(event) {
// 点击隐藏/展开开关时切换主按钮的可见性
event.preventDefault();
setPageToggleCollapsed(!pageToggleCollapsed);
}
function stylePageToggleContainer(container) {
// 固定位置的外层容器,负责放置主按钮与隐藏开关
container.style.position = 'fixed';
container.style.right = '20px';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.gap = '8px';
container.style.zIndex = '9999';
container.style.transition = 'right 0.2s ease, top 0.2s ease';
container.style.bottom = `${CONTAINER_VERTICAL_MARGIN}px`;
}
function styleCollapseToggle(button) {
// 隐藏/展开的小开关保持与主按钮一致的玻璃风格
button.style.padding = '10px 14px';
button.style.borderRadius = '18px';
button.style.border = '1px solid rgba(255, 255, 255, 0.45)';
button.style.background = 'rgba(255, 255, 255, 0.25)';
button.style.backdropFilter = 'blur(20px) saturate(160%)';
button.style.webkitBackdropFilter = 'blur(20px) saturate(160%)';
button.style.boxShadow = '0 8px 24px rgba(15, 23, 42, 0.25)';
button.style.color = '#0f172a';
button.style.fontSize = '12px';
button.style.fontWeight = '600';
button.style.cursor = 'pointer';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.minWidth = '56px';
button.style.height = '36px';
button.style.letterSpacing = '0.4px';
button.style.transition = 'transform 0.2s ease, box-shadow 0.2s ease';
button.title = '隐藏或展开“展示已加载用户信息”按钮';
button.addEventListener('mouseenter', () => {
button.style.transform = 'translateY(-2px)';
button.style.boxShadow = '0 12px 28px rgba(15, 23, 42, 0.25)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'none';
button.style.boxShadow = '0 8px 24px rgba(15, 23, 42, 0.25)';
});
}
function applyCollapseState() {
// 根据隐藏状态调整主按钮与开关的位置与文案
const container = document.getElementById(PAGE_TOGGLE_CONTAINER_ID);
const mainButton = document.getElementById(PAGE_TOGGLE_ID);
const collapseToggle = document.getElementById(PAGE_TOGGLE_HIDE_ID);
if (!(container instanceof HTMLElement) || !(collapseToggle instanceof HTMLButtonElement)) {
return;
}
if (pageToggleCollapsed) {
if (mainButton instanceof HTMLButtonElement) {
mainButton.style.display = 'none';
}
container.style.right = '-6px';
container.style.gap = '0';
collapseToggle.textContent = '展开';
collapseToggle.setAttribute('aria-label', '展开“展示已加载用户信息”按钮');
collapseToggle.style.borderRadius = '18px 0 0 18px';
collapseToggle.style.padding = '10px 18px';
} else {
if (mainButton instanceof HTMLButtonElement) {
mainButton.style.display = 'flex';
}
container.style.right = '20px';
container.style.gap = '8px';
collapseToggle.textContent = '隐藏';
collapseToggle.setAttribute('aria-label', '隐藏“展示已加载用户信息”按钮');
collapseToggle.style.borderRadius = '18px';
collapseToggle.style.padding = '10px 14px';
}
collapseToggle.style.display = 'flex';
}
function ensurePageToggle() {
// 在页面右下角创建玻璃感主按钮及其隐藏开关
if (!isTopicPage()) {
removePageToggle();
return;
}
if (!document.body) {
return;
}
let container = document.getElementById(PAGE_TOGGLE_CONTAINER_ID);
if (!(container instanceof HTMLElement)) {
container = document.createElement('div');
container.id = PAGE_TOGGLE_CONTAINER_ID;
stylePageToggleContainer(container);
document.body.appendChild(container);
}
attachContainerDragHandlers(container);
let button = document.getElementById(PAGE_TOGGLE_ID);
if (!(button instanceof HTMLButtonElement)) {
button = document.createElement('button');
button.id = PAGE_TOGGLE_ID;
button.type = 'button';
stylePageToggle(button);
button.addEventListener('click', handlePageToggleClick);
container.appendChild(button);
} else if (button.parentElement !== container) {
container.appendChild(button);
}
let collapseToggle = document.getElementById(PAGE_TOGGLE_HIDE_ID);
if (!(collapseToggle instanceof HTMLButtonElement)) {
collapseToggle = document.createElement('button');
collapseToggle.id = PAGE_TOGGLE_HIDE_ID;
collapseToggle.type = 'button';
styleCollapseToggle(collapseToggle);
collapseToggle.addEventListener('click', handleCollapseToggleClick);
container.appendChild(collapseToggle);
} else if (collapseToggle.parentElement !== container) {
container.appendChild(collapseToggle);
}
updatePageToggle();
applyStoredContainerPosition(container);
}
function removePageToggle() {
// 在离开帖子详情页时移除容器,避免在其他页面残留
const container = document.getElementById(PAGE_TOGGLE_CONTAINER_ID);
if (container) {
container.remove();
}
}
function stylePageToggle(button) {
// 主按钮保留玻璃风格外观,布局交由容器负责
button.style.padding = '12px 18px';
button.style.borderRadius = '18px';
button.style.border = '1px solid rgba(255, 255, 255, 0.45)';
button.style.background = 'rgba(255, 255, 255, 0.25)';
button.style.backdropFilter = 'blur(20px) saturate(160%)';
button.style.webkitBackdropFilter = 'blur(20px) saturate(160%)';
button.style.boxShadow = '0 8px 24px rgba(15, 23, 42, 0.25)';
button.style.color = '#0f172a';
button.style.fontSize = '14px';
button.style.fontWeight = '600';
button.style.cursor = 'pointer';
button.style.transition = 'transform 0.2s ease, box-shadow 0.2s ease';
button.style.letterSpacing = '0.4px';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.gap = '8px';
button.style.flexShrink = '0';
button.title = '仅针对当前已加载的回复展示用户信息';
button.addEventListener('mouseenter', () => {
button.style.transform = 'translateY(-2px)';
button.style.boxShadow = '0 12px 28px rgba(15, 23, 42, 0.25)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'none';
button.style.boxShadow = '0 8px 24px rgba(15, 23, 42, 0.25)';
});
}
function updatePageToggle() {
const container = document.getElementById(PAGE_TOGGLE_CONTAINER_ID);
const button = document.getElementById(PAGE_TOGGLE_ID);
if (!(container instanceof HTMLElement) || !(button instanceof HTMLButtonElement)) {
return;
}
if (!isTopicPage()) {
if (container.style.display !== 'none') {
container.style.display = 'none';
}
return;
}
if (container.style.display !== 'flex') {
container.style.display = 'flex';
}
const nextText = revealInProgress ? '加载中...' : '展示已加载用户信息';
const nextDisabled = revealInProgress;
const nextAriaBusy = revealInProgress ? 'true' : 'false';
if (button.textContent !== nextText) {
// 仅在状态改变时更新文案,避免滚动触发的重绘闪烁
button.textContent = nextText;
}
if (button.disabled !== nextDisabled) {
button.disabled = nextDisabled;
}
if (button.getAttribute('aria-busy') !== nextAriaBusy) {
button.setAttribute('aria-busy', nextAriaBusy);
}
applyCollapseState();
}
async function handlePageToggleClick(event) {
event.preventDefault();
if (revealInProgress) {
return;
}
revealInProgress = true;
updatePageToggle();
try {
await revealVisibleReplies();
} catch (error) {
console.debug('[idcflare.com 注册信息脚本] 展示用户信息失败:', error);
} finally {
revealInProgress = false;
updatePageToggle();
}
}
async function revealVisibleReplies() {
// 依次展示当前已加载的回复用户信息,避免并发请求造成闪烁
const articles = document.querySelectorAll('.topic-post article');
for (let index = 0; index < articles.length; index += 1) {
const article = articles[index];
if (isOriginalArticle(article, index)) {
continue;
}
const anchor = article.querySelector('.names a[data-user-card]');
if (!(anchor instanceof HTMLElement)) {
continue;
}
const slug = anchor.getAttribute('data-user-card');
if (!slug) {
continue;
}
const normalizedSlug = slug.trim().toLowerCase();
if (!normalizedSlug) {
continue;
}
const alreadyDisplayed = article.querySelector(
`.${MARKER_CLASS}[${DATA_ATTR_USER}="${normalizedSlug}"]`,
);
if (alreadyDisplayed) {
removeInfoButton(anchor, normalizedSlug);
continue;
}
const success = await loadUserInfoForAnchor(anchor, slug, normalizedSlug);
if (success) {
removeInfoButton(anchor, normalizedSlug);
}
}
}
async function loadUserInfoForAnchor(anchor, slug, normalizedSlug) {
const normalized = normalizedSlug || (slug || '').trim().toLowerCase();
if (!normalized) {
return false;
}
const article = anchor.closest('.topic-post article');
if (article && article.querySelector(`.${MARKER_CLASS}[${DATA_ATTR_USER}="${normalized}"]`)) {
return true;
}
if (anchor.dataset.tmRegisterDaysLoading === '1') {
return false;
}
anchor.dataset.tmRegisterDaysLoading = '1';
try {
const info = await fetchUserInfo(slug);
if (!info) {
return false;
}
const infoNode = buildInlineInfoNode(info);
if (!infoNode) {
return false;
}
anchor.insertAdjacentElement('afterend', infoNode);
return true;
} finally {
delete anchor.dataset.tmRegisterDaysLoading;
}
}
function isOriginalArticle(article, index) {
const postWrapper = article.closest('.topic-post');
const postNumber = postWrapper && postWrapper.getAttribute('data-post-number');
return (
postNumber === '1' ||
(postWrapper && postWrapper.classList.contains('topic-owner')) ||
index === 0
);
}
async function enhanceTopicPage() {
if (!isTopicPage()) {
removePageToggle();
return;
}
ensurePageToggle();
document
.querySelectorAll(`.topic-post .${MARKER_CLASS}`)
.forEach((node) => {
if (!node.closest('.names')) {
node.remove();
}
});
const articles = document.querySelectorAll('.topic-post article');
for (let articleIndex = 0; articleIndex < articles.length; articleIndex += 1) {
const article = articles[articleIndex];
const anchor = article.querySelector('.names a[data-user-card]');
if (!(anchor instanceof HTMLElement)) {
continue;
}
const slug = anchor.getAttribute('data-user-card');
if (!slug) {
continue;
}
const normalizedSlug = slug.trim().toLowerCase();
if (!normalizedSlug) {
continue;
}
const isOriginalPost = isOriginalArticle(article, articleIndex);
if (article.querySelector(`.${MARKER_CLASS}[${DATA_ATTR_USER}="${normalizedSlug}"]`)) {
removeInfoButton(anchor, normalizedSlug);
continue;
}
if (anchor.dataset.tmRegisterDaysLoading === '1') {
continue;
}
if (isOriginalPost || userInfoCache.has(normalizedSlug)) {
const loaded = await loadUserInfoForAnchor(anchor, slug, normalizedSlug);
if (loaded) {
removeInfoButton(anchor, normalizedSlug);
} else if (!isOriginalPost) {
ensureInfoButton(anchor, normalizedSlug, slug);
}
continue;
}
ensureInfoButton(anchor, normalizedSlug, slug);
}
}
function ensureInfoButton(anchor, normalizedSlug, rawSlug) {
// 为回复用户创建按需查询按钮,避免首次就触发接口请求
const namesContainer = anchor.closest('.names');
if (!namesContainer) {
return;
}
if (namesContainer.querySelector(`.${BUTTON_CLASS}[${DATA_ATTR_USER}="${normalizedSlug}"]`)) {
return;
}
const button = document.createElement('button');
button.type = 'button';
button.className = BUTTON_CLASS;
button.setAttribute(DATA_ATTR_USER, normalizedSlug);
button.setAttribute(DATA_ATTR_ORIGINAL_USER, rawSlug);
button.textContent = '查看用户信息';
button.style.marginLeft = '6px';
button.style.fontSize = '12px';
button.style.color = '#1a4c7c';
button.style.cursor = 'pointer';
button.style.background = 'none';
button.style.border = 'none';
button.style.padding = '0';
button.addEventListener('click', handleInfoButtonClick);
anchor.insertAdjacentElement('afterend', button);
}
function removeInfoButton(anchor, normalizedSlug) {
// 在信息已展示后移除触发按钮
const namesContainer = anchor.closest('.names');
if (!namesContainer) {
return;
}
const button = namesContainer.querySelector(`.${BUTTON_CLASS}[${DATA_ATTR_USER}="${normalizedSlug}"]`);
if (button) {
button.remove();
}
}
async function handleInfoButtonClick(event) {
// 点击按钮时拉取用户数据,并在成功后移除按钮
const button = event.currentTarget;
if (!(button instanceof HTMLButtonElement)) {
return;
}
if (button.dataset.tmRegisterDaysLoading === '1') {
return;
}
const rawSlug = button.getAttribute(DATA_ATTR_ORIGINAL_USER);
const normalizedSlug = button.getAttribute(DATA_ATTR_USER);
if (!rawSlug || !normalizedSlug) {
return;
}
button.dataset.tmRegisterDaysLoading = '1';
button.textContent = '加载中...';
button.disabled = true;
try {
const anchor = identifyAnchorForButton(button);
if (!anchor) {
button.remove();
return;
}
const loaded = await loadUserInfoForAnchor(anchor, rawSlug, normalizedSlug);
if (!loaded) {
button.textContent = '重试';
button.disabled = false;
return;
}
removeInfoButton(anchor, normalizedSlug);
} catch (error) {
console.debug('[idcflare.com 注册信息脚本] 按钮查询失败:', rawSlug, error);
button.textContent = '重试';
button.disabled = false;
} finally {
delete button.dataset.tmRegisterDaysLoading;
}
}
function identifyAnchorForButton(button) {
// 通过按钮位置回溯到用户链接,确保信息节点插入位置正确
const previous = button.previousElementSibling;
if (previous instanceof HTMLElement && previous.matches('.names a[data-user-card]')) {
return previous;
}
const namesContainer = button.closest('.names');
if (!namesContainer) {
return null;
}
const anchor = namesContainer.querySelector('a[data-user-card]');
return anchor instanceof HTMLElement ? anchor : null;
}
async function fetchUserInfo(slug) {
const normalized = (slug || '').trim().toLowerCase();
if (!normalized) {
return null;
}
if (userInfoCache.has(normalized)) {
return userInfoCache.get(normalized);
}
if (pendingRequests.has(normalized)) {
return pendingRequests.get(normalized);
}
const requestPromise = (async () => {
const [profileData, summaryData] = await Promise.all([
fetchFirstAvailable(PROFILE_API_BUILDERS, slug),
fetchFirstAvailable(SUMMARY_API_BUILDERS, slug),
]);
if (!profileData && !summaryData) {
return null;
}
const user = profileData && (profileData.user || profileData);
const summary = summaryData && (summaryData.user_summary || summaryData.summary || summaryData);
const createdAt = pickCreatedAt(user) || (summary && pickCreatedAt(summary));
if (!createdAt) {
return null;
}
const topicCount = pickFirstNumber(
user && (user.topic_count ?? user.topicCount),
summary && (summary.topic_count ?? summary.topics_count),
);
const totalPostCount = pickFirstNumber(
user && (user.post_count ?? user.postCount),
summary && (summary.post_count ?? summary.posts_count),
);
let repliesCount = pickFirstNumber(
summary && (summary.replies_count ?? summary.reply_count),
);
if (repliesCount === null && totalPostCount !== null && topicCount !== null) {
repliesCount = Math.max(0, totalPostCount - topicCount);
}
const trustLevelRaw = pickFirstValue(
user && (user.trust_level ?? user.trustLevel),
summary && (summary.trust_level ?? summary.trustLevel),
);
const trustLevel = normalizeTrustLevel(trustLevelRaw);
const info = {
slug: normalized,
createdAt,
days: calcDays(createdAt),
topicCount: isNumber(topicCount) ? topicCount : undefined,
repliesCount: isNumber(repliesCount) ? repliesCount : undefined,
trustLevel,
};
return info;
})();
pendingRequests.set(normalized, requestPromise);
try {
const info = await requestPromise;
if (info) {
userInfoCache.set(normalized, info);
}
return info;
} finally {
pendingRequests.delete(normalized);
}
}
async function fetchFirstAvailable(builders, slug) {
// 按顺序尝试候选接口,拿到第一个可用结果即停止,避免同时命中所有地址导致 4 次请求
for (const builder of builders) {
const url = builder(slug);
const data = await safeFetchJson(url);
if (data) {
return data;
}
}
return null;
}
async function safeFetchJson(url) {
try {
const response = await fetch(url, { credentials: 'include' });
if (!response.ok) {
return null;
}
return await response.json();
} catch (error) {
console.debug('[idcflare.com 注册信息脚本] 请求失败:', url, error);
return null;
}
}
function pickFirstNumber(...values) {
for (const value of values) {
const numberValue = Number(value);
if (!Number.isNaN(numberValue)) {
return numberValue;
}
}
return null;
}
function pickFirstValue(...values) {
for (const value of values) {
if (value !== undefined && value !== null) {
return value;
}
}
return null;
}
function normalizeTrustLevel(raw) {
if (raw === undefined || raw === null) {
return undefined;
}
if (typeof raw === 'number' && Number.isFinite(raw)) {
return raw;
}
if (typeof raw === 'string') {
const alias = TRUST_LEVEL_ALIAS[raw.toLowerCase()];
if (alias !== undefined) {
return alias;
}
const numeric = Number(raw);
if (!Number.isNaN(numeric)) {
return numeric;
}
}
return undefined;
}
function pickCreatedAt(source) {
if (!source) {
return null;
}
return (
source.created_at ||
source.createdAt ||
source.registration_date ||
source.registrationDate ||
source.joined ||
source.joinedAt ||
null
);
}
function calcDays(createdAt) {
const createdTime = new Date(createdAt).getTime();
if (Number.isNaN(createdTime)) {
return 0;
}
const diff = Date.now() - createdTime;
return Math.max(0, Math.floor(diff / DAY_IN_MS));
}
function buildInlineInfoNode(profileInfo) {
const segments = [`注册 ${formatNumber(profileInfo.days)} 天`];
if (isNumber(profileInfo.topicCount)) {
segments.push(`发帖 ${formatNumber(profileInfo.topicCount)}`);
}
if (isNumber(profileInfo.repliesCount)) {
segments.push(`回帖 ${formatNumber(profileInfo.repliesCount)}`);
}
if (isNumber(profileInfo.trustLevel)) {
const label = TRUST_LEVEL_LABELS[profileInfo.trustLevel];
segments.push(label || `信任级别 Lv${profileInfo.trustLevel}`);
}
if (!segments.length) {
return null;
}
const span = document.createElement('span');
span.className = MARKER_CLASS;
span.setAttribute(DATA_ATTR_USER, profileInfo.slug);
span.textContent = ` · ${segments.join(' · ')}`;
span.style.marginLeft = '6px';
span.style.fontSize = '12px';
span.style.color = '#1a4c7c';
return span;
}
function isNumber(value) {
return typeof value === 'number' && Number.isFinite(value);
}
function formatNumber(value) {
return Number(value).toLocaleString('zh-CN');
}
window.addEventListener('resize', handleViewportResize);
scheduleEnhance();
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
scheduleEnhance();
}
});
globalObserver.observe(document.body, { childList: true, subtree: true });
window.__tmLinuxDoCache = {
cache: userInfoCache,
pending: pendingRequests,
};
})();
