// ==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 // @downloadURL https://update.greasyfork.icu/scripts/532514/%E6%96%97%E9%B1%BC%E5%85%A8%E6%B0%91%E6%98%9F%E6%8E%A8%E8%8D%90%E8%87%AA%E5%8A%A8%E9%A2%86%E5%8F%96.user.js // @updateURL https://update.greasyfork.icu/scripts/532514/%E6%96%97%E9%B1%BC%E5%85%A8%E6%B0%91%E6%98%9F%E6%8E%A8%E8%8D%90%E8%87%AA%E5%8A%A8%E9%A2%86%E5%8F%96.meta.js // ==/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 = '正在查找... (已开: ' + openedRoomIds.size + ')'; 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 = '再打开一个 (已开: ' + openedRoomIds.size + ')'; // 更新按钮文字和计数 await sleep(OPEN_TAB_DELAY); // 短暂延迟 } catch (e) { log(`打开标签页 ${foundNewUrl} 时出错: ${e.message}`); // 打开失败,不应该将 rid 加入列表,按钮文字也应该恢复 startButton.innerHTML = '打开出错,重试? (已开: ' + openedRoomIds.size + ')'; } } else { log("在 API 列表中未能找到新的、未打开的房间。"); startButton.innerHTML = '无新房间可开 (已开: ' + openedRoomIds.size + ')'; // 这里可以选择让按钮保持禁用,或者几秒后恢复 await sleep(2000); // 等待 2 秒后恢复按钮文字 if (!startButton.disabled) { // 再次检查,以防期间状态改变 startButton.innerHTML = '再打开一个 (已开: ' + openedRoomIds.size + ')'; } } } catch (error) { log(`查找或打开房间时发生错误: ${error.message}`); startButton.innerHTML = '查找出错,重试? (已开: ' + openedRoomIds.size + ')'; } 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 = '打开一个房间 (已开: 0)'; // <-- 修改按钮文字和添加计数显示 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); })();