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



脚本完整代码:
// ==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">×</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
一会儿闻着味儿就来了
已生效
@Liora #25 这个人出了名的导狗,我都把它黑名单了
6666
插眼
@昶- #2 我刚准备去那边叫你的。
666
@hging #5 谁?
支持 先装为敬
帖子里面会有出现现身说法的