ChatGPT模型调用记录

2024-9-18更新,记录并显示ChatGPT模型的调用次数,保存勾选配置,右键清除数据,并添加日期选择功能查看当天调用情况,保存文件功能

目前為 2024-09-18 提交的版本,檢視 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         ChatGPT模型调用记录
// @namespace    http://tampermonkey.net/
// @version      3.9
// @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();
})();