Greasy Fork 支持简体中文。

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

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

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