littlebig / 印迹异常需求 V1.0

// ==UserScript==
// @name         印迹异常需求 V1.0
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  查看当前页面上是否有今天到期、已过期、已提交但没写体验目标的需求
// @license      MIT
// @copyright    2025, littlebig (https://openuserjs.org/users/littlebig)
// @author       littlebig
// @match        https://ingee.meituan.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @grant        unsafeWindow
// ==/UserScript==

// ==OpenUserJS==
// @author littlebig
// ==/OpenUserJS==

(function() {
    'use_strict';

    // --- START: Configuration & Global Variables ---
    const SCRIPT_B_NAME = "印迹异常需求 V1.0 (v4.3.3)";
    let extractedData = {};
    const resultContainerId = 'batch-project-data-extractor-result';
    let isFetchingGlobal = false;
    let refreshButton;
    let popupContentElement;

    const SCRIPT_STATE_KEY = 'meituanExtractor_v4_3_3_scriptState';
    const LAST_REFRESH_TIMESTAMP_KEY = 'meituanExtractor_v4_3_3_lastRefreshTime';
    const THROTTLE_DURATION_MS = 30 * 1000;
    const INVALID_DATE_STRING = "0000-00-00";
    const DESIGN_AIM_TARGET_STATUS = "设计中"; // This constant's value is "设计中"

    let g_passivelyCapturedSchemes = [];
    const TARGET_API_GET_SCHEMES = '/api/search/get-schemes';

    const trueOriginalFetch = unsafeWindow.fetch;
    const trueOriginalXhrOpen = unsafeWindow.XMLHttpRequest.prototype.open;
    const trueOriginalXhrSend = unsafeWindow.XMLHttpRequest.prototype.send;

    let expandDataPanelButton = null;
    let resultContainerElement = null;
    const MASK_SELECTOR = 'div.mtd-drawer-mask';
    // --- END: Configuration & Global Variables ---

    // --- START: Consolidated Network Interception ---
    unsafeWindow.fetch = async function(input, init) {
        const url = (typeof input === 'string' || input instanceof URL) ? String(input) : (input ? input.url : '');
        if (url && url.includes(TARGET_API_GET_SCHEMES)) {
            // console.log(`[${SCRIPT_B_NAME}] Passively Intercepting fetch for:`, url); // Kept for basic logging
            try {
                const response = await trueOriginalFetch.apply(this, arguments);
                const clonedResponse = response.clone();
                clonedResponse.json().then(data => {
                    if (data && data.data && Array.isArray(data.data.schemes)) {
                        g_passivelyCapturedSchemes = data.data.schemes.map(item => ({
                            id: item.id, name: item.name || `[未命名-${item.id}]`
                        })).filter(s => s.id !== undefined && s.id !== null);
                        // console.log(`[${SCRIPT_B_NAME}] Passively captured scheme data (Fetch): ${g_passivelyCapturedSchemes.length} items.`);
                    } else {
                        console.warn(`[${SCRIPT_B_NAME}] Fetch ${TARGET_API_GET_SCHEMES}: response structure not as expected or no schemes found.`, data);
                    }
                }).catch(e => console.error(`[${SCRIPT_B_NAME}] Error parsing JSON from fetched ${TARGET_API_GET_SCHEMES}:`, e));
                return response;
            } catch (error) {
                console.error(`[${SCRIPT_B_NAME}] Fetch interception error for ${TARGET_API_GET_SCHEMES}:`, error);
                throw error;
            }
        }
        return trueOriginalFetch.apply(this, arguments);
    };
    unsafeWindow.XMLHttpRequest.prototype.open = function(method, url) {
        this._tampermonkey_merged_url = url;
        return trueOriginalXhrOpen.apply(this, arguments);
    };
    unsafeWindow.XMLHttpRequest.prototype.send = function() {
        this.addEventListener('load', function() {
            if (this._tampermonkey_merged_url && this._tampermonkey_merged_url.includes(TARGET_API_GET_SCHEMES)) {
                // console.log(`[${SCRIPT_B_NAME}] Passively Intercepting XHR for:`, this._tampermonkey_merged_url);
                try {
                    const responseData = JSON.parse(this.responseText);
                    if (responseData && responseData.data && Array.isArray(responseData.data.schemes)) {
                        g_passivelyCapturedSchemes = responseData.data.schemes.map(item => ({
                            id: item.id, name: item.name || `[未命名-${item.id}]`
                        })).filter(s => s.id !== undefined && s.id !== null);
                        // console.log(`[${SCRIPT_B_NAME}] Passively captured scheme data (XHR): ${g_passivelyCapturedSchemes.length} items.`);
                    } else {
                         console.warn(`[${SCRIPT_B_NAME}] XHR ${TARGET_API_GET_SCHEMES}: response structure not as expected or no schemes found.`, responseData);
                    }
                } catch (e) { console.error(`[${SCRIPT_B_NAME}] Error parsing JSON from XHR ${TARGET_API_GET_SCHEMES}:`, e); }
            }
        });
        return trueOriginalXhrSend.apply(this, arguments);
    };
    // --- END: Consolidated Network Interception ---

    // --- START: Helper Functions ---
    function showToast(message, duration = 3000) {
        let toast = document.createElement('div');
        toast.textContent = message;
        Object.assign(toast.style, {
            position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
            backgroundColor: 'rgba(0,0,0,0.7)', color: 'white', padding: '12px 25px',
            borderRadius: '6px', zIndex: '10001', fontSize: '12px', textAlign: 'center'
        });
        document.body.appendChild(toast);
        setTimeout(() => { if (toast.parentNode) document.body.removeChild(toast); }, duration);
    }

    function updateButtonState(isLoading, message = '') {
        if (refreshButton) {
            refreshButton.disabled = isLoading;
            refreshButton.textContent = isLoading ? (message || '处理中...') : '查看异常';
        }
    }

    function resetScriptStateToIdle() {
        updateButtonState(false);
        isFetchingGlobal = false;
        // console.log(`[${SCRIPT_B_NAME}] Script state reset to idle. isFetchingGlobal:`, isFetchingGlobal);
    }

    function togglePanelVisibility(show) {
        if (!resultContainerElement || !expandDataPanelButton) { return; }
        if (show) {
            resultContainerElement.style.display = 'flex';
            expandDataPanelButton.style.display = 'none';
        } else {
            resultContainerElement.style.display = 'none';
            expandDataPanelButton.style.display = 'flex';
        }
    }

    function waitForMaskToHide(maskElement, timeoutMs) {
        return new Promise((resolve, reject) => {
            if (!maskElement || !maskElement.isConnected || window.getComputedStyle(maskElement).display === 'none') {
                resolve(); return;
            }
            const observer = new MutationObserver((mutationsList, obs) => {
                if (!maskElement.isConnected || window.getComputedStyle(maskElement).display === 'none') {
                    obs.disconnect(); clearTimeout(timeout); resolve();
                }
            });
            const timeout = setTimeout(() => {
                observer.disconnect();
                if (!maskElement.isConnected || window.getComputedStyle(maskElement).display === 'none') {
                    resolve();
                } else {
                    reject(new Error('Timeout waiting for mask to hide.'));
                }
            }, timeoutMs);
            observer.observe(maskElement, { attributes: true, attributeFilter: ['style'] });
            if (maskElement.parentNode) {
                observer.observe(maskElement.parentNode, { childList: true });
            }
        });
    }
    // --- END: Helper Functions ---

    // --- START: Core Logic Functions ---
    async function handlePopupItemClick(event) {
        const clickedLink = event.target.closest('.scheme-link-js');
        if (!clickedLink) return;
        const schemeId = clickedLink.dataset.schemeId;
        const schemeName = clickedLink.dataset.schemeName;
        if (!schemeName) {
            showToast("无法获取项目名称以进行定位。", 3000); return;
        }
        const proceedWithNavigation = () => findAndClickMainPageRowByName(schemeName, schemeId);
        const drawerMask = document.querySelector(MASK_SELECTOR);
        if (drawerMask && window.getComputedStyle(drawerMask).display !== 'none') {
            console.log(`[${SCRIPT_B_NAME}] Drawer mask is visible. Attempting to click it.`);
            drawerMask.click();
            try {
                await waitForMaskToHide(drawerMask, 5000);
                console.log(`[${SCRIPT_B_NAME}] Drawer mask successfully hidden or removed.`);
                proceedWithNavigation();
            } catch (error) {
                console.warn(`[${SCRIPT_B_NAME}] Drawer mask did not hide in time or error waiting:`, error.message);
            }
        } else {
            proceedWithNavigation();
        }
    }

    function findAndClickMainPageRowByName(schemeNameToFind, schemeIdForLog) {
        const tableRows = document.querySelectorAll('tr.mtd-ingee-table-row');
        let foundRow = null;
        for (const row of tableRows) {
            const nameElement = row.querySelector('td:first-child div.name > span');
            if (nameElement && nameElement.textContent.trim() === schemeNameToFind.trim()) {
                foundRow = row; break;
            }
        }
        if (foundRow) {
            foundRow.click();
        } else {
            showToast(`无法在主页面找到项目 "${schemeNameToFind}"。请确保其在当前表格页可见。`, 4000);
        }
    }

    async function fetchSchemeDetails() {
        const schemeIds = Object.keys(extractedData);
        updatePanelHeaderAndContent(`<i>正在加载 ${schemeIds.length} 个项目的详细信息...</i>`, false);
        const today = new Date().toISOString().slice(0, 10);
        let detailFetchErrors = 0;

        for (const schemeId of schemeIds) {
            const apiUrl = `https://ingee.meituan.com/api/schemes/${schemeId}`;
            try {
                const response = await unsafeWindow.fetch(apiUrl);
                if (!response.ok) {
                    detailFetchErrors++; if (extractedData[schemeId]) extractedData[schemeId].hasSpecialMarking = false;
                    console.warn(`[${SCRIPT_B_NAME}] Error fetching details for ${schemeId}: ${response.status}`); continue;
                }
                const schemeDetails = await response.json();
                if (schemeDetails && schemeDetails.data) {
                    const schemeInfo = extractedData[schemeId];
                    if (schemeDetails.data.name && (!schemeInfo.name || schemeInfo.name.startsWith('[未命名-'))) schemeInfo.name = schemeDetails.data.name;
                    if (!schemeInfo.name || schemeInfo.name.startsWith('[未命名-')) schemeInfo.name = `[需求名称未获取] ${schemeId}`;
                    Object.assign(schemeInfo, {
                        designAim: schemeDetails.data.design_aim, statusName: schemeDetails.data.status_name,
                        endTimes: [], planEndTimes: []
                    });
                    if (Array.isArray(schemeDetails.data.segments)) {
                        schemeDetails.data.segments.forEach(segment => {
                            schemeInfo.endTimes.push(segment.end_time ? segment.end_time.slice(0, 10) : null);
                            schemeInfo.planEndTimes.push(segment.plan_end_time ? segment.plan_end_time.slice(0, 10) : null);
                        });
                        schemeInfo.endTimes = schemeInfo.endTimes.slice(0, 2); schemeInfo.planEndTimes = schemeInfo.planEndTimes.slice(0, 2);
                    }

                    let itemHasMarking = false;
                    // MODIFIED: Check statusName against DESIGN_AIM_TARGET_STATUS for due/overdue
                    if ((schemeInfo.statusName || "").trim() === DESIGN_AIM_TARGET_STATUS) {
                        for (let i = 0; i < Math.min(schemeInfo.planEndTimes.length, 2) ; i++) {
                            const planTime = schemeInfo.planEndTimes[i];
                            const actualTime = schemeInfo.endTimes[i] || null;
                            if (!actualTime && planTime && planTime !== INVALID_DATE_STRING) {
                                if (planTime === today || planTime < today) {
                                    itemHasMarking = true; break;
                                }
                            }
                        }
                    }
                    // Condition for missing design aim (remains unchanged)
                    if (!itemHasMarking && (['待评价', '待验收', '待追踪'].includes((schemeInfo.statusName || "").trim())) && !(schemeInfo.designAim || "").trim()) {
                        itemHasMarking = true;
                    }
                    schemeInfo.hasSpecialMarking = itemHasMarking;
                } else { detailFetchErrors++; if (extractedData[schemeId]) extractedData[schemeId].hasSpecialMarking = false; }
            } catch (error) {
                detailFetchErrors++; if (extractedData[schemeId]) extractedData[schemeId].hasSpecialMarking = false;
                console.error(`[${SCRIPT_B_NAME}] Exception fetching details for ${schemeId}:`, error);
            }
        }
        if (detailFetchErrors > 0) showToast(`部分项目详情获取失败 (${detailFetchErrors}个)`, 4000);
        displayExtractedData(false);
    }

    function updatePanelHeaderAndContent(contentHtml, isInitialDisplay) {
        if (!resultContainerElement) return;
        const summaryBoxHtml = isInitialDisplay ? '' : generateSummaryBoxHtml();
        resultContainerElement.innerHTML = `
            <div id="popupHeader" style="position:relative; flex-shrink:0; background-color:white; z-index:1; padding:12px 12px ${isInitialDisplay ? '12px' : '0px'} 12px; border-bottom:1px solid #eee; border-radius:12px 12px 0 0;">
                <span id="collapse-panel-icon" title="收起" style="position:absolute; top:12px; right:12px; cursor:pointer; font-size:24px; line-height:1; color:#777; z-index:2;">&times;</span>
                <h3 style="cursor:move; margin-top:0; margin-bottom:0px; font-size:16px; font-weight: 500; padding-bottom:5px; padding-right: 25px;">当前页面异常需求</h3>
                ${summaryBoxHtml}
            </div>
            <div id="popupContent" style="flex-grow:1; overflow-y:auto; padding:10px 15px 15px 15px;">
                ${contentHtml}
            </div>`;
        popupContentElement = resultContainerElement.querySelector('#popupContent');
        if (popupContentElement) {
            popupContentElement.removeEventListener('click', handlePopupItemClick);
            popupContentElement.addEventListener('click', handlePopupItemClick);
        }
        const dragHandle = resultContainerElement.querySelector('#popupHeader > h3');
        if (dragHandle) makeDraggable(resultContainerElement, dragHandle);
        const collapseIcon = resultContainerElement.querySelector('#collapse-panel-icon');
        if (collapseIcon) collapseIcon.addEventListener('click', () => togglePanelVisibility(false));
    }

    function generateSummaryBoxHtml() {
        const today = new Date().toISOString().slice(0, 10);
        let countTodayExpired = 0, countOverdue = 0, countMissingDesignAim = 0;
        for (const schemeId in extractedData) {
            const schemeInfo = extractedData[schemeId];
            let isToday = false, isOver = false;
            // MODIFIED: Check statusName against DESIGN_AIM_TARGET_STATUS for counting due/overdue
            if ((schemeInfo.statusName || "").trim() === DESIGN_AIM_TARGET_STATUS) {
                for (let i = 0; i < Math.min(schemeInfo.planEndTimes.length, 2); i++) {
                    const planTime = schemeInfo.planEndTimes[i];
                    const actualTime = schemeInfo.endTimes[i] || null;
                    if (!actualTime && planTime && planTime !== INVALID_DATE_STRING) {
                        if (planTime === today) isToday = true;
                        else if (planTime < today) isOver = true;
                    }
                }
            }
            if(isToday) countTodayExpired++;
            if(isOver) countOverdue++;
            if ((['待评价', '待验收', '待追踪'].includes((schemeInfo.statusName || "").trim())) && !(schemeInfo.designAim || "").trim()) {
                countMissingDesignAim++;
            }
        }
        return `
            <div style="margin-bottom:10px; padding:10px; border:0px solid #eee; background-color:#f9f9f9; border-radius:8px; font-size:12px; line-height:1.8;">
              <span style="color:${countTodayExpired > 0 ? '#FF1F1F':'black'}; font-weight:bold;">${countTodayExpired}</span> 个需求今天到期 <br>
              <span style="color:${countOverdue > 0 ? '#FF1F1F':'black'}; font-weight:bold;">${countOverdue}</span> 个需求已过期 <br>
              <span style="color:${countMissingDesignAim > 0 ? '#FF7700':'black'}; font-weight:bold;">${countMissingDesignAim}</span> 个需求已提交但没写体验目标
            </div>`;
    }

    function displayExtractedData(isInitialOrNoDataMessage = false) {
        const today = new Date().toISOString().slice(0, 10);
        let displayedItemCount = 0;
        let itemsDisplayHtml = '';
        let contentHtmlForPanel;

        if (isInitialOrNoDataMessage) {
             if (isFetchingGlobal && (!g_passivelyCapturedSchemes || g_passivelyCapturedSchemes.length === 0)) {
                 contentHtmlForPanel = '<i>尚未捕获到项目列表数据。<br>请确保页面表格已加载或刷新后再试。</i>';
             } else {
                 contentHtmlForPanel = '<i>脚本已激活,被动捕获数据中。<br>请点击“查看异常”按钮处理当前已捕获的数据。</i>';
             }
        } else {
            for (const schemeId in extractedData) {
                const schemeInfo = extractedData[schemeId];
                if (!schemeInfo.hasSpecialMarking) continue;
                displayedItemCount++;
                let schemeNameForDisplay = schemeInfo.name || `[需求ID: ${schemeInfo.schemeId}]`;
                const schemeNameForDataAttr = String(schemeNameForDisplay).replace(/'/g, "&apos;").replace(/"/g, "&quot;");
                itemsDisplayHtml += `<div style="margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px dotted #eee;">
                                   <strong class="scheme-link-js" data-scheme-id="${schemeInfo.schemeId}" data-scheme-name="${schemeNameForDataAttr}" style="color: #333; cursor:pointer;" title="定位并点击主页表格中的: ${schemeNameForDataAttr}">${schemeNameForDisplay}</strong><br>`;
                const planEndTimeTexts = [];
                for (let i = 0; i < Math.max(schemeInfo.planEndTimes.length, schemeInfo.endTimes.length, 0); i++) {
                    if (i >= 2) break;
                    let planTime = schemeInfo.planEndTimes[i], actualTime = schemeInfo.endTimes[i];
                    let timeText;
                    if (planTime === INVALID_DATE_STRING) timeText = 'N/A (0000)';
                    else if (!planTime) timeText = 'N/A';
                    else {
                        timeText = planTime;
                        // MODIFIED: Check statusName against DESIGN_AIM_TARGET_STATUS for displaying due/overdue text
                        if (!actualTime && (schemeInfo.statusName || "").trim() === DESIGN_AIM_TARGET_STATUS) {
                            if (planTime === today) timeText += `<span style="color: #FF1F1F; font-weight: 500;"> (今天到期)</span>`;
                            else if (planTime < today) timeText += `<span style="color: #FF1F1F; font-weight: 500;"> (已过期)</span>`;
                        }
                    }
                    planEndTimeTexts.push(timeText);
                }
                while(planEndTimeTexts.length < 2) planEndTimeTexts.push('N/A');
                itemsDisplayHtml += `截止日期: ${planEndTimeTexts.slice(0,2).join(', ') || 'N/A, N/A'}<br>
                                     状态: ${schemeInfo.statusName || '-'}<br>
                                     体验目标: ${schemeInfo.designAim || '-'}`; // Display designAim here, even if not used for due/overdue logic
                if ((['待评价', '待验收', '待追踪'].includes((schemeInfo.statusName || "").trim())) && !(schemeInfo.designAim || "").trim()) {
                    itemsDisplayHtml += `<span style="color: #FF7700; font-weight: 500;"> (没写体验目标)</span>`;
                }
                itemsDisplayHtml += `</div>`;
            }
            if (Object.keys(extractedData).length > 0 && displayedItemCount === 0) {
                contentHtmlForPanel = '🎉 当前页面没有异常需求';
            } else if (displayedItemCount > 0) {
                contentHtmlForPanel = itemsDisplayHtml;
            } else {
                contentHtmlForPanel = '<i>数据处理完毕,但未找到可显示的项目</i>';
            }
        }
        updatePanelHeaderAndContent(contentHtmlForPanel, isInitialOrNoDataMessage && !isFetchingGlobal);
    }

    function makeDraggable(containerToMove, handleToDragBy) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        if (!handleToDragBy) { console.warn(`[${SCRIPT_B_NAME}] makeDraggable: Drag handle missing.`); return; }
        handleToDragBy.onmousedown = function(e) {
            e = e || window.event; let targetElement = e.target;
            if (targetElement && targetElement.id === 'collapse-panel-icon') return;
            while (targetElement && targetElement !== this) {
                if (targetElement.hasAttribute('onclick') || ['A', 'BUTTON'].includes(targetElement.tagName) ) return;
                targetElement = targetElement.parentNode;
            }
            e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY;
            document.onmouseup = closeDragElement; document.onmousemove = elementDrag;
        };
        function elementDrag(e) {
            e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY;
            pos3 = e.clientX; pos4 = e.clientY;
            containerToMove.style.top = (containerToMove.offsetTop - pos2) + "px";
            containerToMove.style.left = (containerToMove.offsetLeft - pos1) + "px";
        }
        function closeDragElement() { document.onmouseup = null; document.onmousemove = null; }
    }

    async function processCapturedSchemesAndFetchDetails(schemesList) {
        extractedData = {};
        console.log(`[${SCRIPT_B_NAME}] Processing ${schemesList.length} captured schemes for details.`);
        schemesList.forEach(scheme => {
            if (scheme.id) extractedData[scheme.id] = {
                schemeId: scheme.id, name: scheme.name,
                planEndTimes: [], designAim: null, statusName: null, endTimes: [], hasSpecialMarking: false
            };
        });
        // console.log(`[${SCRIPT_B_NAME}] Prepared extractedData: ${Object.keys(extractedData).length} items`);
        await fetchSchemeDetails();
    }

    async function handleRefreshButtonClick() {
        const now = Date.now();
        if (isFetchingGlobal) { showToast('数据仍在加载中...', 2000); return; }
        const lastRefresh = GM_getValue(LAST_REFRESH_TIMESTAMP_KEY, 0);
        if (now - lastRefresh < THROTTLE_DURATION_MS) {
            showToast(`🤔 操作过于频繁,请 ${Math.ceil((THROTTLE_DURATION_MS - (now - lastRefresh)) / 1000)} 秒后再试`, 3000); return;
        }
        togglePanelVisibility(true);
        updateButtonState(true, '处理中...');
        GM_setValue(LAST_REFRESH_TIMESTAMP_KEY, now);
        isFetchingGlobal = true;
        try {
            if (g_passivelyCapturedSchemes && g_passivelyCapturedSchemes.length > 0) {
                // console.log(`[${SCRIPT_B_NAME}] Using ${g_passivelyCapturedSchemes.length} passively captured schemes for processing.`);
                await processCapturedSchemesAndFetchDetails(g_passivelyCapturedSchemes);
            } else {
                console.log(`[${SCRIPT_B_NAME}] No schemes captured to process.`);
                showToast('尚未捕获到项目列表数据。请确保页面表格已加载或刷新后再试。', 4000);
                displayExtractedData(true);
            }
        } catch (error) {
            console.error(`[${SCRIPT_B_NAME}] Error during handleRefreshButtonClick:`, error);
            showToast("处理时发生错误", 3000);
            displayExtractedData(true);
        } finally {
            resetScriptStateToIdle();
        }
    }
    // --- END: Core Logic Functions ---

    // --- START: Initialization ---
    function initializeScript() {
        console.log(`[${SCRIPT_B_NAME}] Initializing...`);
        refreshButton = document.createElement('button');
        Object.assign(refreshButton.style, {
            position: 'fixed', bottom: '0px', left: '0px', zIndex: '10001', padding: '8px 18px',
            backgroundColor: '#FFFFFF', color: '#111925', border: '1px solid #eee', borderRadius: '0 12px 0 0',
            cursor: 'pointer', fontSize: '14px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
        });
        document.body.appendChild(refreshButton);
        refreshButton.addEventListener('click', handleRefreshButtonClick);

        resultContainerElement = document.getElementById(resultContainerId);
        if (!resultContainerElement) {
            resultContainerElement = document.createElement('div');
            resultContainerElement.id = resultContainerId;
            Object.assign(resultContainerElement.style, {
                position: 'fixed', top: '70px', left: '20px', backgroundColor: 'white', color: 'black',
                border: '1px solid #eee', borderRadius: '12px', width: '320px',
                boxShadow: '0 4px 8px rgba(0,0,0,0.1)', fontSize: '12px', lineHeight: '1.8',
                display: 'flex', flexDirection: 'column', maxHeight: 'calc(100vh - 110px)', zIndex: '10000'
            });
            document.body.appendChild(resultContainerElement);
        }

        expandDataPanelButton = document.createElement('button');
        expandDataPanelButton.id = 'tampermonkey-expand-panel-btn';
        expandDataPanelButton.innerHTML = '&#9652;';
        expandDataPanelButton.title = '展开面板';
        Object.assign(expandDataPanelButton.style, {
            display: 'none', position: 'fixed', bottom: '5px',
            left: (refreshButton.offsetLeft + refreshButton.offsetWidth + 62) + 'px',
            zIndex: '10001', width: '30px', height: '30px', backgroundColor: 'white',
            border: '0px solid #ccc', borderRadius: '50%', cursor: 'pointer', fontSize: '24px',
            lineHeight: '28px', textAlign: 'top', padding: '0 0 6px 0', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
            alignItems: 'center', justifyContent: 'center'
        });
        expandDataPanelButton.addEventListener('click', () => togglePanelVisibility(true));
        document.body.appendChild(expandDataPanelButton);

        displayExtractedData(true);

        if (resultContainerElement) resultContainerElement.style.display = 'none';
        if (expandDataPanelButton) expandDataPanelButton.style.display = 'none';

        isFetchingGlobal = false; updateButtonState(false);
        console.log(`[${SCRIPT_B_NAME}] Initialization complete. Panel initially hidden. Passively listening.`);
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initializeScript);
    else initializeScript();
    // --- END: Initialization ---

})();