您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The 100% complete, unabridged version of the Cache-First architecture. Now only processes visible elements in the viewport.
// ==UserScript== // @name Cute-ify All Web Pages (V7.8.2 "Viewport Aware") // @namespace http://tampermonkey.net/ // @version 7.8.2 // @description The 100% complete, unabridged version of the Cache-First architecture. Now only processes visible elements in the viewport. // @author Bytebender // @match *://*/* // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM_registerMenuCommand // @connect * // @license MIT // ==/UserScript== // 可能可以解决并发问题 // @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js (function() { 'use strict'; console.log('[Cuteify] Script execution started.'); // --- ⚙️ 用户配置区 START --- const config = { scanInterval: 2000, batchSize: 10, processShadowDOM: true, enableDebugLogging: true, apiKey: 'sk-3P5P8odkGLoPlNPj0QGBCgw8m083aaI8706HTNYXhujTk405', baseUrl: 'https://elysia.h-e.top/v1', model: 'gpt-4.1-mini', prompt: `你是一个文本风格转换专家。用户会提供一段XML,里面包含多个被 <text><![CDATA[...]]></text> 包裹的字符串。 请将每一个字符串都转换成一种略微可爱、俏皮、活泼的风格。你必须保持其核心意思不变。 你的回复必须是一个格式完全正确的XML,且结构与输入完全相同。每一个翻译后的文本都必须同样被 <text><![CDATA[...]]></text> 包裹。 输入的 <text> 元素数量必须与输出的 <text> 元素数量严格相等。除了这个XML结构,不要包含任何其他说明或标记。`, maxConcurrency: 1, minLengthToProcess: 10, }; // --- ⚙️ 用户配置区 END --- const CACHE_PREFIX = 'cutify_cache_'; const STATE_ATTR = 'data-cutify-state'; const logger = { log: (...args) => config.enableDebugLogging && console.log('[Cuteify]', ...args), error: (...args) => console.error('[Cuteify]', ...args), }; let currentBatch = []; let activeRequests = 0; async function findAndCollectWorkItems(rootNode) { const attributesToProcess = ['placeholder', 'title', 'alt']; const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null, false); let node; while (node = walker.nextNode()) { if (currentBatch.length >= config.batchSize) { dispatchBatch(); } const element = node.nodeType === Node.TEXT_NODE ? node.parentNode : node; if (!element || element.hasAttribute(STATE_ATTR) || !isVisible(element)) { continue; } if (node.nodeType === Node.TEXT_NODE) { const parentTag = element.tagName; if (parentTag && parentTag !== 'SCRIPT' && parentTag !== 'STYLE' && parentTag !== 'NOSCRIPT' && parentTag !== 'TEXTAREA' && !element.isContentEditable) { const text = node.nodeValue.trim(); if (text.length >= config.minLengthToProcess) { const cachedText = await getFromCache(text); if (cachedText) { applyChange({ type: 'text', element: node }, cachedText, 'done'); } else { currentBatch.push({ type: 'text', element: node, originalText: text }); element.setAttribute(STATE_ATTR, 'queued'); } } } } else if (node.nodeType === Node.ELEMENT_NODE) { for (const attr of attributesToProcess) { if (node.hasAttribute(attr)) { const text = node.getAttribute(attr).trim(); if (text.length >= config.minLengthToProcess) { const cachedText = await getFromCache(text); if (cachedText) { applyChange({ type: 'attribute', element: node, attr: attr }, cachedText, 'done'); } else { currentBatch.push({ type: 'attribute', element: node, attr: attr, originalText: text }); node.setAttribute(STATE_ATTR, 'queued'); } break; } } } } } } async function scanAndDispatch() { try { await findAndCollectWorkItems(document.body); if (config.processShadowDOM) { const hosts = document.querySelectorAll('*'); for (const host of hosts) { if (host.shadowRoot) { await findAndCollectWorkItems(host.shadowRoot); } } } if (currentBatch.length > 0) { dispatchBatch(); } } catch (e) { logger.error("Error during scanAndDispatch:", e); } } function dispatchBatch() { const batchToProcess = [...currentBatch]; currentBatch = []; if (batchToProcess.length === 0) return; if (activeRequests >= config.maxConcurrency) { logger.log(`Concurrency limit (${config.maxConcurrency}) reached. Discarding batch of ${batchToProcess.length} items. They will be retried on next scan.`); batchToProcess.forEach(item => { const el = item.type === 'text' ? item.element.parentNode : item.element; if (el) { el.removeAttribute(STATE_ATTR); } }); return; } logger.log(`Dispatching a batch of ${batchToProcess.length} items. Active requests: ${activeRequests + 1}/${config.maxConcurrency}`); activeRequests++; processBatch(batchToProcess).finally(() => { activeRequests--; logger.log(`A batch finished. Active requests: ${activeRequests}/${config.maxConcurrency}`); }); } async function processBatch(batch) { batch.forEach(item => { const el = item.type === 'text' ? item.element.parentNode : item.element; if (el && el.getAttribute(STATE_ATTR) === 'queued') { el.setAttribute(STATE_ATTR, 'processing'); } }); try { const uniqueTextsToFetch = [...new Set(batch.map(item => item.originalText))]; const cuteTexts = await cuteifyBatch(uniqueTextsToFetch); const translationMap = new Map(); uniqueTextsToFetch.forEach((text, i) => translationMap.set(text, cuteTexts[i])); const finalizationTasks = batch.map(async (item) => { const cuteText = translationMap.get(item.originalText); if (cuteText) { await saveToCache(item.originalText, cuteText); applyChange(item, cuteText, 'done'); } else { applyChange(item, item.originalText, null); } }); await Promise.all(finalizationTasks); } catch (error) { logger.error(`Failed to process a batch:`, error, 'Reverting items to "pending".'); batch.forEach(item => applyChange(item, item.originalText, null)); } } function cuteifyBatch(texts) { return new Promise((resolve, reject) => { const xmlPayload = '<texts>' + texts.map(t => `<text><![CDATA[${t}]]></text>`).join('') + '</texts>'; const payload = { model: config.model, messages: [{ role: "system", content: config.prompt },{ role: "user", content: xmlPayload }], temperature: 0.7, stream: false, }; const requestDetails = { method: "POST", url: `${config.baseUrl}/chat/completions`, headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` }, data: JSON.stringify(payload), timeout: 20000 }; logger.log("Sending XML batch request with", texts.length, "items."); GM_xmlhttpRequest({ ...requestDetails, onload: (response) => { if (response.status >= 200 && response.status < 300) { try { const content = JSON.parse(response.responseText).choices[0].message.content; const parser = new DOMParser(); const xmlDoc = parser.parseFromString(content, "text/xml"); if (xmlDoc.getElementsByTagName("parsererror").length > 0) { throw new Error(`AI returned malformed XML: ${xmlDoc.getElementsByTagName("parsererror")[0].innerText}`); } const cuteTextNodes = xmlDoc.querySelectorAll("text"); if (cuteTextNodes.length === texts.length) { const cuteTextsArray = Array.from(cuteTextNodes).map(node => node.textContent); resolve(cuteTextsArray); } else { reject(`XML response format invalid: text count mismatch. Expected ${texts.length}, got ${cuteTextNodes.length}`); } } catch (e) { reject(`Response parsing failed: ${e.message}`); } } else { reject(`API request failed, status: ${response.status}`); } }, onerror: (error) => reject(`Network request error: ${JSON.stringify(error)}`), ontimeout: () => reject("Request timed out after 60 seconds.") }); }); } /** * MODIFIED: This function now checks if an element is truly visible within the browser's viewport. * @param {Element} el The element to check. * @returns {boolean} True if the element is visible on screen, false otherwise. */ function isVisible(el) { if (!el || el.nodeType !== Node.ELEMENT_NODE || !el.isConnected) { return false; } // Use getBoundingClientRect to get geometry and position info. // If width or height is 0, it's not visible (this also covers display: none). const rect = el.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) { return false; } // Check CSS properties that can hide an element without affecting its dimensions. const style = window.getComputedStyle(el); if (style.visibility === 'hidden' || style.opacity === '0') { return false; } // Finally, check if the element is at least partially within the viewport's bounds. const viewportHeight = window.innerHeight || document.documentElement.clientHeight; const viewportWidth = window.innerWidth || document.documentElement.clientWidth; const isInViewport = ( rect.top < viewportHeight && rect.bottom > 0 && rect.left < viewportWidth && rect.right > 0 ); return isInViewport; } async function clearCache() { logger.log('Starting cache clearing process...'); const allKeys = await GM.listValues(); const tasks = allKeys.filter(key => key.startsWith(CACHE_PREFIX)).map(key => GM.deleteValue(key)); await Promise.all(tasks); document.querySelectorAll(`[${STATE_ATTR}]`).forEach(el => el.removeAttribute(STATE_ATTR)); const clearedCount = tasks.length; logger.log(`Cache clearing complete. ${clearedCount} items removed. All state marks reset.`); alert(`可爱化缓存已清除!删除了 ${clearedCount} 个项目。\n页面将重新扫描所有文本。`); scanAndDispatch(); } GM_registerMenuCommand('清除可爱化缓存 (Clear Cute-ify Cache)', clearCache); function applyChange(item, newText, state) { try { const element = item.type === 'text' ? item.element.parentNode : item.element; if (!element || !element.isConnected) return; if (item.type === 'text') { item.element.nodeValue = ` ${newText} `; } else if (item.type === 'attribute') { element.setAttribute(item.attr, newText); } if (state) { element.setAttribute(STATE_ATTR, state); } else { element.removeAttribute(STATE_ATTR); } } catch (e) { /* Ignore */ } } function preflightCheck() { if (!config.apiKey || config.apiKey.includes('sk-xxxxx')) { logger.error("FATAL ERROR: API Key is not configured!"); return false; } return true; } async function getFromCache(key) { return await GM.getValue(CACHE_PREFIX + key, null); } async function saveToCache(key, value) { await GM.setValue(CACHE_PREFIX + key, value); } let scanIntervalId = null; let mutationObserver = null; function main() { if (!preflightCheck()) return; logger.log(`V7.8.2 "Viewport Aware" is running! Instant translations for cached text. (b^-^)b`); // Add scroll and resize event listeners to re-scan when the viewport changes let debounceTimer; const debouncedScan = () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(scanAndDispatch, 100); // 100ms debounce }; window.addEventListener('scroll', debouncedScan, { passive: true }); window.addEventListener('resize', debouncedScan, { passive: true }); scanAndDispatch(); scanIntervalId = setInterval(scanAndDispatch, config.scanInterval); mutationObserver = new MutationObserver(debouncedScan); mutationObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();