PD-CP出款&充值统计

自动捕获 Authorization/fingerprint/x-trace-id 并用 GM_xmlhttpRequest 请求 CP 出款接口 + 充值统计(简洁面板,可收起/展开)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         PD-CP出款&充值统计
// @namespace    http://tampermonkey.net/
// @version      0.6.0
// @description  自动捕获 Authorization/fingerprint/x-trace-id 并用 GM_xmlhttpRequest 请求 CP 出款接口 + 充值统计(简洁面板,可收起/展开)
// @author       Cisco
// @match        https://admin2-397-c1f073.j-d-0-q.com/*
// @grant        GM_notification
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      api2.b-4-s-f.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const NS = 'cpWithdrawGM';
    const CAPTURE_KEY = 'capturedHeaders';

    const CONFIG = {
        apiBaseUrl: 'https://api2.b-4-s-f.com',
        withdrawalPath: '/api/backend/trpc/withdrawal.allReviewedList',
        payPath: '/api/backend/trpc/payRecord.list',
        tenantId: 8801769,
        withdrawalChannels: 2561,
        pageSize: 50,
        refreshInterval: 15000, 
        amountIsCents: true, 
        maxPagesParallel: 6, 
        cacheTTL: 10000
    };

    GM_addStyle(`
        .${NS}-panel { position: fixed; top:20px; right:20px; width:320px; z-index:99999; background:#fff; border:1px solid #ddd; border-radius:6px; padding:12px; box-shadow:0 6px 18px rgba(0,0,0,0.08); font-family:Arial, sans-serif; transition: all 0.25s ease; }
        .${NS}-panel.${NS}_collapsed { width:44px; height:44px; padding:6px; overflow:hidden; }
        .${NS}-toggle { position:absolute; top:8px; right:8px; width:30px; height:30px; border-radius:50%; border:none; background:#f0f0f0; cursor:pointer; display:flex; align-items:center; justify-content:center; }
        .${NS}-header { color:#409EFF; font-weight:700; font-size:15px; margin-bottom:10px; }
        .${NS}-stat-row { display:flex; justify-content:space-between; align-items:center; padding:8px; margin-bottom:8px; background:#fafafa; border-radius:6px; border-left:3px solid #409EFF; }
        .${NS}-stat-row span { font-size:13px; color:#444; }
        .${NS}-stat-row .value { font-weight:700; background:#fff; padding:4px 8px; border-radius:4px; min-width:70px; text-align:center; }
        .${NS}-btn { width:100%; padding:9px; border-radius:6px; border:none; color:#fff; font-weight:700; cursor:pointer; margin-bottom:8px; }
        .${NS}-btn.start { background:#67C23A; } .${NS}-btn.stop{ background:#F56C6C; } .${NS}-btn.clear{ background:#909399; }
        .${NS}-meta { font-size:12px; color:#666; border-top:1px dashed #eee; padding-top:8px; margin-top:6px; }
    `);

    let apiCache = {};
    let dataHistory = GM_getValue(`${NS}_dataHistory`, []) || [];
    let autoInterval = null;
    let isRunning = false;
    let headersCaptured = Boolean(GM_getValue(CAPTURE_KEY, null));

    function getUTC03Range() {
        const now = new Date();
        const y = now.getUTCFullYear(), m = now.getUTCMonth(), d = now.getUTCDate();
        let start = new Date(Date.UTC(y,m,d,3,0,0,0));
        if (now.getTime() < start.getTime()) start = new Date(start.getTime() - 24*3600*1000);
        const end = new Date(start.getTime() + 24*3600*1000 - 1000);
        return { startISO: start.toISOString(), endISO: end.toISOString(), text: `${start.toISOString()} → ${end.toISOString()}` };
    }

    function formatAmount(raw) {
        if (raw === null || raw === undefined || isNaN(Number(raw))) return '--';
        return Math.floor(Number(raw) / 100);
    }

    function addPanel() {
        if (document.getElementById(`${NS}_panel`)) return;
        const panel = document.createElement('div');
        panel.id = `${NS}_panel`;
        panel.className = `${NS}-panel`;
        panel.innerHTML = `
            <button class="${NS}-toggle" id="${NS}_toggle">×</button>
            <div class="${NS}-header">📊 CP 出款&充值统计</div>

            <!-- 充值统计 -->
            <div class="${NS}-stat-row"><span>充值总额</span><span class="value" id="${NS}_payTotal">--</span></div>
            <div class="${NS}-stat-row"><span>充值人数</span><span class="value" id="${NS}_payUsers">--</span></div>

            <!-- 出款统计 -->
            <div class="${NS}-stat-row"><span>今日提现总金额</span><span class="value" id="${NS}_totalAmount">--</span></div>
            <div class="${NS}-stat-row"><span>今日CP出款比列</span><span class="value" id="${NS}_cpRatio">--</span></div>
            <div class="${NS}-stat-row"><span>今日充提差</span><span class="value" id="${NS}_chargeWithdrawDiff">--</span></div>

            <div style="margin-top:8px;">
                <button id="${NS}_start" class="${NS}-btn start">开始统计</button>
                <button id="${NS}_stop" class="${NS}-btn stop" style="display:none">停止统计</button>
                <button id="${NS}_clear" class="${NS}-btn clear">清理缓存</button>
            </div>

            <div class="${NS}-meta">
                <div>状态: <span id="${NS}_status">等待捕获头</span></div>
                <div>最后更新: <span id="${NS}_last">--</span> &nbsp; 下次更新: <span id="${NS}_next">--</span></div>
            </div>
        `;
        document.body.appendChild(panel);
        document.getElementById(`${NS}_toggle`).addEventListener('click', togglePanel);
        document.getElementById(`${NS}_start`).addEventListener('click', start);
        document.getElementById(`${NS}_stop`).addEventListener('click', stop);
        document.getElementById(`${NS}_clear`).addEventListener('click', clearAll);
        updateHeaderStatus();
    }

    function togglePanel() {
        const p = document.getElementById(`${NS}_panel`);
        const isCollapsed = p.classList.toggle(`${NS}_collapsed`);
        const btn = document.getElementById(`${NS}_toggle`);
        btn.innerText = isCollapsed ? '≡' : '×';
    }

    function renderStats(res) {
        const cpRatio = (res.cpUserCount && res.payUsers) ? ((res.cpUserCount / res.payUsers) * 100).toFixed(2) + '%' : '--';
        const chargeWithdrawDiff = (res.totalAmount && res.cpAmount && res.payTotal) 
            ? (((res.totalAmount - res.cpAmount) / res.payTotal) * 100).toFixed(2) + '%' 
            : '--';

        document.getElementById(`${NS}_totalAmount`).textContent = res.totalAmount ?? '--'; // 今日提现总金额
        document.getElementById(`${NS}_cpRatio`).textContent = cpRatio;                  // 今日CP出款比例
        document.getElementById(`${NS}_chargeWithdrawDiff`).textContent = chargeWithdrawDiff;          // 今日充提差

        document.getElementById(`${NS}_payTotal`).textContent = res.payTotal ?? '--';
        document.getElementById(`${NS}_payUsers`).textContent = res.payUsers ?? '--';

        document.getElementById(`${NS}_last`).textContent = new Date().toLocaleTimeString();
    }

    function updateHeaderStatus() {
        const s = document.getElementById(`${NS}_status`);
        const st = GM_getValue(CAPTURE_KEY, null);
        headersCaptured = !!st;
        if (s) {
            s.textContent = headersCaptured ? '已捕获头 ✓' : '等待捕获头...';
            s.style.color = headersCaptured ? '#67C23A' : '#E6A23C';
        }
    }

    function updateNextText() {
        const el = document.getElementById(`${NS}_next`);
        if (!el) return;
        el.textContent = isRunning ? new Date(Date.now() + CONFIG.refreshInterval).toLocaleTimeString() : '--';
    }

    /* ---------- 自动捕获 headers ---------- */
    function setupAutoCapture() {
        if (window.__cp_capture_installed) return;
        window.__cp_capture_installed = true;

        const nativeFetch = window.fetch;
        window.fetch = function(input, init) {
            try { const req = new Request(input, init); attemptCapture(Object.fromEntries(req.headers.entries())); } catch(e){}
            return nativeFetch.apply(this, arguments);
        };

        const origOpen = XMLHttpRequest.prototype.open;
        const origSet = XMLHttpRequest.prototype.setRequestHeader;
        const origSend = XMLHttpRequest.prototype.send;

        XMLHttpRequest.prototype.open = function(method, url) { this.__cp_headers = {}; return origOpen.apply(this, arguments); };
        XMLHttpRequest.prototype.setRequestHeader = function(k,v){ this.__cp_headers[k.toLowerCase()]=v; return origSet.apply(this, arguments); };
        XMLHttpRequest.prototype.send = function(){ attemptCapture(this.__cp_headers); return origSend.apply(this, arguments); };
    }

    function attemptCapture(headers) {
        if (!headers || GM_getValue(CAPTURE_KEY, null)) return;
        const useful = {};
        if (headers['authorization']) useful['authorization']=headers['authorization'];
        if (headers['fingerprint-id']) useful['fingerprint-id']=headers['fingerprint-id'];
        if (headers['x-trace-id']) useful['x-trace-id']=headers['x-trace-id'];
        if (headers['cookie']) useful['cookie']=headers['cookie'];
        if (Object.keys(useful).length>0) GM_setValue(CAPTURE_KEY,{capturedAt:new Date().toISOString(),useful,all:headers});
        updateHeaderStatus();
    }

    /**
     * 尝试通过在页面上下文触发一次 fetch 来促使页面自己刷新/注入 token,
     * 然后等待 setupAutoCapture 捕获新头并写入 GM 存储。
     *
     * timeoutMs: 等待捕获新头的最大时间(毫秒)
     * triggerPath: 用于触发的接口路径(默认使用 payPath)
     *
     * 返回 Promise<boolean>:true 表示捕获到新头,false 表示失败
     */
    function tryAutoRecoverAuth(timeoutMs = 4000, triggerPath = CONFIG.payPath) {
        return new Promise((resolve) => {
            try {
                // 1) 清除旧的捕获,确保后续捕获会写入新的值
                try { GM_deleteValue(CAPTURE_KEY); } catch (e) { /* 忽略 */ }

                // 2) 确保拦截器已安装(如果没有会安装)
                try { setupAutoCapture(); } catch (e) { /* 忽略 */ }

                // 3) 构造会触发页面自己组装 token 的 URL(使用 payPath)
                const range = getUTC03Range();
                const payload = {
                    json: {
                        queryType: 'statistics',
                        page: 1,
                        pageSize: 1,
                        status: 'PAID',
                        timeType: 'createTime',
                        regionId: 1,
                        tenantId: CONFIG.tenantId,
                        startTime: range.startISO,
                        endTime: range.endISO,
                        tableType: 'all'
                    }
                };
                const triggerUrl = `${CONFIG.apiBaseUrl}${triggerPath}?input=${encodeURIComponent(JSON.stringify(payload))}`;

                // 4) 在页面上下文注入并执行 fetch(credentials: 'include' 确保带 cookie)
                //    我们不关心响应体,只要页面发出了请求,setupAutoCapture 就能捕获头
                const scriptId = '__cp_try_recover_fetch';
                try {
                    // 移除可能残留的旧脚本
                    const old = document.getElementById(scriptId);
                    if (old) old.remove();

                    const script = document.createElement('script');
                    script.id = scriptId;
                    script.type = 'text/javascript';
                    script.textContent = `
                        (function(){
                            try {
                                fetch(${JSON.stringify(triggerUrl)}, { method: 'GET', credentials: 'include' })
                                .then(()=>{/* done */}).catch(()=>{/* ignore */});
                            } catch(e) {}
                        })();
                    `;
                    // 插入并稍后移除
                    document.documentElement.appendChild(script);
                } catch (e) {
                    // 如果注入失败(极少见),后面仍会等待但很可能超时
                    console.warn('注入触发脚本失败', e);
                }

                // 5) 轮询检查 GM 存储中是否写入了新的捕获头
                const start = Date.now();
                const checkInterval = 250;
                const checker = setInterval(() => {
                    try {
                        const stored = GM_getValue(CAPTURE_KEY, null);
                        if (stored && stored.useful && (stored.useful.authorization || stored.useful['fingerprint-id'] || stored.useful['x-trace-id'] || stored.useful.cookie)) {
                            clearInterval(checker);
                            // 清理注入脚本
                            try { document.getElementById(scriptId)?.remove(); } catch (e) {}
                            resolve(true);
                            return;
                        }
                    } catch (e) {
                        // 读取 GM 可能抛错,忽略继续等待
                    }
                    if (Date.now() - start > timeoutMs) {
                        clearInterval(checker);
                        try { document.getElementById(scriptId)?.remove(); } catch (e) {}
                        resolve(false);
                    }
                }, checkInterval);

            } catch (err) {
                console.warn('tryAutoRecoverAuth 异常', err);
                resolve(false);
            }
        });
    }

    /**
     * - 若检测到 401/未授权 或 接口返回结构表明 token 失效 => 尝试自动恢复 auth(tryAutoRecoverAuth)
     * - 恢复成功后重试一次请求(只重试一次以避免无限循环)
     */
    function gmFetchJson(url, timeout = 20000, maxRetries = 1) {
        return new Promise((resolve, reject) => {
            let attempt = 0;
            let retriedAfterRecover = false;

            const doRequest = () => {
                attempt++;
                const stored = GM_getValue(CAPTURE_KEY, null);
                const headers = {'accept':'*/*','content-type':'application/json'};
                if (stored && stored.useful) Object.assign(headers, stored.useful);
                // 如果没有 cookie 且页面有 cookie,则注入
                try { if (!headers.cookie && document.cookie) headers.cookie = document.cookie; } catch (e) {}

                try {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        headers: headers,
                        responseType: 'text',
                        timeout: timeout,
                        onload: function(res) {
                            // 如果 401, 先尝试自动恢复 token
                            if (res.status === 401 || res.status === 403 || (res.status >= 200 && res.status < 300 && (res.responseText||'').toLowerCase().includes('unauthorized'))) {
                                console.warn('请求返回未授权或401/403', res.status);
                                // 在遇到 401/403 的时候
                                if (!retriedAfterRecover) {
                                    retriedAfterRecover = true;
                                    const recovered = tryAutoRecoverAuth(4000, CONFIG.payPath); // 强制尝试恢复,不再被已有 token 的存在短路
                                    if (recovered) {
                                        setTimeout(doRequest, 300);
                                        return;
                                    } else {
                                        reject(new Error('未授权(401/403),且自动恢复 token 失败'));
                                        return;
                                    }
                                } else {
                                    const err = new Error('未授权(401/403),重试后仍失败');
                                    reject(err);
                                    return;
                                }
                            }

                            // 非 2xx 状态按错误处理(可重试)
                            if (!(res.status >= 200 && res.status < 300)) {
                                const err = new Error(`HTTP ${res.status}`);
                                if (attempt <= maxRetries) {
                                    setTimeout(doRequest, 200 * attempt);
                                } else {
                                    reject(err);
                                }
                                return;
                            }

                            // 2xx 状态,尝试解析 JSON
                            try {
                                const parsed = JSON.parse(res.responseText);
                                resolve(parsed);
                            } catch (e) {
                                // JSON 解析失败,视作错误,可重试
                                const err = new Error('响应 JSON 解析失败: ' + (e.message || e));
                                if (attempt <= maxRetries) {
                                    setTimeout(doRequest, 200 * attempt);
                                } else {
                                    reject(err);
                                }
                            }
                        },
                        onerror: function(err) {
                            const e = new Error('GM_xmlhttpRequest 网络错误');
                            if (attempt <= maxRetries) {
                                setTimeout(doRequest, 200 * attempt);
                            } else {
                                reject(e);
                            }
                        },
                        ontimeout: function() {
                            const e = new Error('GM_xmlhttpRequest 超时');
                            if (attempt <= maxRetries) {
                                setTimeout(doRequest, 200 * attempt);
                            } else {
                                reject(e);
                            }
                        }
                    });
                } catch (e) {
                    reject(new Error('GM_xmlhttpRequest 调用异常: ' + (e.message || e)));
                }
            };

            doRequest();
        });
    }

    /* ---------- 构造URL ---------- */
    // 今日出款提现数据
    function buildUrlForTotalWithdraw(page=1) {
        const range=getUTC03Range();
        const payload={json:{page,pageSize:CONFIG.pageSize,status:"success",queryTimeType:"completeTime",regionId:1,tenantId:CONFIG.tenantId,startTime:range.startISO,endTime:range.endISO,startTimeUTC:range.startISO,endTimeUTC:range.endISO}};
        return `${CONFIG.apiBaseUrl}${CONFIG.withdrawalPath}?input=${encodeURIComponent(JSON.stringify(payload))}`;
    }
    // 今日coinpay出款提现数据
    function buildUrlForWithdraw(page=1) {
        const range=getUTC03Range();
        const payload={json:{page,pageSize:CONFIG.pageSize,status:"success",queryTimeType:"completeTime",regionId:1,tenantId:CONFIG.tenantId,withdrawalChannels:CONFIG.withdrawalChannels,startTime:range.startISO,endTime:range.endISO,startTimeUTC:range.startISO,endTimeUTC:range.endISO}};
        return `${CONFIG.apiBaseUrl}${CONFIG.withdrawalPath}?input=${encodeURIComponent(JSON.stringify(payload))}`;
    }
    // 今日充值数据
    function buildUrlForPay() {
        const range = getUTC03Range();
        const payload={json:{queryType:'statistics',page:1,pageSize:50,status:'PAID',timeType:'createTime',regionId:1,tenantId:CONFIG.tenantId,startTime:range.startISO,endTime:range.endISO,tableType:'all'}};
        return `${CONFIG.apiBaseUrl}${CONFIG.payPath}?input=${encodeURIComponent(JSON.stringify(payload))}`;
    }

    /* ====== 更健壮的 fetchPayData:防止 result/data 未定义 并提供备用解析 ====== */
    async function fetchPayData() {
        try {
            const url = buildUrlForPay();
            const json = await gmFetchJson(url);

            // 严格检查路径 result -> data -> json -> totalInfo
            const totalInfo = json?.result?.data?.json?.totalInfo
                // 如果上面路径没有命中,尝试一些可能的变体(兼容性处理)
                || json?.data?.json?.totalInfo
                || json?.result?.data?.totalInfo
                || json?.result?.data?.json?.total   // 有些版本把 total 直接放这里
                || null;

            if (!totalInfo) {
                // 打印完整响应头 方便排查(只打印部分)
                console.warn('fetchPayData: 未在响应中找到 totalInfo,响应片段:', 
                    typeof json === 'object' ? JSON.stringify(json).slice(0,1000) : String(json));
                // 返回默认占位,不抛出异常,保证脚本继续运行
                return { payTotal: '--', payCount: '--', payUsers: '--' };
            }

            // 兼容 totalInfo 字段名与类型(部分接口用字符串数字)
            // 优先使用 totalPayAmount -> totalPayAmount 有可能是字符串 "1022200"
            const totalPayRaw = Number(totalInfo.totalPayAmount ?? 0);
            const totalCount = Number(totalInfo.total ?? 0);
            const totalUsers = Number(totalInfo.totalUser ?? 0);

            // 若解析出 NaN,保底置 0
            const safeTotalPay = isNaN(totalPayRaw) ? 0 : totalPayRaw;
            const safeCount = isNaN(totalCount) ? 0 : totalCount;
            const safeUsers = isNaN(totalUsers) ? 0 : totalUsers;

            return {
                payTotal: formatAmount(safeTotalPay), // 以分为单位,formatAmount 会除以100并取整
                payCount: safeCount,                   // 充值单数(原返回 total)
                payUsers: safeUsers                    // 充值人数
            };
        } catch (e) {
            console.warn('拉取充值失败(已捕获异常):', e);
            // 不抛出,返回占位,保证自动刷新不会中断
            return { payTotal: '--', payCount: '--', payUsers: '--' };
        }
    }

    /* ---------- 聚合今日coinpay出款统计 ---------- */
    async function fetchWithdrawData() {
        try {
            const range = getUTC03Range();

            // ========== 1️⃣ 分页查询总出款 ==========
            async function fetchTotalWithdraw() {
                let page = 1;
                let totalAmountRaw = 0;
                const pageSize = CONFIG.pageSize;

                while (true) {
                    const payload = {
                        json: {
                            page,
                            pageSize,
                            status: "success",
                            queryTimeType: "completeTime",
                            regionId: 1,
                            tenantId: CONFIG.tenantId,
                            startTime: range.startISO,
                            endTime: range.endISO
                        }
                    };

                    const url = `${CONFIG.apiBaseUrl}${CONFIG.withdrawalPath}?input=${encodeURIComponent(JSON.stringify(payload))}`;
                    const json = await gmFetchJson(url);
                    const items = json?.result?.data?.json?.queryData ?? [];

                    // 累计金额
                    for (const it of items) {
                        totalAmountRaw += Number(it.actualWithdrawals ?? it.amount ?? 0);
                    }

                    if (items.length < pageSize) break; // 没有下一页
                    page++;
                }

                return totalAmountRaw;
            }

            // ========== 2️⃣ 分页查询 CoinPay 出款 ==========
            async function fetchCoinPayWithdraw() {
                let page = 1;
                let cpAmountRaw = 0;
                const cpUsers = new Set();
                const pageSize = CONFIG.pageSize;

                while (true) {
                    const payload = {
                        json: {
                            page,
                            pageSize,
                            status: "success",
                            queryTimeType: "completeTime",
                            regionId: 1,
                            tenantId: CONFIG.tenantId,
                            withdrawalChannels: CONFIG.withdrawalChannels, // ★ 关键:区分 CoinPay 出款
                            startTime: range.startISO,
                            endTime: range.endISO
                        }
                    };

                    const url = `${CONFIG.apiBaseUrl}${CONFIG.withdrawalPath}?input=${encodeURIComponent(JSON.stringify(payload))}`;
                    const json = await gmFetchJson(url);
                    const items = json?.result?.data?.json?.queryData ?? [];

                    for (const it of items) {
                        cpAmountRaw += Number(it.actualWithdrawals ?? it.amount ?? 0);
                        if (it.userId) cpUsers.add(String(it.userId));
                    }

                    if (items.length < pageSize) break;
                    page++;
                }

                return { cpAmountRaw, cpUsers };
            }

            // ========== 3️⃣ 执行两个分页任务 ==========
            const totalAmountRaw = await fetchTotalWithdraw();
            const { cpAmountRaw, cpUsers } = await fetchCoinPayWithdraw();

            // ========== 4️⃣ 返回统一结构 ==========
            return {
                totalAmount: formatAmount(totalAmountRaw),   // 今日总出款
                cpAmount: formatAmount(cpAmountRaw),         // 今日 CP 出款
                cpUserCount: cpUsers.size                    // 今日 CP 出款人数
            };

        } catch (e) {
            console.warn('拉取出款失败', e);
            return { totalAmount: '--', cpAmount: '--', cpUserCount: '--' };
        }
    }


    async function collectAndCompute() {
        const [withdraw,pay] = await Promise.all([fetchWithdrawData(), fetchPayData()]);
        const result = {...withdraw,...pay};
        dataHistory.push({ts:Date.now(),range:getUTC03Range().text,data:result});
        GM_setValue(`${NS}_dataHistory`, dataHistory.slice(-50));
        return {result, range:getUTC03Range().text};
    }

    async function autoRefresh() {
        if(!headersCaptured) return;
        updateNextText();
        try{
            const {result,range} = await collectAndCompute();
            renderStats(result,range);
        }catch(e){ console.error('自动刷新失败',e); }
    }

    function start() {
        if(!headersCaptured){ alert('请先触发任意 API 请求以捕获头信息'); return; }
        if(isRunning) return;
        isRunning=true;
        document.getElementById(`${NS}_start`).style.display='none';
        document.getElementById(`${NS}_stop`).style.display='block';
        autoRefresh();
        autoInterval=setInterval(autoRefresh,CONFIG.refreshInterval);
    }

    function stop() {
        if(!isRunning) return;
        isRunning=false;
        clearInterval(autoInterval);
        document.getElementById(`${NS}_start`).style.display='block';
        document.getElementById(`${NS}_stop`).style.display='none';
        updateNextText();
    }

    function clearAll() {
        apiCache={};
        dataHistory=[];
        GM_deleteValue(`${NS}_dataHistory`);
        renderStats({ totalAmount:'--', totalCount:'--', userSum:'--', avgAmount:'--', cpRatio:'--', payTotal:'--', payCount:'--', payUsers:'--' }, '--');
    }

    addPanel();
    setupAutoCapture();
})();