您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Deletes your replies on X's /with_replies page. Run on x.com/yourusername/with_replies. Resumes after page reloads. Follow X's ToS.
// ==UserScript== // @name X Reply Deleter // @namespace http://tampermonkey.net/ // @version 1.0 // @description Deletes your replies on X's /with_replies page. Run on x.com/yourusername/with_replies. Resumes after page reloads. Follow X's ToS. // @author You // @match https://x.com/*/with_replies // @grant none // @license AGPL // ==/UserScript== (async () => { 'use strict'; // Function to wait for an element const waitForElement = (selector, context = document, timeout = 8000, retries = 3) => { return new Promise((resolve, reject) => { let attempts = 0; const tryFind = () => { const element = context.querySelector(selector); if (element && element.offsetParent !== null) { console.log(`Found element ${selector}: Visible = ${element.offsetParent !== null}`); return resolve(element); } if (attempts >= retries) return reject(new Error(`Timeout waiting for ${selector} after ${retries} attempts`)); attempts++; const start = Date.now(); const interval = setInterval(() => { const el = context.querySelector(selector); if (el && el.offsetParent !== null) { clearInterval(interval); resolve(el); } else if (Date.now() - start > timeout) { clearInterval(interval); tryFind(); } }, 200); }; tryFind(); }); }; // Function to simulate a click const clickElement = async (element, description) => { if (!element) { console.error(`No ${description} found.`); return false; } element.scrollIntoView({ behavior: 'smooth', block: 'center' }); await new Promise(resolve => setTimeout(resolve, 500)); element.dispatchEvent(new MouseEvent('click', { bubbles: true })); console.log(`Clicked ${description}`); return true; }; // Function to scroll and wait for new content const scrollAndWait = async () => { let lastHeight = document.body.scrollHeight; let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { window.scrollTo(0, document.body.scrollHeight); await new Promise(resolve => setTimeout(resolve, 4000)); const newHeight = document.body.scrollHeight; if (newHeight === lastHeight) break; lastHeight = newHeight; attempts++; } console.log("Finished scrolling. Waiting for replies to load..."); const initialReplyCount = document.querySelectorAll('article[data-testid="tweet"]').length; await new Promise(resolve => setTimeout(resolve, 12000)); const newReplyCount = document.querySelectorAll('article[data-testid="tweet"]').length; console.log(`Replies loaded: ${newReplyCount} (was ${initialReplyCount})`); }; // Function to save progress const saveProgress = (batchCount, processedReplies, isRunning) => { sessionStorage.setItem('xReplyDeleterProgress', JSON.stringify({ batchCount, processedReplies, isRunning })); console.log(`Saved progress: batch ${batchCount}, processed ${processedReplies} replies, running: ${isRunning}`); }; // Function to load progress const loadProgress = () => { const progress = sessionStorage.getItem('xReplyDeleterProgress'); return progress ? JSON.parse(progress) : { batchCount: 0, processedReplies: 0, isRunning: false }; }; // Function to process a single reply const deleteReply = async (article, index) => { try { const tweetText = article.querySelector('div[data-testid="tweetText"]')?.innerText || "No text available"; const userLink = article.querySelector('a[href*="/"]')?.href || "Unknown user"; console.log(`Reply ${index + 1}: Processing reply from ${userLink}: "${tweetText.slice(0, 50)}..."`); console.log(`Reply ${index + 1}: Article HTML: ${article.outerHTML.slice(0, 200)}...`); article.scrollIntoView({ behavior: 'smooth', block: 'center' }); await new Promise(resolve => setTimeout(resolve, 1000)); const moreButtonSelector = 'button[data-testid="caret"]'; let moreButton; try { moreButton = await waitForElement(moreButtonSelector, article, 8000, 3); } catch (error) { console.warn(`Reply ${index + 1}: ${error.message}. Trying alternative selector...`); moreButton = article.querySelector('button[aria-label="More"]'); console.log(`Reply ${index + 1}: Alternative 'More' button: ${moreButton ? moreButton.outerHTML.slice(0, 200) : "Not found"}...`); if (!moreButton) return false; } if (!await clickElement(moreButton, "'More' button")) return false; await new Promise(resolve => setTimeout(resolve, 1500)); const deleteButtonSelector = 'div[role="menuitem"]'; const menuItems = document.querySelectorAll(deleteButtonSelector); let deleteButton = null; for (const item of menuItems) { const spans = item.querySelectorAll('span'); for (const span of spans) { if (span.innerText.toLowerCase().includes("delete")) { deleteButton = item; break; } } if (deleteButton) break; } if (!deleteButton) { console.warn(`Reply ${index + 1}: No 'Delete' option found. Menu HTML: ${document.querySelector('div[role="menu"]')?.outerHTML.slice(0, 200) || "No menu"}...`); return false; } if (!await clickElement(deleteButton, "'Delete' option")) return false; await new Promise(resolve => setTimeout(resolve, 3000)); const modal = document.querySelector('div[class*="css-175oi2r"][class*="r-13qz1uu"]') || document.body; console.log(`Reply ${index + 1}: Modal HTML: ${modal.outerHTML.slice(0, 200)}...`); const confirmButtonSelector = 'button[data-testid="confirmationSheetConfirm"]'; const confirmButton = await waitForElement(confirmButtonSelector, document, 8000, 3); if (!await clickElement(confirmButton, "'Confirm Delete' button")) return false; console.log(`Reply ${index + 1}: Successfully deleted.`); return true; } catch (error) { console.error(`Reply ${index + 1}: Error deleting: ${error.message}`); return false; } }; // Create control panel const createControlPanel = () => { const panel = document.createElement('div'); panel.style.position = 'fixed'; panel.style.top = '10px'; panel.style.right = '10px'; panel.style.zIndex = '10000'; panel.style.background = '#fff'; panel.style.border = '1px solid #000'; panel.style.padding = '10px'; panel.style.borderRadius = '5px'; panel.innerHTML = ` <h3>X Reply Deleter</h3> <p>Status: <span id="deleterStatus">Stopped</span></p> <p>Processed: <span id="deleterProcessed">0</span> replies in <span id="deleterBatch">0</span> batches</p> <button id="startDeleter">Start</button> <button id="stopDeleter" disabled>Stop</button> <p><small>Run on x.com/yourusername/with_replies. Follow X's ToS.</small></p> `; document.body.appendChild(panel); return panel; }; // Main function const main = async () => { let { batchCount, processedReplies, isRunning } = loadProgress(); let isStopped = !isRunning; const panel = createControlPanel(); const status = panel.querySelector('#deleterStatus'); const processedDisplay = panel.querySelector('#deleterProcessed'); const batchDisplay = panel.querySelector('#deleterBatch'); const startButton = panel.querySelector('#startDeleter'); const stopButton = panel.querySelector('#stopDeleter'); const updateUI = () => { status.textContent = isStopped ? 'Stopped' : 'Running'; processedDisplay.textContent = processedReplies; batchDisplay.textContent = batchCount; startButton.disabled = !isStopped; stopButton.disabled = isStopped; }; updateUI(); startButton.addEventListener('click', () => { isStopped = false; isRunning = true; saveProgress(batchCount, processedReplies, isRunning); updateUI(); runLoop(); }); stopButton.addEventListener('click', () => { isStopped = true; isRunning = false; saveProgress(batchCount, processedReplies, isRunning); updateUI(); console.log('Stopped by user.'); }); const runLoop = async () => { let consecutiveBatchFailures = 0; const maxConsecutiveBatchFailures = 3; let isReloading = false; window.addEventListener('beforeunload', () => { if (!isReloading) { saveProgress(batchCount, processedReplies, isRunning); console.log('Page is refreshing. Progress saved.'); } }); while (!isStopped) { if (!window.location.href.includes('/with_replies')) { console.log('Not on /with_replies page. Stopping.'); isStopped = true; isRunning = false; saveProgress(batchCount, processedReplies, isRunning); updateUI(); return; } console.log(`Batch ${batchCount + 1}: Waiting for DOM to stabilize...`); await new Promise(resolve => setTimeout(resolve, 3000)); const username = window.location.pathname.split('/')[1]; const replies = Array.from(document.querySelectorAll('article[data-testid="tweet"]')).filter(article => { const userLink = article.querySelector('a[href*="/' + username + '"]'); return userLink !== null; }); if (!replies.length) { console.log('No more replies found. Attempting to load more...'); await scrollAndWait(); const newReplies = Array.from(document.querySelectorAll('article[data-testid="tweet"]')).filter(article => { const userLink = article.querySelector('a[href*="/' + username + '"]'); return userLink !== null; }); if (!newReplies.length) { console.log('No more replies found after scrolling. Refreshing page...'); isReloading = true; saveProgress(batchCount, processedReplies, isRunning); location.reload(); await new Promise(resolve => setTimeout(resolve, 5000)); consecutiveBatchFailures++; if (consecutiveBatchFailures >= maxConsecutiveBatchFailures) { console.log('Too many consecutive batch failures. Exiting script.'); isStopped = true; isRunning = false; saveProgress(batchCount, processedReplies, isRunning); sessionStorage.removeItem('xReplyDeleterProgress'); updateUI(); return; } continue; } replies.push(...newReplies); } else { consecutiveBatchFailures = 0; } console.log(`Batch ${batchCount + 1}: Found ${replies.length} replies on this page.`); let batchFailures = 0; const maxBatchFailures = 3; const batchTimeout = 60000; const batchStart = Date.now(); for (const [index, reply] of replies.entries()) { if (isStopped) break; if (Date.now() - batchStart > batchTimeout) { console.log(`Batch ${batchCount + 1}: Timeout reached. Refreshing page...`); isReloading = true; saveProgress(batchCount, processedReplies, isRunning); location.reload(); await new Promise(resolve => setTimeout(resolve, 5000)); break; } if (await deleteReply(reply, index)) { batchFailures = 0; processedReplies++; saveProgress(batchCount, processedReplies, isRunning); updateUI(); } else { batchFailures++; if (batchFailures >= maxBatchFailures) { console.log(`Batch ${batchCount + 1}: Too many failures (${batchFailures}). Refreshing page...`); isReloading = true; saveProgress(batchCount, processedReplies, isRunning); location.reload(); await new Promise(resolve => setTimeout(resolve, 5000)); break; } } await new Promise(resolve => setTimeout(resolve, 5000 + Math.random() * 1000)); } if (isStopped) break; batchCount++; saveProgress(batchCount, processedReplies, isRunning); updateUI(); console.log(`Batch ${batchCount}: Finished processing visible replies. Scrolling to load more...`); await scrollAndWait(); } console.log(`Script completed. Processed ${processedReplies} replies across ${batchCount} batches.`); sessionStorage.removeItem('xReplyDeleterProgress'); updateUI(); }; // Auto-start if was running before refresh if (isRunning) { console.log('Resuming from previous session...'); runLoop(); } }; // Initialize main(); })();