您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tracks and displays usage statistics 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.3 // @description Tracks and displays usage statistics and detailed model costs for Premium models on Cursor.com. // @match https://www.cursor.com/settings // @grant none // @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); $.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 mainCaptionCls = genCssId('main-caption'); const hrCls = genCssId('hr'); 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 enhancedTrackerContainerCls = genCssId('enhanced-tracker'); // Specific class for the container const colors = { cursor: { lightGray: '#e5e7eb', gray: '#a7a9ac', grayDark: '#333333', }, 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 = ` .${hrCls} { border: 0; height: 1px; background-color: #333333; margin: 15px 0; } .${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; 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; bottom: 150%; left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; border: 1px solid ${colors.cursor.gray}; font-size: 12px; pointer-events: none; } .${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); // --- Data Parsing Functions --- const parseUsageEventsTable = () => { const modelUsage = {}; let totalPaidRequests = 0; let totalRequests = 0; let erroredRequests = 0; const table = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr'); // debug(`Parsing Usage Events Table: Found ${table.length} rows.`); if (table.length === 0) { error("Recent Usage Events table: Not found or empty."); 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) { totalRequests += requests; if (!modelUsage[model]) { modelUsage[model] = { count: 0, cost: 0 }; } modelUsage[model].count += requests; if (status === 'Usage-based') { totalPaidRequests += requests; } } else if (status === 'Errored, Not Charged') { erroredRequests += 1; } }); // debug('Finished Parsing Usage Events:', { modelUsage, totalPaidRequests, totalRequests }); return { modelUsage, totalPaidRequests, totalRequests, erroredRequests }; }; const parseCurrentUsageCosts = (modelUsage) => { let totalCost = 0; const costTable = $('p:contains("Current Usage"):last').closest('div').find('table tbody tr'); // debug(`Parsing Current Usage Costs Table: Found ${costTable.length} rows.`); if (costTable.length === 0) { error("Current Usage (Cost) table: Not found or empty."); 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; let foundModel = false; for (const modelKey in modelUsage) { // Use modelKey to avoid conflict with 'model' variable if (description.includes(modelKey.toLowerCase())) { modelUsage[modelKey].cost += cost; 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; // Count this as found for "Other Costs" logic } if (!foundModel && cost > 0 && !description.includes('mid-month usage paid')) { // Avoid double-adding "Other Costs" for positive values if it was already an "Extra/Other Premium" if (!modelUsage['Other Costs']) { modelUsage['Other Costs'] = { count: 0, cost: 0 }; } modelUsage['Other Costs'].cost += cost; } }); for (const modelKey in modelUsage) { // Use modelKey modelUsage[modelKey].cost = modelUsage[modelKey].cost || 0; } // debug('Finished Parsing Costs & Updated Model Usage:', { modelUsage, totalCost }); return { totalCost }; }; 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 --- 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; 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; }; const displayEnhancedTrackerData = () => { debug('displayEnhancedTrackerData: Function START'); const tracker = $c(mainCaptionCls); if (tracker.length === 0) { error('displayEnhancedTrackerData: Main caption element NOT FOUND. Cannot display stats.'); return false; } debug(`displayEnhancedTrackerData: Found tracker caption element (length: ${tracker.length}). Class: ${mainCaptionCls}`); // Clear previous tracker data const existingTrackerData = tracker.siblings(`.${enhancedTrackerContainerCls}`); debug(`displayEnhancedTrackerData: Found ${existingTrackerData.length} existing tracker data containers to remove.`); existingTrackerData.remove(); debug('displayEnhancedTrackerData: Calling parsing functions...'); const { modelUsage, totalPaidRequests, totalRequests, erroredRequests } = parseUsageEventsTable(); parseCurrentUsageCosts(modelUsage); const baseUsage = getBaseUsageData(); debug('displayEnhancedTrackerData: Parsing functions COMPLETE.'); const container = $('<div>').addClass(enhancedTrackerContainerCls); 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('Weighted Usage-Based Requests', totalPaidRequests.toFixed(1)); addStat('Total Requests', (totalRequests+baseUsage.used).toFixed(1)); addStat('Errored Requests', (erroredRequests).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) ); debug('displayEnhancedTrackerData: Stats container PREPARED. Appending to DOM...'); tracker.after(container); debug('displayEnhancedTrackerData: Enhanced tracker data supposedly displayed.'); return true; }; // --- Core Script Functions --- const decorateUsageCard = () => { debug("decorateUsageCard: Function START"); if ($c(mainCaptionCls).length > 0) { debug("decorateUsageCard: Card already decorated."); return true; } const usageHeading = $('h2:contains("Usage")'); if (usageHeading.length > 0) { debug(`decorateUsageCard: Found 'h2:contains("Usage")' (count: ${usageHeading.length}). Decorating...`); 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); debug(`decorateUsageCard: Added tracker caption. Check DOM for class '${mainCaptionCls}'. Current count in DOM: ${$c(mainCaptionCls).length}`); return true; } debug("decorateUsageCard: 'h2:contains(\"Usage\")' NOT FOUND."); return false; }; const addUsageTracker = () => { debug("addUsageTracker: Function START"); const success = displayEnhancedTrackerData(); debug(`addUsageTracker: displayEnhancedTrackerData returned ${success}`); return success; }; // --- Main Execution Logic --- const state = { addingUsageTrackerSucceeded: false, addingUsageTrackerAttempts: 0, }; const ATTEMPTS_LIMIT = 15; const ATTEMPTS_INTERVAL = 750; const ATTEMPTS_MAX_DELAY = 5000; const main = () => { state.addingUsageTrackerAttempts++; log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts}...`); const scheduleNextAttempt = () => { if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) { const delay = Math.min(ATTEMPTS_INTERVAL * (state.addingUsageTrackerAttempts), ATTEMPTS_MAX_DELAY); log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts} incomplete/failed. Retrying in ${delay}ms...`); setTimeout(main, delay); } else if (state.addingUsageTrackerSucceeded) { log(`Main Execution: Attempt ${state.addingUsageTrackerAttempts} SUCCEEDED.`); } else { error(`Main Execution: All ${ATTEMPTS_LIMIT} attempts FAILED. Could not add Usage Tracker.`); } }; debug("Main Execution: Calling decorateUsageCard..."); const decorationOkay = decorateUsageCard(); debug(`Main Execution: decorateUsageCard returned ${decorationOkay}`); if (!decorationOkay) { scheduleNextAttempt(); return; } debug("Main Execution: Checking for Recent Usage Events table..."); const usageEventsTable = $('p:contains("Recent Usage Events")').closest('div').find('table tbody tr'); if (usageEventsTable.length === 0) { debug("Main Execution: 'Recent Usage Events' table NOT FOUND YET."); scheduleNextAttempt(); return; } debug(`Main Execution: 'Recent Usage Events' table FOUND (${usageEventsTable.length} rows).`); debug("Main Execution: Attempting to add/update tracker UI via addUsageTracker..."); try { state.addingUsageTrackerSucceeded = addUsageTracker(); } catch (e) { error("Main Execution: CRITICAL ERROR during addUsageTracker call:", e); state.addingUsageTrackerSucceeded = false; } debug(`Main Execution: addUsageTracker process finished. Success: ${state.addingUsageTrackerSucceeded}`); scheduleNextAttempt(); }; $(document).ready(() => { log('Document ready. Script starting...'); // For manual debugging: assign to window instead of unsafeWindow for @grant none window.ut = { jq: $, parseEvents: parseUsageEventsTable, parseCosts: parseCurrentUsageCosts, getBase: getBaseUsageData }; $('head').append($('<style>').text(styles)); setTimeout(main, ATTEMPTS_INTERVAL); }); })();