// ==UserScript== // @name 抖音买大哥助手 // @namespace https://local/douyin-stalker-merged // @version 8.13.0 // @author 小德捡银子 // @description 查看神秘人身份/匿名真实身份,分组管理(老六组/大哥组/待定组,三组互斥,均支持搜索/导入导出) + 卡片信息增强(含曾用名直显) + 进场/发言/送礼弹窗提醒 + 分组自动欢迎/全局等级欢迎(互斥,共用同一套欢迎模板)+ 礼物自动感谢(同用户冷却) + 自动弹幕(顺序/随机定时发送)+ 弹幕预览悬浮区(默认700×100 气泡式排列,可拖动,右下角可拖拽调整大小并记忆尺寸,点击手动发送,独立开关)+ 弹幕发送队列(统一排队串行发送,避免多来源抢占输入框)+ 面板可拖动 + 弹窗透明度 + 曾用名去重 + 手动发送曾用名弹幕 + 主页预览浮窗(与卡片生命周期绑定)【v8.13.0:在 v8.12.1 修复基础上继续优化——① 弹幕预览悬浮区支持原生拖拽调整大小(CSS resize + flex 自适应重排,尺寸用 ResizeObserver 防抖落盘);② 弹幕模板/全局欢迎/礼物感谢/自动弹幕列表统一收敛成同一个按内容缓存的行解析 helper,减少高频消息路径上的重复字符串解析。均为脚本自身内部逻辑的隔离改动,不涉及资料卡片检测时机,不会重现 v8.12.0 的定位问题】 // @license GPL-3.0-only // @match *://*.douyin.com/* // @match *://*.iesdouyin.com/* // @match https://live.douyin.com/* // @exclude *://creator.douyin.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-start // @tag 抖音 // @tag 买大哥 // @tag 抖音直播 // ==/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 = "laoliu_group"; const STORAGE_KEY_DAGE = "dage_group"; const STORAGE_KEY_PENDING = "pending_group"; const PENDING_NOTES_KEY = `${NS}:pending_notes`; const DEFAULTS = { btnX: 16, btnY: 80 }; const DEFAULT_TEMPLATES = { laoliu: { enter: "🚨慎重:@{nickname} 进入直播间 曾用名:{history} ,备注:{note}", chat: "🚨老六发言:@{nickname} 说:{content} ,备注:{note}", }, dage: { enter: "{groupIcon}{note} 欢迎@{nickname} 进入直播间~\n{groupIcon}尊贵的@{nickname} 大驾光临,欢迎欢迎~\n{groupIcon}@{nickname} 来啦,感谢老板支持~", chat: "{groupIcon}{note}@{nickname} 说:{content}", }, }; const DEFAULT_GIFT_TEMPLATE = "🎁 感谢 {nickname} 赠送的 {giftName}×{giftCount}({diamondTotal}钻)!"; const DEFAULT_GLOBAL_WELCOME_TEMPLATE = "👋 欢迎等级 {level} 的 {nickname} 进入直播间!"; const DEFAULT_AUTO_DANMAKU_MESSAGES = "感谢大家的观看,记得点点关注哦~\n主播马上就来,先别走开呀~"; let alertEnterEnabled = true; let alertChatEnabled = true; let alertClickToSendEnabled = true; let alertOpacity = 0.85; let autoWelcomeLaoliu = false; let autoWelcomeDage = false; let globalWelcomeEnabled = false; let globalWelcomeLevelThreshold = 30; let globalWelcomeTemplate = DEFAULT_GLOBAL_WELCOME_TEMPLATE; let giftThanksEnabled = false; let giftThanksMode = 'all'; let giftThanksThreshold = 100; let giftThanksTemplate = DEFAULT_GIFT_TEMPLATE; let giftThanksCooldown = 30; let autoDanmakuEnabled = false; let autoDanmakuMode = 'sequential'; let autoDanmakuInterval = 60; let autoDanmakuMessagesRaw = DEFAULT_AUTO_DANMAKU_MESSAGES; let danmakuPreviewEnabled = false; const QUEUE_SEND_GAP_MS = 1200; let queueMaxSize = 10; function loadSettings() { alertEnterEnabled = GM_getValue(`${NS}:alert_enter_enabled`, true); alertChatEnabled = GM_getValue(`${NS}:alert_chat_enabled`, true); alertClickToSendEnabled = GM_getValue(`${NS}:alert_click_to_send_enabled`, true); alertOpacity = GM_getValue(`${NS}:alert_opacity`, 0.85); autoWelcomeLaoliu = GM_getValue(`${NS}:auto_welcome_laoliu`, false); autoWelcomeDage = GM_getValue(`${NS}:auto_welcome_dage`, false); globalWelcomeEnabled = GM_getValue(`${NS}:global_welcome_enabled`, false); globalWelcomeLevelThreshold = GM_getValue(`${NS}:global_welcome_level_threshold`, 30); globalWelcomeTemplate = GM_getValue(`${NS}:global_welcome_template`, DEFAULT_GLOBAL_WELCOME_TEMPLATE); giftThanksEnabled = GM_getValue(`${NS}:gift_thanks_enabled`, false); giftThanksMode = GM_getValue(`${NS}:gift_thanks_mode`, 'all'); giftThanksThreshold = GM_getValue(`${NS}:gift_thanks_threshold`, 100); giftThanksTemplate = GM_getValue(`${NS}:gift_thanks_template`, DEFAULT_GIFT_TEMPLATE); giftThanksCooldown = GM_getValue(`${NS}:gift_thanks_cooldown`, 30); autoDanmakuEnabled = GM_getValue(`${NS}:auto_danmaku_enabled`, false); autoDanmakuMode = GM_getValue(`${NS}:auto_danmaku_mode`, 'sequential'); autoDanmakuInterval = GM_getValue(`${NS}:auto_danmaku_interval`, 60); autoDanmakuMessagesRaw = GM_getValue(`${NS}:auto_danmaku_messages`, DEFAULT_AUTO_DANMAKU_MESSAGES); danmakuPreviewEnabled = GM_getValue(`${NS}:danmaku_preview_enabled`, false); loadQueueSettings(); } loadSettings(); function saveSetting(key, value) { GM_setValue(`${NS}:${key}`, value); } const HTML_ESCAPE_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; function escapeHTML(str) { return String(str == null ? "" : str).replace(/[&<>"']/g, (c) => HTML_ESCAPE_MAP[c]); } 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, 2000); } 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 openHomepage(secUid) { if (!secUid) { alert("暂无主页信息,请先点击头像加载完整资料后再试"); return; } window.open(`https://www.douyin.com/user/${secUid}`, "_blank"); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function sendDanmakuRaw(text) { return new Promise((resolve) => { try { const input = document.querySelector('#chatInput [contenteditable="true"]') || document.querySelector('[data-slate-editor="true"]'); if (!input) { log('❌ 发送弹幕失败:未找到输入框'); resolve(false); return; } const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(input); selection.removeAllRanges(); selection.addRange(range); document.execCommand('delete', false, null); document.execCommand('insertText', false, text); input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text })); setTimeout(() => { try { const targetSVG = document.querySelector('svg.webcast-chatroom___send-btn'); if (targetSVG) { const evt = document.createEvent('MouseEvents'); evt.initEvent('click', true, true); targetSVG.dispatchEvent(evt); targetSVG.querySelectorAll('path').forEach(p => p.dispatchEvent(evt)); if (targetSVG.parentElement) targetSVG.parentElement.dispatchEvent(evt); log(`✅ 弹幕发送成功: ${text}`); resolve(true); } else { log('❌ 发送弹幕失败:未找到发送按钮'); resolve(false); } } catch (innerE) { log(`❌ 点击发送按钮异常: ${innerE.message}`); resolve(false); } }, 150); } catch (e) { log(`❌ 发送弹幕异常: ${e.message}`); resolve(false); } }); } function sendDanmaku(text) { sendDanmakuRaw(text); return true; } const DANMAKU_QUEUE = []; let queueProcessing = false; let queueCurrentItem = null; function loadQueueSettings() { queueMaxSize = GM_getValue(`${NS}:queue_max_size`, 10); } function refreshQueueStatusUI() { if (!panelVisible || currentTab !== 'queue') return; const statusText = document.getElementById('queue-status-text'); const previewList = document.getElementById('queue-preview-list'); if (!statusText && !previewList) return; const pending = DANMAKU_QUEUE.length; if (statusText) { let text = queueProcessing ? '🟢 队列运行中' : '⚪ 空闲'; text += ` | 待发送:${pending} 条`; if (queueCurrentItem) text += ` | 正在发送:${escapeHTML(queueCurrentItem.text).slice(0, 30)}`; statusText.textContent = text; } if (previewList) { if (pending === 0) { previewList.innerHTML = `
队列为空
`; } else { previewList.innerHTML = DANMAKU_QUEUE.slice(0, 20).map((item, i) => `
  • ${i + 1} ${escapeHTML(item.label ? '[' + item.label + '] ' : '')}${escapeHTML(item.text)}
  • `).join(''); } } } function enqueueDanmaku(text, options = {}) { if (!text) return; const { priority = 'normal', label = '' } = options; if (DANMAKU_QUEUE.length >= queueMaxSize) { const dropped = DANMAKU_QUEUE.shift(); log(`⚠️ 发送队列已满(${queueMaxSize}),丢弃最早排队的一条: ${dropped ? dropped.text : ''}`); } const item = { text, label, priority, addedAt: Date.now() }; if (priority === 'high') DANMAKU_QUEUE.unshift(item); else DANMAKU_QUEUE.push(item); refreshQueueStatusUI(); processQueue(); } async function processQueue() { if (queueProcessing) return; queueProcessing = true; refreshQueueStatusUI(); while (DANMAKU_QUEUE.length > 0) { if (!isLivePage()) { log(`⚠️ 已离开直播间页面,清空剩余 ${DANMAKU_QUEUE.length} 条待发送弹幕`); DANMAKU_QUEUE.length = 0; break; } const item = DANMAKU_QUEUE.shift(); queueCurrentItem = item; refreshQueueStatusUI(); log(`📤 队列发送${item.label ? '[' + item.label + ']' : ''}: ${item.text}`); await sendDanmakuRaw(item.text); queueCurrentItem = null; refreshQueueStatusUI(); if (DANMAKU_QUEUE.length > 0) await sleep(QUEUE_SEND_GAP_MS); } queueProcessing = false; refreshQueueStatusUI(); } function clearQueue() { const n = DANMAKU_QUEUE.length; DANMAKU_QUEUE.length = 0; refreshQueueStatusUI(); log(`🧹 已清空发送队列,移除 ${n} 条待发送弹幕`); } let _pendingNotesCache = null; function getPendingNotes() { if (_pendingNotesCache) return _pendingNotesCache; try { _pendingNotesCache = GM_getValue(PENDING_NOTES_KEY, {}) || {}; } catch (e) { _pendingNotesCache = {}; } return _pendingNotesCache; } function setPendingNotes(obj) { _pendingNotesCache = obj; try { GM_setValue(PENDING_NOTES_KEY, obj); } catch (e) { errorLog("保存待定备注失败:", e); } } function getPendingNote(shortId) { if (!shortId) return ""; return getPendingNotes()[shortId] || ""; } function setPendingNote(shortId, note) { if (!shortId) return; const notes = getPendingNotes(); if (note) notes[shortId] = note; else delete notes[shortId]; setPendingNotes(notes); } function clearPendingNote(shortId) { setPendingNote(shortId, ""); } function getEffectiveNote(shortId) { if (!shortId) return { note: "", gm: null }; if (laoliuGM.map.has(shortId)) return { note: laoliuGM.map.get(shortId).note || "", gm: laoliuGM }; if (dageGM.map.has(shortId)) return { note: dageGM.map.get(shortId).note || "", gm: dageGM }; if (pendingGM.map.has(shortId)) return { note: pendingGM.map.get(shortId).note || "", gm: pendingGM }; return { note: getPendingNote(shortId), gm: null }; } function getEffectiveHistory(shortId) { if (!shortId) return []; for (const gm of [laoliuGM, dageGM, pendingGM]) { const member = gm.map.get(shortId); if (member) return (member.history || []).map(h => h.nickname); } return []; } function saveCardNote(shortId, text, cardData) { if (!shortId) return; const trimmed = String(text || "").trim(); if (laoliuGM.map.has(shortId)) { laoliuGM.updateNote(shortId, trimmed); refreshGroupUI(laoliuGM); return; } if (dageGM.map.has(shortId)) { dageGM.updateNote(shortId, trimmed); refreshGroupUI(dageGM); return; } if (pendingGM.map.has(shortId)) { pendingGM.updateNote(shortId, trimmed); if (cardData) pendingGM.updateMember(shortId, cardData.nickname, cardData.level, cardData.secUid, cardData.displayId); refreshGroupUI(pendingGM); return; } if (trimmed && cardData && cardData.nickname) { const result = pendingGM.add(shortId, cardData.nickname, cardData.level, trimmed, cardData.secUid, cardData.displayId); if (result.ok) { refreshGroupUI(pendingGM); log(`⏳ 已将 ${cardData.nickname} 归入待定组(有备注但未加入老六组/大哥组)`); return; } } setPendingNote(shortId, trimmed); } function refreshOpenCardsForShortId(shortId) { if (!shortId) return; document.querySelectorAll('.my-card-container').forEach((container) => { if (container.dataset.shortId === shortId) updateCardNoteAndButtons(container, shortId); }); } function extractCoreFromProfile(rawData) { if (!rawData) return null; const inner = rawData.data || rawData; const user = inner.user_data || {}; const shortId = user.short_id || ""; if (!shortId) return null; const nickname = user.nickname || ""; const secUid = user.sec_uid || ""; const displayId = user.display_id || ""; let level = 0; if (user.pay_grade && user.pay_grade.level !== undefined) level = Number(user.pay_grade.level) || 0; else if (user.badge_image_list && user.badge_image_list.length > 0) { const badge = user.badge_image_list[0]; if (badge.content && badge.content.level !== undefined) level = Number(badge.content.level) || 0; } return { shortId: String(shortId), nickname: String(nickname), secUid: String(secUid), displayId: String(displayId), level }; } function createGroupManager(storageKey, meta) { let group = []; try { const raw = GM_getValue(storageKey, "[]"); if (typeof raw === "string") { group = JSON.parse(raw); } else if (Array.isArray(raw)) { group = raw; } else { group = []; } if (!Array.isArray(group)) group = []; } catch (e) { errorLog(`加载${meta.label}失败,本次将保留空列表以避免崩溃(原始数据未被覆盖,可尝试手动检查 GM 存储):`, e); group = []; } group.forEach(m => { if (m && m.shortId !== undefined && m.shortId !== null) m.shortId = String(m.shortId); }); const map = new Map(group.map(m => [m.shortId, m])); const obj = { meta, list: group, map, others: [], save() { try { GM_setValue(storageKey, JSON.stringify(this.list)); } catch (e) { errorLog(`保存${meta.label}失败:`, e); } }, setAll(newList) { for (const og of this.others) { const otherList = og.list; let touched = false; for (const item of newList) { const idx = otherList.findIndex(m => m.shortId === item.shortId); if (idx !== -1) { otherList.splice(idx, 1); og.map.delete(item.shortId); touched = true; } } if (touched) og.save(); } this.list.length = 0; this.list.push(...newList); this.map.clear(); newList.forEach(m => this.map.set(m.shortId, m)); this.save(); }, add(shortId, nickname, level, note, secUid, displayId) { if (!shortId) return { ok: false, reason: "no-id" }; if (this.map.has(shortId)) return { ok: false, reason: "exists" }; let memberFromOther = null; for (const og of this.others) { if (!og.map.has(shortId)) continue; const otherList = og.list; const idx = otherList.findIndex(m => m.shortId === shortId); if (idx !== -1) { memberFromOther = otherList[idx]; otherList.splice(idx, 1); og.map.delete(shortId); og.save(); } break; } let member; if (memberFromOther) { member = memberFromOther; if (nickname) member.nickname = nickname; if (level !== undefined && level !== null && !isNaN(level)) member.level = Number(level); if (note !== undefined && note !== null && note !== "") member.note = note; if (secUid) member.secUid = secUid; if (displayId) member.displayId = displayId; } else { const finalNote = (note !== undefined && note !== null && note !== "") ? note : getPendingNote(shortId); member = { shortId, nickname: nickname || "", level: level || 0, note: finalNote || "", secUid: secUid || "", displayId: displayId || "", history: [], addedAt: new Date().toISOString() }; } this.list.push(member); this.map.set(shortId, member); this.save(); clearPendingNote(shortId); return { ok: true, member }; }, removeAt(index) { const m = this.list[index]; if (!m) return; this.map.delete(m.shortId); this.list.splice(index, 1); this.save(); }, updateMember(shortId, newNickname, newLevel, newSecUid, newDisplayId) { const member = this.map.get(shortId); if (!member) return false; let changed = false; if (newNickname && member.nickname !== newNickname) { member.history = member.history || []; const exists = member.history.some(h => h.nickname === member.nickname); if (!exists) { member.history.push({ nickname: member.nickname, changedAt: new Date().toISOString() }); } member.nickname = newNickname; changed = true; } if (newLevel !== undefined && newLevel !== null && !isNaN(newLevel) && Number(newLevel) !== member.level) { member.level = Number(newLevel); changed = true; } if (newSecUid && member.secUid !== newSecUid) { member.secUid = newSecUid; changed = true; } if (newDisplayId && member.displayId !== newDisplayId) { member.displayId = newDisplayId; changed = true; } if (changed) { this.save(); return true; } return false; }, updateNote(shortId, newNote) { const member = this.map.get(shortId); if (!member) return false; if (member.note === newNote) return false; member.note = newNote; this.save(); return true; } }; return obj; } const laoliuGM = createGroupManager(STORAGE_KEY_LAOLIU, { label: "老六组", tabKey: "laoliu", tabLabel: "🕵️ 老六组", alertLabel: "老六出没", alertIcon: "🚨", accent: "#ff4444" }); const dageGM = createGroupManager(STORAGE_KEY_DAGE, { label: "大哥组", tabKey: "dage", tabLabel: "💰 大哥组", alertLabel: "大哥驾到", alertIcon: "👑", accent: "#ffb020" }); const pendingGM = createGroupManager(STORAGE_KEY_PENDING, { label: "待定组", tabKey: "pending", tabLabel: "⏳ 待定组", alertLabel: "待定用户", alertIcon: "⏳", accent: "#9ca3af" }); laoliuGM.others = [dageGM, pendingGM]; dageGM.others = [laoliuGM, pendingGM]; pendingGM.others = [laoliuGM, dageGM]; const groupManagers = { laoliu: laoliuGM, dage: dageGM, pending: pendingGM }; function updateGroupMemberFromProfile(core) { if (!core || !core.shortId) return; for (const gm of [laoliuGM, dageGM, pendingGM]) { const member = gm.map.get(core.shortId); if (!member) continue; const changed = gm.updateMember(core.shortId, core.nickname, core.level, core.secUid, core.displayId); if (changed) { refreshGroupUI(gm); log(`✅ 已从 profile 更新分组内成员 ${core.nickname} (${core.shortId})`); } } } function customTemplateStorageKey(groupKey, type) { return `custom_danmaku_template_${groupKey}_${type}`; } function getCustomTemplateRaw(groupKey, type) { try { return GM_getValue(`${NS}:${customTemplateStorageKey(groupKey, type)}`, ""); } catch (e) { return ""; } } function setCustomTemplate(groupKey, type, template) { try { GM_setValue(`${NS}:${customTemplateStorageKey(groupKey, type)}`, template); } catch (e) { errorLog(`保存自定义弹幕模板失败 (${groupKey}/${type}):`, e); } } const rawLinesCache = new Map(); function getCachedLines(cacheKey, raw, fallback) { const cached = rawLinesCache.get(cacheKey); if (cached && cached.raw === raw) return cached.lines; const source = (raw && raw.trim()) ? raw : (fallback || ""); const lines = source.split("\n").map(s => s.trim()).filter(Boolean); rawLinesCache.set(cacheKey, { raw, lines }); return lines; } function getTemplateLines(groupKey, type) { const raw = getCustomTemplateRaw(groupKey, type); const fallback = (DEFAULT_TEMPLATES[groupKey] && DEFAULT_TEMPLATES[groupKey][type]) || ""; return getCachedLines(`tpl_${groupKey}_${type}`, raw, fallback); } function pickRandomTemplate(groupKey, type) { const lines = getTemplateLines(groupKey, type); if (lines.length === 0) return ""; return lines[Math.floor(Math.random() * lines.length)]; } function migrateLegacyTemplates() { const legacyMap = { laoliu: "custom_danmaku_template_laoliu", dage: "custom_danmaku_template_dage" }; for (const [groupKey, legacyKey] of Object.entries(legacyMap)) { let legacyVal = ""; try { legacyVal = GM_getValue(`${NS}:${legacyKey}`, ""); } catch (e) { legacyVal = ""; } const newVal = getCustomTemplateRaw(groupKey, "enter"); if (legacyVal && !newVal) { setCustomTemplate(groupKey, "enter", legacyVal); log(`ℹ️ 已将${groupKey === 'laoliu' ? '老六组' : '大哥组'}旧版模板迁移为新版"进场模板"`); } } } const TEMPLATE_PLACEHOLDER_RE = /\{(\w+)\}/g; function applyTemplate(template, data) { return template.replace(TEMPLATE_PLACEHOLDER_RE, (match, key) => { return Object.prototype.hasOwnProperty.call(data, key) ? data[key] : match; }); } function buildDanmakuText(member, gm, action = "", type = "enter", content = "", extraData = {}) { const groupKey = gm.meta.tabKey; const template = pickRandomTemplate(groupKey, type); const historyStr = (member.history && member.history.length > 0) ? member.history.map(h => h.nickname).join("、") : "无"; const noteStr = member.note || "无备注"; const data = { nickname: member.nickname, shortId: member.shortId, level: member.level, group: gm.meta.label, groupIcon: gm.meta.alertIcon, groupLabel: gm.meta.alertLabel, action: action || "", history: historyStr, note: noteStr, content: content || "", ...extraData }; if (!template) { if (type === "chat") return `${gm.meta.alertIcon}${gm.meta.alertLabel}${gm.meta.alertIcon} ${data.nickname} 说:${data.content || "(无内容)"}`; return `${gm.meta.alertIcon}${gm.meta.alertLabel}${gm.meta.alertIcon} ${data.nickname} ${data.action} | 曾用名:${data.history} | 备注:${data.note}`; } return applyTemplate(template, data); } function buildChatAlertHTML(member, gm, content) { const groupKey = gm.meta.tabKey; const template = pickRandomTemplate(groupKey, "chat"); const historyStr = (member.history && member.history.length > 0) ? member.history.map(h => h.nickname).join("、") : "无"; const noteStr = member.note || "无备注"; const data = { nickname: member.nickname, shortId: member.shortId, level: member.level, group: gm.meta.label, groupIcon: gm.meta.alertIcon, groupLabel: gm.meta.alertLabel, action: "发言了", history: historyStr, note: noteStr, content: content || "" }; if (!template) { return `${escapeHTML(data.nickname)} 说:${escapeHTML(data.content || "(无内容)")}`; } const escapedTemplate = escapeHTML(template); return escapedTemplate.replace(TEMPLATE_PLACEHOLDER_RE, (match, key) => { if (!Object.prototype.hasOwnProperty.call(data, key)) return match; const escVal = escapeHTML(data[key]); if (key === "nickname") return `${escVal}`; if (key === "content") return `${escVal}`; return escVal; }); } function getGlobalWelcomeTemplateLines() { const raw = GM_getValue(`${NS}:global_welcome_template`, DEFAULT_GLOBAL_WELCOME_TEMPLATE); return getCachedLines('global_welcome', raw, ''); } function pickGlobalWelcomeTemplate() { const lines = getGlobalWelcomeTemplateLines(); if (lines.length === 0) return ""; return lines[Math.floor(Math.random() * lines.length)]; } function buildAutoWelcomeText(member, extra = {}) { const template = pickGlobalWelcomeTemplate(); const data = { nickname: member.nickname, shortId: member.shortId, level: member.level, note: member.note || "无备注", groupIcon: extra.groupIcon || "", group: extra.group || "", groupLabel: extra.groupLabel || "" }; if (!template) return `👋 欢迎 ${data.nickname}(等级${data.level})进入直播间!`; return applyTemplate(template, data); } function getGiftThanksTemplateLines() { const raw = GM_getValue(`${NS}:gift_thanks_template`, DEFAULT_GIFT_TEMPLATE); return getCachedLines('gift_thanks', raw, ''); } function pickGiftThanksTemplate() { const lines = getGiftThanksTemplateLines(); if (lines.length === 0) return ""; return lines[Math.floor(Math.random() * lines.length)]; } function buildGiftThanksText(user, giftName, giftCount, diamondTotal) { const template = pickGiftThanksTemplate(); const data = { nickname: user.nickname || "未知用户", shortId: user.shortId || "", level: user.level || 0, giftName: giftName || "礼物", giftCount: giftCount || 1, diamondTotal: diamondTotal || 0 }; if (!template) return `🎁 感谢 ${data.nickname} 赠送的 ${data.giftName}×${data.giftCount}(${data.diamondTotal}钻)!`; return applyTemplate(template, data); } let autoDanmakuTimer = null; let autoDanmakuIndex = 0; function getAutoDanmakuMessages() { const raw = GM_getValue(`${NS}:auto_danmaku_messages`, DEFAULT_AUTO_DANMAKU_MESSAGES); return getCachedLines('auto_danmaku', raw, ''); } function stopAutoDanmakuTimer() { if (autoDanmakuTimer) { clearInterval(autoDanmakuTimer); autoDanmakuTimer = null; } } function startAutoDanmakuTimer() { stopAutoDanmakuTimer(); if (!autoDanmakuEnabled) return; const intervalSec = Math.max(5, Number(autoDanmakuInterval) || 60); autoDanmakuTimer = setInterval(() => { if (!isLivePage()) return; const list = getAutoDanmakuMessages(); if (list.length === 0) return; let msg; if (autoDanmakuMode === 'random') { msg = list[Math.floor(Math.random() * list.length)]; } else { if (autoDanmakuIndex >= list.length) autoDanmakuIndex = 0; msg = list[autoDanmakuIndex]; autoDanmakuIndex += 1; } enqueueDanmaku(msg, { label: '自动弹幕' }); }, intervalSec * 1000); } const DANMAKU_PREVIEW_PANEL_ID = "dylive-danmaku-preview-panel"; let danmakuPreviewPanelEl = null; let danmakuPreviewListEl = null; let danmakuPreviewResizeObserver = null; function injectDanmakuPreviewStyle() { addStyle("dy-danmaku-preview-style", ` #${DANMAKU_PREVIEW_PANEL_ID} { position:fixed; top:20px; right:20px; width:700px; height:100px; min-width:320px; min-height:70px; max-width:96vw; max-height:80vh; background:rgba(20,20,24,0.94); border:1px solid rgba(255,255,255,0.12); border-radius:10px; box-shadow:0 8px 24px rgba(0,0,0,0.5); z-index:999997; display:flex; flex-direction:column; overflow:hidden; font-family:'Segoe UI',system-ui,sans-serif; box-sizing:border-box; resize:both; } #${DANMAKU_PREVIEW_PANEL_ID}.dragging { opacity:0.9; user-select:none; } #${DANMAKU_PREVIEW_PANEL_ID} .dp-header { flex:0 0 22px; height:22px; display:flex; align-items:center; justify-content:space-between; padding:0 8px; background:#1c1c22; cursor:move; font-size:11px; color:rgba(255,255,255,0.65); flex-shrink:0; } #${DANMAKU_PREVIEW_PANEL_ID} .dp-header .dp-close { cursor:pointer; opacity:0.6; font-size:13px; padding:0 2px; } #${DANMAKU_PREVIEW_PANEL_ID} .dp-header .dp-close:hover { opacity:1; } #${DANMAKU_PREVIEW_PANEL_ID} .dp-list { flex:1; overflow-y:auto; padding:6px 8px; display:flex; flex-wrap:wrap; align-content:flex-start; gap:6px; } #${DANMAKU_PREVIEW_PANEL_ID} .dp-item { padding:4px 12px; font-size:12px; line-height:1.3; color:#ddd; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.14); border-radius:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:220px; cursor:pointer; transition:background 0.12s, border-color 0.12s, transform 0.08s; flex-shrink:0; } #${DANMAKU_PREVIEW_PANEL_ID} .dp-item:hover { background:rgba(255,255,255,0.18); color:#fff; border-color:rgba(255,255,255,0.32); } #${DANMAKU_PREVIEW_PANEL_ID} .dp-item:active { transform:scale(0.96); background:rgba(255,255,255,0.26); } #${DANMAKU_PREVIEW_PANEL_ID} .dp-empty { width:100%; padding:10px; text-align:center; color:#666; font-size:12px; } `); } function renderDanmakuPreviewList() { if (!danmakuPreviewListEl) return; const list = getAutoDanmakuMessages(); if (list.length === 0) { danmakuPreviewListEl.innerHTML = `
    弹幕列表为空,请到"📢 弹幕"设置页填写
    `; return; } danmakuPreviewListEl.innerHTML = list.map((msg, i) => `
    ${escapeHTML(msg)}
    ` ).join(''); } function createDanmakuPreviewPanel() { if (danmakuPreviewPanelEl) { renderDanmakuPreviewList(); return; } injectDanmakuPreviewStyle(); const el = document.createElement('div'); el.id = DANMAKU_PREVIEW_PANEL_ID; el.innerHTML = `
    📋 弹幕预览(点击发送,右下角可拖拽调整大小)
    `; document.documentElement.appendChild(el); danmakuPreviewPanelEl = el; danmakuPreviewListEl = el.querySelector('.dp-list'); const savedW = GM_getValue(`${NS}:danmakuPreviewW`, null); const savedH = GM_getValue(`${NS}:danmakuPreviewH`, null); if (savedW) el.style.width = `${savedW}px`; if (savedH) el.style.height = `${savedH}px`; danmakuPreviewListEl.addEventListener('click', (e) => { const item = e.target.closest('.dp-item'); if (!item) return; const idx = parseInt(item.dataset.index); const list = getAutoDanmakuMessages(); const msg = list[idx]; if (msg) enqueueDanmaku(msg, { label: '弹幕预览', priority: 'high' }); }); el.querySelector('.dp-close').addEventListener('click', (e) => { e.stopPropagation(); setDanmakuPreviewEnabled(false); syncDanmakuPreviewToggleUI(); }); makeDraggable(el, { handle: el.querySelector('.dp-header'), storageKeyX: `${NS}:danmakuPreviewX`, storageKeyY: `${NS}:danmakuPreviewY`, }); let resizeSaveTimer = null; danmakuPreviewResizeObserver = new ResizeObserver(() => { clearTimeout(resizeSaveTimer); resizeSaveTimer = setTimeout(() => { if (!danmakuPreviewPanelEl) return; const rect = danmakuPreviewPanelEl.getBoundingClientRect(); GM_setValue(`${NS}:danmakuPreviewW`, Math.round(rect.width)); GM_setValue(`${NS}:danmakuPreviewH`, Math.round(rect.height)); }, 300); }); danmakuPreviewResizeObserver.observe(el); renderDanmakuPreviewList(); } function destroyDanmakuPreviewPanel() { if (!danmakuPreviewPanelEl) return; if (danmakuPreviewResizeObserver) { danmakuPreviewResizeObserver.disconnect(); danmakuPreviewResizeObserver = null; } danmakuPreviewPanelEl.remove(); danmakuPreviewPanelEl = null; danmakuPreviewListEl = null; } function refreshDanmakuPreviewPanelIfVisible() { if (danmakuPreviewEnabled && danmakuPreviewPanelEl) renderDanmakuPreviewList(); } function setDanmakuPreviewEnabled(enabled) { danmakuPreviewEnabled = !!enabled; GM_setValue(`${NS}:danmaku_preview_enabled`, danmakuPreviewEnabled); if (danmakuPreviewEnabled) { if (isLivePage()) createDanmakuPreviewPanel(); } else { destroyDanmakuPreviewPanel(); } } function syncDanmakuPreviewToggleUI() { const checkbox = document.getElementById('danmaku-preview-enabled'); if (checkbox) checkbox.checked = danmakuPreviewEnabled; } const processedCache = new Map(); const giftCooldownMap = new Map(); setInterval(() => { const now = Date.now(); for (const [key, time] of processedCache) if (now - time > 10000) processedCache.delete(key); for (const [key, time] of giftCooldownMap) if (now - time > giftThanksCooldown * 1000) giftCooldownMap.delete(key); }, 5000); function extractUserAndContentFromPayload(payload) { const user = payload?.user || payload?.common?.user || payload?.data?.user || payload?.from_user || {}; const shortId = String(user?.short_id || ""); const nickname = String(user?.nickname || ""); const level = (user?.pay_grade && user.pay_grade.level !== undefined) ? (Number(user.pay_grade.level) || 0) : undefined; const content = payload?.content ?? payload?.data?.content ?? payload?.common?.content ?? ""; return { shortId, nickname, level, content: String(content || "") }; } function syncGroupMemberFromHook(shortId, hookNickname, hookLevel) { if (!shortId) return; for (const gm of [laoliuGM, dageGM, pendingGM]) { if (!gm.map.has(shortId)) continue; const changed = gm.updateMember(shortId, hookNickname || undefined, hookLevel); if (changed) refreshGroupUI(gm); } } function checkGroupsForAlert(shortId, alertMethod, content) { if (!shortId) return; const type = alertMethod === "WebcastMemberMessage" ? "enter" : "chat"; const action = type === "enter" ? "进入直播间" : "发言了"; for (const gm of [laoliuGM, dageGM]) { const member = gm.map.get(shortId); if (!member) continue; const autoWelcome = gm.meta.tabKey === 'laoliu' ? autoWelcomeLaoliu : autoWelcomeDage; if (type === "enter" && autoWelcome) return; const show = type === "enter" ? alertEnterEnabled : alertChatEnabled; if (show) showAlert(member, action, gm, type, content); } } function handleGiftMessage(payload) { if (!giftThanksEnabled) return; const user = payload?.user || payload?.common?.user || payload?.data?.user || {}; const shortId = String(user?.short_id || ""); if (!shortId) return; const gift = payload?.gift || payload?.data?.gift || {}; const giftName = gift.name || "礼物"; const diamondSingle = gift.diamond_count || 0; const groupCount = payload?.group_count || payload?.data?.group_count || 1; const repeatCount = payload?.repeat_count || payload?.data?.repeat_count || 1; const totalDiamond = diamondSingle * groupCount * repeatCount; if (totalDiamond < giftThanksThreshold) return; if (giftThanksMode === 'dage' && !dageGM.map.has(shortId)) return; const now = Date.now(); if (giftCooldownMap.has(shortId) && now - giftCooldownMap.get(shortId) < giftThanksCooldown * 1000) { return; } giftCooldownMap.set(shortId, now); syncGroupMemberFromHook(shortId, user.nickname || "", user.pay_grade?.level); const member = { nickname: user.nickname || "未知", shortId, level: user.pay_grade?.level || 0 }; const msg = buildGiftThanksText(member, giftName, groupCount * repeatCount, totalDiamond); enqueueDanmaku(msg, { label: '礼物感谢' }); } function processMessage(method, payload) { if (method === "WebcastGiftMessage") { handleGiftMessage(payload); return; } if (method !== "WebcastMemberMessage" && method !== "WebcastChatMessage") return; const { shortId, nickname: hookNickname, level: hookLevel, content } = extractUserAndContentFromPayload(payload); if (!shortId) return; const cacheKey = `${method}_${shortId}`; const now = Date.now(); if (now - (processedCache.get(cacheKey) || 0) < 1000) return; processedCache.set(cacheKey, now); syncGroupMemberFromHook(shortId, hookNickname, hookLevel); let groupWelcomeTriggered = false; if (method === "WebcastMemberMessage") { if (autoWelcomeLaoliu && laoliuGM.map.has(shortId)) { const member = laoliuGM.map.get(shortId); const msg = buildAutoWelcomeText(member, { groupIcon: laoliuGM.meta.alertIcon, group: laoliuGM.meta.label, groupLabel: laoliuGM.meta.alertLabel }); enqueueDanmaku(msg, { label: '自动欢迎-老六组' }); groupWelcomeTriggered = true; } else if (autoWelcomeDage && dageGM.map.has(shortId)) { const member = dageGM.map.get(shortId); const msg = buildAutoWelcomeText(member, { groupIcon: dageGM.meta.alertIcon, group: dageGM.meta.label, groupLabel: dageGM.meta.alertLabel }); enqueueDanmaku(msg, { label: '自动欢迎-大哥组' }); groupWelcomeTriggered = true; } if (groupWelcomeTriggered) return; } if (method === "WebcastMemberMessage" && globalWelcomeEnabled) { const user = payload?.user || payload?.common?.user || payload?.data?.user || {}; const level = user.pay_grade?.level || 0; if (level >= globalWelcomeLevelThreshold) { const shortId = user.short_id || ""; const member = { nickname: user.nickname || "未知", shortId, level, note: getEffectiveNote(shortId).note || "" }; const msg = buildAutoWelcomeText(member); enqueueDanmaku(msg, { label: '全局欢迎' }); return; } } checkGroupsForAlert(shortId, method, content); } const alertContainers = {}; const MAX_ALERTS = 10; addStyle("laoliu-alert-anim", ` @keyframes slideInLeft { from { opacity:0; transform:translateX(-20px);} to { opacity:1; transform:translateX(0);} } @keyframes slideInRight { from { opacity:0; transform:translateX(20px);} to { opacity:1; transform:translateX(0);} } @keyframes fadeScaleIn { from { opacity:0; transform:scale(0.85);} to { opacity:1; transform:scale(1);} } @keyframes slideDown { from { opacity:0; transform:translateY(-30px);} to { opacity:1; transform:translateY(0);} } .dm-alert-chat-item { border-radius:10px !important; } .dm-alert-chat-item .dm-name { font-weight:700; margin-right:2px; } .dm-alert-chat-item.dm-chat-laoliu { background:rgba(46,10,10,var(--alert-opacity,0.92)) !important; box-shadow:0 2px 12px rgba(255,68,68,0.35) !important; } .dm-alert-chat-item.dm-chat-laoliu .dm-name { color:#ff6b6b; } .dm-alert-chat-item.dm-chat-laoliu .dm-content { color:#ffe3e3; } .dm-alert-chat-item.dm-chat-dage { background:rgba(48,36,4,var(--alert-opacity,0.92)) !important; box-shadow:0 2px 14px rgba(255,176,32,0.4) !important; font-size:15px !important; } .dm-alert-chat-item.dm-chat-dage .dm-name { color:#ffd54f; } .dm-alert-chat-item.dm-chat-dage .dm-content { color:#fff6dc; } .dm-alert-item { background:rgba(0,0,0,var(--alert-opacity,0.85)) !important; } `); function setAlertOpacity(value) { document.documentElement.style.setProperty('--alert-opacity', value); } setAlertOpacity(alertOpacity); function getAlertContainer(side, type) { const key = `${side}_${type}`; if (alertContainers[key]) return alertContainers[key]; const el = document.createElement("div"); el.id = `laoliu-alert-container-${key}`; let css; if (type === "chat") { if (side === 'dage') { css = `position:fixed;left:50%;top:80px;z-index:9999999;display:flex;flex-direction:column;max-height:50vh;width:380px;pointer-events:none;gap:6px;overflow:hidden;transform:translateX(-50%);`; } else { css = `position:fixed;left:12px;bottom:50%;top:auto;z-index:9999999;display:flex;flex-direction:column-reverse;max-height:50vh;width:360px;pointer-events:none;gap:6px;overflow:hidden;transform:none;`; } } else { let leftPos = side === 'dage' ? '50%' : '10px'; let transformVal = side === 'dage' ? 'translateX(-50%)' : 'none'; css = `position:fixed;left:${leftPos};bottom:10px;z-index:9999999;display:flex;flex-direction:column-reverse;max-height:60vh;width:350px;pointer-events:none;gap:4px;overflow:hidden;transform:${transformVal};`; } el.style.cssText = css; document.documentElement.appendChild(el); alertContainers[key] = el; return el; } function showAlert(member, action, gm, type = "enter", content = "") { const side = gm.meta.tabKey; const container = getAlertContainer(side, type); const item = document.createElement("div"); if (type === "chat") { const html = buildChatAlertHTML(member, gm, content); let anim = side === 'dage' ? 'slideDown' : 'slideInLeft'; item.className = `dm-alert-chat-item ${side === 'dage' ? 'dm-chat-dage' : 'dm-chat-laoliu'}`; const cursorStyle = alertClickToSendEnabled ? 'pointer' : 'default'; item.style.cssText = `color:#fff;padding:8px 14px;font-size:14px;box-shadow:0 2px 8px rgba(0,0,0,0.5);animation:${anim} 0.4s ease;pointer-events:auto;word-break:break-word;line-height:1.5;flex-shrink:0;cursor:${cursorStyle};`; item.innerHTML = html; const plainMsg = buildDanmakuText(member, gm, action, type, content); item.addEventListener('click', (e) => { e.stopPropagation(); if (!alertClickToSendEnabled) return; enqueueDanmaku(plainMsg, { label: '点击弹窗', priority: 'high' }); }); container.appendChild(item); while (container.children.length > MAX_ALERTS) container.removeChild(container.firstChild); const duration = side === 'dage' ? 25000 : 15000; setTimeout(() => { if (item.parentNode === container) container.removeChild(item); if (container.children.length === 0) { container.remove(); delete alertContainers[`${side}_${type}`]; } }, duration); return; } const msg = buildDanmakuText(member, gm, action, type, content); const borderSide = side === 'dage' ? 'border-right' : 'border-left'; const anim = side === 'dage' ? 'slideInRight' : 'slideInLeft'; item.className = 'dm-alert-item'; const cursorStyleEnter = alertClickToSendEnabled ? 'pointer' : 'default'; item.style.cssText = `background:rgba(0,0,0,var(--alert-opacity,0.85));color:#fff;padding:6px 12px;border-radius:8px;font-size:14px;${borderSide}:4px solid ${gm.meta.accent};box-shadow:0 2px 8px rgba(0,0,0,0.5);animation:${anim} 0.3s ease;pointer-events:auto;word-break:break-word;line-height:1.4;flex-shrink:0;cursor:${cursorStyleEnter};`; item.textContent = msg; item.addEventListener('click', (e) => { e.stopPropagation(); if (!alertClickToSendEnabled) return; enqueueDanmaku(msg, { label: '点击弹窗', priority: 'high' }); }); container.appendChild(item); while (container.children.length > MAX_ALERTS) container.removeChild(container.firstChild); setTimeout(() => { if (item.parentNode === container) container.removeChild(item); if (container.children.length === 0) { container.remove(); delete alertContainers[`${side}_${type}`]; } }, 15000); } 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("未找到解码器,提醒功能可能不可用,建议刷新重试"); 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); }); } let latestProfileCore = null; function handleProfileResponseData(rawData) { const core = extractCoreFromProfile(rawData); if (!core) return; latestProfileCore = core; updateGroupMemberFromProfile(core); document.querySelectorAll(CARD_CONFIG.POPOVER_SELECTOR).forEach((card) => { if (!card.isConnected) return; const container = card.querySelector('.my-card-container'); if (!container) return; const existing = getCardData(card); if (!existing || existing.shortId === core.shortId) { updateCardDisplay(card, core); } }); } function hookProfileRequests() { const originalFetch = window.fetch; window.fetch = function (...args) { const request = args[0]; const url = typeof request === 'string' ? request : (request.url || ''); if (url.includes('/webcast/user/profile/')) { return originalFetch.apply(this, args).then(async (response) => { const cloned = response.clone(); try { handleProfileResponseData(await cloned.json()); } catch (e) {} return response; }); } return originalFetch.apply(this, args); }; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._url = url; return originalOpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function (...args) { this.addEventListener('load', function () { if (this._url && this._url.includes('/webcast/user/profile/')) { try { handleProfileResponseData(JSON.parse(this.responseText)); } catch (e) {} } }); return originalSend.call(this, ...args); }; } const CARD_CONFIG = { POPOVER_SELECTOR: '.semi-popover', USERNAME_SELECTOR: 'h2, .user_name, [class*="user_name"]', AVATAR_SELECTOR: 'img[src*="avatar"], .semi-avatar img', MYSTERY_TEXT: '神秘人', }; function isUserCard(element) { if (!element || !element.querySelector) return false; const hasUsername = !!element.querySelector(CARD_CONFIG.USERNAME_SELECTOR); const hasAvatar = !!element.querySelector(CARD_CONFIG.AVATAR_SELECTOR); const hasMystery = element.textContent.includes(CARD_CONFIG.MYSTERY_TEXT); return (hasUsername && hasAvatar) || hasMystery; } const cardDataMap = new WeakMap(); function setCardData(card, data) { if (card) cardDataMap.set(card, data); } function getCardData(card) { return card ? (cardDataMap.get(card) || null) : null; } function getCardFromContainer(container) { return container ? container.closest(CARD_CONFIG.POPOVER_SELECTOR) : null; } const HOMEPAGE_PANEL_WIDTH = 650; const HOMEPAGE_PANEL_HEIGHT = 480; const HOMEPAGE_PANEL_GAP = 10; const HOMEPAGE_PANEL_FALLBACK_MS = 6000; const homepagePanelMap = new Map(); function injectHomepagePanelStyle() { addStyle("dy-homepage-panel-style", ` .my-homepage-panel { position:fixed; width:${HOMEPAGE_PANEL_WIDTH}px; height:${HOMEPAGE_PANEL_HEIGHT}px; max-height:calc(100vh - 16px); background:#1f202b; border:1px solid rgba(255,255,255,.12); border-radius:12px; box-shadow:0 18px 56px rgba(0,0,0,.38); overflow:hidden; z-index:2147483000; display:flex; flex-direction:column; color:#fff; } .my-homepage-panel-header { flex:0 0 32px; height:32px; display:flex; align-items:center; padding:0 10px; background:#171821; border-bottom:1px solid rgba(255,255,255,.08); font-size:12px; color:rgba(255,255,255,.85); } .my-homepage-panel-body { position:relative; flex:1 1 auto; min-height:0; background:#1f202b; } .my-homepage-panel-frame { display:block; width:100%; height:100%; border:0; background:#1f202b; } .my-homepage-panel-fallback { position:absolute; inset:0; display:none; align-items:center; justify-content:center; padding:16px; text-align:center; font-size:12px; color:rgba(255,255,255,.72); line-height:1.7; background:#1f202b; } .my-homepage-panel-fallback.is-visible { display:flex; } .my-homepage-panel-fallback a { color:#7aa7ff; text-decoration:none; } `); } function computeHomepagePanelPosition(card) { const rect = card.getBoundingClientRect(); const w = HOMEPAGE_PANEL_WIDTH; const h = Math.min(HOMEPAGE_PANEL_HEIGHT, window.innerHeight - 16); let left = rect.right + HOMEPAGE_PANEL_GAP; if (left + w > window.innerWidth - 8) left = rect.left - HOMEPAGE_PANEL_GAP - w; if (left < 8) left = Math.max(8, Math.min(window.innerWidth - w - 8, rect.left)); let top = rect.top; if (top + h > window.innerHeight - 8) top = window.innerHeight - h - 8; if (top < 8) top = 8; return { left, top, width: w, height: h }; } function repositionHomepagePanel(card, el) { if (!card || !el || !card.isConnected) return; const pos = computeHomepagePanelPosition(card); el.style.left = `${pos.left}px`; el.style.top = `${pos.top}px`; el.style.width = `${pos.width}px`; el.style.height = `${pos.height}px`; } function scheduleHomepagePanelFallback(entry) { if (!entry) return; entry.fallback.classList.remove('is-visible'); clearTimeout(entry.fallbackTimer); entry.fallbackTimer = setTimeout(() => { entry.fallback.classList.add('is-visible'); }, HOMEPAGE_PANEL_FALLBACK_MS); } function showHomepagePanelForCard(card, secUid) { if (!card) return; if (!secUid) { removeHomepagePanelForCard(card); return; } const url = `https://www.douyin.com/user/${secUid}`; const existing = homepagePanelMap.get(card); if (existing) { if (existing.secUid !== secUid) { existing.secUid = secUid; existing.iframe.src = url; existing.fallbackLink.href = url; scheduleHomepagePanelFallback(existing); } repositionHomepagePanel(card, existing.el); return; } injectHomepagePanelStyle(); const el = document.createElement('div'); el.className = 'my-homepage-panel'; el.innerHTML = `
    🏠 主页预览
    如果这里长时间空白,可能是抖音限制了页面内嵌。
    在新标签页打开主页
    `; document.documentElement.appendChild(el); const iframeEl = el.querySelector('.my-homepage-panel-frame'); const fallbackEl = el.querySelector('.my-homepage-panel-fallback'); const fallbackLinkEl = el.querySelector('.my-homepage-panel-fallback-link'); fallbackLinkEl.href = url; iframeEl.src = url; const entry = { el, iframe: iframeEl, fallback: fallbackEl, fallbackLink: fallbackLinkEl, secUid, fallbackTimer: null }; iframeEl.addEventListener('load', () => { clearTimeout(entry.fallbackTimer); fallbackEl.classList.remove('is-visible'); }); homepagePanelMap.set(card, entry); scheduleHomepagePanelFallback(entry); repositionHomepagePanel(card, el); } function removeHomepagePanelForCard(card) { const entry = homepagePanelMap.get(card); if (!entry) return; clearTimeout(entry.fallbackTimer); entry.el.remove(); homepagePanelMap.delete(card); } window.addEventListener('resize', () => { if (homepagePanelMap.size === 0) return; homepagePanelMap.forEach((entry, card) => { if (card.isConnected) repositionHomepagePanel(card, entry.el); else removeHomepagePanelForCard(card); }); }); function updateGroupButtons(container, shortId) { const btnLaoliu = container.querySelector('.my-join-laoliu-btn'); const btnDage = container.querySelector('.my-join-dage-btn'); if (!btnLaoliu || !btnDage) return; const inLaoliu = laoliuGM.map.has(shortId); const inDage = dageGM.map.has(shortId); if (inLaoliu) { btnLaoliu.textContent = '✅ 已在老六组'; btnLaoliu.disabled = true; btnDage.textContent = '🔀 移至大哥组'; btnDage.disabled = false; } else if (inDage) { btnDage.textContent = '✅ 已在大哥组'; btnDage.disabled = true; btnLaoliu.textContent = '🔀 移至老六组'; btnLaoliu.disabled = false; } else { btnLaoliu.textContent = '🕵️ 加入老六组'; btnLaoliu.disabled = false; btnDage.textContent = '👑 加入大哥组'; btnDage.disabled = false; } } function updateCardNoteAndButtons(container, shortId) { exitNoteEditModeIfAny(container); const noteDisplay = container.querySelector('.my-note-display'); if (noteDisplay) { const note = getEffectiveNote(shortId).note; noteDisplay.dataset.raw = note || ''; noteDisplay.textContent = note || '无备注'; noteDisplay.classList.toggle('my-note-empty', !note); } const historyDisplay = container.querySelector('.my-field-history'); if (historyDisplay) { const history = getEffectiveHistory(shortId); historyDisplay.textContent = history.length > 0 ? history.join('、') : '无'; historyDisplay.title = history.length > 0 ? history.join('、') : ''; historyDisplay.classList.toggle('my-note-empty', history.length === 0); } updateGroupButtons(container, shortId); } function updateCardDisplay(card, core) { if (!card || !core) return; const container = card.querySelector('.my-card-container'); if (!container) return; setCardData(card, core); container.dataset.shortId = core.shortId || ''; container.querySelector('.my-field-nickname').textContent = core.nickname || '-'; container.querySelector('.my-field-displayid').textContent = core.displayId || '-'; container.querySelector('.my-field-level').textContent = String(core.level ?? '-'); const homeBtn = container.querySelector('.my-card-home-btn'); if (homeBtn) { if (core.secUid) { homeBtn.style.display = 'flex'; homeBtn.onclick = () => openHomepage(core.secUid); } else homeBtn.style.display = 'none'; } if (core.shortId) updateCardNoteAndButtons(container, core.shortId); if (core.secUid) showHomepagePanelForCard(card, core.secUid); else removeHomepagePanelForCard(card); } function autoResizeTextarea(textarea) { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; } function enterNoteEditMode(container) { const field = container.querySelector('.my-field-note'); if (!field) return; const noteDisplay = field.querySelector('.my-note-display'); if (!noteDisplay) return; const currentText = noteDisplay.dataset.raw || ''; const editWrap = document.createElement('div'); editWrap.className = 'my-note-edit-wrap'; editWrap.dataset.original = currentText; editWrap.innerHTML = ` `; const label = field.querySelector('.my-label'); field.innerHTML = ''; if (label) field.appendChild(label); field.appendChild(editWrap); const textarea = editWrap.querySelector('.my-note-input'); const saveBtn = editWrap.querySelector('.my-note-save-btn'); const cancelBtn = editWrap.querySelector('.my-note-cancel-btn'); autoResizeTextarea(textarea); textarea.addEventListener('input', () => autoResizeTextarea(textarea)); requestAnimationFrame(() => { textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); autoResizeTextarea(textarea); }); textarea.addEventListener('keydown', (e) => { e.stopPropagation(); if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); saveNoteEdit(container, textarea.value); } else if (e.key === 'Escape') { e.preventDefault(); exitNoteEditMode(container, currentText); } }); saveBtn.addEventListener('click', (e) => { e.stopPropagation(); saveNoteEdit(container, textarea.value); }); cancelBtn.addEventListener('click', (e) => { e.stopPropagation(); exitNoteEditMode(container, currentText); }); } function exitNoteEditMode(container, text) { const field = container.querySelector('.my-field-note'); if (!field) return; const editWrap = field.querySelector('.my-note-edit-wrap'); if (!editWrap) return; const label = field.querySelector('.my-label'); field.innerHTML = ''; if (label) field.appendChild(label); const displaySpan = document.createElement('span'); displaySpan.className = 'my-note-display my-value'; displaySpan.title = '点击修改备注'; displaySpan.dataset.raw = text || ''; displaySpan.textContent = text || '无备注'; if (!text) displaySpan.classList.add('my-note-empty'); field.appendChild(displaySpan); } function exitNoteEditModeIfAny(container) { const field = container.querySelector('.my-field-note'); if (!field) return; const editWrap = field.querySelector('.my-note-edit-wrap'); if (!editWrap) return; exitNoteEditMode(container, editWrap.dataset.original || ''); } function saveNoteEdit(container, newNote) { const shortId = container.dataset.shortId; if (!shortId) { alert('无法获取用户标识,请刷新重试'); return; } const trimmed = String(newNote || '').trim(); const card = getCardFromContainer(container); const cardData = getCardData(card); saveCardNote(shortId, trimmed, cardData); exitNoteEditMode(container, trimmed); refreshOpenCardsForShortId(shortId); log(`备注已保存 (${shortId}): ${trimmed}`); } function handleJoinClick(gm, container) { const shortId = container.dataset.shortId; const card = getCardFromContainer(container); const data = getCardData(card); if (!shortId || !data) { alert('尚未获取到用户数据,请稍等'); return; } const note = getEffectiveNote(shortId).note; const result = gm.add(shortId, data.nickname, data.level, note, data.secUid, data.displayId); if (result.ok) { refreshGroupUI(gm); gm.others.forEach(og => refreshGroupUI(og)); updateCardNoteAndButtons(container, shortId); log(`✅ 已将 ${data.nickname} 加入${gm.meta.label}`); } else { updateGroupButtons(container, shortId); } } function injectCardButtons(card) { if (card.querySelector('.my-card-container')) return; const container = document.createElement('div'); container.className = 'my-card-container'; container.innerHTML = `
    昵称加载中...
    曾用名-
    抖音号-
    等级-
    备注 加载中...
    `; container.addEventListener('mousedown', (e) => e.stopPropagation()); container.addEventListener('click', (e) => { e.stopPropagation(); if (e.target.closest('.my-note-display')) enterNoteEditMode(container); }); container.querySelector('.my-join-laoliu-btn').addEventListener('click', (e) => { e.stopPropagation(); handleJoinClick(laoliuGM, container); }); container.querySelector('.my-join-dage-btn').addEventListener('click', (e) => { e.stopPropagation(); handleJoinClick(dageGM, container); }); let contentArea = card.querySelector('.semi-popover-content, .NZ4dNxK4, .QNogr1uz'); if (!contentArea) contentArea = card.firstElementChild; (contentArea || card).appendChild(container); let existing = getCardData(card); if (!existing && latestProfileCore) existing = latestProfileCore; if (existing) updateCardDisplay(card, existing); } function scanForUserCards(root) { if (root.matches && root.matches(CARD_CONFIG.POPOVER_SELECTOR) && isUserCard(root)) { if (!root.querySelector('.my-card-container')) injectCardButtons(root); return; } if (!root.querySelectorAll) return; const cards = root.querySelectorAll(CARD_CONFIG.POPOVER_SELECTOR); for (const card of cards) if (isUserCard(card) && !card.querySelector('.my-card-container')) injectCardButtons(card); } function cleanupHomepagePanelsForRemovedNode(node) { if (homepagePanelMap.size === 0) return; if (node.matches && node.matches(CARD_CONFIG.POPOVER_SELECTOR)) { removeHomepagePanelForCard(node); return; } if (node.querySelectorAll) node.querySelectorAll(CARD_CONFIG.POPOVER_SELECTOR).forEach(card => removeHomepagePanelForCard(card)); } function observeCardAppear() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type !== 'childList') continue; for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; if (node.matches && node.matches(CARD_CONFIG.POPOVER_SELECTOR)) { scanForUserCards(node); continue; } if (node.querySelector && node.querySelector(CARD_CONFIG.POPOVER_SELECTOR)) scanForUserCards(node); } for (const node of mutation.removedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; cleanupHomepagePanelsForRemovedNode(node); } } }); observer.observe(document.body, { childList: true, subtree: true }); let pollTimer = null; const startPolling = () => { if (!pollTimer) pollTimer = setInterval(() => scanForUserCards(document.body), 8000); }; const stopPolling = () => { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } }; document.addEventListener('visibilitychange', () => { if (document.hidden) stopPolling(); else startPolling(); }); if (!document.hidden) startPolling(); } 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:580px; max-height:80vh; 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}.dragging { opacity:0.92; box-shadow:0 14px 40px rgba(0,0,0,0.7); user-select:none; } #${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; cursor:move; } #${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:2px; border-bottom:1px solid #333; margin-bottom:6px; flex-shrink:0; flex-wrap:nowrap; overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; } #${PANEL_ID} .tabs::-webkit-scrollbar { height:4px; } #${PANEL_ID} .tabs::-webkit-scrollbar-thumb { background:#444; border-radius:2px; } #${PANEL_ID} .tab-btn { padding:6px 9px; background:transparent; color:#aaa; border:none; border-bottom:2px solid transparent; cursor:pointer; font-size:12px; white-space:nowrap; flex-shrink:0; transition:all 0.2s; } #${PANEL_ID} .tab-btn.active { color:#fff; border-bottom-color:#ff6b6b; font-weight:600; } #${PANEL_ID} .tab-btn.active[data-tab="dage"] { border-bottom-color:#ffb020; } #${PANEL_ID} .tab-btn.active[data-tab="pending"] { border-bottom-color:#9ca3af; } #${PANEL_ID} .tab-btn.active[data-tab="alert"] { border-bottom-color:#8b8cf0; } #${PANEL_ID} .tab-btn.active[data-tab="welcome"] { border-bottom-color:#4ade80; } #${PANEL_ID} .tab-btn.active[data-tab="giftthanks"] { border-bottom-color:#f472b6; } #${PANEL_ID} .tab-btn.active[data-tab="autodanmaku"] { border-bottom-color:#60a5fa; } #${PANEL_ID} .tab-btn.active[data-tab="queue"] { border-bottom-color:#34d399; } #${PANEL_ID} .tab-content { flex:1; overflow-y:auto; padding:4px 0; } #${PANEL_ID} .tab-content.hidden { display:none; } #${PANEL_ID} .empty-msg { color:#666; text-align:center; padding:20px 0; } #${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 .actions .move-select { background:#1a1a20; border:1px solid #3a3a42; border-radius:4px; color:#4ade80; font-size:11px; padding:2px 3px; max-width:80px; cursor:pointer; } #${PANEL_ID} .player-item .actions .move-select:hover { border-color:#4ade80; } #${PANEL_ID} .player-item .actions .move-select:focus { outline:none; border-color:#22c55e; } #${PANEL_ID} .player-item .actions .send-history-btn { color:#60a5fa; font-size:11px; } #${PANEL_ID} .player-item .actions .send-history-btn:hover { color:#3b82f6; } #${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} .badge { background:#3b3b4a; border-radius:10px; padding:0 6px; font-size:10px; color:#b0b0c0; } #${PANEL_ID} .manual-add-btn, #${PANEL_ID} .data-btn { background:#3b3b4a; border:none; color:#8b8cf0; padding:4px 12px; border-radius:12px; cursor:pointer; font-size:12px; transition:background 0.2s; margin-right:4px; } #${PANEL_ID} .manual-add-btn:hover, #${PANEL_ID} .data-btn:hover { background:#4a4a5a; } #${PANEL_ID} .setting-label { display:flex; align-items:center; gap:8px; margin-bottom:8px; color:#ddd; cursor:pointer; font-size:12px; } #${PANEL_ID} .setting-label input[type="checkbox"] { appearance:none; -webkit-appearance:none; -moz-appearance:none; position:relative; width:34px; height:18px; min-width:34px; border-radius:10px; background:#3a3a42; outline:none; cursor:pointer; transition:background 0.2s ease; vertical-align:middle; margin:0; } #${PANEL_ID} .setting-label input[type="checkbox"]::after { content:''; position:absolute; top:2px; left:2px; width:14px; height:14px; border-radius:50%; background:#ddd; box-shadow:0 1px 2px rgba(0,0,0,0.4); transition:transform 0.2s ease, background 0.2s ease; } #${PANEL_ID} .setting-label input[type="checkbox"]:checked { background:linear-gradient(135deg,#ff6b6b,#ee5a24); } #${PANEL_ID} .setting-label input[type="checkbox"]:checked::after { transform:translateX(16px); background:#fff; } #${PANEL_ID} .setting-label input[type="checkbox"]:hover { box-shadow:0 0 0 3px rgba(255,255,255,0.06); } #${PANEL_ID} .setting-label input[type="number"], #${PANEL_ID} .setting-label select { background:#111; color:#eee; border:1px solid #333; border-radius:4px; padding:2px 6px; width:70px; } #${PANEL_ID} .setting-desc { font-size:11px; color:#888; margin-top:6px; line-height:1.6; } #${PANEL_ID} .settings-section { margin-bottom:16px; border-bottom:1px solid #2a2a30; padding-bottom:12px; } #${PANEL_ID} .settings-section:last-child { border-bottom:none; } #${PANEL_ID} .settings-section-title { font-size:14px; font-weight:600; color:#ddd; margin-bottom:8px; } #${PANEL_ID} .settings-row { display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-bottom:6px; } #${PANEL_ID} .settings-row label { display:flex; align-items:center; gap:4px; color:#bbb; font-size:12px; } #${PANEL_ID} .settings-row textarea { background:#111; color:#eee; border:1px solid #333; border-radius:4px; padding:6px; font-size:12px; resize:vertical; font-family:monospace; width:100%; box-sizing:border-box; line-height:1.5; } #${PANEL_ID} .settings-row textarea:focus { outline:none; border-color:#8b8cf0; } #${PANEL_ID} .settings-row .preview-text { font-size:11px; color:#888; word-break:break-word; } #${PANEL_ID} .slider-row { display:flex; align-items:center; gap:10px; } #${PANEL_ID} .slider-row input[type="range"] { flex:1; accent-color:#8b8cf0; } #${PANEL_ID} .slider-value { min-width:30px; text-align:center; color:#ccc; } #${PANEL_ID} .param-help { font-size:11px; color:#888; border-top:1px solid #333; margin-top:8px; padding-top:6px; line-height:1.8; } #${PANEL_ID} .param-help code { background:#2a2a32; padding:1px 5px; border-radius:4px; color:#c7c7f5; } `); } let panelElement = null, groupContainers = {}, currentTab = 'laoliu', panelVisible = false; function moveGroupMember(fromGm, toGm, index, fromContainer) { const member = fromGm.list[index]; if (!member) return; if (toGm.map.has(member.shortId)) { alert(`该成员已在${toGm.meta.label}中,无法重复添加`); return; } fromGm.removeAt(index); toGm.list.push(member); toGm.map.set(member.shortId, member); toGm.save(); log(`🔀 将 ${member.nickname} 从${fromGm.meta.label}移动到${toGm.meta.label}`); if (fromContainer) renderGroupList(fromContainer, fromGm, groupSearchState[fromGm.meta.tabKey]); refreshGroupUI(toGm); refreshOpenCardsForShortId(member.shortId); } function sendHistoryDanmaku(member) { if (!member) return; const historyStr = (member.history && member.history.length > 0) ? member.history.map(h => h.nickname).join("、") : "无"; const msg = `📜 ${member.nickname} 的曾用名:${historyStr}`; enqueueDanmaku(msg, { label: '曾用名', priority: 'high' }); } const groupSearchState = { laoliu: '', dage: '', pending: '' }; function debounce(fn, wait) { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), wait); }; } function memberMatchesKeyword(member, kwLower) { if (!kwLower) return true; if (member.nickname && member.nickname.toLowerCase().includes(kwLower)) return true; if (member.shortId && String(member.shortId).toLowerCase().includes(kwLower)) return true; if (member.note && member.note.toLowerCase().includes(kwLower)) return true; if (member.displayId && member.displayId.toLowerCase().includes(kwLower)) return true; if (member.history && member.history.length) { for (const h of member.history) { if (h.nickname && h.nickname.toLowerCase().includes(kwLower)) return true; } } return false; } function filterGroupEntries(gm, keyword) { const kw = String(keyword || '').trim().toLowerCase(); if (!kw) return gm.list.map((member, index) => ({ member, index })); const entries = []; for (let i = 0; i < gm.list.length; i++) { if (memberMatchesKeyword(gm.list[i], kw)) entries.push({ member: gm.list[i], index: i }); } return entries; } function renderGroupList(container, gm, keyword = '') { if (!container || !gm) return 0; const list = gm.list; if (list.length === 0) { container.innerHTML = `
    ${gm.meta.label}暂无成员
    `; return 0; } const entries = filterGroupEntries(gm, keyword); if (entries.length === 0) { container.innerHTML = `
    没有找到匹配"${escapeHTML(String(keyword || '').trim())}"的成员
    `; return 0; } const moveOptionsHTML = gm.others.map(og => ``).join(''); let html = ''; for (const { member, index } of entries) { html += `
  • ${escapeHTML(member.nickname)}🆔${escapeHTML(member.shortId)}Lv.${member.level}${(member.history||[]).length}次改名
  • `; } container.innerHTML = html; bindGroupListDelegation(container, gm); return entries.length; } const delegatedListContainers = new WeakSet(); function bindGroupListDelegation(container, gm) { if (delegatedListContainers.has(container)) return; delegatedListContainers.add(container); container.addEventListener("change", (e) => { const input = e.target.closest(".note-input"); if (input) { const item = input.closest(".player-item"); if (!item) return; const index = parseInt(item.dataset.index); if (!isNaN(index) && gm.list[index]) { gm.list[index].note = input.value; gm.save(); refreshOpenCardsForShortId(gm.list[index].shortId); } return; } const select = e.target.closest(".move-select"); if (select) { const item = select.closest(".player-item"); if (!item) return; const index = parseInt(item.dataset.index); if (isNaN(index) || !gm.list[index]) return; const targetGm = groupManagers[select.value]; if (targetGm) moveGroupMember(gm, targetGm, index, container); return; } }); container.addEventListener("click", (e) => { const nickEl = e.target.closest(".nick"); if (nickEl) { openHomepage(nickEl.dataset.secuid); return; } const item = e.target.closest(".player-item"); if (!item) return; const index = parseInt(item.dataset.index); if (isNaN(index) || !gm.list[index]) return; if (e.target.closest(".send-history-btn")) { sendHistoryDanmaku(gm.list[index]); return; } if (e.target.closest(".del-btn")) { if (confirm(`确定将 ${gm.list[index].nickname} 从${gm.meta.label}移除吗?`)) { const shortId = gm.list[index].shortId; gm.removeAt(index); renderGroupList(container, gm, groupSearchState[gm.meta.tabKey]); refreshOpenCardsForShortId(shortId); } return; } if (e.target.closest(".history-btn")) { const member = gm.list[index]; const history = member.history || []; let msg = `「${member.nickname}」的曾用名记录:\n`; if (history.length === 0) msg += "(无)"; else history.forEach(h => { msg += ` ${h.nickname} (${new Date(h.changedAt).toLocaleString()})\n`; }); alert(msg); } }); } function refreshGroupUI(gm) { if (panelVisible && currentTab === gm.meta.tabKey) { const container = groupContainers[gm.meta.tabKey]; const list = container ? container.querySelector(".group-list") : null; if (list) renderGroupList(list, gm, groupSearchState[gm.meta.tabKey]); } } function manualAddToGroup(gm) { const nickname = prompt("请输入当前昵称:"); if (nickname === null) return; const trimmedNick = nickname.trim(); if (!trimmedNick) { alert("昵称不能为空"); return; } const shortId = prompt("请输入 short_id(数字ID):"); if (shortId === null) return; const trimmedId = shortId.trim(); if (!trimmedId) { alert("short_id 不能为空"); return; } const note = prompt("请输入备注(可选,直接确定跳过):"); const trimmedNote = note !== null ? note.trim() : ""; const result = gm.add(trimmedId, trimmedNick, 0, trimmedNote); if (result.ok) { alert(`✅ 已添加 ${trimmedNick} 到 ${gm.meta.label}`); refreshGroupUI(gm); gm.others.forEach(og => refreshGroupUI(og)); } else alert(`⚠️ 该 short_id 已在 ${gm.meta.label} 中`); } function exportGroup(gm) { if (gm.list.length === 0) { alert(`${gm.meta.label}为空,无可导出数据。`); return; } copyText(JSON.stringify(gm.list, null, 2)); alert(`已导出 ${gm.list.length} 个成员数据到剪贴板。`); } function importGroupOverwrite(gm) { const input = prompt(`请粘贴${gm.meta.label} JSON 数据(将覆盖当前所有数据):`); if (input === null) return; const trimmed = input.trim(); if (!trimmed) { alert("输入内容为空,导入取消。"); return; } try { const parsed = JSON.parse(trimmed); if (!Array.isArray(parsed)) { alert("数据格式错误:根必须是数组。"); return; } for (let i = 0; i < parsed.length; i++) { const item = parsed[i]; if (!item.shortId || typeof item.nickname !== 'string') { alert(`第 ${i + 1} 个成员缺少 shortId 或 nickname 字段,导入失败。`); return; } item.shortId = String(item.shortId); if (!item.history) item.history = []; if (!item.note) item.note = ""; if (!item.secUid) item.secUid = ""; if (!item.displayId) item.displayId = ""; if (typeof item.level !== 'number') item.level = 0; } if (!confirm(`即将覆盖当前 ${gm.list.length} 个成员,导入 ${parsed.length} 个新成员,确定吗?`)) return; gm.setAll(parsed); log(`✅ ${gm.meta.label}导入成功,共 ${gm.list.length} 个成员`); refreshGroupUI(gm); alert(`导入成功,共 ${gm.list.length} 个成员。`); } catch (e) { alert(`导入失败:${e.message}`); errorLog("导入解析错误:", e); } } function importGroupMerge(gm) { const input = prompt(`请粘贴${gm.meta.label} JSON 数据(将合并导入,已存在的 shortId 将被跳过):`); if (input === null) return; const trimmed = input.trim(); if (!trimmed) { alert("输入内容为空,导入取消。"); return; } try { const parsed = JSON.parse(trimmed); if (!Array.isArray(parsed)) { alert("数据格式错误:根必须是数组。"); return; } for (let i = 0; i < parsed.length; i++) { const item = parsed[i]; const hasShortId = item.shortId !== undefined && item.shortId !== null && item.shortId !== ""; if (!hasShortId || typeof item.nickname !== 'string') { alert(`第 ${i + 1} 个成员缺少 shortId 或 nickname 字段,导入失败。`); return; } } if (!confirm(`即将合并导入 ${parsed.length} 个成员,当前${gm.meta.label}有 ${gm.list.length} 个成员,已存在的 shortId 将被跳过,确定吗?`)) return; let addedCount = 0, skippedCount = 0; for (const item of parsed) { const result = gm.add(String(item.shortId), item.nickname, typeof item.level === 'number' ? item.level : 0, typeof item.note === 'string' ? item.note : "", item.secUid || "", item.displayId || ""); if (result.ok) addedCount++; else skippedCount++; } log(`✅ ${gm.meta.label}合并导入完成,新增 ${addedCount} 个,跳过已存在 ${skippedCount} 个`); refreshGroupUI(gm); alert(`导入成功!新增 ${addedCount} 个成员,跳过 ${skippedCount} 个已存在成员。`); } catch (e) { alert(`导入失败:${e.message}`); errorLog("导入解析错误:", e); } } function renderAlertSettingsTab(container) { if (!container) return; const currentAlertEnter = GM_getValue(`${NS}:alert_enter_enabled`, true); const currentAlertChat = GM_getValue(`${NS}:alert_chat_enabled`, true); const currentClickToSend = GM_getValue(`${NS}:alert_click_to_send_enabled`, true); const currentOpacity = GM_getValue(`${NS}:alert_opacity`, 0.85); container.innerHTML = `
    🔔 弹窗提醒 & 弹幕模板
    弹窗背景透明度 ${Math.round(currentOpacity * 100)}%
    开启"自动欢迎"的分组,其成员进场将不再弹窗,改为直接发送弹幕(见"自动欢迎设置"选项卡)。以下弹幕模板仅用于弹窗提醒的展示内容,以及点击弹窗手动发送的弹幕,不会影响自动欢迎。
    🕵️ 老六组
    👑 大哥组
    弹窗模板可用参数(仅用于弹窗提醒): {nickname} {shortId} {level} {group} {groupIcon} {groupLabel} {action} {history} {note} (发言模板额外支持 {content}
    `; container.querySelector('#setting-enter-alert').addEventListener('change', function() { alertEnterEnabled = this.checked; GM_setValue(`${NS}:alert_enter_enabled`, alertEnterEnabled); }); container.querySelector('#setting-chat-alert').addEventListener('change', function() { alertChatEnabled = this.checked; GM_setValue(`${NS}:alert_chat_enabled`, alertChatEnabled); }); container.querySelector('#setting-click-to-send').addEventListener('change', function() { alertClickToSendEnabled = this.checked; GM_setValue(`${NS}:alert_click_to_send_enabled`, alertClickToSendEnabled); }); const opacitySlider = container.querySelector('#setting-opacity'); const opacityValue = container.querySelector('#opacity-value'); opacitySlider.addEventListener('input', function() { const val = parseFloat(this.value); opacityValue.textContent = Math.round(val * 100) + '%'; alertOpacity = val; GM_setValue(`${NS}:alert_opacity`, val); setAlertOpacity(val); }); const templatePreviewSamples = { laoliu: { enter: { nickname: '示例用户', shortId: '123456', level: 5, group: '老六组', groupIcon: '🚨', groupLabel: '老六出没', action: '进入直播间', history: '曾用名A、曾用名B', note: '示例备注', content: '' }, chat: { nickname: '示例用户', shortId: '123456', level: 5, group: '老六组', groupIcon: '🚨', groupLabel: '老六出没', action: '发言了', history: '曾用名A、曾用名B', note: '示例备注', content: '你好呀' } }, dage: { enter: { nickname: '示例大哥', shortId: '654321', level: 40, group: '大哥组', groupIcon: '👑', groupLabel: '大哥驾到', action: '进入直播间', history: '无', note: '示例备注', content: '' }, chat: { nickname: '示例大哥', shortId: '654321', level: 40, group: '大哥组', groupIcon: '👑', groupLabel: '大哥驾到', action: '发言了', history: '无', note: '示例备注', content: '主播好棒' } }, }; function bindGroupTemplateField(groupKey, type) { const textarea = container.querySelector(`#tpl-${groupKey}-${type}`); const preview = container.querySelector(`#preview-${groupKey}-${type}`); function updatePreview() { const lines = textarea.value.split('\n').map(s => s.trim()).filter(Boolean); const fallback = DEFAULT_TEMPLATES[groupKey][type]; const sample = lines[0] || fallback; preview.textContent = applyTemplate(sample, templatePreviewSamples[groupKey][type]); } textarea.addEventListener('input', updatePreview); textarea.addEventListener('change', function() { setCustomTemplate(groupKey, type, this.value); }); updatePreview(); } [['laoliu', 'enter'], ['laoliu', 'chat'], ['dage', 'enter'], ['dage', 'chat']].forEach(([g, t]) => bindGroupTemplateField(g, t)); container.querySelector('#custom-template-reset-all').addEventListener('click', function() { if (!confirm('确定清空所有自定义弹幕模板吗?清空后将恢复为默认模板。')) return; ['laoliu', 'dage'].forEach(g => ['enter', 'chat'].forEach(t => setCustomTemplate(g, t, ''))); log('🧹 已清空所有自定义弹幕模板'); renderAlertSettingsTab(container); }); } function renderWelcomeSettingsTab(container) { if (!container) return; const currentAutoLaoliu = GM_getValue(`${NS}:auto_welcome_laoliu`, false); const currentAutoDage = GM_getValue(`${NS}:auto_welcome_dage`, false); const currentGlobalWelcome = GM_getValue(`${NS}:global_welcome_enabled`, false); const currentGlobalLevel = GM_getValue(`${NS}:global_welcome_level_threshold`, 30); const currentGlobalTemplate = GM_getValue(`${NS}:global_welcome_template`, DEFAULT_GLOBAL_WELCOME_TEMPLATE); container.innerHTML = `
    🤖 自动欢迎

    ⚠️ 分组自动欢迎与全局等级欢迎互斥,若任一分组的自动欢迎开启,全局欢迎将不会触发;三者共用同一套"自动欢迎模板"。
    预览:
    可用参数:{nickname} {shortId} {level} {note}(分组自动欢迎时另可用 {group} {groupIcon} {groupLabel},全局欢迎时这三项为空)
    `; container.querySelector('#auto-welcome-laoliu').addEventListener('change', function() { autoWelcomeLaoliu = this.checked; GM_setValue(`${NS}:auto_welcome_laoliu`, autoWelcomeLaoliu); }); container.querySelector('#auto-welcome-dage').addEventListener('change', function() { autoWelcomeDage = this.checked; GM_setValue(`${NS}:auto_welcome_dage`, autoWelcomeDage); }); container.querySelector('#global-welcome-enabled').addEventListener('change', function() { globalWelcomeEnabled = this.checked; GM_setValue(`${NS}:global_welcome_enabled`, globalWelcomeEnabled); }); container.querySelector('#global-welcome-level').addEventListener('change', function() { const val = parseInt(this.value); if (!isNaN(val) && val >= 0) { globalWelcomeLevelThreshold = val; GM_setValue(`${NS}:global_welcome_level_threshold`, val); } }); const globalWelcomeTextarea = container.querySelector('#global-welcome-template'); const previewGlobalWelcome = container.querySelector('#preview-global-welcome'); function updateGlobalWelcomePreview() { const lines = globalWelcomeTextarea.value.split('\n').map(s => s.trim()).filter(Boolean); const sample = lines[0] || DEFAULT_GLOBAL_WELCOME_TEMPLATE; previewGlobalWelcome.textContent = applyTemplate(sample, { nickname: '示例用户', shortId: '123456', level: globalWelcomeLevelThreshold || 30, note: '无备注' }); } globalWelcomeTextarea.addEventListener('input', updateGlobalWelcomePreview); globalWelcomeTextarea.addEventListener('change', function() { globalWelcomeTemplate = this.value; GM_setValue(`${NS}:global_welcome_template`, this.value); }); updateGlobalWelcomePreview(); } function renderGiftThanksSettingsTab(container) { if (!container) return; const currentGiftThanks = GM_getValue(`${NS}:gift_thanks_enabled`, false); const currentGiftMode = GM_getValue(`${NS}:gift_thanks_mode`, 'all'); const currentGiftThreshold = GM_getValue(`${NS}:gift_thanks_threshold`, 100); const currentGiftTemplate = GM_getValue(`${NS}:gift_thanks_template`, DEFAULT_GIFT_TEMPLATE); const currentGiftCooldown = GM_getValue(`${NS}:gift_thanks_cooldown`, 30); container.innerHTML = `
    🎁 礼物自动感谢
    预览:
    可用参数:{nickname} {shortId} {level} {giftName} {giftCount} {diamondTotal}
    `; container.querySelector('#gift-thanks-enabled').addEventListener('change', function() { giftThanksEnabled = this.checked; GM_setValue(`${NS}:gift_thanks_enabled`, giftThanksEnabled); }); container.querySelector('#gift-thanks-mode').addEventListener('change', function() { giftThanksMode = this.value; GM_setValue(`${NS}:gift_thanks_mode`, giftThanksMode); }); container.querySelector('#gift-thanks-threshold').addEventListener('change', function() { const val = parseInt(this.value); if (!isNaN(val) && val > 0) { giftThanksThreshold = val; GM_setValue(`${NS}:gift_thanks_threshold`, val); } }); container.querySelector('#gift-thanks-cooldown').addEventListener('change', function() { const val = parseInt(this.value); if (!isNaN(val) && val >= 0) { giftThanksCooldown = val; GM_setValue(`${NS}:gift_thanks_cooldown`, val); } }); const giftThanksTextarea = container.querySelector('#gift-thanks-template'); const previewGiftThanks = container.querySelector('#preview-gift-thanks'); function updateGiftThanksPreview() { const lines = giftThanksTextarea.value.split('\n').map(s => s.trim()).filter(Boolean); const sample = lines[0] || DEFAULT_GIFT_TEMPLATE; previewGiftThanks.textContent = applyTemplate(sample, { nickname: '示例用户', shortId: '123456', level: 10, giftName: '玫瑰花', giftCount: 66, diamondTotal: 660 }); } giftThanksTextarea.addEventListener('input', updateGiftThanksPreview); giftThanksTextarea.addEventListener('change', function() { giftThanksTemplate = this.value; GM_setValue(`${NS}:gift_thanks_template`, this.value); }); updateGiftThanksPreview(); } function renderAutoDanmakuTab(container) { if (!container) return; const currentEnabled = GM_getValue(`${NS}:auto_danmaku_enabled`, false); const currentMode = GM_getValue(`${NS}:auto_danmaku_mode`, 'sequential'); const currentInterval = GM_getValue(`${NS}:auto_danmaku_interval`, 60); const currentMessages = GM_getValue(`${NS}:auto_danmaku_messages`, DEFAULT_AUTO_DANMAKU_MESSAGES); const currentPreviewEnabled = GM_getValue(`${NS}:danmaku_preview_enabled`, false); container.innerHTML = `
    📢 自动弹幕
    最短间隔为 5 秒,避免刷屏被平台限制。顺序发送会按列表从上到下依次循环;随机发送每次从列表中随机挑一条。

    开启后网页上会出现一个可拖动的小窗口,把上面弹幕列表逐条列出来,点哪条就立即发送哪条(走发送队列,优先处理);窗口右下角可拖拽调整大小,气泡会自动换行铺满,尺寸会记住,下次打开自动还原;关闭后该区域会被完全移除。
    `; function updateStatus() { const status = container.querySelector('#auto-danmaku-status'); const list = autoDanmakuMessagesRaw.split('\n').map(s => s.trim()).filter(Boolean); if (!autoDanmakuEnabled) { status.textContent = '当前未启用'; return; } if (list.length === 0) { status.textContent = '⚠️ 弹幕列表为空,不会发送'; return; } status.textContent = `已启用,每 ${autoDanmakuInterval} 秒${autoDanmakuMode === 'random' ? '随机' : '顺序'}发送一条,共 ${list.length} 条候选`; } container.querySelector('#auto-danmaku-enabled').addEventListener('change', function() { autoDanmakuEnabled = this.checked; GM_setValue(`${NS}:auto_danmaku_enabled`, autoDanmakuEnabled); startAutoDanmakuTimer(); updateStatus(); }); container.querySelector('#auto-danmaku-mode').addEventListener('change', function() { autoDanmakuMode = this.value; GM_setValue(`${NS}:auto_danmaku_mode`, autoDanmakuMode); startAutoDanmakuTimer(); updateStatus(); }); container.querySelector('#auto-danmaku-interval').addEventListener('change', function() { const val = parseInt(this.value); const safeVal = (!isNaN(val) && val >= 5) ? val : 60; this.value = safeVal; autoDanmakuInterval = safeVal; GM_setValue(`${NS}:auto_danmaku_interval`, safeVal); startAutoDanmakuTimer(); updateStatus(); }); const messagesTextarea = container.querySelector('#auto-danmaku-messages'); messagesTextarea.addEventListener('change', function() { autoDanmakuMessagesRaw = this.value; GM_setValue(`${NS}:auto_danmaku_messages`, this.value); autoDanmakuIndex = 0; updateStatus(); refreshDanmakuPreviewPanelIfVisible(); }); container.querySelector('#auto-danmaku-test-btn').addEventListener('click', function() { const list = messagesTextarea.value.split('\n').map(s => s.trim()).filter(Boolean); if (list.length === 0) { alert('弹幕列表为空,请先填写至少一条弹幕'); return; } const msg = autoDanmakuMode === 'random' ? list[Math.floor(Math.random() * list.length)] : list[0]; enqueueDanmaku(msg, { label: '测试发送', priority: 'high' }); }); container.querySelector('#danmaku-preview-enabled').addEventListener('change', function() { setDanmakuPreviewEnabled(this.checked); }); updateStatus(); } function renderQueueTab(container) { if (!container) return; const currentMaxSize = GM_getValue(`${NS}:queue_max_size`, 10); container.innerHTML = `
    📬 发送队列
    自动欢迎、礼物感谢、自动弹幕、点击弹窗手动发送、曾用名弹幕等所有发送弹幕的入口,都会先进入这个队列,串行依次真正发送,避免多个来源同时抢占聊天输入框导致互相打断、内容错乱或漏发。手动触发的发送(点击弹窗、测试发送、发送曾用名)会插到队首优先处理。
    每条消息"多久发一次"由各功能自己的设置决定——比如自动弹幕的"发送间隔"、礼物感谢的"冷却时间";这里的队列只负责"排队串行发送"这一件事,两条消息挤在同一瞬间时按固定的极短间隔错开发送,不需要额外配置。
    队列上限用于防止极端情况下(比如短时间内大量观众进场触发自动欢迎)待发消息无限堆积;超出上限会自动丢弃最早排队的一条。
    ⚪ 空闲 | 待发送:0 条
    `; container.querySelector('#queue-max-size-input').addEventListener('change', function() { const val = parseInt(this.value); const safeVal = (!isNaN(val) && val >= 5) ? val : 10; this.value = safeVal; queueMaxSize = safeVal; GM_setValue(`${NS}:queue_max_size`, safeVal); }); container.querySelector('#queue-clear-btn').addEventListener('click', function() { if (!confirm('确定清空当前待发送队列吗?')) return; clearQueue(); }); refreshQueueStatusUI(); } function buildPanelContent($panel) { if ($panel.querySelector(".tabs")) return; $panel.innerHTML = `
    🎯 抓老六
    `; $panel.querySelector("#stalker-panel-close").addEventListener("click", () => { panelVisible = false; $panel.classList.remove("show"); }); $panel.querySelectorAll(".tab-btn").forEach(btn => btn.addEventListener("click", () => switchTab(btn.dataset.tab))); makeDraggable($panel, { handle: $panel.querySelector(".header"), storageKeyX: `${NS}:panelX`, storageKeyY: `${NS}:panelY`, }); groupContainers.laoliu = document.getElementById("tab-laoliu"); buildGroupTab(groupContainers.laoliu, laoliuGM); groupContainers.dage = document.getElementById("tab-dage"); buildGroupTab(groupContainers.dage, dageGM); groupContainers.pending = document.getElementById("tab-pending"); buildGroupTab(groupContainers.pending, pendingGM); renderAlertSettingsTab(document.getElementById("tab-alert")); renderWelcomeSettingsTab(document.getElementById("tab-welcome")); renderGiftThanksSettingsTab(document.getElementById("tab-giftthanks")); renderAutoDanmakuTab(document.getElementById("tab-autodanmaku")); renderQueueTab(document.getElementById("tab-queue")); currentTab = 'laoliu'; renderGroupList(groupContainers.laoliu.querySelector(".group-list"), laoliuGM, groupSearchState.laoliu); renderGroupList(groupContainers.dage.querySelector(".group-list"), dageGM, groupSearchState.dage); renderGroupList(groupContainers.pending.querySelector(".group-list"), pendingGM, groupSearchState.pending); } function buildGroupTab(container, gm) { if (!container) return; container.innerHTML = `
    `; container.querySelector('[data-act="manual"]').addEventListener("click", () => manualAddToGroup(gm)); container.querySelector('[data-act="export"]').addEventListener("click", () => exportGroup(gm)); container.querySelector('[data-act="import-merge"]').addEventListener("click", () => importGroupMerge(gm)); container.querySelector('[data-act="import-overwrite"]').addEventListener("click", () => importGroupOverwrite(gm)); const searchInput = container.querySelector('.group-search-input'); const countLabel = container.querySelector('.group-search-count'); const listEl = container.querySelector('.group-list'); function updateCount(matched) { const kw = (groupSearchState[gm.meta.tabKey] || '').trim(); countLabel.textContent = kw ? `${matched}/${gm.list.length}` : ''; } const doFilter = debounce(() => { const kw = searchInput.value; groupSearchState[gm.meta.tabKey] = kw; const matched = renderGroupList(listEl, gm, kw); updateCount(matched); }, 150); searchInput.addEventListener('input', doFilter); } function switchTab(tab) { currentTab = tab; const panel = panelElement; if (!panel) return; panel.querySelectorAll(".tab-btn").forEach(b => b.classList.toggle("active", b.dataset.tab === tab)); ["laoliu", "dage", "pending", "alert", "welcome", "giftthanks", "autodanmaku", "queue"].forEach(t => { const el = document.getElementById(`tab-${t}`); if (el) el.classList.toggle("hidden", t !== tab); }); if (tab === "queue") refreshQueueStatusUI(); if (tab === "laoliu" || tab === "dage" || tab === "pending") { const gm = groupManagers[tab]; const container = groupContainers[tab]; const list = container ? container.querySelector(".group-list") : null; if (list) renderGroupList(list, gm, groupSearchState[gm.meta.tabKey]); } } function refreshAllTabs() { if (currentTab !== "laoliu" && currentTab !== "dage" && currentTab !== "pending") return; const gm = groupManagers[currentTab]; const container = groupContainers[currentTab]; const list = container ? container.querySelector(".group-list") : null; if (list) renderGroupList(list, gm, groupSearchState[gm.meta.tabKey]); } function injectCardStyles() { addStyle("dy-card-helper-style", ` .my-card-container { display:flex; flex-direction:column; gap:10px; margin:12px 0 8px 0; padding:12px 14px; background:#f8f9fa; border-radius:10px; border:1px solid #e9ecef; box-shadow:0 1px 3px rgba(0,0,0,0.04); width:100%; box-sizing:border-box; } .my-card-info { font-size:14px; line-height:1.7; color:#212529; } .my-card-fields { display:flex; flex-direction:column; gap:2px; } .my-field { display:flex; align-items:center; min-height:26px; padding:2px 0; } .my-field-note { align-items:flex-start; } .my-label { font-weight:500; color:#6c757d; width:60px; flex-shrink:0; font-size:13px; padding-top:1px; } .my-value { color:#212529; font-weight:500; word-break:break-word; } .my-note-display { flex:1; cursor:pointer; white-space:pre-wrap; border-bottom:1px dashed transparent; border-radius:2px; padding:1px 2px; margin:-1px -2px; transition:border-color 0.15s, background 0.15s; } .my-note-display:hover { border-bottom-color:#adb5bd; background:rgba(0,0,0,0.03); } .my-note-display.my-note-empty, .my-field-history.my-note-empty { color:#adb5bd; font-weight:400; font-style:italic; } .my-field-history { white-space:normal; word-break:break-word; } .my-note-edit-wrap { flex:1; display:flex; gap:6px; align-items:flex-end; } .my-note-edit-wrap textarea { flex:1; min-width:0; resize:none; overflow:hidden; max-height:140px; padding:4px 8px; border:1px solid #ced4da; border-radius:4px; font-size:13px; font-family:inherit; line-height:1.5; white-space:pre-wrap; word-break:break-word; box-sizing:border-box; transition:border-color 0.15s, box-shadow 0.15s; } .my-note-edit-wrap textarea:focus { border-color:#86b7fe; outline:0; box-shadow:0 0 0 0.2rem rgba(13,110,253,0.25); } .my-note-save-btn, .my-note-cancel-btn { flex-shrink:0; width:24px; height:24px; line-height:22px; text-align:center; padding:0; border:none; border-radius:50%; font-size:12px; cursor:pointer; transition:background 0.15s, transform 0.1s; } .my-note-save-btn:active, .my-note-cancel-btn:active { transform:scale(0.9); } .my-note-save-btn { background:#28a745; color:#fff; } .my-note-save-btn:hover { background:#218838; } .my-note-cancel-btn { background:#e9ecef; color:#495057; } .my-note-cancel-btn:hover { background:#dee2e6; } .my-card-actions { display:flex; gap:8px; align-items:center; } .my-card-actions button { flex:1; padding:6px 12px; border:none; border-radius:16px; font-size:12px; cursor:pointer; white-space:nowrap; transition:background 0.15s, transform 0.1s, opacity 0.15s; } .my-card-actions button:active { transform:scale(0.97); } .my-card-home-btn { display:flex; align-items:center; justify-content:center; background:#0d6efd; color:#fff; } .my-card-home-btn:hover { background:#0b5ed7; } .my-group-btn-laoliu { background:#4CAF50; color:#fff; } .my-group-btn-laoliu:hover:not(:disabled) { background:#43a047; } .my-group-btn-dage { background:#FF9800; color:#fff; } .my-group-btn-dage:hover:not(:disabled) { background:#fb8c00; } .my-group-btn:disabled { opacity:0.55; cursor:not-allowed; } `); } function makeDraggable(el, options) { const { handle = el, storageKeyX = null, storageKeyY = null, defaultX = null, defaultY = null, ignoreSelector = 'button, input, textarea, select, a, .close', onDragEnd = null, } = options || {}; let isDragging = false; let wasDragged = false; let startX = 0, startY = 0, origLeft = 0, origTop = 0; let elWidth = 0, elHeight = 0; let rafId = null; let pendingLeft = 0, pendingTop = 0; function applyPosition(left, top) { el.style.left = left + "px"; el.style.top = top + "px"; el.style.right = "auto"; el.style.bottom = "auto"; } function savePosition() { if (!storageKeyX || !storageKeyY) return; const rect = el.getBoundingClientRect(); GM_setValue(storageKeyX, Math.round(rect.left)); GM_setValue(storageKeyY, Math.round(rect.top)); } function scheduleFrame() { if (rafId !== null) return; rafId = requestAnimationFrame(() => { rafId = null; applyPosition(pendingLeft, pendingTop); }); } const onMouseMove = (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (Math.abs(dx) > 4 || Math.abs(dy) > 4) wasDragged = true; let left = origLeft + dx; let top = origTop + dy; const maxX = Math.max(0, window.innerWidth - elWidth); const maxY = Math.max(0, window.innerHeight - elHeight); left = Math.max(0, Math.min(left, maxX)); top = Math.max(0, Math.min(top, maxY)); pendingLeft = left; pendingTop = top; scheduleFrame(); }; const onMouseUp = () => { if (!isDragging) return; isDragging = false; el.classList.remove("dragging"); if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; applyPosition(pendingLeft, pendingTop); } if (wasDragged) { savePosition(); if (onDragEnd) onDragEnd(); } document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); setTimeout(() => { wasDragged = false; }, 100); }; handle.addEventListener("mousedown", (e) => { if (e.button !== 0) return; if (ignoreSelector && e.target.closest(ignoreSelector)) return; isDragging = true; wasDragged = false; el.classList.add("dragging"); const rect = el.getBoundingClientRect(); elWidth = rect.width; elHeight = rect.height; startX = e.clientX; startY = e.clientY; origLeft = rect.left; origTop = rect.top; document.addEventListener("mousemove", onMouseMove, { passive: true }); document.addEventListener("mouseup", onMouseUp); e.preventDefault(); }); if (storageKeyX && storageKeyY) { const savedX = GM_getValue(storageKeyX, defaultX); const savedY = GM_getValue(storageKeyY, defaultY); if (savedX !== null && savedY !== null) { el.style.right = "auto"; el.style.bottom = "auto"; el.style.left = savedX + "px"; el.style.top = savedY + "px"; } } return { wasDragged: () => wasDragged }; } function buildUI() { injectPanelStyle(); injectCardStyles(); const $btn = document.createElement("div"); $btn.id = BUTTON_ID; $btn.textContent = "🎯"; $btn.title = "抓老六 (按住拖动 | 点击打开面板)"; document.documentElement.appendChild($btn); const btnDrag = makeDraggable($btn, { storageKeyX: `${NS}:btnX`, storageKeyY: `${NS}:btnY`, defaultX: DEFAULTS.btnX, defaultY: DEFAULTS.btnY, ignoreSelector: null, }); $btn.addEventListener("click", () => { if (btnDrag.wasDragged()) return; panelVisible = !panelVisible; const $panel = document.getElementById(PANEL_ID); $panel.classList.toggle("show", panelVisible); if (panelVisible) { if (!$panel.querySelector(".tabs")) buildPanelContent($panel); refreshAllTabs(); } }); const $panel = document.createElement("div"); $panel.id = PANEL_ID; document.documentElement.appendChild($panel); panelElement = $panel; buildPanelContent($panel); } let initialized = false; function initForLivePage() { if (!isLivePage()) { if (panelElement) panelElement.classList.remove("show"); destroyDanmakuPreviewPanel(); return; } if (!document.getElementById(BUTTON_ID)) buildUI(); if (danmakuPreviewEnabled && !danmakuPreviewPanelEl) createDanmakuPreviewPanel(); if (!initialized) { initialized = true; tryHookDecoder().then((hooked) => { if (!hooked) log("⚠️ Hook 失败,提醒功能可能不可用,请刷新重试"); }); hookProfileRequests(); observeCardAppear(); log("✅ 抓老六(合并增强版 v8.13.0)已启动"); } } function init() { migrateLegacyTemplates(); startAutoDanmakuTimer(); onReady(() => { initForLivePage(); watchRouteChange(() => { if (isLivePage()) initForLivePage(); else { if (panelElement) panelElement.classList.remove("show"); destroyDanmakuPreviewPanel(); } }); }); try { GM_registerMenuCommand("🎯 切换抓老六面板", () => { const panel = document.getElementById(PANEL_ID); if (panel) { panelVisible = !panelVisible; panel.classList.toggle("show", panelVisible); if (panelVisible) refreshAllTabs(); } }); GM_registerMenuCommand("🔄 手动保存所有分组数据", () => { laoliuGM.save(); dageGM.save(); pendingGM.save(); log("已保存"); }); GM_registerMenuCommand("📥 合并导入老六组", () => importGroupMerge(laoliuGM)); GM_registerMenuCommand("♻️ 覆盖导入老六组", () => importGroupOverwrite(laoliuGM)); GM_registerMenuCommand("📥 合并导入大哥组", () => importGroupMerge(dageGM)); GM_registerMenuCommand("♻️ 覆盖导入大哥组", () => importGroupOverwrite(dageGM)); GM_registerMenuCommand("📥 合并导入待定组", () => importGroupMerge(pendingGM)); GM_registerMenuCommand("♻️ 覆盖导入待定组", () => importGroupOverwrite(pendingGM)); GM_registerMenuCommand("📢 切换自动弹幕开关", () => { autoDanmakuEnabled = !autoDanmakuEnabled; GM_setValue(`${NS}:auto_danmaku_enabled`, autoDanmakuEnabled); startAutoDanmakuTimer(); log(`自动弹幕已${autoDanmakuEnabled ? '开启' : '关闭'}`); }); } catch (e) {} } init(); })();