Greasy Fork

ChatGPT模型调用记录

2024-9-18更新,记录并显示ChatGPT各模型的调用情况,清除/显示/编辑/下载/指定日期的调用情况。精确到每个时辰各模型调用情况。

// ==UserScript==
// @name         ChatGPT模型调用记录
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  2024-9-18更新,记录并显示ChatGPT各模型的调用情况,清除/显示/编辑/下载/指定日期的调用情况。精确到每个时辰各模型调用情况。
// @author       狐狸的狐狸画
// @match        https://chatgpt.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const modelVisibilityKey = 'model_visibility'; // 用于存储复选框状态的键
    const modelDisplayOrderKey = 'model_display_order'; // 用于存储勾选顺序的键
    const modelCountKey = 'model_counts'; // 存储全局计数的键
    const placeholderSymbol = '✨'; // 当所有模型未选中时显示的符号
    let selectedDate = new Date().toISOString().split('T')[0]; // 默认选中今天
    let savedFilePath = ''; // 保存文件的路径
    let displayOrder = JSON.parse(localStorage.getItem(modelDisplayOrderKey) || '[]'); // 从 localStorage 加载显示顺序

    // 拦截 fetch 请求以监控模型调用
    const originalFetch = window.fetch;
    window.fetch = function (url, options) {
        if (url.includes("/backend-api/conversation") && options && options.method === "POST") {
            try {
                const body = JSON.parse(options.body);
                if (body.model) {
                    updateModelCount(body.model); // 更新模型调用计数
                }
            } catch (e) {
                console.error('解析请求体失败:', e);
            }
        }
        return originalFetch.apply(this, arguments);
    };

    // 更新模型调用计数
    function updateModelCount(model) {
        const counts = JSON.parse(localStorage.getItem(modelCountKey) || '{}');
        counts[model] = (counts[model] || 0) + 1;
        localStorage.setItem(modelCountKey, JSON.stringify(counts));
        updateHourlyCounts(model);
        displayCounts();
    }

    // 更新每小时计数
    function updateHourlyCounts(model) {
        const date = new Date();
        const hours = date.getHours();
        const dayKey = date.toISOString().split('T')[0];
        const hourlyCountKey = `hourly_counts_${dayKey}`;
        const hourlyCounts = JSON.parse(localStorage.getItem(hourlyCountKey) || '{}');

        if (!hourlyCounts[hours]) {
            hourlyCounts[hours] = {};
        }

        hourlyCounts[hours][model] = (hourlyCounts[hours][model] || 0) + 1;
        localStorage.setItem(hourlyCountKey, JSON.stringify(hourlyCounts));
    }

    // 获取某一天每个模型的调用次数
    function getCountsForDate(date) {
        const hourlyCountKey = `hourly_counts_${date}`;
        const hourlyCounts = JSON.parse(localStorage.getItem(hourlyCountKey) || '{}');

        const countsByModel = {};
        for (let hour in hourlyCounts) {
            for (let model in hourlyCounts[hour]) {
                countsByModel[model] = (countsByModel[model] || 0) + hourlyCounts[hour][model];
            }
        }
        return { hourlyCounts, countsByModel };
    }

    // 保存选中日期的数据到文件并下载
    function saveSelectedDataToFile(selectedDates) {
        const allData = {};

        // 遍历选中的日期
        selectedDates.forEach(date => {
            const hourlyCountKey = `hourly_counts_${date}`;
            const hourlyCounts = JSON.parse(localStorage.getItem(hourlyCountKey) || '{}');
            allData[date] = hourlyCounts;
        });

        // 创建合并后的文件
        const blob = new Blob([JSON.stringify(allData, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `selected_model_usage_data.json`; // 合并后的文件名
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);

        URL.revokeObjectURL(url);
    }

    // 显示日期选择弹窗并处理下载操作
    function showDownloadDateSelection() {
        // 获取所有存储在 localStorage 中的日期
        const availableDates = Object.keys(localStorage).filter(key => key.startsWith('hourly_counts_')).map(key => key.replace('hourly_counts_', ''));

        // 构建弹窗内容
        let popup = document.createElement('div');
        popup.id = 'date-selection-popup';
        Object.assign(popup.style, {
            position: 'fixed',
            top: '20%',
            left: '50%',
            transform: 'translate(-50%, -20%)',
            backgroundColor: '#fff',
            padding: '20px',
            border: '1px solid #ccc',
            borderRadius: '10px',
            zIndex: '1002',
            width: '300px',
            boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.2)',
            textAlign: 'center',
        });

        let popupContent = `<strong>选择要下载的日期:</strong><br><br>`;
        availableDates.forEach(date => {
            const checked = (date === selectedDate) ? 'checked' : ''; // 默认勾选当天
            popupContent += `
                <div style="margin-bottom: 8px;">
                    <label>
                        <input type="checkbox" class="date-checkbox" data-date="${date}" ${checked}> ${date}
                    </label>
                </div>`;
        });

        popupContent += `
            <div style="margin-top: 15px;">
                <button id="download-selected-data-button" style="padding: 10px 20px; border: none; border-radius: 5px; background-color: #4CAF50; color: white; cursor: pointer;">下载所选日期数据</button>
                <button id="cancel-download-button" style="padding: 10px 20px; border: none; border-radius: 5px; background-color: #f44336; color: white; cursor: pointer; margin-left: 10px;">取消</button>
            </div>
        `;
        popup.innerHTML = popupContent;
        document.body.appendChild(popup);

        // 添加下载按钮事件处理器
        document.getElementById('download-selected-data-button').onclick = () => {
            const selectedDates = Array.from(document.querySelectorAll('.date-checkbox:checked')).map(checkbox => checkbox.dataset.date);
            if (selectedDates.length > 0) {
                saveSelectedDataToFile(selectedDates);
                document.body.removeChild(popup); // 关闭弹窗
            } else {
                alert('请选择至少一个日期下载数据!');
            }
        };

        // 添加取消按钮事件处理器
        document.getElementById('cancel-download-button').onclick = () => {
            document.body.removeChild(popup); // 关闭弹窗
        };
    }

    // 显示编辑数据面板(全局 + 选中日期)
    function showEditDataPanel() {
        const hourlyCountKey = `hourly_counts_${selectedDate}`;
        const dateData = JSON.parse(localStorage.getItem(hourlyCountKey) || '{}'); // 选中日期数据
        const globalData = JSON.parse(localStorage.getItem(modelCountKey) || '{}'); // 全局数据

        const combinedData = {
            globalCounts: globalData,
            dateCounts: dateData
        };

        const jsonData = JSON.stringify(combinedData, null, 2);

        // 创建编辑面板
        let panel = document.createElement('div');
        panel.id = 'edit-data-panel';
        Object.assign(panel.style, {
            position: 'fixed',
            top: '20%',
            left: '50%',
            transform: 'translate(-50%, -20%)',
            backgroundColor: '#fff',
            padding: '20px',
            border: '1px solid #ccc',
            borderRadius: '10px',
            zIndex: '1003',
            width: '500px',
            boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.2)',
            textAlign: 'center',
        });

        let panelContent = `
            <strong>编辑 ${selectedDate} 和全局计数的数据:</strong><br><br>
            <textarea id="edit-json-data" style="width: 100%; height: 250px; padding: 10px; font-family: monospace;">${jsonData}</textarea><br><br>
            <button id="save-data-button" style="padding: 10px 20px; border: none; border-radius: 5px; background-color: #4CAF50; color: white; cursor: pointer;">保存</button>
            <button id="cancel-edit-button" style="padding: 10px 20px; border: none; border-radius: 5px; background-color: #f44336; color: white; cursor: pointer; margin-left: 10px;">取消</button>
        `;
        panel.innerHTML = panelContent;
        document.body.appendChild(panel);

        // 保存数据按钮事件
        document.getElementById('save-data-button').onclick = () => {
            try {
                const editedData = JSON.parse(document.getElementById('edit-json-data').value);

                // 分离全局数据和日期数据
                const newGlobalData = editedData.globalCounts;
                const newDateData = editedData.dateCounts;

                // 保存全局数据
                localStorage.setItem(modelCountKey, JSON.stringify(newGlobalData));
                // 保存选中日期的数据
                localStorage.setItem(hourlyCountKey, JSON.stringify(newDateData));

                alert('数据已保存!');
                document.body.removeChild(panel); // 关闭面板
            } catch (error) {
                alert('JSON 格式错误,请检查并重新输入!');
            }
        };

        // 取消按钮事件
        document.getElementById('cancel-edit-button').onclick = () => {
            document.body.removeChild(panel); // 关闭面板
        };
    }

    // 显示计数,按勾选顺序排列
    function displayCounts() {
        const counts = JSON.parse(localStorage.getItem(modelCountKey) || '{}');
        const visibility = JSON.parse(localStorage.getItem(modelVisibilityKey) || '{}');

        let displayText = '';
        let anyVisible = false;

        // 按照 localStorage 中保存的顺序显示模型
        for (let model of displayOrder) {
            if (visibility[model] !== false && counts[model]) { // 仅显示勾选且有计数的模型
                displayText += `<div style="margin-right: 10px;">${model}: ${counts[model]}</div>`;
                anyVisible = true;
            }
        }

        if (!anyVisible) {
            displayText = `<div style="font-size: 24px; color: rgba(66,66,66,1);">${placeholderSymbol}</div>`; // 显示符号
        }

        let displayDiv = document.getElementById('model-count-display');
        if (!displayDiv) {
            displayDiv = document.createElement('div');
            displayDiv.id = 'model-count-display';
            Object.assign(displayDiv.style, {
                display: 'flex',
                position: 'fixed',
                top: '5px',
                left: '500px',
                backgroundColor: 'rgba(0,0,0,0)',
                color: 'rgba(66,66,66,1)',
                padding: '10px',
                borderRadius: '5px',
                zIndex: '1000',
                fontSize: '14px',
                fontWeight: 'bold',
            });

            document.body.appendChild(displayDiv);
        }
        displayDiv.innerHTML = displayText;
    }

    // 清除选中日期的模型数据,并添加提示
    function clearData() {
        const date = selectedDate;
        const hourlyCountKey = `hourly_counts_${date}`;

        // 显示二次确认提示
        if (confirm(`你确定要清除 ${date} 的所有模型数据吗?此操作不可恢复!`)) {
            localStorage.removeItem(hourlyCountKey); // 仅清除选中的日期的数据
            displayCounts(); // 刷新显示
        }
    }

    // 更新复选框状态并保存顺序到 localStorage
    function updateCheckboxState(model, isChecked) {
        const visibility = JSON.parse(localStorage.getItem(modelVisibilityKey) || '{}');
        visibility[model] = isChecked;
        localStorage.setItem(modelVisibilityKey, JSON.stringify(visibility));

        // 按勾选顺序管理显示顺序,并存储到 localStorage
        if (isChecked) {
            if (!displayOrder.includes(model)) {
                displayOrder.push(model);
                localStorage.setItem(modelDisplayOrderKey, JSON.stringify(displayOrder));
            }
        } else {
            const index = displayOrder.indexOf(model);
            if (index > -1) {
                displayOrder.splice(index, 1);
                localStorage.setItem(modelDisplayOrderKey, JSON.stringify(displayOrder));
            }
        }

        displayCounts(); // 刷新显示
    }

    // 显示右键菜单,包含日期选择、清除数据、下载数据、编辑数据功能
    function showTodayCountsPanel(x, y) {
        const { hourlyCounts, countsByModel } = getCountsForDate(selectedDate);
        let panel = document.getElementById('today-counts-panel');
        if (!panel) {
            panel = document.createElement('div');
            panel.id = 'today-counts-panel';
            Object.assign(panel.style, {
                position: 'absolute',
                backgroundColor: '#fff',
                color: '#333',
                padding: '15px',
                border: '1px solid #ccc',
                borderRadius: '10px',
                zIndex: '1001',
                fontSize: '13px',
                fontWeight: 'bold',
                maxHeight: '400px',
                overflowY: 'auto',
                width: '300px',
                boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.2)',
                transition: 'all 0.3s ease',
            });

            document.body.appendChild(panel);
        }

        let panelContent = `
        <div style="text-align: center; margin-bottom: 10px;">
            <div style="display: inline-flex; align-items: center; justify-content: center; width: 100%;">
                <strong style="font-size: 16px; margin-right: 10px;">模型计数:</strong>
                <input type="date" id="date-picker" value="${selectedDate}"
                       style="padding: 6px 10px; font-size: 16px; border-radius: 5px; border: none; background-color: #FFFFFF; color: #000000; text-align: center; cursor: pointer; height: auto; line-height: 1;">
            </div>
        </div><br>`;

        for (let model in countsByModel) {
            const visibility = JSON.parse(localStorage.getItem(modelVisibilityKey) || '{}');
            const isChecked = visibility[model] !== false; // 默认勾选
            panelContent += `
                <div style="margin-bottom: 8px;">
                    <label>
                        <input type="checkbox" data-model="${model}" ${isChecked ? 'checked' : ''}> ${model}: ${countsByModel[model]}
                    </label>
                </div>`;
        }

        panelContent += `
            <hr>
            <div style="display: flex; flex-direction: column; align-items: center;">
                <button id="clear-data-button" style="margin: 5px; padding: 10px 20px; width: 100%; border: none; border-radius: 5px; background-color: #f44336; color: white; cursor: pointer;">清除数据</button>
                <button id="download-data-button" style="margin: 5px; padding: 10px 20px; width: 100%; border: none; border-radius: 5px; background-color: #4CAF50; color: white; cursor: pointer;">下载数据</button>
                <button id="edit-data-button" style="margin: 5px; padding: 10px 20px; width: 100%; border: none; border-radius: 5px; background-color: #2196F3; color: white; cursor: pointer;">编辑数据</button>
            </div>`;
        panel.innerHTML = panelContent;

        // 清除选中日期数据,添加确认提示
        document.getElementById('clear-data-button').onclick = () => {
            clearData();
            panel.style.display = 'none';
        };

        document.getElementById('download-data-button').onclick = () => {
            showDownloadDateSelection(); // 弹出日期选择框
            panel.style.display = 'none';
        };

        // 显示编辑数据面板
        document.getElementById('edit-data-button').onclick = () => {
            showEditDataPanel();
            panel.style.display = 'none';
        };

        document.getElementById('date-picker').onchange = (e) => {
            selectedDate = e.target.value;
            showTodayCountsPanel(x, y); // 重新渲染菜单
        };

        // 添加复选框事件监听器
        panel.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
            checkbox.onchange = (e) => {
                const model = e.target.dataset.model;
                updateCheckboxState(model, e.target.checked);
            };
        });

        const displayDiv = document.getElementById('model-count-display');
        if (displayDiv) {
            const rect = displayDiv.getBoundingClientRect();
            panel.style.top = `${rect.bottom + window.scrollY + 10}px`;
            panel.style.left = `${rect.left + window.scrollX}px`;
        }

        panel.style.display = 'block';
    }

    // 右键点击事件
    window.addEventListener('contextmenu', (e) => {
        const displayDiv = document.getElementById('model-count-display');
        if (displayDiv && displayDiv.contains(e.target)) {
            e.preventDefault();
            showTodayCountsPanel(e.clientX, e.clientY);
        } else {
            const panel = document.getElementById('today-counts-panel');
            if (panel) {
                panel.style.display = 'none';
            }
        }
    });

    // 点击其他地方隐藏面板
    window.addEventListener('click', (e) => {
        const panel = document.getElementById('today-counts-panel');
        if (panel && !panel.contains(e.target)) {
            panel.style.display = 'none';
        }
    });

    displayCounts();
})();