复活玩机器鱼吧

在指定关鱼吧(5496243)页面加载/发帖/评论/回复/点赞 (学习用)

// ==UserScript==
// @name         复活玩机器鱼吧
// @namespace    http://tampermonkey.net/
// @version      1.11
// @description  在指定关鱼吧(5496243)页面加载/发帖/评论/回复/点赞 (学习用)
// @author       ysl
// @match        https://yuba.douyu.com/discussion/5496243/posts*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_cookie
// @connect      yuba.douyu.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置常量 ---
    const API_FEED_LIST_URL = 'https://yuba.douyu.com/wgapi/yubanc/api/feed/groupTabFeedList'; // 获取帖子列表 API
    const API_VERIFY_URL = 'https://yuba.douyu.com/wgapi/yubanc/api/feed/publishFeedRiskVerify'; // 发帖风险验证 API
    const API_PUBLISH_URL = 'https://yuba.douyu.com/wgapi/yubanc/api/feed/publish'; // 发布帖子 API
    const API_COMMENT_LIST_URL = 'https://yuba.douyu.com/wgapi/yubanc/api/comment/list'; // 获取评论列表 API
    const API_COMMENT_SEND_URL = 'https://yuba.douyu.com/wgapi/yubanc/api/comment/send'; // 发送评论 API
    const API_REPLY_SEND_URL = 'https://yuba.douyu.com/wgapi/yubanc/api/reply/send'; // 发送回复 API
    const API_LIKE_URL = 'https://yuba.douyu.com/wgapi/yubanc/api/user/like'; // 点赞 API
    const API_UNLIKE_URL = 'https://yuba.douyu.com/wgapi/yubanc/api/user/unlike'; // 取消点赞 API
    const TARGET_GROUP_ID = '5496243'; // 目标鱼吧 ID
    const POST_LIMIT = 30; // 每次加载帖子的数量
    const COMMENT_LIMIT = 10; // 每次加载评论的数量
    const CSRF_COOKIE_NAME = 'acf_yb_t'; // CSRF Token 的 Cookie 名称

    // SVG 图标定义
    const ICONS = {
        like: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.987 1.77C6.057-.017 3.17.098 1.369 2.036-.442 3.983-.379 7.159 1.105 9.328c1.507 2.203 3.995 4.385 5.53 5.626.79.639 1.903.627 2.683-.024 1.494-1.247 3.922-3.42 5.54-5.613 1.578-2.136 1.546-5.355-.257-7.289S9.915-.018 7.987 1.771M2.249 2.854c1.355-1.456 3.481-1.535 4.92-.2l.818.757.817-.758c1.438-1.334 3.563-1.258 4.918.196 1.367 1.466 1.44 4.034.17 5.755-1.53 2.071-3.864 4.168-5.345 5.405a.9.9 0 0 1-1.156.012c-1.524-1.233-3.893-3.322-5.294-5.37C.864 6.847.895 4.31 2.249 2.853" clip-rule="evenodd"></path></svg>`,
        liked: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="url(#dzd_svg__a)" d="M9.318 14.93c-.78.651-1.893.663-2.682.024-1.536-1.241-4.024-3.423-5.531-5.626-1.484-2.17-1.547-5.345.264-7.293C3.171.097 6.057-.018 7.987 1.77 9.915-.018 12.799.094 14.6 2.028c1.803 1.934 1.835 5.153.258 7.289-1.62 2.193-4.047 4.366-5.541 5.614"></path><defs><linearGradient id="dzd_svg__a" x1="0" x2="0" y1="0.5" y2="16" gradientUnits="userSpaceOnUse"><stop stop-color="#FF8990"></stop><stop offset="1" stop-color="#FF515B"></stop></linearGradient></defs></svg>`,
        comment: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="currentColor" fill-rule="evenodd" d="M8 1.6a6.4 6.4 0 0 0-5.512 9.654l.142.24-.848 2.435a.294.294 0 0 0 .362.377l2.607-.786.219.118A6.4 6.4 0 1 0 8 1.6M.4 8a7.6 7.6 0 1 1 4.226 6.811l-2.136.644c-1.168.353-2.243-.769-1.842-1.921l.668-1.915A7.6 7.6 0 0 1 .4 7.999" clip-rule="evenodd"></path></svg>`
    };

    // --- 状态变量 ---
    let currentPostOffset = 0; // 当前加载帖子的偏移量
    let isLoadingPosts = false; // 是否正在加载帖子列表
    let isPosting = false; // 是否正在发布新帖
    let isCommenting = false; // 是否正在提交评论/回复
    let isLiking = {}; // 存储每个帖子的点赞操作状态 { feed_id: boolean }
    const commentStates = {}; // 存储每个帖子的评论加载状态 { feed_id: { offset: number, isLoading: boolean, hasMore: boolean, loaded: boolean } }
    let currentReplyTarget = null; // 当前回复的目标 { feedId, commentId, replyId, nickName, isSubReply }

    // --- 帮助函数:获取 CSRF Token (异步) ---
    /**
     * 使用 GM_cookie 异步获取指定名称的 CSRF Token Cookie。
     * @param {string} name Cookie 名称 (e.g., 'acf_yb_t')
     * @returns {Promise<string>} 返回 Promise,解析为 Cookie 值。
     * @rejects {Error} 如果获取失败或找不到 Cookie。
     */
    async function getCsrfTokenFromGmCookie(name) {
        return new Promise((resolve, reject) => {
            GM_cookie.list({
                name: name,
                domain: 'yuba.douyu.com'
            }, (cookies, error) => {
                if (error) {
                    console.error("GM_cookie.list error:", error);
                    reject(new Error('获取 Cookie 时出错 (GM_cookie)'));
                } else if (cookies && cookies.length > 0) {
                    // 优先查找精确匹配 domain 和 name 的 cookie
                    const targetCookie = cookies.find(c => c.domain.includes('yuba.douyu.com') && c.name === name);
                    if (targetCookie) {
                        console.log(`Found cookie '${name}' via GM_cookie:`, targetCookie);
                        resolve(targetCookie.value);
                    } else if (cookies[0].name === name) {
                        // 降级:如果第一个 cookie 名称匹配,也使用它(可能 domain 略有不同)
                        console.log(`Using first found cookie '${name}' as fallback:`, cookies[0]);
                        resolve(cookies[0].value);
                    } else {
                         // 如果没有找到精确匹配或第一个也不匹配
                        console.error(`Precise cookie '${name}' not found in list:`, cookies);
                        reject(new Error(`无法精确找到 Cookie: ${name}`));
                    }
                } else {
                    console.warn(`Cookie '${name}' not found via GM_cookie.`);
                    reject(new Error(`无法获取 CSRF Token (Cookie: ${name})`));
                }
            });
        });
    }

    // --- 帮助函数:创建并发送 API 请求 ---
    /**
     * 封装 GM_xmlhttpRequest 以发送 API 请求。
     * @param {object} details 请求详情,包含 method, url, headers, data, responseType 等。
     * @returns {Promise<object>} 返回 Promise,解析为 API 的 JSON 响应。
     * @rejects {Error} 如果请求失败 (网络错误、HTTP 错误、API 业务错误)。
     */
    function sendApiRequest(details) {
        return new Promise((resolve, reject) => {
            const isFormData = details.data instanceof FormData;
            const headers = { ...details.headers };

            // 如果是 FormData,GM_xmlhttpRequest 会自动设置 Content-Type,无需手动指定
            if (isFormData) {
                delete headers['Content-Type'];
            }

            GM_xmlhttpRequest({
                method: details.method,
                url: details.url,
                headers: headers,
                data: details.data,
                responseType: 'json', // 期望返回 JSON
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        console.log(`API Response (${details.url}):`, response.response);
                        if (response.response) {
                            // 检查 API 返回的业务错误码
                            // 注意: 斗鱼 API 的 error 字段有时为 0 表示成功,有时为 null 或 undefined 也可能表示成功
                            if (typeof response.response.error !== 'undefined' && response.response.error !== 0 && response.response.error !== null) {
                                console.error(`API Business Error (${details.url}):`, response.response);
                                reject(new Error(response.response.msg || `API返回错误码: ${response.response.error}`));
                            } else {
                                resolve(response.response); // API 业务成功
                            }
                        } else {
                            // 处理空响应或非 JSON 响应 (特殊情况,如点赞、回复可能返回空或非标准 JSON)
                            if (details.url.includes('/like') || details.url.includes('/unlike') || details.url.includes('/reply/send') || details.url.includes('/comment/send') ) {
                                console.log(`API Success (potentially empty/non-JSON response) for ${details.url}`);
                                resolve(response.response || {}); // 返回空对象或原始响应
                            } else {
                                console.error(`API Error (${details.url}): Empty or non-JSON response.`);
                                reject(new Error("API未返回有效JSON数据"));
                            }
                        }
                    } else {
                        console.error(`API HTTP Error (${details.url}):`, response.status, response.statusText, response.response);
                        reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
                    }
                },
                onerror: function(error) {
                    console.error(`API Network Error (${details.url}):`, error);
                    reject(new Error('网络错误,无法连接API'));
                },
                ontimeout: function() {
                    console.error(`API Timeout Error (${details.url})`);
                    reject(new Error('API请求超时'));
                }
            });
        });
    }

    // --- HTML 实体解码 和 简化斗鱼标记处理 ---
    /**
     * 格式化帖子或评论内容,解码 HTML 实体并转换斗鱼特定标记。
     * @param {string} text 原始文本内容。
     * @returns {string} 格式化后的 HTML 字符串。
     */
    function formatPostContent(text) {
        if (!text) return '';

        // 1. 解码 HTML 实体 (如 )
        const textarea = document.createElement('textarea');
        textarea.innerHTML = text.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec))
                                  .replace(/&/g, '&') // 基本实体也要处理一下
                                  .replace(/</g, '<')
                                  .replace(/>/g, '>')
                                  .replace(/"/g, '"')
                                  .replace(/ /g, ' '); // 处理
        let decodedText = textarea.value;

        // 2. 转换斗鱼特定标记为 HTML
        // [topic src="..."]#话题内容[/topic] -> <span class="post-topic">#话题内容#</span>
        decodedText = decodedText.replace(/\[topic src="[^"]*"]#([^\[]+)\[\/topic]/g, '<span class="post-topic">#$1#</span>');
        // [link src="..." url="..."]链接文字[/link] -> <a href="..." ...>链接文字</a>
        decodedText = decodedText.replace(/\[link src="[^"]*" url="([^"]+)"[^\]]*]([^\[]+)\[\/link]/g, '<a href="$1" target="_blank" rel="noopener noreferrer" class="post-link">$2</a>');
        // 转换换行符
        decodedText = decodedText.replace(/\n/g, '<br>');

        return decodedText;
    }

    // --- 渲染单个帖子到容器 ---
    /**
     * 创建并渲染单个帖子的 HTML 元素。
     * @param {object} postData 帖子数据对象。
     * @param {HTMLElement} container 要将帖子添加到的父容器元素。
     */
    function renderPost(postData, container) {
        const postElement = document.createElement('div');
        postElement.className = 'yuba-viewer-post-item';
        postElement.dataset.feedId = postData.feed_id; // 存储 feed_id 以供后续操作使用

        const publisher = postData.publisher || {};
        const group = postData.group || {};
        const feedId = postData.feed_id;
        const isLiked = postData.is_liked || false;
        const likeCount = postData.like_count || 0;
        const commentCount = postData.comment_count || 0;

        // 初始化该帖子的评论状态和点赞状态(如果尚未存在)
        if (!commentStates[feedId]) {
            commentStates[feedId] = { offset: 0, isLoading: false, hasMore: true, loaded: false };
        }
        if (typeof isLiking[feedId] === 'undefined') {
            isLiking[feedId] = false;
        }

        // 构建帖子 HTML
        postElement.innerHTML = `
            <div class="post-header">
                <img class="post-avatar" src="${publisher.avatar || ''}" alt="avatar">
                <div class="post-author-info">
                    <span class="post-nickname">${publisher.nickname || '未知用户'}</span>
                    <span class="post-uid">(UID: ${publisher.uid || 'N/A'})</span>
                    ${group.group_level_title ? `<span class="post-level">${group.group_level_title}</span>` : ''}
                </div>
                <span class="post-timestamp">${new Date(postData.ctime * 1000).toLocaleString()}</span>
            </div>
            <div class="post-body">
                <div class="post-text-content">${formatPostContent(postData.text)}</div>
                ${renderImages(postData.image_video_list)}
            </div>
            <div class="post-footer">
                <button class="like-toggle-button icon-button" data-feed-id="${feedId}" data-liked="${isLiked}" title="${isLiked ? '取消点赞' : '点赞'}">
                    ${isLiked ? ICONS.liked : ICONS.like} <span class="like-count-display">${likeCount > 0 ? likeCount : '赞'}</span>
                </button>
                <button class="view-comments-button icon-button" data-feed-id="${feedId}" title="查看/收起评论">
                    ${ICONS.comment} <span class="comment-count-display">${commentCount > 0 ? commentCount : '评论'}</span>
                </button>
                <span class="locality-display"> | 位置: ${postData.locality || '未知'}</span>
                <a href="${postData.share_url || '#'}" target="_blank" class="share-link"> | 查看原帖</a>
                <span class="like-status" data-feed-id="${feedId}"></span> <!-- 用于显示点赞操作状态 -->
            </div>
            <div class="post-comments-list-container" data-feed-id="${feedId}" style="display: none;">
                <div class="comments-list"></div> <!-- 评论列表将插入此处 -->
                <button class="load-more-comments-button" style="display: none;">加载更多评论</button>
                <div class="comments-load-status" style="font-size: 0.9em; color: #888; margin-top: 5px;"></div>
            </div>
            <div class="post-comment-section" data-feed-id="${feedId}">
                <div class="reply-target-indicator" style="display: none; font-size: 0.9em; color: #555; margin-bottom: 5px;">
                    回复 <span class="reply-target-nickname" style="font-weight: bold;"></span>:
                    <button class="cancel-reply-button" style="margin-left: 5px; font-size: 0.8em; padding: 1px 3px;">取消</button>
                </div>
                <textarea class="comment-input" placeholder="添加评论..." rows="2"></textarea>
                <button class="comment-submit-button">评论</button>
                <div class="comment-status"></div> <!-- 用于显示评论/回复操作状态 -->
            </div>
        `;

        container.appendChild(postElement);

        // 绑定事件监听器
        postElement.querySelector('.comment-submit-button').addEventListener('click', handleCommentSubmit);
        postElement.querySelector('.view-comments-button').addEventListener('click', toggleCommentsView);
        postElement.querySelector('.load-more-comments-button').addEventListener('click', handleLoadMoreComments);
        postElement.querySelector('.like-toggle-button').addEventListener('click', handleLikeToggle);
        postElement.querySelector('.cancel-reply-button').addEventListener('click', cancelReply);
    }

    // --- 设置回复目标 ---
    /**
     * 设置当前要回复的目标(评论或楼中楼),并更新 UI。
     * @param {string} feedId 帖子 ID。
     * @param {string} commentId 顶级评论 ID。
     * @param {string|null} replyId 楼中楼回复的 ID (如果回复的是楼中楼),否则为 null。
     * @param {string} nickName 被回复者的昵称。
     * @param {boolean} isSubReply 是否是回复楼中楼。
     */
    function setReplyTarget(feedId, commentId, replyId, nickName, isSubReply) {
        currentReplyTarget = { feedId, commentId, replyId, nickName, isSubReply };
        const postItem = document.querySelector(`.yuba-viewer-post-item[data-feed-id="${feedId}"]`);
        if (!postItem) return;

        const indicator = postItem.querySelector('.reply-target-indicator');
        const nicknameSpan = indicator.querySelector('.reply-target-nickname');
        const textarea = postItem.querySelector('.comment-input');

        nicknameSpan.textContent = nickName;
        indicator.style.display = 'block';
        textarea.placeholder = `回复 ${nickName}:`;
        textarea.focus(); // 聚焦输入框
    }

    // --- 取消回复 ---
    /**
     * 取消当前的回复状态,并重置 UI。
     * @param {Event} event 点击事件对象。
     */
    function cancelReply(event) {
        currentReplyTarget = null;
        const postItem = event.target.closest('.yuba-viewer-post-item');
        if (!postItem) return;

        const indicator = postItem.querySelector('.reply-target-indicator');
        const textarea = postItem.querySelector('.comment-input');

        indicator.style.display = 'none';
        textarea.placeholder = '添加评论...';
    }

    // --- 处理点赞/取消点赞切换 ---
    /**
     * 处理点赞按钮的点击事件,发送点赞或取消点赞请求。
     * @param {Event} event 点击事件对象。
     */
    async function handleLikeToggle(event) {
        const button = event.currentTarget;
        const feedId = button.dataset.feedId;
        const currentlyLiked = button.dataset.liked === 'true';
        const likeStatusSpan = document.querySelector(`.like-status[data-feed-id="${feedId}"]`);
        const likeCountSpan = button.querySelector('.like-count-display');

        // 防止重复点击
        if (isLiking[feedId]) return;
        isLiking[feedId] = true;
        button.disabled = true;
        if (likeStatusSpan) likeStatusSpan.textContent = '处理中...';

        let csrfToken;
        try {
            csrfToken = await getCsrfTokenFromGmCookie(CSRF_COOKIE_NAME);
            if (!csrfToken) throw new Error(`无法获取 CSRF Token`);

            const targetUrl = currentlyLiked ? API_UNLIKE_URL : API_LIKE_URL;
            const actionName = currentlyLiked ? '取消点赞' : '点赞';
            const requestData = new URLSearchParams({ 'feed_id': feedId }).toString();

            console.log(`${actionName} Request Data for feed ${feedId}:`, requestData);
            await sendApiRequest({
                method: 'POST',
                url: targetUrl,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-CSRF-Token': csrfToken,
                    'Accept': 'application/json'
                },
                data: requestData
            });

            // 更新 UI
            const newLikedState = !currentlyLiked;
            button.dataset.liked = newLikedState;
            button.title = newLikedState ? '取消点赞' : '点赞';
            if (likeStatusSpan) likeStatusSpan.textContent = `${actionName}成功!`;

            // 更新点赞数显示
            if (likeCountSpan) {
                let currentCount = 0;
                const countText = likeCountSpan.textContent.trim();
                 // 尝试从文本中提取数字,如果不是数字(如“赞”),则视为 0
                const countMatch = countText.match(/\d+/);
                if (countMatch) {
                    currentCount = parseInt(countMatch[0], 10);
                }

                currentCount = newLikedState ? currentCount + 1 : Math.max(0, currentCount - 1); // 增加或减少计数
                // 更新按钮内部 HTML,保留图标和新的计数
                 button.innerHTML = `${newLikedState ? ICONS.liked : ICONS.like} <span class="like-count-display">${currentCount > 0 ? currentCount : '赞'}</span>`;
            }

            console.log(`${actionName} successful for feed ${feedId}`);

        } catch (error) {
            console.error(`${currentlyLiked ? '取消点赞' : '点赞'}失败:`, error);
            if (likeStatusSpan) likeStatusSpan.textContent = `${currentlyLiked ? '取消点赞' : '点赞'}失败: ${error.message}`;
            // 失败时 UI 状态回滚可能比较复杂,暂时只显示错误信息
        } finally {
            isLiking[feedId] = false;
            button.disabled = false;
            // 延迟清除状态信息
            setTimeout(() => {
                if (likeStatusSpan) likeStatusSpan.textContent = '';
            }, 3000);
        }
    }

    // --- 切换评论区显示/隐藏 ---
    /**
     * 处理“查看/收起评论”按钮的点击事件。
     * @param {Event} event 点击事件对象。
     */
    function toggleCommentsView(event) {
        const button = event.currentTarget;
        const feedId = button.dataset.feedId;
        const commentsContainer = document.querySelector(`.post-comments-list-container[data-feed-id="${feedId}"]`);
        if (!commentsContainer) return;

        const isVisible = commentsContainer.style.display !== 'none';
        if (isVisible) {
            commentsContainer.style.display = 'none';
            button.title = '查看评论';
        } else {
            commentsContainer.style.display = 'block';
            button.title = '收起评论';
            // 如果评论尚未加载过,则触发加载
            if (!commentStates[feedId].loaded) {
                fetchAndRenderComments(feedId);
            }
        }
    }

    // --- 加载更多评论的处理 ---
    /**
     * 处理“加载更多评论”按钮的点击事件。
     * @param {Event} event 点击事件对象。
     */
    function handleLoadMoreComments(event) {
        const button = event.target;
        const feedId = button.closest('.post-comments-list-container').dataset.feedId;
        fetchAndRenderComments(feedId);
    }

    // --- 渲染图片 ---
    /**
     * 根据 image_video_list 数据生成图片 HTML。
     * @param {Array<object>} imageVideoList 包含图片/视频信息的数组。
     * @returns {string} 包含所有图片 <img> 标签的 HTML 字符串。
     */
    function renderImages(imageVideoList) {
        if (!imageVideoList || imageVideoList.length === 0) return '';
        let imagesHTML = '<div class="post-images-container">';
        imageVideoList.forEach(item => {
            // 只处理图片类型 (type === 1) 且包含有效图片数据
            if (item.type === 1 && item.images && item.images.images && item.images.images.length > 0) {
                item.images.images.forEach(imgData => {
                    // 使用 <a> 标签包裹 <img>,允许点击查看原图
                    imagesHTML += `<a href="${imgData.url}" target="_blank" title="点击查看原图"><img src="${imgData.url}" alt="帖子图片" class="post-image"></a>`;
                });
            }
            // 可以在此添加对视频类型的处理 (if item.type === 2)
        });
        imagesHTML += '</div>';
        return imagesHTML;
    }

    // --- 获取并渲染帖子列表 ---
    /**
     * 异步获取帖子列表数据并渲染到页面。
     */
    async function fetchAndRenderPosts() {
        if (isLoadingPosts) return; // 防止重复加载
        isLoadingPosts = true;
        updateMainStatus('正在加载帖子...');
        setLoadMorePostsButtonState(false, '加载中...'); // 禁用按钮并显示加载中

        const apiUrl = new URL(API_FEED_LIST_URL);
        const params = {
            group_id: TARGET_GROUP_ID,
            group_tab_id: 1, // 通常是默认的帖子列表 tab
            offset: currentPostOffset,
            limit: POST_LIMIT,
            data_type: 3, // 可能是某种数据类型标识
            sink_feeds_status: 0, // 可能与置底/沉帖相关
            sink_offset: 0 // 可能与置底/沉帖相关
        };
        Object.keys(params).forEach(key => apiUrl.searchParams.append(key, params[key]));

        try {
            const response = await sendApiRequest({
                method: 'GET',
                url: apiUrl.toString(),
                headers: { 'Accept': 'application/json' }
            });

            const postContainer = document.getElementById('yuba-viewer-post-list');
            if (!postContainer) return; // 容器不存在则退出

            if (response && response.data && Array.isArray(response.data.feed_list)) {
                const feedList = response.data.feed_list;
                if (feedList.length > 0) {
                    feedList.forEach(post => renderPost(post, postContainer));
                    // 更新下一次请求的偏移量
                    currentPostOffset = response.data.next_offset || (currentPostOffset + feedList.length);
                    updateMainStatus(`已加载 ${postContainer.children.length} 条帖子`);

                    // 检查是否还有更多帖子
                    const hasMore = typeof response.data.has_more !== 'undefined' ? response.data.has_more : (feedList.length === POST_LIMIT);
                    if (hasMore) {
                        setLoadMorePostsButtonState(true, '加载更多帖子'); // 启用按钮
                    } else {
                        updateMainStatus(`已加载全部 ${postContainer.children.length} 条帖子`);
                        setLoadMorePostsButtonState(false, '已加载全部'); // 禁用按钮并提示已全部加载
                    }
                } else {
                    // 没有更多帖子了
                    updateMainStatus(currentPostOffset === 0 ? '该鱼吧没有帖子' : `已加载全部 ${postContainer.children.length} 条帖子`);
                    setLoadMorePostsButtonState(false, '没有更多帖子了');
                }
            } else {
                // API 返回的数据格式不符合预期
                updateMainStatus('未能获取帖子数据或列表为空');
                setLoadMorePostsButtonState(false, '加载失败或无数据');
            }
        } catch (error) {
            updateMainStatus(`加载帖子失败: ${error.message}`);
            setLoadMorePostsButtonState(true, '加载失败,点击重试'); // 允许用户重试
        } finally {
            isLoadingPosts = false; // 请求完成,解除锁定
        }
    }

    // --- 获取并渲染评论列表 ---
    /**
     * 异步获取指定帖子的评论列表并渲染。
     * @param {string} feedId 帖子 ID。
     */
    async function fetchAndRenderComments(feedId) {
        const state = commentStates[feedId];
        // 如果状态不存在、正在加载或没有更多评论,则直接返回
        if (!state || state.isLoading || !state.hasMore) return;

        state.isLoading = true;
        const commentsListDiv = document.querySelector(`.post-comments-list-container[data-feed-id="${feedId}"] .comments-list`);
        const loadMoreButton = document.querySelector(`.post-comments-list-container[data-feed-id="${feedId}"] .load-more-comments-button`);
        const statusDiv = document.querySelector(`.post-comments-list-container[data-feed-id="${feedId}"] .comments-load-status`);

        if (!commentsListDiv || !loadMoreButton || !statusDiv) return; // 确保元素存在

        statusDiv.textContent = '加载评论中...';
        loadMoreButton.style.display = 'none'; // 隐藏加载按钮

        const apiUrl = new URL(API_COMMENT_LIST_URL);
        const params = {
            feed_id: feedId,
            group_id: TARGET_GROUP_ID,
            limit: COMMENT_LIMIT,
            sort: 1, // 排序方式,1 可能表示按时间倒序
            offset: state.offset // 当前评论偏移量
        };
        Object.keys(params).forEach(key => apiUrl.searchParams.append(key, params[key]));

        try {
            const response = await sendApiRequest({
                method: 'GET',
                url: apiUrl.toString(),
                headers: { 'Accept': 'application/json' }
            });

            console.log(`Comment list response for feed ${feedId}:`, JSON.stringify(response, null, 2)); // 详细日志记录

            if (response && response.data && Array.isArray(response.data.comments)) {
                const commentList = response.data.comments;
                if (commentList.length > 0) {
                    commentList.forEach(comment => renderComment(comment, commentsListDiv, feedId));
                    // 更新下一次请求的偏移量
                    state.offset = response.data.next_offset || (state.offset + commentList.length);
                    statusDiv.textContent = ''; // 清除加载状态

                    // 检查是否还有更多评论
                    // 注意:next_offset 为 "0" 或 0 时通常表示没有更多了
                    const hasMore = typeof response.data.has_more !== 'undefined' ? response.data.has_more : (commentList.length === COMMENT_LIMIT && response.data.next_offset !== "0" && response.data.next_offset !== 0);

                    if (hasMore) {
                        state.hasMore = true;
                        loadMoreButton.style.display = 'block'; // 显示加载更多按钮
                        loadMoreButton.disabled = false;
                        loadMoreButton.textContent = '加载更多评论';
                    } else {
                        state.hasMore = false;
                        loadMoreButton.style.display = 'none'; // 隐藏按钮
                        statusDiv.textContent = '已加载全部评论'; // 提示已全部加载
                    }
                } else {
                    // 本次请求没有返回评论
                    state.hasMore = false;
                    loadMoreButton.style.display = 'none';
                    statusDiv.textContent = state.offset === 0 ? '暂无评论' : '已加载全部评论'; // 区分是本来就没评论还是加载完了
                }
                state.loaded = true; // 标记该帖子的评论至少加载过一次
            } else {
                statusDiv.textContent = '未能获取评论数据或列表为空';
                state.hasMore = false; // 标记为没有更多
                loadMoreButton.style.display = 'none';
            }
        } catch (error) {
            statusDiv.textContent = `加载评论失败: ${error.message}`;
            // 允许重试
            if(state.hasMore) { // 只有在理论上还有更多时才显示重试按钮
                 loadMoreButton.style.display = 'block';
                 loadMoreButton.disabled = false;
                 loadMoreButton.textContent = '加载失败,点击重试';
            } else {
                 loadMoreButton.style.display = 'none'; // 如果已经没有更多了,失败了也不显示
            }
        } finally {
            state.isLoading = false; // 请求完成,解除锁定
        }
    }


    // --- 渲染单条评论 (包括楼中楼) ---
    /**
     * 创建并渲染单条顶级评论及其楼中楼回复。
     * @param {object} commentData 评论数据对象。
     * @param {HTMLElement} container 评论列表容器。
     * @param {string} feedId 所属帖子的 ID。
     */
    function renderComment(commentData, container, feedId) {
        console.log(`Rendering comment ID: ${commentData.comment_id || commentData.comment_id_str}`, "Data:", JSON.stringify(commentData, null, 2));

        const commentElement = document.createElement('div');
        commentElement.className = 'yuba-viewer-comment-item';

        // !! 关键: 优先使用 comment_id_str,因为它通常是准确的字符串 ID,comment_id 可能是数字,可能导致精度问题
        const topCommentId = commentData.comment_id_str || String(commentData.comment_id); // 确保是字符串
        commentElement.dataset.commentId = topCommentId; // 用于回复时定位顶级评论
        commentElement.dataset.nickName = commentData.nick_name || '未知用户'; // 存储昵称方便回复时引用
        commentElement.dataset.isSubReply = 'false'; // 标记这是顶级评论

        // 渲染楼中楼回复 (如果存在)
        const subCommentsHTML = commentData.sub_replies && commentData.sub_replies.length > 0
            ? renderSubComments(commentData.sub_replies, feedId, topCommentId) // 传递 feedId 和 topCommentId
            : '';

        // 构建评论 HTML
        commentElement.innerHTML = `
            <div class="comment-header">
                 <img class="comment-avatar" src="${commentData.avatar || ''}" alt="avatar">
                 <span class="comment-nickname">${commentData.nick_name || '未知用户'}</span>
                 <span class="comment-timestamp">${new Date(commentData.created_ts * 1000).toLocaleString()}</span>
                 <span class="comment-locality">${commentData.locality ? ` | ${commentData.locality}` : ''}</span>
                 <button class="reply-button" data-is-sub-reply="false" style="margin-left: 10px; font-size:0.8em; cursor:pointer; color:#888;">回复</button>
            </div>
            <div class="comment-content">${formatPostContent(commentData.content)}</div>
            ${subCommentsHTML} <!-- 插入楼中楼 HTML -->
        `;
        container.appendChild(commentElement);

        // 为这个评论元素内的所有回复按钮(包括楼中楼里的)绑定事件
        bindReplyButtons(commentElement, feedId, topCommentId);
    }

    // --- 渲染楼中楼回复 (子评论) ---
    /**
     * 创建并渲染楼中楼回复的 HTML。
     * @param {Array<object>} subReplies 楼中楼回复数据数组。
     * @param {string} feedId 所属帖子的 ID。
     * @param {string} topCommentId 所属顶级评论的 ID。
     * @returns {string} 包含所有楼中楼回复的 HTML 字符串。
     */
    function renderSubComments(subReplies, feedId, topCommentId) {
        console.log("Rendering sub-comments for top comment:", topCommentId, "Data:", JSON.stringify(subReplies, null, 2));
        let subHTML = '<div class="sub-comments-container">';
        subReplies.forEach(reply => {
            const user = reply; // API 返回结构中,回复者信息直接在顶层
            const targetUser = reply.target_user_info || {}; // 被回复者信息

            // !! 关键: 使用 comment_reply_id 作为楼中楼回复的唯一标识符
            const replyId = reply.comment_reply_id; // replyId 用于回复楼中楼时指定目标

            // 必须要有 replyId 才能进行后续的回复操作
            if (!replyId) {
                console.warn("Sub-reply missing 'comment_reply_id':", reply);
                return; // 跳过没有有效 ID 的回复
            }

            subHTML += `
                 <div class="sub-comment-item" data-reply-id="${replyId}" data-nick-name="${user.nick_name || '?'}"> <!-- 存储 replyId 和昵称 -->
                     <span class="comment-nickname">${user.nick_name || '?'}</span>
                     ${targetUser.uid && targetUser.nick_name ? ` 回复 <span class="comment-nickname">${targetUser.nick_name}</span>` : ''}:
                     <span class="sub-comment-content">${formatPostContent(reply.content)}</span>
                     <button class="reply-button sub-reply-button" data-is-sub-reply="true" style="margin-left: 5px; font-size:0.8em; cursor:pointer; color:#888;">回复</button>
                 </div>
             `;
        });
        subHTML += '</div>';
        return subHTML;
    }


    // --- 统一绑定回复按钮事件 ---
    /**
     * 为指定父元素下的所有回复按钮(顶级评论和楼中楼)绑定点击事件。
     * @param {HTMLElement} parentElement 包含回复按钮的父元素 (通常是 .yuba-viewer-comment-item)。
     * @param {string} feedId 帖子 ID。
     * @param {string} topCommentId 顶级评论 ID。
     */
    function bindReplyButtons(parentElement, feedId, topCommentId) {
        parentElement.querySelectorAll('.reply-button').forEach(button => {
            // 防止重复绑定监听器
            if (button.dataset.listenerAttached) return;

            button.addEventListener('click', (e) => {
                const targetButton = e.currentTarget;
                const isSubReply = targetButton.dataset.isSubReply === 'true'; // 判断是回复顶级评论还是楼中楼

                let replyId = null; // 楼中楼回复的目标 ID
                let nickName = '未知用户';

                if (isSubReply) {
                    // 如果是回复楼中楼,找到对应的 .sub-comment-item 获取 replyId 和 nickName
                    const subItem = targetButton.closest('.sub-comment-item');
                    if (subItem) {
                        replyId = subItem.dataset.replyId; // 获取楼中楼自身的 ID
                        nickName = subItem.dataset.nickName; // 获取这条楼中楼发布者的昵称
                    }
                } else {
                    // 如果是回复顶级评论,找到对应的 .yuba-viewer-comment-item 获取 nickName
                    const topItem = targetButton.closest('.yuba-viewer-comment-item');
                    if (topItem) {
                        nickName = topItem.dataset.nickName; // 获取顶级评论发布者的昵称
                        // replyId 保持为 null
                    }
                }

                console.log("Reply button clicked:", { feedId, topCommentId, replyId, nickName, isSubReply });

                // 验证获取到的 ID 是否有效
                if (isSubReply && !replyId) {
                    console.error("无法获取有效的子评论 replyId!请检查 renderSubComments 或 API 响应。 Sub Item:", targetButton.closest('.sub-comment-item'));
                    alert("无法获取回复目标的 ID (子评论),请检查脚本或 API 响应。");
                    return;
                }
                if (!topCommentId) {
                    console.error("无法获取有效的顶级评论 commentId!");
                    alert("无法获取顶级评论 ID,请检查脚本或 API 响应。");
                    return;
                }

                // 设置全局回复目标
                setReplyTarget(feedId, topCommentId, replyId, nickName, isSubReply);
            });
            button.dataset.listenerAttached = 'true'; // 标记已绑定
        });
    }


    // --- 处理发新帖 ---
    /**
     * 处理“发布新帖”按钮的点击事件,执行发帖流程。
     */
    async function handlePostSubmit() {
        if (isPosting) return; // 防止重复提交
        const postContentInput = document.getElementById('new-post-content');
        const postButton = document.getElementById('new-post-submit-button');
        const postStatusDiv = document.getElementById('new-post-status');
        const textContent = postContentInput.value.trim();

        if (!textContent) {
            postStatusDiv.textContent = '错误:帖子内容不能为空';
            postStatusDiv.style.color = 'red';
            return;
        }

        isPosting = true;
        postButton.disabled = true;
        postButton.textContent = '发布中...';
        postStatusDiv.textContent = '正在发布...';
        postStatusDiv.style.color = '#888';

        let csrfToken;
        try {
            // --- 步骤 0: 获取 CSRF Token ---
            updateMainStatus('获取 CSRF Token...');
            csrfToken = await getCsrfTokenFromGmCookie(CSRF_COOKIE_NAME);
            if (!csrfToken) throw new Error(`无法获取 CSRF Token`);
            updateMainStatus('CSRF Token 获取成功');

            // --- 步骤 1: 风险验证 (获取 req_id) ---
            let reqId = null;
            try {
                updateMainStatus('发帖步骤 1: 请求风险验证...');
                const verifyData = new URLSearchParams({
                    'text': textContent,
                    'face_names': '[]', // 表情名称,暂时不支持
                    'has_image': 'false', // 是否有图片,暂时不支持
                    'has_video': 'false', // 是否有视频,暂时不支持
                    'feed_scope': '1' // 帖子范围,1 通常表示普通帖
                }).toString();

                console.log("Verify Post Request Data:", verifyData);
                const verifyResponse = await sendApiRequest({
                    method: 'POST',
                    url: API_VERIFY_URL,
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'X-CSRF-Token': csrfToken,
                        'Accept': 'application/json'
                    },
                    data: verifyData
                });

                // 检查验证响应是否包含 req_id
                if (!verifyResponse || !verifyResponse.data || typeof verifyResponse.data.req_id === 'undefined') {
                    throw new Error('发帖验证响应格式错误,未找到 req_id');
                }
                reqId = verifyResponse.data.req_id;
                updateMainStatus(`发帖步骤 1 成功! req_id: ${reqId}`);
                console.log('Got post req_id:', reqId);
            } catch (error) {
                updateMainStatus(`发帖步骤 1 失败: ${error.message}`);
                throw error; // 抛出错误,中断后续步骤
            }

            // --- 步骤 2: 正式发布帖子 ---
            try {
                updateMainStatus('发帖步骤 2: 发布帖子...');
                const msgId = `${Date.now()}${Math.random()}`; // 生成一个本地唯一的消息 ID
                console.log('Generated post msg_id:', msgId);

                const publishData = new URLSearchParams({
                    'req_id': reqId, // 使用上一步获取的 req_id
                    'msg_id': msgId, // 本地生成的消息 ID
                    'text': textContent,
                    'channel_id': '0', // 频道 ID,通常为 0
                    'group_id': TARGET_GROUP_ID, // 目标鱼吧 ID
                    'feed_scope': '1', // 帖子范围
                    'face_names': '[]' // 表情
                    // 这里可以根据需要添加图片、视频等参数
                }).toString();

                console.log("Publish Post Request Data:", publishData);
                const publishResponse = await sendApiRequest({
                    method: 'POST',
                    url: API_PUBLISH_URL,
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'X-CSRF-Token': csrfToken,
                        'Accept': 'application/json'
                    },
                    data: publishData
                });

                 // 检查发布响应是否成功 (通常会返回 feed_id)
                if (!publishResponse || !publishResponse.data || typeof publishResponse.data.feed_id === 'undefined') {
                    // 有些成功响应可能没有 feed_id,但 error 为 0 或 null
                    if (publishResponse.error !== 0 && publishResponse.error !== null) {
                         throw new Error(publishResponse.msg || '帖子发布响应格式错误,未找到 feed_id 且 error 不为 0/null');
                    }
                    // 如果 error 为 0 或 null 但没有 feed_id,也视为成功,但给个提示
                     postStatusDiv.textContent = `帖子发布成功!(未返回 Feed ID)`;
                     console.log('New post successful (no feed_id returned):', publishResponse);
                } else {
                    postStatusDiv.textContent = `帖子发布成功!Feed ID: ${publishResponse.data.feed_id}`;
                    console.log('New post successful:', publishResponse);
                }

                postStatusDiv.style.color = 'green';
                postContentInput.value = ''; // 清空输入框

                // 发布成功后刷新帖子列表
                currentPostOffset = 0; // 重置偏移量
                document.getElementById('yuba-viewer-post-list').innerHTML = ''; // 清空现有列表
                fetchAndRenderPosts(); // 重新加载第一页
                updateMainStatus('新帖发布成功,刷新列表...');

            } catch (error) {
                updateMainStatus(`发帖步骤 2 失败: ${error.message}`);
                throw error; // 抛出错误
            }

        } catch (error) {
            console.error("发帖失败:", error);
            postStatusDiv.textContent = `发帖失败: ${error.message}`;
            postStatusDiv.style.color = 'red';
        } finally {
            isPosting = false; // 解除锁定
            postButton.disabled = false;
            postButton.textContent = '发布新帖';
            // 延迟清除状态信息
            setTimeout(() => {
                if (postStatusDiv) postStatusDiv.textContent = '';
            }, 5000);
        }
    }


    // --- 处理评论/回复提交 ---
    /**
     * 处理评论/回复按钮的点击事件。
     * @param {Event} event 点击事件对象。
     */
    async function handleCommentSubmit(event) {
        if (isCommenting) return; // 防止重复提交
        const button = event.target;
        const postItem = button.closest('.yuba-viewer-post-item');
        const commentSection = button.closest('.post-comment-section');
        const textarea = commentSection.querySelector('.comment-input');
        const commentStatusDiv = commentSection.querySelector('.comment-status');
        const feedId = postItem.dataset.feedId;
        const content = textarea.value.trim();

        if (!content || !feedId) {
            commentStatusDiv.textContent = '错误:内容或帖子ID无效';
            commentStatusDiv.style.color = 'red';
            return;
        }

        isCommenting = true;
        button.disabled = true;
        button.textContent = '提交中...';
        commentStatusDiv.textContent = '正在提交...';
        commentStatusDiv.style.color = '#888';

        let csrfToken;
        try {
            csrfToken = await getCsrfTokenFromGmCookie(CSRF_COOKIE_NAME);
            if (!csrfToken) throw new Error(`无法获取 CSRF Token`);

            let apiUrl;
            let requestParams = {};
            let actionType = "评论"; // 用于日志

            // 判断是回复还是新评论
            if (currentReplyTarget && currentReplyTarget.feedId === feedId) {
                // --- 是回复 ---
                apiUrl = API_REPLY_SEND_URL;
                actionType = "回复";
                requestParams = {
                    'content_version': '2', // 内容版本,通常是 2
                    'comment_id': currentReplyTarget.commentId, // 必须提供顶级评论 ID
                    'group_id': TARGET_GROUP_ID,
                    'feed_id': feedId,
                    'content': content,
                };
                // 如果是回复楼中楼 (子回复),需要添加目标回复 ID
                if (currentReplyTarget.isSubReply && currentReplyTarget.replyId) {
                    requestParams['dst_comment_reply_id'] = currentReplyTarget.replyId; // 使用正确的子评论 ID (comment_reply_id)
                }
                console.log("Reply Request Data:", requestParams);
            } else {
                // --- 是新评论 ---
                apiUrl = API_COMMENT_SEND_URL;
                actionType = "评论";
                requestParams = {
                    'group_id': TARGET_GROUP_ID,
                    'feed_id': feedId,
                    'content_version': '2',
                    'content': content,
                    // 可能还需要其他参数,如 source_type 等,根据实际抓包情况调整
                };
                console.log("Comment Request Data:", requestParams);
            }

            // 发送请求
            const requestData = new URLSearchParams(requestParams).toString();
            const response = await sendApiRequest({
                method: 'POST',
                url: apiUrl,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-CSRF-Token': csrfToken,
                    'Accept': 'application/json'
                },
                data: requestData
            });

            console.log(`${actionType} Response:`, response);
            commentStatusDiv.textContent = `${actionType}成功!`;
            commentStatusDiv.style.color = 'green';
            textarea.value = ''; // 清空输入框

            // 如果是回复,则取消回复状态
            if (currentReplyTarget && currentReplyTarget.feedId === feedId) {
                // 手动触发取消回复的逻辑,传入关联的按钮元素
                const cancelButton = commentSection.querySelector('.cancel-reply-button');
                 if (cancelButton) {
                     cancelReply({ target: cancelButton }); // 模拟事件对象传递按钮本身
                 }
            }

            // 更新帖子上的评论数 (如果是新评论)
            // 注意:回复楼中楼通常不直接增加帖子主评论数,所以只在非回复或回复顶级评论时增加?
            // API 设计可能不同,保险起见,提交成功后总是刷新评论列表更可靠
            /*
            if (!currentReplyTarget || (currentReplyTarget && !currentReplyTarget.isSubReply)) {
                 const countSpan = postItem.querySelector('.comment-count-display');
                 if (countSpan) {
                     let currentCount = 0;
                     const countMatch = countSpan.textContent.match(/\d+/);
                     if (countMatch) {
                         currentCount = parseInt(countMatch[0], 10);
                     }
                     // API 可能在后台自己增加计数,前端简单+1可能不准,最好是刷新
                     countSpan.textContent = `${currentCount > 0 ? currentCount + 1 : 1}`;
                 }
            }
            */

            // 刷新评论列表以显示新评论/回复
            const commentsListDiv = postItem.querySelector('.post-comments-list-container .comments-list');
            const commentsContainer = postItem.querySelector('.post-comments-list-container');
            // 只有当评论区可见时才立即刷新
            if (commentsListDiv && commentsContainer && commentsContainer.style.display !== 'none') {
                commentsListDiv.innerHTML = ''; // 清空现有评论
                commentStates[feedId].offset = 0; // 重置评论偏移量
                commentStates[feedId].hasMore = true; // 假设刷新后可能有更多(让加载逻辑重新判断)
                commentStates[feedId].loaded = false; // 标记为未加载,强制重新请求
                fetchAndRenderComments(feedId); // 重新加载评论
                updateMainStatus(`${actionType}成功,刷新评论列表...`);
            } else {
                 // 如果评论区不可见,只需重置状态,下次打开时会刷新
                 commentStates[feedId].offset = 0;
                 commentStates[feedId].hasMore = true;
                 commentStates[feedId].loaded = false;
                 updateMainStatus(`${actionType}成功!`);
                 // 可以考虑更新评论数显示
                 const countSpan = postItem.querySelector('.comment-count-display');
                 if(countSpan) {
                     const currentCountText = countSpan.textContent.trim();
                     let currentCount = 0;
                     const countMatch = currentCountText.match(/\d+/);
                     if(countMatch) currentCount = parseInt(countMatch[0], 10);
                     else if(currentCountText === '评论') currentCount = 0; // 处理初始为“评论”的情况

                     // 简单+1,可能不完全精确,但比不更新好
                     countSpan.textContent = `${currentCount + 1}`;
                 }
            }

        } catch (error) {
            console.error(`${actionType}失败:`, error);
            commentStatusDiv.textContent = `${actionType}失败: ${error.message}`;
            commentStatusDiv.style.color = 'red';
        } finally {
            isCommenting = false; // 解除锁定
            button.disabled = false;
            button.textContent = '评论'; // 恢复按钮文字
            // 延迟清除状态信息
            setTimeout(() => {
                if (commentStatusDiv) commentStatusDiv.textContent = '';
            }, 5000);
        }
    }


    // --- 更新主状态显示 ---
    /**
     * 更新页面顶部的主状态信息。
     * @param {string} message 要显示的状态信息。
     */
    function updateMainStatus(message) {
        const statusDiv = document.getElementById('yuba-viewer-main-status');
        if (statusDiv) {
            statusDiv.textContent = `主状态:${message}`;
        }
        console.log(`Main Status: ${message}`); // 同时在控制台输出
    }

    // --- 设置加载更多帖子按钮状态 ---
    /**
     * 更新“加载更多帖子”按钮的启用状态和文本。
     * @param {boolean} enabled 是否启用按钮。
     * @param {string} text 按钮显示的文本。
     */
    function setLoadMorePostsButtonState(enabled, text) {
        const button = document.getElementById('yuba-viewer-load-more-posts');
        if (button) {
            button.disabled = !enabled;
            button.textContent = text;
            // 控制按钮的显示:启用时显示,或者当文本包含特定状态(如“全部”、“没有”、“失败”)时也显示
            button.style.display = enabled || text.includes('全部') || text.includes('没有') || text.includes('失败') ? 'block' : 'none';
        }
    }

    // --- 创建基础 UI 结构 (发帖区置顶) ---
    /**
     * 初始化脚本的用户界面,包括帖子列表容器、状态显示、发帖区域等。
     */
    function setupUI() {
        // 尝试隐藏原生的“鱼吧已关闭”提示
        const closedNotice = document.querySelector('.YubaClosed') || document.querySelector('.yuba-closed-tip');
        if (closedNotice) {
            closedNotice.style.display = 'none';
            console.log("已隐藏 '鱼吧已关闭' 提示。");
        }

        // 查找合适的父容器来插入我们的 UI
        // 优先尝试 .Main .content_wrap,这是常见的内容区域
        // 如果找不到,尝试 #root .PageLayout (新版斗鱼布局?)
        // 最坏情况插入到 body
        let mainContainer = document.querySelector('.Main .content_wrap');
        if (!mainContainer) {
            console.warn("未能找到 .Main .content_wrap 容器,将尝试其他插入点...");
            mainContainer = document.querySelector('#root .PageLayout'); // 尝试备选容器
            if (!mainContainer) {
                console.warn("备选容器 #root .PageLayout 也未找到,将直接插入到 body。");
                mainContainer = document.body; // 最后选择
            }
        }

        // 创建脚本的主容器
        const viewerContainer = document.createElement('div');
        viewerContainer.id = 'yuba-viewer-container'; // 主容器 ID

        // 定义 UI 的 HTML 结构,将发帖区域放在顶部
        viewerContainer.innerHTML = `
            <h2>6657UPUP</h2>
            <div class="poster-warning" style="text-align:center; color: #d9534f; font-weight: bold; margin-bottom: 15px; font-size: 12px; border: 1px dashed #d9534f; padding: 5px; background-color: #f2dede;">
                ⚠️ 仅供学习研究,请勿滥用,后果自负!
            </div>

            <!-- 发帖区域 -->
            <div id="new-post-section" style="border: 1px solid #ddd; padding: 15px; margin-bottom: 20px; background-color: #f8f8f8; border-radius: 3px;">
                <h3>发布新帖</h3>
                <div>
                    <textarea id="new-post-content" rows="4" placeholder="输入新帖子内容 (暂不支持图片/表情)" style="width: calc(100% - 12px); padding: 5px; border: 1px solid #ccc; border-radius: 3px; font-size: 14px; resize: vertical; min-height: 80px; margin-bottom: 10px;"></textarea>
                </div>
                <button id="new-post-submit-button" style="padding: 8px 15px; background-color: #5cb85c; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 14px; width: 100%; box-sizing: border-box;">发布新帖</button>
                <div id="new-post-status" style="font-size: 0.9em; margin-top: 8px; min-height: 1.2em; color: #888;"></div> <!-- 发帖状态显示 -->
            </div>

            <hr class="section-divider" style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">

            <!-- 帖子列表区域 -->
            <h3>帖子列表</h3>
            <div id="yuba-viewer-main-status">准备加载...</div> <!-- 主状态显示 -->
            <div id="yuba-viewer-post-list"></div> <!-- 帖子列表容器 -->
            <button id="yuba-viewer-load-more-posts" style="display: none;">加载更多帖子</button> <!-- 加载更多按钮 -->
        `;

        // 将脚本 UI 插入到找到的容器的开头
        mainContainer.insertBefore(viewerContainer, mainContainer.firstChild);

        // 绑定顶层按钮的事件监听器
        document.getElementById('yuba-viewer-load-more-posts').addEventListener('click', fetchAndRenderPosts);
        document.getElementById('new-post-submit-button').addEventListener('click', handlePostSubmit);

        console.log("UI setup complete.");
    }

    // --- 添加 CSS 样式 ---
    // GM_addStyle 用于注入 CSS 到页面
    GM_addStyle(`
        /* --- 主容器和基本布局 --- */
        #yuba-viewer-container {
            margin: 20px auto;
            padding: 15px;
            background-color: #fff;
            border: 1px solid #e1e1e1;
            max-width: 800px; /* 限制最大宽度,使其在宽屏上更易读 */
            box-shadow: 0 1px 3px rgba(0,0,0,0.05);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; /* 使用系统默认字体 */
            font-size: 14px;
            color: #333;
        }
        #yuba-viewer-container h2,
        #yuba-viewer-container h3 {
            text-align: center;
            margin-top: 10px;
            margin-bottom: 15px;
            color: #333;
            font-weight: 500;
        }
        #yuba-viewer-main-status {
            text-align: center;
            margin-bottom: 10px;
            color: #888;
            font-style: italic;
            font-size: 0.9em;
        }

        /* --- 帖子列表和加载更多 --- */
        #yuba-viewer-post-list {
            margin-bottom: 20px;
        }
        #yuba-viewer-load-more-posts {
            display: block;
            margin: 20px auto;
            padding: 10px 20px;
            background-color: #ff6a00; /* 斗鱼橙色 */
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s ease;
        }
        #yuba-viewer-load-more-posts:hover:not(:disabled) {
            background-color: #e05a00;
        }
        #yuba-viewer-load-more-posts:disabled {
            background-color: #ccc;
            cursor: not-allowed;
        }

        /* --- 单个帖子样式 --- */
        .yuba-viewer-post-item {
            border: 1px solid #eee;
            margin-bottom: 15px;
            padding: 10px 15px;
            background-color: #f9f9f9;
            border-radius: 3px;
            transition: box-shadow 0.2s ease;
        }
        .yuba-viewer-post-item:hover {
            box-shadow: 0 2px 5px rgba(0,0,0,0.08);
        }

        /* 帖子头部 (头像、昵称、时间等) */
        .post-header {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
            padding-bottom: 8px;
            border-bottom: 1px dashed #eee;
            flex-wrap: wrap; /* 允许换行 */
        }
        .post-avatar {
            width: 35px;
            height: 35px;
            border-radius: 50%;
            margin-right: 10px;
            flex-shrink: 0; /* 防止头像被压缩 */
        }
        .post-author-info {
            flex-grow: 1; /* 占据剩余空间 */
            min-width: 100px; /* 保证一定宽度 */
        }
        .post-nickname {
            font-weight: bold;
            color: #ff6a00; /* 斗鱼橙色 */
            margin-right: 5px;
        }
        .post-uid {
            font-size: 0.9em;
            color: #999;
        }
        .post-level {
            background-color: #eee;
            color: #777;
            padding: 1px 4px;
            border-radius: 3px;
            font-size: 0.8em;
            margin-left: 5px;
            white-space: nowrap;
        }
        .post-timestamp {
            font-size: 0.85em;
            color: #aaa;
            margin-left: auto; /* 推到最右边 */
            white-space: nowrap; /* 防止时间换行 */
            padding-left: 10px; /* 与左侧内容保持间距 */
        }

        /* 帖子主体 (内容、图片) */
        .post-body {
            margin-bottom: 10px;
            line-height: 1.6;
            word-wrap: break-word; /* 允许长单词换行 */
        }
        .post-text-content {
            margin-bottom: 10px;
        }
        .post-topic { /* 话题样式 */
            color: #007bff;
            font-weight: bold;
            margin-right: 3px;
        }
        .post-link { /* 链接样式 */
            color: #1e90ff;
            text-decoration: underline;
        }
        .post-images-container {
            display: flex;
            flex-wrap: wrap;
            gap: 5px; /* 图片间距 */
        }
        .post-image {
            max-width: 150px; /* 限制图片预览大小 */
            max-height: 150px;
            object-fit: cover; /* 保持图片比例并裁剪 */
            border: 1px solid #ddd;
            cursor: pointer;
            border-radius: 3px;
            transition: opacity 0.2s ease;
        }
        .post-image:hover {
             opacity: 0.8;
        }

        /* 帖子底部 (点赞、评论按钮、位置、原帖链接) */
        .post-footer {
            font-size: 0.9em;
            color: #888;
            border-top: 1px dashed #eee;
            padding-top: 8px;
            margin-top: 10px;
            display: flex;
            align-items: center;
            flex-wrap: wrap; /* 允许换行 */
            gap: 10px; /* 元素间距 */
        }
        .post-footer span, .post-footer a, .post-footer button {
            margin: 0; /* Reset default margins */
            vertical-align: middle; /* 垂直居中对齐 */
        }
        .locality-display, .share-link {
            font-size: inherit;
            color: inherit;
            text-decoration: none;
        }
        .share-link:hover {
            text-decoration: underline;
        }
        .like-status { /* 点赞操作状态提示 */
            font-size: 0.85em;
            margin-left: 5px;
            color: #888;
            font-style: italic;
        }

        /* --- 评论区样式 --- */
        .post-comments-list-container {
            background-color: #fdfdfd; /* 比帖子背景稍浅 */
            padding: 10px;
            border: 1px solid #f0f0f0;
            border-radius: 3px;
            margin-top: 10px;
        }
        .yuba-viewer-comment-item { /* 单条评论 */
            margin-bottom: 10px;
            padding-bottom: 8px;
            border-bottom: 1px dotted #eee;
        }
        .yuba-viewer-comment-item:last-child {
            margin-bottom: 0;
            padding-bottom: 0;
            border-bottom: none;
        }

        /* 评论头部 (头像、昵称、时间、位置) */
        .comment-header {
            display: flex;
            align-items: center;
            margin-bottom: 5px;
            flex-wrap: wrap; /* 允许换行 */
        }
        .comment-avatar {
            width: 25px;
            height: 25px;
            border-radius: 50%;
            margin-right: 8px;
        }
        .comment-nickname {
            font-weight: bold;
            color: #555;
            margin-right: 8px;
            font-size: 0.95em;
        }
        .comment-timestamp {
            font-size: 0.8em;
            color: #bbb;
            margin-left: auto; /* 推到右边 */
            white-space: nowrap;
            padding-left: 10px;
        }
        .comment-locality {
            font-size: 0.8em;
            color: #bbb;
            white-space: nowrap;
            margin-left: 5px; /* 与昵称或时间保持距离 */
        }

        /* 评论内容 */
        .comment-content {
            font-size: 0.95em;
            line-height: 1.5;
            margin-left: 33px; /* 与头像对齐 (25px + 8px margin) */
            word-wrap: break-word;
        }

        /* 楼中楼 (子评论) */
        .sub-comments-container {
            margin-left: 33px; /* 与评论内容对齐 */
            margin-top: 8px;
            border-left: 2px solid #eee;
            padding-left: 10px;
            font-size: 0.9em;
        }
        .sub-comment-item {
            margin-bottom: 5px;
            line-height: 1.4;
            color: #666;
        }
        .sub-comment-content {
            color: #444; /* 子评论内容颜色稍深 */
            margin-left: 2px; /* 轻微缩进 */
        }

        /* 加载更多评论按钮和状态 */
        .load-more-comments-button {
            display: block;
            margin: 10px auto 0;
            padding: 5px 10px;
            background-color: #eee;
            color: #555;
            border: 1px solid #ddd;
            border-radius: 3px;
            cursor: pointer;
            font-size: 0.9em;
            transition: background-color 0.2s ease;
        }
        .load-more-comments-button:hover:not(:disabled) {
             background-color: #e0e0e0;
        }
        .load-more-comments-button:disabled {
            background-color: #f5f5f5;
            cursor: not-allowed;
            color: #aaa;
        }
        .comments-load-status {
            text-align: center;
            font-size: 0.9em;
            color: #888;
            margin-top: 5px;
        }

        /* --- 评论输入区 --- */
        .post-comment-section {
            margin-top: 15px;
            padding-top: 10px;
            border-top: 1px solid #eee;
        }
        .reply-target-indicator { /* 回复提示 */
            border-left: 3px solid #ff6a00;
            padding-left: 8px;
            margin-bottom: 8px;
            background-color: #fff3e0; /* 淡橙色背景 */
            border-radius: 2px;
            padding-top: 3px;
            padding-bottom: 3px;
        }
        .cancel-reply-button { /* 取消回复按钮 */
            background: #eee;
            border: 1px solid #ccc;
            border-radius: 3px;
            cursor: pointer;
            padding: 1px 4px;
            font-size: 0.8em;
            margin-left: 5px;
            vertical-align: middle;
        }
        .cancel-reply-button:hover {
            background: #ddd;
        }
        .comment-input { /* 评论输入框 */
            width: calc(100% - 12px); /* 考虑 padding */
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 3px;
            font-size: 13px;
            margin-bottom: 5px;
            resize: vertical; /* 允许垂直调整大小 */
            min-height: 40px;
            box-sizing: border-box;
        }
        .comment-submit-button { /* 评论/回复提交按钮 */
            padding: 5px 12px;
            background-color: #ff6a00;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            font-size: 13px;
            float: right; /* 右对齐 */
            transition: background-color 0.2s ease;
        }
        .comment-submit-button:hover:not(:disabled) {
            background-color: #e05a00;
        }
        .comment-submit-button:disabled {
            background-color: #ccc;
            cursor: not-allowed;
        }
        .comment-status { /* 评论/回复操作状态 */
            font-size: 0.9em;
            margin-top: 5px;
            padding-left: 2px;
            min-height: 1.2em; /* 避免状态消失时跳动 */
            clear: both; /* 清除浮动 */
            color: #888;
        }

        /* --- 图标按钮通用样式 --- */
        .icon-button {
            background: none;
            border: none;
            padding: 0;
            cursor: pointer;
            display: inline-flex; /* 使图标和文字在同一行 */
            align-items: center; /* 垂直居中对齐 */
            font-size: inherit; /* 继承父元素字体大小 */
            color: #888; /* 默认图标颜色 */
            vertical-align: middle;
            transition: color 0.2s ease;
        }
        .icon-button:hover:not(:disabled) {
            color: #ff6a00; /* 悬停时变橙色 */
        }
        .icon-button:disabled {
            cursor: not-allowed;
            opacity: 0.7;
        }
        .icon-button svg {
            width: 16px;
            height: 16px;
            margin-right: 4px; /* 图标和文字间距 */
            vertical-align: middle; /* 确保 SVG 垂直居中 */
            fill: currentColor; /* SVG 颜色继承按钮颜色 */
        }

        /* 点赞按钮特殊样式 */
        .like-toggle-button[data-liked="true"] {
            color: rgb(255, 93, 103); /* 已点赞时的颜色 */
        }
        /* 已点赞时,确保 SVG 使用定义的渐变色 */
        .like-toggle-button[data-liked="true"] svg path {
             fill: url(#dzd_svg__a); /* 应用 SVG 内定义的渐变 */
        }

        /* 点赞数/评论数显示 */
        .like-count-display, .comment-count-display {
            margin-left: 0; /* 移除可能的默认边距 */
            font-size: inherit;
            color: inherit;
        }

        /* 回复按钮 */
        .reply-button {
            background: none;
            border: none;
            color: #888;
            cursor: pointer;
            font-size: 0.8em; /* 回复按钮小一点 */
            padding: 0 3px;
            vertical-align: middle;
            transition: color 0.2s ease;
        }
        .reply-button:hover {
            color: #ff6a00;
            text-decoration: underline;
        }
        .sub-reply-button { /* 楼中楼回复按钮的微调 */
             margin-left: 5px;
        }
    `);

    // --- 脚本入口 ---
    // 使用 'load' 事件确保页面基本结构加载完毕后再执行脚本
    window.addEventListener('load', () => {
        console.log("斗鱼指定鱼吧全能助手 v1.12.1 尝试初始化...");
        try {
            setupUI(); // 创建 UI 界面
            fetchAndRenderPosts(); // 开始加载第一页帖子
            console.log("斗鱼指定鱼吧全能助手 v1.12.1 已成功加载并运行");
        } catch (e) {
            console.error("脚本初始化或 UI 创建失败:", e);
            // 在页面上显示错误信息,方便调试
            const errorDiv = document.createElement('div');
            errorDiv.style.cssText = 'position:fixed; bottom:10px; left:10px; background:red; color:white; padding:10px; z-index:10000; border-radius: 5px; font-family: sans-serif; font-size: 12px;';
            errorDiv.textContent = `[鱼吧助手脚本错误] ${e.message}. 按 F12 查看控制台获取详细信息。`;
            document.body.appendChild(errorDiv);
        }
    });

})(); // 立即执行函数结束

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址