您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Highlight company directors and display their last action.
// ==UserScript== // @name Directors Last Action // @namespace https://www.torn.com/ // @version 3.7 // @description Highlight company directors and display their last action. // @author GFOUR [3498427] // @match https://www.torn.com/joblist.php* // @run-at document-end // @license Apache License 2.0 // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function () { 'use strict'; // Configuration (API key is stored dynamically) const API_KEY_STORAGE_KEY = 'torn_director_api_key'; // Function to get the API key (from storage) function getApiKey() { return GM_getValue(API_KEY_STORAGE_KEY, ''); } // Function to set the API key via GM menu prompt function setApiKey() { const currentKey = getApiKey(); const newKey = prompt('Enter your Torn API key (or "###PDA-APIKEY###" for TORN-PDA):', currentKey); if (newKey !== null && newKey.trim() !== '') { GM_setValue(API_KEY_STORAGE_KEY, newKey.trim()); console.log('API key updated! Refreshing directors.'); processDirectors(); // Refresh immediately without reload } } // Register menu command for easy access GM_registerMenuCommand('Set Torn API Key', setApiKey); // Get the API key const API_KEY = getApiKey(); // If no key is set, log warning and exit if (!API_KEY.trim()) { console.warn('Director Last Action Script: No API key set. Use the script menu (e.g., Tampermonkey icon > Set Torn API Key) to configure it. Processing skipped.'); return; } // Rest of the configuration const CACHE_TTL = 10 * 60 * 1000; // 10 minutes in ms (cache expiration) const REFRESH_INTERVAL = 60 * 1000; // 60 seconds for background refresh const DEBOUNCE_TIME = 500; // ms to debounce MutationObserver triggers const BATCH_SIZE = 25; // Fetch in batches of 20 to avoid rate limits (adjust if needed) const BATCH_DELAY = 1000; // 1 second delay between batches // Cache: Map of directorID => { lastAction: string, timestamp: number, activityLevel: 'active'|'moderate'|'inactive'|'unknown' } const cache = new Map(); const processing = new Set(); // To prevent concurrent fetches for the same ID // Menu command to clear cache GM_registerMenuCommand('Clear Cache (Force Refresh)', () => { cache.clear(); console.log('Cache cleared! Refreshing directors.'); processDirectors(); }); // Possible activity levels for class removal const activityLevels = ['active', 'moderate', 'inactive', 'unknown']; // Inject CSS for highlighting (only on the timestamp span) const style = document.createElement('style'); style.textContent = ` .director-last-action { font-size: 0.9em; margin-left: 5px; padding: 1px 3px; border-radius: 3px; font-weight: normal; } .director-last-action.active { color: #28a745; } .director-last-action.moderate { color: #d39e00; } .director-last-action.inactive { color: #c82333; } .director-last-action.unknown { color: #6c757d; } `; document.head.appendChild(style); // Helper: Parse relative time to minutes ago (approximate) function parseRelativeToMinutes(relative) { if (!relative || relative === 'Unknown' || relative === 'Error') return Infinity; const match = relative.match(/(\d+)\s*(second|minute|hour|day|week|month|year)s?\s*ago/); if (!match) return Infinity; const value = parseInt(match[1], 10); const unit = match[2]; switch (unit) { case 'second': return value / 60; case 'minute': return value; case 'hour': return value * 60; case 'day': return value * 1440; case 'week': return value * 10080; case 'month': return value * 43200; case 'year': return value * 525600; default: return Infinity; } } // Helper: Get activity level based on minutes ago function getActivityLevel(minutesAgo) { if (minutesAgo < 24 * 60) return 'active'; if (minutesAgo < 3 * 24 * 60) return 'moderate'; return 'inactive'; } // Fetch last action for multiple IDs concurrently, with batching to avoid rate limits async function fetchLastActions(ids) { const results = []; for (let i = 0; i < ids.length; i += BATCH_SIZE) { const batch = ids.slice(i, i + BATCH_SIZE); const batchPromises = batch.map(async (id) => { // Check cache first const cached = cache.get(id); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return { id, ...cached }; } try { const response = await fetch(`https://api.torn.com/user/${id}?key=${API_KEY}&selections=profile`); if (!response.ok) throw new Error(`API error: ${response.status}`); const data = await response.json(); console.log(`API response for ID ${id}:`, data.last_action); // Debug log const lastAction = data.last_action?.relative || 'Unknown'; const minutesAgo = parseRelativeToMinutes(lastAction); const activityLevel = (lastAction === 'Unknown' || lastAction === 'Error') ? 'unknown' : getActivityLevel(minutesAgo); // Cache successful responses only (skip if 'Unknown' to allow retry) if (lastAction !== 'Unknown' && lastAction !== 'Error') { cache.set(id, { lastAction, timestamp: Date.now(), activityLevel }); } return { id, lastAction, activityLevel }; } catch (error) { console.error(`Failed to fetch data for director ID ${id}:`, error); // Don't cache errors - allow retry next time return { id, lastAction: 'Error', activityLevel: 'unknown' }; } }); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // Delay between batches to avoid rate limits if (i + BATCH_SIZE < ids.length) { await new Promise(resolve => setTimeout(resolve, BATCH_DELAY)); } } return results; } // Process directors in the list async function processDirectors() { const companiesList = document.querySelectorAll('.company-list .item'); const directorsToFetch = new Set(); // Unique IDs to fetch const elementsMap = new Map(); // ID => { link: element, directorElement: element } companiesList.forEach(company => { const directorElement = company.querySelector('.director'); if (!directorElement) return; const directorLink = directorElement.querySelector('a'); if (!directorLink) return; // Extract ID const directorID = directorLink.href.split('XID=')[1]; if (!directorID) return; // Skip if being processed or if already has fresh data if (processing.has(directorID)) return; const existingSpan = directorLink.querySelector('.director-last-action'); const existingData = directorLink.getAttribute('data-last-action'); const cached = cache.get(directorID); if (existingSpan && cached && cached.lastAction === existingData && Date.now() - cached.timestamp < CACHE_TTL) { return; // Skip if fresh } directorsToFetch.add(directorID); elementsMap.set(directorID, { link: directorLink, directorElement }); }); if (directorsToFetch.size === 0) return; // Mark as processing directorsToFetch.forEach(id => processing.add(id)); // Fetch and update const results = await fetchLastActions(Array.from(directorsToFetch)); results.forEach(({ id, lastAction, activityLevel }) => { const { link } = elementsMap.get(id); if (!link) return; // Skip UI update for 'Unknown' or 'Error' (hides them to reduce clutter) if (lastAction === 'Unknown' || lastAction === 'Error') { // Optional: Uncomment next line to display ": Unknown" anyway // lastAction = 'Unknown'; processing.delete(id); return; } // Find or create the timestamp span (prevention: check again) let timestampSpan = link.querySelector('.director-last-action'); if (!timestampSpan) { timestampSpan = document.createElement('span'); timestampSpan.classList.add('director-last-action'); link.appendChild(timestampSpan); } // Update text and class timestampSpan.textContent = `: ${lastAction}`; // Remove old activity classes activityLevels.forEach(level => timestampSpan.classList.remove(level)); timestampSpan.classList.add(activityLevel); // Mark as processed/updated link.setAttribute('data-last-action', lastAction); // Done processing processing.delete(id); }); } // Debounce function to prevent rapid calls function debounce(fn, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; } // Set up MutationObserver to watch for dynamic changes const observer = new MutationObserver(debounce(() => processDirectors(), DEBOUNCE_TIME)); const targetNode = document.querySelector('.company-list') || document.body; // Fallback to body if not found if (targetNode) { observer.observe(targetNode, { childList: true, subtree: true }); } // Initial process processDirectors(); // Background refresh interval (re-processes without clearing cache) setInterval(() => { processDirectors(); }, REFRESH_INTERVAL); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址