Greasy Fork

Greasy Fork is available in English.

NodeSeek 私信优化脚本

NodeSeek 私信记录本地缓存与WebDAV备份

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         NodeSeek 私信优化脚本
// @namespace    https://www.nodeseek.com/
// @version      1.0.0
// @description  NodeSeek 私信记录本地缓存与WebDAV备份
// @author       yuyan
// @match        https://www.nodeseek.com/notification*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      www.nodeseek.com
// @connect      dav.jianguoyun.com
// @connect      *
// ==/UserScript==

(function() {
    'use strict';

    /**
     * 工具函数集合
     */
    const Utils = {
        // Debug开关,设置为false可以减少日志输出,true显示详细日志
        DEBUG: false,

        /**
         * 格式化日期为文件名安全的字符串
         * @param {Date} date - 要格式化的日期对象
         * @returns {string} 格式化后的日期字符串
         */
        formatDate(date) {
            return date.toISOString().replace(/[:.]/g, '-').slice(0, -5) + '-' + Date.now().toString().slice(-6);
        },

        /**
         * 将UTC时间字符串转换为本地时间字符串
         * @param {string} utcString - UTC时间字符串
         * @returns {string} 本地时间字符串
         */
        parseUTCToLocal(utcString) {
            return new Date(utcString).toLocaleString();
        },

        /**
         * 记录日志信息
         * @param {string} message - 日志消息
         * @param {string} type - 日志类型,默认为'info'
         */
        log(message, type = 'info') {
            if (!this.DEBUG && type === 'info') return;
            const typeStr = typeof type === 'string' ? type.toUpperCase() : 'INFO';
            console.log(`[NodeSeek私信优化] ${typeStr}: ${message}`);
        },

        /**
         * 记录调试信息(只在DEBUG模式下显示)
         * @param {string} message - 调试消息
         * @param {*} data - 可选的数据对象
         */
        debug(message, data = null) {
            if (!this.DEBUG) return;
            if (data !== null) {
                console.log(`[NodeSeek私信优化] DEBUG: ${message}`, data);
            } else {
                console.log(`[NodeSeek私信优化] DEBUG: ${message}`);
            }
        },

        /**
         * 记录错误信息
         * @param {string} message - 错误消息
         * @param {Error|null} error - 错误对象,可选
         */
        error(message, error = null) {
            console.error(`[NodeSeek私信优化] ERROR: ${message}`, error);
        },

        /**
         * 开启调试模式
         */
        enableDebug() {
            this.DEBUG = true;
            console.log('[NodeSeek私信优化] 调试模式已开启');
        },

        /**
         * 关闭调试模式
         */
        disableDebug() {
            this.DEBUG = false;
            console.log('[NodeSeek私信优化] 调试模式已关闭');
        }
    };

    /**
     * IndexedDB 数据存储模块
     * 用于本地存储聊天记录数据
     */
    class ChatDB {
        /**
         * 构造函数
         * @param {number} userId - 用户ID
         */
        constructor(userId) {
            this.userId = userId;
            this.dbName = `nodeseek_chat_${userId}`;
            this.version = 1;
            this.db = null;
        }

        /**
         * 初始化数据库连接
         * @returns {Promise<void>}
         */
        async init() {
            return new Promise((resolve, reject) => {
                const request = indexedDB.open(this.dbName, this.version);

                request.onerror = () => reject(request.error);
                request.onsuccess = () => {
                    this.db = request.result;
                    resolve();
                };

                request.onupgradeneeded = (event) => {
                    const db = event.target.result;

                    if (!db.objectStoreNames.contains('talk_messages')) {
                        const store = db.createObjectStore('talk_messages', { keyPath: 'member_id' });
                        store.createIndex('created_at', 'created_at', { unique: false });
                    }

                    if (!db.objectStoreNames.contains('metadata')) {
                        db.createObjectStore('metadata', { keyPath: 'key' });
                    }
                };
            });
        }

        /**
         * 保存聊天消息数据
         * @param {Object} memberData - 成员聊天数据
         * @returns {Promise<void>}
         */
        async saveTalkMessage(memberData) {
            const transaction = this.db.transaction(['talk_messages'], 'readwrite');
            const store = transaction.objectStore('talk_messages');

            return new Promise((resolve, reject) => {
                const request = store.put(memberData);
                request.onsuccess = () => resolve();
                request.onerror = () => reject(request.error);
            });
        }

        /**
         * 获取指定成员的聊天消息
         * @param {number} memberId - 成员ID
         * @returns {Promise<Object|undefined>} 聊天消息数据
         */
        async getTalkMessage(memberId) {
            const transaction = this.db.transaction(['talk_messages'], 'readonly');
            const store = transaction.objectStore('talk_messages');

            return new Promise((resolve, reject) => {
                const request = store.get(memberId);
                request.onsuccess = () => resolve(request.result);
                request.onerror = () => reject(request.error);
            });
        }

        /**
         * 获取所有聊天消息
         * @returns {Promise<Array>} 所有聊天消息数组
         */
        async getAllTalkMessages() {
            const transaction = this.db.transaction(['talk_messages'], 'readonly');
            const store = transaction.objectStore('talk_messages');

            return new Promise((resolve, reject) => {
                const request = store.getAll();
                request.onsuccess = () => resolve(request.result);
                request.onerror = () => reject(request.error);
            });
        }

        /**
         * 设置元数据
         * @param {string} key - 键名
         * @param {*} value - 值
         * @returns {Promise<void>}
         */
        async setMetadata(key, value) {
            const transaction = this.db.transaction(['metadata'], 'readwrite');
            const store = transaction.objectStore('metadata');

            return new Promise((resolve, reject) => {
                const request = store.put({ key, value });
                request.onsuccess = () => resolve();
                request.onerror = () => reject(request.error);
            });
        }

        /**
         * 获取元数据
         * @param {string} key - 键名
         * @returns {Promise<*>} 元数据值
         */
        async getMetadata(key) {
            const transaction = this.db.transaction(['metadata'], 'readonly');
            const store = transaction.objectStore('metadata');

            return new Promise((resolve, reject) => {
                const request = store.get(key);
                request.onsuccess = () => resolve(request.result?.value);
                request.onerror = () => reject(request.error);
            });
        }
    }

    /**
     * NodeSeek API 访问模块
     * 用于与NodeSeek网站API进行交互
     */
    class NodeSeekAPI {
        /**
         * 构造函数
         */
        constructor() {
            this.baseUrl = 'https://www.nodeseek.com/api';
        }

        /**
         * 发送HTTP请求
         * @param {string} url - 请求URL
         * @param {Object} options - 请求选项
         * @returns {Promise<Object>} 响应数据
         */
        async request(url, options = {}) {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    headers: {
                        'Accept': 'application/json',
                        'Referer': 'https://www.nodeseek.com/',
                        ...options.headers
                    },
                    onload: (response) => {
                        try {
                            Utils.debug(`API响应状态: ${response.status}`);
                            Utils.debug(`API响应内容: ${response.responseText.substring(0, 200)}...`);

                            if (response.status !== 200) {
                                reject(new Error(`HTTP错误: ${response.status} ${response.statusText}`));
                                return;
                            }

                            const data = JSON.parse(response.responseText);
                            if (data.status === 404 && data.message === "USER NOT FOUND") {
                                reject(new Error('用户未登录'));
                                return;
                            }
                            resolve(data);
                        } catch (e) {
                            Utils.error(`响应解析失败,原始响应: ${response.responseText}`, e);
                            reject(new Error(`响应解析失败: ${e.message}`));
                        }
                    },
                    onerror: (error) => reject(error),
                    ontimeout: () => reject(new Error('请求超时'))
                });
            });
        }

        /**
         * 获取当前用户ID
         * @returns {Promise<number>} 用户ID
         */
        async getUserId() {
            try {
                Utils.debug('正在获取用户ID...');
                const data = await this.request(`${this.baseUrl}/notification/message/with/5230`);
                Utils.debug('getUserId API响应:', data);

                if (data.success && data.msgArray && data.msgArray.length > 0) {
                    const userId = data.msgArray[0].receiver_id;
                    Utils.log(`获取到用户ID: ${userId}`);
                    return userId;
                }

                Utils.error('API响应格式不正确或无数据', data);
                throw new Error('无法获取用户ID: API响应格式不正确');
            } catch (error) {
                Utils.error('获取用户ID失败', error);
                throw error;
            }
        }

        /**
         * 获取与指定用户的聊天消息
         * @param {number} userId - 用户ID
         * @returns {Promise<Object>} 聊天消息数据
         */
        async getChatMessages(userId) {
            const data = await this.request(`${this.baseUrl}/notification/message/with/${userId}`);
            return data;
        }

        /**
         * 获取消息列表
         * @returns {Promise<Object>} 消息列表数据
         */
        async getMessageList() {
            const data = await this.request(`${this.baseUrl}/notification/message/list`);
            return data;
        }
    }

    /**
     * WebDAV 备份模块
     * 用于将聊天记录备份到WebDAV服务器
     */
    class WebDAVBackup {
        /**
         * 构造函数
         * @param {number} userId - 用户ID
         */
        constructor(userId) {
            this.userId = userId;
            this.configKey = `webdav_config_${userId}`;
        }

        /**
         * 获取WebDAV配置
         * @returns {Object|null} WebDAV配置对象
         */
        getConfig() {
            const config = GM_getValue(this.configKey, null);
            return config ? JSON.parse(config) : null;
        }

        /**
         * 保存WebDAV配置
         * @param {Object} config - WebDAV配置对象
         */
        saveConfig(config) {
            GM_setValue(this.configKey, JSON.stringify(config));
        }

        /**
         * 构建完整的WebDAV URL
         * @param {string} path - 文件路径
         * @returns {string} 完整的URL
         */
        buildFullUrl(path) {
            const config = this.getConfig();
            if (!config) {
                throw new Error('WebDAV配置未设置');
            }

            Utils.debug(`buildFullUrl 输入参数: path="${path}"`);
            Utils.debug(`WebDAV配置: serverUrl="${config.serverUrl}", backupPath="${config.backupPath}"`);

            // 如果path已经是完整的URL,直接返回
            if (path.startsWith('http://') || path.startsWith('https://')) {
                Utils.debug(`path是完整URL,直接返回: ${path}`);
                return path;
            }

            const serverBase = config.serverUrl.replace(/\/$/, '');
            Utils.debug(`处理后的serverBase: "${serverBase}"`);

            // 如果path是绝对路径(以/开头),需要检查是否与serverUrl重复
            if (path.startsWith('/')) {
                // 检查serverUrl是否已经包含了path的开头部分
                const serverPath = new URL(serverBase).pathname;
                Utils.debug(`serverUrl的路径部分: "${serverPath}"`);

                // 如果path已经包含了serverUrl的路径部分,避免重复
                if (path.startsWith(serverPath) && serverPath !== '/') {
                    const result = `${new URL(serverBase).origin}${path}`;
                    Utils.debug(`避免路径重复,拼接结果: ${result}`);
                    return result;
                } else {
                    const result = `${serverBase}${path}`;
                    Utils.debug(`path是绝对路径,拼接结果: ${result}`);
                    return result;
                }
            }

            // 如果path是相对路径,需要拼接备份目录
            // 注意:确保不会重复路径部分
            const backupBase = config.backupPath.replace(/^\/+|\/+$/g, ''); // 去除首尾的斜杠
            const fileName = path.replace(/^\/+/, ''); // 去除开头的斜杠

            Utils.debug(`处理后的backupBase: "${backupBase}"`);
            Utils.debug(`处理后的fileName: "${fileName}"`);

            const result = `${serverBase}/${backupBase}/${fileName}`;
            Utils.debug(`最终拼接结果: ${result}`);

            return result;
        }

        async ensureDirectoryExists(directoryPath) {
            const config = this.getConfig();
            if (!config) {
                throw new Error('WebDAV 配置未设置');
            }

            const url = `${config.serverUrl.replace(/\/$/, '')}${directoryPath.replace(/\/$/, '')}/`;

            return new Promise((resolve, reject) => {
                // 首先检查目录是否存在
                GM_xmlhttpRequest({
                    method: 'PROPFIND',
                    url: url,
                    headers: {
                        'Authorization': `Basic ${btoa(`${config.username}:${config.password}`)}`,
                        'Depth': '0'
                    },
                    onload: (response) => {
                        if (response.status >= 200 && response.status < 300) {
                            // 目录已存在
                            Utils.log(`目录已存在: ${directoryPath}`);
                            resolve();
                        } else if (response.status === 404) {
                            // 目录不存在,尝试创建
                            Utils.log(`目录不存在,正在创建: ${directoryPath}`);
                            GM_xmlhttpRequest({
                                method: 'MKCOL',
                                url: url,
                                headers: {
                                    'Authorization': `Basic ${btoa(`${config.username}:${config.password}`)}`
                                },
                                onload: (createResponse) => {
                                    if (createResponse.status >= 200 && createResponse.status < 300) {
                                        Utils.log(`目录创建成功: ${directoryPath}`);
                                        resolve();
                                    } else {
                                        reject(new Error(`创建目录失败: ${createResponse.status} ${createResponse.statusText}`));
                                    }
                                },
                                onerror: (error) => reject(new Error(`创建目录网络错误: ${error?.message || '未知错误'}`))
                            });
                        } else {
                            reject(new Error(`检查目录失败: ${response.status} ${response.statusText}`));
                        }
                    },
                    onerror: (error) => reject(new Error(`检查目录网络错误: ${error?.message || '未知错误'}`))
                });
            });
        }

        async uploadBackup(data, retryCount = 0) {
            const config = this.getConfig();
            if (!config) {
                throw new Error('WebDAV 配置未设置');
            }

            try {
                // 确保备份目录存在
                await this.ensureDirectoryExists(config.backupPath);
            } catch (error) {
                throw new Error(`确保目录存在失败: ${error.message}`);
            }

            const filename = `nodeseek_chat_backup_${Utils.formatDate(new Date())}.json`;
            const url = `${config.serverUrl.replace(/\/$/, '')}${config.backupPath.replace(/\/$/, '')}/${filename}`;

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'PUT',
                    url: url,
                    headers: {
                        'Authorization': `Basic ${btoa(`${config.username}:${config.password}`)}`,
                        'Content-Type': 'application/json'
                    },
                    data: JSON.stringify(data),
                    onload: async (response) => {
                        if (response.status >= 200 && response.status < 300) {
                            resolve(filename);
                        } else if (response.status === 409) {
                            if (retryCount < 3) {
                                // 409冲突错误,可能是目录不存在或文件冲突,等待一段时间后重试
                                Utils.log(`备份冲突 (${response.status}),${1000 * (retryCount + 1)}ms后重试 (${retryCount + 1}/3)`);
                                setTimeout(async () => {
                                    try {
                                        const result = await this.uploadBackup(data, retryCount + 1);
                                        resolve(result);
                                    } catch (error) {
                                        reject(error);
                                    }
                                }, 1000 * (retryCount + 1));
                            } else {
                                // 重试次数用完,提供更详细的错误信息
                                reject(new Error(`备份失败: 目录可能不存在或权限不足 (${response.status})。请检查WebDAV配置和目录权限。`));
                            }
                        } else {
                            const errorMsg = `备份失败: ${response.status} ${response.statusText}`;
                            reject(new Error(errorMsg));
                        }
                    },
                    onerror: (error) => reject(new Error(`备份上传网络错误: ${error?.message || '未知错误'}`))
                });
            });
        }

        async listBackups() {
            const config = this.getConfig();
            if (!config) return [];

            const url = `${config.serverUrl.replace(/\/$/, '')}${config.backupPath.replace(/\/$/, '')}/`;

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'PROPFIND',
                    url: url,
                    headers: {
                        'Authorization': `Basic ${btoa(`${config.username}:${config.password}`)}`,
                        'Depth': '1'
                    },
                    onload: (response) => {
                        if (response.status >= 200 && response.status < 300) {
                            Utils.debug(`备份列表响应: ${response.responseText.substring(0, 500)}...`);

                            // 解析WebDAV响应,提取备份文件列表
                            const parser = new DOMParser();
                            const doc = parser.parseFromString(response.responseText, 'text/xml');
                            const files = Array.from(doc.querySelectorAll('response'))
                                .map(response => {
                                    const href = response.querySelector('href')?.textContent;
                                    const lastModified = response.querySelector('getlastmodified')?.textContent;

                                    Utils.debug(`找到文件: href=${href}, lastModified=${lastModified}`);

                                    return { href, lastModified };
                                })
                                .filter(file => {
                                    const isBackupFile = file.href && file.href.includes('nodeseek_chat_backup_');
                                    Utils.debug(`文件过滤: ${file.href} -> ${isBackupFile}`);
                                    return isBackupFile;
                                })
                                .sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified));

                            Utils.debug(`最终备份文件列表: ${files.length} 个文件`);
                            resolve(files);
                        } else {
                            Utils.debug(`获取备份列表失败: ${response.status} - ${response.responseText}`);
                            reject(new Error(`获取备份列表失败: ${response.status}`));
                        }
                    },
                    onerror: (error) => reject(new Error(`获取备份列表网络错误: ${error?.message || '未知错误'}`))
                });
            });
        }

        async cleanOldBackups() {
            try {
                const backups = await this.listBackups();
                if (backups.length > 30) {
                    const config = this.getConfig();
                    const toDelete = backups.slice(30);

                    for (const backup of toDelete) {
                        const deleteUrl = this.buildFullUrl(backup.href);
                        await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: 'DELETE',
                                url: deleteUrl,
                                headers: {
                                    'Authorization': `Basic ${btoa(`${config.username}:${config.password}`)}`
                                },
                                onload: () => resolve(),
                                onerror: (error) => reject(new Error(`删除备份文件网络错误: ${error?.message || '未知错误'}`))
                            });
                        });
                    }
                }
            } catch (error) {
                Utils.error('清理旧备份失败', error);
            }
        }
    }

    /**
     * UI 管理模块
     * 负责用户界面的创建和管理
     */
    class UIManager {
        /**
         * 构造函数
         */
        constructor() {
            this.modals = new Set();
            this.stylesLoaded = false;
            this.talkListObserver = null;
            this.lastTalkListPresent = false;
        }

        /**
         * 检测私信页面出现/消失的回调
         * @param {boolean} isPresent - 私信页面是否存在
         */
        onMessagePageChange(isPresent) {
            if (isPresent) {
                Utils.debug('私信页面出现了');
                this.addHistoryButton();
            } else {
                Utils.debug('私信页面消失了');
                this.removeHistoryButton();
            }
        }

        /**
         * 检查私信页面状态
         */
        checkMessagePage() {
            const appSwitch = document.querySelector('.app-switch');
            const messageLink = appSwitch?.querySelector('a[href="#/message?mode=list"]');
            const isMessagePage = messageLink?.classList.contains('router-link-active');

            if (isMessagePage !== this.lastTalkListPresent) {
                this.lastTalkListPresent = isMessagePage;
                this.onMessagePageChange(isMessagePage);
            }
        }

        /**
         * 初始化私信页面监听器
         */
        initTalkListObserver() {
            if (this.talkListObserver) {
                this.talkListObserver.disconnect();
            }

            this.talkListObserver = new MutationObserver(() => {
                this.checkMessagePage();
            });

            this.talkListObserver.observe(document.body, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['class']
            });

            this.checkMessagePage();
        }

        /**
         * 确保样式已加载
         */
        ensureStylesLoaded() {
            if (this.stylesLoaded || document.querySelector('#nodeseek-modal-styles')) {
                this.stylesLoaded = true;
                return;
            }

            const styles = document.createElement('style');
            styles.id = 'nodeseek-modal-styles';
            styles.textContent = `
                .nodeseek-modal {
                    position: fixed;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    z-index: 10000;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                }
                .nodeseek-modal-overlay {
                    position: absolute;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    background: rgba(0, 0, 0, 0.5);
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    padding: 20px;
                    box-sizing: border-box;
                }
                .nodeseek-modal-content {
                    background: white;
                    border-radius: 8px;
                    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
                    overflow: hidden;
                    width: 100%;
                    display: flex;
                    flex-direction: column;
                }
                .nodeseek-modal-header {
                    padding: 16px 20px;
                    border-bottom: 1px solid #e0e0e0;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    background: #f8f9fa;
                }
                .nodeseek-modal-header h3 {
                    margin: 0;
                    font-size: 18px;
                    color: #333;
                }
                .nodeseek-modal-close {
                    background: none;
                    border: none;
                    font-size: 24px;
                    cursor: pointer;
                    color: #666;
                    padding: 0;
                    width: 30px;
                    height: 30px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    border-radius: 4px;
                }
                .nodeseek-modal-close:hover {
                    background: #e0e0e0;
                    color: #333;
                }
                .nodeseek-modal-body {
                    padding: 20px;
                    overflow-y: auto;
                    flex: 1;
                }
                .nodeseek-btn {
                    background: #007bff;
                    color: white;
                    border: none;
                    padding: 8px 16px;
                    border-radius: 4px;
                    cursor: pointer;
                    font-size: 14px;
                    margin: 0 4px;
                    transition: background 0.2s;
                }
                .nodeseek-btn:hover {
                    background: #0056b3;
                }
                .nodeseek-btn-secondary {
                    background: #6c757d;
                }
                .nodeseek-btn-secondary:hover {
                    background: #545b62;
                }
                .nodeseek-btn-success {
                    background: #28a745;
                }
                .nodeseek-btn-success:hover {
                    background: #1e7e34;
                }
                .nodeseek-form-group {
                    margin-bottom: 16px;
                }
                .nodeseek-form-group label {
                    display: block;
                    margin-bottom: 4px;
                    font-weight: 500;
                    color: #333;
                }
                .nodeseek-form-group input, .nodeseek-form-group textarea {
                    width: 100%;
                    padding: 8px 12px;
                    border: 1px solid #ddd;
                    border-radius: 4px;
                    font-size: 14px;
                    box-sizing: border-box;
                }
                .nodeseek-form-group input:focus, .nodeseek-form-group textarea:focus {
                    outline: none;
                    border-color: #007bff;
                    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
                }
                .nodeseek-chat-item {
                    display: flex;
                    align-items: center;
                    padding: 12px;
                    border-bottom: 1px solid #e0e0e0;
                    transition: background 0.2s;
                }
                .nodeseek-chat-item:hover {
                    background: #f8f9fa;
                }
                .nodeseek-chat-avatar {
                    width: 40px;
                    height: 40px;
                    border-radius: 50%;
                    margin-right: 12px;
                    object-fit: cover;
                }
                .nodeseek-chat-info {
                    flex: 1;
                    min-width: 0;
                }
                .nodeseek-chat-name {
                    font-weight: 500;
                    color: #333;
                    margin-bottom: 4px;
                }
                .nodeseek-chat-message {
                    color: #666;
                    font-size: 14px;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                }
                .nodeseek-chat-time {
                    color: #999;
                    font-size: 12px;
                    margin-left: 12px;
                    white-space: nowrap;
                }
                .nodeseek-chat-actions {
                    margin-left: 12px;
                }
                .nodeseek-history-btn {
                    display: inline-block;
                    background: #007bff;
                    color: white;
                    border: none;
                    padding: 8px 12px;
                    border-radius: 4px;
                    cursor: pointer;
                    font-size: 12px;
                    margin-left: 8px;
                    text-decoration: none;
                    vertical-align: middle;
                    transition: all 0.2s;
                    line-height: 1.2;
                }
                .nodeseek-history-btn:hover {
                    background: #0056b3;
                    color: white;
                    text-decoration: none;
                }
            `;
            document.head.appendChild(styles);
            this.stylesLoaded = true;
        }

        /**
         * 创建模态框
         * @param {string} title - 模态框标题
         * @param {string} content - 模态框内容HTML
         * @param {Object} options - 选项配置
         * @returns {HTMLElement} 模态框元素
         */
        createModal(title, content, options = {}) {
            const modal = document.createElement('div');
            modal.className = 'nodeseek-modal';
            modal.innerHTML = `
                <div class="nodeseek-modal-overlay">
                    <div class="nodeseek-modal-content" style="max-width: ${options.width || '600px'}; max-height: ${options.height || '80vh'};">
                        <div class="nodeseek-modal-header">
                            <h3>${title}</h3>
                            <button class="nodeseek-modal-close">&times;</button>
                        </div>
                        <div class="nodeseek-modal-body">
                            ${content}
                        </div>
                    </div>
                </div>
            `;

            // 确保样式已加载
            this.ensureStylesLoaded();

            // 事件处理
            const closeBtn = modal.querySelector('.nodeseek-modal-close');
            const overlay = modal.querySelector('.nodeseek-modal-overlay');

            const closeModal = () => {
                modal.remove();
                this.modals.delete(modal);
            };

            closeBtn.addEventListener('click', closeModal);
            overlay.addEventListener('click', (e) => {
                if (e.target === overlay) closeModal();
            });

            document.body.appendChild(modal);
            this.modals.add(modal);

            return modal;
        }

        /**
         * 显示WebDAV配置对话框
         * @param {WebDAVBackup} webdavBackup - WebDAV备份实例
         * @param {Function} onSave - 保存回调函数
         */
        showWebDAVConfig(webdavBackup, onSave) {
            const config = webdavBackup.getConfig() || {};

            const content = `
                <div class="nodeseek-form-group">
                    <label>服务器地址</label>
                    <input type="url" id="webdav-server" value="${config.serverUrl || ''}" placeholder="https://dav.jianguoyun.com/dav/">
                </div>
                <div class="nodeseek-form-group">
                    <label>用户名</label>
                    <input type="text" id="webdav-username" value="${config.username || ''}" placeholder="用户名">
                </div>
                <div class="nodeseek-form-group">
                    <label>密码</label>
                    <input type="password" id="webdav-password" value="${config.password || ''}" placeholder="密码">
                </div>
                <div class="nodeseek-form-group">
                    <label>备份路径</label>
                    <input type="text" id="webdav-path" value="${config.backupPath || '/nodeseek_messages_backup/'}" placeholder="/nodeseek_messages_backup/">
                </div>
                <div style="text-align: right; margin-top: 20px;">
                    <button class="nodeseek-btn nodeseek-btn-secondary" id="webdav-cancel">取消</button>
                    <button class="nodeseek-btn nodeseek-btn-success" id="webdav-save">保存</button>
                </div>
            `;

            const modal = this.createModal('WebDAV 配置', content);

            modal.querySelector('#webdav-cancel').addEventListener('click', () => modal.remove());
            modal.querySelector('#webdav-save').addEventListener('click', () => {
                const newConfig = {
                    serverUrl: modal.querySelector('#webdav-server').value.trim(),
                    username: modal.querySelector('#webdav-username').value.trim(),
                    password: modal.querySelector('#webdav-password').value.trim(),
                    backupPath: modal.querySelector('#webdav-path').value.trim()
                };

                if (!newConfig.serverUrl || !newConfig.username || !newConfig.password) {
                    alert('请填写完整的配置信息');
                    return;
                }

                webdavBackup.saveConfig(newConfig);
                modal.remove();
                if (onSave) onSave();
            });
        }

        /**
         * 显示历史聊天记录
         * @param {Array} chatData - 聊天数据数组
         * @param {boolean} showLatest - 是否显示最新聊天,默认false
         * @returns {HTMLElement} 模态框元素
         */
        showHistoryChats(chatData, showLatest = false) {
            const sortedChats = chatData
                .filter(chat => showLatest || !chat.isLatest)
                .sort((a, b) => new Date(b.created_at) - new Date(a.created_at));

            let content = `
                <div style="margin-bottom: 16px; display: flex; gap: 8px; flex-wrap: wrap;">
                    <button class="nodeseek-btn" id="webdav-config-btn">WebDAV设置</button>
                    <button class="nodeseek-btn nodeseek-btn-success" id="backup-now-btn">立即备份</button>
                    <button class="nodeseek-btn nodeseek-btn-secondary" id="restore-btn">从WebDAV恢复</button>
                    <label style="display: flex; align-items: center; margin-left: auto;">
                        <input type="checkbox" id="show-latest-toggle" ${showLatest ? 'checked' : ''} style="margin-right: 4px;">
                        显示最新聊天
                    </label>
                </div>
                <div style="max-height: 400px; overflow-y: auto;">
            `;

            if (sortedChats.length === 0) {
                content += '<div style="text-align: center; color: #666; padding: 40px;">暂无历史聊天记录</div>';
            } else {
                sortedChats.forEach(chat => {
                    const avatarUrl = `https://www.nodeseek.com/avatar/${chat.member_id}.png`;
                    const chatUrl = `https://www.nodeseek.com/notification#/message?mode=talk&to=${chat.member_id}`;
                    const timeStr = Utils.parseUTCToLocal(chat.created_at);

                    content += `
                        <div class="nodeseek-chat-item">
                            <img class="nodeseek-chat-avatar" src="${avatarUrl}" alt="${chat.member_name}" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMjAiIGZpbGw9IiNlMGUwZTAiLz4KPHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4PSI4IiB5PSI4Ij4KPHBhdGggZD0iTTEyIDEyQzE0LjIwOTEgMTIgMTYgMTAuMjA5MSAxNiA4QzE2IDUuNzkwODYgMTQuMjA5MSA0IDEyIDRDOS43OTA4NiA0IDggNS43OTA4NiA4IDhDOCAxMC4yMDkxIDkuNzkwODYgMTIgMTIgMTJaIiBmaWxsPSIjOTk5Ii8+CjxwYXRoIGQ9Ik0xMiAxNEM5LjMzIDEzLjk5IDcuMDEgMTUuNjIgNiAxOEMxMC4wMSAyMCAxMy45OSAyMCAxOCAxOEMxNi45OSAxNS42MiAxNC42NyAxMy45OSAxMiAxNFoiIGZpbGw9IiM5OTkiLz4KPC9zdmc+Cjwvc3ZnPgo='">
                            <div class="nodeseek-chat-info">
                                <div class="nodeseek-chat-name">${chat.member_name} (ID: ${chat.member_id})</div>
                                <div class="nodeseek-chat-message">${chat.content.replace(/<[^>]*>/g, '').substring(0, 50)}${chat.content.length > 50 ? '...' : ''}</div>
                            </div>
                            <div class="nodeseek-chat-time">${timeStr}</div>
                            <div class="nodeseek-chat-actions">
                                <a href="${chatUrl}" target="_blank" class="nodeseek-btn" style="text-decoration: none; font-size: 12px; padding: 4px 8px;">打开对话</a>
                            </div>
                        </div>
                    `;
                });
            }

            content += '</div>';

            return this.createModal('历史聊天记录', content, { width: '800px', height: '600px' });
        }

        /**
         * 添加历史聊天按钮
         */
        addHistoryButton() {
            this.ensureStylesLoaded();

            const existingBtn = document.querySelector('.nodeseek-history-btn');
            if (existingBtn) existingBtn.remove();

            const appSwitch = document.querySelector('.app-switch');
            const messageLink = appSwitch?.querySelector('a[href="#/message?mode=list"]');

            if (!appSwitch || !messageLink) {
                Utils.debug('app-switch 或私信链接元素不存在,无法添加按钮');
                return;
            }

            const btn = document.createElement('a');
            btn.className = 'nodeseek-history-btn';
            btn.textContent = '历史私信';
            btn.href = 'javascript:void(0)';
            btn.addEventListener('click', (e) => {
                e.preventDefault();
                window.chatBackup?.showHistoryChats();
            });

            // 将按钮插入到私信链接后面
            messageLink.insertAdjacentElement('afterend', btn);
            Utils.debug('历史聊天按钮已添加到私信链接后面');
        }

        /**
         * 移除历史聊天按钮
         */
        removeHistoryButton() {
            const btn = document.querySelector('.nodeseek-history-btn');
            if (btn) btn.remove();
        }

        /**
         * 显示提示消息
         * @param {string} message - 提示消息内容
         * @param {string} type - 消息类型:'success', 'error', 'warning', 'info'
         * @param {number} duration - 显示持续时间(毫秒),默认3000
         */
        showToast(message, type = 'success', duration = 3000) {
            // 移除已存在的提示
            const existingToast = document.querySelector('.nodeseek-toast');
            if (existingToast) existingToast.remove();

            const toast = document.createElement('div');
            toast.className = 'nodeseek-toast';

            const bgColor = type === 'success' ? '#28a745' :
                           type === 'error' ? '#dc3545' :
                           type === 'warning' ? '#ffc107' : '#007bff';

            toast.style.cssText = `
                position: fixed;
                top: 20px;
                right: 20px;
                background: ${bgColor};
                color: white;
                padding: 12px 20px;
                border-radius: 6px;
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
                z-index: 10001;
                font-size: 14px;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                max-width: 300px;
                word-wrap: break-word;
                opacity: 0;
                transform: translateX(100%);
                transition: all 0.3s ease;
            `;

            toast.textContent = message;
            document.body.appendChild(toast);

            // 显示动画
            setTimeout(() => {
                toast.style.opacity = '1';
                toast.style.transform = 'translateX(0)';
            }, 10);

            // 自动消失
            setTimeout(() => {
                toast.style.opacity = '0';
                toast.style.transform = 'translateX(100%)';
                setTimeout(() => {
                    if (toast.parentNode) {
                        toast.remove();
                    }
                }, 300);
            }, duration);
        }
    }

    /**
     * 主控制模块
     * 负责协调各个模块的工作
     */
    class ChatBackup {
        /**
         * 构造函数
         */
        constructor() {
            this.api = new NodeSeekAPI();
            this.db = null;
            this.webdav = null;
            this.ui = new UIManager();
            this.userId = null;
            this.backupTimer = null;
            this.lastHash = '';
            this.showLatestChats = GM_getValue('show_latest_chats', false);
        }

        /**
         * 初始化应用
         * @returns {Promise<void>}
         */
        async init() {
            try {
                Utils.debug('开始初始化脚本...');

                // 检查是否在正确的域名
                if (window.location.hostname !== 'www.nodeseek.com') {
                    Utils.debug('不在NodeSeek域名,跳过初始化');
                    return;
                }

                // 获取用户ID
                this.userId = await this.api.getUserId();

                // 初始化数据库和WebDAV
                this.db = new ChatDB(this.userId);
                await this.db.init();

                this.webdav = new WebDAVBackup(this.userId);

                // 设置定时备份
                this.setupAutoBackup();

                // 监听页面变化
                this.setupPageListener();

                // 注册菜单命令
                this.registerMenuCommands();

                // 处理当前页面
                this.handlePageChange();

                // 初始化talk-list监听器
                this.ui.initTalkListObserver();

                Utils.log('NodeSeek私信优化脚本初始化完成');
            } catch (error) {
                Utils.error('初始化失败', error);
                // 显示用户友好的错误提示
                if (error.message.includes('用户未登录')) {
                    console.warn('[NodeSeek私信优化] 请先登录NodeSeek账户');
                } else if (error.message.includes('响应解析失败')) {
                    console.warn('[NodeSeek私信优化] 网络请求失败,请检查网络连接或稍后重试');
                } else {
                    console.warn('[NodeSeek私信优化] 初始化失败,请刷新页面重试');
                }
            }
        }

        /**
         * 设置自动备份
         */
        setupAutoBackup() {
            this.backupTimer = setInterval(() => {
                this.performBackup();
            }, 6 * 60 * 60 * 1000);

            document.addEventListener('visibilitychange', () => {
                if (!document.hidden) {
                    const lastBackup = GM_getValue(`last_backup_${this.userId}`, 0);
                    const now = Date.now();
                    if (now - lastBackup > 6 * 60 * 60 * 1000) {
                        this.performBackup();
                    }
                }
            });
        }

        /**
         * 设置页面监听器
         */
        setupPageListener() {
            window.addEventListener('hashchange', () => {
                this.handlePageChange();
            });

            const observer = new MutationObserver(() => {
                if (window.location.hash !== this.lastHash) {
                    this.lastHash = window.location.hash;
                    this.handlePageChange();
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        /**
         * 处理页面变化
         * @returns {Promise<void>}
         */
        async handlePageChange() {
            const hash = window.location.hash;
            this.lastHash = hash;

            try {
                if (hash.includes('mode=talk&to=')) {
                    const match = hash.match(/to=(\d+)/);
                    if (match) {
                        const targetUserId = parseInt(match[1]);
                        await this.handleChatPage(targetUserId);
                    }
                } else if (hash.includes('mode=list')) {
                    await this.handleMessageListPage();
                }
            } catch (error) {
                Utils.error('页面处理失败', error);
            }
        }

        /**
         * 处理聊天页面
         * @param {number} targetUserId - 目标用户ID
         * @returns {Promise<void>}
         */
        async handleChatPage(targetUserId) {
            try {
                const response = await this.api.getChatMessages(targetUserId);
                if (response.success && response.msgArray && response.msgArray.length > 0) {
                    const latestMessage = response.msgArray[response.msgArray.length - 1];
                    const talkTo = response.talkTo;

                    const chatData = {
                        member_id: talkTo.member_id,
                        member_name: talkTo.member_name,
                        content: latestMessage.content,
                        created_at: latestMessage.created_at,
                        sender_id: latestMessage.sender_id,
                        receiver_id: latestMessage.receiver_id,
                        message_id: latestMessage.id,
                        viewed: latestMessage.viewed,
                        updated_at: new Date().toISOString()
                    };

                    // 检查是否需要更新
                    const existingData = await this.db.getTalkMessage(talkTo.member_id);
                    if (!existingData || existingData.created_at !== latestMessage.created_at) {
                        await this.db.saveTalkMessage(chatData);
                        Utils.log(`更新聊天记录: ${talkTo.member_name}`);
                        this.performBackup();
                    }
                }
            } catch (error) {
                if (error.message === '用户未登录') {
                    Utils.log('用户未登录,停止操作');
                    return;
                }
                Utils.error('处理聊天页面失败', error);
            }
        }

        /**
         * 处理消息列表页面
         * @returns {Promise<void>}
         */
        async handleMessageListPage() {
            try {
                const response = await this.api.getMessageList();
                if (response.success && response.msgArray) {
                    let hasUpdates = false;
                    const currentChatUserIds = new Set();

                    for (const msg of response.msgArray) {
                        // 判断聊天对象
                        let chatUserId, chatUserName;
                        if (msg.sender_id === this.userId) {
                            chatUserId = msg.receiver_id;
                            chatUserName = msg.receiver_name;
                        } else {
                            chatUserId = msg.sender_id;
                            chatUserName = msg.sender_name;
                        }

                        currentChatUserIds.add(chatUserId);

                        const chatData = {
                            member_id: chatUserId,
                            member_name: chatUserName,
                            content: msg.content,
                            created_at: msg.created_at,
                            sender_id: msg.sender_id,
                            receiver_id: msg.receiver_id,
                            message_id: msg.max_id,
                            viewed: msg.viewed,
                            updated_at: new Date().toISOString(),
                            isLatest: true
                        };

                        // 检查是否需要更新
                        const existingData = await this.db.getTalkMessage(chatUserId);
                        if (!existingData || existingData.created_at !== msg.created_at) {
                            await this.db.saveTalkMessage(chatData);
                            hasUpdates = true;
                        }
                    }

                    // 更新其他聊天记录的isLatest标记
                    const allChats = await this.db.getAllTalkMessages();
                    for (const chat of allChats) {
                        if (!currentChatUserIds.has(chat.member_id) && chat.isLatest) {
                            chat.isLatest = false;
                            await this.db.saveTalkMessage(chat);
                        }
                    }

                    if (hasUpdates) {
                        this.performBackup();
                    }
                }
            } catch (error) {
                if (error.message === '用户未登录') {
                    Utils.log('用户未登录,停止操作');
                    return;
                }
                Utils.error('处理消息列表页面失败', error);
            }
        }

        /**
         * 执行备份操作
         * @returns {Promise<void>}
         */
        async performBackup() {
            try {
                const config = this.webdav.getConfig();
                if (!config) {
                    Utils.log('WebDAV未配置,跳过备份');
                    throw new Error('WebDAV未配置');
                }

                const allChats = await this.db.getAllTalkMessages();
                const metadata = {
                    userId: this.userId,
                    backupTime: new Date().toISOString(),
                    totalChats: allChats.length
                };

                const backupData = {
                    metadata,
                    chats: allChats
                };

                const filename = await this.webdav.uploadBackup(backupData);
                await this.webdav.cleanOldBackups();

                GM_setValue(`last_backup_${this.userId}`, Date.now());
                Utils.log(`备份完成: ${filename}`);
            } catch (error) {
                Utils.error('备份失败', error);
                throw error;
            }
        }

        /**
         * 清空所有聊天数据
         * @returns {Promise<void>}
         */
        async clearAllChatData() {
            try {
                const transaction = this.db.db.transaction(['talk_messages'], 'readwrite');
                const store = transaction.objectStore('talk_messages');

                return new Promise((resolve, reject) => {
                    const request = store.clear();
                    request.onsuccess = () => {
                        Utils.debug('所有聊天记录已清空');
                        resolve();
                    };
                    request.onerror = () => reject(request.error);
                });
            } catch (error) {
                Utils.error('清空聊天数据失败', error);
                throw error;
            }
        }

        /**
         * 显示历史聊天记录
         * @returns {Promise<void>}
         */
        async showHistoryChats() {
            try {
                const allChats = await this.db.getAllTalkMessages();
                const modal = this.ui.showHistoryChats(allChats, this.showLatestChats);

                // 绑定事件
                modal.querySelector('#webdav-config-btn').addEventListener('click', () => {
                    this.ui.showWebDAVConfig(this.webdav, () => {
                        Utils.log('WebDAV配置已保存');
                        this.ui.showToast('WebDAV配置已保存');
                        this.performBackup();
                    });
                });

                modal.querySelector('#backup-now-btn').addEventListener('click', async () => {
                    try {
                        await this.performBackup();
                        this.ui.showToast('备份完成');
                    } catch (error) {
                        this.ui.showToast('备份失败: ' + error.message, 'error');
                    }
                });

                modal.querySelector('#restore-btn').addEventListener('click', () => {
                    this.showRestoreOptions();
                });

                modal.querySelector('#show-latest-toggle').addEventListener('change', (e) => {
                    this.showLatestChats = e.target.checked;
                    GM_setValue('show_latest_chats', this.showLatestChats);
                    this.ui.showToast(e.target.checked ? '已显示最新聊天' : '已隐藏最新聊天');
                    modal.remove();
                    this.showHistoryChats();
                });

            } catch (error) {
                Utils.error('显示历史聊天失败', error);
            }
        }

        /**
         * 显示恢复选项
         * @returns {Promise<void>}
         */
        async showRestoreOptions() {
            try {
                Utils.debug('正在获取备份列表...');
                const backups = await this.webdav.listBackups();

                if (backups.length === 0) {
                    this.ui.showToast('没有找到备份文件', 'warning');
                    return;
                }

                Utils.debug(`找到 ${backups.length} 个备份文件`);

                let content = `
                    <div style="margin-bottom: 16px; padding: 12px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; font-size: 14px;">
                        <strong>⚠️ 重要提示:</strong>恢复操作会<strong>完全覆盖</strong>现有的本地聊天数据,原有数据将被删除且无法恢复!
                    </div>
                    <div style="max-height: 300px; overflow-y: auto;">
                `;

                backups.forEach((backup, index) => {
                    const date = new Date(backup.lastModified).toLocaleString();
                    const fileName = backup.href.split('/').pop();
                    content += `
                        <div style="padding: 12px; border-bottom: 1px solid #eee; cursor: pointer; transition: background 0.2s;"
                             data-backup="${backup.href}"
                             onmouseover="this.style.background='#f8f9fa'"
                             onmouseout="this.style.background='transparent'">
                            <div style="font-weight: 500; margin-bottom: 4px;">备份 ${index + 1}</div>
                            <div style="font-size: 12px; color: #666; margin-bottom: 2px;">时间: ${date}</div>
                            <div style="font-size: 11px; color: #999;">文件: ${fileName}</div>
                        </div>
                    `;
                });
                content += '</div>';

                const modal = this.ui.createModal('选择要恢复的备份', content, { width: '500px' });

                modal.querySelectorAll('[data-backup]').forEach(item => {
                    item.addEventListener('click', async () => {
                        const backupPath = item.dataset.backup;
                        const fileName = backupPath.split('/').pop();

                        // 确认对话框
                        if (confirm(`⚠️ 确定要恢复备份文件 "${fileName}" 吗?\n\n警告:此操作会完全覆盖现有的本地聊天数据!\n原有数据将被永久删除且无法恢复!\n\n请确认您真的要继续此操作。`)) {
                            modal.remove();

                            // 显示恢复进度
                            this.ui.showToast('正在恢复备份,请稍候...', 'info', 10000);

                            try {
                                await this.restoreFromBackup(backupPath);
                            } catch (error) {
                                Utils.error('恢复过程中出错', error);
                            }
                        }
                    });
                });

            } catch (error) {
                Utils.error('获取备份列表失败', error);

                let errorMessage = '获取备份列表失败';
                if (error.message.includes('401')) {
                    errorMessage = 'WebDAV认证失败,请检查用户名和密码';
                } else if (error.message.includes('403')) {
                    errorMessage = 'WebDAV权限不足,请检查账户权限';
                } else if (error.message.includes('404')) {
                    errorMessage = 'WebDAV备份目录不存在';
                } else if (error.message.includes('网络')) {
                    errorMessage = '网络连接失败,请检查网络连接';
                }

                this.ui.showToast(errorMessage, 'error', 5000);
            }
        }

        /**
         * 从备份恢复数据
         * @param {string} backupPath - 备份文件路径
         * @returns {Promise<void>}
         */
        async restoreFromBackup(backupPath) {
            try {
                const config = this.webdav.getConfig();
                if (!config) {
                    throw new Error('WebDAV配置未设置');
                }

                // 构建正确的URL
                const url = this.webdav.buildFullUrl(backupPath);
                Utils.debug(`正在从以下URL恢复备份: ${url}`);

                const response = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        headers: {
                            'Authorization': `Basic ${btoa(`${config.username}:${config.password}`)}`,
                            'Accept': 'application/json'
                        },
                        onload: (response) => {
                            Utils.debug(`恢复请求响应状态: ${response.status}`);
                            Utils.debug(`恢复请求响应头: ${response.responseHeaders}`);

                            if (response.status >= 200 && response.status < 300) {
                                try {
                                    const data = JSON.parse(response.responseText);
                                    resolve(data);
                                } catch (parseError) {
                                    Utils.error(`解析备份文件失败: ${parseError.message}`);
                                    Utils.debug(`原始响应内容: ${response.responseText.substring(0, 500)}`);
                                    reject(new Error(`备份文件格式错误: ${parseError.message}`));
                                }
                            } else {
                                let errorMessage = `HTTP错误 ${response.status}`;

                                // 针对不同的HTTP状态码提供更具体的错误信息
                                switch (response.status) {
                                    case 401:
                                        errorMessage = '认证失败,请检查WebDAV用户名和密码';
                                        break;
                                    case 403:
                                        errorMessage = '权限不足,无法访问备份文件';
                                        break;
                                    case 404:
                                        errorMessage = '备份文件不存在或已被删除';
                                        break;
                                    case 409:
                                        errorMessage = '文件访问冲突,请稍后重试';
                                        break;
                                    case 500:
                                        errorMessage = 'WebDAV服务器内部错误';
                                        break;
                                    default:
                                        errorMessage = `服务器返回错误: ${response.status} ${response.statusText}`;
                                }

                                Utils.debug(`详细错误信息: ${response.responseText}`);
                                reject(new Error(errorMessage));
                            }
                        },
                        onerror: (error) => {
                            Utils.error('网络请求失败', error);
                            reject(new Error('网络连接失败,请检查网络连接'));
                        },
                        ontimeout: () => {
                            reject(new Error('请求超时,请稍后重试'));
                        },
                        timeout: 30000 // 30秒超时
                    });
                });

                if (response && response.chats && Array.isArray(response.chats)) {
                    Utils.debug(`开始恢复 ${response.chats.length} 条聊天记录`);

                    // 完全覆盖模式:先清空现有数据
                    Utils.debug('清空现有聊天记录...');
                    await this.clearAllChatData();

                    let successCount = 0;
                    for (const chat of response.chats) {
                        try {
                            await this.db.saveTalkMessage(chat);
                            successCount++;
                        } catch (dbError) {
                            Utils.error(`保存聊天记录失败 (ID: ${chat.member_id})`, dbError);
                        }
                    }

                    const message = `恢复完成,已覆盖本地数据,共恢复 ${successCount} 条聊天记录`;
                    Utils.log(message);
                    this.ui.showToast(message);
                } else {
                    throw new Error('备份文件格式不正确或不包含聊天数据');
                }

            } catch (error) {
                Utils.error('恢复备份失败', error);

                // 显示用户友好的错误提示
                let userMessage = error.message;
                if (error.message.includes('409') || error.message.includes('冲突')) {
                    userMessage = '文件访问冲突,请稍后重试。如果问题持续存在,请检查WebDAV服务器状态。';
                }

                this.ui.showToast(`恢复失败: ${userMessage}`, 'error', 5000);
            }
        }

        /**
         * 注册菜单命令
         */
        registerMenuCommands() {
            GM_registerMenuCommand('WebDAV 配置', () => {
                this.ui.showWebDAVConfig(this.webdav, () => {
                    Utils.log('WebDAV配置已保存');
                    this.ui.showToast('WebDAV配置已保存');
                    this.performBackup();
                });
            });

            GM_registerMenuCommand('立即备份', async () => {
                try {
                    await this.performBackup();
                    this.ui.showToast('备份完成');
                } catch (error) {
                    this.ui.showToast('备份失败: ' + error.message, 'error');
                }
            });

            GM_registerMenuCommand('历史聊天记录', () => {
                this.showHistoryChats();
            });
        }
    }

    /**
     * 全局变量
     */
    let chatBackup;

    /**
     * 初始化脚本
     */
    function initScript() {
        try {
            Utils.debug('脚本开始加载...');

            if (window.location.hostname !== 'www.nodeseek.com') {
                Utils.debug('不在NodeSeek域名,脚本不会运行');
                return;
            }

            chatBackup = new ChatBackup();
            window.chatBackup = chatBackup;

            if (document.readyState === 'loading') {
                Utils.debug('等待DOM加载完成...');
                document.addEventListener('DOMContentLoaded', () => {
                    Utils.debug('DOM加载完成,1秒后开始初始化');
                    setTimeout(() => chatBackup.init(), 1000);
                });
            } else {
                Utils.debug('DOM已加载,1秒后开始初始化');
                setTimeout(() => chatBackup.init(), 1000);
            }
        } catch (error) {
            Utils.error('脚本初始化失败', error);
        }
    }

    /**
     * 启动脚本
     */
    initScript();

})();