您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Gerencia conversas em massa e substitui o botão "Upgrade" pelo painel de controle.
// ==UserScript== // @name Gerenciador de chats ChatGPT (Substitui Botão Upgrade) // @namespace http://tampermonkey.net/ // @version 2.1 // @description Gerencia conversas em massa e substitui o botão "Upgrade" pelo painel de controle. // @author luascfl // @match https://chat.openai.com/* // @match https://chatgpt.com/* // @icon https://cdn-icons-png.flaticon.com/512/16459/16459818.png // @home https://github.com/luascfl/manage-chats-chatgpt // @supportURL https://github.com/luascfl/manage-chats-chatgpt/issues // @license MIT // @grant none // ==/UserScript== (function() { 'use strict'; console.log('[ChatManager v2.1] Script iniciado.'); const PLATFORMS = { 'chat.openai.com': { name: 'ChatGPT', selectors: { entryPoint: 'nav', chatItems: 'a[href^="/c/"]', chatLink: 'a[href^="/c/"]', chatTitle: 'div.truncate' }, api: { base: window.location.origin, tokenEndpoint: '/api/auth/session', conversationEndpoint: '/backend-api/conversation/', tokenExtractor: (data) => data.accessToken }, priorityEmoji: '❗', upgradeButtonText: 'Fazer upgrade do plano' }, }; const PLATFORM = PLATFORMS[window.location.hostname] || PLATFORMS['chat.openai.com']; const SELECTOR = PLATFORM.selectors; const PRIORITY_EMOJI = PLATFORM.priorityEmoji; const API_BASE = PLATFORM.api.base; class UIManager { constructor() { this.addStyles(); } addStyles() { if (document.getElementById('chat-manager-styles')) return; const styleEl = document.createElement('style'); styleEl.id = 'chat-manager-styles'; styleEl.innerHTML = ` .mass-actions { padding: 8px; margin: 2px 8px; border-radius: 8px; background-color: var(--surface-secondary); } .mass-actions-title { font-weight: bold; margin-bottom: 10px; font-size: 14px; text-align: center; color: var(--text-primary); } .mass-actions-btn { width: 100%; justify-content: center; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border-medium); margin-bottom: 5px; background-color: var(--surface-primary); display: flex; align-items: center; } .mass-actions-btn:hover { background-color: var(--surface-tertiary); } .btn-delete { background-color: rgba(255, 76, 76, 0.1); color: #ff4c4c; } .btn-delete:hover { background-color: rgba(255, 76, 76, 0.2); } .dialog-checkbox { cursor: pointer; } .chat-action-status { position: fixed; top: 20px; right: 20px; padding: 12px 16px; background: var(--surface-primary); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 2000; display: flex; align-items: center; font-size: 14px; } .status-icon { margin-right: 8px; font-size: 18px; } .status-success { color: #4caf50; } .status-error { color: #f44336; } .status-loading { color: #2196f3; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-spinner { animation: spin 1s linear infinite; display: inline-block; } .select-count { font-size: 12px; color: var(--text-secondary); text-align: center; margin-top: 4px; display: block; } `; document.head.appendChild(styleEl); } showStatus(message, type = 'loading') { document.querySelector('.chat-action-status')?.remove(); const statusEl = document.createElement('div'); statusEl.className = 'chat-action-status'; let icon = ''; if (type === 'loading') icon = '<span class="status-icon status-loading"><span class="loading-spinner">⟳</span></span>'; else if (type === 'success') icon = '<span class="status-icon status-success">✓</span>'; else if (type === 'error') icon = '<span class="status-icon status-error">✕</span>'; statusEl.innerHTML = `${icon}${message}`; document.body.appendChild(statusEl); if (type !== 'loading') setTimeout(() => statusEl.remove(), 3000); return statusEl; } createCheckbox(chatItem) { if (chatItem.querySelector('.dialog-checkbox-container')) return; const checkboxContainer = document.createElement('div'); checkboxContainer.className = 'dialog-checkbox-container flex items-center pr-2'; checkboxContainer.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const checkbox = e.currentTarget.querySelector('.dialog-checkbox'); if (checkbox) { checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change', { bubbles: true })); } }); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'dialog-checkbox h-4 w-4 rounded'; checkbox.addEventListener('change', () => this.updateSelectedCount()); checkboxContainer.appendChild(checkbox); chatItem.prepend(checkboxContainer); } updateSelectedCount() { const selectedCount = document.querySelectorAll('.dialog-checkbox:checked').length; const countElement = document.querySelector('.selected-count'); if (countElement) countElement.textContent = selectedCount > 0 ? `${selectedCount} selecionado${selectedCount > 1 ? 's' : ''}` : 'Nenhum selecionado'; } setupControlPanel() { const upgradeButtonText = PLATFORM.upgradeButtonText; let upgradeButton = null; // A busca pelo botão de upgrade precisa ser mais flexível const menuItems = document.querySelectorAll('.__menu-item'); for (const item of menuItems) { if (item.textContent.includes(upgradeButtonText)) { upgradeButton = item; break; } } if (!upgradeButton) { return; } if (document.querySelector('.mass-actions')) { this.updateSelectedCount(); return; } const controls = this.createControlsElement(); upgradeButton.parentNode.replaceChild(controls, upgradeButton); this.updateSelectedCount(); } createControlsElement() { const controls = document.createElement('div'); controls.className = 'mass-actions'; controls.innerHTML = ` <div class="mass-actions-title">Gerenciamento em Massa</div> <button class="mass-actions-btn btn-select-all">Selecionar Tudo</button> <button class="mass-actions-btn btn-select-without-emoji">Sel. sem ${PRIORITY_EMOJI}</button> <button class="mass-actions-btn btn-deselect-all">Desmarcar Tudo</button> <hr class="my-2 border-token-border-light"> <button class="mass-actions-btn btn-archive">Arquivar Selecionados</button> <button class="mass-actions-btn btn-delete">Excluir Selecionados</button> <span class="selected-count"></span>`; this.setupButtonHandlers(controls); return controls; } setupButtonHandlers(controls) { const handlers = { '.btn-select-all': () => this.toggleAllCheckboxes(true), '.btn-select-without-emoji': () => window.chatManager.selectChatsWithoutPriorityEmoji(), '.btn-deselect-all': () => this.toggleAllCheckboxes(false), '.btn-archive': () => { if (confirm('Deseja arquivar as conversas selecionadas?')) window.chatManager.updateChats({ is_archived: true }); }, '.btn-delete': () => { if (confirm('Deseja excluir permanentemente as conversas selecionadas?')) window.chatManager.updateChats({ is_visible: false }); } }; for (const [selector, handler] of Object.entries(handlers)) { controls.querySelector(selector).addEventListener('click', handler); } } toggleAllCheckboxes(state) { document.querySelectorAll(SELECTOR.chatItems).forEach(item => { const cb = item.querySelector('.dialog-checkbox'); if (cb) cb.checked = state; }); this.updateSelectedCount(); } } // --- CLASSE CHATMANAGER CORRIGIDA --- class ChatManager { constructor(uiManager) { this.ui = uiManager; } async getAccessToken() { try { const response = await fetch(`${API_BASE}${PLATFORM.api.tokenEndpoint}`); if (!response.ok) throw new Error(`A resposta da rede não foi OK: ${response.statusText}`); const data = await response.json(); return PLATFORM.api.tokenExtractor(data); } catch (error) { console.error('Erro ao obter token:', error); this.ui.showStatus(`Erro de token: ${error.message}`, 'error'); return null; } } getChatId(element) { const chatLink = element.closest(SELECTOR.chatLink); return chatLink ? new URL(chatLink.href).pathname.split('/').pop() : null; } hasPriorityEmoji(chatItem) { const titleDiv = chatItem.querySelector(SELECTOR.chatTitle); return titleDiv && titleDiv.textContent.includes(PRIORITY_EMOJI); } selectChatsWithoutPriorityEmoji() { document.querySelectorAll(SELECTOR.chatItems).forEach(item => { const checkbox = item.querySelector('.dialog-checkbox'); if (checkbox) checkbox.checked = !this.hasPriorityEmoji(item); }); this.ui.updateSelectedCount(); } async updateChats(body) { const checkedItems = Array.from(document.querySelectorAll('.dialog-checkbox:checked')); if (checkedItems.length === 0) { this.ui.showStatus('Nenhuma conversa selecionada', 'error'); return; } const action = body.is_archived ? 'arquivando' : 'excluindo'; const statusEl = this.ui.showStatus(`${action} ${checkedItems.length} conversas...`); const accessToken = await this.getAccessToken(); if (!accessToken) return; const promises = checkedItems.map(async (checkbox) => { const chatItem = checkbox.closest(SELECTOR.chatItems); const chatId = this.getChatId(chatItem); if (!chatId) return Promise.reject('Chat ID não encontrado'); const response = await fetch(`${API_BASE}${PLATFORM.api.conversationEndpoint}${chatId}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!response.ok) return Promise.reject(`HTTP ${response.status}`); if (chatItem) chatItem.style.opacity = '0.5'; return Promise.resolve(); }); const results = await Promise.allSettled(promises); const processed = results.filter(r => r.status === 'fulfilled').length; if (processed > 0) { this.ui.showStatus(`${processed} conversas ${body.is_archived ? 'arquivadas' : 'excluídas'} com sucesso!`, 'success'); setTimeout(() => window.location.reload(), 1500); } else { this.ui.showStatus(`Erro ao processar conversas.`, 'error'); } } } class ChatManagerApp { constructor() { this.uiManager = new UIManager(); this.chatManager = new ChatManager(this.uiManager); window.chatManager = this.chatManager; } run() { this.uiManager.setupControlPanel(); document.querySelectorAll(SELECTOR.chatItems).forEach(item => { this.uiManager.createCheckbox(item); }); } setupObserver() { const observer = new MutationObserver(() => { if (this.debounceTimeout) clearTimeout(this.debounceTimeout); this.debounceTimeout = setTimeout(() => { this.run(); }, 500); }); observer.observe(document.body, { childList: true, subtree: true }); } } function waitForElement(selector, callback) { const interval = setInterval(() => { if (document.querySelector(selector)) { clearInterval(interval); callback(); } }, 500); } waitForElement(SELECTOR.entryPoint, () => { const app = new ChatManagerApp(); app.run(); app.setupObserver(); }); })();