原帖子:https://www.deepflood.com/post-5632-1

出现问题的地方

3840 * 2160 分辨率上,左侧导航栏会呈现重复部分,应该是直接 append 导致的
image

找AI修改了下,测试可去重

// ==UserScript==
// @name         NodeSeek <-> DeepFlood 联合访问
// @namespace    http://tampermonkey.net/
// @license      AGPL-3.0
// @version      2025-09-28
// @description  Visit nodeseek.com and deepflood.com at the same time and push hot posts
// @author       xykt
// @match        https://nodeseek.com/
// @match        https://www.nodeseek.com/
// @match        https://nodeseek.com/page-*
// @match        https://www.nodeseek.com/page-*
// @match        https://deepflood.com/
// @match        https://www.deepflood.com/
// @match        https://deepflood.com/page-*
// @match        https://www.deepflood.com/page-*
// @grant        GM_xmlhttpRequest
// @connect      nodeseek.com
// @connect      www.nodeseek.com
// @connect      deepflood.com
// @connect      www.deepflood.com
// @icon         https://www.nodeseek.com/static/image/favicon/android-chrome-192x192.png
// @run-at       document-start
// @downloadURL https://update.greasyfork.org/scripts/550955/NodeSeek%20%3C-%3E%20DeepFlood%20%E8%81%94%E5%90%88%E8%AE%BF%E9%97%AE.user.js
// @updateURL https://update.greasyfork.org/scripts/550955/NodeSeek%20%3C-%3E%20DeepFlood%20%E8%81%94%E5%90%88%E8%AE%BF%E9%97%AE.meta.js
// ==/UserScript==

(function () {
  'use strict';
  const host = location.hostname.replace(/^www\./, '');
  const A = host;
  const B = A === 'nodeseek.com' ? 'deepflood.com' : 'nodeseek.com';
  const scheme = location.protocol;
  const baseA = scheme + '//' + A;
  const baseB = 'https://' + B;
  const pathMatch = location.pathname.match(/^\/(page-\d+)?\/?$/);
  if (!pathMatch) return;
  const currentPath = pathMatch[1] ? '/' + pathMatch[1] : '/';
  document.documentElement.style.visibility = 'hidden';
  function parseHTML(html) {
    const parser = new DOMParser();
    return parser.parseFromString(html, 'text/html');
  }
  function isRelative(url) {
    if (!url || typeof url !== 'string') return false;
    url = url.trim();
    const lower = url.toLowerCase();
    if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('//') ||
      lower.startsWith('mailto:') || lower.startsWith('tel:') || lower.startsWith('#') ||
      lower.startsWith('data:')) {
      return false;
    }
    return true;
  }
  function absolutizeUrl(url, base) {
    if (!url) return url;
    if (!isRelative(url)) return url;
    if (url.startsWith('/')) return base + url;
    return base + '/' + url;
  }
  function convertRelativePaths(doc, base) {
    const attrList = ['href', 'src', 'action', 'poster', 'data-src', 'data-href'];
    attrList.forEach(attr => {
      doc.querySelectorAll('[' + attr + ']').forEach(el => {
        const val = el.getAttribute(attr);
        if (isRelative(val)) el.setAttribute(attr, absolutizeUrl(val, base));
      });
    });
    doc.querySelectorAll('[srcset]').forEach(el => {
      const ss = el.getAttribute('srcset') || '';
      const parts = ss.split(',').map(p => {
        const seg = p.trim();
        const spaceIdx = seg.indexOf(' ');
        if (spaceIdx === -1) {
          return isRelative(seg) ? absolutizeUrl(seg, base) : seg;
        } else {
          const u = seg.slice(0, spaceIdx);
          const rest = seg.slice(spaceIdx + 1);
          return (isRelative(u) ? absolutizeUrl(u, base) : u) + ' ' + rest;
        }
      });
      el.setAttribute('srcset', parts.join(', '));
    });
    doc.querySelectorAll('[style]').forEach(el => {
      let s = el.getAttribute('style');
      if (!s) return;
      s = s.replace(/url\((['"]?)(?!https?:|\/\/|data:|#)([^'")]+)\1\)/g,
        (m, q, p1) => 'url(' + absolutizeUrl(p1, base) + ')');
      el.setAttribute('style', s);
    });
    doc.querySelectorAll('style').forEach(st => {
      let txt = st.textContent;
      if (!txt) return;
      txt = txt.replace(/url\((['"]?)(?!https?:|\/\/|data:|#)([^'")]+)\1\)/g,
        (m, q, p1) => 'url(' + absolutizeUrl(p1, base) + ')');
      st.textContent = txt;
    });
    return doc;
  }
  function fetchBAndMerge() {
    GM_xmlhttpRequest({
      method: 'GET',
      url: 'https://' + B + currentPath,
      responseType: 'text',
      onload: function (res) {
        if (res.status >= 200 && res.status < 400 && res.responseText) {
          try {
            const docB = parseHTML(res.responseText);
            convertRelativePaths(docB, baseB);
            function doMerge() {
              try {
                const headA = document.querySelector('div#nsk-head.nsk-container, div#nsk-head');
                if (headA) {
                  const strong = headA.querySelector('strong.site-title');
                  let newInner = '';
                  if (A === 'nodeseek.com') {
                    newInner = `
<a href="https://nodeseek.com/"><img src="https://nodeseek.com/static/image/favicon/android-chrome-192x192.png" alt="logo" style="max-height: 36px;vertical-align: middle;"> <span class="title-text" style="vertical-align: middle;">NodeSeek</span><span class="beta-icon">beta</span></a>
<span class="title-text" style="vertical-align: middle;">+</span>
<a href="https://deepflood.com/"><span class="title-text" style="vertical-align: middle;"><span style="color:#0084ff">Deep</span><span style="color:#00a3a3">Flood</span></span><span class="beta-icon">beta</span></a>`;
                  } else {
                    newInner = `
<a href="https://deepflood.com/"><span class="title-text" style="vertical-align: middle;"><span style="color:#0084ff">Deep</span><span style="color:#00a3a3">Flood</span></span><span class="beta-icon">beta</span></a>
<span class="title-text" style="vertical-align: middle;">+</span>
<a href="https://nodeseek.com/"><img src="https://nodeseek.com/static/image/favicon/android-chrome-192x192.png" alt="logo" style="max-height: 36px;vertical-align: middle;"> <span class="title-text" style="vertical-align: middle;">NodeSeek</span><span class="beta-icon">beta</span></a>`;
                  }
                  if (strong) strong.innerHTML = newInner;
                }
                const containerA = document.querySelector('#nsk-left-panel-container');
                const panelsB = Array.from(docB.querySelectorAll('div.nsk-panel.category-list'));
                if (containerA && panelsB.length > 0) {
                  // 当前页面已有的面板
                  const panelsA = Array.from(containerA.querySelectorAll('div.nsk-panel.category-list'));

                  // 生成面板签名:标题(归一化) + 前若干链接的 pathname
                  const panelSig = (panel, baseForUrl) => {
                    const titleEl =
                      panel.querySelector('.panel-title, .nsk-panel-title, h2, h3, .title') || null;
                    const title = (titleEl ? titleEl.textContent : '')
                      .trim()
                      .toLowerCase()
                      .replace(/\s+/g, ' ');

                    const links = Array.from(panel.querySelectorAll('a[href]')).map(a => {
                      let href = a.getAttribute('href') || '';
                      try {
                        // 统一成 pathname,避免域名不同导致的误判
                        href = new URL(href, baseForUrl).pathname || href;
                      } catch (e) { }
                      return href;
                    });

                    // 只取前 5 个链接参与签名,既稳定又避免过长
                    return title + '||' + links.slice(0, 5).join('|');
                  };

                  const existing = new Set(panelsA.map(p => panelSig(p, baseA)));

                  panelsB.forEach(bPanel => {
                    const sig = panelSig(bPanel, baseB);
                    if (!existing.has(sig)) {
                      const cloned = document.importNode(bPanel, true);
                      containerA.appendChild(cloned);
                      existing.add(sig);
                    }
                  });
                }

                (function handlePostList(docA, docB, site) {
                  const listA = Array.from(docA.querySelectorAll('ul.post-list > li.post-list-item:not([class*="topic-carousel"])'));
                  const listB = Array.from(docB.querySelectorAll('ul.post-list > li.post-list-item:not([class*="topic-carousel"])'));
                  const icons = {
                    nodeseek: `<span class="info-item info-site"><img src="https://www.nodeseek.com/static/image/favicon/android-chrome-192x192.png" width="12" height="12"></span> `,
                    deepflood: `<span class="info-item info-site"><img src="https://www.deepflood.com/static/image/favicon/android-chrome-192x192.png" width="12" height="12"></span>`
                  };
                  const siteShort = (typeof site === 'string' && site.indexOf('nodeseek') !== -1) ? 'nodeseek' : 'deepflood';
                  const siteOther = siteShort === 'nodeseek' ? 'deepflood' : 'nodeseek';
                  function markSite(posts, siteFrom) {
                    posts.forEach(li => {
                      const info = li.querySelector(".post-info");
                      if (info) {
                        if (!info.querySelector('.info-item.info-site')) {
                          info.insertAdjacentHTML("afterbegin", icons[siteFrom]);
                        }
                      }
                    });
                  }
                  markSite(listA, siteShort);
                  markSite(listB, siteOther);
                  let allPosts = [...listA, ...listB];
                  function parsePost(li) {
                    let timeTitle = "";
                    const timeEl = li.querySelector(".post-info time[title]");
                    if (timeEl) timeTitle = timeEl.getAttribute("title") || "";
                    let time = 0;
                    if (timeTitle) {
                      const parsed = Date.parse(timeTitle);
                      time = isNaN(parsed) ? (new Date(timeTitle)).getTime() || 0 : parsed;
                    }
                    let views = 0;
                    const viewsSpan = li.querySelector(".post-info .info-views span[title], .post-info .info-views span");
                    if (viewsSpan) {
                      const vt = viewsSpan.getAttribute("title") || viewsSpan.textContent || "";
                      const m = vt.match(/(\d[\d,]*)/);
                      if (m) views = parseInt(m[1].replace(/,/g, ''), 10) || 0;
                    }
                    let comments = 0;
                    const commentsSpan = li.querySelector(".post-info .info-comments-count span[title], .post-info .info-comments-count span");
                    if (commentsSpan) {
                      const ct = commentsSpan.getAttribute("title") || commentsSpan.textContent || "";
                      const m2 = ct.match(/(\d[\d,]*)/);
                      if (m2) comments = parseInt(m2[1].replace(/,/g, ''), 10) || 0;
                    }
                    const sticky = !!li.querySelector('use[href="#pin"], use[href="#pin"]');
                    return {
                      el: li,
                      time: time || 0,
                      weight: (views || 0) + (comments || 0) * 5,
                      sticky: !!sticky
                    };
                  }
                  let postsData = allPosts.map(parsePost);
                  let stickyPosts = postsData.filter(p => p.sticky).sort((a, b) => b.time - a.time);
                  let normalPosts = postsData.filter(p => !p.sticky);
                  normalPosts.sort((a, b) => b.weight - a.weight);
                  let hotPosts = normalPosts.slice(0, 5);
                  hotPosts.forEach(p => {
                    const info = p.el.querySelector(".post-info");
                    if (info) {
                      if (!info.querySelector('.info-item.info-hot')) {
                        info.insertAdjacentHTML("beforeend", `<span class="info-item info-hot"><svg class="iconpark-icon"><use href="#rocket" style="color: red;"></use></svg><a style="color: red;"> 热帖</a></span>`);
                      }
                    }
                  });
                  let otherPosts = normalPosts.slice(5).sort((a, b) => b.time - a.time);
                  let finalPosts = [...stickyPosts, ...hotPosts, ...otherPosts];
                  const postListA = docA.querySelector('ul.post-list:not([class*="topic-carousel"])');
                  if (postListA) {
                    postListA.innerHTML = "";
                    finalPosts.forEach(p => {
                      const nodeToInsert = (p.el.ownerDocument === document) ? p.el.cloneNode(true) : document.importNode(p.el, true);
                      postListA.appendChild(nodeToInsert);
                    });
                  }
                })(document, docB, A);
                const userCard = document.querySelector('div[data-v-244123cf].user-card, div.user-card[data-v-244123cf]');
                if (userCard) {
                  let newUserHtml = '';
                  if (A === 'nodeseek.com') {
                    newUserHtml = `
<a href="/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">Nodeseek发帖</span></a>
<a href="https://deepflood.com/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">DeepFlood发帖</span></a>`;
                  } else {
                    newUserHtml = `
<a href="/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">DeepFlood发帖</span></a>
<a href="https://nodeseek.com/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">Nodeseek发帖</span></a>`;
                  }
                  const nextDiv = userCard.nextElementSibling;
                  if (nextDiv && nextDiv.tagName.toLowerCase() === 'div') {
                    nextDiv.innerHTML = newUserHtml;
                  }
                }
                document.documentElement.style.visibility = '';
              } catch (err) {
                console.error('合并出错', err);
                document.documentElement.style.visibility = '';
              }
            }
            if (document.readyState === 'loading') {
              document.addEventListener('DOMContentLoaded', doMerge, { once: true });
            } else {
              doMerge();
            }
          } catch (e) {
            console.error('解析失败', e);
            document.documentElement.style.visibility = '';
          }
        } else {
          document.documentElement.style.visibility = '';
        }
      },
      onerror: function () {
        document.documentElement.style.visibility = '';
      }
    });
  }
  fetchBAndMerge();
})();