Greasy Fork is available in English.
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 });
})();