油猴脚本内容如下,可以直接使用:
如果你有其他脚本,不喜欢太多,可以让AI把它们合并到一起。
// ==UserScript==
// @name NodeLoc 域名插件(yee.cx)
// @namespace yee.cx
// @version 1.1.0
// @description 在 NodeLoc 论坛帖子正文中识别域名,域名右侧显示放大镜,点击弹出该域名的基础信息(注册状态/注册商/注册时间/到期时间),来自 yee.cx
// @author Evan
// @match https://www.nodeloc.com/*
// @match https://nodeloc.com/*
// @grant GM_xmlhttpRequest
// @connect yee.cx
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
var API_ENRICH = 'https://yee.cx/api/enrich';
var BASE_URL = 'https://yee.cx/';
var CACHE = Object.create(null); // 域名 -> Promise
var SKIP_HOSTS = { 'nodeloc.com': 1 };
var SKIP_TLDS = { local: 1, localhost: 1, test: 1, invalid: 1, example: 1, lan: 1, internal: 1, onion: 1 };
/* ---------------- 工具函数 ---------------- */
function isPlainDomain(d) {
if (!d) return false;
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(d)) return false; // 纯 IP
var parts = d.split('.');
var tld = parts[parts.length - 1];
if (SKIP_TLDS[tld]) return false;
return true;
}
// 常见多级公共后缀表(多级公共后缀下的注册域,如 evan.com.cn 属于注册域而非子域)
var PUBLIC_SUFFIXES = [
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn', 'ac.cn',
'com.ee', 'pri.ee', 'org.ee',
'co.uk', 'org.uk', 'me.uk', 'ac.uk', 'gov.uk',
'com.au', 'net.au', 'org.au',
'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'go.jp',
'com.hk', 'com.tw', 'org.tw', 'com.kr', 'co.kr', 'or.kr',
'com.sg', 'com.my', 'com.vn', 'co.nz', 'com.br', 'com.mx',
'com.tr', 'com.co', 'co.in', 'ac.in', 'com.ph', 'com.ua',
'com.pl', 'com.ru', 'com.ar', 'com.pe', 'co.id', 'com.th'
];
// 匹配最长公共后缀(多级后缀优先,等价于从长到短尝试)
function matchPublicSuffix(d) {
var best = '';
for (var i = 0; i < PUBLIC_SUFFIXES.length; i++) {
var s = PUBLIC_SUFFIXES[i];
if (s.length > best.length && d.slice(-(s.length + 1)) === '.' + s) best = s;
}
return best;
}
// 判断是否为真子域(不支持 yee.cx 查询):
// 域名去掉末尾公共后缀后,剩余标签恰好 1 个即为注册域(允许查询);
// 剩余标签 >1 才判定为真子域(如 demo.cocaapi.com 仍不支持查询)
function isSubDomain(d) {
if (!d) return false;
var ps = matchPublicSuffix(d);
var rest = ps ? d.slice(0, d.length - ps.length - 1) : d.slice(0, d.lastIndexOf('.'));
if (!rest) return false;
return rest.split('.').length > 1;
}
function normalizeDomain(d) {
var v = String(d || '').toLowerCase();
v = v.replace(/^https?:\/\//i, '').replace(/[\/?#].*$/, '').trim();
if (v.indexOf('www.') === 0) v = v.slice(4);
try {
var host = new URL('http://' + v).hostname;
if (host && host.indexOf('.') > 0) v = host;
} catch (e) {}
return v;
}
function esc(s) {
var d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
/* ---------------- 放大镜图标 ---------------- */
var MAG_SVG = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>';
function makeMagnifier(domain) {
var a = document.createElement('a');
a.className = 'nlc-mag';
a.href = 'javascript:void(0)';
a.title = '查询域名信息:' + domain;
a.setAttribute('data-nlc-domain', domain);
a.innerHTML = MAG_SVG;
a.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
showPopup(domain, a);
});
return a;
}
/* ---------------- 小窗 ---------------- */
var activePopup = null;
function closePopup() {
if (activePopup) {
if (activePopup.parentNode) activePopup.parentNode.removeChild(activePopup);
activePopup = null;
}
document.removeEventListener('keydown', onDocKey);
}
function onDocKey(e) {
if (e.key === 'Escape') closePopup();
}
function showPopup(domain, anchor) {
closePopup();
var pop = document.createElement('div');
pop.className = 'nlc-pop';
var head = document.createElement('div');
head.className = 'nlc-pop-head';
var title = document.createElement('span');
title.className = 'nlc-pop-title';
title.textContent = domain;
var closeBtn = document.createElement('button');
closeBtn.className = 'nlc-pop-close';
closeBtn.type = 'button';
closeBtn.textContent = '×';
closeBtn.addEventListener('click', closePopup);
head.appendChild(title);
head.appendChild(closeBtn);
var body = document.createElement('div');
body.className = 'nlc-pop-body';
body.innerHTML = '<div class="nlc-loading"><span class="nlc-spinner"></span>加载中…</div>';
var foot = document.createElement('div');
foot.className = 'nlc-pop-foot';
var moreLabel = document.createTextNode('更多信息 ');
var more = document.createElement('a');
more.className = 'nlc-pop-more';
more.href = BASE_URL + encodeURIComponent(domain);
more.target = '_blank';
more.rel = 'noopener';
more.textContent = BASE_URL + domain;
foot.appendChild(moreLabel);
foot.appendChild(more);
pop.appendChild(head);
pop.appendChild(body);
pop.appendChild(foot);
document.body.appendChild(pop);
// 定位:图标下方,视口溢出则翻转
var r = anchor.getBoundingClientRect();
var pw = pop.offsetWidth;
var ph = pop.offsetHeight;
var left = r.left;
var maxLeft = window.innerWidth - pw - 8;
if (left > maxLeft) left = maxLeft;
if (left < 8) left = 8;
var top = r.bottom + 6;
if (top + ph > window.innerHeight - 8) top = r.top - ph - 6;
if (top < 8) top = 8;
pop.style.left = left + 'px';
pop.style.top = top + 'px';
activePopup = pop;
document.addEventListener('keydown', onDocKey);
if (isSubDomain(domain)) {
body.innerHTML = '<div class="nlc-err">不支持二级域名查询</div>';
} else {
fetchEnrich(domain).then(function (data) {
if (activePopup !== pop) return;
renderBody(body, domain, data);
}).catch(function (err) {
if (activePopup !== pop) return;
body.innerHTML = '<div class="nlc-err">查询失败</div>';
if (err) console.error('[nlc-magnifier]', domain, err);
});
}
setTimeout(function () {
document.addEventListener('click', function once(e) {
if (!activePopup || !activePopup.contains(e.target) && !(e.target.closest && e.target.closest('.nlc-mag'))) {
closePopup();
}
document.removeEventListener('click', once);
});
}, 0);
}
function fetchEnrich(domain) {
if (CACHE[domain]) return CACHE[domain];
CACHE[domain] = new Promise(function (resolve, reject) {
GM_xmlhttpRequest({
method: 'POST',
url: API_ENRICH,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ domain: domain }),
timeout: 12000,
onload: function (res) {
var d;
try { d = JSON.parse(res.responseText); } catch (e) { reject(e); return; }
if (d && d.success && d.data) resolve(d.data);
else reject(new Error((d && d.error) || 'empty response'));
},
onerror: reject,
ontimeout: function () { reject(new Error('timeout')); }
});
});
return CACHE[domain];
}
// 降级:whois 原始数据接口
function fetchWhois(domain) {
var key = 'whois:' + domain;
if (CACHE[key]) return CACHE[key];
CACHE[key] = new Promise(function (resolve, reject) {
GM_xmlhttpRequest({
method: 'POST',
url: API_WHOIS,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ domain: domain }),
timeout: 12000,
onload: function (res) {
var d;
try { d = JSON.parse(res.responseText); } catch (e) { reject(e); return; }
if (d && d.success && d.data && d.data.raw) resolve(d.data.raw);
else reject(new Error((d && d.error) || 'empty whois response'));
},
onerror: reject,
ontimeout: function () { reject(new Error('timeout')); }
});
});
return CACHE[key];
}
// 解析 whois 原始文本,提取注册商 / 注册时间 / 到期时间(日期取前 10 位)
function parseWhoisRaw(raw) {
var text = raw || '';
var out = { registrar: '', registered: '', expires: '' };
var m;
m = text.match(/^registrar:\s*(.+)$/im);
if (m) out.registrar = m[1].trim();
m = text.match(/^(?:creation\s*date|registry\s*creation\s*date):\s*(\S+)/im);
if (m) out.registered = String(m[1]).slice(0, 10);
m = text.match(/^(?:registry\s*expiry\s*date|expiry\s*date):\s*(\S+)/im);
if (m) out.expires = String(m[1]).slice(0, 10);
return out;
}
function renderBody(body, domain, data) {
var p = (data && data.parsed) || {};
var state = '未知';
if (data.type === 0) state = '未注册(可注册)';
else if (data.type === 1) state = '已注册';
function d10(v) { return v ? String(v).slice(0, 10) : '—'; }
var rows = [
['域名状态', state],
['注册商', (p.registrar && p.registrar.name) || '—'],
['注册时间', d10(p.registered)],
['到期时间', d10(p.expires)]
];
var html = '<div class="nlc-row"><span class="nlc-label">域名</span><span class="nlc-value">' + esc(domain) + '</span></div>';
rows.forEach(function (r) {
html += '<div class="nlc-row"><span class="nlc-label">' + esc(r[0]) + '</span><span class="nlc-value">' + esc(r[1]) + '</span></div>';
});
body.innerHTML = html;
}
/* ---------------- 正文扫描 ---------------- */
var DOMAIN_RE = /(^|[^a-z0-9.-])((?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63})(?=$|[^a-z0-9_.-])/gi;
// 链接:外部绝对 URL,在其后插放大镜
function handleAnchor(a) {
if (a.dataset.nlcDone || !a.closest('.cooked')) return;
var href = a.getAttribute('href') || '';
if (!/^https?:\/\//i.test(href)) return;
var host = '';
try { host = new URL(href, location.href).hostname.toLowerCase(); } catch (e) { return; }
if (SKIP_HOSTS[host]) return;
var dom = normalizeDomain(host);
if (!isPlainDomain(dom)) return;
a.dataset.nlcDone = '1';
var mag = makeMagnifier(dom);
a.parentNode.insertBefore(mag, a.nextSibling);
}
// 文本节点:匹配域名,拆分为 [前文][域名span][放大镜][后文]
function handleTextNode(node) {
var text = node.nodeValue;
if (!text || !/\./.test(text)) return;
DOMAIN_RE.lastIndex = 0;
var m;
var hits = [];
while ((m = DOMAIN_RE.exec(text))) {
var dom = normalizeDomain(m[2]);
if (!isPlainDomain(dom) || SKIP_HOSTS[dom]) continue;
var domStart = m.index + m[0].indexOf(m[2]);
hits.push({ dom: dom, start: domStart, len: m[2].length });
}
if (!hits.length) return;
var frag = document.createDocumentFragment();
var pos = 0;
hits.forEach(function (h) {
if (h.start < pos) return; // 防重叠
frag.appendChild(document.createTextNode(text.slice(pos, h.start)));
var span = document.createElement('span');
span.className = 'nlc-dom';
span.setAttribute('data-nlc-domain', h.dom);
span.textContent = text.substr(h.start, h.len);
frag.appendChild(span);
frag.appendChild(makeMagnifier(h.dom));
pos = h.start + h.len;
});
frag.appendChild(document.createTextNode(text.slice(pos)));
node.parentNode.replaceChild(frag, node);
}
function scanCooked(root) {
if (!root || root.nodeType !== 1) return;
// 自身已扫描过则跳过(避免 MutationObserver 反复传入同一容器时重复注入)
if (root.getAttribute && root.getAttribute('data-nlc-scanned') === '1') return;
// 后代中已有已扫描容器,说明这是外层容器,跳过不处理(防整层重复)
if (root.querySelector && root.querySelector('.cooked[data-nlc-scanned]')) return;
root.setAttribute('data-nlc-scanned', '1');
if (root.classList && root.classList.contains('promotion-nested-ad__content')) return; // 广告楼层
// 1. 链接
var anchors = root.querySelectorAll ? root.querySelectorAll('a[href]') : [];
for (var i = 0; i < anchors.length; i++) handleAnchor(anchors[i]);
// 2. 文本节点(跳过 a / pre / code / script / style)
var walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (n) {
var p = n.parentNode;
while (p && p !== root) {
if (p.tagName === 'A' || p.tagName === 'PRE' || p.tagName === 'CODE' ||
p.tagName === 'SCRIPT' || p.tagName === 'STYLE' || p.tagName === 'TEXTAREA' ||
p.classList && p.classList.contains('nlc-mag')) {
return NodeFilter.FILTER_REJECT;
}
p = p.parentNode;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
var nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
for (var j = 0; j < nodes.length; j++) handleTextNode(nodes[j]);
}
/* ---------------- 注入与监听 ---------------- */
function injectStyles() {
var css = [
'.nlc-mag{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin:0 2px;vertical-align:middle;color:#8b949e;cursor:pointer;border-radius:4px;text-decoration:none;opacity:.75;transition:opacity .15s,color .15s,background .15s}',
'.nlc-mag:hover{opacity:1;color:#2f81f7;background:rgba(47,129,247,.12)}',
'.nlc-mag svg{display:block}',
'.nlc-pop{position:fixed;z-index:2147483646;width:290px;max-width:calc(100vw - 24px);background:#161b22;color:#e6edf3;border:1px solid #30363d;border-radius:10px;box-shadow:0 10px 34px rgba(0,0,0,.42);font:13px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;overflow:hidden}',
'.nlc-pop-head{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;border-bottom:1px solid #30363d;background:#0d1117}',
'.nlc-pop-title{font-size:14px;font-weight:600;color:#fff;word-break:break-all;padding-right:8px}',
'.nlc-pop-close{width:24px;height:24px;flex:0 0 auto;border:0;background:transparent;color:#8b949e;font-size:20px;line-height:1;cursor:pointer;border-radius:6px}',
'.nlc-pop-close:hover{color:#fff;background:rgba(255,255,255,.08)}',
'.nlc-pop-body{padding:8px 12px}',
'.nlc-row{display:flex;gap:10px;padding:4px 0;border-bottom:1px dashed rgba(48,54,61,.6)}',
'.nlc-row:last-child{border-bottom:0}',
'.nlc-label{flex:0 0 58px;color:#8b949e;white-space:nowrap}',
'.nlc-value{color:#e6edf3;word-break:break-all;min-width:0}',
'.nlc-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:10px 0;color:#8b949e;text-align:center}',
'.nlc-spinner{display:inline-block;width:15px;height:15px;flex:0 0 auto;border:2px solid rgba(139,148,158,.25);border-top-color:#2f81f7;border-radius:50%;animation:nlc-spin .8s linear infinite}',
'@keyframes nlc-spin{to{transform:rotate(360deg)}}',
'.nlc-err{padding:10px 0;color:#f85149;text-align:center}',
'.nlc-pop-foot{padding:8px 12px 10px;border-top:1px solid #30363d;background:#0d1117;text-align:left;display:flex;align-items:center;justify-content:flex-start;gap:6px;color:#8b949e;font-size:13px;white-space:nowrap;overflow:hidden}',
'.nlc-pop-more{color:#2f81f7;text-decoration:none;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 1 auto;min-width:0}',
'.nlc-pop-more:hover{color:#79b8ff;text-decoration:none}'
].join('');
var style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
function init() {
injectStyles();
var outlet = document.getElementById('main-outlet') || document.body;
var cooked = outlet.querySelectorAll('article .cooked');
for (var i = 0; i < cooked.length; i++) scanCooked(cooked[i]);
var observer = new MutationObserver(function (muts) {
for (var i = 0; i < muts.length; i++) {
var nodes = muts[i].addedNodes;
for (var j = 0; j < nodes.length; j++) {
var n = nodes[j];
if (n.nodeType !== 1) continue;
var list = n.classList && n.classList.contains('cooked')
? [n]
: (n.querySelectorAll ? Array.prototype.slice.call(n.querySelectorAll('.cooked')) : []);
for (var k = 0; k < list.length; k++) scanCooked(list[k]);
}
}
});
observer.observe(outlet, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
