// ==UserScript== // @name 抓老六(买大哥赛道) // @namespace https://local/douyin-stalker // @version 1.0.0 // @author 小德捡银子 // @description 骗子找个班上吧 // @license GPL-3.0-only // @match *://*.douyin.com/* // @match *://*.iesdouyin.com/* // @exclude *://creator.douyin.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function () { "use strict"; const NS = "dylive-stalker"; const log = (...args) => console.log(`[${NS}]`, ...args); const errorLog = (...args) => console.error(`[${NS}]`, ...args); const _win = (() => (typeof unsafeWindow !== "undefined" ? unsafeWindow : window))(); const STORAGE_KEY = "laoliu_group"; const DEFAULTS = { btnX: 16, btnY: 80 }; const GM = { get(key) { try { const v = GM_getValue(`${NS}:${key}`, undefined); return v === undefined ? DEFAULTS[key] : v; } catch (e) { errorLog("GM_getValue 失败:", e); return DEFAULTS[key]; } }, set(key, value) { try { GM_setValue(`${NS}:${key}`, value); } catch (e) { errorLog("GM_setValue 失败:", e); } }, }; function loadLaoliuGroup() { try { return JSON.parse(GM_getValue(STORAGE_KEY, "[]")); } catch (e) { errorLog("加载老六组失败:", e); return []; } } function saveLaoliuGroup(group) { try { GM_setValue(STORAGE_KEY, JSON.stringify(group)); } catch (e) { errorLog("保存老六组失败:", e); } } function addStyle(id, css) { let $style = document.getElementById(id); if (!$style) { $style = document.createElement("style"); $style.id = id; (document.head || document.documentElement).appendChild($style); } $style.textContent = css || ""; } function onReady(cb) { if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", cb, { once: true }); else cb(); } function isLivePage() { try { const { hostname, pathname } = location; if (hostname === "live.douyin.com") return true; if ((hostname === "www.douyin.com" || hostname === "douyin.com") && (pathname.startsWith("/follow/live/") || pathname.startsWith("/root/live/"))) return true; } catch (e) {} return false; } function watchRouteChange(callback) { let lastHref = location.href; const check = () => { if (location.href !== lastHref) { lastHref = location.href; callback(); } }; ["pushState", "replaceState"].forEach((method) => { const origin = history[method]; history[method] = function (...args) { const ret = origin.apply(this, args); setTimeout(check, 0); return ret; }; }); window.addEventListener("popstate", () => setTimeout(check, 0)); setInterval(check, 1000); } function copyText(text) { if (!text) return; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => log("已复制:", text)).catch(() => fallbackCopy(text)); } else fallbackCopy(text); } function fallbackCopy(text) { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); try { document.execCommand("copy"); log("已复制(兜底):", text); } catch (e) { log("复制失败:", e); } document.body.removeChild(textarea); } function extractUserInfo(payload) { const user = payload?.user || payload?.common?.user || payload?.data?.user || payload?.from_user || {}; const nickname = user?.nick_name || user?.nickname || user?.display_id || user?.name || ""; const shortId = user?.short_id || ""; const level = Number(user?.pay_grade?.level) || Number(user?.pay_grade?.grade_level) || Number(user?.level) || Number(user?.user_level) || Number(user?.grade?.level) || Number(user?.grade_level) || 0; return { nickname: String(nickname || ""), shortId: String(shortId || ""), userId: String(user?.user_id || ""), level: isNaN(level) ? 0 : level, raw: user }; } // ---------- 全局数据 ---------- const tempPlayers = new Map(); let laoliuGroup = loadLaoliuGroup(); const processedCache = new Map(); // 去重 let panelElement = null, tempContainer = null, laoliuContainer = null, currentTab = 'temp', panelVisible = false; // ---------- 核心处理 ---------- function processMessage(method, payload) { const info = extractUserInfo(payload); if (!info.nickname || !info.shortId) return; // 去重 const cacheKey = `${method}_${info.shortId}`; const now = Date.now(); if (now - (processedCache.get(cacheKey) || 0) < 1000) { log(`去重忽略: ${cacheKey}`); return; } processedCache.set(cacheKey, now); for (const [key, time] of processedCache) { if (now - time > 10000) processedCache.delete(key); } // 临时记录 if (["WebcastChatMessage","WebcastGiftMessage","WebcastMemberMessage"].includes(method)) { if (!tempPlayers.has(info.shortId)) { tempPlayers.set(info.shortId, { nickname: info.nickname, level: info.level, firstSeen: new Date().toLocaleTimeString() }); } else { const existing = tempPlayers.get(info.shortId); if (existing.nickname !== info.nickname) existing.nickname = info.nickname; if (existing.level !== info.level) existing.level = info.level; } if (panelVisible && currentTab === 'temp' && tempContainer) { const grid = tempContainer.querySelector("#bubble-grid"); if (grid) renderTempList(grid); } } // 老六检查 const member = laoliuGroup.find(m => m.shortId === info.shortId); if (member) { if (member.nickname !== info.nickname) { member.history = member.history || []; member.history.push({ nickname: member.nickname, changedAt: new Date().toISOString() }); member.nickname = info.nickname; saveLaoliuGroup(laoliuGroup); if (panelVisible && currentTab === 'laoliu' && laoliuContainer) { const list = laoliuContainer.querySelector("#laoliu-list"); if (list) renderLaoliuList(list); } } if (method === "WebcastMemberMessage" || method === "WebcastChatMessage") { showAlert(member, method === "WebcastMemberMessage" ? "进入直播间" : "发言了"); } } } // ---------- 提醒弹窗 ---------- let alertContainer = null; const MAX_ALERTS = 10; addStyle("laoliu-alert-anim", `@keyframes slideInLeft { from { opacity:0; transform:translateX(-20px); } to { opacity:1; transform:translateX(0); } }`); function showAlert(member, action) { const historyNames = (member.history && member.history.length > 0) ? member.history.map(h => h.nickname).join("、") : "无"; const note = member.note || "无备注"; const msg = `🚨老六出没🚨 ${member.nickname} ${action} | 曾用名:${historyNames} | 备注:${note}`; if (!alertContainer) { alertContainer = document.createElement("div"); alertContainer.id = "laoliu-alert-container"; alertContainer.style.cssText = `position:fixed;left:10px;bottom:10px;z-index:9999999;display:flex;flex-direction:column-reverse;max-height:60vh;width:350px;pointer-events:none;gap:4px;overflow:hidden;`; document.documentElement.appendChild(alertContainer); } const item = document.createElement("div"); item.style.cssText = `background:rgba(0,0,0,0.85);color:#fff;padding:6px 12px;border-radius:8px;font-size:14px;border-left:4px solid #ff4444;box-shadow:0 2px 8px rgba(0,0,0,0.5);animation:slideInLeft 0.3s ease;pointer-events:auto;word-break:break-word;line-height:1.4;flex-shrink:0;`; item.textContent = msg; alertContainer.appendChild(item); while (alertContainer.children.length > MAX_ALERTS) alertContainer.removeChild(alertContainer.firstChild); setTimeout(() => { if (item.parentNode === alertContainer) alertContainer.removeChild(item); if (alertContainer.children.length === 0) { alertContainer.remove(); alertContainer = null; } }, 15000); } // ---------- Hook ---------- async function tryHookDecoder(timeoutMs = 8000) { const start = Date.now(); return new Promise((resolve) => { const timer = setInterval(() => { const decoder = _win["__MESSAGE_INSTANCE__"]?.decoder; const found = decoder && typeof decoder.decode === "function"; if (found || Date.now() - start > timeoutMs) { clearInterval(timer); if (!found) { log("未找到解码器,仅靠 DOM 监听不可靠,建议刷新重试"); resolve(false); return; } const originDecode = decoder.decode; decoder.decode = async function (...args) { const [, method] = args; const payload = await Reflect.apply(originDecode, this, args); try { processMessage(method, payload); } catch (e) { errorLog("处理消息出错:", e); } return payload; }; log("✅ Hook 成功"); resolve(true); } }, 200); }); } // ---------- UI 渲染 ---------- const PANEL_ID = "dylive-stalker-panel"; const BUTTON_ID = "dylive-stalker-btn"; function injectPanelStyle() { addStyle("dylive-stalker-style", ` #${BUTTON_ID} { position:fixed; right:16px; bottom:80px; width:44px; height:44px; border-radius:50%; background:linear-gradient(135deg,#ff6b6b,#ee5a24); box-shadow:0 4px 12px rgba(0,0,0,0.25); display:flex; align-items:center; justify-content:center; cursor:grab; z-index:999998; color:#fff; font-size:20px; user-select:none; transition:box-shadow 0.2s; font-weight:bold; } #${BUTTON_ID}:hover { box-shadow:0 6px 20px rgba(0,0,0,0.4); } #${BUTTON_ID}.dragging { cursor:grabbing; opacity:0.85; } #${PANEL_ID} { position:fixed; right:16px; bottom:130px; width:480px; max-height:70vh; overflow:hidden; background:#1e1e22; color:#f2f2f2; border-radius:14px; box-shadow:0 10px 30px rgba(0,0,0,0.6); z-index:999999; font-size:13px; display:none; flex-direction:column; padding:10px 12px; box-sizing:border-box; font-family:'Segoe UI',system-ui,sans-serif; } #${PANEL_ID}.show { display:flex; } #${PANEL_ID} .header { display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #333; padding-bottom:6px; margin-bottom:6px; flex-shrink:0; } #${PANEL_ID} .header .close { cursor:pointer; opacity:0.6; font-size:16px; } #${PANEL_ID} .header .close:hover { opacity:1; } #${PANEL_ID} .tabs { display:flex; gap:4px; border-bottom:1px solid #333; margin-bottom:6px; flex-shrink:0; } #${PANEL_ID} .tab-btn { padding:6px 12px; background:transparent; color:#aaa; border:none; border-bottom:2px solid transparent; cursor:pointer; font-size:13px; transition:all 0.2s; } #${PANEL_ID} .tab-btn.active { color:#fff; border-bottom-color:#ff6b6b; font-weight:600; } #${PANEL_ID} .tab-content { flex:1; overflow-y:auto; padding:4px 0; } #${PANEL_ID} .tab-content.hidden { display:none; } #${PANEL_ID} .search-bar { display:flex; gap:6px; margin-bottom:8px; flex-wrap:wrap; flex-shrink:0; } #${PANEL_ID} .search-bar input, #${PANEL_ID} .search-bar select { background:#2a2a30; border:1px solid #3a3a42; border-radius:6px; color:#fff; font-size:12px; padding:4px 8px; } #${PANEL_ID} .search-bar input { flex:1; min-width:100px; } #${PANEL_ID} .search-bar input::placeholder { color:#666; } #${PANEL_ID} .bubble-grid { display:flex; flex-wrap:wrap; gap:6px; padding:4px 0; } #${PANEL_ID} .bubble { background:#2a2a32; border-radius:20px; padding:4px 12px 4px 10px; display:inline-flex; align-items:center; gap:4px; cursor:pointer; font-size:12px; transition:background 0.2s; border:1px solid transparent; position:relative; } #${PANEL_ID} .bubble:hover { background:#3a3a4a; border-color:#ff6b6b; } #${PANEL_ID} .bubble .nick { color:#fbbf24; font-weight:500; } #${PANEL_ID} .bubble .lvl { color:#a78bfa; font-size:10px; background:#3b3b4a; padding:0 6px; border-radius:10px; } #${PANEL_ID} .bubble .sid { color:#6b7280; font-size:9px; margin-left:2px; } #${PANEL_ID} .bubble-menu { display:none; position:absolute; top:calc(100%+4px); left:0; background:#2a2a32; border:1px solid #4a4a52; border-radius:8px; padding:4px 0; min-width:120px; z-index:10; box-shadow:0 4px 12px rgba(0,0,0,0.4); } #${PANEL_ID} .bubble-menu.show { display:block; } #${PANEL_ID} .bubble-menu .menu-item { padding:6px 12px; color:#ddd; cursor:pointer; font-size:12px; transition:background 0.15s; white-space:nowrap; } #${PANEL_ID} .bubble-menu .menu-item:hover { background:#3a3a4a; color:#fff; } #${PANEL_ID} .bubble-menu .menu-item.add { color:#8b8cf0; } #${PANEL_ID} .bubble-menu .menu-item.view { color:#60a5fa; } #${PANEL_ID} .player-list { list-style:none; padding:0; margin:0; } #${PANEL_ID} .player-item { display:flex; justify-content:space-between; align-items:center; padding:4px 6px; border-bottom:1px solid #2a2a2e; font-size:12px; } #${PANEL_ID} .player-item:hover { background:#2a2a32; } #${PANEL_ID} .player-item .info { display:flex; align-items:center; gap:8px; flex:1; overflow:hidden; } #${PANEL_ID} .player-item .info .nick { color:#fbbf24; font-weight:500; cursor:pointer; } #${PANEL_ID} .player-item .info .nick:hover { text-decoration:underline; } #${PANEL_ID} .player-item .info .sid { color:#6b7280; font-size:10px; } #${PANEL_ID} .player-item .info .lvl { background:#3b3b4a; padding:0 6px; border-radius:10px; font-size:10px; color:#a78bfa; } #${PANEL_ID} .player-item .actions { display:flex; gap:4px; flex-shrink:0; align-items:center; } #${PANEL_ID} .player-item .actions button { background:transparent; border:none; color:#8b8cf0; cursor:pointer; font-size:12px; padding:0 4px; } #${PANEL_ID} .player-item .actions button:hover { color:#a78bfa; } #${PANEL_ID} .player-item .actions .del-btn { color:#ff6b6b; } #${PANEL_ID} .player-item .actions .del-btn:hover { color:#ff4444; } #${PANEL_ID} .player-item .note-input { background:#1a1a20; border:1px solid #3a3a42; border-radius:4px; color:#ddd; font-size:11px; padding:2px 4px; width:100px; } #${PANEL_ID} .player-item .note-input:focus { outline:none; border-color:#8b8cf0; } #${PANEL_ID} .empty-msg { color:#666; text-align:center; padding:20px 0; } #${PANEL_ID} .badge { background:#3b3b4a; border-radius:10px; padding:0 6px; font-size:10px; color:#b0b0c0; } #${PANEL_ID} .manual-add-btn { background:#3b3b4a; border:none; color:#8b8cf0; padding:4px 12px; border-radius:12px; cursor:pointer; font-size:12px; transition:background 0.2s; } #${PANEL_ID} .manual-add-btn:hover { background:#4a4a5a; } `); } function buildUI() { injectPanelStyle(); const $btn = document.createElement("div"); $btn.id = BUTTON_ID; $btn.textContent = "🎯"; $btn.title = "抓老六"; document.documentElement.appendChild($btn); const savedX = GM.get("btnX"), savedY = GM.get("btnY"); if (savedX !== undefined && savedY !== undefined) { $btn.style.right = "auto"; $btn.style.bottom = "auto"; $btn.style.left = savedX + "px"; $btn.style.top = savedY + "px"; } let isDragging = false, startX, startY, origLeft, origTop; $btn.addEventListener("mousedown", (e) => { if (e.button !== 0) return; isDragging = true; $btn.classList.add("dragging"); const rect = $btn.getBoundingClientRect(); startX = e.clientX; startY = e.clientY; origLeft = rect.left; origTop = rect.top; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); e.preventDefault(); }); const onMouseMove = (e) => { if (!isDragging) return; const dx = e.clientX - startX, dy = e.clientY - startY; let left = origLeft + dx, top = origTop + dy; const btnWidth = 44, btnHeight = 44; const maxX = window.innerWidth - btnWidth, maxY = window.innerHeight - btnHeight; left = Math.max(0, Math.min(left, maxX)); top = Math.max(0, Math.min(top, maxY)); $btn.style.left = left + "px"; $btn.style.top = top + "px"; $btn.style.right = "auto"; $btn.style.bottom = "auto"; }; const onMouseUp = (e) => { if (isDragging) { isDragging = false; $btn.classList.remove("dragging"); const rect = $btn.getBoundingClientRect(); GM.set("btnX", rect.left); GM.set("btnY", rect.top); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); } }; const $panel = document.createElement("div"); $panel.id = PANEL_ID; document.documentElement.appendChild($panel); panelElement = $panel; $btn.addEventListener("click", (e) => { if (isDragging) return; panelVisible = !panelVisible; $panel.classList.toggle("show", panelVisible); if (panelVisible) { if (!$panel.querySelector(".tabs")) buildPanelContent($panel); refreshAllTabs(); } }); buildPanelContent($panel); } function buildPanelContent($panel) { if ($panel.querySelector(".tabs")) return; $panel.innerHTML = `