您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tracks and displays the usage statistics and payment cycles for Premium models on Cursor.com, helping users monitor their subscriptions and usage limits.
当前为
// ==UserScript== // @name Cursor.com Usage Tracker // @author monnef, Sonnet 3.5 (via Perplexity and Cursor), some help from Cursor Tab and Cursor Small // @namespace http://monnef.eu // @version 0.1 // @description Tracks and displays the usage statistics and payment cycles for Premium models on Cursor.com, helping users monitor their subscriptions and usage limits. // @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 // ==/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 genCssId = name => `ut-${name}`; 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 donationModalCls = genCssId('donation-modal'); 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 colors = { cursor: { blue: '#3864f6', blueDarker: '#2e53cc', lightGray: '#e5e7eb', gray: '#a7a9ac', grayDark: '#5f5f5f', } }; 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; cursor: pointer; } .${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} p { } .${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; } `; const getUsageCard = () => $('.card:contains("Usage")'); const decorateUsageCard = () => { if ($c(mainCaptionCls).length > 0) return true; const usageCard = getUsageCard(); if (!usageCard.length) { log('Usage card not found. Not decorating.'); return false; } log('Usage card found. Decorating.', usageCard); const caption = $('<div>') .addClass('font-medium gt-standard-mono') .addClass(mainCaptionCls) .text('Usage Tracker'); const sig = $('<span>') .addClass(sigCls) .text('by monnef') .attr('title', 'Enjoying this script? Consider a small donation.') ; caption.append(sig); usageCard.append($('<hr>'), caption); addSettingsButton(caption); sig.click(() => { log('Signature clicked, showing donation modal'); $c(donationModalCls).show(); }); if ($c(donationModalCls).length === 0) { $('body').append(createDonationModal()); } // Add message for unset billing date const paymentDay = GM_getValue('paymentDay'); if (!paymentDay) { const message = $('<div>') .addClass(getSettingsTextClassName()) .html('Billing date not set. Please set it in the <strong>Usage Tracker settings</strong> to see usage statistics (top right of this section).'); usageCard.append( // $('<div>').addClass(hSpaceMdCls), message ); } return true; }; const addUsageTracker = () => { const paymentDay = GM_getValue('paymentDay'); log('Checking payment day.'); if (!paymentDay) { log('Payment day not set. Not adding tracker.'); return false; } log(`Payment day is set to: ${paymentDay}`); const usageData = getUsageData(); if (usageData.used !== undefined && usageData.total !== undefined) { log(`Retrieved usage data: ${JSON.stringify(usageData)}`); displayTrackerData(usageData, paymentDay); return true; } else { log('Failed to retrieve usage data.'); return false; } }; const getPremiumModelsLabel = () => $('.gt-standard-mono.text-sm:contains("Premium models")'); /** * @returns {{ used: number, total: number }} */ const getUsageData = () => { log('Attempting to find usage data...'); const usageElement = getPremiumModelsLabel().next(); if (usageElement.length === 0) { log('Usage element not found.'); return {}; } const usageText = usageElement.text(); log(`Extracted 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); log(`Parsed values - Used: ${used}, Total: ${total}`); return { used, total }; } else { log('Regex did not match the extracted text.'); return {}; } }; /** * @param {{ used: number, total: number }} usageData * @param {number} paymentDay * @returns {void} */ const displayTrackerData = (usageData, paymentDay) => { const daysInfo = calculateDaysPassed({ today: new Date(), paymentDay }); const hueShift = 120; if (daysInfo) { const { premiumRequestsPerDay, remainingUsesPerDay } = calculateMetrics(usageData, daysInfo); const progressBar = createProgressBar({ value: daysInfo.daysPassed, max: daysInfo.totalDays, label: 'Days', rightLabel: `${daysInfo.daysPassed} / ${daysInfo.totalDays}`, textMiddle: ' days passed out of ', textAfter: ' days in the current billing cycle.', hueShift, }); const usageCard = getUsageCard(); usageCard.append(progressBar); log('Progress bar added to the page'); // Create new progress bars for the new metrics const premiumRequestsBar = createProgressBar({ value: premiumRequestsPerDay.toFixed(2), max: (usageData.total / daysInfo.totalDays).toFixed(2), label: 'Premium Requests/Day', rightLabel: `${premiumRequestsPerDay.toFixed(2)} / ${(usageData.total / daysInfo.totalDays).toFixed(2)}`, textBefore: 'So far you have used ', textMiddle: ' requests per day on average.', hideMax: true, hueShift, }); const remainingUsesBar = createProgressBar({ value: remainingUsesPerDay.toFixed(2), max: (usageData.total / daysInfo.totalDays).toFixed(2), label: 'Remaining Uses/Day', rightLabel: `${remainingUsesPerDay.toFixed(2)} / ${(usageData.total / daysInfo.totalDays).toFixed(2)}`, textBefore: 'You have ', textMiddle: ' uses per day available for the remaining ', textAfter: ` days.`, overrideMax: `${daysInfo.totalDays - daysInfo.daysPassed}`, hueShift, }); // Append new progress bars to the usage card usageCard.append(premiumRequestsBar, remainingUsesBar); } }; /** * @param {{ used: number, total: number }} usageData * @param {{ daysPassed: number, totalDays: number }} daysInfo * @returns {{ premiumRequestsPerDay: number, remainingUsesPerDay: number }} */ const calculateMetrics = (usageData, daysInfo) => { const premiumRequestsPerDay = daysInfo.daysPassed > 0 ? (usageData.used / daysInfo.daysPassed) : 0; // Avoid division by zero const remainingUsesPerDay = (usageData.total - usageData.used) / (daysInfo.totalDays - daysInfo.daysPassed || 1); // Prevent division by zero return { premiumRequestsPerDay, remainingUsesPerDay }; }; /** * @param {{ today: Date, paymentDay: number }} params * @returns {{ daysPassed: number, totalDays: number }} */ const calculateDaysPassed = ({ today, paymentDay, disableLog = false }) => { 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) { log(`Calculated days - Passed: ${res.daysPassed}, Total: ${res.totalDays}, Progress: ${res.progress}`); } return res; }; const TEST_calculateDaysPassed = () => { const testCases = [ { today: new Date(2023, 9, 15), paymentDay: 15, expected: { daysPassed: 0, totalDays: 31 } }, { today: new Date(2023, 9, 16), paymentDay: 15, expected: { daysPassed: 1, totalDays: 31 } }, { today: new Date(2023, 10, 1), paymentDay: 15, expected: { daysPassed: 17, totalDays: 31 } }, { today: new Date(2023, 10, 16), paymentDay: 15, expected: { daysPassed: 1, totalDays: 30 } }, { today: new Date(2023, 9, 1), paymentDay: 15, expected: { daysPassed: 16, totalDays: 30 } }, { today: new Date(2023, 12, 29), paymentDay: 2, expected: { daysPassed: 27, totalDays: 31 } }, ]; return testCases.every(({ today, paymentDay, expected }) => { const result = calculateDaysPassed({ today, paymentDay, disableLog: true }); const passed = result.daysPassed === expected.daysPassed && result.totalDays === expected.totalDays; if (!passed) { log(`Test failed for today: ${today.toDateString()}, paymentDay: ${paymentDay}. Expected: ${JSON.stringify(expected)}, got: ${JSON.stringify(result)}`); } return passed; }); }; const TEST_calculateMetrics = () => { const testCases = [ { usageData: { used: 10, total: 100 }, daysInfo: { daysPassed: 5, totalDays: 30 }, expected: { premiumRequestsPerDay: 2, remainingUsesPerDay: 3.6 } }, { usageData: { used: 0, total: 100 }, daysInfo: { daysPassed: 0, totalDays: 30 }, expected: { premiumRequestsPerDay: 0, remainingUsesPerDay: 3.3333333333333335 } }, { usageData: { used: 50, total: 100 }, daysInfo: { daysPassed: 10, totalDays: 30 }, expected: { premiumRequestsPerDay: 5, remainingUsesPerDay: 2.5 } }, ]; return testCases.every(({ usageData, daysInfo, expected }) => { const result = calculateMetrics(usageData, daysInfo); const passed = result.premiumRequestsPerDay === expected.premiumRequestsPerDay && result.remainingUsesPerDay === expected.remainingUsesPerDay; if (!passed) { log(`Test failed for usageData: ${JSON.stringify(usageData)}, daysInfo: ${JSON.stringify(daysInfo)}. Expected: ${JSON.stringify(expected)}, got: ${JSON.stringify(result)}`); } return passed; }); }; const TEST = () => { const tests = { calculateDaysPassed: TEST_calculateDaysPassed(), calculateMetrics: TEST_calculateMetrics(), }; const allPassed = Object.values(tests).every(testRes => testRes); if (allPassed) { log('TEST: All tests passed successfully.', tests); } else { log('TEST: Some tests failed. Check the logs above for details.', tests); } }; /** * @returns {string|null} */ const getClassNameByPrefix = prefix => { const classes = $('[class]') .map((_, element) => $(element).attr('class').match(new RegExp(`\\b${prefix}\\S+`, 'g')) || []) .get(); if (classes.length === 0) { log('No classes found'); return null; } const firstClass = classes[0]; if (classes.some(cls => cls !== firstClass)) { error(`Classes are not the same: ${classes.join(', ')}`); } return firstClass; }; /** * @returns {string|null} */ const getSettingsTextClassName = () => getClassNameByPrefix('SettingsV2_settingsText__'); /** * @returns {string|null} */ const getInfoNumberClassName = () => getClassNameByPrefix('SettingsV2_infoNumber__'); /** * @param {{ value: number, max: number, label: string, rightLabel: string, textBefore?: string, textMiddle?: string, textAfter?: string, hideMax?: boolean, overrideMax?: string, hueShift?: number }} params * @returns {string} */ const createProgressBar = ({ value, max, label, rightLabel, textBefore = '', textMiddle = '', textAfter = '', hideMax = false, overrideMax, hueShift }) => { const percentage = (value / max) * 100; return `<div class='flex flex-col gap-1.5 flex-1'> <div class='flex items-center justify-between gap-2'> <div class='gt-standard-mono text-sm font-medium flex items-center gap-1'> <span class='truncate'>${label}</span> </div> <div class='font-mono text-sm font-semibold truncate'>${rightLabel}</div> </div> <div class='h-[6px] w-full rounded-full bg-[#EEEEEE] overflow-hidden'> <div class='h-full' style='background-color: rgb(141, 236, 180); width: ${percentage.toFixed(2)}%; filter: hue-rotate(${hueShift ?? 0}deg);' title='${percentage.toFixed(0)}%'></div> </div> <div class='flex items-center gap-1'> <div class='${getSettingsTextClassName()}'> ${textBefore}<div class='${getInfoNumberClassName()}'>${value}</div>${textMiddle}<div class='${getInfoNumberClassName()}' ${hideMax ? 'style="display: none;"' : ''}>${overrideMax ? overrideMax : max}</div>${textAfter} </div> </div> </div>`; }; /** * @param {{ className: string, title: string, content: JQuery<HTMLElement> }} params * @returns {JQuery<HTMLElement>} */ const createModal = ({ className, title, titleIconAfter, 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); if (titleIconAfter) { titleElement.append( createLucideIcon({ iconName: titleIconAfter, size: '32px', invert: true }) .css({ marginLeft: '10px' }) ); } ; 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(); // Refresh to update tracker } else { errorMessage.text('Invalid input. Please enter a number between 1 and 31.').show(); } }; const saveButton = $('<button>') .addClass(buttonCls) .text('Save & Reload') .click(saveAndReload); // Add keypress event listener to the input input.on('keypress', (e) => { if (e.which === 13) { // 13 is the Enter key code saveAndReload(); } }); const madeByText = $('<p>').append( 'Made with ', createLucideIcon({ iconName: 'heart', size: '14px', invert: true }), ' by monnef & Sonnet 3.5' ); const content = $('<div>').append( subtitle, $('<div>').addClass(hSpaceSmCls), input, tip, errorMessage, $('<div>').addClass(hSpaceLgCls), $('<div>').addClass(flexBetweenCls).append(madeByText, saveButton) ); const modal = createModal({ className: settingsModalCls, title: 'Usage Tracker Settings', content: content }); return modal; }; const createDonationModal = () => { const subtitle = $('<p>').text('Thank you for considering a donation! Your support is appreciated.'); const createCopyButton = (text, successMessage) => { const button = $('<button>') .addClass(buttonDarkCls) .addClass(copyButtonCls) .text('Copy'); button.click(async () => { try { await navigator.clipboard.writeText(text); button.text(successMessage); setTimeout(() => button.text('Copy'), 2000); } catch (err) { error('Clipboard write failed:', err); button.text('Failed'); setTimeout(() => button.text('Copy'), 2000); } }); return button; }; const addressText = $('<p>').text('Bitcoin Address:'); const addressInput = $('<input>') .addClass(inputCls) .addClass(inputWithButtonCls) .attr('type', 'text') .val(bitcoinAddress) .prop('readonly', true); const copyAddressButton = createCopyButton(bitcoinAddress, 'Copied!'); const paymentLinkText = $('<p>').text('Payment link:'); const paymentLinkInput = $('<input>') .addClass(inputCls) .addClass(inputWithButtonCls) .attr('type', 'text') .val(bitcoinPaymentLink) .prop('readonly', true); const copyPaymentLinkButton = createCopyButton(bitcoinPaymentLink, 'Copied!'); const content = $('<div>').append( subtitle, $('<hr>'), addressText, addressInput, copyAddressButton, $('<hr>'), paymentLinkText, paymentLinkInput, copyPaymentLinkButton, $('<div>').addClass(hSpaceMdCls) ); return createModal({ className: donationModalCls, title: 'Donate', titleIconAfter: 'heart-handshake', content: content }); }; const createLucideIcon = ({ iconName, size = '16px', invert = false }) => { return $('<img>') .attr('src', `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`) .css({ width: size, height: size, display: 'inline-block', verticalAlign: 'text-bottom', filter: invert ? 'invert(1)' : 'none' }); }; const addSettingsButton = (mainCaption) => { const settingsButton = $('<button>') .css({ position: 'absolute', top: '-10px', right: '0px', height: '29.4px', width: '29.4px', padding: '0px', }) .addClass(buttonWhiteCls) .attr('title', 'Usage Tracker settings') .append(createLucideIcon({ iconName: 'settings' })); settingsButton.click(() => { log('Usage Tracker settings button clicked.'); $c(settingsModalCls).show(); const inputField = $c(settingsModalCls).find(`.${inputCls}`); inputField.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'); } }; const state = { addingUsageTrackerSucceeded: false, addingUsageTrackerAttempts: 0, }; const ATTEMPTS_LIMIT = 10; const ATTEMPTS_INTERVAL = 250; const ATTEMPTS_MAX_DELAY = 3000; /** * @returns {void} */ const main = () => { const scheduleNextAttempt = () => { if (!state.addingUsageTrackerSucceeded && state.addingUsageTrackerAttempts < ATTEMPTS_LIMIT) { const delay = Math.min(ATTEMPTS_INTERVAL * (2 ** (state.addingUsageTrackerAttempts - 1)), ATTEMPTS_MAX_DELAY); log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} failed. Retrying in ${delay}ms...`); setTimeout(main, delay); } else if (state.addingUsageTrackerSucceeded) { log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} succeeded.`); } else { log(`Attempt ${state.addingUsageTrackerAttempts} of ${ATTEMPTS_LIMIT} failed. No more attempts.`); } }; const decorationOkay = decorateUsageCard(); const paymentDay = GM_getValue('paymentDay'); if (decorationOkay && !paymentDay) { log('Payment day not set. Not adding tracker.'); return; } if (!decorationOkay) { log('Decoration failed. Try again later.'); scheduleNextAttempt(); state.addingUsageTrackerAttempts++; return; } state.addingUsageTrackerSucceeded = addUsageTracker(); state.addingUsageTrackerAttempts++; if (state.addingUsageTrackerAttempts === 1) { const paymentDay = GM_getValue('paymentDay'); if (!paymentDay) { log('Payment day not set. Not adding tracker.'); return; } } scheduleNextAttempt(); }; const bitcoinAddress = 'bc1qr7crhydmp68qpa0gumuf2h6jcvdtta4wju49r7'; const bitcoinPaymentLink = `bitcoin:${bitcoinAddress}`; $(document).ready(() => { log('Script started'); unsafeWindow.ut = { jq: $, resetSettings: () => { GM_setValue('paymentDay', undefined); location.reload(); } }; $('head').append($('<style>').text(styles)); $('body').append(createSettingsModal()); $('body').append(createDonationModal()); setTimeout(() => { TEST(); main(); }, ATTEMPTS_INTERVAL); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址