Greasy Fork

来自缓存

Greasy Fork is available in English.

斗鱼单窗口多房间钓鱼脚本

斗鱼多房间自动钓鱼脚本,支持数据记录,支持在大赛时间段自动钓鱼,参考小淳大佬的部分代码

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 result = await request.requestFn();
            request.resolve(result);
        } 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.logFishingResult(rid, fishRes);
            state.isFishing = false;
        } catch (error) {
            console.error(`【endFishing 函数2:房间 ${rid}】收杆失败:`, error);
            this.storageManager.saveErrorLog(rid, '收杆失败: ' + error.message);
        }
    }

    logFishingResult(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);
})();