Greasy Fork

Greasy Fork is available in English.

ChatGPT Bulk Deleter

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.

当前为 2025-06-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 });

})();