自加入Ns时候就开始了收集星辰的玩法,刷星辰兑换邀请码这种脑回路我是万万没想到的.
油猴插件:NodeSeek 全量星辰账本分析器 (完整校准版)
// ==UserScript==
// @name NodeSeek 全量星辰账本分析器 (完整校准版)
// @namespace https://www.nodeseek.com/
// @version 4.6.0
// @description 全量精准核算星辰账本:校准下一页按钮、数学核算真实余额(1810)、导出MD前自动并发补齐100%全量用户真实昵称、秒级时间戳、转账收支(+388/-520)、获赠点赞(+1),支持包含贡献者统计小计的 tabs Markdown 导出
// @author Sherlock
// @match https://www.nodeseek.com/*
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
COUNT_PER_REQ: 1000,
PAGE_SIZE: 50,
DELAY_MS: 300,
USER_FETCH_DELAY_MS: 100,
BATCH_CONCURRENCY: 5
};
GM_addStyle(`
#ns-stardust-btn {
position: fixed;
bottom: 85px;
right: 20px;
z-index: 9999;
background: #2563eb;
color: #fff;
padding: 9px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 12px rgba(37,99,235,0.35);
transition: all 0.2s ease;
border: 1px solid rgba(255,255,255,0.2);
user-select: none;
}
#ns-stardust-btn:hover {
background: #1d4ed8;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(37,99,235,0.45);
}
#ns-modal-mask {
display: none;
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0,0,0,0.65);
z-index: 10000;
backdrop-filter: blur(4px);
justify-content: center;
align-items: center;
}
#ns-stardust-modal {
background: #1e222d;
color: #e2e8f0;
width: 92%;
max-width: 1260px;
height: 85vh;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
border: 1px solid #334155;
}
.ns-modal-header {
padding: 14px 20px;
border-bottom: 1px solid #334155;
display: flex;
justify-content: space-between;
align-items: center;
background: #181b24;
}
.ns-modal-title { font-size: 16px; font-weight: bold; color: #60a5fa; }
.ns-close-btn { cursor: pointer; font-size: 20px; color: #94a3b8; line-height: 1; }
.ns-close-btn:hover { color: #fff; }
.ns-stats-bar {
padding: 10px 20px;
background: #14171f;
display: flex;
gap: 10px;
flex-wrap: wrap;
border-bottom: 1px solid #334155;
font-size: 13px;
}
.ns-stat-item { background: #1e293b; padding: 5px 10px; border-radius: 6px; }
.ns-stat-val { font-weight: bold; margin-left: 4px; }
.val-balance { color: #38bdf8; font-size: 14px; font-weight: 800; }
.val-plus { color: #4ade80; }
.val-minus { color: #f87171; }
.ns-controls {
padding: 10px 20px;
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
background: #181b24;
border-bottom: 1px solid #334155;
}
.ns-tab-btn {
background: #334155;
color: #cbd5e1;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.ns-tab-btn.active { background: #2563eb; color: #fff; font-weight: bold; }
.ns-search-input {
background: #0f172a;
border: 1px solid #334155;
color: #fff;
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
margin-left: auto;
outline: none;
width: 200px;
}
.ns-table-container {
flex: 1;
overflow-y: auto;
padding: 0;
}
.ns-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
text-align: left;
}
.ns-table th {
position: sticky;
top: 0;
background: #1e293b;
color: #94a3b8;
padding: 10px 14px;
border-bottom: 1px solid #334155;
z-index: 1;
}
.ns-table td {
padding: 9px 14px;
border-bottom: 1px solid #242b38;
color: #cbd5e1;
vertical-align: middle;
}
.ns-table tr:hover { background: rgba(255,255,255,0.04); }
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.badge-upvote-in { background: rgba(34, 197, 94, 0.2); color: #86efac; border: 1px solid rgba(34, 197, 94, 0.4); }
.badge-transfer-out { background: rgba(239, 68, 68, 0.2); color: #fca5a5; border: 1px solid rgba(239, 68, 68, 0.4); }
.badge-transfer-in { background: rgba(59, 130, 246, 0.2); color: #93c5fd; border: 1px solid rgba(59, 130, 246, 0.4); }
.ns-time-cell {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: #94a3b8;
font-size: 11.5px;
white-space: nowrap;
}
.ns-uid-link {
color: #60a5fa;
text-decoration: none;
font-family: monospace;
font-size: 12px;
}
.ns-uid-link:hover {
color: #93c5fd;
text-decoration: underline;
}
.ns-user-tag {
color: #38bdf8;
font-weight: 600;
text-decoration: none;
background: rgba(56, 189, 248, 0.12);
padding: 2px 6px;
border-radius: 4px;
border: 1px solid rgba(56, 189, 248, 0.25);
display: inline-block;
margin-right: 8px;
}
.ns-user-tag:hover {
color: #7dd3fc;
background: rgba(56, 189, 248, 0.2);
text-decoration: none;
}
.ns-balance-tag {
color: #94a3b8;
font-size: 11px;
background: #0f172a;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #334155;
margin-right: 6px;
font-family: monospace;
}
.ns-memo-tag {
color: #f1f5f9;
margin-left: 4px;
}
.ns-pagination-bar {
padding: 8px 20px;
background: #181b24;
border-top: 1px solid #334155;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #94a3b8;
}
.ns-page-controls { display: flex; gap: 8px; }
.ns-page-btn {
background: #334155;
color: #fff;
border: none;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
}
.ns-page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.ns-progress { padding: 40px; text-align: center; color: #94a3b8; font-size: 14px; }
`);
let cachedRecords = [];
let currentFilter = 'all';
let currentPage = 1;
let myUid = null;
let currentWalletBalance = 0;
const userCache = new Map();
let isFetchingUsers = false;
function getBalanceFromSidebar() {
try {
const sidebar = document.querySelector('.sidebar, #sidebar, .user-card, .menu-list, .dashboard-container');
const targetText = sidebar ? sidebar.innerText : '';
const match = targetText.match(/星辰\s*[::]?\s*([0-9]+)/);
if (match && match[1]) {
const num = parseInt(match[1], 10);
if (!isNaN(num) && num > 0) return num;
}
} catch (e) {}
return 0;
}
async function fetchSingleUser(uid) {
try {
const res = await fetch(`https://www.nodeseek.com/api/account/getInfo/${uid}`);
const data = await res.json();
if (data && data.success && data.detail && data.detail.member_name) {
userCache.set(uid, data.detail.member_name);
} else {
userCache.set(uid, `UID:${uid}`);
}
} catch (e) {
userCache.set(uid, `UID:${uid}`);
}
}
async function resolveVisibleUserNames(uids) {
if (isFetchingUsers) return;
const missingUids = [...new Set(uids)].filter(id => id && !userCache.has(id));
if (missingUids.length === 0) return;
isFetchingUsers = true;
for (const uid of missingUids) {
await fetchSingleUser(uid);
document.querySelectorAll(`.ns-user-tag[data-uid="${uid}"]`).forEach(el => {
const name = userCache.get(uid);
el.innerText = (name && !name.startsWith('UID:')) ? `@${name}` : `@UID:${uid}`;
});
await new Promise(r => setTimeout(r, CONFIG.USER_FETCH_DELAY_MS));
}
isFetchingUsers = false;
}
function formatExactTime(val) {
if (!val) return '-';
let d;
if (typeof val === 'number') {
d = new Date(val > 1e11 ? val : val * 1000);
} else {
d = new Date(val);
}
if (isNaN(d.getTime())) return String(val);
const pad = (n) => String(n).padStart(2, '0');
const Y = d.getFullYear();
const M = pad(d.getMonth() + 1);
const D = pad(d.getDate());
const h = pad(d.getHours());
const m = pad(d.getMinutes());
const s = pad(d.getSeconds());
return `${Y}/${M}/${D} ${h}:${m}:${s}`;
}
function extractAmount(r) {
const priorityKeys = [
'amount', 'stardust', 'stardusts', 'count', 'delta', 'value', 'points',
'num', 'quantity', 'change', 'trade_stardust', 'credit', 'score', 'gain'
];
for (const k of priorityKeys) {
if (r[k] !== undefined && r[k] !== null && r[k] !== '') {
const num = parseFloat(String(r[k]).replace(/[^\d.-]/g, ''));
if (!isNaN(num) && num !== 0) return num;
}
}
const ignoreKeys = new Set([
'id', '_id', 'member_id', 'peer_id', 'ref_id', 'post_id', 'comment_id',
'created_at', 'updated_at', 'timestamp', 'time', 'status', 'code',
'floor', 'balance', 'remain', 'type'
]);
for (const [k, v] of Object.entries(r)) {
if (!ignoreKeys.has(k)) {
if (typeof v === 'number' && v !== 0) return v;
if (typeof v === 'string' && /^-?\d+(\.\d+)?$/.test(v.trim())) {
const parsed = parseFloat(v.trim());
if (!isNaN(parsed) && parsed !== 0) return parsed;
}
}
}
return 0;
}
function extractBalance(r) {
const keys = [
'balance', 'current_balance', 'remain', 'remain_stardust', 'after_balance',
'user_balance', 'stardust_balance', 'current_stardust', 'after', 'current',
'user_stardust', 'total', 'cur_balance', 'post_balance', 'remained', 'available',
'total_stardust'
];
for (const k of keys) {
if (r[k] !== undefined && r[k] !== null && r[k] !== '') {
const num = parseFloat(String(r[k]).replace(/[^\d.-]/g, ''));
if (!isNaN(num) && num >= 0) return num;
}
}
return null;
}
function formatDetailHtml(item) {
const parts = [];
if (item.targetUid) {
const cachedName = userCache.get(item.targetUid);
const showName = (cachedName && !cachedName.startsWith('UID:')) ? `@${cachedName}` : `@UID:${item.targetUid}`;
parts.push(`<a class="ns-user-tag" data-uid="${item.targetUid}" href="https://www.nodeseek.com/space/${item.targetUid}#/general" target="_blank" title="点击访问对方空间">${showName}</a>`);
}
if (item.balance !== null && item.balance !== undefined) {
parts.push(`<span class="ns-balance-tag">💰 变动后余额: ${item.balance}</span>`);
}
if (item.memo) {
parts.push(`<span class="ns-memo-tag">📝 留言: ${item.memo}</span>`);
}
return parts.length > 0 ? parts.join(' ') : '-';
}
function buildMarkdownTable(list) {
if (!list || list.length === 0) {
return `*(暂无相关记录)*\n\n`;
}
let md = `| 时间 | 类型 | 数额 | 对方 UID | 明细信息 (@对方用户名 / 变动后余额 / 留言) |\n`;
md += `| :--- | :--- | :--- | :--- | :--- |\n`;
list.forEach(item => {
let typeLabel = '';
if (item.category === 'upvote_in') typeLabel = '💖 收到点赞';
else if (item.category === 'transfer_out') typeLabel = '💸 送出转账';
else if (item.category === 'transfer_in') typeLabel = '💰 收到转账';
else typeLabel = '星辰流水';
const amountLabel = item.amount > 0 ? `+${item.amount}` : `${item.amount}`;
const uidLabel = item.targetUid ? `[UID: ${item.targetUid}](https://www.nodeseek.com/space/${item.targetUid}#/general)` : '-';
const detailParts = [];
if (item.targetUid) {
const name = userCache.get(item.targetUid);
const showName = (name && !name.startsWith('UID:')) ? `@${name}` : `@UID:${item.targetUid}`;
detailParts.push(`[${showName}](https://www.nodeseek.com/space/${item.targetUid}#/general)`);
}
if (item.balance !== null && item.balance !== undefined) {
detailParts.push(`余额: ${item.balance}`);
}
if (item.memo) {
detailParts.push(`留言: ${item.memo.replace(/\|/g, '/')}`);
}
const detailLabel = detailParts.length > 0 ? detailParts.join(' · ') : '-';
md += `| ${item.time} | ${typeLabel} | ${amountLabel} | ${uidLabel} | ${detailLabel} |\n`;
});
md += `\n`;
return md;
}
function buildUserSummaryTable(list, actionText) {
if (!list || list.length === 0) return '';
const userMap = new Map();
list.forEach(item => {
if (!item.targetUid) return;
if (!userMap.has(item.targetUid)) {
const rawName = userCache.get(item.targetUid);
const showName = (rawName && !rawName.startsWith('UID:')) ? `@${rawName}` : `@UID:${item.targetUid}`;
userMap.set(item.targetUid, {
uid: item.targetUid,
name: showName,
count: 0,
totalAmount: 0
});
}
const stat = userMap.get(item.targetUid);
stat.count += 1;
stat.totalAmount += Math.abs(item.amount);
});
const sortedUsers = Array.from(userMap.values()).sort((a, b) => b.count - a.count || b.totalAmount - a.totalAmount);
if (sortedUsers.length === 0) return '';
let md = `### 👑 贡献者统计排行 (小计共 ${sortedUsers.length} 位用户)\n\n`;
md += `| 排名 | 用户 (@昵称) | 对方 UID | ${actionText}次数 | 累计星辰 |\n`;
md += `| :--- | :--- | :--- | :--- | :--- |\n`;
sortedUsers.forEach((u, index) => {
const rank = index + 1;
const rankIcon = rank === 1 ? '🥇 1' : rank === 2 ? '🥈 2' : rank === 3 ? '🥉 3' : String(rank);
md += `| ${rankIcon} | [${u.name}](https://www.nodeseek.com/space/${u.uid}#/general) | [UID: ${u.uid}](https://www.nodeseek.com/space/${u.uid}#/general) | **${u.count} 次** | **${u.totalAmount}** |\n`;
});
md += `\n`;
return md;
}
async function copyMarkdownTabs() {
if (cachedRecords.length === 0) {
alert("暂无流水数据可导出,请先抓取数据!");
return;
}
const copyBtn = document.getElementById('ns-btn-copy-md');
const originText = copyBtn ? copyBtn.innerText : '📋 复制 Markdown (含小计)';
const allDistinctUids = [...new Set(cachedRecords.map(r => r.targetUid).filter(Boolean))];
const unparsedUids = allDistinctUids.filter(uid => !userCache.has(uid) || userCache.get(uid).startsWith('UID:'));
if (unparsedUids.length > 0) {
if (copyBtn) copyBtn.disabled = true;
let finishedCount = 0;
const totalToFetch = unparsedUids.length;
for (let i = 0; i < totalToFetch; i += CONFIG.BATCH_CONCURRENCY) {
const batch = unparsedUids.slice(i, i + CONFIG.BATCH_CONCURRENCY);
await Promise.all(batch.map(fetchSingleUser));
finishedCount += batch.length;
if (copyBtn) {
copyBtn.innerText = `⏳ 正在解析全量昵称 (${finishedCount}/${totalToFetch})...`;
}
await new Promise(r => setTimeout(r, 60));
}
if (copyBtn) copyBtn.disabled = false;
renderTable();
}
const upvoteInList = cachedRecords.filter(r => r.category === 'upvote_in');
const transferOutList = cachedRecords.filter(r => r.category === 'transfer_out');
const transferInList = cachedRecords.filter(r => r.category === 'transfer_in');
let totalIncome = 0;
let totalExpense = 0;
let upvoteInStardust = 0;
let transferOutStardust = 0;
let transferInStardust = 0;
cachedRecords.forEach(item => {
if (item.category === 'upvote_in') {
upvoteInStardust += item.amount;
totalIncome += item.amount;
} else if (item.category === 'transfer_in') {
transferInStardust += item.amount;
totalIncome += item.amount;
} else if (item.category === 'transfer_out') {
const absVal = Math.abs(item.amount);
transferOutStardust += absVal;
totalExpense += absVal;
}
});
const calculatedBal = totalIncome - totalExpense;
const displayBal = (calculatedBal > 0) ? calculatedBal : currentWalletBalance;
let fullMd = `:::: tabs\n`;
fullMd += `::: tab-item 💖 收到的点赞\n`;
fullMd += buildUserSummaryTable(upvoteInList, '获赠点赞');
fullMd += `### 📜 点赞明细记录\n\n`;
fullMd += buildMarkdownTable(upvoteInList);
fullMd += `:::\n`;
fullMd += `::: tab-item 💸 送出的转账\n`;
fullMd += buildUserSummaryTable(transferOutList, '转出');
fullMd += `### 📜 送出转账明细\n\n`;
fullMd += buildMarkdownTable(transferOutList);
fullMd += `:::\n`;
fullMd += `::: tab-item 💰 收到的转账\n`;
fullMd += buildUserSummaryTable(transferInList, '转入');
fullMd += `### 📜 收到转账明细\n\n`;
fullMd += buildMarkdownTable(transferInList);
fullMd += `:::\n`;
fullMd += `::: tab-item 🎁 全部流水\n`;
fullMd += `### 📊 账本总览汇总\n\n`;
fullMd += `- **当前星辰余额**: ${displayBal}\n`;
fullMd += `- **真实总流水**: ${cachedRecords.length} 条\n`;
fullMd += `- **累计收入**: +${totalIncome}\n`;
fullMd += `- **累计支出**: -${totalExpense}\n`;
fullMd += `- **收到点赞**: ${upvoteInList.length} 次 (+${upvoteInStardust})\n`;
fullMd += `- **送出转账**: ${transferOutList.length} 笔 (-${transferOutStardust})\n`;
fullMd += `- **收到转账**: ${transferInList.length} 笔 (+${transferInStardust})\n\n`;
fullMd += `### 📜 全部流水明细\n\n`;
fullMd += buildMarkdownTable(cachedRecords);
fullMd += `:::\n`;
fullMd += `::::\n`;
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(fullMd, 'text');
} else if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(fullMd);
}
if (copyBtn) {
copyBtn.innerText = "✅ 全量用户名 MD 已复制到剪贴板!";
copyBtn.style.background = "#059669";
setTimeout(() => {
copyBtn.innerText = originText;
copyBtn.style.background = "#7c3aed";
}, 2500);
}
}
function initUI() {
if (document.getElementById('ns-stardust-btn')) return;
const btn = document.createElement('div');
btn.id = 'ns-stardust-btn';
btn.innerHTML = `🌟 星辰账本`;
btn.onclick = openModal;
document.body.appendChild(btn);
const modalMask = document.createElement('div');
modalMask.id = 'ns-modal-mask';
modalMask.innerHTML = `
<div id="ns-stardust-modal">
<div class="ns-modal-header">
<div class="ns-modal-title">🌌 NodeSeek 全量星辰账本分析器</div>
<div class="ns-close-btn" id="ns-modal-close">×</div>
</div>
<div class="ns-stats-bar">
<div class="ns-stat-item">当前星辰余额: <span id="ns-stat-balance" class="ns-stat-val val-balance">0</span></div>
<div class="ns-stat-item">真实总流水: <span id="ns-stat-total" class="ns-stat-val">0 条</span></div>
<div class="ns-stat-item">累计收入: <span id="ns-stat-income" class="ns-stat-val val-plus">+0</span></div>
<div class="ns-stat-item">累计支出: <span id="ns-stat-expense" class="ns-stat-val val-minus">-0</span></div>
<div class="ns-stat-item">收到点赞: <span id="ns-stat-upvote" class="ns-stat-val val-plus">0 次 (+0)</span></div>
<div class="ns-stat-item">送出转账: <span id="ns-stat-transfer-out" class="ns-stat-val val-minus">0 笔 (-0)</span></div>
<div class="ns-stat-item">收到转账: <span id="ns-stat-transfer-in" class="ns-stat-val val-plus">0 笔 (+0)</span></div>
<div class="ns-stat-item">状态: <span id="ns-status-text" style="color:#60a5fa;font-weight:bold;">待加载</span></div>
</div>
<div class="ns-controls">
<button class="ns-tab-btn active" data-filter="all">全部流水</button>
<button class="ns-tab-btn" data-filter="upvote_in">💖 收到点赞 (+1)</button>
<button class="ns-tab-btn" data-filter="transfer_out">💸 送出的转账 (支出)</button>
<button class="ns-tab-btn" data-filter="transfer_in">💰 收到转账 (收入)</button>
<button class="ns-tab-btn" id="ns-btn-reload" style="background:#059669;color:#fff;">🔄 重新抓取</button>
<button class="ns-tab-btn" id="ns-btn-copy-md" style="background:#7c3aed;color:#fff;font-weight:bold;">📋 复制 Markdown (含小计)</button>
<input type="text" id="ns-search" class="ns-search-input" placeholder="搜索 用户名 / UID / 留言..." />
</div>
<div class="ns-table-container" id="ns-table-box">
<div class="ns-progress">点击开始抓取...</div>
</div>
<div class="ns-pagination-bar" id="ns-pagination-bar">
<span id="ns-page-info">第 1 / 1 页 (共 0 条)</span>
<div class="ns-page-controls">
<button class="ns-page-btn" id="ns-prev-page">上一页</button>
<button class="ns-page-btn" id="ns-next-page">下一页</button>
</div>
</div>
</div>
`;
document.body.appendChild(modalMask);
document.getElementById('ns-modal-close').onclick = closeModal;
modalMask.onclick = (e) => { if (e.target === modalMask) closeModal(); };
document.querySelectorAll('.ns-tab-btn[data-filter]').forEach(b => {
b.onclick = () => {
document.querySelectorAll('.ns-tab-btn[data-filter]').forEach(btn => btn.classList.remove('active'));
b.classList.add('active');
currentFilter = b.getAttribute('data-filter');
currentPage = 1;
renderTable();
};
});
document.getElementById('ns-btn-reload').onclick = () => loadAllData(true);
document.getElementById('ns-btn-copy-md').onclick = copyMarkdownTabs;
document.getElementById('ns-search').oninput = () => {
currentPage = 1;
renderTable();
};
document.getElementById('ns-prev-page').onclick = () => {
if (currentPage > 1) {
currentPage--;
renderTable();
}
};
document.getElementById('ns-next-page').onclick = () => {
currentPage++;
renderTable();
};
}
function openModal() {
document.getElementById('ns-modal-mask').style.display = 'flex';
const sidebarBal = getBalanceFromSidebar();
if (sidebarBal > 0) {
currentWalletBalance = sidebarBal;
renderStats();
}
if (cachedRecords.length === 0) {
loadAllData();
}
}
function closeModal() {
document.getElementById('ns-modal-mask').style.display = 'none';
}
async function getMyUid() {
if (myUid) return myUid;
try {
const res = await fetch("https://www.nodeseek.com/api/account/getInfo");
const data = await res.json();
if (data && data.success && data.detail) {
myUid = parseInt(data.detail.member_id || data.detail.id || data.detail.uid, 10);
if (data.detail.member_name) {
userCache.set(myUid, data.detail.member_name);
}
if (data.detail.stardust !== undefined && data.detail.stardust !== null) {
const parsed = Number(data.detail.stardust);
if (!isNaN(parsed) && parsed > 0) currentWalletBalance = parsed;
}
}
} catch (e) {}
if (!myUid) {
const manual = prompt("未能自动识别当前登录 UID,请输入您的 UID:");
myUid = parseInt(manual, 10);
}
return myUid;
}
async function loadAllData(force = false) {
if (cachedRecords.length > 0 && !force) return;
const tableBox = document.getElementById('ns-table-box');
const statusText = document.getElementById('ns-status-text');
if (tableBox) tableBox.innerHTML = `<div class="ns-progress">⏳ 正在精准拉取个人真实全量流水账本...</div>`;
if (statusText) statusText.innerText = `正在初始化...`;
const uid = await getMyUid();
if (!uid) {
if (tableBox) tableBox.innerHTML = `<div class="ns-progress">❌ 未能获取到当前 UID</div>`;
if (statusText) statusText.innerText = `UID 异常`;
return;
}
let rawList = [];
let cursor = null;
let hasMore = true;
let batch = 0;
try {
while (hasMore) {
batch++;
const url = new URL("https://www.nodeseek.com/api/stardust/list");
url.searchParams.set("count", String(CONFIG.COUNT_PER_REQ));
url.searchParams.set("member_id", String(uid));
if (cursor) url.searchParams.set("before_id", String(cursor));
if (statusText) statusText.innerText = `第 ${batch} 批(已获取 ${rawList.length} 条)...`;
const res = await fetch(url);
const data = await res.json();
if (!data.success || !data.records || data.records.length === 0) break;
rawList = rawList.concat(data.records);
cursor = data.cursor;
hasMore = data.exist_more;
if (!hasMore) break;
await new Promise(r => setTimeout(r, CONFIG.DELAY_MS));
}
const uniqueMap = new Map();
rawList.forEach(r => {
const rawCreatedAt = typeof r.created_at === 'number' ? (r.created_at > 1e11 ? r.created_at : r.created_at * 1000) : new Date(r.created_at).getTime();
const id = r.id || `${rawCreatedAt}_${r.peer_id}_${r.member_id}`;
if (!uniqueMap.has(id)) {
uniqueMap.set(id, {
raw: r,
id: id,
rawCreatedAt: rawCreatedAt || 0
});
}
});
const sortedList = Array.from(uniqueMap.values()).sort((a, b) => b.rawCreatedAt - a.rawCreatedAt);
const processed = [];
sortedList.forEach(item => {
const r = item.raw;
const mId = r.member_id ? parseInt(r.member_id, 10) : 0;
const pId = r.peer_id ? parseInt(r.peer_id, 10) : 0;
const rawType = (r.type || '').toLowerCase();
const refId = parseInt(r.ref_id || r.refId || 0, 10);
const extractedNum = extractAmount(r);
const isUpvote = (refId === 10) || rawType.includes('upvote') || rawType.includes('like') || Boolean(r.comment_id);
let category = 'transfer_out';
let finalAmount = 0;
if (isUpvote) {
category = 'upvote_in';
finalAmount = extractedNum > 0 ? extractedNum : 1;
} else {
if (extractedNum > 0 || rawType.includes('in') || rawType.includes('receive') || (mId !== uid && mId > 0)) {
category = 'transfer_in';
finalAmount = Math.abs(extractedNum || 1);
} else if (extractedNum < 0 || rawType.includes('out') || rawType.includes('send') || rawType.includes('pay')) {
category = 'transfer_out';
finalAmount = -Math.abs(extractedNum || 1);
} else {
category = (mId === uid && pId) ? 'transfer_out' : 'transfer_in';
finalAmount = (category === 'transfer_out') ? -1 : 1;
}
}
const targetUid = (pId && pId !== uid) ? pId : ((mId && mId !== uid) ? mId : (pId || mId || null));
processed.push({
id: item.id,
rawCreatedAt: item.rawCreatedAt,
amount: finalAmount,
targetUid: targetUid,
memo: r.memo || r.reason || r.description || r.note || null,
balance: extractBalance(r),
category: category,
time: formatExactTime(r.created_at)
});
});
cachedRecords = processed;
currentPage = 1;
if (statusText) statusText.innerText = `已就绪`;
renderStats();
renderTable();
} catch (e) {
if (tableBox) tableBox.innerHTML = `<div class="ns-progress">❌ 获取中断: ${e.message}</div>`;
if (statusText) statusText.innerText = `获取失败`;
}
}
function renderStats() {
let totalIncome = 0;
let totalExpense = 0;
let upvoteInCount = 0;
let upvoteInStardust = 0;
let transferOutCount = 0;
let transferOutStardust = 0;
let transferInCount = 0;
let transferInStardust = 0;
cachedRecords.forEach(item => {
if (item.category === 'upvote_in') {
upvoteInCount++;
upvoteInStardust += item.amount;
totalIncome += item.amount;
} else if (item.category === 'transfer_in') {
transferInCount++;
transferInStardust += item.amount;
totalIncome += item.amount;
} else if (item.category === 'transfer_out') {
const absVal = Math.abs(item.amount);
transferOutCount++;
transferOutStardust += absVal;
totalExpense += absVal;
}
});
const netCalculated = totalIncome - totalExpense;
if (netCalculated > 0) {
currentWalletBalance = netCalculated;
} else {
const sidebarBal = getBalanceFromSidebar();
if (sidebarBal > 0) currentWalletBalance = sidebarBal;
}
const elBalance = document.getElementById('ns-stat-balance');
const elTotal = document.getElementById('ns-stat-total');
const elIncome = document.getElementById('ns-stat-income');
const elExpense = document.getElementById('ns-stat-expense');
const elUpvote = document.getElementById('ns-stat-upvote');
const elTransferOut = document.getElementById('ns-stat-transfer-out');
const elTransferIn = document.getElementById('ns-stat-transfer-in');
if (elBalance) elBalance.innerText = String(currentWalletBalance);
if (elTotal) elTotal.innerText = `${cachedRecords.length} 条`;
if (elIncome) elIncome.innerText = `+${totalIncome}`;
if (elExpense) elExpense.innerText = `-${totalExpense}`;
if (elUpvote) elUpvote.innerText = `${upvoteInCount} 次 (+${upvoteInStardust})`;
if (elTransferOut) elTransferOut.innerText = `${transferOutCount} 笔 (-${transferOutStardust})`;
if (elTransferIn) elTransferIn.innerText = `${transferInCount} 笔 (+${transferInStardust})`;
}
function renderTable() {
const tableBox = document.getElementById('ns-table-box');
const searchKeyword = document.getElementById('ns-search').value.trim().toLowerCase();
const filtered = cachedRecords.filter(item => {
if (currentFilter !== 'all' && item.category !== currentFilter) {
return false;
}
if (searchKeyword) {
const uidStr = String(item.targetUid || '');
const nameStr = (userCache.get(item.targetUid) || '').toLowerCase();
const memoStr = String(item.memo || '').toLowerCase();
const timeStr = String(item.time || '');
return uidStr.includes(searchKeyword) || nameStr.includes(searchKeyword) || memoStr.includes(searchKeyword) || timeStr.includes(searchKeyword);
}
return true;
});
const totalItems = filtered.length;
const totalPages = Math.ceil(totalItems / CONFIG.PAGE_SIZE) || 1;
if (currentPage > totalPages) currentPage = totalPages;
if (currentPage < 1) currentPage = 1;
const startIndex = (currentPage - 1) * CONFIG.PAGE_SIZE;
const pageRecords = filtered.slice(startIndex, startIndex + CONFIG.PAGE_SIZE);
const pageInfo = document.getElementById('ns-page-info');
const prevBtn = document.getElementById('ns-prev-page');
const nextBtn = document.getElementById('ns-next-page');
if (pageInfo) pageInfo.innerText = `第 ${currentPage} / ${totalPages} 页 (共 ${totalItems} 条)`;
if (prevBtn) prevBtn.disabled = currentPage <= 1;
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
if (pageRecords.length === 0) {
if (tableBox) tableBox.innerHTML = `<div class="ns-progress">未找到符合条件的记录</div>`;
return;
}
const visibleUids = [];
const rowsHtml = pageRecords.map(item => {
let badgeHtml = '';
let amountHtml = '';
if (item.amount > 0) {
amountHtml = `<span class="val-plus">+${item.amount}</span>`;
} else if (item.amount < 0) {
amountHtml = `<span class="val-minus">${item.amount}</span>`;
} else {
amountHtml = `<span>0</span>`;
}
switch (item.category) {
case 'upvote_in':
badgeHtml = `<span class="badge badge-upvote-in">💖 收到点赞</span>`;
break;
case 'transfer_out':
badgeHtml = `<span class="badge badge-transfer-out">💸 送出转账</span>`;
break;
case 'transfer_in':
badgeHtml = `<span class="badge badge-transfer-in">💰 收到转账</span>`;
break;
default:
badgeHtml = `<span class="badge badge-transfer-out">💸 送出转账</span>`;
break;
}
let uidCellHtml = '-';
if (item.targetUid) {
visibleUids.push(item.targetUid);
uidCellHtml = `<a class="ns-uid-link" href="https://www.nodeseek.com/space/${item.targetUid}#/general" target="_blank">UID: ${item.targetUid}</a>`;
}
const detailHtml = formatDetailHtml(item);
return `
<tr>
<td class="ns-time-cell">${item.time}</td>
<td>${badgeHtml}</td>
<td><b>${amountHtml}</b></td>
<td>${uidCellHtml}</td>
<td>${detailHtml}</td>
</tr>
`;
}).join('');
if (tableBox) {
tableBox.innerHTML = `
<table class="ns-table">
<thead>
<tr>
<th style="width: 170px;">时间 (精确秒级)</th>
<th style="width: 120px;">类型</th>
<th style="width: 90px;">数额</th>
<th style="width: 110px;">对方 UID</th>
<th>明细信息 (@对方用户名 / 变动后余额 / 转账留言)</th>
</tr>
</thead>
<tbody>${rowsHtml}</tbody>
</table>
`;
}
resolveVisibleUserNames(visibleUids);
}
GM_registerMenuCommand("🌟 打开星辰账本分析器", openModal);
initUI();
})();
nsapp://stardust-receive?member_id=39375&ref_id=78504510&description=人生若只如初见,何须以猜忌量真心;愿你卸下满怀疑云,还彼此最初的坦荡与笃定。&diff=1&onetime=false