用于增强 NodeSeek/DeepFlood 论坛体验的用户脚本:提供自动签到、下拉加载、快速评论、内容过滤、等级标记、浏览历史、Callout 渲染、图片预览、快捷键等功能,并带可视化设置面板可自由开关配置。
// ==UserScript==
// @name NodeSeek X
// @namespace http://www.nodeseek.com/
// @version 1.1.0
// @description 用于增强 NodeSeek/DeepFlood 论坛体验的用户脚本:提供自动签到、下拉加载、快速评论、内容过滤、等级标记、浏览历史、Callout 渲染、图片预览、快捷键等功能,并带可视化设置面板可自由开关配置。
// @author dabao
// @match *://www.nodeseek.com/*
// @match *://www.deepflood.com/*
// @require https://s4.zstatic.net/ajax/libs/layui/2.10.3/layui.min.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_getResourceURL
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-idle
// @license GPL-3.0
// @supportURL https://www.nodeseek.com/post-36263-1
// @homepageURL https://www.nodeseek.com/post-36263-1
// ==/UserScript==
(function () {
'use strict';
// NSX Core - 核心
// 环境 + DOM + 网络 + 存储 + 模块管理
const SITES = [
{ host: "www.nodeseek.com", code: "ns", name: "NodeSeek" },
{ host: "www.deepflood.com", code: "df", name: "DeepFlood" }
];
const info = GM_info?.script || {};
const site = SITES.find(s => s.host === location.host);
let debug = false;
try { debug = GM_getValue("settings", {})?.debug?.enabled; } catch { }
// ===== 环境 =====
const env = {
info, site, BASE_URL: location.origin,
log: (...a) => debug && console.log(`[NSX]`, ...a),
warn: (...a) => debug && console.warn(`[NSX]`, ...a),
error: (...a) => console.error(`[NSX]`, ...a)
};
// ===== DOM =====
const $ = (s, r = document) => r?.querySelector(s);
const $$ = (s, r = document) => [...(r?.querySelectorAll(s) || [])];
function addStyle(id, val) {
if (document.getElementById(id)) return;
const isUrl = /^(https?:|blob:|data:)/.test(val) || /^\/\//.test(val);
const el = document.createElement(isUrl ? "link" : "style");
el.id = id;
isUrl ? (el.rel = "stylesheet", el.href = val) : (el.textContent = val);
document.head?.appendChild(el);
}
function addScript(id, val) {
if (document.getElementById(id)) return;
const el = document.createElement("script");
el.id = id;
/^(https?:)?\/\//.test(val) ? (el.src = val) : (el.textContent = val);
document.body?.appendChild(el);
}
const debounce = (fn, ms) => {
let t; const d = (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
d.cancel = () => clearTimeout(t); return d;
};
const throttle = (fn, ms) => {
let last = 0;
return (...a) => { const now = Date.now(); if (now - last >= ms) { last = now; fn(...a); } };
};
// ===== 存储 =====
const cfgFragments = new Map(), metaFragments = new Map();
let cfgCache = null;
const isObj = v => v && typeof v === "object" && !Array.isArray(v);
const merge = (t, s) => { for (const k in s) isObj(s[k]) ? (isObj(t[k]) || (t[k] = {}), merge(t[k], s[k])) : t[k] === undefined && (t[k] = s[k]); };
const getPath = (o, p) => p.split(".").reduce((a, k) => a?.[k], o);
const setPath = (o, p, v) => { const ks = p.split("."), l = ks.pop(); ks.reduce((a, k) => a[k] ??= {}, o)[l] = v; };
const store = {
reg(id, cfg, meta) { cfg && cfgFragments.set(id, cfg); meta && metaFragments.set(id, meta); },
getDefaults() { const d = { version: info.version, debug: { enabled: false } }; cfgFragments.forEach(f => merge(d, f)); return d; },
getMeta() { const m = {}; metaFragments.forEach(f => merge(m, f)); return m; },
init() {
if (cfgCache) return cfgCache;
const def = this.getDefaults();
cfgCache = GM_getValue("settings", null) || {};
merge(cfgCache, def);
cfgCache.version = def.version;
GM_setValue("settings", cfgCache);
return cfgCache;
},
get(p, fb) { const v = getPath(this.init(), p); return v === undefined ? fb : v; },
set(p, v) { setPath(this.init(), p, v); GM_setValue("settings", cfgCache); }
};
// ===== 网络 =====
const net = {
async fetch(url, { method = "GET", data, headers = {}, type = "json" } = {}) {
const r = await fetch(url.startsWith("http") ? url : env.BASE_URL + url, {
method, credentials: "include",
headers: { ...(data ? { "Content-Type": "application/json" } : {}), ...headers },
body: data ? JSON.stringify(data) : undefined
});
return r[type]().catch(() => null);
},
get: (u, h, t) => net.fetch(u, { headers: h, type: t }),
post: (u, d, h, t) => net.fetch(u, { method: "POST", data: d, headers: h, type: t })
};
// ===== 模块管理 =====
const modules = new Map();
function define(cfg) {
if (!cfg?.id) throw new Error("id required");
cfg.deps ??= [];
cfg.order ??= 100;
modules.set(cfg.id, cfg);
cfg.cfg && store.reg(cfg.id, cfg.cfg, cfg.meta);
return cfg;
}
function boot(ctx) {
store.init();
// 拓扑排序
const list = [...modules.values()];
const indeg = new Map(list.map(m => [m.id, 0]));
const edges = new Map(list.map(m => [m.id, []]));
list.forEach(m => m.deps.forEach(d => { if (modules.has(d)) { edges.get(d).push(m.id); indeg.set(m.id, indeg.get(m.id) + 1); } }));
const q = list.filter(m => indeg.get(m.id) === 0).sort((a, b) => a.order - b.order);
const sorted = [];
while (q.length) {
const cur = q.shift(); sorted.push(cur);
edges.get(cur.id).forEach(n => { indeg.set(n, indeg.get(n) - 1); if (!indeg.get(n)) q.push(modules.get(n)); });
q.sort((a, b) => a.order - b.order);
}
// 初始化与注册监听
sorted.forEach(m => {
try {
if (m.match?.(ctx) !== false) {
m.init?.(ctx);
if (ctx.watch && m.watch) {
const w = typeof m.watch === "function" ? m.watch(ctx) : m.watch;
[].concat(w || []).filter(Boolean).forEach(i => ctx.watch(i.sel, i.fn, i.opts));
}
}
} catch (e) {
env.error(m.id, e);
}
});
}
// 下拉加载翻页
const PROFILES = {
list: { path: /^\/(categories\/|page|award|search|$)/, threshold: 1500, next: ".nsk-pager a.pager-next", list: "ul.post-list:not(.topic-carousel-panel)", pagerTop: "div.nsk-pager.pager-top", pagerBot: "div.nsk-pager.pager-bottom" },
post: { path: /^\/post-/, threshold: 690, next: ".nsk-pager a.pager-next", list: "ul.comments", pagerTop: "div.nsk-pager.post-top-pager", pagerBot: "div.nsk-pager.post-bottom-pager" }
};
const autoLoading = {
id: "autoLoading",
order: 100,
cfg: { loading_post: { enabled: true }, loading_comment: { enabled: true } },
meta: { loading_post: { label: "加载帖子", group: "内容设置" }, loading_comment: { label: "加载评论", group: "内容设置" } },
match: ctx => ctx.store.get("loading_post.enabled", true) || ctx.store.get("loading_comment.enabled", true),
init(ctx) {
const profile = (ctx.isList && ctx.store.get("loading_post.enabled", true)) ? PROFILES.list :
(ctx.isPost && ctx.store.get("loading_comment.enabled", true)) ? PROFILES.post : null;
if (!profile) return;
let busy = false, prevY = scrollY;
const blockByLevel = (doc) => {
const lv = ctx.user?.rank || 0;
doc.querySelectorAll('.post-list-item use[href="#lock"]').forEach(el => {
const n = +(el.closest("span")?.textContent?.match(/\d+/)?.[0] || 0);
if (n > lv) el.closest(".post-list-item")?.classList.add("blocked-post");
});
};
const load = async () => {
if (busy) return;
const atBottom = document.documentElement.scrollHeight <= innerHeight + scrollY + profile.threshold;
if (!atBottom) return;
const nextUrl = ctx.$(profile.next)?.href;
if (!nextUrl) return;
busy = true;
try {
const html = await net.get(nextUrl, {}, "text");
const doc = new DOMParser().parseFromString(html, "text/html");
blockByLevel(doc);
// 评论数据同步
if (ctx.isPost) {
const json = doc.getElementById("temp-script")?.textContent;
if (json) try {
const cfg = JSON.parse(decodeURIComponent(atob(json).split("").map(c => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join("")));
if (cfg?.postData?.comments) ctx.uw.__config__.postData.comments.push(...cfg.postData.comments);
} catch { }
}
const src = doc.querySelector(profile.list), dst = document.querySelector(profile.list);
if (src && dst) dst.append(...src.children);
// 渲染新加载评论的 Vue 组件
if (ctx.isPost) {
const vue = $(".comment-menu")?.__vue__;
if (vue) $$(".content-item").forEach((item, index) => {
const mp = $(".comment-menu-mount", item);
if (mp) { const inst = new vue.$root.constructor(vue.$options); inst.setIndex(index); inst.$mount(mp); }
});
}
[profile.pagerTop, profile.pagerBot].forEach(sel => {
const s = doc.querySelector(sel), d = document.querySelector(sel);
if (s && d) d.innerHTML = s.innerHTML;
});
history.pushState(null, null, nextUrl);
} catch (e) { ctx.env.error("autoLoading", e); }
busy = false;
};
const deb = debounce(load, 300);
addEventListener("scroll", throttle(() => { if (scrollY > prevY) deb(); prevY = scrollY; }, 200), { passive: true });
}
};
const __vite_glob_0_0 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: autoLoading
}, Symbol.toStringTag, { value: 'Module' }));
// 屏蔽用户
const blockMembers = {
id: "blockMembers",
order: 240,
cfg: { block_members: { enabled: true } },
meta: { block_members: { label: "屏蔽用户", group: "过滤设置" } },
match: ctx => ctx.loggedIn && ctx.store.get("block_members.enabled", true),
init(ctx) {
addStyle("nsx-block", ".usercard-button-group .btn{padding:0 .8rem}");
const block = name => net.post("/api/block-list/add", { block_member_name: name })
.then(r => ctx.ui.alert?.("提示", r?.success ? `屏蔽【${name}】成功` : `屏蔽失败:${r?.message || ""}`));
document.querySelectorAll(".post-list .post-list-item,.content-item").forEach(item => {
const avatar = item.querySelector(".avatar-normal");
if (!avatar) return;
avatar.addEventListener("click", () => {
let tries = 0;
const check = setInterval(() => {
if (++tries > 60) { clearInterval(check); return; } // 3 秒超时 (50ms × 60)
const card = document.querySelector("div.user-card.hover-user-card");
const pm = card?.querySelector("a.btn");
if (!card || !pm) return;
clearInterval(check);
const name = card.querySelector("a.Username")?.textContent;
if (!name || card.querySelector(".nsx-block-btn")) return;
const btn = pm.cloneNode(false);
btn.className = "btn nsx-block-btn";
btn.textContent = "屏蔽";
btn.style.cssText = "float:left;background-color:rgba(0,0,0,.3)!important";
btn.onclick = e => { e.preventDefault(); ctx.ui.confirm?.(`屏蔽"${name}"?`, "可在设置=>屏蔽用户解除", () => block(name)); };
pm.after(btn);
}, 50);
});
});
}
};
const __vite_glob_0_1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: blockMembers
}, Symbol.toStringTag, { value: 'Module' }));
// 屏蔽帖子(关键词)
const mark$2 = new WeakSet();
const run$1 = (els, ctx) => {
const kws = (ctx.store.get("block_posts.keywords", []) || []).map(k => String(k).trim().toLowerCase()).filter(Boolean);
els.forEach(item => {
if (mark$2.has(item)) return;
mark$2.add(item);
const title = item.querySelector(".post-title>a")?.textContent?.toLowerCase() || "";
if (kws.some(k => title.includes(k))) item.classList.add("blocked-post");
});
};
const blockPosts = {
id: "blockPosts",
order: 220,
cfg: { block_posts: { enabled: true, keywords: [] } },
meta: { block_posts: { label: "屏蔽帖子", group: "过滤设置", fields: { keywords: { type: "TEXTAREA", label: "关键词", placeholder: "每行一个", valueType: "array" } } } },
match: ctx => ctx.isList && ctx.store.get("block_posts.enabled", true),
init(ctx) { run$1($$(".post-list-item"), ctx); },
watch: ctx => ({ sel: ".post-list-item", fn: els => run$1(els, ctx), opts: { debounce: 80 } })
};
const __vite_glob_0_2 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: blockPosts
}, Symbol.toStringTag, { value: 'Module' }));
// 屏蔽低等级可见帖子
const mark$1 = new WeakSet();
const run = (els, ctx) => {
const lv = ctx.user?.rank || 0;
els.forEach(el => {
const item = el.closest(".post-list-item");
if (!item || mark$1.has(item)) return;
mark$1.add(item);
const n = +(el.closest("span")?.textContent?.match(/\d+/)?.[0] || 0);
if (n > lv) item.classList.add("blocked-post");
});
};
const blockViewLevel = {
id: "blockViewLevel",
order: 222,
cfg: { block_view_level: { enabled: true } },
meta: { block_view_level: { label: "隐藏高权限帖", group: "过滤设置" } },
match: ctx => ctx.isList && ctx.store.get("block_view_level.enabled", true),
init(ctx) { run($$('.post-list-item use[href="#lock"]'), ctx); },
watch: ctx => ({ sel: '.post-list-item use[href="#lock"]', fn: els => run(els, ctx), opts: { debounce: 80 } })
};
const __vite_glob_0_3 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: blockViewLevel
}, Symbol.toStringTag, { value: 'Module' }));
// Callout 支持 + 编辑器插入菜单
const CSS_RENDER = `.post-content blockquote{border-left:none;border-radius:4px;margin:1em 0;box-shadow:inset 4px 0 0 0 rgba(0,0,0,.1)}.callout{--c:8,109,221;overflow:hidden;border-radius:4px;margin:1em 0;padding:12px 12px 12px 24px!important;box-shadow:inset 4px 0 0 0 rgba(var(--c),.5)}.callout.is-collapsible .callout-title{cursor:pointer}.callout-title{display:flex;gap:4px;color:rgb(var(--c));line-height:1.3;align-items:flex-start}.callout-content{overflow-x:auto}.callout-icon{flex:0 0 auto;display:flex;align-items:center}.callout-icon .svg-icon,.callout-fold .svg-icon{color:rgb(var(--c));height:18px;width:18px}.callout-title-inner{font-weight:600}.callout-fold{display:flex;align-items:center;padding-inline-end:8px}.callout-fold .svg-icon{transition:transform .1s}.callout-fold.is-collapsed .svg-icon{transform:rotate(-90deg)}.callout.is-collapsed .callout-content{display:none}.callout[data-callout="abstract"],.callout[data-callout="summary"],.callout[data-callout="tldr"]{--c:83,223,221}.callout[data-callout="info"],.callout[data-callout="todo"]{--c:8,109,221}.callout[data-callout="tip"],.callout[data-callout="hint"],.callout[data-callout="important"]{--c:83,223,221}.callout[data-callout="success"],.callout[data-callout="check"],.callout[data-callout="done"]{--c:68,207,110}.callout[data-callout="question"],.callout[data-callout="help"],.callout[data-callout="faq"]{--c:236,117,0}.callout[data-callout="warning"],.callout[data-callout="caution"],.callout[data-callout="attention"]{--c:236,117,0}.callout[data-callout="failure"],.callout[data-callout="fail"],.callout[data-callout="missing"]{--c:233,49,71}.callout[data-callout="danger"],.callout[data-callout="error"]{--c:233,49,71}.callout[data-callout="bug"]{--c:233,49,71}.callout[data-callout="example"]{--c:120,82,238}.callout[data-callout="quote"],.callout[data-callout="cite"]{--c:158,158,158}`;
const CSS_COLORFUL = `.callout{background:rgba(var(--c),.1)}`;
const CSS_EDITOR = `.callout-inserter-wrapper{position:relative;display:inline-flex;align-items:center}.callout-inserter-btn{padding:0;border:none;background:0 0;cursor:pointer;display:flex;color:currentColor}.callout-inserter-btn:hover{opacity:.7}.callout-inserter-dropdown{position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:8px;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);z-index:1000;min-width:160px;display:none;overflow:auto;max-height:240px}.callout-inserter-dropdown.show{display:block}.callout-inserter-item{padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:13px;transition:background .15s}.callout-inserter-item:hover{background:#f5f5f5}.callout-inserter-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}`;
const ICONS = { note: "M21.17 6.81a1 1 0 0 0-3.99-3.99L3.84 16.17a2 2 0 0 0-.5.83l-1.32 4.35a.5.5 0 0 0 .62.62l4.35-1.32a2 2 0 0 0 .83-.5zm-6.17-1.81 4 4", abstract: "M8 2h8v4H8zM16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2M12 11h4M12 16h4M8 11h.01M8 16h.01", info: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 14v-4m0-4h.01", tip: "M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4", success: "M20 6 9 17l-5-5", question: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01", warning: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4m0 4h.01", failure: "M18 6 6 18M6 6l12 12", danger: "M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z", bug: "M12 20v-9m2-6a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4zM14.12 3.88 16 2M8 2l1.88 1.88M9 7.13V6a3 3 0 1 1 6 0v1.13", example: "M3 5h.01M3 12h.01M3 19h.01M8 5h13M8 12h13M8 19h13", quote: "M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2zM5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z", fold: "m6 9 6 6 6-6" };
const TYPE_MAP = { summary: "abstract", tldr: "abstract", hint: "tip", important: "tip", check: "success", done: "success", help: "question", faq: "question", caution: "warning", attention: "warning", fail: "failure", missing: "failure", error: "danger", cite: "quote" };
const MENUS = [{ k: "note", n: "笔记", c: "8,109,221" }, { k: "info", n: "信息", c: "8,109,221" }, { k: "tip", n: "提示", c: "83,223,221" }, { k: "warning", n: "警告", c: "236,117,0" }, { k: "danger", n: "危险", c: "233,49,71" }, { k: "success", n: "成功", c: "68,207,110" }, { k: "question", n: "问题", c: "236,117,0" }, { k: "example", n: "示例", c: "120,82,238" }];
const svg = d => `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon"><path d="${d}"/></svg>`;
const RE = /^\[!(\w+)\]([+-])?(?:\s+([^<\n]+))?(?:<br\s*\/?>)?([\s\S]*)$/i;
const render = (els) => {
els.forEach(bq => {
if (bq.classList.contains("oc-done") || bq.closest("blockquote.oc-done")) return;
bq.classList.add("oc-done");
const p = bq.querySelector(":scope > p");
const m = (p?.innerHTML?.trim() || "").match(RE);
if (!m) return;
const [, type, fold, title, content] = m;
const t = type.toLowerCase(), base = TYPE_MAP[t] || t, icon = ICONS[base] || ICONS.note;
const isColl = fold === "+" || fold === "-", isCol = fold === "-";
const wrap = document.createElement("div");
wrap.className = `callout${isColl ? " is-collapsible" : ""}${isCol ? " is-collapsed" : ""}`;
wrap.dataset.callout = t;
const titleEl = document.createElement("div");
titleEl.className = "callout-title";
titleEl.innerHTML = `<div class="callout-icon">${svg(icon)}</div><div class="callout-title-inner">${title?.trim() || type[0].toUpperCase() + type.slice(1)}</div>`;
if (isColl) {
const foldEl = document.createElement("div");
foldEl.className = `callout-fold${isCol ? " is-collapsed" : ""}`;
foldEl.innerHTML = svg(ICONS.fold);
titleEl.appendChild(foldEl);
titleEl.onclick = () => { wrap.classList.toggle("is-collapsed"); foldEl.classList.toggle("is-collapsed"); };
}
wrap.appendChild(titleEl);
const cont = document.createElement("div");
cont.className = "callout-content";
if (content?.trim()) { const pp = document.createElement("p"); pp.innerHTML = content.trim(); cont.appendChild(pp); }
let sib = p.nextSibling;
while (sib) { const next = sib.nextSibling; cont.appendChild(sib); sib = next; }
if (cont.childNodes.length) wrap.appendChild(cont);
bq.replaceWith(wrap);
});
};
const insertCallout = (editor, type) => {
const cm = editor.querySelector(".CodeMirror")?.CodeMirror;
if (!cm) return;
const doc = cm.getDoc();
let cur = doc.getCursor();
const lvl = (doc.getLine(cur.line).match(/^(>\s*)+/)?.[0].match(/>/g) || []).length;
if (lvl > 0) {
let last = cur.line;
for (let i = cur.line + 1; i < doc.lineCount(); i++) { if (doc.getLine(i).match(/^>\s*/)) last = i; else break; }
cur = { line: last, ch: doc.getLine(last).length };
}
const pre = lvl > 0 ? ">".repeat(lvl + 1) + " " : "> ";
doc.replaceRange((lvl > 0 ? "\n" : "") + `${pre}[!${type}] \n${pre}`, cur);
doc.setCursor({ line: cur.line + (lvl > 0 ? 1 : 0), ch: `${pre}[!${type}] `.length });
cm.focus();
};
let clickBound = false;
const createInserter = () => {
const editor = $(".md-editor");
const bar = editor?.querySelector(".mde-toolbar");
if (!editor || !bar || bar.querySelector(".callout-inserter-wrapper")) return;
const vAttr = [...(bar.querySelector(".toolbar-item")?.attributes || [])].find(a => a.name.startsWith("data-v-"))?.name;
const setV = el => vAttr && el.setAttribute(vAttr, "");
const wrap = document.createElement("span");
wrap.className = "callout-inserter-wrapper toolbar-item";
wrap.title = "Callout - NodeSeek X";
setV(wrap);
const btn = document.createElement("span");
btn.className = "callout-inserter-btn i-icon";
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 48 48" fill="none"><path d="M44 8H4v30h15l5 5 5-5h15V8Z" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 18v10" stroke="currentColor" stroke-width="4" stroke-linecap="round"/><circle cx="24" cy="33" r="2" fill="currentColor"/></svg>`;
setV(btn);
const drop = document.createElement("div");
drop.className = "callout-inserter-dropdown";
MENUS.forEach(t => {
const item = document.createElement("div");
item.className = "callout-inserter-item";
item.innerHTML = `<span class="callout-inserter-dot" style="background:rgb(${t.c})"></span>${t.n}[${t.k}]`;
item.onclick = e => { e.stopPropagation(); insertCallout(editor, t.k); drop.classList.remove("show"); };
drop.appendChild(item);
});
btn.onclick = e => { e.stopPropagation(); drop.classList.toggle("show"); };
if (!clickBound) { document.addEventListener("click", () => $$(".callout-inserter-dropdown.show").forEach(d => d.classList.remove("show"))); clickBound = true; }
const sep = document.createElement("div");
sep.className = "sep";
setV(sep);
wrap.append(btn, drop);
bar.append(sep, wrap);
};
const callout = {
id: "callout",
order: 360,
cfg: {
callout: {
enabled: true,
render: true,
editor: true,
style: "colorful"
}
},
meta: {
callout: {
label: "Callout 支持",
group: "内容设置",
fields: {
render: { type: "SWITCH", label: "正文渲染" },
editor: { type: "SWITCH", label: "编辑器按钮" },
style: { type: "RADIO", label: "渲染风格", options: [{ value: "colorful", text: "绚丽" }, { value: "clean", text: "清新" }] }
}
}
},
match: ctx => (ctx.isPost || /^\/new-discussion/.test(location.pathname)) && ctx.store.get("callout.enabled", true) && (ctx.store.get("callout.render", true) || ctx.store.get("callout.editor", true)),
init(ctx) {
if (ctx.store.get("callout.render", true)) {
const style = ctx.store.get("callout.style", "colorful");
addStyle("nsx-callout-render", CSS_RENDER + (style === "colorful" ? CSS_COLORFUL : ""));
render($$(".post-content blockquote"));
}
if (ctx.store.get("callout.editor", true)) {
addStyle("nsx-callout-editor", CSS_EDITOR);
createInserter();
document.addEventListener("click", e => { if (e.target?.closest?.(".md-editor")) requestAnimationFrame(createInserter); });
}
},
watch: ctx => {
const w = [];
if (ctx.store.get("callout.render", true)) {
w.push({ sel: ".post-content blockquote", fn: render, opts: { debounce: 80 } });
}
if (ctx.store.get("callout.editor", true)) {
w.push({ sel: ".mde-toolbar", fn: createInserter, opts: { debounce: 80 } });
}
return w;
}
};
const __vite_glob_0_4 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: callout
}, Symbol.toStringTag, { value: 'Module' }));
// 代码高亮
const codeHighlight = {
id: "codeHighlight",
order: 140,
cfg: { code_highlight: { enabled: true } },
meta: { code_highlight: { label: "代码高亮", group: "内容设置" } },
match: ctx => ctx.store.get("code_highlight.enabled", true),
init(ctx) {
// 挂载 highlight.js
addScript("nsx-hljs-script", "https://s4.zstatic.net/ajax/libs/highlight.js/11.9.0/highlight.min.js");
addScript("nsx-hljs-onload", `(()=>{const r=()=>{if(window.hljs&&typeof hljs.highlightAll==="function")hljs.highlightAll()};document.readyState==="complete"?r():window.addEventListener("load",r,{once:true})})()`);
},
watch: ctx => ({ sel: ".post-content pre code", fn: els => els.forEach(el => ctx.uw.hljs?.highlightElement(el)), opts: { debounce: 80 } })
};
const __vite_glob_0_5 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: codeHighlight
}, Symbol.toStringTag, { value: 'Module' }));
// 回帖足迹模块 (NodeSeek & DeepFlood)
const DB = 'nsx-comments-db';
const ST = 'nsx-comments-store';
const K = { I: "nsx_init", P: "nsx_page", T: "nsx_time", C: "nsx_count" };
const REPLIED_BADGE_CSS = `
.replied-badge {
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
font-size: 12px;
color: #fff;
background: #10b981;
border-radius: 4px;
text-decoration: none;
transition: background 0.2s ease, transform 0.1s ease;
}
.replied-badge:hover {
background: #059669;
color: #fff !important;
transform: translateY(-1px);
}
`;
// 数据库单例连接与操作
let dbInstance = null;
let dbInitPromise = null;
const getDB = () => {
if (dbInstance) return Promise.resolve(dbInstance);
if (dbInitPromise) return dbInitPromise;
dbInitPromise = new Promise((resolve, reject) => {
const r = indexedDB.open(DB, 1);
r.onerror = () => {
dbInitPromise = null;
reject(r.error || new Error('DB Open Failed'));
};
r.onupgradeneeded = e => {
const db = e.target.result;
if (db.objectStoreNames.contains(ST)) db.deleteObjectStore(ST);
const s = db.createObjectStore(ST, { keyPath: ['uid', 'post_id', 'floor_id'] });
s.createIndex('upid', ['uid', 'post_id']);
};
r.onsuccess = e => {
dbInstance = e.target.result;
dbInstance.onclose = () => {
dbInstance = null;
dbInitPromise = null;
};
dbInstance.onversionchange = () => {
dbInstance.close();
dbInstance = null;
dbInitPromise = null;
};
resolve(dbInstance);
};
});
return dbInitPromise;
};
const dbAct = async (mode, fn) => {
const db = await getDB();
return new Promise((res, rej) => {
const tx = db.transaction([ST], mode);
tx.onerror = () => rej(tx.error || new Error('Transaction Error'));
tx.onabort = () => rej(tx.error || new Error('Transaction Aborted'));
try {
fn(tx.objectStore(ST), res, rej);
} catch (e) {
rej(e);
}
});
};
// 列表帖子标记具体实现
const markElement = async (ctx, el, uid) => {
const linkEl = el.querySelector('.post-title a');
if (!linkEl) return;
const match = linkEl.href.match(/-(\d+)-/);
if (!match) return;
const pid = parseInt(match[1]);
try {
const max = await dbAct('readonly', (s, r) => {
const range = IDBKeyRange.bound([uid, pid, 0], [uid, pid, Infinity]);
s.openCursor(range, 'prev').onsuccess = e => r(e.target.result?.value.floor_id || 0);
});
if (max > 0 && !el.querySelector('.replied-badge')) {
const b = document.createElement('a');
const commentPerPage = ctx.uw?.__config__?.commentPerPage || 10;
const targetPage = Math.ceil(max / commentPerPage);
Object.assign(b, {
className: 'replied-badge',
target: '_blank',
textContent: `已回复 #${max}`,
href: `/post-${pid}-${targetPage}#${max}`
});
el.querySelector('.post-title').append(b);
}
} catch (e) {
ctx.env.warn('[Mark Error]', pid, e);
}
};
const commentFootprint = {
id: "commentFootprint",
order: 360,
cfg: {
comment_footprint: {
enabled: false,
reset_db: "",
show_stats: ""
}
},
meta: {
comment_footprint: {
label: "回帖足迹",
group: "实验性",
fields: {
reset_db: {
type: "BUTTON",
label: "重置数据",
buttonText: "重置足迹",
action: "comment_footprint:reset",
desc: "清空本地数据库中的回帖历史并重新同步。"
},
show_stats: {
type: "BUTTON",
label: "数据统计",
buttonText: "查看统计",
action: "comment_footprint:stats",
desc: "查看当前账号的回帖同步状态与记录总数。"
}
}
}
},
match: ctx => ctx.loggedIn && ctx.store.get("comment_footprint.enabled", false),
init(ctx) {
const uid = ctx.uid;
const uName = ctx.user?.member_name;
const SID = location.host.replace(/\W/g, '');
addStyle("nsx-replied-badge", REPLIED_BADGE_CSS);
// 存储与抓取配置
const getProgress = (k, def) => (GM_getValue(SID, {})[uid]?.[k] ?? def);
const setProgress = (k, v) => {
const d = GM_getValue(SID, {});
if (!d[uid]) d[uid] = {};
d[uid][k] = v;
GM_setValue(SID, d);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
// 核心同步逻辑
const sync = async (mode) => {
const isInit = mode === 'init';
let p = isInit ? getProgress(K.P, 1) : 1;
let n = 0;
let stop = 0;
const max = Math.ceil((ctx.user?.nComment || 0) / 15) || 999;
ctx.env.log(`[${SID}#${uName}] ${mode} start p:${p}`);
while (!stop && (isInit ? p <= max : true)) {
const subEl = document.querySelector('.msc-sub');
if (subEl) {
subEl.textContent = `正在同步: 第 ${p} / ${isInit ? max : '?'} 页`;
}
const res = await ctx.net.get(`/api/content/list-comments?uid=${uid}&page=${p}`);
if (!res || !res.success || !res.comments?.length) break;
for (const c of res.comments) {
if (!c.floor_id) continue;
const exist = await dbAct('readonly', (s, r) => s.get([uid, c.post_id, c.floor_id]).onsuccess = e => r(!!e.target.result));
if (!isInit && exist) {
stop = 1;
} else {
await dbAct('readwrite', (s, r) => s.put({ uid, post_id: c.post_id, floor_id: c.floor_id }).onsuccess = () => r(n++));
}
}
if (isInit) setProgress(K.P, p);
p++;
await sleep(1000);
}
const total = await dbAct('readonly', (s, r) => s.index('upid').count(IDBKeyRange.bound([uid, 0], [uid, Infinity])).onsuccess = e => r(e.target.result));
setProgress(K.C, total);
setProgress(K.T, Date.now());
if (isInit) {
setProgress(K.I, true);
setProgress(K.P, 1);
}
return n;
};
const markAll = () => {
if (!ctx.isList) return;
ctx.$$('.post-list-item').forEach(el => {
if (!el.classList.contains('nsx-replied-checked')) {
el.classList.add('nsx-replied-checked');
markElement(ctx, el, uid);
}
});
};
// 启动主同步流(加排他锁)
const startSync = () => {
navigator.locks.request(`nsx_sync_${uid}`, { ifAvailable: true }, async lock => {
markAll();
if (!lock) return;
try {
if (!getProgress(K.I)) {
const last = getProgress(K.P, 1);
const title = last > 1 ? '断点续传' : '初始化回复数据';
const msg = last > 1
? `检测到账号 [${uName}] 上次同步中断,进度第 ${last} 页。\n是否继续?`
: `检测到账号 [${uName}] 尚未同步记录。\n是否开始抓取?`;
ctx.ui.confirm(title, msg, async () => {
ctx.ui.alert('正在同步', `账号: ${uName}\n请保持页面开启...`);
try {
const n = await sync('init');
const confirmEl = document.querySelector('.msc-confirm');
if (confirmEl) confirmEl.remove();
ctx.ui.success(`同步完成: 新增 ${n} 条记录`);
markAll();
} catch (e) {
const confirmEl = document.querySelector('.msc-confirm');
if (confirmEl) confirmEl.remove();
ctx.ui.error(`同步失败: ${e.message}`);
}
});
} else {
const n = await sync('inc');
if (n > 0) markAll();
}
} catch (e) {
ctx.env.error('[NSX Critical Error]', e);
}
});
};
// 事件动作响应
const handleActions = async (e) => {
if (e.detail === 'comment_footprint:reset') {
if (!ctx.ui.layer) return;
ctx.ui.layer.confirm('仅清空当前账号的缓存记录,不影响其他账号。', { title: '确认重置?', icon: 3 }, async (index) => {
ctx.ui.layer.close(index);
try {
await dbAct('readwrite', (s, r) => {
const req = s.index('upid').openCursor(IDBKeyRange.bound([uid, 0], [uid, Infinity]));
req.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
cursor.delete();
cursor.continue();
} else {
r();
}
};
});
const d = GM_getValue(SID, {});
delete d[uid];
GM_setValue(SID, d);
ctx.ui.success("重置成功,页面即将刷新...");
setTimeout(() => location.reload(), 1000);
} catch (err) {
ctx.ui.error(`重置失败: ${err.message}`);
}
});
} else if (e.detail === 'comment_footprint:stats') {
if (!ctx.ui.layer) return;
const timeStr = getProgress(K.T) ? new Date(getProgress(K.T)).toLocaleString() : '无';
ctx.ui.layer.alert(`用户: ${uName}<br>状态: ${getProgress(K.I) ? '✅ 完成' : '⏳ 进行中'}<br>更新: ${timeStr}<br>记录: ${getProgress(K.C, 0)} 条`, { title: '数据统计', icon: 1 });
}
};
document.addEventListener('nsx-action', handleActions);
startSync();
},
watch: ctx => ({
sel: '.post-list-item:not(.nsx-replied-checked)',
fn: els => {
els.forEach(el => {
el.classList.add('nsx-replied-checked');
markElement(ctx, el, ctx.uid);
});
}
})
};
const __vite_glob_0_6 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: commentFootprint
}, Symbol.toStringTag, { value: 'Module' }));
// 快捷键发送评论 (Ctrl+Enter)
const commentShortcut = {
id: "commentShortcut",
order: 135,
cfg: { comment_shortcut: { enabled: true } },
meta: { comment_shortcut: { label: "快捷键发帖", group: "内容设置" } },
match: ctx => ctx.isPost && ctx.store.get("comment_shortcut.enabled", true),
init(ctx) {
const getBtn = () => $(".md-editor button.submit.btn.focus-visible");
$$(".CodeMirror").forEach(cmEl => {
const cm = cmEl?.CodeMirror;
if (!cm || cm.__nsx) return;
cm.__nsx = true;
const bind = () => {
const btn = getBtn();
if (btn && !/Ctrl\+Enter/i.test(btn.textContent)) btn.textContent += "(Ctrl+Enter)";
if (btn && !cm.__nsxMap) {
cm.__nsxMap = { "Ctrl-Enter": () => getBtn()?.click() };
cm.addKeyMap(cm.__nsxMap);
} else if (!btn && cm.__nsxMap) {
cm.removeKeyMap(cm.__nsxMap);
cm.__nsxMap = null;
}
};
bind();
cmEl.addEventListener("focusin", bind, true);
cmEl.addEventListener("focusout", bind, true);
});
}
};
const __vite_glob_0_7 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: commentShortcut
}, Symbol.toStringTag, { value: 'Module' }));
// 暗色模式样式切换
const darkMode = {
id: "darkMode",
order: 180,
init(ctx) {
const body = document.body;
if (!body) return;
const apply = () => {
const dark = body.classList.contains("dark-layout");
// 为 html 添加/移除 .dark 类以触发 layui 深色主题
document.documentElement.classList.toggle("dark", dark);
};
apply();
new MutationObserver(() => apply()).observe(body, { attributes: true, attributeFilter: ["class"] });
}
};
const __vite_glob_0_8 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: darkMode
}, Symbol.toStringTag, { value: 'Module' }));
// 浏览历史
const CSS$4 = `#nsx-history-panel{position:fixed;right:12px;top:56px;width:min(380px,94vw);height:70vh;background:#fff;border:1px solid #e4e4e4;border-radius:12px;box-shadow:0 16px 32px rgba(0,0,0,.12);z-index:9999;display:none;flex-direction:column;font-size:13px;color:#1f1f1f;box-sizing:border-box;font-family:"Segoe UI","Microsoft YaHei",sans-serif}#nsx-history-panel.show{display:flex}.nsx-history-header{display:flex;align-items:center;justify-content:space-between;padding:12px 12px 6px}.nsx-history-title{font-size:15px;font-weight:600}.nsx-history-action{border:0;background:0;color:#666;cursor:pointer;font-size:12px;padding:4px 8px;border-radius:6px}.nsx-history-action:hover{background:#f2f3f5}.nsx-history-search{display:flex;align-items:center;gap:6px;margin:0 12px 8px;border:1px solid #e1e1e1;border-radius:8px;padding:6px 8px}.nsx-history-search input{border:0;background:0;outline:0;width:100%;font-size:13px}.nsx-history-tabs{display:flex;gap:16px;padding:0 12px 6px;border-bottom:1px solid #f0f0f0}.nsx-history-tab{border:0;background:0;cursor:pointer;color:#6b6b6b;font-size:12px;padding:6px 0;font-weight:600;border-bottom:2px solid transparent}.nsx-history-tab.is-active{color:#0a62ff;border-bottom-color:#0a62ff}.nsx-history-list{flex:1;overflow-y:auto;padding:6px 8px 12px}.nsx-history-group{margin-bottom:10px}.nsx-history-group-title{display:flex;align-items:center;justify-content:space-between;padding:4px;color:#666;font-size:12px}.nsx-history-items{list-style:none;margin:0;padding:0}.nsx-history-item{display:flex;align-items:center;gap:8px;padding:6px;border-radius:8px}.nsx-history-item:hover{background:#f5f7fb}.nsx-history-link{display:flex;align-items:center;gap:8px;flex:1;min-width:0;text-decoration:none;color:inherit}.nsx-history-icon{width:20px;height:20px;border-radius:50%;background:#f0f0f0;display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0}.nsx-history-icon img{width:100%;height:100%;object-fit:cover}.nsx-history-item-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nsx-history-time{color:#9a9a9a;font-size:12px;margin-left:auto}.nsx-history-empty{padding:10px 6px;color:#999;font-size:12px}.nsx-history-close,.nsx-history-restore{border:0;background:0;cursor:pointer;font-size:12px;padding:2px 4px;border-radius:6px;display:none}.nsx-history-close{color:#999}.nsx-history-restore{color:#0a62ff}.nsx-history-item:hover .nsx-history-time{display:none}.nsx-history-item:hover .nsx-history-close,.nsx-history-item:hover .nsx-history-restore{display:block}.nsx-history-group-title .nsx-history-close{display:block;opacity:.9}.nsx-history-close:hover{color:#ff4d4f}.nsx-history-restore:hover{background:#eef3ff}.dark-layout #nsx-history-panel{background:#1e1e1e;border-color:#3a3a3a;color:#e0e0e0}.dark-layout .nsx-history-action{color:#999}.dark-layout .nsx-history-action:hover{background:#2a2a2a}.dark-layout .nsx-history-search{border-color:#3a3a3a}.dark-layout .nsx-history-search input{color:#e0e0e0}.dark-layout .nsx-history-tabs{border-bottom-color:#3a3a3a}.dark-layout .nsx-history-tab{color:#999}.dark-layout .nsx-history-group-title{color:#888}.dark-layout .nsx-history-item:hover{background:#2a2a2a}.dark-layout .nsx-history-icon{background:#3a3a3a}.dark-layout .nsx-history-time{color:#666}.dark-layout .nsx-history-empty{color:#666}`;
const HKEY = "nsx_browsing_history", RKEY = "nsx_recently_closed";
const pad = n => String(n).padStart(2, "0");
const fmtDate = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
const fmtTime = d => `${pad(d.getHours())}:${pad(d.getMinutes())}`;
const now = () => new Date().toISOString();
const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
const WEEK = ["日", "一", "二", "三", "四", "五", "六"];
const history$1 = {
id: "history",
order: 300,
cfg: { history: { enabled: true, limit: 100, days: 7 } },
meta: { history: { label: "浏览历史", group: "显示设置", fields: { limit: { type: "NUMBER", label: "保存上限", valueType: "number" }, days: { type: "NUMBER", label: "保存天数", valueType: "number" } } } },
match: ctx => (ctx.isPost || ctx.isList) && ctx.store.get("history.enabled", true),
init(ctx) {
const maxItems = ctx.store.get("history.limit", 100) || 100;
const maxAge = (ctx.store.get("history.days", 7) || 7) * 864e5;
const prune = arr => {
const t = Date.now();
return (arr || []).filter(i => t - new Date(i.time).getTime() < maxAge).sort((a, b) => new Date(a.time) - new Date(b.time)).slice(-maxItems);
};
const load = k => { try { const r = JSON.parse(localStorage.getItem(k) || "[]"); const n = prune(r); if (n.length !== r.length) localStorage.setItem(k, JSON.stringify(n)); return n; } catch { return []; } };
const save = (k, a) => localStorage.setItem(k, JSON.stringify(prune(a)));
const getH = () => load(HKEY), saveH = a => save(HKEY, a);
const getR = () => load(RKEY), saveR = a => save(RKEY, a);
// 使用 postData 获取帖子信息
const add = (pd, list, saveFn) => {
if (!pd?.postId) return;
const id = pd.postId;
const h = list(), i = h.findIndex(x => x.postId === id);
const e = { postId: id, title: pd.title || document.title, time: now(), uid: pd.op?.uid || null, author: pd.op?.name || null };
i > -1 ? Object.assign(h[i], e) : h.push(e);
saveFn(h);
};
addStyle("nsx-hist", CSS$4);
let panel = null, trigger = null, state = { open: false, tab: "all", kw: "" };
const orig = $("#nsk-head .color-theme-switcher");
if (!orig) return;
trigger = orig.cloneNode(false);
trigger.classList.replace("color-theme-switcher", "history-dropdown-on");
trigger.title = "历史记录";
trigger.innerHTML = `<svg class="iconpark-icon" style="width:17px;height:17px"><use href="#history"></use></svg>`;
orig.before(trigger);
const fmtDayTitle = day => {
const d = new Date(`${day}T00:00:00`);
const title = `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 星期${WEEK[d.getDay()]}`;
return day === fmtDate(new Date()) ? `今天 - ${title}` : title;
};
const open = () => {
if (!panel) {
panel = document.createElement("div");
panel.id = "nsx-history-panel";
panel.innerHTML = `<div class="nsx-history-header"><div class="nsx-history-title">历史记录</div><button class="nsx-history-action" data-a="clear">清空</button></div><div class="nsx-history-search">🔍<input placeholder="搜索"/></div><div class="nsx-history-tabs"><button class="nsx-history-tab is-active" data-t="all">全部</button><button class="nsx-history-tab" data-t="recent">最近关闭</button></div><div class="nsx-history-list"></div>`;
document.body.appendChild(panel);
panel.querySelector("input").oninput = e => { state.kw = e.target.value.toLowerCase(); render(); };
panel.onclick = e => {
e.stopPropagation();
const t = e.target.closest("[data-t]");
if (t) { state.tab = t.dataset.t; render(); return; }
const a = e.target.closest("[data-a]");
if (!a) return;
const act = a.dataset.a, id = a.dataset.id;
if (act === "clear") ctx.ui.confirm("确认", "确定要清空所有记录吗?", () => { localStorage.removeItem(state.tab === "recent" ? RKEY : HKEY); render(); });
if (act === "del") { state.tab === "recent" ? saveR(getR().filter(x => x.postId != id)) : saveH(getH().filter(x => x.postId != id)); render(); }
if (act === "clear-day") { const key = state.tab === "recent" ? RKEY : HKEY; save(key, load(key).filter(i => fmtDate(new Date(i.time)) !== a.dataset.day)); render(); }
if (act === "restore") window.open(`/post-${id}-1`, "_blank");
};
document.addEventListener("click", e => { if (state.open && !panel.contains(e.target) && !trigger.contains(e.target)) close(); });
document.addEventListener("keydown", e => { if (state.open && e.key === "Escape") close(); });
}
const r = trigger.getBoundingClientRect();
panel.style.top = `${r.bottom + 8}px`;
panel.style.height = `${innerHeight - r.bottom - 16}px`;
render();
panel.classList.add("show");
state.open = true;
};
const close = () => { panel?.classList.remove("show"); state.open = false; };
const toggle = () => state.open ? close() : open();
const render = () => {
let list = (state.tab === "recent" ? getR() : getH()).sort((a, b) => new Date(b.time) - new Date(a.time));
if (state.kw) list = list.filter(i => (i.title || "").toLowerCase().includes(state.kw));
panel.querySelectorAll(".nsx-history-tab").forEach(b => b.classList.toggle("is-active", b.dataset.t === state.tab));
const lEl = panel.querySelector(".nsx-history-list");
if (!list.length) { lEl.innerHTML = `<div class="nsx-history-empty">暂无记录</div>`; return; }
const g = {};
list.forEach(i => { const d = fmtDate(new Date(i.time)); (g[d] ||= []).push(i); });
lEl.innerHTML = Object.entries(g).map(([day, items]) => {
const itemsHtml = items.map(i => {
if (!i.postId) return "";
const url = `/post-${i.postId}-1`;
const avatar = i.uid ? `<img src="/avatar/${i.uid}.png" onerror="this.style.display='none'">` : "";
const restore = state.tab === "recent" ? `<button class="nsx-history-restore" data-a="restore" data-id="${i.postId}" title="恢复">↗</button>` : "";
return `<li class="nsx-history-item"><a class="nsx-history-link" href="${url}"><span class="nsx-history-icon"${i.author ? ` title="@${esc(i.author)}"` : ""}>${avatar}</span><span class="nsx-history-item-title">${esc((i.title || "").slice(0, 32))}</span></a><span class="nsx-history-time">${fmtTime(new Date(i.time))}</span>${restore}<button class="nsx-history-close" data-a="del" data-id="${i.postId}">✖</button></li>`;
}).join("");
return `<div class="nsx-history-group"><div class="nsx-history-group-title"><span>${fmtDayTitle(day)}</span><button class="nsx-history-close" data-a="clear-day" data-day="${day}" title="清除当天">✕</button></div><ul class="nsx-history-items">${itemsHtml}</ul></div>`;
}).join("");
};
trigger.onclick = e => { e.preventDefault(); e.stopPropagation(); toggle(); };
// 记录当前页面
const pd = ctx.uw?.__config__?.postData;
if (pd) add(pd, getH, saveH);
// 监听页面关闭
addEventListener("beforeunload", () => {
const pd = ctx.uw?.__config__?.postData;
if (pd) add(pd, getR, saveR);
}, { capture: true });
}
};
const __vite_glob_0_9 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: history$1
}, Symbol.toStringTag, { value: 'Module' }));
// 图片预览
const mark = new WeakSet();
const bind = (els, ctx) => {
els.forEach(img => {
const post = img.closest("article.post-content");
if (!post || mark.has(img)) return;
mark.add(img);
const newImg = img.cloneNode(true);
img.replaceWith(newImg);
mark.add(newImg);
newImg.addEventListener("click", e => {
e.preventDefault();
const imgs = [...post.querySelectorAll("img:not(.sticker)")];
const data = imgs.map((x, i) => ({ alt: x.alt, pid: i + 1, src: x.src }));
ctx.ui.layer?.photos({ photos: { title: "图片预览", start: imgs.indexOf(newImg), data } });
}, true);
});
};
const imageSlide = {
id: "imageSlide",
order: 160,
cfg: { image_slide: { enabled: true } },
meta: { image_slide: { label: "图片预览", group: "内容设置" } },
match: ctx => ctx.isPost && ctx.store.get("image_slide.enabled", true),
init(ctx) { bind($$("article.post-content img:not(.sticker)"), ctx); },
watch: ctx => ({ sel: "article.post-content img:not(.sticker)", fn: els => bind(els, ctx), opts: { debounce: 80 } })
};
const __vite_glob_0_10 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: imageSlide
}, Symbol.toStringTag, { value: 'Module' }));
// 图床上传模块 (NodeSeek 编辑器增强)
// 🌐 底层跨域网络请求封装
const api = (url, data, h = {}) => new Promise((res, rej) => GM_xmlhttpRequest({
method: 'POST',
url,
headers: h,
data,
onload: r => {
try {
res(JSON.parse(r.responseText));
} catch (e) {
rej(new Error(`解析响应失败: ${r.responseText}`));
}
},
onerror: rej
}));
const getFd = (k, f, ex = {}) => {
let d = new FormData();
d.append(k, f);
Object.entries(ex).forEach(([key, val]) => d.append(key, val));
return d;
};
const getImg = items => Array.from(items || []).filter(i => /image\//.test(i.type || i.kind)).map(i => i.getAsFile ? i.getAsFile() : i);
let uploadFn = null;
const imageUpload = {
id: "imageUpload",
order: 250,
cfg: {
image_upload: {
enabled: false,
active: "Chevereto",
url: "",
token: "",
headers: ""
}
},
meta: {
image_upload: {
label: "图床上传",
group: "图床设置",
fields: {
active: {
type: "SELECT",
label: "当前图床",
options: [
{ text: "Chevereto", value: "Chevereto" },
{ text: "LskyPro", value: "LskyPro" },
{ text: "EasyImages", value: "EasyImages" },
{ text: "Telegraph (含自建)", value: "Telegraph" },
{ text: "Telegraph v2", value: "Telegraph2" }
]
},
url: { type: "TEXT", label: "图床 URL", placeholder: "https://example.com", desc: "图床服务的基础 URL(例如:https://example.com)" },
token: { type: "TEXT", label: "API Token", placeholder: "chv_q2L_... 或留空", desc: "API Token 或 Key,Telegraph 可不填" },
headers: { type: "TEXTAREA", label: "自定义 Headers", placeholder: "{\n \"Authorization\": \"Basic YWRtaW46ODMwNTA2NjM=\"\n}", desc: "可选,标准 JSON 格式,例如:{\"Authorization\": \"Basic ...\"}" }
}
}
},
match: ctx => ctx.store.get("image_upload.enabled", true),
init(ctx) {
// ⚡ 并发上传引擎
const upload = async (files) => {
const active = ctx.store.get("image_upload.active", "Chevereto");
const baseUrl = ctx.store.get("image_upload.url", "https://example.com").replace(/\/$/, "");
const token = ctx.store.get("image_upload.token", "");
let extraHeaders = {};
try {
const rawHeaders = ctx.store.get("image_upload.headers", "");
if (rawHeaders) {
extraHeaders = JSON.parse(rawHeaders);
}
} catch (e) {
console.error("[NSX-IMG] 解析自定义 Headers 失败:", e);
}
// 🔀 图床策略路由 (支持自定义 Header 合并)
const HOSTS = {
Telegraph: f => ({ u: `${baseUrl}/upload`, d: getFd('file', f), h: { ...extraHeaders }, p: r => ` ? '' : '/'}${r[0].src})` }),
Telegraph2: f => ({ u: `${baseUrl}/upload`, d: getFd('file', f), h: { ...extraHeaders }, p: r => `` }),
LskyPro: f => ({ u: `${baseUrl}/api/v1/upload`, d: getFd('file', f), h: { Accept: 'application/json', Authorization: `Bearer ${token}`, ...extraHeaders }, p: r => `` }),
Chevereto: f => ({ u: `${baseUrl}/api/1/upload`, d: getFd('source', f), h: { Accept: 'application/json', 'X-API-Key': token, ...extraHeaders }, p: r => `` }),
EasyImages: f => ({ u: `${baseUrl}${token ? '/api/index.php' : '/app/upload.php'}`, d: getFd(token ? 'image' : 'file', f, token ? { token: token } : { sign: Math.floor(Date.now() / 1000) }), h: { ...extraHeaders }, p: r => `` })
};
const S = HOSTS[active];
if (!S || !files.length) return;
const cm = document.querySelector('.CodeMirror')?.CodeMirror;
console.log(`[NSX-IMG] 🚀 并发上传 ${files.length} 张图片...`);
const log = (msg, col = '') => {
let b = document.getElementById('ex-log') || document.querySelector('.mde-toolbar')?.appendChild(Object.assign(document.createElement('div'), { id: 'ex-log' }));
if (b) b.innerHTML = `<span style="color:${col}; margin-left:10px">${msg}</span>`;
};
log('正在上传', 'green');
if (ctx.ui?.info) {
ctx.ui.info(`开始并发上传 ${files.length} 张图片...`);
}
let successCount = 0;
let failCount = 0;
await Promise.all(files.map(async f => {
try {
let { u, d, h, p } = S(f), res = await api(u, d, h);
if (cm) cm.replaceRange(`\n${p(res)}\n`, cm.getCursor());
successCount++;
log('上传成功', 'green');
} catch (e) {
failCount++;
log('上传失败', 'red');
console.error('[NSX-IMG] ❌ 上传失败', e);
}
}));
if (ctx.ui?.toast) {
if (failCount === 0) {
ctx.ui.success(`全部图片上传成功!(共 ${successCount} 张)`);
} else if (successCount > 0) {
ctx.ui.warning(`图片上传完成: ${successCount} 张成功, ${failCount} 张失败。`);
} else {
ctx.ui.error(`图片上传全部失败!`);
}
}
};
uploadFn = upload;
// 1. 全局事件委托劫持粘贴 (粘贴拦截)
document.addEventListener('paste', e => {
if (!e.target.closest('.CodeMirror') && !e.target.closest('.mde-toolbar')) return;
let f = getImg((e.clipboardData || e.originalEvent.clipboardData).items);
if (f.length) {
e.preventDefault();
upload(f);
}
});
// 2. 全局事件委托劫持拖拽 (拖拽拦截)
document.addEventListener('dragover', e => {
if (e.target.closest('.CodeMirror')) {
e.preventDefault();
}
});
document.addEventListener('drop', e => {
if (e.target.closest('.CodeMirror')) {
e.preventDefault();
let f = getImg(e.dataTransfer.files);
if (f.length) {
upload(f);
}
}
});
},
watch: ctx => ({
sel: '.i-icon-pic[title="图片"]:not(.t-hj)',
fn: els => els.forEach(ob => {
let nb = ob.cloneNode(true);
nb.classList.add('t-hj');
ob.replaceWith(nb);
nb.onclick = () => {
let i = document.createElement('input');
i.type = 'file';
i.multiple = true;
i.accept = 'image/*';
i.onchange = e => {
if (uploadFn) {
uploadFn(getImg(e.target.files));
}
};
i.click();
};
})
})
};
const __vite_glob_0_11 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: imageUpload
}, Symbol.toStringTag, { value: 'Module' }));
// 悬停预加载
const instantPage = {
id: "instantPage",
order: 320,
cfg: { instant_page: { enabled: true } },
meta: { instant_page: { label: "网页预加载", group: "内容设置" } },
match: ctx => ctx.store.get("instant_page.enabled", true),
init(ctx) {
const done = new Set();
const link = document.createElement("link");
link.rel = "prefetch";
document.body.addEventListener("mouseover", e => {
const a = e.target.closest("a");
if (!a?.href?.startsWith(`${location.origin}/post-`) || done.has(a.href)) return;
setTimeout(() => {
if (a.matches(":hover")) {
link.href = a.href;
document.head.appendChild(link);
done.add(a.href);
}
}, 65);
}, { passive: true });
}
};
const __vite_glob_0_12 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: instantPage
}, Symbol.toStringTag, { value: 'Module' }));
// 等级标签
const CSS$3 = `.role-tag.user-level{color:#fafafa}.user-lv0{background:#c7c2c2;border-color:#c7c2c2}.user-lv1{background:#ffb74d;border-color:#ffb74d}.user-lv2{background:#ff9400;border-color:#ff9400}.user-lv3{background:#ff5252;border-color:#ff5252}.user-lv4{background:#e53935;border-color:#e53935}.user-lv5{background:#ab47bc;border-color:#ab47bc}.user-lv6{background:#8e24aa;border-color:#8e24aa}.user-lv7{background:#42a5f5;border-color:#42a5f5}.user-lv8{background:#1e88e5;border-color:#1e88e5}.user-lv9{background:#66bb6a;border-color:#66bb6a}.user-lv10{background:#2e7d32;border-color:#2e7d32}.user-lv11{background:#ffca28;border-color:#ffca28}.user-lv12{background:#ffb300;border-color:#ffb300}.user-lv13{background:#b388ff;border-color:#b388ff}.user-lv14{background:#7c4dff;border-color:#7c4dff}.user-lv15{background:#000;border-color:#000;color:#ffd700}`;
const levelTag = {
id: "levelTag",
order: 260,
cfg: { level_tag: { enabled: true, low_lv_alarm: true, low_lv_max_days: 30 } },
meta: { level_tag: { label: "等级标签", group: "显示设置", fields: { low_lv_alarm: { type: "SWITCH", label: "低等级警告" }, low_lv_max_days: { type: "NUMBER", label: "注册天数", valueType: "number" } } } },
match: ctx => ctx.loggedIn && ctx.isPost && ctx.store.get("level_tag.enabled", true),
async init(ctx) {
addStyle("nsx-lv", CSS$3);
const opUid = ctx.uw?.__config__?.postData?.op?.uid;
if (!opUid) return;
let user;
try {
const r = await net.get(`/api/account/getInfo/${opUid}`);
if (!r?.success) return;
user = r.detail;
} catch { return; }
const days = Math.floor((Date.now() - new Date(user.created_at)) / 864e5);
const alarm = ctx.store.get("level_tag.low_lv_alarm") && days < ctx.store.get("level_tag.low_lv_max_days", 30) ? "⚠️" : "";
const coin = user.coin < 0 ? 0 : user.coin;
const rank = Math.floor(Math.sqrt(coin) / 10);
const span = document.createElement("span");
span.className = `nsk-badge role-tag user-level user-lv${rank}`;
span.innerHTML = `<span>${alarm}Lv ${rank}</span>`;
span.onmouseenter = () => ctx.ui.tips?.(`注册 <span class="layui-badge">${days}</span> 天;帖子 ${user.nPost};评论 ${user.nComment}`, span, { tips: 3, time: 0 });
span.onmouseleave = () => ctx.ui.layer?.closeAll?.();
ctx.$('#nsk-body .nsk-post .nsk-content-meta-info .author-info>a')?.after(span);
}
};
const __vite_glob_0_13 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: levelTag
}, Symbol.toStringTag, { value: 'Module' }));
// 链接净化器 — DSL 规则引擎 / 去跳板 / 短链解析 / 外链标记
/* ── 默认规则 (DSL 文本) ── */
const DEFAULT_RULES = `
# ── 宏定义 ──
@utm = utm_source, utm_medium, utm_campaign, utm_content, utm_term
@ad_ids = ad_id, clickid, gclid, fbclid, sc_cid
@invite = ic, invite, invitation, invited_by, ref, referral, referrer
@aff = aff, affiliate, partner, promo, promocode, coupon, subid, affid, aff_id
@track = aid, pid, cid, tid, sid, uid, ref_id, tag
@channel = via, from, source, campaign, channel
# ── 全局过滤 ──
* >> @utm, @ad_ids, @invite, @aff, @track, @channel
# ── YouTube ──
*.youtube.com youtu.be >> si, feature, pp
# ── B站 ──
*.bilibili.com b23.tv >> spm_id_from, from_source, from_spmid, from, seid, share_source, share_medium, share_plat, share_tag, share_session_id, share_from, bbid, ts, timestamp, unique_k, rt, tdsourcetag, spm, vd_source, trackid
# ── Amazon Path 正则 ──
*.amazon.com >> /\\/ref=[^\\/]+/
# ── 豁免 (防误杀) ──
~github.com ~gitlab.com ~gitee.com >> ref
~t.me ~telegram.me >> start
`.trim();
const DEF_SHORT = [
'bit.ly', 'goo.gl', 't.co', 't.cn', 'ow.ly', 'is.gd',
'buff.ly', 'tinyurl.com', 'tr.im', 'shorturl.at', 'rebrand.ly',
'su.pr', 'i3z.cc', 'b23.tv'
];
const SEL = '.post-content a[href], .markdown-body a[href], .comment-content a[href]';
/* ── CSS ── */
const ICON_SVG = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'%3E%3C/path%3E%3Cpolyline points='15 3 21 3 21 9'%3E%3C/polyline%3E%3Cline x1='10' y1='14' x2='21' y2='3'%3E%3C/line%3E%3C/svg%3E")`;
const CSS$2 = `
a.nsp-ext::after{content:"";display:inline-block;width:12px;height:12px;margin-left:4px;background:${ICON_SVG} no-repeat center/contain;vertical-align:middle;opacity:.7}
a.nsp-cleaned{border-bottom:1px dashed #28a745!important;text-decoration:none}
a.nsp-cleaned:hover{background:rgba(40,167,69,.1)}
a.nsp-cleaned[data-nsp-tip]:hover::before{content:attr(data-nsp-tip);position:absolute;background:#333;color:#f0f0f0;padding:5px 10px;border-radius:4px;font-size:12px;font-family:monospace;white-space:pre;transform:translateY(-100%);margin-top:-6px;z-index:9999;pointer-events:none;box-shadow:0 4px 6px rgba(0,0,0,.3);border:1px solid #444}
a.nsp-resolving{cursor:wait;opacity:.6}
a.nsp-resolving::after{content:"";display:inline-block;width:10px;height:10px;margin-left:5px;border:2px solid #888;border-top-color:transparent;border-radius:50%;animation:nsp-spin 1s linear infinite;vertical-align:middle;background-image:none!important}
@keyframes nsp-spin{to{transform:rotate(360deg)}}`;
/* ── 工具 ── */
const tryURL = (v, b) => { try { return new URL(v, b); } catch { return null; } };
const isExt = u => /^https?:$/.test(u.protocol) && u.hostname && u.hostname !== location.hostname;
/* ── DSL 规则解析器 (源自 t.js) ── */
function parseRules(text) {
const rules = { allow: [], block: [], pathBlock: [] }, macros = {};
text.split('\n').filter(l => l.trim() && l[0] !== '#').forEach(line => {
if (line[0] === '@') {
const idx = line.indexOf('=');
if (idx === -1) return;
macros[line.slice(0, idx).trim()] = line.slice(idx + 1).split(',').map(s => s.trim());
return;
}
const idx = line.indexOf('>>');
if (idx === -1) return;
const scopeStr = line.slice(0, idx).trim();
const paramStr = line.slice(idx + 2).trim();
if (!paramStr) return;
const isAllow = scopeStr[0] === '~';
const scopes = scopeStr.split(/\s+/).map(s => s.replace(/^~/, ''));
paramStr.split(',').flatMap(p => macros[p.trim()] || [p.trim()]).forEach(p => {
if (p.startsWith('/') && p.endsWith('/')) {
try { if (!isAllow) rules.pathBlock.push({ scopes, regex: new RegExp(p.slice(1, -1)) }); } catch { }
} else {
let matcher;
if (p === '*') matcher = () => true;
else if (p.includes('*')) {
const re = new RegExp('^' + p.split('*').map(s => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$', 'i');
matcher = t => re.test(t);
} else {
const lp = p.toLowerCase();
matcher = t => t.toLowerCase() === lp;
}
rules[isAllow ? 'allow' : 'block'].push({ scopes, matcher });
}
});
});
return rules;
}
/* ── URL 净化 (源自 t.js,增加日志输出) ── */
function purifyUrl(rawUrl, rules) {
try {
const u = new URL(rawUrl);
if (!u.protocol.startsWith('http')) return { url: rawUrl, logs: [] };
if (!u.search && !u.hash.includes('?') && !rules.pathBlock.length) return { url: rawUrl, logs: [] };
const match = (t, s) => s === '*' || t === s || t.endsWith('.' + s);
const logs = [];
const clean = paramStr => {
const params = new URLSearchParams(paramStr);
const del = [...params.keys()].filter(k => {
const hit = list => list.some(r => r.scopes.some(s => match(u.hostname, s)) && r.matcher(k));
return !hit(rules.allow) && hit(rules.block);
});
if (!del.length) return null;
del.forEach(k => { params.delete(k); logs.push(k); });
return params.toString();
};
let mod = false;
const ns = clean(u.search);
if (ns !== null) { u.search = ns; mod = true; }
if (u.hash.includes('?')) {
const qIdx = u.hash.indexOf('?');
const hp = u.hash.slice(0, qIdx);
const hq = u.hash.slice(qIdx + 1);
const nh = clean(hq);
if (nh !== null) { u.hash = nh ? `${hp}?${nh}` : hp; mod = true; }
}
let np = u.pathname;
rules.pathBlock.forEach(r => {
if (r.scopes.some(s => match(u.hostname, s))) np = np.replace(r.regex, '');
});
if (np !== u.pathname) { u.pathname = np.replace(/\/+/g, '/') || '/'; mod = true; logs.push('(path)'); }
return { url: mod ? u.toString() : rawUrl, logs };
} catch { return { url: rawUrl, logs: [] }; }
}
/* ── 去跳板 ── */
function unwrapJump(u) {
const logs = [];
for (let i = 0; i < 3 && u.origin === location.origin && u.pathname === '/jump' && u.searchParams.has('to'); i++) {
const next = tryURL(u.searchParams.get('to'), location.href);
if (!next) break;
u = next;
logs.push('🛡️ 去重定向直连');
}
return { u, logs };
}
/* ── 短链解析 ── */
const shortCache = new Map();
function resolveShort(href) {
if (!shortCache.has(href)) {
shortCache.set(href, new Promise(resolve => {
const fallback = () => m === 'HEAD' ? try_('GET') : resolve({ ok: false, url: href });
const try_ = m => GM_xmlhttpRequest({
method: m, url: href, timeout: 10000,
onload: r => resolve({ ok: true, url: r.finalUrl || href }),
onerror: fallback,
ontimeout: fallback
});
try_('HEAD');
}));
}
return shortCache.get(href);
}
/* ── 规则编辑模态框 ── */
function openRuleEditor(ctx) {
if (!ctx.ui.layer) return;
const cur = ctx.store.get("link_purifier.rules", DEFAULT_RULES);
ctx.ui.layer.open({
type: 1, title: "📝 链接净化规则", area: ['660px', '520px'],
content: '<div style="padding:15px"><textarea id="nsp-rule-ta" style="width:100%;height:380px;font-family:monospace;font-size:13px;line-height:1.6;resize:vertical;padding:12px;border:1px solid #ddd;border-radius:6px;white-space:pre;tab-size:4;box-sizing:border-box;outline:none"></textarea></div>',
btn: ['保存规则', '恢复默认', '取消'],
success: () => { const ta = document.getElementById('nsp-rule-ta'); if (ta) ta.value = cur; },
yes(idx) {
const ta = document.getElementById('nsp-rule-ta');
if (ta) {
const newRules = ta.value.trim();
ctx.store.set("link_purifier.rules", newRules);
ctx.ui.layer.msg("规则已保存,刷新页面后生效");
}
ctx.ui.layer.close(idx);
},
btn2: () => { const ta = document.getElementById('nsp-rule-ta'); if (ta) ta.value = DEFAULT_RULES; return false; }
});
}
/* ── 模块导出 ── */
const linkPurifier = {
id: "link_purifier",
order: 300,
cfg: { link_purifier: { enabled: true, short_hosts: DEF_SHORT, mark_external: true, force_blank: true, edit_rules: null } },
meta: {
link_purifier: {
label: "链接净化",
group: "实验性",
hidden: ["rules"],
fields: {
short_hosts: { type: "TEXTAREA", label: "短链域名", placeholder: "每行一个域名", desc: "将这里的域名当做短链接网关,脚本会自动解析并替换为真实的最终跳转地址!" },
mark_external: { label: "外链图标标记" },
force_blank: { label: "外链新标签页打开" },
edit_rules: { type: "BUTTON", label: "净化规则", buttonText: "编辑规则", action: "edit_link_rules" }
}
}
},
match: ctx => ctx.store.get("link_purifier.enabled", true),
init(ctx) {
addStyle("nsx-link-purifier", CSS$2);
const shortHosts = new Set(ctx.store.get("link_purifier.short_hosts", DEF_SHORT).map(s => s.toLowerCase()));
const markExt = ctx.store.get("link_purifier.mark_external", true);
const forceBlank = ctx.store.get("link_purifier.force_blank", true);
const activeRules = parseRules(ctx.store.get("link_purifier.rules", DEFAULT_RULES));
document.addEventListener("nsx-action", e => {
if (e.detail === "edit_link_rules") openRuleEditor(ctx);
});
const processed = new WeakMap();
async function processLink(a) {
const href = a.getAttribute('href');
if (!href) return;
if (processed.get(a) === href) return;
let u = tryURL(href, location.href);
if (!u) return;
const logs = [];
let modified = false;
// 1. 短链解析
if (shortHosts.has(u.hostname.toLowerCase())) {
a.classList.add('nsp-resolving');
const r = await resolveShort(u.toString());
a.classList.remove('nsp-resolving');
if (r.ok) {
const res = tryURL(r.url);
if (res) { u = res; logs.push(`🔍 短链: ${new URL(href, location.href).hostname}`); modified = true; }
}
}
// 2. 去跳板
const j = unwrapJump(u);
if (j.logs.length) { u = j.u; logs.push(...j.logs); modified = true; }
if (!a.isConnected || a.getAttribute('href') !== href) return;
// 3. DSL 规则净化
const p = purifyUrl(u.toString(), activeRules);
if (p.logs.length) {
u = new URL(p.url);
logs.push(`✂️ 移除: ${p.logs.join(', ')}`);
modified = true;
}
// 4. 应用结果
if (modified) {
a.href = u.toString();
a.classList.add('nsp-cleaned');
if (logs.length) a.setAttribute('data-nsp-tip', logs.join('\n'));
}
// 5. 外链标记
if (isExt(u)) {
if (markExt) a.classList.add('nsp-ext');
if (forceBlank) {
a.target = '_blank';
if (a.relList) a.relList.add('noopener', 'noreferrer');
}
}
processed.set(a, a.getAttribute('href'));
}
// 批处理队列
const queue = new Set();
let flushing = false;
function enqueue(root) {
if (root instanceof HTMLAnchorElement && root.matches(SEL)) queue.add(root);
root?.querySelectorAll?.(SEL).forEach(a => queue.add(a));
if (flushing) return;
flushing = true;
Promise.resolve().then(async () => {
flushing = false;
const batch = [...queue];
queue.clear();
await Promise.allSettled(batch.map(processLink));
if (queue.size) enqueue();
});
}
enqueue(document);
const root = document.body || document.documentElement;
if (!root) return;
new MutationObserver(ms => {
for (const m of ms) {
if (m.type === 'childList') m.addedNodes.forEach(n => n.nodeType === 1 && enqueue(n));
else if (m.type === 'attributes' && m.target instanceof HTMLAnchorElement) enqueue(m.target);
}
}).observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] });
}
};
const __vite_glob_0_14 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: linkPurifier
}, Symbol.toStringTag, { value: 'Module' }));
// 菜单系统(油猴菜单 + 高级设置面板)
const CSS$1 = `#nsx-config-menu{height:100%;overflow-y:visible;border-right:1px solid #eee}#nsx-config-content{height:100%;overflow-y:auto;padding:0 15px;background:#f8f8f8}.nsx-config-card{margin-bottom:20px}.nsx-config-card .layui-card-header{display:flex;align-items:center;justify-content:space-between;font-weight:700}.nsx-config-card .header-checkbox{position:absolute;right:15px;top:50%;transform:translateY(-50%)}.nsx-config-card .layui-form-switch{margin-top:0!important}.nsx-config-card .layui-card-body:empty{padding-top:0;padding-bottom:0}.dark-layout #nsx-config-menu{border-right-color:#3a3a3a}.dark-layout #nsx-config-content{background:#1e1e1e}`;
const el = (t, c, p, s) => { const e = document.createElement(t); if (c) e.className = c; if (s) e.style.cssText = s; if (p) p.appendChild(e); return e; };
const menus = {
id: "menus",
order: 30,
match: () => true,
init(ctx) {
ctx.uw; const code = ctx.site?.code || "ns";
const ids = [];
const txt = (m, v) => `${m.text}: ${m.states[v].s1} ${m.states[v].s2}`;
const regMenus = () => {
ids.splice(0).forEach(i => GM_unregisterMenuCommand(i));
menus.forEach(m => {
let lbl = m.text;
if (m.states.length > 0) {
let v = 0;
if (m.name === "sign_in") v = store.get(`sign_in.${code}.method`, 0);
else v = store.get(`${m.name}.enabled`, true) === false ? 0 : 1;
lbl = txt(m, v);
}
const id = GM_registerMenuCommand(lbl, () => m.cb(m.name, m.states), { autoClose: m.autoClose ?? true });
ids.push(id || lbl);
});
};
const switchState = (n, states) => {
if (n === "sign_in") {
if (!ctx.site) return;
let cur = store.get(`sign_in.${code}.method`, 0);
cur = (cur + 1) % states.length;
store.set(`sign_in.${code}.enabled`, cur !== 0);
store.set(`sign_in.${code}.method`, cur || 1);
} else if (n === "loading_post") {
const next = !store.get("loading_post.enabled", true);
store.set("loading_post.enabled", next);
store.set("loading_comment.enabled", next);
} else {
store.set(`${n}.enabled`, !store.get(`${n}.enabled`, true));
}
regMenus();
};
const reSign = () => {
if (!ctx.loggedIn || store.get(`sign_in.${code}.enabled`, true) === false) return ctx.ui.alert("提示", "签到已关闭");
store.set(`sign_in.${code}.last_date`, "1753/1/1");
location.reload();
};
const advSettings = () => {
if (!ctx.ui.layer || !window.layui) return;
addStyle("nsx-cfg", CSS$1);
// 获取所有模块的 cfg 和 meta
const defs = store.getDefaults(), metas = store.getMeta();
const ignore = new Set(["version", "debug", "ui"]);
const entries = Object.entries(metas).filter(([k]) => defs[k] && !ignore.has(k)).map(([k, m]) => ({ key: k, meta: m }));
const groups = {};
entries.forEach(e => { const g = e.meta.group || "其他设置"; (groups[g] ||= []).push(e); });
const cont = document.createElement("div");
cont.className = "layui-row";
cont.style.cssText = "display:flex;height:100%";
const menuDiv = el("div", "layui-panel layui-col-xs3", cont);
menuDiv.id = "nsx-config-menu";
const menuList = el("ul", "layui-menu", menuDiv);
const wrapper = el("div", "layui-col-xs9", cont);
wrapper.id = "nsx-config-content";
const isObj = v => v && typeof v === "object" && !Array.isArray(v);
const inferType = (v, m) => m?.type || (Array.isArray(v) ? "TEXTAREA" : typeof v === "boolean" ? "SWITCH" : typeof v === "number" ? "NUMBER" : "TEXT");
const inferVT = (v, m) => m?.valueType || (Array.isArray(v) ? "array" : typeof v === "number" ? "number" : typeof v === "boolean" ? "boolean" : "string");
const makeField = (f, path, val, defaultCol = 12) => {
const col = f.col ?? defaultCol;
const w = el("div", `layui-col-md${col}`), item = el("div", "layui-form-item", w);
const lbl = el("label", "layui-form-label", item); lbl.textContent = f.label || f.key;
if (f.desc) {
const icon = el("i", "layui-icon layui-icon-help", lbl);
icon.style.cssText = "font-size:14px;color:#999;margin-left:4px;";
lbl.style.cursor = "help";
lbl.setAttribute("data-desc", f.desc);
}
const blk = el("div", "layui-input-block", item);
let inp;
if (f.type === "SWITCH") { inp = el("input", "", blk); inp.type = "checkbox"; if (val) inp.setAttribute("checked", ""); inp.setAttribute("lay-skin", "switch"); inp.setAttribute("lay-text", "开启|关闭"); inp.name = path; }
else if (f.type === "TEXTAREA") { inp = el("textarea", "layui-textarea", blk); inp.setAttribute("placeholder", f.placeholder || ""); inp.textContent = Array.isArray(val) ? val.join("\n") : (val ?? ""); inp.name = path; }
else if (f.type === "RADIO" && f.options) {
f.options.forEach(opt => {
const r = el("input", "", blk); r.type = "radio"; r.name = path; r.setAttribute("value", opt.value);
r.dataset.valueType = f.valueType || "";
if (String(val) === String(opt.value)) r.setAttribute("checked", "");
r.setAttribute("title", opt.text);
});
inp = blk.querySelector("input");
}
else if (f.type === "SELECT" && f.options) {
inp = el("select", "", blk); inp.name = path;
f.options.forEach(opt => {
const o = el("option", "", inp); o.value = opt.value; o.textContent = opt.text;
if (String(val) === String(opt.value)) o.setAttribute("selected", "");
});
}
else if (f.type === "COLOR") {
const inpWrap = el("div", "layui-input-inline", blk); inpWrap.style.width = "100px";
inp = el("input", "layui-input", inpWrap); inp.type = "text"; inp.name = path; inp.value = val ?? ""; inp.readOnly = true;
inp.style.cssText = `background:${val || "#fff"};cursor:pointer;color:transparent`;
const cpWrap = el("div", "layui-inline", blk); cpWrap.style.left = "-11px";
const wrap = el("div", "", cpWrap);
wrap.dataset.colorPath = path; wrap.dataset.colorVal = val ?? ""; wrap.dataset.colorInp = inp.name; wrap.dataset.colorDefault = f.defaultVal ?? "";
}
else if (f.type === "BUTTON") {
inp = el("button", "layui-btn layui-btn-primary layui-btn-sm", blk);
inp.type = "button";
inp.textContent = f.buttonText || "点击执行";
inp.style.marginTop = "4px";
if (f.action) inp.setAttribute("onclick", `document.dispatchEvent(new CustomEvent('nsx-action', { detail: '${f.action}' }))`);
}
else { inp = el("input", "layui-input", blk); inp.type = f.type === "NUMBER" ? "number" : "text"; inp.setAttribute("placeholder", f.placeholder || ""); inp.setAttribute("value", val ?? ""); inp.name = path; }
if (inp) inp.dataset.valueType = f.valueType || "";
return w;
};
const makeCard = (entry, siteCode) => {
const m = entry.meta || {};
let base = entry.key, cfg = defs[entry.key];
if (entry.key === "sign_in") { cfg = defs.sign_in?.[siteCode] || defs.sign_in?.ns || {}; base = `sign_in.${siteCode}`; }
if (!isObj(cfg)) return null;
const card = el("div", "layui-card layui-form nsx-config-card");
card.setAttribute("lay-filter", `nsx-${entry.key}`);
const hdr = el("div", "layui-card-header", card); hdr.textContent = m.label || entry.key;
if (typeof cfg.enabled === "boolean") {
const cbW = el("div", "header-checkbox", hdr), cb = el("input", "", cbW);
cb.type = "checkbox"; cb.name = `${base}.enabled`; if (store.get(`${base}.enabled`, cfg.enabled)) cb.setAttribute("checked", "");
cb.setAttribute("lay-skin", "switch"); cb.setAttribute("lay-text", "开启|关闭");
cb.setAttribute("lay-filter", "nsx-main-switch");
}
const body = el("div", "layui-card-body layui-row layui-col-space10", card);
const fields = m.fields || {}, hidden = new Set(m.hidden || []);
const cols = m.cols || 1, defaultCol = Math.floor(12 / cols);
Object.keys(cfg).filter(k => k !== "enabled" && !isObj(cfg[k]) && !hidden.has(k)).forEach(k => {
const fm = fields[k] || {};
const f = { key: k, label: fm.label || k, type: inferType(cfg[k], fm), options: fm.options, placeholder: fm.placeholder, valueType: inferVT(cfg[k], fm), col: fm.col, defaultVal: cfg[k], action: fm.action, buttonText: fm.buttonText, desc: fm.desc };
const cur = store.get(`${base}.${k}`, cfg[k]);
const fe = makeField(f, `${base}.${k}`, cur, defaultCol);
if (fe) body.appendChild(fe);
});
return card;
};
const orderArr = ["基本设置", "显示设置", "内容设置", "图床设置", "过滤设置", "其他设置", "实验性"];
const groupOrderMap = {};
orderArr.forEach((g, i) => { groupOrderMap[g.trim()] = i; });
const sortedGroups = Object.entries(groups).sort(([g1], [g2]) => {
const idx1 = groupOrderMap[g1] ?? 999;
const idx2 = groupOrderMap[g2] ?? 999;
return idx1 !== idx2 ? idx1 - idx2 : g1.localeCompare(g2);
});
sortedGroups.forEach(([g, list], i) => {
const fs = el("fieldset", "layui-elem-field layui-field-title", wrapper); fs.id = `group-${i}`;
const lg = el("legend", "", fs); lg.textContent = g;
const fd = el("div", "layui-form", wrapper);
list.forEach(e => { const c = makeCard(e, code); if (c) fd.appendChild(c); });
const mi = el("li", "", menuList); if (i === 0) mi.classList.add("layui-menu-item-checked");
const mb = el("div", "layui-menu-body-title", mi), a = el("a", "", mb); a.href = `#group-${i}`; a.textContent = g;
});
// 底部提示
const endFs = el("fieldset", "layui-elem-field layui-field-title", wrapper, "text-align:center");
const endLg = el("legend", "", endFs, "font-size:0.8em;opacity:0.5");
endLg.textContent = "到底了";
const w = window.layui.device().mobile ? "100%" : "620px";
ctx.ui.layer.open({
type: 1, offset: "r", anim: "slideLeft", area: [w, "100%"], scrollbar: false, shade: 0.1, shadeClose: false,
btn: ["保存设置", "取消"], btnAlign: "r", title: "NodeSeek X 设置", id: "setting-layer-direction-r", content: cont.outerHTML,
success: ly => {
const r = ly?.[0] || ly;
try { window.layui.form?.render(); } catch { }
r?.querySelectorAll?.("label[data-desc]").forEach(lbl => {
lbl.onmouseenter = () => ctx.ui.tips?.(lbl.getAttribute("data-desc"), lbl, { tips: [1, '#333'], time: 0 });
lbl.onmouseleave = () => ctx.ui.layer?.closeAll?.('tips');
});
// 滚动同步:右侧滚动时高亮左侧菜单
const content = r?.querySelector?.("#nsx-config-content");
const menu = r?.querySelector?.("#nsx-config-menu");
if (content && menu) {
const items = menu.querySelectorAll("li");
content.addEventListener("scroll", () => {
const groups = content.querySelectorAll("fieldset[id^='group-']");
let activeIdx = 0;
groups.forEach((g, i) => { if (g.offsetTop - content.scrollTop <= 50) activeIdx = i; });
items.forEach((li, i) => li.classList.toggle("layui-menu-item-checked", i === activeIdx));
}, { passive: true });
}
// 主开关联动
const toggleCard = (card, on) => {
card.querySelectorAll(".layui-card-body input,.layui-card-body select,.layui-card-body textarea").forEach(el => {
el.disabled = !on;
el.closest(".layui-form-item")?.classList.toggle("layui-disabled", !on);
});
window.layui.form?.render(null, card.getAttribute("lay-filter"));
};
// 初始 + 监听
r?.querySelectorAll?.(".header-checkbox input").forEach(cb => !cb.checked && toggleCard(cb.closest(".nsx-config-card"), false));
window.layui.form?.on("switch(nsx-main-switch)", d => toggleCard(d.elem.closest(".nsx-config-card"), d.elem.checked));
window.layui.use("colorpicker", () => {
const cp = window.layui.colorpicker;
r?.querySelectorAll?.("[data-color-path]").forEach(wrap => {
const inp = r.querySelector(`input[name="${wrap.dataset.colorInp}"]`), init = wrap.dataset.colorVal || "", def = wrap.dataset.colorDefault || "";
const setBg = c => { if (inp) inp.style.background = c || ""; };
const render = color => cp.render({ elem: wrap, color, alpha: true, predefine: true, format: "rgb", change: setBg, done(c) { if (inp) inp.value = c || def; if (!c && def) { render(def); setBg(def); } }, cancel: setBg });
render(init);
});
});
},
yes: (idx, ly) => {
const r = ly?.[0] || ly, sc = r?.querySelector ? r : document;
sc.querySelectorAll("input,select,textarea").forEach(el => {
if (!el.name) return;
// radio 只保存选中的那个
if (el.type === "radio" && !el.checked) return;
let v;
const vt = el.dataset.valueType;
if (el.type === "checkbox") v = el.checked;
else if (el.type === "radio") v = vt === "number" ? Number(el.value) : el.value;
else if (el.tagName === "TEXTAREA") v = vt === "array" ? el.value.split("\n").map(s => s.trim()).filter(Boolean) : el.value;
else if (el.type === "number" || vt === "number") { const n = Number(el.value); v = Number.isFinite(n) ? n : 0; }
else v = el.value;
if (v !== undefined) store.set(el.name, v);
});
ctx.ui.layer.msg("设置已保存,刷新生效");
ctx.ui.layer.close(idx);
}
});
};
const menus = [
{ name: "sign_in", cb: switchState, text: "自动签到", states: [{ s1: "❌", s2: "关闭" }, { s1: "🎲", s2: "随机🍗" }, { s1: "📌", s2: "5个🍗" }] },
{ name: "re_sign", cb: reSign, text: "🔂 重试签到", states: [] },
{ name: "loading_post", cb: switchState, text: "下拉加载翻页", states: [{ s1: "❌", s2: "关闭" }, { s1: "✅", s2: "开启" }] },
{ name: "open_post_in_new_tab", cb: (n, s) => { switchState(n, s); ctx.ui.layer.msg("刷新页面生效"); }, text: "新标签页打开帖子", states: [{ s1: "❌", s2: "关闭" }, { s1: "✅", s2: "开启" }] },
{ name: "advanced_settings", cb: advSettings, text: "⚙️ 高级设置", states: [] },
{ name: "feedback", cb: () => GM_openInTab("https://greasyfork.org/zh-CN/scripts/479426/feedback", { active: true, insert: true, setParent: true }), text: "💬 反馈 & 建议", states: [] }
];
regMenus();
}
};
const __vite_glob_0_15 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: menus
}, Symbol.toStringTag, { value: 'Module' }));
// 新标签页打开站内帖子(底层同步)
const openPostInNewTab = {
id: "openPostInNewTab",
order: 35,
cfg: { open_post_in_new_tab: { enabled: false } },
meta: { open_post_in_new_tab: { label: "新标签页打开帖子", group: "内容设置" } },
match: () => true,
init(ctx) {
const val = ctx.store.get("open_post_in_new_tab.enabled", false);
try {
ctx.uw.indexedDB.open("ns-preference-db").onsuccess = e => {
const db = e.target.result;
const s = db.transaction("ns-preference-store", "readwrite").objectStore("ns-preference-store");
s.get("configuration").onsuccess = e2 => {
const c = e2.target.result || {};
if (c.openPostInNewPage !== val) {
c.openPostInNewPage = val;
s.put(c, "configuration");
}
};
};
} catch { }
}
};
const __vite_glob_0_16 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: openPostInNewTab
}, Symbol.toStringTag, { value: 'Module' }));
// 快捷评论
const quickComment = {
id: "quickComment",
order: 120,
cfg: { quick_comment: { enabled: true } },
meta: { quick_comment: { label: "快捷评论", group: "内容设置" } },
match: ctx => ctx.loggedIn && ctx.isPost && ctx.store.get("loading_comment.enabled", true) && ctx.store.get("quick_comment.enabled", true),
init(ctx) {
const editor = $(".md-editor"), parent = $("#back-to-parent"), group = $("#fast-nav-button-group");
if (!editor || !parent || !group) return;
let open = false;
const show = e => {
if (open) return;
e?.preventDefault?.();
editor.style.cssText = `position:fixed;bottom:0;margin:0;width:100%;max-width:${editor.clientWidth || 720}px;z-index:999`;
addClose();
open = true;
};
const btn = parent.cloneNode(true);
btn.id = "back-to-comment";
btn.innerHTML = `<svg class="iconpark-icon" style="width:24px;height:24px"><use href="#comments"></use></svg>`;
btn.onclick = show;
parent.before(btn);
$$(".nsk-post .comment-menu,.comment-container .comments").forEach(el => el.addEventListener("click", e => {
if (["引用", "回复", "编辑"].includes(e.target?.textContent)) show(e);
}, true));
function addClose() {
const tb = $("#editor-body .window_header > :last-child");
if (!tb || $(".nsx-close-editor")) return;
const cb = tb.cloneNode(true);
cb.classList.add("nsx-close-editor");
cb.title = "关闭";
const sp = cb.querySelector("span");
if (sp) {
sp.classList.replace("i-icon-full-screen-one", "i-icon-close");
sp.innerHTML = `<svg width="16" height="16" viewBox="0 0 48 48" fill="none"><path d="M8 8L40 40M8 40L40 8" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
}
cb.onclick = () => { editor.style.cssText = ""; cb.remove(); open = false; };
tb.after(cb);
}
}
};
const __vite_glob_0_17 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: quickComment
}, Symbol.toStringTag, { value: 'Module' }));
// 自动签到
const signIn = {
id: "signIn",
order: 80,
cfg: {
sign_in: {
ns: { enabled: true, method: 1, last_date: "" },
df: { enabled: true, method: 1, last_date: "" }
}
},
meta: {
sign_in: {
label: "自动签到", group: "基本设置",
fields: { method: { type: "RADIO", label: "签到方式", valueType: "number", options: [{ value: 1, text: "随机🍗" }, { value: 2, text: "5个🍗" }] } },
hidden: ["last_date"]
}
},
match: ctx => ctx.site && ctx.loggedIn && ctx.store.get(`sign_in.${ctx.site.code}.enabled`, true),
async init(ctx) {
const code = ctx.site.code;
const method = ctx.store.get(`sign_in.${code}.method`, 0);
const now = (() => {
const off = new Date().getTimezoneOffset() + 480;
const bj = new Date(Date.now() + off * 60000);
return `${bj.getFullYear()}/${bj.getMonth() + 1}/${bj.getDate()}`;
})();
if (ctx.store.get(`sign_in.${code}.last_date`) === now) return;
ctx.store.set(`sign_in.${code}.last_date`, now);
try {
const r = await net.post(`/api/attendance?random=${method === 1}`);
r?.success ? ctx.ui.success?.(`签到成功!+${r.gain}🍗,共${r.current}🍗`) : ctx.ui.info?.(r?.message || "签到失败");
} catch (e) { ctx.ui.info?.(e?.message || "签到错误"); }
}
};
const __vite_glob_0_18 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: signIn
}, Symbol.toStringTag, { value: 'Module' }));
// 签到提示
const CSS = `.nsplus-tip{background:rgba(255,217,0,.8);padding:3px;text-align:center;animation:blink 5s ease infinite}.nsplus-tip p,.nsplus-tip p a{color:#f00}.nsplus-tip p a:hover{color:#0ff}`;
const signinTips = {
id: "signinTips",
order: 82,
cfg: { signin_tips: { enabled: true } },
meta: { signin_tips: { label: "签到提示", group: "基本设置" } },
match(ctx) {
if (!ctx.site || !ctx.loggedIn || !ctx.store.get("signin_tips.enabled", true)) return false;
return ctx.store.get(`sign_in.${ctx.site.code}.enabled`, true) === false;
},
init(ctx) {
addStyle("nsx-signtip", CSS);
const code = ctx.site.code;
const now = (() => { const d = new Date(Date.now() + (new Date().getTimezoneOffset() + 480) * 6e4); return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`; })();
if (now === ctx.store.get(`sign_in.${code}.ignore_date`) || now === ctx.store.get(`sign_in.${code}.last_date`)) return;
const header = $("header");
if (!header) return;
const tip = document.createElement("div");
tip.className = "nsplus-tip";
tip.innerHTML = `<p>今天还没签到!【<a class="nsx-sign" data-r="1">随机🍗</a>】【<a class="nsx-sign" data-r="0">5个🍗</a>】【<a class="nsx-ign">今天不提示</a>】</p>`;
header.appendChild(tip);
$$(".nsx-sign", tip).forEach(a => a.onclick = async e => {
e.preventDefault();
try {
const r = await net.post(`/api/attendance?random=${a.dataset.r === "1"}`);
r?.success ? ctx.ui.success?.(`签到成功!+${r.gain}🍗`) : ctx.ui.info?.(r?.message || "签到失败");
} catch (e) { ctx.ui.warning?.(e?.message || "失败"); }
tip.remove();
ctx.store.set(`sign_in.${code}.last_date`, now);
});
$(".nsx-ign", tip).onclick = e => { e.preventDefault(); tip.remove(); ctx.store.set(`sign_in.${code}.ignore_date`, now); };
}
};
const __vite_glob_0_19 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: signinTips
}, Symbol.toStringTag, { value: 'Module' }));
// 平滑滚动
const smoothScroll = {
id: "smoothScroll",
order: 340,
cfg: { smooth_scroll: { enabled: true } },
meta: { smooth_scroll: { label: "平滑滚动", group: "显示设置" } },
match: ctx => ctx.store.get("smooth_scroll.enabled", true),
init() {
addStyle("nsx-smooth", "html{scroll-behavior:smooth}");
}
};
const __vite_glob_0_20 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: smoothScroll
}, Symbol.toStringTag, { value: 'Module' }));
// 用户卡片扩展 - 跨标签页同步未读消息
class Broadcast {
static ins = new Map();
constructor(name) {
if (Broadcast.ins.has(name)) return Broadcast.ins.get(name);
this.myId = `${Date.now()}-${Math.random()}`;
this.recv = [];
this.KEY = `nsx_tab_${name}`;
try { this.ch = new BroadcastChannel(name); this.ch.onmessage = e => this.recv.forEach(f => f(e.data)); } catch { this.ch = null; }
addEventListener("storage", e => { if (e.key === this.KEY) { e.newValue || localStorage.setItem(this.KEY, this.myId); this._up(); } });
addEventListener("beforeunload", () => { if (this.active) localStorage.removeItem(this.KEY); });
localStorage.setItem(this.KEY, this.myId);
this._up();
Broadcast.ins.set(name, this);
}
_up() { this.active = localStorage.getItem(this.KEY) === this.myId; }
on(fn) { this.recv.push(fn); }
send(data) { if (!this.ch) return; const m = { sender: this.myId, data }; this.ch.postMessage(m); this.recv.forEach(f => f(m)); }
task(fn, ms) { setInterval(async () => { if (!this.active) return; try { const d = await fn(); if (d !== undefined) this.send(d); } catch { } }, ms); }
}
const userCardExt = {
id: "userCardExt",
order: 200,
cfg: { user_card_ext: { enabled: true } },
meta: { user_card_ext: { label: "用户卡片扩展", group: "显示设置" } },
match: ctx => ctx.loggedIn && (ctx.isPost || ctx.isList) && ctx.store.get("user_card_ext.enabled", true),
async init(ctx) {
const bn = new Broadcast("nsx_notify");
const card = $(".user-card .user-stat");
const last = card?.querySelector(".stat-block:first-child > :last-child");
if (!card || !last) return;
const atEl = last.cloneNode(true), msgEl = last.cloneNode(true);
last.after(atEl);
card.querySelector(".stat-block:last-child")?.append(msgEl);
const up = (el, href, icon, text, cnt) => {
const a = el.querySelector("a");
if (!a) return;
a.href = href;
el.querySelector("a svg use")?.setAttribute("href", icon);
const t = el.querySelector("a > :nth-child(2)");
if (t) t.textContent = `${text} `;
const c = el.querySelector("a > :last-child");
if (c) { c.textContent = cnt; c.classList.toggle("notify-count", cnt > 0); }
};
const upAll = c => { up(atEl, "/notification#/atMe", "#at-sign", "我", c.atMe); up(msgEl, "/notification#/message?mode=list", "#envelope-one", "私信", c.message); up(last, "/notification#/reply", "#remind-6nce9p47", "回复", c.reply); };
bn.on(({ data }) => { if (data?.type === "unreadCount" && data.counts) upAll(data.counts); });
bn.send({ type: "unreadCount", counts: ctx.user?.unViewedCount || {}, timestamp: Date.now() });
bn.task(async () => {
const d = await net.get("/api/notification/unread-count");
if (d?.success && d.unreadCount) return { type: "unreadCount", counts: d.unreadCount, timestamp: Date.now() };
throw 0;
}, 5000);
}
};
const __vite_glob_0_21 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: userCardExt
}, Symbol.toStringTag, { value: 'Module' }));
// 已访问链接颜色
const DEFAULT_LIGHT = "#afb9c1";
const DEFAULT_DARK = "#393f4e";
const visitedColor = {
id: "visitedColor",
order: 350,
cfg: { visited_color: { enabled: true, light: DEFAULT_LIGHT, dark: DEFAULT_DARK } },
meta: {
visited_color: {
label: "已访问颜色",
group: "显示设置",
// cols: 2,
fields: {
light: { type: "COLOR", label: "浅色模式" },
dark: { type: "COLOR", label: "深色模式" }
}
}
},
match: ctx => ctx.isList && ctx.store.get("visited_color.enabled", true),
init(ctx) {
const light = ctx.store.get("visited_color.light", DEFAULT_LIGHT);
const dark = ctx.store.get("visited_color.dark", DEFAULT_DARK);
addStyle("nsx-visited-color", `.post-list .post-title a:visited{color:${light}}body.dark-layout .post-list .post-title a:visited{color:${dark}}`);
}
};
const __vite_glob_0_22 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: visitedColor
}, Symbol.toStringTag, { value: 'Module' }));
// ===== SVG 图标 =====
const SVG_SPRITE = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="copy" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M13 12.432v-4.62A2.813 2.813 0 0 1 15.813 5h24.374A2.813 2.813 0 0 1 43 7.813v24.375A2.813 2.813 0 0 1 40.188 35h-4.672M7.813 13h24.374A2.813 2.813 0 0 1 35 15.813v24.374A2.813 2.813 0 0 1 32.188 43H7.813A2.813 2.813 0 0 1 5 40.188V15.813A2.813 2.813 0 0 1 7.813 13Z"/></symbol>
<symbol id="check" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m4 24 5-5 10 10L39 9l5 5-25 25L4 24Z"/></symbol>
<symbol id="history" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path d="M5.818 6.727V14h7.273"/><path d="M4 24c0 11.046 8.954 20 20 20s20-8.954 20-20S35.046 4 24 4c-7.32 0-13.715 3.932-17.192 9.8"/><path d="M24 12v14l9.33 9.33"/></g></symbol>
<symbol id="comments" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="4"><path d="M44 6H4v30h8.5v7l9-7H44V6Z"/><path stroke-linecap="round" d="M14 19.5h20M14 27.5h12"/></g></symbol>
<symbol id="at-sign" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4 4 12.954 4 24s8.954 20 20 20"/><path d="M32 24c0 4.418-3.582 10-8 10s-8-5.582-8-10 3.582-8 8-8 8 3.582 8 8m0 0v10c0 3 3 6 6 6"/></g></symbol>
<symbol id="envelope-one" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path d="M4 39h40V9H4z"/><path d="m4 9 20 15L44 9"/></g></symbol>
<symbol id="remind-6nce9p47" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"><path d="M24 44c1.387 0 2.732-.123 4.023-.357M44 24a20 20 0 0 0-40 0c0 4.59 1.55 8.82 4.157 12.194L4 44l7.806-4.157A19.9 19.9 0 0 0 24 44a20 20 0 0 0 4.023-.357"/><path d="M33.805 40a6 6 0 1 0 5.857-9.805"/></g></symbol>
<symbol id="down" viewBox="0 0 48 48"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m36 18-12 12-12-12"/></symbol>
</svg>`;
// ===== 基础 CSS =====
const BASE_CSS = `.blocked-post{display:none!important}#back-to-comment{display:flex}#fast-nav-button-group .nav-item-btn:nth-last-child(4){bottom:120px}header div.history-dropdown-on{color:var(--link-hover-color);cursor:pointer;padding:0 5px;position:absolute;right:50px}.msc-overlay{background-color:var(--bg-sub-color)}`;
// ===== Observer =====
class Observer {
constructor() { this.listeners = []; this.mo = null; }
watch(sel, fn, opts = {}) {
this.listeners.push({ sel, fn, opts });
if (!this.mo) {
this.mo = new MutationObserver(debounce(() => this._run(), 50));
this.mo.observe(document.body, { childList: true, subtree: true });
}
}
_run() {
this.listeners.forEach(({ sel, fn, opts }) => {
const els = $$(sel);
if (els.length) fn(els, opts);
});
}
}
// ===== 创建 ctx =====
function createCtx(obs) {
const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
return {
env, $, $$, addStyle, store, net,
uw,
get loggedIn() { return !!uw?.__config__?.user; },
get user() { return uw?.__config__?.user; },
get uid() { return uw?.__config__?.user?.member_id; },
site: env.site,
isPost: /^\/post-/.test(location.pathname),
isList: /^\/(categories\/|page|award|search|$)/.test(location.pathname),
watch: obs.watch.bind(obs),
ui: {}
};
}
// ===== 启动 =====
function start() {
// 注入资源
document.body?.insertAdjacentHTML("beforeend", SVG_SPRITE);
addStyle("nsx-base", BASE_CSS);
// layui CSS
addStyle("nsx-layui-css", "https://s.cfn.pp.ua/layui/2.10.3/css/layui.css");
addStyle("nsx-layui-dark", "https://s.cfn.pp.ua/layui/theme-dark/2.10.3/css/layui-theme-dark-selector.css");
// 加载模块
const mods = /* #__PURE__ */ Object.assign({"./features/autoLoading.js": __vite_glob_0_0,"./features/blockMembers.js": __vite_glob_0_1,"./features/blockPosts.js": __vite_glob_0_2,"./features/blockViewLevel.js": __vite_glob_0_3,"./features/callout.js": __vite_glob_0_4,"./features/codeHighlight.js": __vite_glob_0_5,"./features/commentFootprint.js": __vite_glob_0_6,"./features/commentShortcut.js": __vite_glob_0_7,"./features/darkMode.js": __vite_glob_0_8,"./features/history.js": __vite_glob_0_9,"./features/imageSlide.js": __vite_glob_0_10,"./features/imageUpload.js": __vite_glob_0_11,"./features/instantPage.js": __vite_glob_0_12,"./features/levelTag.js": __vite_glob_0_13,"./features/linkPurifier.js": __vite_glob_0_14,"./features/menus.js": __vite_glob_0_15,"./features/openPostInNewTab.js": __vite_glob_0_16,"./features/quickComment.js": __vite_glob_0_17,"./features/signIn.js": __vite_glob_0_18,"./features/signinTips.js": __vite_glob_0_19,"./features/smoothScroll.js": __vite_glob_0_20,"./features/userCardExt.js": __vite_glob_0_21,"./features/visitedColor.js": __vite_glob_0_22});
Object.values(mods).forEach(m => m.default && define(m.default));
// 创建 Observer & ctx
const obs = new Observer();
const ctx = createCtx(obs);
// 初始化 UI (依赖 layui)
const noop = () => { };
const UI_NOOP = { toast: noop, info: noop, success: noop, warning: noop, error: noop, alert: noop, confirm: noop, tips: noop };
const initUI = () => {
if (!window.layui?.layer) return (ctx.ui = UI_NOOP);
const layer = window.layui.layer, uw = ctx.uw;
ctx.ui = {
layer,
toast: (text, style) => { const idx = layer.msg(text, { offset: 't', area: ['100%', 'auto'], anim: 'slideDown' }); layer.style(idx, Object.assign({ opacity: 0.9 }, style)); return idx; },
info: msg => ctx.ui.toast(msg, { "background-color": "#4D82D6" }),
success: msg => ctx.ui.toast(msg, { "background-color": "#57BF57" }),
warning: msg => ctx.ui.toast(msg, { "background-color": "#D6A14D" }),
error: msg => ctx.ui.toast(msg, { "background-color": "#E1715B" }),
alert: (t, c, fn) => uw?.mscAlert ? (c === undefined ? uw.mscAlert(t) : uw.mscAlert(t, c)) : layer.alert(c, { title: t, icon: 0, btn: ["确定"] }, fn),
confirm: (t, c, y, n) => uw?.mscConfirm ? uw.mscConfirm(t, c, y, n) : layer.confirm(c, { title: t, icon: 0, btn: ["确定", "取消"] }, y, n),
tips: (msg, el, opts) => layer.tips(msg, el, opts)
};
};
initUI();
if (!ctx.ui.layer) {
const timer = setInterval(() => { if (window.layui?.layer) { initUI(); clearInterval(timer); } }, 100);
setTimeout(() => clearInterval(timer), 5000);
}
// 启动所有模块
boot(ctx);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start);
} else {
start();
}
})();