// ==UserScript==
// @name 斗鱼全民星推荐自动领取
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 自动打开、领取并切换直播间处理全民星推荐活动红包
// @author ysl
// @match *://www.douyu.com/6657*
// @match *://www.douyu.com/*
// @match *://www.douyu.com/topic/*?rid=[0-9]*
// @grant GM_openInTab
// @grant GM_closeTab
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_log
// @grant GM_xmlhttpRequest
// @connect list-www.douyu.com
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 常量与配置 ---
const CONTROL_ROOM_ID = "6657"; // 控制页面房间号
const SCRIPT_PREFIX = "[全民星推荐助手]";
const CHECK_INTERVAL = 5000; // 主循环检查间隔 (ms)
const POPUP_WAIT_TIMEOUT = 15000; // 等待中间红包弹窗超时 (ms)
const POPUP_CHECK_INTERVAL = 1000; // 检查中间红包弹窗间隔 (ms)
const CLOSE_POPUP_DELAY = 4000; // 打开红包后等待多久关闭弹窗 (ms)
const PANEL_WAIT_TIMEOUT = 10000; // 等待活动面板/容器出现超时 (ms) - 稍微增加
const ELEMENT_WAIT_TIMEOUT = 30000; // 等待关键页面元素加载的超时时间 (ms) - 新增
const LOAD_DELAY_THRESHOLD = 3; // 连续多少次找不到红包区域算作领完
const MIN_DELAY = 1000; // 随机延迟最小值 (ms)
const MAX_DELAY = 2500; // 随机延迟最大值 (ms) - 稍微增加
const OPEN_TAB_DELAY = 1000; // 打开每个新标签页之间的延迟 (ms) - 稍微增加
const CLOSE_TAB_DELAY = 1500; // 尝试关闭标签页前的延迟 (ms) - 稍微增加
const INITIAL_SCRIPT_DELAY = 3000; // 脚本整体初始化的延迟 (ms) - 稍微增加
const DRAGGABLE_BUTTON_ID = 'douyu-qmx-starter-button'; // 启动按钮 ID
const BUTTON_POS_STORAGE_KEY = 'douyu_qmx_button_position'; // 存储按钮位置的 Key
const API_URL = "https://www.douyu.com/japi/livebiznc/web/anchorstardiscover/redbag/square/list";
const MAX_TAB_LIFETIME_MS = 10 * 60 * 1000; // 设置单个网页最大生存时间为 20 分钟 (可调整)
// --- 选择器 (请根据实际情况调整,特别是 redEnvelopeContainer) ---
const SELECTORS = {
// 右下角红包相关
// 这个选择器路径较长,如果失效,需要用开发者工具检查更新
redEnvelopeContainer: "#layout-Player-aside div.LiveNewAnchorSupportT-enter", // 尝试简化选择器,使其更通用
// 备用选择器 (如果上面那个不行,可以试试这个更具体的)
// redEnvelopeContainer: "#layout-Player-aside > div.layout-Player-asideMainTop > div.layout-Player-effect > div.LiveNewAnchorSupportT-enter",
countdownTimer: "span.LiveNewAnchorSupportT-enter--bottom", // 含倒计时或"抢红包"文字
// 中间弹窗相关
popupModal: "body > div.LiveNewAnchorSupportT-pop",
openButton: "div.LiveNewAnchorSupportT-singleBag--btnOpen",
closeButton: "div.LiveNewAnchorSupportT-pop--close",
// 用于初始化时等待的关键元素 (选择一个页面加载后肯定会存在的元素)
criticalElement: "#js-player-video", // 播放器视频元素,通常加载较快且稳定
// criticalElement: "div.PlayerToolbar-Wealth", // 或者右下角礼物栏的部分
};
// --- 状态变量 ---
let mainIntervalId = null;
let isWaitingForPopup = false;
let isSwitchingRoom = false;
let notFoundCounter = 0;
let isDragging = false; // 用于拖拽按钮
let dragOffsetX = 0;
let dragOffsetY = 0;
let openedRoomIds = new Set();
let tabStartTime = 0; // 记录工作标签页启动时间戳
// --- 辅助函数 ---
function log(message) {
GM_log(`${SCRIPT_PREFIX} ${message}`);
console.log(`${SCRIPT_PREFIX} ${message}`); // 同时在 Tampermonkey 日志和浏览器控制台输出
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getRandomDelay(min = MIN_DELAY, max = MAX_DELAY) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 封装点击操作,加入随机延迟和日志
async function safeClick(element, description, delayBefore = true, delayAfter = true) {
if (!element) {
log(`[点击失败] 无法找到元素: ${description}`);
return false;
}
try {
// 检查元素是否可见可交互 (基本检查)
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || element.offsetParent === null || element.disabled) {
log(`[点击失败] 元素存在但不可见或不可交互: ${description}`);
return false;
}
if (delayBefore) {
const waitBefore = getRandomDelay(MIN_DELAY / 2, MAX_DELAY / 2); // 点击前的延迟可以短一些
log(`准备点击 ${description},先等待 ${waitBefore}ms`);
await sleep(waitBefore);
}
log(`尝试点击: ${description}`);
element.click();
if (delayAfter) {
const waitAfter = getRandomDelay();
log(`点击 ${description} 后等待 ${waitAfter}ms`);
await sleep(waitAfter);
}
return true;
} catch (error) {
log(`[点击异常] ${description} 时发生错误: ${error.message}`);
console.error(`Click error on ${description}:`, error);
return false;
}
}
// 查找元素并等待其出现 (检查可见性)
async function findElement(selector, timeout = PANEL_WAIT_TIMEOUT, parent = document) {
log(`开始查找元素: ${selector} (超时 ${timeout}ms)`);
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const element = parent.querySelector(selector);
if (element) {
const style = window.getComputedStyle(element);
// 检查 display 和 visibility,以及尺寸
if (style.display !== 'none' && style.visibility !== 'hidden' && element.offsetWidth > 0 && element.offsetHeight > 0) {
log(`找到可见元素: ${selector}`);
return element;
}
}
await sleep(300); // 短暂等待后重试
}
log(`查找元素超时: ${selector}`);
return null;
}
// --- API 调用 ---
function getRoomsFromApi(count) {
return new Promise((resolve, reject) => {
log(`开始调用 API 获取房间列表: ${API_URL}`);
GM_xmlhttpRequest({
method: "GET",
url: API_URL,
headers: {
'Referer': 'https://www.douyu.com/', // 添加 Referer
'User-Agent': navigator.userAgent
},
responseType: "json",
timeout: 10000, // 10秒超时
onload: function(response) {
log(`API 响应状态: ${response.status}`);
// console.log("API 完整响应:", response.response); // 调试时可以取消注释看完整响应
if (response.status >= 200 && response.status < 300 && response.response) {
const data = response.response;
if (data.error === 0 && data.data && Array.isArray(data.data.redBagList)) {
log(`API 返回成功,原始找到 ${data.data.redBagList.length} 个房间。`);
const roomUrls = data.data.redBagList
.map(item => item.rid)
.filter(rid => rid)
.slice(0, count * 2) // 多获取一些备用,以防重复
.map(rid => `https://www.douyu.com/${rid}`);
log(`提取到 ${roomUrls.length} 个 URL。`);
resolve(roomUrls);
} else {
log(`API 返回数据格式错误或 error 不为 0: error=${data.error}, msg=${data.msg}`);
reject(new Error(`API 数据错误: ${data.msg || '未知错误'}`));
}
} else {
log(`API 请求失败,状态码: ${response.status}`);
reject(new Error(`API 请求失败,状态码: ${response.status}`));
}
},
onerror: function(error) {
log(`API 请求网络错误: ${error.statusText || '未知网络错误'}`);
console.error("API onerror:", error);
reject(new Error(`API 网络错误: ${error.statusText || '未知'}`));
},
ontimeout: function() {
log("API 请求超时。");
reject(new Error("API 请求超时"));
}
});
});
}
// --- 标签页关闭函数 ---
async function closeCurrentTab() {
log("尝试关闭当前标签页...");
// 停止可能仍在运行的定时器
if (mainIntervalId) {
log("关闭前停止主循环定时器。");
clearInterval(mainIntervalId);
mainIntervalId = null;
}
await sleep(500); // 短暂延迟确保状态更新
try {
log("优先尝试 GM_closeTab()...");
GM_closeTab();
// 如果上面成功,脚本实例应该结束了
log("GM_closeTab() 已调用 (若标签页未关闭,则无效或被阻止)。");
} catch (e) {
log(`GM_closeTab() 失败或不可用: ${e.message}`);
log("尝试备用方法: window.close()..."); // window.open('', '_self').close() 基本无效了
try {
window.close();
log("备用关闭方法 window.close() 已调用。");
} catch (e2) {
log(`备用关闭方法也失败: ${e2.message}`);
}
}
}
// --- 控制页面 (/6657) 相关函数 ---
function makeDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const savedPos = GM_getValue(BUTTON_POS_STORAGE_KEY);
if (savedPos && savedPos.top && savedPos.left) {
element.style.top = savedPos.top;
element.style.left = savedPos.left;
log(`恢复按钮位置: top=${savedPos.top}, left=${savedPos.left}`);
} else {
element.style.top = '100px'; // 默认位置
element.style.left = '20px';
}
element.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
// 仅当鼠标左键按下时触发拖拽
if (e.button !== 0) return;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
isDragging = true;
element.style.cursor = 'grabbing';
log("开始拖拽按钮");
}
function elementDrag(e) {
if (!isDragging) return;
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
let newTop = element.offsetTop - pos2;
let newLeft = element.offsetLeft - pos1;
newTop = Math.max(0, Math.min(window.innerHeight - element.offsetHeight, newTop));
newLeft = Math.max(0, Math.min(window.innerWidth - element.offsetWidth, newLeft));
element.style.top = newTop + "px";
element.style.left = newLeft + "px";
}
function closeDragElement(e) {
// 确保是鼠标左键松开
if (e.button !== 0 && isDragging) return; // 如果不是左键松开,或者没有在拖拽,忽略
document.onmouseup = null;
document.onmousemove = null;
if (isDragging) { // 只有真正拖拽了才保存位置和改光标
isDragging = false;
element.style.cursor = 'grab';
log("结束拖拽按钮");
GM_setValue(BUTTON_POS_STORAGE_KEY, { top: element.style.top, left: element.style.left });
log(`保存按钮位置: top=${element.style.top}, left=${element.style.left}`);
} else {
// 如果只是点击没有拖拽,确保光标恢复
element.style.cursor = 'grab';
}
}
}
async function openOneNewTab() {
const startButton = document.getElementById(DRAGGABLE_BUTTON_ID);
if (!startButton || startButton.disabled) return; // 防止重复点击
startButton.disabled = true;
startButton.innerHTML = '正在查找... <span class="count">(已开: ' + openedRoomIds.size + ')</span>';
log("开始通过 API 查找下一个可打开的房间...");
try {
// 获取 API 房间列表 (可以多获取一些,比如前 10 个)
const apiRoomList = await getRoomsFromApi(10); // 调用 API 函数
let foundNewUrl = null;
let foundNewRid = null;
if (apiRoomList && apiRoomList.length > 0) {
log(`API 返回 ${apiRoomList.length} 个房间,开始查找未打开的...`);
for (const url of apiRoomList) {
const ridMatch = url.match(/\/(\d+)/); // 从 URL 提取 rid
if (ridMatch && ridMatch[1]) {
const rid = ridMatch[1];
if (!openedRoomIds.has(rid)) { // 检查此 rid 是否已在 Set 中
foundNewUrl = url;
foundNewRid = rid;
log(`找到未打开的房间: rid=${rid}, url=${url}`);
break; // 找到第一个就停止查找
} else {
// log(`房间 rid=${rid} 已打开,跳过。`); // 可选调试日志
}
}
}
} else {
log("API 未返回有效的房间列表。");
}
if (foundNewUrl && foundNewRid) {
log(`准备打开新标签页: ${foundNewUrl}`);
try {
GM_openInTab(foundNewUrl, { active: false, setParent: true });
openedRoomIds.add(foundNewRid); // 将新打开的 rid 加入 Set
log(`房间 rid=${foundNewRid} 已添加到打开列表。当前列表大小: ${openedRoomIds.size}`);
startButton.innerHTML = '再打开一个 <span class="count">(已开: ' + openedRoomIds.size + ')</span>'; // 更新按钮文字和计数
await sleep(OPEN_TAB_DELAY); // 短暂延迟
} catch (e) {
log(`打开标签页 ${foundNewUrl} 时出错: ${e.message}`);
// 打开失败,不应该将 rid 加入列表,按钮文字也应该恢复
startButton.innerHTML = '打开出错,重试? <span class="count">(已开: ' + openedRoomIds.size + ')</span>';
}
} else {
log("在 API 列表中未能找到新的、未打开的房间。");
startButton.innerHTML = '无新房间可开 <span class="count">(已开: ' + openedRoomIds.size + ')</span>';
// 这里可以选择让按钮保持禁用,或者几秒后恢复
await sleep(2000); // 等待 2 秒后恢复按钮文字
if (!startButton.disabled) { // 再次检查,以防期间状态改变
startButton.innerHTML = '再打开一个 <span class="count">(已开: ' + openedRoomIds.size + ')</span>';
}
}
} catch (error) {
log(`查找或打开房间时发生错误: ${error.message}`);
startButton.innerHTML = '查找出错,重试? <span class="count">(已开: ' + openedRoomIds.size + ')</span>';
} finally {
startButton.disabled = false; // 无论结果如何,最终都恢复按钮可用性
log("按钮已恢复可用。");
}
}
function setupLauncherUI() {
log("设置控制页面 UI...");
GM_addStyle(`
#${DRAGGABLE_BUTTON_ID} {
position: fixed; /* 使用 fixed 更好 */
z-index: 99999; /* 提高层级 */
background-color: #ff5d23;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: grab;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: background-color 0.2s, opacity 0.3s;
opacity: 0.9; /* 默认稍微透明 */
}
#${DRAGGABLE_BUTTON_ID}:hover {
background-color: #e04a10;
opacity: 1; /* 悬停时不透明 */
}
#${DRAGGABLE_BUTTON_ID}:active {
cursor: grabbing;
background-color: #c8400a;
}
#${DRAGGABLE_BUTTON_ID}:disabled {
background-color: #cccccc;
cursor: not-allowed;
opacity: 0.7;
}
#${DRAGGABLE_BUTTON_ID} span.count { /* 新增样式用于显示计数 */
font-size: 10px;
margin-left: 5px;
opacity: 0.7;
}
`);
if (document.getElementById(DRAGGABLE_BUTTON_ID)) {
log("启动按钮已存在。");
return;
}
const button = document.createElement('button');
button.id = DRAGGABLE_BUTTON_ID;
button.innerHTML = '打开一个房间 <span class="count">(已开: 0)</span>'; // <-- 修改按钮文字和添加计数显示
button.onclick = openOneNewTab; // <-- 修改点击事件处理函数
document.body.appendChild(button);
makeDraggable(button);
log("启动按钮已创建并可拖拽。");
}
// --- 工作页面 相关函数 ---
// 等待并点击中间的红包弹窗及关闭
async function waitForPopupAndClick() {
log("开始等待中间红包弹窗...");
isWaitingForPopup = true; // 标记开始等待
const popup = await findElement(SELECTORS.popupModal, POPUP_WAIT_TIMEOUT);
if (!popup) {
log("等待红包弹窗超时或未找到。");
isWaitingForPopup = false; // 超时重置状态
return false; // 返回失败
}
log("红包弹窗已出现。查找打开按钮...");
const openBtn = popup.querySelector(SELECTORS.openButton);
if (await safeClick(openBtn, "红包弹窗的打开按钮")) {
log(`红包打开按钮已点击,等待 ${CLOSE_POPUP_DELAY}ms 后尝试关闭弹窗...`);
await sleep(CLOSE_POPUP_DELAY);
log("尝试关闭领取结果弹窗...");
// 重新获取弹窗和关闭按钮,因为 DOM 可能已更新
// 注意:此时弹窗选择器可能不变,也可能改变,这里假设不变
const finalPopup = document.querySelector(SELECTORS.popupModal); // 重新查找当前的弹窗
if (finalPopup) {
const closeBtn = finalPopup.querySelector(SELECTORS.closeButton);
if (!await safeClick(closeBtn, "领取结果弹窗的关闭按钮", true, false)) { // 点击前延迟,点击后不延迟
log("关闭按钮未找到或点击失败。");
// 即使关闭失败,也认为本次领取操作流程结束
} else {
log("关闭按钮已点击。");
}
} else {
log("领取结果弹窗似乎已自动消失或无法重新找到。");
}
isWaitingForPopup = false; // 重置状态
return true; // 返回成功
} else {
log("错误:找到了弹窗,但找不到或无法点击打开按钮。");
isWaitingForPopup = false; // 重置状态
return false; // 返回失败
}
}
// 处理切换房间 (API优先)
async function handleSwitchRoom() {
if (isSwitchingRoom) {
log("已在执行切换房间操作,本次跳过。");
return;
}
isSwitchingRoom = true;
log("开始尝试通过 API 获取下一个房间并切换...");
try {
const currentRoomId = window.location.pathname.match(/\/(\d+)/)?.[1] || window.location.search.match(/rid=(\d+)/)?.[1];
log(`当前房间 ID: ${currentRoomId || '未知'}`);
const roomList = await getRoomsFromApi(5); // 多获取几个备选
let nextUrl = null;
if (roomList && roomList.length > 0) {
// 查找第一个与当前房间不同的 URL
for (const url of roomList) {
const nextRoomId = url.match(/\/(\d+)/)?.[1];
if (nextRoomId && nextRoomId !== currentRoomId) {
log(`找到下一个不同的房间: ${url}`);
nextUrl = url;
break;
} else if (!currentRoomId && nextRoomId) {
// 如果当前房间 ID 未知,随便找一个有效的就行
log(`当前房间 ID 未知,选择第一个获取到的房间: ${url}`);
nextUrl = url;
break;
}
}
if (!nextUrl && roomList.length > 0 && roomList[0].match(/\/(\d+)/)?.[1] !== currentRoomId) {
// 如果循环完还没找到不同的(例如API只返回了当前房间),但列表里还有其他房间,就用第一个
// 这种情况比较少见,除非API有问题或者只有一个房间有活动了
log("未找到与当前不同的房间,但API列表非空,尝试使用列表第一个");
nextUrl = roomList[0];
}
}
if (nextUrl) {
log(`确定下一个房间链接: ${nextUrl}`);
log("准备打开新标签页并关闭当前页...");
try {
GM_openInTab(nextUrl, { active: false, setParent: true });
log(`新标签页打开指令已发送: ${nextUrl}`);
// 立即停止当前页的主循环定时器
if (mainIntervalId) {
clearInterval(mainIntervalId);
mainIntervalId = null;
log("当前页主循环已停止。");
}
await sleep(CLOSE_TAB_DELAY + getRandomDelay(0, 500));
await closeCurrentTab(); // 尝试关闭当前标签页
// closeCurrentTab 之后理论上脚本会停止,下面的代码可能不会执行
isSwitchingRoom = false; // 以防万一,重置状态
} catch (tabError) {
log(`打开或关闭标签页时发生错误: ${tabError.message}`);
if (mainIntervalId) clearInterval(mainIntervalId); // 确保停止
isSwitchingRoom = false; // 出错,重置状态
}
// 如果 closeCurrentTab 成功,脚本会结束
} else {
log("未能从 API 获取到合适的下一个房间链接。可能没有其他活动房间了。停止当前页面脚本。");
if (mainIntervalId) clearInterval(mainIntervalId);
mainIntervalId = null;
isSwitchingRoom = false;
// 可以在这里尝试调用 closeCurrentTab 关闭最后一个标签页
log("尝试关闭这个最后的标签页...");
await sleep(CLOSE_TAB_DELAY);
await closeCurrentTab();
}
} catch (error) {
log(`通过 API 切换房间时发生严重错误: ${error.message}`);
console.error(error);
isSwitchingRoom = false; // 出错重置
// 发生错误时也尝试停止循环
if (mainIntervalId) {
clearInterval(mainIntervalId);
mainIntervalId = null;
log("因错误停止当前页主循环。");
}
}
}
// 主循环 (工作页面)
async function mainLoop() {
// 添加 try...catch 保证循环健壮性
try {
if (isWaitingForPopup || isSwitchingRoom) {
// log("状态繁忙 (等待弹窗或切换中),跳过此次检查。");
return;
}
// log("主循环检查..."); // 减少日志频率,需要时再打开
const redEnvelopeDiv = document.querySelector(SELECTORS.redEnvelopeContainer);
if (!redEnvelopeDiv) {
notFoundCounter++;
// log(`未找到右下角红包区域 (连续次数: ${notFoundCounter})`); // 减少日志频率
if (notFoundCounter === 1) log("首次未找到红包区域,可能已领完或加载中..."); // 第一次未找到时提示
if (notFoundCounter >= LOAD_DELAY_THRESHOLD) {
log(`红包区域连续 ${notFoundCounter} 次未找到,判定为活动结束或页面异常,触发切换房间。`);
notFoundCounter = 0; // 重置计数器
await handleSwitchRoom(); // 调用切换房间函数
}
return; // 未找到则不继续执行本次循环
}
// 如果找到了红包区域,重置未找到计数器
if (notFoundCounter > 0) {
log("重新找到了红包区域。");
notFoundCounter = 0;
}
// 检查红包区域是否可见(有时元素存在但被隐藏)
const style = window.getComputedStyle(redEnvelopeDiv);
if (style.display === 'none' || style.visibility === 'hidden' || redEnvelopeDiv.offsetParent === null) {
// log("红包区域元素存在但不可见,等待下次检查..."); // 减少日志频率
return;
}
// 查找状态显示元素
const statusSpan = redEnvelopeDiv.querySelector(SELECTORS.countdownTimer);
if (statusSpan) {
const statusText = statusSpan.textContent.trim();
if (statusText.includes(':')) { // 包含冒号,认为是倒计时
// log(`等待倒计时: ${statusText}`); // 减少日志频率
} else if (statusText.includes('抢') || statusText.includes('领')) { // "抢红包" 或类似的文字
log(`检测到可点击状态: "${statusText}"`);
// 点击整个红包区域触发弹窗
if (await safeClick(redEnvelopeDiv, "右下角红包区域")) {
// isWaitingForPopup = true; // waitForPopupAndClick 内部会设置
await waitForPopupAndClick(); // 开始等待并处理中间弹窗
// 不论 waitForPopupAndClick 成功与否,本次循环任务完成
} else {
log("尝试点击右下角红包区域失败。");
// 可以选择在这里稍微等待后重试,或者等待下个循环周期
await sleep(getRandomDelay());
}
} else if (statusText === "") {
// log("红包状态文本为空,可能在加载中,等待下次检查..."); // 减少日志频率
}
else {
log(`红包区域状态未知: "${statusText}",等待下次检查...`);
}
} else {
log("警告:在红包区域内找不到状态元素 (span.LiveNewAnchorSupportT-enter--bottom)。");
// 即使找不到状态文字,如果红包区域可见,也可以尝试点击(可选逻辑)
// log("尝试直接点击红包区域...");
// if (await safeClick(redEnvelopeDiv, "右下角红包区域 (无状态文字)")) {
// await waitForPopupAndClick();
// }
}
} catch (error) {
log(`主循环发生未捕获错误: ${error.message}`);
console.error("Main loop error:", error);
// 考虑是否需要停止循环或进行其他错误处理
if (mainIntervalId) {
log("因主循环错误,停止定时器。");
clearInterval(mainIntervalId);
mainIntervalId = null;
// 也可以尝试切换房间
// await handleSwitchRoom();
}
}
}
// --- 脚本初始化 ---
async function initializeScript() {
log("脚本初始化...");
const currentUrl = window.location.href;
if (currentUrl.includes(`/${CONTROL_ROOM_ID}`)) {
log(`当前是控制页面 (${CONTROL_ROOM_ID})。`);
setupLauncherUI();
} else if (currentUrl.match(/douyu\.com\/(\d+)/) || currentUrl.match(/douyu\.com\/topic\/.*rid=(\d+)/)) {
log("当前是工作页面。");
tabStartTime = Date.now(); // <--- 在这里记录启动时间
// **关键改进:等待关键元素出现后再启动主循环**
log(`等待关键元素 "${SELECTORS.criticalElement}" 出现 (最长 ${ELEMENT_WAIT_TIMEOUT / 1000} 秒)...`);
const criticalElement = await findElement(SELECTORS.criticalElement, ELEMENT_WAIT_TIMEOUT);
if (criticalElement) {
log(`关键元素已找到。将在 ${CHECK_INTERVAL}ms 后开始主循环检测...`);
// 确保之前的定时器被清除
if(mainIntervalId) clearInterval(mainIntervalId);
// 稍微延迟启动,给页面更多渲染时间
await sleep(CHECK_INTERVAL);
log("开始主循环...");
mainIntervalId = setInterval(mainLoop, CHECK_INTERVAL);
} else {
log(`等待关键元素超时或未找到。脚本在当前页面可能无法正常工作。`);
// 可以选择尝试关闭标签页
log("尝试关闭此无法正常初始化的标签页...");
await sleep(CLOSE_TAB_DELAY);
await closeCurrentTab();
}
} else {
log("当前页面不是指定的控制页或直播间工作页,脚本不活动。 URL:", currentUrl);
}
}
// --- 启动 ---
// 使用 setTimeout 延迟执行初始化,给页面一些基础加载时间
log(`脚本将在 ${INITIAL_SCRIPT_DELAY}ms 后开始初始化...`);
setTimeout(initializeScript, INITIAL_SCRIPT_DELAY);
})();