您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tracks and displays usage statistics and detailed model costs for Premium models on Cursor.com, with consistent model colors and a legend sorted by usage.
当前为
// ==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.11 // @description Tracks and displays usage statistics and detailed model costs for Premium models on Cursor.com, with consistent model colors and a legend sorted by usage. // @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'); const legendCls = genCssId('legend'); const legendItemCls = genCssId('legend-item'); const legendColorBoxCls = genCssId('legend-color-box'); const colors = { cursor: { lightGray: '#e5e7eb', gray: '#a7a9ac', grayDark: '#333333', }, modelColorPalette: [ '#FF6F61', '#4CAF50', '#2196F3', '#FFEB3B', '#9C27B0', '#FF9800', '#00BCD4', '#E91E63', '#8BC34A', '#3F51B5', '#CDDC39', '#673AB7', '#FFC107', '#009688', '#FF5722', '#795548', '#607D8B', '#9E9E9E', '#F44336', '#4DD0E1', '#FFB74D', '#BA68C8', '#AED581', '#7986CB', '#A1887F' ] }; 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: 8px; background-color: ${colors.cursor.grayDark}; border-radius: 9999px; margin: 10px 0; } .${barSegmentCls} { height: 100%; position: relative; transition: filter 0.2s ease-in-out; } .${barSegmentCls}:first-child { border-top-left-radius: 9999px; border-bottom-left-radius: 9999px; } .${barSegmentCls}:last-child { border-top-right-radius: 9999px; border-bottom-right-radius: 9999px; } .${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; } .${legendCls} { margin-top: 15px; padding: 10px; background-color: #1e1e1e; border-radius: 6px; display: flex; flex-wrap: wrap; gap: 8px 15px; } .${legendItemCls} { display: flex; align-items: center; font-size: 12px; color: ${colors.cursor.lightGray}; } .${legendColorBoxCls} { width: 12px; height: 12px; margin-right: 6px; border: 1px solid #444; flex-shrink: 0; } `; 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'); if (table.length === 0) { error("Recent Usage Events table: Not found or empty."); return { modelUsage, totalPaidRequests, totalRequests, erroredRequests }; } 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; } }); return { modelUsage, totalPaidRequests, totalRequests, erroredRequests }; }; const parseCurrentUsageCosts = (modelUsage) => { let overallTotalCost = 0; const costTable = $('p:contains("Current Usage"):last').closest('div').find('table tbody tr'); if (costTable.length === 0) { error("Current Usage (Cost) table: Not found or empty."); for (const modelKey in modelUsage) { if (!modelUsage[modelKey].hasOwnProperty('cost')) { modelUsage[modelKey].cost = 0; } } return { overallTotalCost }; } for (const modelKey in modelUsage) { modelUsage[modelKey].cost = 0; } if (!modelUsage['Extra/Other Premium']) { modelUsage['Extra/Other Premium'] = { count: 0, cost: 0 }; } if (!modelUsage['Other Costs']) { modelUsage['Other Costs'] = { count: 0, cost: 0 }; } 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; overallTotalCost += cost; if (cost <= 0 && description.includes('paid for')) { return; } let foundModel = false; for (const modelKey in modelUsage) { if (modelKey === 'Extra/Other Premium' || modelKey === 'Other Costs') continue; if (description.includes(modelKey.toLowerCase())) { modelUsage[modelKey].cost += cost; foundModel = true; } } if (!foundModel && (description.includes('extra') || description.includes('premium')) && cost > 0 ) { modelUsage['Extra/Other Premium'].cost += cost; foundModel = true; } if (!foundModel && cost > 0) { modelUsage['Other Costs'].cost += cost; } }); if (modelUsage['Extra/Other Premium'] && modelUsage['Extra/Other Premium'].cost === 0 && modelUsage['Extra/Other Premium'].count === 0) { delete modelUsage['Extra/Other Premium']; } if (modelUsage['Other Costs'] && modelUsage['Other Costs'].cost === 0 && modelUsage['Other Costs'].count === 0) { delete modelUsage['Other Costs']; } return { overallTotalCost }; }; const getBaseUsageData = () => { const premiumLabel = $('span:contains("Premium models")').first(); if (premiumLabel.length === 0) { return {}; } const usageSpan = premiumLabel.siblings('span').last(); const usageText = usageSpan.text(); const regex = /(\d+) \/ (\d+)/; const matches = usageText.match(regex); if (matches && matches.length === 3) { return { used: parseInt(matches[1], 10), total: parseInt(matches[2], 10) }; } return {}; }; // --- Display Functions --- const createGenericProgressBar = (modelUsage, weightField, modelToColorMap) => { const barContainer = $('<div>').addClass(multiBarCls); const totalWeight = Object.values(modelUsage) .reduce((sum, model) => sum + (model[weightField] || 0), 0); if (totalWeight === 0) { return barContainer.text(`No data for progress bar.`).css({ height: 'auto', padding: '5px' }); } const sortedModels = Object.entries(modelUsage) .filter(([_, data]) => (data[weightField] || 0) > 0) .sort(([, a], [, b]) => (b[weightField] || 0) - (a[weightField] || 0)); sortedModels.forEach((entry) => { const [model, data] = entry; const percentage = (data[weightField] / totalWeight) * 100; const reqCount = (data.count || 0).toFixed(1); const costAmount = (data.cost || 0).toFixed(2); const color = modelToColorMap[model] || colors.cursor.gray; const tooltipText = `${model}: ${reqCount} reqs ($${costAmount})`; const tooltip = $('<span>').addClass(tooltipCls).text(tooltipText); const segment = $('<div>') .addClass(barSegmentCls) .css({ width: `${percentage}%`, backgroundColor: color }) .append(tooltip); barContainer.append(segment); }); return barContainer; }; const createLegend = (modelToColorMap, modelUsage) => { const legendContainer = $('<div>').addClass(legendCls); // Get models that are in the color map (meaning they have some usage/cost) const modelsInLegend = Object.keys(modelToColorMap); // Sort these models by their usage count (descending) const sortedModelsForLegend = modelsInLegend.sort((modelA, modelB) => { const countA = modelUsage[modelA]?.count || 0; const countB = modelUsage[modelB]?.count || 0; return countB - countA; // Sort descending by request count }); for (const model of sortedModelsForLegend) { const color = modelToColorMap[model]; const count = (modelUsage[model]?.count || 0).toFixed(1); const cost = (modelUsage[model]?.cost || 0).toFixed(2); const colorBox = $('<span>') .addClass(legendColorBoxCls) .css('background-color', color); const legendItem = $('<div>') .addClass(legendItemCls) .append(colorBox) .append(document.createTextNode(`${model}`)); legendContainer.append(legendItem); } return legendContainer; }; const displayEnhancedTrackerData = () => { debug('displayEnhancedTrackerData: Function START'); const tracker = $c(mainCaptionCls); if (tracker.length === 0) { error('displayEnhancedTrackerData: Main caption element NOT FOUND.'); return false; } debug(`displayEnhancedTrackerData: Found tracker caption element.`); tracker.siblings(`.${enhancedTrackerContainerCls}`).remove(); const { modelUsage, totalPaidRequests, totalRequests, erroredRequests } = parseUsageEventsTable(); const { overallTotalCost } = parseCurrentUsageCosts(modelUsage); const baseUsage = getBaseUsageData(); const modelToColorMap = {}; let colorPaletteIndex = 0; const uniqueModelsInUsage = Object.keys(modelUsage) .filter(modelName => (modelUsage[modelName].count || 0) > 0 || (modelUsage[modelName].cost || 0) > 0); for (const modelName of uniqueModelsInUsage) { modelToColorMap[modelName] = colors.modelColorPalette[colorPaletteIndex % colors.modelColorPalette.length]; colorPaletteIndex++; } debug('displayEnhancedTrackerData: modelToColorMap built:', modelToColorMap); 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('Usage-Based Requests', totalPaidRequests.toFixed(1)); addStat('Total Requests', (totalPaidRequests+baseUsage.used).toFixed(1)); addStat('Errored Requests', erroredRequests); container.append(statsContainer); container.append($('<p>').css({ fontSize: '13px', color: colors.cursor.gray, marginTop: '15px', marginBottom: '2px' }) .text('Model Usage Breakdown:') ); container.append(createGenericProgressBar(modelUsage, 'count', modelToColorMap)); if (Object.keys(modelToColorMap).length > 0) { // Pass modelUsage to createLegend to access counts for sorting and display container.append(createLegend(modelToColorMap, 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...'); window.ut = { jq: $, parseEvents: parseUsageEventsTable, parseCosts: parseCurrentUsageCosts, getBase: getBaseUsageData }; $('head').append($('<style>').text(styles)); setTimeout(main, ATTEMPTS_INTERVAL); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址