Gerenciador de chats ChatGPT

Gerenciamento em massa de conversas em plataformas de IA

  1. // ==UserScript==
  2. // @name Gerenciador de chats ChatGPT
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Gerenciamento em massa de conversas em plataformas de IA
  6. // @author luascfl
  7. // @match https://chat.openai.com/*
  8. // @match https://chatgpt.com/*
  9. // @icon https://cdn-icons-png.flaticon.com/512/16459/16459818.png
  10. // @home https://github.com/luascfl/manage-chats-chatgpt
  11. // @supportURL https://github.com/luascfl/manage-chats-chatgpt/issues
  12. // @license MIT
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. /**
  20. * Configuração específica para cada plataforma
  21. * Facilita a adaptação para outras plataformas de IA no futuro
  22. */
  23. const PLATFORMS = {
  24. 'chat.openai.com': {
  25. name: 'ChatGPT',
  26. selectors: {
  27. chatList: '.flex.flex-col.gap-2.text-token-text-primary',
  28. chatItems: 'ol > li',
  29. chatLink: 'a[href^="/c/"]',
  30. chatTitle: 'div[dir="auto"]'
  31. },
  32. api: {
  33. base: window.location.origin,
  34. tokenEndpoint: '/api/auth/session',
  35. conversationEndpoint: '/backend-api/conversation/',
  36. tokenExtractor: (data) => data.accessToken
  37. },
  38. priorityEmoji: '❗'
  39. },
  40. // Modelo para adicionar outras plataformas
  41. /*
  42. 'exemplo.com': {
  43. name: 'Nome da Plataforma',
  44. selectors: {
  45. chatList: '.seletor-lista-chats',
  46. chatItems: '.seletor-item-chat',
  47. chatLink: '.seletor-link-chat',
  48. chatTitle: '.seletor-titulo-chat'
  49. },
  50. api: {
  51. base: 'https://api.exemplo.com',
  52. tokenEndpoint: '/auth/token',
  53. conversationEndpoint: '/api/conversations/',
  54. tokenExtractor: (data) => data.token
  55. },
  56. priorityEmoji: '⭐'
  57. }
  58. */
  59. };
  60.  
  61. // Detecta a plataforma atual
  62. const getCurrentPlatform = () => {
  63. const hostname = window.location.hostname;
  64. return PLATFORMS[hostname] || PLATFORMS['chat.openai.com']; // Padrão para ChatGPT
  65. };
  66.  
  67. const PLATFORM = getCurrentPlatform();
  68. const API_BASE = PLATFORM.api.base;
  69. const SELECTOR = PLATFORM.selectors;
  70. const PRIORITY_EMOJI = PLATFORM.priorityEmoji;
  71.  
  72. /**
  73. * Gerenciador de UI
  74. */
  75. class UIManager {
  76. constructor() {
  77. this.addStyles();
  78. }
  79.  
  80. addStyles() {
  81. const styleEl = document.createElement('style');
  82. styleEl.innerHTML = `
  83. .mass-actions {
  84. background-color: var(--surface-primary);
  85. padding: 10px;
  86. border-radius: 8px;
  87. margin-bottom: 16px;
  88. box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  89. }
  90.  
  91. .mass-actions-title {
  92. font-weight: bold;
  93. margin-bottom: 10px;
  94. font-size: 14px;
  95. color: var(--text-primary);
  96. }
  97.  
  98. .mass-actions-btn {
  99. padding: 6px 12px;
  100. border-radius: 6px;
  101. font-size: 13px;
  102. font-weight: 500;
  103. cursor: pointer;
  104. transition: all 0.2s;
  105. border: 1px solid var(--border-primary);
  106. margin-bottom: 5px;
  107. }
  108.  
  109. .mass-actions-btn:hover {
  110. opacity: 0.9;
  111. }
  112.  
  113. .btn-select-all {
  114. background-color: var(--surface-secondary);
  115. }
  116.  
  117. .btn-deselect-all {
  118. background-color: var(--surface-secondary);
  119. }
  120.  
  121. .btn-select-without-emoji {
  122. background-color: var(--surface-secondary);
  123. }
  124.  
  125. .btn-archive {
  126. background-color: var(--surface-tertiary);
  127. }
  128.  
  129. .btn-delete {
  130. background-color: rgba(255, 76, 76, 0.1);
  131. color: #ff4c4c;
  132. }
  133.  
  134. .checkbox-container {
  135. position: absolute;
  136. left: 8px;
  137. top: 0;
  138. bottom: 0;
  139. display: flex;
  140. align-items: center;
  141. z-index: 10;
  142. }
  143.  
  144. .dialog-checkbox {
  145. cursor: pointer;
  146. width: 16px;
  147. height: 16px;
  148. }
  149.  
  150. .chat-item-container {
  151. position: relative;
  152. }
  153.  
  154. .chat-link-padded {
  155. padding-left: 30px !important;
  156. }
  157.  
  158. .chat-action-status {
  159. position: fixed;
  160. top: 20px;
  161. right: 20px;
  162. padding: 12px 16px;
  163. background: var(--surface-primary);
  164. border-radius: 8px;
  165. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  166. z-index: 1000;
  167. display: flex;
  168. align-items: center;
  169. font-size: 14px;
  170. }
  171.  
  172. .status-icon {
  173. margin-right: 8px;
  174. font-size: 18px;
  175. }
  176.  
  177. .status-success {
  178. color: #4caf50;
  179. }
  180.  
  181. .status-error {
  182. color: #f44336;
  183. }
  184.  
  185. .status-loading {
  186. color: #2196f3;
  187. }
  188.  
  189. @keyframes spin {
  190. 0% { transform: rotate(0deg); }
  191. 100% { transform: rotate(360deg); }
  192. }
  193.  
  194. .loading-spinner {
  195. animation: spin 1s linear infinite;
  196. display: inline-block;
  197. }
  198.  
  199. .select-count {
  200. margin-left: 8px;
  201. font-size: 13px;
  202. color: var(--text-secondary);
  203. }
  204. `;
  205. document.head.appendChild(styleEl);
  206. }
  207.  
  208. showStatus(message, type = 'loading') {
  209. // Remove qualquer status existente
  210. const existingStatus = document.querySelector('.chat-action-status');
  211. if (existingStatus) existingStatus.remove();
  212.  
  213. const statusEl = document.createElement('div');
  214. statusEl.className = 'chat-action-status';
  215.  
  216. let icon = '';
  217. if (type === 'loading') {
  218. icon = '<span class="status-icon status-loading"><span class="loading-spinner">⟳</span></span>';
  219. } else if (type === 'success') {
  220. icon = '<span class="status-icon status-success">✓</span>';
  221. } else if (type === 'error') {
  222. icon = '<span class="status-icon status-error">✕</span>';
  223. }
  224.  
  225. statusEl.innerHTML = `${icon}${message}`;
  226. document.body.appendChild(statusEl);
  227.  
  228. if (type !== 'loading') {
  229. setTimeout(() => {
  230. statusEl.remove();
  231. }, 3000);
  232. }
  233.  
  234. return statusEl;
  235. }
  236.  
  237. updateSelectedCount() {
  238. const selectedCount = document.querySelectorAll('.dialog-checkbox:checked').length;
  239. const countElement = document.querySelector('.selected-count');
  240. if (countElement) {
  241. countElement.textContent = selectedCount > 0 ? `${selectedCount} selecionado${selectedCount > 1 ? 's' : ''}` : '';
  242. }
  243. }
  244.  
  245. createCheckbox(chatItem) {
  246. // Verifica se já existe um checkbox
  247. if (chatItem.querySelector('.checkbox-container')) return;
  248.  
  249. // Adiciona classe ao container do chat
  250. chatItem.classList.add('chat-item-container');
  251.  
  252. // Encontra o link de chat
  253. const chatLink = chatItem.querySelector(SELECTOR.chatLink);
  254. if (!chatLink) return;
  255.  
  256. // Adiciona classe ao link para dar espaço ao checkbox
  257. chatLink.classList.add('chat-link-padded');
  258.  
  259. // Cria container do checkbox
  260. const checkboxContainer = document.createElement('div');
  261. checkboxContainer.className = 'checkbox-container';
  262.  
  263. // Cria o checkbox
  264. const checkbox = document.createElement('input');
  265. checkbox.type = 'checkbox';
  266. checkbox.className = 'dialog-checkbox';
  267. checkbox.addEventListener('change', () => this.updateSelectedCount());
  268. checkbox.addEventListener('click', (e) => {
  269. e.stopPropagation(); // Impede que o clique no checkbox navegue para o chat
  270. });
  271.  
  272. // Adiciona checkbox ao container
  273. checkboxContainer.appendChild(checkbox);
  274.  
  275. // Adiciona o container ao item de chat
  276. chatItem.appendChild(checkboxContainer);
  277. }
  278.  
  279. ensureCorrectCheckboxes() {
  280. document.querySelectorAll(SELECTOR.chatItems).forEach(chatItem => {
  281. // Remove qualquer checkbox antigo que possa estar dentro do link
  282. const oldCheckbox = chatItem.querySelector('a .dialog-checkbox');
  283. if (oldCheckbox) {
  284. oldCheckbox.remove();
  285. }
  286.  
  287. // Cria um novo checkbox corretamente posicionado
  288. this.createCheckbox(chatItem);
  289. });
  290. }
  291.  
  292. setupControlPanel() {
  293. const chatList = document.querySelector(SELECTOR.chatList);
  294. if (!chatList || chatList.querySelector('.mass-actions')) return;
  295.  
  296. const controls = document.createElement('div');
  297. controls.className = 'mass-actions';
  298. controls.innerHTML = `
  299. <div class="mass-actions-title">Gerenciamento em massa</div>
  300. <div class="flex gap-2 flex-wrap">
  301. <button class="mass-actions-btn btn-select-all">Selecionar tudo</button>
  302. <button class="mass-actions-btn btn-select-without-emoji">Selecionar sem ${PRIORITY_EMOJI}</button>
  303. <button class="mass-actions-btn btn-deselect-all">Desmarcar tudo</button>
  304. <button class="mass-actions-btn btn-archive">Arquivar selecionados</button>
  305. <button class="mass-actions-btn btn-delete">Excluir selecionados</button>
  306. <span class="selected-count select-count"></span>
  307. </div>
  308. `;
  309.  
  310. // Configura os eventos dos botões
  311. this.setupButtonHandlers(controls);
  312.  
  313. chatList.prepend(controls);
  314.  
  315. // Adiciona checkboxes a todos os itens de chat existentes
  316. document.querySelectorAll(SELECTOR.chatItems).forEach(chatItem => this.createCheckbox(chatItem));
  317. this.updateSelectedCount();
  318. }
  319.  
  320. setupButtonHandlers(controls) {
  321. controls.querySelector('.btn-select-all').addEventListener('click', () => {
  322. document.querySelectorAll('.dialog-checkbox').forEach(cb => cb.checked = true);
  323. this.updateSelectedCount();
  324. });
  325.  
  326. controls.querySelector('.btn-select-without-emoji').addEventListener('click', () => {
  327. chatManager.selectChatsWithoutPriorityEmoji();
  328. this.updateSelectedCount();
  329. });
  330.  
  331. controls.querySelector('.btn-deselect-all').addEventListener('click', () => {
  332. document.querySelectorAll('.dialog-checkbox').forEach(cb => cb.checked = false);
  333. this.updateSelectedCount();
  334. });
  335.  
  336. controls.querySelector('.btn-archive').addEventListener('click', () => {
  337. if (confirm('Deseja arquivar todas as conversas selecionadas?')) {
  338. chatManager.updateChats({ is_archived: true });
  339. }
  340. });
  341.  
  342. controls.querySelector('.btn-delete').addEventListener('click', () => {
  343. if (confirm('Deseja excluir todas as conversas selecionadas? Esta ação não pode ser desfeita.')) {
  344. chatManager.updateChats({ is_visible: false });
  345. }
  346. });
  347. }
  348. }
  349.  
  350. /**
  351. * Gerenciador de Chats
  352. */
  353. class ChatManager {
  354. constructor(uiManager) {
  355. this.ui = uiManager;
  356. }
  357.  
  358. async getAccessToken() {
  359. try {
  360. const response = await fetch(`${API_BASE}${PLATFORM.api.tokenEndpoint}`);
  361. const data = await response.json();
  362. return PLATFORM.api.tokenExtractor(data);
  363. } catch (error) {
  364. console.error('Erro ao obter token:', error);
  365. return null;
  366. }
  367. }
  368.  
  369. getChatId(element) {
  370. const chatItem = element.closest('li');
  371. const link = chatItem.querySelector(SELECTOR.chatLink);
  372. return link ? new URL(link.href).pathname.split('/').pop() : null;
  373. }
  374.  
  375. hasPriorityEmoji(chatItem) {
  376. const link = chatItem.querySelector(SELECTOR.chatLink);
  377. if (!link) return false;
  378.  
  379. const titleDiv = link.querySelector(SELECTOR.chatTitle);
  380. return titleDiv && titleDiv.textContent.includes(PRIORITY_EMOJI);
  381. }
  382.  
  383. selectChatsWithoutPriorityEmoji() {
  384. const chatItems = document.querySelectorAll(SELECTOR.chatItems);
  385.  
  386. chatItems.forEach(chatItem => {
  387. const checkbox = chatItem.querySelector('.dialog-checkbox');
  388. if (checkbox) {
  389. // Marca o checkbox apenas se NÃO tiver o emoji de prioridade
  390. checkbox.checked = !this.hasPriorityEmoji(chatItem);
  391. }
  392. });
  393. }
  394.  
  395. async updateChats(body) {
  396. const checkboxes = document.querySelectorAll('.dialog-checkbox:checked');
  397. if (checkboxes.length === 0) {
  398. this.ui.showStatus('Nenhuma conversa selecionada', 'error');
  399. return;
  400. }
  401.  
  402. const action = body.is_archived ? 'arquivando' : 'excluindo';
  403. const statusEl = this.ui.showStatus(`${action.charAt(0).toUpperCase() + action.slice(1)} ${checkboxes.length} conversas...`);
  404.  
  405. const accessToken = await this.getAccessToken();
  406. if (!accessToken) {
  407. this.ui.showStatus('Token de acesso não encontrado', 'error');
  408. return;
  409. }
  410.  
  411. try {
  412. let processed = 0;
  413. await Promise.all(Array.from(checkboxes).map(async (checkbox) => {
  414. const chatId = this.getChatId(checkbox);
  415. if (!chatId) return;
  416.  
  417. const response = await fetch(`${API_BASE}${PLATFORM.api.conversationEndpoint}${chatId}`, {
  418. method: 'PATCH',
  419. headers: {
  420. Authorization: `Bearer ${accessToken}`,
  421. 'Content-Type': 'application/json'
  422. },
  423. body: JSON.stringify(body)
  424. });
  425.  
  426. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  427. checkbox.closest('li').style.opacity = '0.5';
  428. processed++;
  429.  
  430. // Atualizar status com progresso
  431. statusEl.innerHTML = `<span class="status-icon status-loading"><span class="loading-spinner">⟳</span></span>${action.charAt(0).toUpperCase() + action.slice(1)} conversas... (${processed}/${checkboxes.length})`;
  432. }));
  433.  
  434. this.ui.showStatus(`${processed} conversas ${body.is_archived ? 'arquivadas' : 'excluídas'} com sucesso!`, 'success');
  435.  
  436. // Recarregar a página após um breve atraso para mostrar o status
  437. setTimeout(() => {
  438. window.location.reload();
  439. }, 1500);
  440. } catch (error) {
  441. console.error('Erro ao processar conversas:', error);
  442. this.ui.showStatus(`Erro ao processar conversas: ${error.message}`, 'error');
  443. }
  444. }
  445. }
  446.  
  447. /**
  448. * Classe principal que orquestra tudo
  449. */
  450. class ChatManagerApp {
  451. constructor() {
  452. this.uiManager = new UIManager();
  453. this.chatManager = new ChatManager(this.uiManager);
  454.  
  455. // Expõe o chatManager para uso nos event handlers
  456. window.chatManager = this.chatManager;
  457. }
  458.  
  459. init() {
  460. // Inicialização com delay para garantir que a página carregou completamente
  461. setTimeout(() => {
  462. this.uiManager.setupControlPanel();
  463. this.uiManager.ensureCorrectCheckboxes();
  464. this.setupObserver();
  465. }, 1000);
  466. }
  467.  
  468. setupObserver() {
  469. // Observador para detectar mudanças na lista de chats
  470. const observer = new MutationObserver((mutations) => {
  471. const chatList = document.querySelector(SELECTOR.chatList);
  472. if (chatList) {
  473. this.uiManager.setupControlPanel();
  474. this.uiManager.ensureCorrectCheckboxes();
  475.  
  476. // Adiciona checkboxes a novos itens
  477. mutations.forEach(mutation => {
  478. if (mutation.addedNodes.length) {
  479. mutation.addedNodes.forEach(node => {
  480. if (node.nodeType === 1 && node.matches(SELECTOR.chatItems)) {
  481. this.uiManager.createCheckbox(node);
  482. } else if (node.nodeType === 1) {
  483. node.querySelectorAll(SELECTOR.chatItems).forEach(item =>
  484. this.uiManager.createCheckbox(item)
  485. );
  486. }
  487. });
  488. }
  489. });
  490. }
  491. });
  492.  
  493. observer.observe(document.body, {
  494. childList: true,
  495. subtree: true
  496. });
  497. }
  498. }
  499.  
  500. // Inicializa a aplicação
  501. const app = new ChatManagerApp();
  502. app.init();
  503. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址