您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tracks and displays usage statistics, payment cycles, and detailed model costs for Premium models on Cursor.com.
当前为
// ==UserScript== // @name Cursor.com Usage Tracker (Enhanced) // @author monnef, Sonnet 3.5 (via Perplexity and Cursor), some help from Cursor Tab and Cursor Small, NoahBPeterson, Sonnet 3.7, Gemini // @namespace http://monnef.eu // @version 0.5.2 // @description Tracks and displays usage statistics, payment cycles, and detailed model costs for Premium models on Cursor.com. // @match https://www.cursor.com/settings // @grant GM_getValue // @grant GM_setValue // @require https://code.jquery.com/jquery-3.6.0.min.js // @license AGPL-3.0 // @icon https://www.cursor.com/favicon-48x48.png // ==/UserScript== (function () { 'use strict'; const $ = jQuery.noConflict(); const $c = (cls, parent) => $(`.${cls}`, parent); const $i = (id, parent) => $(`#${id}`, parent); $.fn.nthParent = function (n) { return this.parents().eq(n - 1); }; const log = (...messages) => { console.log(`[UsageTracker]`, ...messages); }; const error = (...messages) => { console.error(`[UsageTracker]`, ...messages); }; const debug = (...messages) => { console.debug(`[UsageTracker Debug]`, ...messages); }; const genCssId = name => `ut-${name}`; // --- CSS Class Names --- const sigCls = genCssId('sig'); const buttonCls = genCssId('button'); const buttonWhiteCls = genCssId('button-white'); const buttonDarkCls = genCssId('button-dark'); const mainCaptionCls = genCssId('main-caption'); const modalCls = genCssId('modal'); const modalContentCls = genCssId('modal-content'); const modalCloseCls = genCssId('modal-close'); const copyButtonCls = genCssId('copy-button'); const inputCls = genCssId('input'); const inputWithButtonCls = genCssId('input-with-button'); const errorMessageCls = genCssId('error-message'); const settingsModalCls = genCssId('settings-modal'); const hrCls = genCssId('hr'); const debugContainerCls = genCssId('debug-container'); const hSpaceSmCls = genCssId('h-space-sm'); const hSpaceMdCls = genCssId('h-space-md'); const hSpaceLgCls = genCssId('h-space-lg'); const flexCenterCls = genCssId('flex-center'); const flexRightCls = genCssId('flex-right'); const flexBetweenCls = genCssId('flex-between'); const multiBarCls = genCssId('multi-bar'); const barSegmentCls = genCssId('bar-segment'); const tooltipCls = genCssId('tooltip'); const statsContainerCls = genCssId('stats-container'); const statItemCls = genCssId('stat-item'); const colors = { cursor: { blue: '#3864f6', blueDarker: '#2e53cc', lightGray: '#e5e7eb', gray: '#a7a9ac', grayDark: '#333333', green: '#63a11a', // Original green }, segments: [ // A palette for the multi-segment bar '#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B' ] }; const styles = ` .${hSpaceSmCls} { height: 5px; } .${hSpaceMdCls} { height: 10px; } .${hSpaceLgCls} { height: 20px; } .${flexCenterCls} { display: flex; justify-content: center; align-items: center; } .${flexRightCls} { display: flex; justify-content: flex-end; align-items: center; } .${flexBetweenCls} { display: flex; justify-content: space-between; align-items: center; } .${sigCls} { font-size: 0.75rem; color: ${colors.cursor.gray}; margin-left: 0.75rem; opacity: 0.2; transition: opacity 0.1s ease-in-out; } .${sigCls}:hover { opacity: 1; } .${buttonCls}, .${buttonWhiteCls}, .${buttonDarkCls} { background-color: ${colors.cursor.blue}; color: white; font-size: 14px; border: none; border-radius: 4px; cursor: pointer; padding: 4.25px 8px; font-weight: 400; } .${buttonCls}:hover { background-color: ${colors.cursor.blueDarker}; } .${buttonWhiteCls} { background-color: white; color: black; border: 1px solid ${colors.cursor.lightGray}; padding: 3px 8px; } .${buttonWhiteCls}:hover { background-color: ${colors.cursor.lightGray}; } .${buttonDarkCls} { background-color: black; color: white; border: 1px solid black; padding: 3px 8px; } .${buttonDarkCls}:hover { background-color: white; color: black; } .${modalCls} { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.4); backdrop-filter: blur(5px) contrast(0.5); } .${modalContentCls} { background-color: black; color: white; margin: 15% auto; padding: 15px 20px; width: 600px; border-radius: 4px; position: relative; } .${modalCloseCls} { color: white; position: absolute; top: 0px; right: 10px; font-size: 25px; font-weight: bold; cursor: pointer; } .${modalCloseCls}:hover { color: ${colors.cursor.lightGray}; } .${copyButtonCls} { margin-left: 10px; width: 5em; } .${modalContentCls} h2 { margin-bottom: 20px; } .${modalContentCls} hr { border: 0; height: 1px; background-color: ${colors.cursor.grayDark}; margin: 10px 0; } .${inputCls} { background-color: white; color: black; border: 1px solid ${colors.cursor.lightGray}; padding: 5px; width: 100%; border-radius: 4px; font-size: 14px; } .${inputWithButtonCls} { width: calc(100% - 5em - 10px); } .${errorMessageCls} { color: #ff4d4f; font-size: 14px; margin-top: 5px; } .${hrCls} { border: 0; height: 1px; background-color: #333333; /* Darker HR */ margin: 15px 0; } .${debugContainerCls} { background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 10px; margin-top: 10px; font-family: monospace; font-size: 12px; overflow-wrap: break-word; max-height: 200px; overflow-y: auto; } .${statsContainerCls} { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; margin: 15px 0; padding: 15px; background-color: #1a1a1a; border-radius: 8px; } .${statItemCls} { font-size: 14px; } .${statItemCls} .label { color: ${colors.cursor.gray}; } .${statItemCls} .value { color: white; font-weight: bold; } .${multiBarCls} { display: flex; width: 100%; height: 15px; background-color: ${colors.cursor.grayDark}; border-radius: 4px; /* REMOVED: overflow: hidden; */ margin: 10px 0; } .${barSegmentCls} { height: 100%; position: relative; transition: filter 0.2s ease-in-out; } .${barSegmentCls}:hover { filter: brightness(1.2); } .${barSegmentCls} .${tooltipCls} { visibility: hidden; width: max-content; background-color: black; color: #fff; text-align: center; border-radius: 6px; padding: 5px 10px; position: absolute; z-index: 50; /* INCREASED Z-INDEX */ bottom: 150%; /* Adjusted slightly up */ left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; border: 1px solid ${colors.cursor.gray}; font-size: 12px; pointer-events: none; /* Prevent tooltip from interfering with hover */ } .${barSegmentCls}:hover .${tooltipCls} { visibility: visible; opacity: 1; } .${barSegmentCls} .${tooltipCls}::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: black transparent transparent transparent; } `; const genHr = () => $('<hr>').addClass(hrCls); const getUsageCard = () => { const usageHeading = $('h2:contains("Usage")'); if (usageHeading.length > 0) { const card = usageHeading.closest('.rounded-2xl, .rounded-3xl'); debug(`Found Usage card via h2: ${card.length > 0}`); return card.length > 0 ? card : null; } debug('Usage card not found.'); return null; }; // --- Data Parsing Functions --- /** * Finds and parses the "Recent Usage Events" table. * @returns {{ modelUsage: Record<string, { count: number, cost: number }>, totalPaidRequests: number, totalRequests: number }} */ const parseUsageEventsTable = () => { const modelUsage = {}; let totalPaidRequests = 0; let totalRequests = 0; const table = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr'); debug(`Found ${table.length} rows in Recent Usage Events table.`); if (table.length === 0) { error("Could not find 'Recent Usage Events' table."); return { modelUsage, totalPaidRequests, totalRequests }; } table.each((_, row) => { const $row = $(row); const model = $row.find('td:eq(1)').text().trim(); const status = $row.find('td:eq(2)').text().trim(); const requestsStr = $row.find('td:eq(3)').text().trim(); const requests = parseFloat(requestsStr) || 0; if (status !== 'Errored, Not Charged' && model) { // Ensure model name exists totalRequests += requests; if (!modelUsage[model]) { modelUsage[model] = { count: 0, cost: 0 }; // Initialize cost here } modelUsage[model].count += requests; if (status === 'Usage-based') { totalPaidRequests += requests; } } }); debug('Parsed Usage Events:', { modelUsage, totalPaidRequests, totalRequests }); return { modelUsage, totalPaidRequests, totalRequests }; }; /** * Finds and parses the "Current Usage" cost summary table. * @param {Record<string, { count: number, cost: number }>} modelUsage - The object to update with costs. * @returns {{ totalCost: number }} */ const parseCurrentUsageCosts = (modelUsage) => { let totalCost = 0; const costTable = $('p:contains("Current Usage"):last').closest('div').find('table tbody tr'); // Find the *cost* table debug(`Found ${costTable.length} rows in Current Usage (Cost) table.`); if (costTable.length === 0) { error("Could not find 'Current Usage' (Cost) table."); return { totalCost }; } costTable.each((_, row) => { const $row = $(row); const description = $row.find('td:eq(0)').text().trim().toLowerCase(); const costStr = $row.find('td:eq(1)').text().trim().replace('$', ''); const cost = parseFloat(costStr) || 0; totalCost += cost; // Sum all costs, including negative ones (payments) let foundModel = false; for (const model in modelUsage) { if (description.includes(model.toLowerCase())) { modelUsage[model].cost += cost; // Add cost to the model foundModel = true; } } if (!foundModel && (description.includes('extra') || description.includes('premium')) && cost > 0 ) { if (!modelUsage['Extra/Other Premium']) { modelUsage['Extra/Other Premium'] = { count: 0, cost: 0 }; } modelUsage['Extra/Other Premium'].cost += cost; foundModel = true; } if (!foundModel && cost > 0) { if (!modelUsage['Other Costs']) { modelUsage['Other Costs'] = { count: 0, cost: 0 }; } modelUsage['Other Costs'].cost += cost; } }); // Ensure all models in modelUsage have a cost, even if 0 for (const model in modelUsage) { modelUsage[model].cost = modelUsage[model].cost || 0; } debug('Parsed Costs & Updated Model Usage:', { modelUsage, totalCost }); return { totalCost }; // Keep returning totalCost in case we need it later, even if not displayed. }; /** * Extracts the basic "X / Y" usage if available. * @returns {{ used: number, total: number }} */ const getBaseUsageData = () => { debug('Attempting to find base usage data...'); const premiumLabel = $('span:contains("Premium models")').first(); if (premiumLabel.length === 0) { debug('Base Premium models label not found.'); return {}; } const usageSpan = premiumLabel.siblings('span').last(); const usageText = usageSpan.text(); debug(`Found base usage text: "${usageText}"`); const regex = /(\d+) \/ (\d+)/; const matches = usageText.match(regex); if (matches && matches.length === 3) { const used = parseInt(matches[1], 10); const total = parseInt(matches[2], 10); debug(`Parsed base values - Used: ${used}, Total: ${total}`); return { used, total }; } else { debug('Regex did not match the base usage text.'); return {}; } }; // --- Display Functions --- /** * Creates a multi-segment progress bar. * @param {Record<string, { count: number, cost: number }>} modelUsage * @returns {JQuery<HTMLElement>} */ const createMultiSegmentProgressBar = (modelUsage) => { const barContainer = $('<div>').addClass(multiBarCls); const totalRequests = Object.values(modelUsage).reduce((sum, model) => sum + (model.count || 0), 0); if (totalRequests === 0) { return barContainer.text('No usage data for bar.').css({ height: 'auto', padding: '5px' }); } let colorIndex = 0; // Sort models by count descending for better visualization const sortedModels = Object.entries(modelUsage) .filter(([_, data]) => data.count > 0) .sort(([, a], [, b]) => b.count - a.count); for (const [model, data] of sortedModels) { const percentage = (data.count / totalRequests) * 100; const cost = data.cost.toFixed(2); const color = colors.segments[colorIndex % colors.segments.length]; const tooltipText = `${model}: ${data.count.toFixed(1)} reqs ($${cost})`; const tooltip = $('<span>').addClass(tooltipCls).text(tooltipText); const segment = $('<div>') .addClass(barSegmentCls) .css({ width: `${percentage}%`, backgroundColor: color, }) .append(tooltip); barContainer.append(segment); colorIndex++; } return barContainer; }; /** * Displays all the new tracker data. * @param {number} paymentDay */ const displayEnhancedTrackerData = (paymentDay) => { const tracker = $c(mainCaptionCls); if (tracker.length === 0) { error('Main caption not found for displaying enhanced data.'); return false; // Indicate failure } // Clear previous tracker data before parsing again tracker.siblings(`.${genCssId('enhanced-tracker')}`).remove(); const { modelUsage, totalPaidRequests, totalRequests } = parseUsageEventsTable(); parseCurrentUsageCosts(modelUsage); // Call this to populate costs in modelUsage const baseUsage = getBaseUsageData(); const container = $('<div>').addClass(genCssId('enhanced-tracker')); const statsContainer = $('<div>').addClass(statsContainerCls); const addStat = (label, value) => { statsContainer.append( $('<div>').addClass(statItemCls).append( $('<span>').addClass('label').text(`${label}: `), $('<span>').addClass('value').text(value) ) ); } addStat('Total Usage-Based Requests (Weighted)', totalPaidRequests.toFixed(1)); addStat('Grand Total Requests (Weighted)', (totalRequests + baseUsage.total).toFixed(1)); container.append( statsContainer, $('<p>').css({ fontSize: '13px', color: colors.cursor.gray, marginTop: '10px' }).text('Model Usage Breakdown (Weighted, Hover for details):'), createMultiSegmentProgressBar(modelUsage) ); tracker.after(container); debug('Enhanced tracker data displayed.'); return true; // Indicate success }; // --- Original Script Functions (Mostly Unchanged or Slightly Modified) --- const decorateUsageCard = () => { if ($c(mainCaptionCls).length > 0) { return true; } const usageHeading = $('h2:contains("Usage")'); if (usageHeading.length > 0) { const caption = $('<div>') .addClass('font-medium gt-standard-mono text-xl/[1.375rem] font-semibold -tracking-4 md:text-2xl/[1.875rem]') .addClass(mainCaptionCls) .text('Usage Tracker'); usageHeading.after(genHr(), caption); addSettingsButton(caption); // Add settings button here debug("Added tracker after Usage heading"); return true; } return false; }; const addUsageTracker = () => { const paymentDay = GM_getValue('paymentDay'); return displayEnhancedTrackerData(paymentDay); }; const calculateDaysPassed = ({ today, paymentDay, disableLog = false }) => { if (!paymentDay) return null; const currentMonth = today.getMonth(); const currentYear = today.getFullYear(); const lastPaymentDate = new Date(currentYear, currentMonth, paymentDay); if (today < lastPaymentDate) { lastPaymentDate.setMonth(lastPaymentDate.getMonth() - 1); } const daysPassed = Math.floor((today - lastPaymentDate) / (1000 * 60 * 60 * 24)); const nextPaymentDate = new Date(lastPaymentDate); nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1); const totalDays = Math.floor((nextPaymentDate - lastPaymentDate) / (1000 * 60 * 60 * 24)); const res = { daysPassed, totalDays, progress: daysPassed / totalDays }; if (!disableLog) { debug(`Calculated days - Passed: ${res.daysPassed}, Total: ${res.totalDays}, Progress: ${res.progress}`); } return res; }; const createModal = ({ className, title, content }) => { const modal = $('<div>').addClass(modalCls).addClass(className); const modalContent = $('<div>').addClass(modalContentCls); const closeButton = $('<span>').addClass(modalCloseCls).text('×'); const titleElement = $('<h1>') .addClass('text-4xl gt-standard-mono font-medium') .text(title); modalContent.append( closeButton, titleElement, $('<div>').addClass(hSpaceMdCls), content ); modal.append(modalContent); closeButton.click(() => modal.hide()); $(window).click(event => { if (event.target === modal[0]) { modal.hide(); } }); return modal; }; const createSettingsModal = () => { const subtitle = $('<p>').text('Enter the day of the month when you are billed (1-31):'); const input = $('<input>') .addClass(inputCls) .attr('type', 'number') .attr('min', '1') .attr('max', '31') .val(GM_getValue('paymentDay') || ''); const tip = $('<p>') .addClass('text-sm text-gray-500 mt-1') .text('You can find your billing date via the "Manage Subscription" button on the left.'); const errorMessage = $('<p>').addClass(errorMessageCls).hide(); const saveAndReload = () => { const newPaymentDay = parseInt(input.val(), 10); if (newPaymentDay && newPaymentDay >= 1 && newPaymentDay <= 31) { GM_setValue('paymentDay', newPaymentDay); log(`Payment day has been set to: ${newPaymentDay}`); $c(settingsModalCls).hide(); location.reload(); } else { errorMessage.text('Invalid input. Please enter a number between 1 and 31.').show(); } }; const saveButton = $('<button>') .addClass(buttonCls) .text('Save & Reload') .click(saveAndReload); input.on('keypress', (e) => { if (e.which === 13) saveAndReload(); }); const madeByText = $('<p>').html( // Updated authors 'Made with ❤️ by monnef, Sonnet 3.5 & Gemini' ); const content = $('<div>').append( subtitle, $('<div>').addClass(hSpaceSmCls), input, tip, errorMessage, $('<div>').addClass(hSpaceLgCls), $('<div>').addClass(flexBetweenCls).append(madeByText, saveButton) ); return createModal({ className: settingsModalCls, title: 'Usage Tracker Settings', content: content }); }; const addSettingsButton = (mainCaption) => { const settingsButton = $('<button>') .css({ position: 'absolute', top: '-10px', right: '0px', height: '29.4px', width: '29.4px', padding: '0px', filter: 'invert(1)', }) .addClass(buttonWhiteCls) .attr('title', 'Usage Tracker settings') .append($(`<svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>`).css({ display: 'inline-block', verticalAlign: 'text-bottom' })); settingsButton.click(() => { log('Usage Tracker settings button clicked.'); $c(settingsModalCls).show(); $c(settingsModalCls).find(`.${inputCls}`).focus(); }); const buttonWrapper = $('<div>').css({ position: 'relative', height: '0px' }); buttonWrapper.append(settingsButton); if (mainCaption.length > 0) { mainCaption.prepend(buttonWrapper); log('Settings button wrapper added to the page'); } else { log('Main caption not found, settings button not added'); } }; // --- Main Execution Logic --- const state = { addingUsageTrackerSucceeded: false, addingUsageTrackerAttempts: 0, }; const ATTEMPTS_LIMIT = 15; const ATTEMPTS_INTERVAL = 500; const ATTEMPTS_MAX_DELAY = 4000; const main = () => { state.addingUsageTrackerAttempts++; log(`Attempt ${state.addingUsageTrackerAttempts}...`); const scheduleNextAttempt = () => { if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) { const delay = Math.min(ATTEMPTS_INTERVAL * (state.addingUsageTrackerAttempts), ATTEMPTS_MAX_DELAY); log(`Attempt ${state.addingUsageTrackerAttempts} failed or incomplete. Retrying in ${delay}ms...`); setTimeout(main, delay); } else if (state.addingUsageTrackerSucceeded) { log(`Attempt ${state.addingUsageTrackerAttempts} succeeded.`); } else { error(`All ${ATTEMPTS_LIMIT} attempts failed. Could not add Usage Tracker. Check selectors or page structure.`); } }; const decorationOkay = decorateUsageCard(); if (!decorationOkay) { debug('Decoration failed. Will retry.'); scheduleNextAttempt(); return; } const usageEventsTable = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr'); if (usageEventsTable.length === 0) { debug("'Recent Usage Events' table not found yet. Will retry."); scheduleNextAttempt(); return; } debug(`Found ${usageEventsTable.length} usage events.`); try { state.addingUsageTrackerSucceeded = addUsageTracker(); } catch (e) { error("Error during addUsageTracker:", e); state.addingUsageTrackerSucceeded = false; } scheduleNextAttempt(); }; $(document).ready(() => { log('Script started'); unsafeWindow.ut = { // For manual debugging jq: $, resetSettings: () => { GM_setValue('paymentDay', undefined); location.reload(); }, parseEvents: parseUsageEventsTable, parseCosts: parseCurrentUsageCosts, getBase: getBaseUsageData, }; $('head').append($('<style>').text(styles)); $('body').append(createSettingsModal()); setTimeout(main, ATTEMPTS_INTERVAL); // Initial delay }); })();