您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tracks and displays ChatGPT message usage based on model limits, with a toggle button to reopen the info panel. Adds support for gpt-4 model, makes model usage collapsible, and persists collapse state between page reloads.
// ==UserScript== // @name ChatGPT Message Tracker // @namespace http://tampermonkey.net/ // @version 1.4.4 // @description Tracks and displays ChatGPT message usage based on model limits, with a toggle button to reopen the info panel. Adds support for gpt-4 model, makes model usage collapsible, and persists collapse state between page reloads. // @author @MartianInGreen // @license MIT // @match https://chatgpt.com/* // @grant none // ==/UserScript== (function () { "use strict"; // Check if we're in an artefact context if (window.location.href.includes('/artefact') || window.parent !== window) { return; // Exit early if we're in an artefact or iframe } /*********************** * Configuration ***********************/ // Define the target URL to monitor const TARGET_URL = "https://chatgpt.com/backend-api/conversation"; // Define model limits and rolling window durations (in milliseconds) const MODEL_LIMITS = { "gpt-4o": { limit: 80, window: 3 * 60 * 60 * 1000, // 3 hours unlimited: false, }, "gpt-4o-mini": { limit: Infinity, window: 3 * 60 * 60 * 1000, // 3 hours unlimited: true, }, "o1-preview": { limit: 50, window: 7 * 24 * 60 * 60 * 1000, // 1 week unlimited: false, }, "o1-mini": { limit: 50, window: 24 * 60 * 60 * 1000, // 1 day unlimited: false, }, // Added gpt-4 model "gpt-4": { limit: 40, window: 3 * 60 * 60 * 1000, // 3 hours unlimited: false, }, }; // LocalStorage keys const STORAGE_KEY = "chatgpt_message_tracker"; const COLLAPSE_STATE_KEY = "chatgpt_message_tracker_collapse_state"; /*********************** * Utility Functions ***********************/ /** * Retrieves the stored data from localStorage. * @returns {Object} The stored data or an empty object. */ function getStoredData() { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : {}; } /** * Saves the data to localStorage. * @param {Object} data The data to store. */ function saveData(data) { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } /** * Retrieves the collapse state from localStorage. * @returns {Object} The collapse state or an empty object. */ function getCollapseState() { const state = localStorage.getItem(COLLAPSE_STATE_KEY); return state ? JSON.parse(state) : {}; } /** * Saves the collapse state to localStorage. * @param {Object} state The state to store. */ function saveCollapseState(state) { localStorage.setItem(COLLAPSE_STATE_KEY, JSON.stringify(state)); } /** * Cleans up old timestamps based on the rolling window. * @param {Array<number>} timestamps Array of timestamp numbers. * @param {number} window Duration in milliseconds. * @returns {Array<number>} Cleaned array of timestamps. */ function cleanTimestamps(timestamps, window) { const now = Date.now(); return timestamps.filter((timestamp) => now - timestamp <= window); } /** * Formats remaining time for display. * @param {number} ms Milliseconds. * @returns {string} Formatted time string. */ function formatTime(ms) { const totalSeconds = Math.floor(ms / 1000); const days = Math.floor(totalSeconds / (24 * 3600)); const hours = Math.floor((totalSeconds % (24 * 3600)) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; let parts = []; if (days > 0) parts.push(`${days}d`); if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); parts.push(`${seconds}s`); return parts.join(" "); } /*********************** * Data Tracking ***********************/ // Initialize or retrieve stored data let usageData = getStoredData(); /** * Logs a message sent using a specific model. * @param {string} model The model used. */ function logMessage(model) { if (!(model in MODEL_LIMITS)) return; // Ignore unknown models const now = Date.now(); // Initialize usage arrays if not present if (!usageData[model]) { usageData[model] = []; } // Log the message for the specific model usageData[model].push(now); // Clean old timestamps const window = MODEL_LIMITS[model].window; if (window > 0) { usageData[model] = cleanTimestamps(usageData[model], window); } // If the model is gpt-4, also log it towards gpt-4o if (model === "gpt-4") { logGpt4oMessage(now); } // Save updated data saveData(usageData); // Update UI updateUI(); } function logGpt4oMessage(timestamp) { const gpt4oModel = "gpt-4o"; if (!usageData[gpt4oModel]) { usageData[gpt4oModel] = []; } usageData[gpt4oModel].push(timestamp); const gpt4oWindow = MODEL_LIMITS[gpt4oModel].window; usageData[gpt4oModel] = cleanTimestamps(usageData[gpt4oModel], gpt4oWindow); } /*********************** * Network Interception ***********************/ /** * Intercepts fetch calls. */ (function () { const originalFetch = window.fetch; window.fetch = function (...args) { const [resource, config] = args; if (typeof resource === "string" && resource === TARGET_URL) { // Clone the request to read the body return originalFetch.apply(this, args).then((response) => { if (config && config.method === "POST" && config.body) { try { const body = JSON.parse(config.body); let modelToLog = body.model; // Check for gizmo_interaction if (body.conversation_mode && body.conversation_mode.kind === "gizmo_interaction") { modelToLog = "gpt-4o"; } if (modelToLog) { logMessage(modelToLog); } } catch (e) { console.error("Failed to parse fetch request body:", e); } } return response; }); } return originalFetch.apply(this, args); }; })(); /** * Intercepts XMLHttpRequest calls. */ (function () { const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function ( method, url, async, user, password ) { this._method = method; this._url = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { if (this._url === TARGET_URL && this._method === "POST" && body) { try { const parsedBody = JSON.parse(body); const model = parsedBody.model; if (model) { logMessage(model); } } catch (e) { console.error("Failed to parse XHR request body:", e); } } return originalSend.apply(this, arguments); }; })(); /*********************** * UI Creation ***********************/ // Create the UI container const uiContainer = document.createElement("div"); uiContainer.style.position = "fixed"; uiContainer.style.bottom = "50px"; uiContainer.style.right = "50px"; uiContainer.style.width = "250px"; uiContainer.style.maxHeight = "500px"; uiContainer.style.overflowY = "auto"; uiContainer.style.backgroundColor = "rgba(0, 0, 0, 0.85)"; uiContainer.style.color = "#fff"; uiContainer.style.padding = "15px"; uiContainer.style.borderRadius = "8px"; uiContainer.style.boxShadow = "0 0 10px rgba(0,0,0,0.5)"; uiContainer.style.zIndex = "1"; uiContainer.style.fontFamily = "Arial, sans-serif"; uiContainer.style.fontSize = "14px"; uiContainer.style.cursor = "move"; uiContainer.style.display = "none"; // Ensure it's visible initially uiContainer.style.left = "auto"; // Reset left and top to allow positioning uiContainer.style.top = "auto"; // Add a header const header = document.createElement("div"); header.textContent = "📊 Message Tracker"; header.style.fontWeight = "bold"; header.style.marginBottom = "10px"; header.style.position = "relative"; uiContainer.appendChild(header); // Add a close button const closeButton = document.createElement("span"); closeButton.textContent = "✖"; closeButton.style.position = "absolute"; closeButton.style.top = "0"; closeButton.style.right = "0"; closeButton.style.cursor = "pointer"; closeButton.title = "Close"; closeButton.addEventListener("click", () => { uiContainer.style.display = "none"; toggleButton.style.display = "block"; // Show the toggle button when panel is closed }); header.appendChild(closeButton); // Add content area const content = document.createElement("div"); uiContainer.appendChild(content); // Append to body document.body.appendChild(uiContainer); /** * Updates the UI with the current usage data. */ function updateUI() { // Clear existing content content.innerHTML = ""; const now = Date.now(); // Retrieve collapse state const collapseState = getCollapseState(); for (const [model, config] of Object.entries(MODEL_LIMITS)) { const modelName = model; const usage = usageData[model] || []; let used = 0; let remaining = config.limit; if (config.unlimited) { used = usage.length; remaining = "∞"; } else { // Clean old timestamps const cleaned = cleanTimestamps(usage, config.window); if (cleaned.length !== usage.length) { usageData[model] = cleaned; saveData(usageData); } used = cleaned.length; remaining = config.limit - used; if (remaining < 0) remaining = 0; } // Calculate time until the oldest message falls out of the window let timeLeft = "N/A"; if ( !config.unlimited && usageData[model] && usageData[model].length > 0 ) { const oldest = usageData[model][0]; const elapsed = now - oldest; const windowDuration = config.window; if (elapsed < windowDuration) { const remainingTime = windowDuration - elapsed; timeLeft = formatTime(remainingTime); } } // Create a container for the model const modelContainer = document.createElement("div"); modelContainer.style.marginBottom = "8px"; modelContainer.style.borderBottom = "1px solid #444"; modelContainer.style.paddingBottom = "8px"; // Create the clickable header for collapsing const modelHeader = document.createElement("div"); modelHeader.textContent = `Model: ${modelName}`; modelHeader.style.fontWeight = "bold"; modelHeader.style.cursor = "pointer"; modelHeader.style.display = "flex"; modelHeader.style.justifyContent = "space-between"; modelHeader.style.alignItems = "center"; // Add an arrow indicator const arrow = document.createElement("span"); arrow.textContent = collapseState[model] === false ? "▼" : "▶"; arrow.style.transition = "transform 0.2s"; modelHeader.appendChild(arrow); modelContainer.appendChild(modelHeader); // Create the details section const details = document.createElement("div"); details.style.marginTop = "5px"; const usageInfo = document.createElement("div"); usageInfo.textContent = `Used: ${used} / ${ config.unlimited ? "∞" : config.limit } messages`; details.appendChild(usageInfo); if (!config.unlimited) { const remainingInfo = document.createElement("div"); remainingInfo.textContent = `Remaining: ${remaining} messages`; details.appendChild(remainingInfo); const timeInfo = document.createElement("div"); timeInfo.textContent = `Time until reset: ${timeLeft}`; details.appendChild(timeInfo); } modelContainer.appendChild(details); content.appendChild(modelContainer); // Set initial display based on collapse state if (collapseState[model] === false) { details.style.display = "block"; arrow.style.transform = "rotate(0deg)"; } else { details.style.display = "none"; arrow.style.transform = "rotate(-90deg)"; } // Toggle functionality modelHeader.addEventListener("click", () => { if (details.style.display === "none") { details.style.display = "block"; arrow.style.transform = "rotate(0deg)"; collapseState[model] = false; } else { details.style.display = "none"; arrow.style.transform = "rotate(-90deg)"; collapseState[model] = true; } saveCollapseState(collapseState); }); } } /*********************** * UI Interactivity ***********************/ // Make the UI draggable (function () { let isDragging = false; let startX, startY, initialX, initialY; header.addEventListener("mousedown", (e) => { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = uiContainer.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); e.preventDefault(); // Prevent text selection }); function onMouseMove(e) { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; uiContainer.style.left = `${initialX + dx}px`; uiContainer.style.top = `${initialY + dy}px`; uiContainer.style.right = "auto"; uiContainer.style.bottom = "auto"; } function onMouseUp() { isDragging = false; document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); } })(); /*********************** * Toggle Button Creation ***********************/ // Create the toggle button const toggleButton = document.createElement("button"); toggleButton.textContent = "📊"; toggleButton.style.fontSize = "10px"; toggleButton.style.position = "fixed"; toggleButton.style.bottom = "40px"; toggleButton.style.right = "12px"; toggleButton.style.width = "22px"; toggleButton.style.height = "22px"; toggleButton.style.backgroundColor = "#212121"; toggleButton.style.color = "#fff"; toggleButton.style.border = "2px solid #676767"; toggleButton.style.borderRadius = "50%"; toggleButton.style.cursor = "pointer"; toggleButton.style.boxShadow = "0 2px 6px rgba(0,0,0,0.3)"; toggleButton.style.zIndex = "12"; toggleButton.style.display = "block"; // Correctly kept as hidden initially toggleButton.style.justifyContent = "center"; toggleButton.style.alignItems = "center"; toggleButton.addEventListener("click", () => { uiContainer.style.display = "block"; toggleButton.style.display = "none"; }); document.body.appendChild(toggleButton); /*********************** * Initial UI Update ***********************/ updateUI(); /*********************** * Periodic Cleanup and UI Refresh ***********************/ // Periodically clean old timestamps and refresh UI setInterval(() => { let dataChanged = false; const now = Date.now(); for (const [model, config] of Object.entries(MODEL_LIMITS)) { if (!usageData[model]) continue; const cleaned = cleanTimestamps(usageData[model], config.window); if (cleaned.length !== usageData[model].length) { usageData[model] = cleaned; dataChanged = true; } } updateUI(); if (dataChanged) { saveData(usageData); } }, 30 * 1000); // Every 30 seconds })();