斗鱼多房间自动钓鱼脚本,支持数据记录,支持在大赛时间段自动钓鱼,参考小淳大佬的部分代码
// ==UserScript== // @name 斗鱼单窗口多房间钓鱼脚本 // @namespace https://github.com/your_username // @version 1.3 // @description 斗鱼多房间自动钓鱼脚本,支持数据记录,支持在大赛时间段自动钓鱼,参考小淳大佬的部分代码 // @match https://www.douyu.com/pages/fish-act/mine // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== // 配置项 const CONFIG = { CHECK_INTERVAL: 1000,//检查间隔 RETRY_DELAY: 1500,//重试间隔 MIN_OPERATION_INTERVAL:500,//队列间隔 STORAGE_KEYS: { AUTO_FISH: 'ExSave_AutoFish', EVENTAUTO_FISH: 'ExSave_EventAutoFish', FISH_RECORDS: 'ExSave_FishRecords', ERROR_LOGS: 'ExSave_ErrorLogs' }, MAX_RECORDS: 1000, // 每个房间最多保存的记录数 MAX_ERRORS: 100, // 每个房间最多保存的错误数 API_ENDPOINTS: { FISH_INFO: 'https://www.douyu.com/japi/revenuenc/web/actfans/achieve/accList', HOMEPAGE: 'https://www.douyu.com/japi/revenuenc/web/actfans/fishing/homePage', START_FISH: 'https://www.douyu.com/japi/revenuenc/web/actfans/fishing/fishing', END_FISH: 'https://www.douyu.com/japi/revenuenc/web/actfans/fishing/reelIn' } }; // 房间配置 const rids = [ 1031342, 1767547]; //这里设置所有需要钓鱼的房间号 const roomStates = {}; // 初始化房间状态 rids.forEach(rid => { roomStates[rid] = { baitId: null, nextFishEndTime: 0, isFishing: false, timer: null, lock:false }; }); // 工具函数 let lastUpdateTime = null; const utils = { sleep: (time) => new Promise((resolve) => setTimeout(resolve, time)), setCookie: (name, value) => { const exp = new Date(); exp.setTime(exp.getTime() + 3 * 60 * 60 * 1000); document.cookie = `${name}=${escape(value)}; path=/; expires=${exp.toGMTString()}`; }, getCookie: (name) => { const reg = new RegExp(`(^| )${name}=([^;]*)(;|$)`); const arr = document.cookie.match(reg); return arr ? unescape(arr[2]) : null; }, getCCN: () => { let ccn = utils.getCookie("acf_ccn"); //console.log(`获取acf_ccn 数值为: ${ccn}`); if (!ccn) { utils.setCookie("acf_ccn", "1"); ccn = "1"; //console.log(`设置acf_ccn 数值为: ${ccn}`); } return ccn; }, isActivityTime:() => { let now = new Date(); let hour = now.getHours(); let minute = now.getMinutes(); // 判断是否在活动时间范围内(中午 12:00 到凌晨 00:30 每个准点开启半小时) return (hour >= 12 && hour < 24 && minute < 30) || (hour === 0 && minute < 30); }, updateTime:() =>{ // const timePanel = document.getElementById('time-panel'); // const now = new Date(); // timePanel.innerText = "最后检查时间: " + now.toLocaleString(); const timePanel = document.getElementById('time-panel'); const now = new Date(); const currentTime = now.getTime(); // 如果上次更新时间存在 if (lastUpdateTime!== null) { // 计算时间差(毫秒) const timeDiff = currentTime - lastUpdateTime; // 如果时间差大于1秒 if (timeDiff > 1500) { // 保存当前时间 const errorLogDiv = document.getElementById('error-log'); errorLogDiv.innerText += `Error: 时间间隔大于1秒${timeDiff},上次更新时间:${new Date(lastUpdateTime).toLocaleString()}\n`; // // 延迟1秒后更新时间(可根据需要调整延迟时间) // setTimeout(() => { timePanel.innerText = "最后检查时间: " + now.toLocaleString(); lastUpdateTime = now.getTime(); // }, 1000); } else { // 正常更新时间 timePanel.innerText = "最后检查时间: " + now.toLocaleString(); lastUpdateTime = now.getTime(); } } else { // 首次更新时间 timePanel.innerText = "最后检查时间: " + now.toLocaleString(); lastUpdateTime = now.getTime(); } } }; // 1. 首先添加一个请求队列管理器类 class RequestQueueManager { constructor() { this.queue = []; this.isProcessing = false; } // 添加请求到队列 async addRequest(requestFn, priority = 0) { return new Promise((resolve, reject) => { this.queue.push({ requestFn, priority, resolve, reject, timestamp: Date.now() }); // 按优先级和时间戳排序 this.queue.sort((a, b) => { if (a.priority !== b.priority) { return b.priority - a.priority; } return a.timestamp - b.timestamp; }); this.processQueue(); }); } // 处理队列 async processQueue() { if (this.isProcessing || this.queue.length === 0) return; this.isProcessing = true; try { const request = this.queue.shift(); const r###lt = await request.requestFn(); request.resolve(r###lt); } catch (error) { const request = this.queue[0]; request.reject(error); } finally { this.isProcessing = false; // 添加延迟以控制请求频率 await utils.sleep(CONFIG.MIN_OPERATION_INTERVAL); // 继续处理队列中的下一个请求 if (this.queue.length > 0) { this.processQueue(); } } } // 清空队列 clearQueue() { this.queue = []; } // 获取队列长度 get length() { return this.queue.length; } } class ApiManager { constructor(queueManager) { this.queueManager = queueManager; } async request(url, options = {}, priority = 0) { return this.queueManager.addRequest(async () => { try { const defaultOptions = { mode: "no-cors", cache: "default", credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded", } }; const response = await fetch(url, { ...defaultOptions, ...options, headers: { ...defaultOptions.headers, ...options.headers } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error(`API请求失败: ${url}`, error); return { error: -1, msg: `请求失败: ${error.message}` }; } }, priority); } async getFishInfo(rid) { return this.request( `${CONFIG.API_ENDPOINTS.FISH_INFO}?rid=${rid}&type=1&period=1`, {}, 1 ).then(response => response.data?.accList || []); } async getHomepageData(rid) { return this.request( `${CONFIG.API_ENDPOINTS.HOMEPAGE}?rid=${rid}&opt=1`, {}, 1 ); } async startFish(rid, baitId) { return this.request( CONFIG.API_ENDPOINTS.START_FISH, { method: "POST", body: `ctn=${utils.getCCN()}&rid=${rid}&baitId=${baitId}` }, 2 ); } async endFish(rid) { return this.request( CONFIG.API_ENDPOINTS.END_FISH, { method: "POST", body: `ctn=${utils.getCCN()}&rid=${rid}` }, 2 ); } } // 数据存储管理 class StorageManager { constructor() { this.initializeStorage(); } initializeStorage() { if (!localStorage.getItem(CONFIG.STORAGE_KEYS.FISH_RECORDS)) { localStorage.setItem(CONFIG.STORAGE_KEYS.FISH_RECORDS, JSON.stringify({})); } if (!localStorage.getItem(CONFIG.STORAGE_KEYS.ERROR_LOGS)) { localStorage.setItem(CONFIG.STORAGE_KEYS.ERROR_LOGS, JSON.stringify({})); } } saveFishRecord(rid, record) { const records = this.getFishRecords(); if (!records[rid]) records[rid] = []; records[rid].unshift({ ...record, timestamp: new Date().toISOString() }); // 限制记录数量 if (records[rid].length > CONFIG.MAX_RECORDS) { records[rid] = records[rid].slice(0, CONFIG.MAX_RECORDS); } localStorage.setItem(CONFIG.STORAGE_KEYS.FISH_RECORDS, JSON.stringify(records)); } saveErrorLog(rid, error) { const errors = this.getErrorLogs(); if (!errors[rid]) errors[rid] = []; errors[rid].unshift({ error, timestamp: new Date().toISOString() }); // 限制错误日志数量 if (errors[rid].length > CONFIG.MAX_ERRORS) { errors[rid] = errors[rid].slice(0, CONFIG.MAX_ERRORS); } localStorage.setItem(CONFIG.STORAGE_KEYS.ERROR_LOGS, JSON.stringify(errors)); } getFishRecords() { return JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.FISH_RECORDS) || '{}'); } getErrorLogs() { return JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.ERROR_LOGS) || '{}'); } clearRecords(rid) { const records = this.getFishRecords(); if (rid) { delete records[rid]; } else { Object.keys(records).forEach(key => delete records[key]); } localStorage.setItem(CONFIG.STORAGE_KEYS.FISH_RECORDS, JSON.stringify(records)); } clearErrors(rid) { const errors = this.getErrorLogs(); if (rid) { delete errors[rid]; } else { Object.keys(errors).forEach(key => delete errors[key]); } localStorage.setItem(CONFIG.STORAGE_KEYS.ERROR_LOGS, JSON.stringify(errors)); } } class FishingManager { constructor(storageManager, apiManager) { this.fishInfo = []; this.storageManager = storageManager; this.apiManager = apiManager; } async init() { try { // 获取鱼类信息 this.fishInfo = await this.apiManager.getFishInfo(rids[0]); // 检查初始状态 return this.checkInitialState(); } catch (error) { console.error('初始化失败:', error); this.storageManager.saveErrorLog('system', '初始化失败: ' + error.message); return false; } } async checkInitialState() { try { for (const rid of rids) { const homepageRes = await this.apiManager.getHomepageData(rid); if (!homepageRes.data) { console.error(`【房间 ${rid}】未能获取活动信息`); this.storageManager.saveErrorLog(rid, '未能获取活动信息'); return false; } // 检查鱼饵 const baitData = homepageRes.data.baits.find(item => item.inUse); if (!baitData) { console.error(`【房间 ${rid}】请设置鱼饵`); this.storageManager.saveErrorLog(rid, '请设置鱼饵'); return false; } roomStates[rid].baitId = baitData.id; // 检查形象 if (!homepageRes.data.myCh) { console.error(`【房间 ${rid}】请设置形象`); this.storageManager.saveErrorLog(rid, '请设置形象'); return false; } // 检查钓鱼状态 await this.handleFishingState(rid, homepageRes.data.fishing); } return true; } catch (error) { console.error('检查初始状态失败:', error); this.storageManager.saveErrorLog('system', '检查初始状态失败: ' + error.message); return false; } } async handleFishingState(rid, fishingData) { const state = roomStates[rid]; switch(fishingData.stat) { case 0: // 未开始 state.isFishing = false; state.nextFishEndTime = 0; break; case 1: // 进行中 state.isFishing = true; state.nextFishEndTime = fishingData.fishEtMs; break; case 2: // 未收杆 await this.endFishing(rid); await utils.sleep(CONFIG.RETRY_DELAY); break; } } async startFishing(rid) { const eventAutoFish = JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.EVENTAUTO_FISH))?.isAutoFish; if (eventAutoFish) { if (!utils.isActivityTime()) { //console.log("不在活动时间内,不执行钓鱼操作"); return false; } } try { const state = roomStates[rid]; state.lock = true; const fishRes = await this.apiManager.startFish(rid, state.baitId); state.lock = false; if (fishRes.error !== 0) { const errorMsg = `【startFishing 函数:房间 ${rid}】${fishRes.msg}`; console.error(errorMsg); this.storageManager.saveErrorLog(rid, errorMsg); //1001007 操作失败刷新重试 // if (fishRes.error === 1001007) await this.endFishing(rid); // if (fishRes.error === 1001007) { // setTimeout(async () => { // await this.endFishing(rid); // }, 1000);//1秒后重试 // }; 不需要重试 定时器会自动重试 if (fishRes.error === 1005003) this.stopFishing(rid); return false; } state.isFishing = true; state.nextFishEndTime = fishRes.data.fishing.fishEtMs; return true; } catch (error) { console.error(`【startFishing 函数:房间 ${rid}】开始钓鱼失败:`, error); this.storageManager.saveErrorLog(rid, '开始钓鱼失败: ' + error.message); return false; } } async endFishing(rid) { try { const state = roomStates[rid]; state.lock = true; const fishRes = await this.apiManager.endFish(rid); state.lock = false; if (fishRes.error !== 0) { const errorMsg = `endFishing 函数1:房间 ${rid} 收杆失败: ${fishRes.msg || JSON.stringify(fishRes)}`; console.error(errorMsg); this.storageManager.saveErrorLog(rid, errorMsg); const homepageRes = await this.apiManager.getHomepageData(rid); if (homepageRes.data?.fishing.stat === 0) { state.isFishing = false; state.nextFishEndTime = 0; } return; } this.logFishingR###lt(rid, fishRes); state.isFishing = false; } catch (error) { console.error(`【endFishing 函数2:房间 ${rid}】收杆失败:`, error); this.storageManager.saveErrorLog(rid, '收杆失败: ' + error.message); } } logFishingR###lt(rid, fishRes) { try { const record = { fishId: fishRes.data.fish.id, weight: fishRes.data.fish.wei, awards: fishRes.data.awards || [] }; const fishData = this.fishInfo.find(item => item.fishId === record.fishId); if (fishData) { record.fishName = fishData.name; } // 保存记录到存储 this.storageManager.saveFishRecord(rid, record); // 控制台输出 const messages = [`【房间 ${rid} 钓鱼】`]; if (fishData) { messages.push(`获得${fishData.name}${record.weight}斤`); } if (record.awards.length > 0) { const awards = record.awards.map( award => `获得${award.awardName}x${award.awardNumShow}` ); messages.push(...awards); } if (messages.length > 1) { console.log(messages.join(fishData ? "," : "")); } } catch (error) { console.error(`【房间 ${rid}】记录结果失败:`, error); this.storageManager.saveErrorLog(rid, '记录结果失败: ' + error.message); } } startAutoFishing(rid) { const state = roomStates[rid]; state.timer = setInterval(async () => { utils.updateTime(); try { if(!state.lock) { if (state.isFishing) { const now = new Date().getTime(); if (now <= state.nextFishEndTime) return; // console.log(`${rid} 476开始收杆`); await this.endFishing(rid); } else { // console.log(`${rid} 479开始抛竿`); await this.startFishing(rid); } } } catch (error) { console.error(`【startautoFishing 函数:房间 ${rid}】自动钓鱼异常:`, error); this.storageManager.saveErrorLog(rid, '自动钓鱼异常: ' + error.message); } }, CONFIG.CHECK_INTERVAL); } stopFishing(rid) { if (roomStates[rid].timer) { clearInterval(roomStates[rid].timer); roomStates[rid].timer = null; } } stopAllFishing() { rids.forEach(rid => this.stopFishing(rid)); } } // UI管理器增强 class UIManager { constructor(fishingManager, storageManager) { this.fishingManager = fishingManager; this.storageManager = storageManager; this.createUI(); this.loadSavedState(); } createUI() { // 创建控制面板容器 const controlPanel = document.createElement('div'); Object.assign(controlPanel.style, { position: 'fixed', top: '50px', right: '20px', zIndex: '9999', backgroundColor: '#f0f0f0', padding: '10px', border: '1px solid #ccc', borderRadius: '5px' }); controlPanel.innerHTML = ` <label> <input id="extool__autofish_start" type="checkbox" style="margin-top:5px;"> 无限自动钓鱼 </label> <label> <input id="extool__eventautofish_start" type="checkbox" style="margin-top:10px;"> 大赛时间段自动钓鱼 </label> <button id="extool__show_records" style="margin-left: 10px;">显示记录</button> <div id="time-panel"></div> <div id="error-log"></div> `; document.body.appendChild(controlPanel); // 创建记录查看窗口 const recordsWindow = document.createElement('div'); Object.assign(recordsWindow.style, { display: 'none', position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', zIndex: '10000', backgroundColor: 'white', padding: '20px', border: '1px solid #ccc', borderRadius: '5px', boxShadow: '0 2px 10px rgba(0,0,0,0.1)', maxHeight: '80vh', width: '80vw', overflowY: 'auto' }); recordsWindow.innerHTML = ` <div style="display: flex; justify-content: space-between; margin-bottom: 15px;"> <select id="extool__room_select" style="padding: 5px;"> <option value="">选择房间</option> ${rids.map(rid => `<option value="${rid}">${rid}</option>`).join('')} </select> <div> <button id="extool__clear_records">清除记录</button> <button id="extool__close_records" style="margin-left: 10px;">关闭</button> </div> </div> <div id="extool__records_content"></div> `; document.body.appendChild(recordsWindow); this.bindEvents(recordsWindow); } bindEvents(recordsWindow) { // 绑定自动钓鱼开关事件 const checkbox = document.getElementById("extool__autofish_start"); const eventcheckbox = document.getElementById("extool__eventautofish_start"); checkbox.addEventListener("click", async () => { const isStart = checkbox.checked; this.saveState(isStart); if (!isStart) { this.fishingManager.stopAllFishing(); return; }else{ //如果checked 取消另一个checkbox勾选 this.saveEventState(!isStart); eventcheckbox.checked = false; } console.log("【自动钓鱼】开始自动钓鱼"); const initialized = await this.fishingManager.init(); if (!initialized) { checkbox.checked = false; return; } //对每个房间开启计时器检查 rids.forEach(rid => this.fishingManager.startAutoFishing(rid)); }); // 绑定自动钓鱼开关事件 eventcheckbox.addEventListener("click", async () => { const isStart = eventcheckbox.checked; this.saveEventState(isStart); if (!isStart) { this.fishingManager.stopAllFishing(); return; }else{ //如果checked 取消另一个checkbox勾选 this.saveState(!isStart); checkbox.checked = false; } console.log("【大赛时间段自动钓鱼】开始自动钓鱼"); const initialized = await this.fishingManager.init(); if (!initialized) { eventcheckbox.checked = false; return; } rids.forEach(rid => this.fishingManager.startAutoFishing(rid)); }); // 绑定显示记录按钮事件 document.getElementById("extool__show_records").addEventListener("click", () => { recordsWindow.style.display = 'block'; this.updateRecordsDisplay(); }); // 绑定关闭按钮事件 document.getElementById("extool__close_records").addEventListener("click", () => { recordsWindow.style.display = 'none'; }); // 绑定房间选择事件 document.getElementById("extool__room_select").addEventListener("change", () => { this.updateRecordsDisplay(); }); // 绑定清除记录按钮事件 document.getElementById("extool__clear_records").addEventListener("click", () => { const rid = document.getElementById("extool__room_select").value; this.storageManager.clearRecords(rid); this.storageManager.clearErrors(rid); this.updateRecordsDisplay(); }); } updateRecordsDisplay() { const rid = document.getElementById("extool__room_select").value; const records = this.storageManager.getFishRecords(); const errors = this.storageManager.getErrorLogs(); const content = document.getElementById("extool__records_content"); let html = ''; if (rid) { // 显示特定房间的记录 html += '<h3>钓鱼记录</h3>'; if (records[rid] && records[rid].length > 0) { html += this.generateRecordsTable(records[rid]); } else { html += '<p>暂无钓鱼记录</p>'; } html += '<h3>错误日志</h3>'; if (errors[rid] && errors[rid].length > 0) { html += this.generateErrorsTable(errors[rid]); } else { html += '<p>暂无错误记录</p>'; } } else { // 显示所有房间的统计信息 html += '<h3>房间统计</h3>'; html += this.generateStatsTable(records, errors); } content.innerHTML = html; } generateRecordsTable(records) { return ` <table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;"> <thead> <tr> <th style="border: 1px solid #ddd; padding: 8px;">时间</th> <th style="border: 1px solid #ddd; padding: 8px;">鱼类</th> <th style="border: 1px solid #ddd; padding: 8px;">重量</th> <th style="border: 1px solid #ddd; padding: 8px;">奖励</th> </tr> </thead> <tbody> ${records.map(record => ` <tr> <td style="border: 1px solid #ddd; padding: 8px;">${new Date(record.timestamp).toLocaleString()}</td> <td style="border: 1px solid #ddd; padding: 8px;">${record.fishName || '未知'}</td> <td style="border: 1px solid #ddd; padding: 8px;">${record.weight}斤</td> <td style="border: 1px solid #ddd; padding: 8px;"> ${record.awards.map(award => `${award.awardName}x${award.awardNum}` ).join(', ') || '无'} </td> </tr> `).join('')} </tbody> </table> `; } generateErrorsTable(errors) { return ` <table style="width: 100%; border-collapse: collapse;"> <thead> <tr> <th style="border: 1px solid #ddd; padding: 8px;">时间</th> <th style="border: 1px solid #ddd; padding: 8px;">错误信息</th> </tr> </thead> <tbody> ${errors.map(error => ` <tr> <td style="border: 1px solid #ddd; padding: 8px;">${new Date(error.timestamp).toLocaleString()}</td><td style="border: 1px solid #ddd; padding: 8px;">${error.error}</td> </tr> `).join('')} </tbody> </table> `; } generateStatsTable(records, errors) { return ` <table style="width: 100%; border-collapse: collapse;"> <thead> <tr> <th style="border: 1px solid #ddd; padding: 8px;">房间号</th> <th style="border: 1px solid #ddd; padding: 8px;">钓鱼次数</th> <th style="border: 1px solid #ddd; padding: 8px;">总重量</th> <th style="border: 1px solid #ddd; padding: 8px;">错误次数</th> <th style="border: 1px solid #ddd; padding: 8px;">最后活动</th> </tr> </thead> <tbody> ${rids.map(rid => { const roomRecords = records[rid] || []; const roomErrors = errors[rid] || []; const totalWeight = roomRecords.reduce((sum, record) => sum + parseFloat(record.weight), 0); const lastActivity = [...roomRecords, ...roomErrors] .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))[0]?.timestamp; return ` <tr> <td style="border: 1px solid #ddd; padding: 8px;">${rid}</td> <td style="border: 1px solid #ddd; padding: 8px;">${roomRecords.length}</td> <td style="border: 1px solid #ddd; padding: 8px;">${totalWeight.toFixed(1)}斤</td> <td style="border: 1px solid #ddd; padding: 8px;">${roomErrors.length}</td> <td style="border: 1px solid #ddd; padding: 8px;"> ${lastActivity ? new Date(lastActivity).toLocaleString() : '无记录'} </td> </tr> `; }).join('')} </tbody> </table> `; } saveState(isAutoFish) { localStorage.setItem(CONFIG.STORAGE_KEYS.AUTO_FISH, JSON.stringify({ isAutoFish })); } saveEventState(isAutoFish) { localStorage.setItem(CONFIG.STORAGE_KEYS.EVENTAUTO_FISH, JSON.stringify({ isAutoFish })); } loadSavedState() { const saved = localStorage.getItem(CONFIG.STORAGE_KEYS.AUTO_FISH); if (saved) { const { isAutoFish } = JSON.parse(saved); if (isAutoFish) { document.getElementById("extool__autofish_start").click(); } } const eventSaved = localStorage.getItem(CONFIG.STORAGE_KEYS.EVENTAUTO_FISH); if (eventSaved) { const { isAutoFish } = JSON.parse(eventSaved); if (isAutoFish) { document.getElementById("extool__eventautofish_start").click(); } } } } // 初始化 (() => { const queueManager = new RequestQueueManager(); const apiManager = new ApiManager(queueManager); const storageManager = new StorageManager(); const fishingManager = new FishingManager(storageManager, apiManager); new UIManager(fishingManager, storageManager); })();