您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a "Select Chats" button to ChatGPT for deleting multiple conversations at once. Bypasses the UI and uses direct API calls for speed and reliability.
// ==UserScript== // @name ChatGPT Bulk Deleter // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description Adds a "Select Chats" button to ChatGPT for deleting multiple conversations at once. Bypasses the UI and uses direct API calls for speed and reliability. // @author Tano // @match https://chatgpt.com/* // @connect chatgpt.com // @grant GM_addStyle // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Styles for the UI controls --- GM_addStyle(` .bulk-delete-controls { padding: 8px; margin: 8px; border: 1px solid var(--token-border-light); border-radius: 12px; background-color: var(--token-main-surface-primary); display: flex; flex-direction: column; gap: 8px; } .bulk-delete-btn { display: inline-block; width: 100%; padding: 10px 12px; border: none; border-radius: 8px; cursor: pointer; text-align: center; font-size: 14px; font-weight: 500; transition: background-color 0.2s, color 0.2s; } #toggle-select-btn { background-color: #4C50D3; color: white; } #toggle-select-btn:hover { background-color: #3a3eab; } #toggle-select-btn.selection-active { background-color: #FFD6D6; color: #D34C4C; } #delete-selected-btn { background-color: #D34C4C; color: white; display: none; } #delete-selected-btn:hover { background-color: #b03a3a; } #delete-selected-btn:disabled { background-color: #7c7c7c; cursor: not-allowed; } .chat-selectable { cursor: cell !important; } a.chat-selected { background-color: rgba(76, 80, 211, 0.2) !important; border: 1px solid #4C50D3 !important; border-radius: 8px; } `); let selectionMode = false; const selectedChats = new Set(); let authToken = null; /** * Fetches the authorization token required for API calls. * The token is cached after the first successful retrieval. */ async function getAuthToken() { if (authToken) return authToken; try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://chatgpt.com/api/auth/session", onload: resolve, onerror: reject }); }); const data = JSON.parse(response.responseText); if (data && data.accessToken) { authToken = data.accessToken; console.log("Auth Token successfully retrieved."); return authToken; } throw new Error("accessToken not found in session response."); } catch (error) { console.error("Failed to retrieve auth token:", error); alert("Could not retrieve authorization token. The script cannot continue."); return null; } } /** * Creates and injects the control buttons into the UI. */ function initialize() { const historyContainer = document.querySelector('#history'); if (!historyContainer || document.getElementById('toggle-select-btn')) return; getAuthToken(); // Pre-fetch the token on load console.log("ChatGPT Bulk Deleter: Initializing Command Center..."); const controlsContainer = document.createElement('div'); controlsContainer.className = 'bulk-delete-controls'; const toggleBtn = document.createElement('button'); toggleBtn.id = 'toggle-select-btn'; toggleBtn.className = 'bulk-delete-btn'; toggleBtn.textContent = 'Select Chats to Delete'; toggleBtn.onclick = toggleSelectionMode; const deleteBtn = document.createElement('button'); deleteBtn.id = 'delete-selected-btn'; deleteBtn.className = 'bulk-delete-btn'; deleteBtn.textContent = 'Delete Selected (0)'; deleteBtn.onclick = deleteSelectedChats; controlsContainer.appendChild(toggleBtn); controlsContainer.appendChild(deleteBtn); historyContainer.parentNode.insertBefore(controlsContainer, historyContainer); } /** * Toggles the chat selection mode on/off. */ function toggleSelectionMode() { selectionMode = !selectionMode; const toggleBtn = document.getElementById('toggle-select-btn'), deleteBtn = document.getElementById('delete-selected-btn'), chatItems = document.querySelectorAll('a[href^="/c/"]'); if (selectionMode) { toggleBtn.textContent = 'Cancel Selection'; toggleBtn.classList.add('selection-active'); deleteBtn.style.display = 'block'; chatItems.forEach(chat => { chat.classList.add('chat-selectable'); chat.addEventListener('click', handleChatClick, true); }); } else { toggleBtn.textContent = 'Select Chats to Delete'; toggleBtn.classList.remove('selection-active'); deleteBtn.style.display = 'none'; chatItems.forEach(chat => { chat.classList.remove('chat-selectable', 'chat-selected'); chat.removeEventListener('click', handleChatClick, true); }); selectedChats.clear(); updateDeleteButton(); } } /** * Handles clicks on chat items when in selection mode. */ function handleChatClick(event) { event.preventDefault(); event.stopPropagation(); const chatElement = event.currentTarget; if (selectedChats.has(chatElement)) { selectedChats.delete(chatElement); chatElement.classList.remove('chat-selected'); } else { selectedChats.add(chatElement); chatElement.classList.add('chat-selected'); } updateDeleteButton(); } /** * Updates the counter on the delete button. */ function updateDeleteButton() { const deleteBtn = document.getElementById('delete-selected-btn'); if(deleteBtn) deleteBtn.textContent = `Delete Selected (${selectedChats.size})`; } /** * Main function to delete selected chats via API calls. */ async function deleteSelectedChats() { if (selectedChats.size === 0) return alert('Please select at least one chat first.'); const token = await getAuthToken(); if (!token) return; if (!confirm(`Are you sure you want to delete ${selectedChats.size} chat(s)? This action is irreversible.`)) return; const chatsToDelete = Array.from(selectedChats); const total = chatsToDelete.length; const deleteBtn = document.getElementById('delete-selected-btn'); const toggleBtn = document.getElementById('toggle-select-btn'); deleteBtn.disabled = true; toggleBtn.disabled = true; let successCount = 0; let errorCount = 0; for (let i = 0; i < total; i++) { const chatElement = chatsToDelete[i]; const href = chatElement.getAttribute('href'); const conversationId = href.split('/').pop(); const chatTitle = chatElement.textContent.trim(); deleteBtn.textContent = `Deleting... (${i + 1}/${total})`; console.log(`[API CALL] Deleting chat ${i + 1}/${total}: "${chatTitle}" (ID: ${conversationId})`); try { // The API call that "hides" the chat, effectively deleting it from view. await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "PATCH", url: `https://chatgpt.com/backend-api/conversation/${conversationId}`, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, data: JSON.stringify({ is_visible: false }), onload: (response) => { if (response.status >= 200 && response.status < 300) { resolve(response); } else { reject(new Error(`Server responded with status ${response.status}`)); } }, onerror: reject }); }); console.log(` -> [SUCCESS] Chat "${chatTitle}" successfully hidden via API.`); chatElement.style.transition = 'opacity 0.5s'; chatElement.style.opacity = '0'; setTimeout(() => chatElement.remove(), 500); // Visually remove from the list successCount++; } catch (error) { console.error(` -> [FAIL] API call failed for "${chatTitle}":`, error); chatElement.style.border = '2px solid red'; // Mark chats that failed to delete errorCount++; } } alert(`Complete. Successfully deleted: ${successCount}. Errors: ${errorCount}.`); deleteBtn.disabled = false; toggleBtn.disabled = false; toggleSelectionMode(); // Reset the UI } // --- Observer to initialize the script when the history list is loaded --- const observer = new MutationObserver(() => { if (document.querySelector('#history') && !document.getElementById('toggle-select-btn')) { initialize(); } }); observer.observe(document.body, { childList: true, subtree: true }); })();