logo NodeSeekbeta

【脚本开源】NodeSeek论坛倒狗监控V1.0.0 不喜欢倒狗?那就使用这个脚本吧!

长话短说:

做了一个倒狗监控,可以自定义输入倒狗的ID、监控关键词。主页检测到倒狗发帖时右下角跳出提示。点击进入本帖自带一键回复功能。
让可爱的倒狗降低倒卖概率,减少MJJ金钱损失。本脚本默认附带上我群公认倒狗ID,请用户自行进行替换,或者补充。
添加监控目标后会在监控目标ID后添加醒目标签。

Greasy Fork安装连接

脚本界面:

image
image
image

脚本完整代码:
// ==UserScript==
// @name         NodeSeek 倒狗雷达 V1.0.0
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  去你妈的倒狗。
// @author       cshaizhihao
// @match        *://*.nodeseek.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ================== 默认配置 ==================
    const defaultSettings = {
        targets: "44627", // 默认测试ID,多个用逗号隔开
        buyKeywords: "收,买,求购,收购",
        buyReply: "+1块钱,楼主优先。",
        sellKeywords: "卖,出,出售,明盘",
        sellReply: "5折排队,楼主好出。",
        enableBackgroundFetch: true
    };

    let config = GM_getValue('ns_daogou_config', defaultSettings);
    let historyRecords = GM_getValue('ns_daogou_history', []);
    let fetchErrorCount = 0;

    const saveConfig = () => GM_setValue('ns_daogou_config', config);
    const saveHistory = () => GM_setValue('ns_daogou_history', historyRecords);

    // ================== 工具函数 ==================
    const debounce = (func, wait) => {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    };

    const waitForElement = (selector, timeout = 10000) => {
        return new Promise((resolve, reject) => {
            if (document.querySelector(selector)) return resolve(document.querySelector(selector));

            const observer = new MutationObserver(() => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });

            setTimeout(() => {
                observer.disconnect();
                reject(new Error(`等待元素 ${selector} 超时`));
            }, timeout);
        });
    };

    // ================== 注入 UI 样式 ==================
    GM_addStyle(`
        #ns-dg-app { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
        #ns-dg-app .btn-float {
            position: fixed; top: 80px; right: 20px; z-index: 9999;
            background: linear-gradient(135deg, #f59e0b, #d97706);
            color: white; padding: 10px 14px; border-radius: 10px;
            font-size: 14px; font-weight: bold; cursor: pointer;
            box-shadow: 0 4px 15px rgba(217, 119, 6, 0.4); border: none; transition: 0.2s;
        }
        #ns-dg-app .btn-float:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(217, 119, 6, 0.6); }
        @keyframes pulse-amber { 0% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); } 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); } }
        #ns-dg-app .has-unread { animation: pulse-amber 2s infinite !important; background: linear-gradient(135deg, #ef4444, #b91c1c); }

        #ns-dg-app .panel {
            position: fixed; top: 20%; left: 50%; transform: translateX(-50%);
            width: 480px; max-height: 85vh; background: rgba(24, 24, 27, 0.95);
            backdrop-filter: blur(10px); color: #e4e4e7; border-radius: 12px;
            box-shadow: 0 20px 40px rgba(0,0,0,0.6); z-index: 10000; display: none;
            flex-direction: column; border: 1px solid #3f3f46;
        }
        #ns-dg-app .header {
            padding: 15px 20px; background: rgba(39, 39, 42, 0.8); border-bottom: 1px solid #3f3f46;
            display: flex; justify-content: space-between; align-items: center;
            border-radius: 12px 12px 0 0; cursor: move;
        }
        #ns-dg-app .header h3 { margin: 0; font-size: 16px; color: #f59e0b; user-select: none; }
        #ns-dg-app .close-btn { cursor: pointer; font-size: 22px; color: #a1a1aa; transition: 0.2s; }
        #ns-dg-app .close-btn:hover { color: #fff; }
        #ns-dg-app .content { padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }

        #ns-dg-app .form-group { display: flex; flex-direction: column; gap: 6px; }
        #ns-dg-app .form-group label { font-size: 12px; color: #a1a1aa; font-weight: 500; }
        #ns-dg-app .form-group input {
            background: #27272a; border: 1px solid #52525b; color: #fff;
            padding: 10px; border-radius: 8px; font-size: 13px; outline: none; transition: 0.2s;
        }
        #ns-dg-app .form-group input:focus { border-color: #f59e0b; background: #3f3f46; }

        #ns-dg-app .actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px; }
        #ns-dg-app .btn { background: #3f3f46; color: white; border: none; padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500;}
        #ns-dg-app .btn:hover { background: #52525b; }
        #ns-dg-app .btn.primary { background: #f59e0b; color: #18181b; }
        #ns-dg-app .btn.primary:hover { background: #fbbf24; }

        #ns-dg-app .history { margin-top: 10px; border-top: 1px dashed #52525b; padding-top: 15px;}
        #ns-dg-app .history-item { background: #27272a; padding: 12px; border-radius: 8px; margin-bottom: 10px; font-size: 13px; border: 1px solid #3f3f46;}
        #ns-dg-app .history-item.unread { border-left: 4px solid #f59e0b; }
        #ns-dg-app .history-item a { color: #f59e0b; text-decoration: none; margin-top: 8px; display: inline-block; font-weight: 600;}

        .daogou-tag {
            background: linear-gradient(135deg, #f59e0b, #d97706) !important;
            color: #ffffff !important;
            font-size: 11px; padding: 2px 6px; border-radius: 6px; margin-left: 6px;
            font-weight: bold; display: inline-flex; vertical-align: middle;
            user-select: none; border: 1px solid #b45309; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
        }

        #ns-dg-app .toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 10001; display: flex; flex-direction: column; gap: 10px; }
        #ns-dg-app .toast {
            background: rgba(24, 24, 27, 0.95); backdrop-filter: blur(5px);
            border: 1px solid #3f3f46; border-left: 5px solid #f59e0b;
            color: #fff; padding: 15px; border-radius: 8px; width: 320px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.5); animation: slideIn 0.3s forwards;
        }
        #ns-dg-app .toast h4 { margin: 0 0 8px 0; font-size: 14px; color: #f59e0b; }
        #ns-dg-app .toast p { margin: 0 0 12px 0; font-size: 12px; color: #d4d4d8; line-height: 1.5; word-break: break-all; }
        #ns-dg-app .toast-action { display: flex; justify-content: space-between; align-items: center;}
        #ns-dg-app .toast-action a { background: #f59e0b; color: #18181b; padding: 6px 12px; border-radius: 6px; text-decoration: none; font-size: 12px; font-weight: bold; cursor: pointer; }
        #ns-dg-app .toast-action span { color: #a1a1aa; font-size: 12px; cursor: pointer; }
        @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
    `);

    // ================== UI 构造与交互 ==================
    const createUI = () => {
        const appContainer = document.createElement('div');
        appContainer.id = 'ns-dg-app';
        document.body.appendChild(appContainer);

        const btn = document.createElement('button');
        btn.className = 'btn-float';
        btn.id = 'dg-trigger-btn';
        btn.innerHTML = '🐶 倒狗监控';
        appContainer.appendChild(btn);

        const panel = document.createElement('div');
        panel.className = 'panel';
        panel.id = 'dg-main-panel';
        panel.innerHTML = `
            <div class="header" id="dg-panel-header">
                <h3>🐶 倒狗雷达 V3 控制中心</h3>
                <span class="close-btn">&times;</span>
            </div>
            <div class="content">
                <div class="form-group">
                    <label>监控用户ID (英文逗号隔开):</label>
                    <input type="text" id="dg-targets" value="${config.targets}">
                </div>
                <div class="form-group">
                    <label>【收/买】关键词 & 自动回复:</label>
                    <input type="text" id="dg-buy-keys" value="${config.buyKeywords}" placeholder="关键词...">
                    <input type="text" id="dg-buy-reply" value="${config.buyReply}" placeholder="回复内容...">
                </div>
                <div class="form-group">
                    <label>【出/卖】关键词 & 自动回复:</label>
                    <input type="text" id="dg-sell-keys" value="${config.sellKeywords}" placeholder="关键词...">
                    <input type="text" id="dg-sell-reply" value="${config.sellReply}" placeholder="回复内容...">
                </div>
                <div class="actions">
                    <button class="btn" id="dg-clear-history">清空历史</button>
                    <button class="btn primary" id="dg-save">保存配置</button>
                </div>
                <div class="history" id="dg-history-list"></div>
            </div>
        `;
        appContainer.appendChild(panel);

        const toasts = document.createElement('div');
        toasts.className = 'toast-container';
        appContainer.appendChild(toasts);

        btn.addEventListener('click', () => {
            panel.style.display = panel.style.display === 'flex' ? 'none' : 'flex';
            if (panel.style.display === 'flex') renderHistory();
        });
        panel.querySelector('.close-btn').addEventListener('click', () => panel.style.display = 'none');

        panel.querySelector('#dg-save').addEventListener('click', () => {
            config.targets = document.getElementById('dg-targets').value;
            config.buyKeywords = document.getElementById('dg-buy-keys').value;
            config.buyReply = document.getElementById('dg-buy-reply').value;
            config.sellKeywords = document.getElementById('dg-sell-keys').value;
            config.sellReply = document.getElementById('dg-sell-reply').value;
            saveConfig();
            panel.style.display = 'none';
        });

        panel.querySelector('#dg-clear-history').addEventListener('click', () => {
            historyRecords = []; saveHistory(); renderHistory(); updateUnreadStatus();
        });

        dragElement(panel, document.getElementById('dg-panel-header'));
    };

    const dragElement = (elmnt, header) => {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        header.onmousedown = (e) => {
            e.preventDefault();
            pos3 = e.clientX; pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        };
        const elementDrag = (e) => {
            e.preventDefault();
            pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;
            pos3 = e.clientX; pos4 = e.clientY;
            elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
            elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
            elmnt.style.transform = "none";
        };
        const closeDragElement = () => {
            document.onmouseup = null; document.onmousemove = null;
        };
    };

    const renderHistory = () => {
        const list = document.getElementById('dg-history-list');
        list.innerHTML = '<div style="font-size:13px; color:#f59e0b; margin-bottom:10px;">🕒 近期雷达记录</div>';
        if (historyRecords.length === 0) return list.innerHTML += '<div style="color:#71717a; font-size: 12px;">太安静了,暂时没有发现目标。</div>';

        [...historyRecords].reverse().slice(0, 30).forEach(record => {
            list.innerHTML += `
                <div class="history-item ${record.read ? '' : 'unread'}">
                    <div style="color:#a1a1aa; font-size:11px;">${new Date(record.time).toLocaleString()} | ID: ${record.authorId}</div>
                    <div style="margin-top:4px;">${record.title}</div>
                    <a href="${record.postUrl}?daogou_action=${record.type}" target="_blank" onclick="window.dgMarkRead('${record.id}')">🚀 去对线 (${record.type === 'buy' ? '收' : '出'})</a>
                </div>
            `;
        });
    };

    window.dgMarkRead = (id) => {
        let record = historyRecords.find(r => r.id === id);
        if (record) { record.read = true; saveHistory(); updateUnreadStatus(); }
    };

    const updateUnreadStatus = () => {
        const btn = document.getElementById('dg-trigger-btn');
        if (btn) historyRecords.some(r => !r.read) ? btn.classList.add('has-unread') : btn.classList.remove('has-unread');
    };

    // ================== 核心检测引擎 ==================

    const showToast = (record) => {
        const toasts = document.querySelector('#ns-dg-app .toast-container');
        const toast = document.createElement('div');
        toast.className = 'toast';
        toast.innerHTML = `
            <h4>🚨 发现目标相关动态</h4>
            <p><strong>[ID:${record.authorId}]</strong><br>${record.title}</p>
            <div class="toast-action">
                <a href="${record.postUrl}?daogou_action=${record.type}" target="_blank" class="go-btn">⚡ 去对线</a>
                <span class="ignore-btn">忽略</span>
            </div>
        `;
        toasts.appendChild(toast);

        toast.querySelector('.go-btn').onclick = () => { window.dgMarkRead(record.id); toast.remove(); };
        toast.querySelector('.ignore-btn').onclick = () => toast.remove();
        setTimeout(() => { if (toast.parentNode) toast.remove(); }, 15000);
    };

    // 接收 cleanTitle 仅用于展示,实际匹配使用 blockText (全文)
    const analyzePost = (cleanTitle, blockText, authorId, postUrl) => {
        if (historyRecords.some(r => r.id === postUrl)) return;

        let type = null;
        const buyKeys = config.buyKeywords.split(',').map(s => s.trim()).filter(Boolean);
        const sellKeys = config.sellKeywords.split(',').map(s => s.trim()).filter(Boolean);

        // 使用全文文本进行暴力正则判定,避免 DOM 空文本陷阱
        if (buyKeys.some(k => blockText.includes(k))) type = 'buy';
        else if (sellKeys.some(k => blockText.includes(k))) type = 'sell';

        if (type) {
            const record = { id: postUrl, postUrl, title: cleanTitle, authorId, type, time: Date.now(), read: false };
            historyRecords.push(record);
            saveHistory(); updateUnreadStatus(); showToast(record);
        }
    };

    // 【重构】双向兼容的 DOM 核心抽取器
    const extractPostsFromDOM = (rootDoc, isBackground = false) => {
        const targets = config.targets.split(',').map(s => s.trim()).filter(Boolean);
        if (!targets.length) return;

        // 1. 从目标用户出发 (哪怕主页结构再乱,TAG 能挂上去,就说明一定能抓到 userLink)
        rootDoc.querySelectorAll('a[href*="/space/"]').forEach(userLink => {
            const hasImg = userLink.querySelector('img');
            const match = userLink.getAttribute('href').match(/\/space\/(\d+)/);
            if (!match) return;
            const userId = match[1];

            if (targets.includes(userId)) {

                // 2. 注入 TAG 逻辑 (如果是后台静默解析则跳过)
                if (!isBackground && !hasImg && !userLink.dataset.dgTagged && userLink.textContent.trim().length > 0) {
                    const tag = document.createElement('span');
                    tag.className = 'daogou-tag'; tag.innerText = '🐶 导狗';
                    userLink.insertAdjacentElement('afterend', tag);
                    userLink.dataset.dgTagged = 'true';
                }

                // 3. 【核心修复】反向溯源:从用户标签往上找卡片容器
                let container = userLink.closest('li, article, .post-list-item, .post-item, .item-container, .row')
                             || userLink.parentElement?.parentElement?.parentElement;

                if (container && (!isBackground ? !container.dataset.dgChecked : true)) {
                    // 提取整张卡片的纯文本!无论标题有没有 A 标签包裹都能兜底
                    const blockText = container.textContent || "";

                    // 寻找包含 URL 跳转的节点
                    let postLink = null;
                    const pLinks = container.querySelectorAll('a[href*="/post"], a[href*="/discussions"]');
                    for (let p of pLinks) {
                        if (p.textContent.trim().length > 1) { postLink = p; break; }
                    }
                    if (!postLink && pLinks.length > 0) postLink = pLinks[0]; // 兜底空连接

                    let fullUrl = postLink ? new URL(postLink.getAttribute('href'), window.location.origin).href.split(/[?#]/)[0] : null;
                    if (!fullUrl) return;

                    // 尽力获取干净的标题用于 Toast 显示
                    let cleanTitle = postLink ? postLink.textContent.trim() : "";
                    if (!cleanTitle || cleanTitle.length < 2) {
                        cleanTitle = blockText.split('\\n').map(s => s.trim()).filter(Boolean)[0] || "检测到目标新动态";
                    }
                    if (cleanTitle.length > 60) cleanTitle = cleanTitle.substring(0, 60) + '...';

                    if (!isBackground) container.dataset.dgChecked = 'true';

                    // 将标题和提取出的全文块都送入分析器
                    analyzePost(cleanTitle, blockText, userId, fullUrl);
                }
            }
        });
    };

    const scanDOM = debounce(() => {
        extractPostsFromDOM(document, false);
    }, 500);

    // ================== 反 CF 静默抓取引擎 ==================
    const backgroundFetch = async () => {
        if (!config.enableBackgroundFetch) return;
        try {
            const res = await fetch('/');
            if (!res.ok) throw new Error(`HTTP ${res.status}`);

            const html = await res.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');
            // 后台解析同样享受新的反向查找机制的加持
            extractPostsFromDOM(doc, true);

            fetchErrorCount = 0;
        } catch (e) {
            fetchErrorCount++;
            console.warn(`[倒狗雷达] 抓取受挫 (CF验证阻截?), 失败次数: ${fetchErrorCount}`, e);
        }

        let baseInterval = Math.floor(Math.random() * (75000 - 35000 + 1)) + 35000;
        let nextInterval = baseInterval * Math.pow(1.5, fetchErrorCount);
        if (nextInterval > 300000) nextInterval = 300000;
        setTimeout(backgroundFetch, nextInterval);
    };

    // ================== SPA 健壮自动回复 ==================
    const autoReplyEngine = async () => {
        const action = new URLSearchParams(window.location.search).get('daogou_action');
        if (!action) return;

        const targetText = action === 'buy' ? config.buyReply : config.sellReply;

        try {
            const editor = await waitForElement('textarea, .ProseMirror, [contenteditable="true"]', 8000);

            window.scrollTo(0, document.body.scrollHeight);
            editor.focus();

            if (editor.tagName.toLowerCase() === 'textarea') {
                const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
                nativeSetter.call(editor, targetText);
                editor.dispatchEvent(new Event('input', { bubbles: true }));
                editor.dispatchEvent(new Event('change', { bubbles: true }));
            } else {
                document.execCommand('insertText', false, targetText);
            }

            window.history.replaceState({}, document.title, window.location.pathname);

            const msg = document.createElement('div');
            msg.innerHTML = `✅ 已就绪:<b>${targetText}</b> <br> <span style="font-size:12px;">(为防误触封号,请手动点击发送)</span>`;
            msg.style.cssText = "position:fixed; top:20px; left:50%; transform:translateX(-50%); background:rgba(34, 197, 94, 0.9); backdrop-filter:blur(5px); color:white; padding:12px 24px; border-radius:8px; z-index:9999; box-shadow:0 10px 25px rgba(0,0,0,0.3); text-align:center;";
            document.body.appendChild(msg);
            setTimeout(() => msg.remove(), 4000);

        } catch (error) {
            console.warn("[倒狗雷达] 未找到评论框:", error);
        }
    };

    // ================== 启动入口 ==================
    const init = () => {
        createUI();
        updateUnreadStatus();

        if (window.location.href.includes('/post') || window.location.href.includes('/discussions/')) {
            autoReplyEngine();
        }

        scanDOM();
        new MutationObserver(() => scanDOM()).observe(document.body, { childList: true, subtree: true });

        setTimeout(backgroundFetch, 10000);
    };

    setTimeout(init, 1000);

})();

推荐论坛自带避免倒狗破坏交易的有效方式https://www.nodeseek.com/setting#block

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有59918位seeker

🎉欢迎新用户🎉