// ==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 = `
`;
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 = `
`;
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 = `
`;
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 = `
`;
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 = `
📬 发送队列
自动欢迎、礼物感谢、自动弹幕、点击弹窗手动发送、曾用名弹幕等所有发送弹幕的入口,都会先进入这个队列,串行依次真正发送,避免多个来源同时抢占聊天输入框导致互相打断、内容错乱或漏发。手动触发的发送(点击弹窗、测试发送、发送曾用名)会插到队首优先处理。
每条消息"多久发一次"由各功能自己的设置决定——比如自动弹幕的"发送间隔"、礼物感谢的"冷却时间";这里的队列只负责"排队串行发送"这一件事,两条消息挤在同一瞬间时按固定的极短间隔错开发送,不需要额外配置。
队列上限用于防止极端情况下(比如短时间内大量观众进场触发自动欢迎)待发消息无限堆积;超出上限会自动丢弃最早排队的一条。
`;
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();
})();