您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Add hours remaining and total spend rate to balance display on Vast.ai + Improved Rate Limiting
// ==UserScript== // @license MIT // @name Vast.ai Balance Hours Remaining // @namespace http://tampermonkey.net/ // @version 0.2 // @description Add hours remaining and total spend rate to balance display on Vast.ai + Improved Rate Limiting // @author You // @match https://cloud.vast.ai/* // @grant GM_xmlhttpRequest // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // State variables let lastCredit = 0; let totalSpendRate = 0; let isUpdating = false; const UPDATE_INTERVAL = 120000; // 2 minutes between updates const RETRY_DELAYS = [30000, 60000, 120000, 300000]; // Progressive retry delays let retryAttempt = 0; // Throttle function to prevent too frequent updates const throttle = (func, limit) => { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } } function formatTimeRemaining(hours) { if (!isFinite(hours) || isNaN(hours)) return "-- remaining"; const totalHours = Math.floor(hours); const minutes = Math.round((hours - totalHours) * 60); if (totalHours === 0) { return `${minutes}m remaining`; } else if (minutes === 0) { return `${totalHours}h remaining`; } else { return `${totalHours}h ${minutes}m remaining`; } } function updateDisplay() { const creditDiv = document.evaluate( '/html/body/div[1]/div[1]/header/div[2]/span[1]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; if (creditDiv) { let hoursDiv = document.getElementById('hours-remaining'); if (!hoursDiv) { hoursDiv = document.createElement('span'); hoursDiv.id = 'hours-remaining'; hoursDiv.style.fontSize = '14px'; hoursDiv.style.marginLeft = '10px'; creditDiv.after(hoursDiv); } const hoursRemaining = totalSpendRate > 0 ? lastCredit / totalSpendRate : 0; hoursDiv.innerHTML = `<span style="color: green; font-weight: bold">($${totalSpendRate.toFixed(3)}/hr)</span> <span style="color: #66cc66; font-weight: bold">${formatTimeRemaining(hoursRemaining)}</span>`; } } async function fetchWithRetry(url, attempt = 0) { try { const response = await fetch(url); if (response.ok) { retryAttempt = 0; // Reset retry attempts on success return await response.json(); } if (response.status === 429) { const delay = RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1]; console.warn(`Rate limited. Retrying in ${delay/1000} seconds...`); await new Promise(resolve => setTimeout(resolve, delay)); return fetchWithRetry(url, attempt + 1); } throw new Error(`HTTP error! status: ${response.status}`); } catch (error) { console.error(`Error fetching ${url}:`, error); throw error; } } async function updateData() { if (isUpdating) return; isUpdating = true; try { // Fetch user data and instances in sequence const userData = await fetchWithRetry('https://cloud.vast.ai/api/v0/users/current/'); lastCredit = userData.credit; const instancesData = await fetchWithRetry('https://cloud.vast.ai/api/v0/instances/'); totalSpendRate = 0; if (instancesData.instances && Array.isArray(instancesData.instances)) { instancesData.instances.forEach(instance => { if (instance.search && instance.search.totalHour) { totalSpendRate += instance.search.totalHour; } }); } updateDisplay(); } catch (error) { console.error('Failed to update data:', error); } finally { isUpdating = false; } } // Throttled version of updateData const throttledUpdate = throttle(updateData, UPDATE_INTERVAL); // Initial update updateData(); // Set up periodic updates setInterval(throttledUpdate, UPDATE_INTERVAL); // Modified fetch interceptor const originalFetch = window.fetch; window.fetch = async function(...args) { const response = await originalFetch.apply(this, args); const url = args[0].toString(); if ((url.includes('/api/v0/users/current/') || url.includes('/api/v0/instances/'))) { throttledUpdate(); } return response; }; })();