【NL Domain Lookup Plugin】Seamlessly embedded in NL websites, it automatically identifies domain names in content and lets you look them up quickly.

The Tampermonkey script is as follows and can be used directly:

If you have other scripts and don’t want to use so many, you can ask an AI to merge them together.

// ==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();
  }
})();


3 Likes

I learned something new, thanks.

Haha :grinning_face_with_smiling_eyes: