【NL域名查询插件】无感内嵌在NL网站中,可自动识别内容中的域名并可以快速查询。

油猴脚本内容如下,可以直接使用:

如果你有其他脚本,不喜欢太多,可以让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();
  }
})();


3 个赞

学习了,多谢

哈哈:grinning_face_with_smiling_eyes: