捕获 Token,批量删除 ChatGPT 对话,支持分页获取(修复100条限制)、分组全选;新增每条会话“跳转”按钮,优先同页路由跳转,失败则新标签打开;导出功能已停用
当前为
// ==UserScript==
// @name ChatGPT-批量对话删除器(修复版+跳转按钮 v1.7)
// @namespace https://chatgpt.com/
// @version 1.7
// @description 捕获 Token,批量删除 ChatGPT 对话,支持分页获取(修复100条限制)、分组全选;新增每条会话“跳转”按钮,优先同页路由跳转,失败则新标签打开;导出功能已停用
// @author Your Name
// @match https://chatgpt.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
let authToken = null;
let allConversations = [];
let isFirstClick = true;
// =========================
// 新增:通用前置样式(z-index 超大,保证前台)
// =========================
const GLOBAL_Z = 2147483647;
// --- 拦截 fetch 以捕获 Authorization Token ---
const originalFetch = window.fetch;
window.fetch = async function (...args) {
const response = await originalFetch.apply(this, args);
const url = args[0];
if (typeof url === 'string' && url.includes('chatgpt.com/backend-api')) {
try {
const requestHeaders = args[1]?.headers || {};
if (requestHeaders.Authorization || requestHeaders.authorization) {
const token = requestHeaders.Authorization || requestHeaders.authorization;
if (token && token !== authToken) {
authToken = token;
console.log('🎯 抓到 token: 成功 (来自: ' + url + ')');
}
}
} catch (err) {
console.warn('解析 fetch 请求头时出错:', err);
}
}
return response;
};
function getAuthHeaders() {
return {
'Content-Type': 'application/json',
Authorization: authToken || '',
};
}
function showUserTip(message, duration = 5000) {
const tipBox = document.createElement('div');
tipBox.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 25px;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
z-index: ${GLOBAL_Z};
font-size: 14px;
font-weight: 500;
animation: slideDown 0.3s ease;
max-width: 400px;
text-align: center;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes slideDown {
from { transform: translateX(-50%) translateY(-20px); opacity: 0; }
to { transform: translateX(-50%) translateY(0); opacity: 1; }
}
`;
document.head.appendChild(style);
const closeBtn = document.createElement('span');
closeBtn.textContent = '✕';
closeBtn.style.cssText = `
position: absolute;
top: 5px;
right: 10px;
cursor: pointer;
font-size: 18px;
opacity: 0.8;
`;
closeBtn.onclick = () => tipBox.remove();
tipBox.innerHTML = message;
tipBox.appendChild(closeBtn);
document.body.appendChild(tipBox);
setTimeout(() => {
if (tipBox.parentNode) {
tipBox.style.animation = 'slideDown 0.3s ease reverse';
setTimeout(() => tipBox.remove(), 300);
}
}, duration);
}
// --- 获取对话列表(分页)---
async function fetchConversations() {
console.log('📡 开始获取对话列表...');
const limit = 50;
let offset = 0;
let allConvs = [];
const maxIterations = 100;
let iteration = 0;
while (iteration < maxIterations) {
console.log(`⏳ 正在获取对话: offset=${offset}, limit=${limit}`);
if (document.getElementById('loading-progress')) {
document.getElementById('loading-progress').textContent = `加载中... 已获取 ${allConvs.length} 条对话`;
}
try {
const response = await fetch(
`/backend-api/conversations?offset=${offset}&limit=${limit}&order=updated`,
{
method: 'GET',
headers: getAuthHeaders(),
credentials: 'include',
}
);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
const items = data.items || [];
if (items.length === 0) {
console.log('✅ 没有更多对话了');
break;
}
allConvs = allConvs.concat(items);
console.log(`📦 本批次获取 ${items.length} 条,累计 ${allConvs.length} 条`);
if (items.length < limit) {
console.log('✅ 获取完成(返回数量少于请求数量)');
break;
}
offset += limit;
iteration++;
await new Promise((resolve) => setTimeout(resolve, 300));
} catch (error) {
console.error('❌ 获取对话时出错:', error);
break;
}
}
console.log(`📊 最终获取 ${allConvs.length} 条对话`);
return allConvs;
}
// =========================
// 路由跳转辅助(优先 SPA;失败回退新标签)+ 滚动位置保存-恢复
// =========================
function trySpaNavigate(pathname) {
try {
// 1) 常见 Next.js Router 探测
const candidate =
window.next?.router ||
window.__NEXT_ROUTER__ ||
window.__NUXT__?.$router;
if (candidate && typeof candidate.push === 'function') {
candidate.push(pathname);
return true;
}
// 2) History API 回退(部分 SPA 会监听)
history.pushState({}, '', pathname);
window.dispatchEvent(new Event('pushstate'));
window.dispatchEvent(new PopStateEvent('popstate'));
return true;
} catch (e) {
console.debug('SPA 内部跳转失败:', e);
return false;
}
}
// ★ 新增:多帧重试恢复滚动(防止路由渲染异步导致首次设置失效)
function restoreScrollWithRetry(targetElGetter, savedScrollTop, maxTries = 20) {
let tries = 0;
function tick() {
const el = targetElGetter();
if (el) {
el.scrollTop = savedScrollTop;
// 若已成功恢复或滚动位置接近目标,直接结束
if (Math.abs(el.scrollTop - savedScrollTop) < 2) return;
}
if (++tries < maxTries) {
requestAnimationFrame(tick);
}
}
requestAnimationFrame(tick);
}
function openConversation(convId, forceNewTab = false) {
const path = `/c/${convId}`;
// 在跳转前记录滚动位置(若找不到容器则记 0)
const panel = document.getElementById('chatgpt-cleaner-container');
const contentEl = document.getElementById('chatgpt-cleaner-content');
const savedScrollTop = contentEl ? contentEl.scrollTop : 0;
// “强制新标签”用于中键/Ctrl+Click
if (forceNewTab) {
window.open(path, '_blank');
return;
}
// 优先尝试同页 SPA 跳转
const ok = trySpaNavigate(path);
if (!ok) {
// 回退:新标签页保证可用
window.open(path, '_blank');
return;
}
// ★ 保持面板最前,但不再 append(避免潜在滚动重置)
if (panel) {
panel.style.zIndex = String(GLOBAL_Z);
}
// ★ 路由切换后,多帧重试恢复到跳转前的滚动位置
restoreScrollWithRetry(
() => document.getElementById('chatgpt-cleaner-content'),
savedScrollTop
);
}
function renderUI(conversations) {
console.log('🎉 会话总数:', conversations.length);
const existingContainer = document.getElementById('chatgpt-cleaner-container');
if (existingContainer) existingContainer.remove();
const container = document.createElement('div');
container.id = 'chatgpt-cleaner-container';
container.style.cssText = `
position: fixed;
top: 50px;
right: 20px;
width: 450px;
max-height: 80vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0,0,0,0.3);
z-index: ${GLOBAL_Z};
display: flex;
flex-direction: column;
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;
`;
// 标题栏
const header = document.createElement('div');
header.style.cssText = `
padding: 20px;
color: white;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid rgba(255,255,255,0.2);
display: flex;
justify-content: space-between;
align-items: center;
`;
header.innerHTML = `
<span>ChatGPT 履历管理(共 ${conversations.length} 条)</span>
<button id="close-cleaner" style="
background: none; border: none; color: white; font-size: 24px; cursor: pointer;
padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
border-radius: 50%; transition: background-color 0.3s;
" onmouseover="this.style.backgroundColor='rgba(255,255,255,0.2)'"
onmouseout="this.style.backgroundColor='transparent'">×</button>
`;
container.appendChild(header);
// 内容区域
const content = document.createElement('div');
content.id = 'chatgpt-cleaner-content'; // ★ 新增:用于获取与恢复滚动
content.style.cssText = `
flex: 1;
overflow-y: auto;
background: white;
padding: 20px;
overscroll-behavior: contain; /* ★ 新增:防止外层滚动联动 */
`;
// 分组
const grouped = {};
conversations.forEach((conv) => {
const category = getTimeCategory(conv.update_time);
if (!grouped[category]) grouped[category] = [];
grouped[category].push(conv);
});
const categoryOrder = ['今天', '昨天', '7天内', '30天内', '更早'];
categoryOrder.forEach((category) => {
if (!grouped[category] || grouped[category].length === 0) return;
const groupDiv = document.createElement('div');
groupDiv.style.cssText = `margin-bottom: 25px;`;
const groupHeader = document.createElement('div');
groupHeader.style.cssText = `
display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;
`;
const groupTitle = document.createElement('h3');
groupTitle.style.cssText = `color:#4a5568;font-size:14px;font-weight:600;margin:0;`;
groupTitle.textContent = `${category} (${grouped[category].length})`;
const selectAllBtn = document.createElement('button');
selectAllBtn.className = `group-select-${category.replace(/\s+/g, '-')}`;
selectAllBtn.style.cssText = `
background:#4299e1;color:white;border:none;padding:4px 12px;border-radius:4px;
font-size:12px;cursor:pointer;transition:all 0.3s;
`;
selectAllBtn.textContent = '全选';
const updateGroupSelectBtn = () => {
const groupCheckboxes = groupDiv.querySelectorAll('input[type="checkbox"]');
const checkedCount = Array.from(groupCheckboxes).filter((cb) => cb.checked).length;
const totalCount = groupCheckboxes.length;
if (checkedCount === 0) {
selectAllBtn.textContent = '全选';
selectAllBtn.style.background = '#4299e1';
} else if (checkedCount === totalCount) {
selectAllBtn.textContent = '取消';
selectAllBtn.style.background = '#e53e3e';
} else {
selectAllBtn.textContent = '部分';
selectAllBtn.style.background = '#ed8936';
}
};
selectAllBtn.onclick = () => {
const groupCheckboxes = groupDiv.querySelectorAll('input[type="checkbox"]');
const checkedCount = Array.from(groupCheckboxes).filter((cb) => cb.checked).length;
const shouldCheck = checkedCount < groupCheckboxes.length;
groupCheckboxes.forEach((checkbox) => (checkbox.checked = shouldCheck));
updateGroupSelectBtn();
updateGlobalSelectAll();
};
groupHeader.appendChild(groupTitle);
groupHeader.appendChild(selectAllBtn);
groupDiv.appendChild(groupHeader);
// 列表
grouped[category].forEach((conv) => {
const item = document.createElement('div');
item.className = 'conversation-item';
item.style.cssText = `
padding: 12px; margin-bottom: 8px; background:#f7fafc; border-radius: 8px;
display:flex; align-items:center; transition: all 0.2s; cursor: pointer;
`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = conv.id;
checkbox.style.cssText = `margin-right:12px;width:18px;height:18px;cursor:pointer;`;
checkbox.onchange = () => {
updateGroupSelectBtn();
updateGlobalSelectAll();
};
const label = document.createElement('div');
label.style.cssText = `flex:1; cursor:pointer; display:flex; flex-direction:column;`;
const title = document.createElement('span');
title.style.cssText = `color:#2d3748;font-size:14px;margin-bottom:4px;`;
title.textContent = conv.title || '无标题对话';
const time = document.createElement('span');
time.style.cssText = `color:#718096;font-size:12px;`;
time.textContent = new Date(conv.update_time).toLocaleString();
label.appendChild(title);
label.appendChild(time);
// =========================
// 新增:「跳转」按钮(核心)
// =========================
const jumpBtn = document.createElement('button');
jumpBtn.textContent = '跳转';
jumpBtn.title = '打开该对话(优先同页跳转,失败则新标签)';
jumpBtn.style.cssText = `
margin-left: 8px; background:#805ad5; color:white; border:none; padding:6px 10px;
border-radius:6px; cursor:pointer; font-size:12px; transition:all 0.2s; flex-shrink:0;
`;
jumpBtn.onmouseover = () => (jumpBtn.style.background = '#6b46c1');
jumpBtn.onmouseout = () => (jumpBtn.style.background = '#805ad5');
// 防止点击“跳转”影响复选框/选中态
jumpBtn.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
// Ctrl/Meta 或 中键 => 强制新标签
const forceNew =
evt.ctrlKey || evt.metaKey || evt.button === 1 /* middle button */;
openConversation(conv.id, forceNew);
});
// 支持中键直接新标签
jumpBtn.addEventListener('auxclick', (evt) => {
if (evt.button === 1) {
evt.preventDefault();
evt.stopPropagation();
openConversation(conv.id, true);
}
});
item.appendChild(checkbox);
item.appendChild(label);
item.appendChild(jumpBtn);
// 行 hover 效果
item.onmouseover = () => {
item.style.background = '#e6fffa';
item.style.transform = 'translateX(-2px)';
};
item.onmouseout = () => {
item.style.background = '#f7fafc';
item.style.transform = 'translateX(0)';
};
// 点击整行 = 切换复选框
item.onclick = (e) => {
// 如果点的是按钮,则已 stopPropagation,这里不执行
checkbox.checked = !checkbox.checked;
updateGroupSelectBtn();
updateGlobalSelectAll();
};
groupDiv.appendChild(item);
});
content.appendChild(groupDiv);
});
container.appendChild(content);
// 底部操作栏
const footer = document.createElement('div');
footer.style.cssText = `
padding: 15px 20px; background:white; border-top:1px solid #e2e8f0;
display:flex; justify-content:space-between; align-items:center;
`;
const selectAllBtn = document.createElement('button');
selectAllBtn.id = 'select-all-btn';
selectAllBtn.style.cssText = `
background:#4299e1;color:white;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;
font-size:14px;font-weight:500;transition:all 0.3s;
`;
selectAllBtn.textContent = '全选所有';
selectAllBtn.onclick = () => {
const allCheckboxes = content.querySelectorAll('input[type="checkbox"]');
const checkedCount = Array.from(allCheckboxes).filter((cb) => cb.checked).length;
const shouldCheck = checkedCount < allCheckboxes.length;
allCheckboxes.forEach((checkbox) => (checkbox.checked = shouldCheck));
['今天', '昨天', '7天内', '30天内', '更早'].forEach((category) => {
const groupBtn = content.querySelector(`.group-select-${category.replace(/\s+/g, '-')}`);
if (groupBtn) {
groupBtn.textContent = shouldCheck ? '取消' : '全选';
groupBtn.style.background = shouldCheck ? '#e53e3e' : '#4299e1';
}
});
updateGlobalSelectAll();
};
function updateGlobalSelectAll() {
const allCheckboxes = content.querySelectorAll('input[type="checkbox"]');
const checkedCount = Array.from(allCheckboxes).filter((cb) => cb.checked).length;
const totalCount = allCheckboxes.length;
if (checkedCount === 0) {
selectAllBtn.textContent = '全选所有';
selectAllBtn.style.background = '#4299e1';
} else if (checkedCount === totalCount) {
selectAllBtn.textContent = '取消全选';
selectAllBtn.style.background = '#e53e3e';
} else {
selectAllBtn.textContent = `已选 ${checkedCount}/${totalCount}`;
selectAllBtn.style.background = '#ed8936';
}
}
const exportBtn = document.createElement('button');
exportBtn.style.cssText = `
background:#cbd5e0;color:#a0aec0;border:none;padding:8px 16px;border-radius:6px;
cursor:not-allowed;font-size:14px;font-weight:500;transition:all 0.3s;position:relative;
`;
exportBtn.textContent = '导出(暂停)';
exportBtn.disabled = true;
exportBtn.title = '导出功能暂时停止开放';
const deleteBtn = document.createElement('button');
deleteBtn.style.cssText = `
background:#e53e3e;color:white;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;
font-size:14px;font-weight:500;transition:all 0.3s;
`;
deleteBtn.textContent = '删除选中';
deleteBtn.onmouseover = () => (deleteBtn.style.background = '#c53030');
deleteBtn.onmouseout = () => (deleteBtn.style.background = '#e53e3e');
deleteBtn.onclick = async () => {
const checkboxes = content.querySelectorAll('input[type="checkbox"]:checked');
if (checkboxes.length === 0) {
alert('请先选择要删除的对话');
return;
}
if (!confirm(`确定要删除选中的 ${checkboxes.length} 条对话吗?`)) return;
let successCount = 0;
let failCount = 0;
for (let i = 0; i < checkboxes.length; i++) {
const checkbox = checkboxes[i];
const convId = checkbox.value;
try {
await deleteSingleConversation(convId);
successCount++;
const row = checkbox.closest('.conversation-item');
if (row) {
row.style.opacity = '0.5';
row.style.textDecoration = 'line-through';
}
} catch (error) {
console.error(`删除对话 ${convId} 失败:`, error);
failCount++;
}
deleteBtn.textContent = `删除中... (${i + 1}/${checkboxes.length})`;
await new Promise((resolve) => setTimeout(resolve, 300));
}
alert(`删除完成!\n成功: ${successCount} 条\n失败: ${failCount} 条`);
const remainingConvs = allConversations.filter(
(c) => !Array.from(checkboxes).some((cb) => cb.value === c.id)
);
allConversations = remainingConvs;
renderUI(remainingConvs);
};
footer.appendChild(selectAllBtn);
footer.appendChild(exportBtn);
footer.appendChild(deleteBtn);
container.appendChild(footer);
document.body.appendChild(container);
document.getElementById('close-cleaner').onclick = () => container.remove();
console.log('✅ UI 渲染完成');
}
function getTimeCategory(dateStr) {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now - date;
const oneDay = 24 * 60 * 60 * 1000;
if (date.toDateString() === now.toDateString()) return '今天';
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) return '昨天';
if (diffMs <= 7 * oneDay) return '7天内';
if (diffMs <= 30 * oneDay) return '30天内';
return '更早';
}
async function main() {
console.log('🚀 ChatGPT 清理器启动...');
if (!authToken) {
showUserTip('❌ 未能获取 Authorization Token,请刷新页面重试');
return;
}
const loadingDiv = document.createElement('div');
loadingDiv.id = 'loading-indicator';
loadingDiv.style.cssText = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background:white;
padding:30px; border-radius:12px; box-shadow:0 10px 25px rgba(0,0,0,0.2); z-index:${GLOBAL_Z}; text-align:center;
`;
loadingDiv.innerHTML = `
<div style="font-size:16px;color:#4a5568;margin-bottom:10px;">正在加载对话列表...</div>
<div id="loading-progress" style="font-size:14px;color:#718096;">准备中...</div>
<div style="margin-top:20px;">
<div style="width:200px;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;">
<div style="width:100%;height:100%;background:linear-gradient(90deg,#667eea 0%,#764ba2 100%);
animation: loading 1.5s ease-in-out infinite;"></div>
</div>
</div>
<style>
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
</style>
`;
document.body.appendChild(loadingDiv);
try {
allConversations = await fetchConversations();
renderUI(allConversations);
} catch (error) {
console.error('❌ 错误:', error);
showUserTip('❌ 获取对话列表失败,请检查控制台');
} finally {
loadingDiv.remove();
}
}
function createToggleButton() {
const toggleButton = document.createElement('button');
toggleButton.id = 'chatgpt-cleaner-toggle';
toggleButton.style.cssText = `
position: fixed; bottom: 30px; right: 30px; width: 60px; height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border: none; border-radius: 50%; cursor: pointer;
box-shadow: 0 4px 15px rgba(102,126,234,0.4);
z-index: ${GLOBAL_Z - 1};
font-size: 12px; font-weight: bold; transition: all 0.3s; display:flex; align-items:center; justify-content:center; text-align:center; line-height:1.2;
`;
toggleButton.textContent = '履历\n删除';
toggleButton.title = '打开对话履历管理器';
toggleButton.onmouseover = () => {
toggleButton.style.transform = 'scale(1.1)';
toggleButton.style.boxShadow = '0 6px 20px rgba(102,126,234,0.5)';
};
toggleButton.onmouseout = () => {
toggleButton.style.transform = 'scale(1)';
toggleButton.style.boxShadow = '0 4px 15px rgba(102,126,234,0.4)';
};
toggleButton.onclick = async () => {
console.log('🖱️ 切换按钮点击:' + (isFirstClick ? '首次加载数据...' : '切换显示/隐藏'));
if (!authToken) {
showUserTip('💡 请先向 ChatGPT 发送一条消息,以激活对话管理功能');
return;
}
const existingContainer = document.getElementById('chatgpt-cleaner-container');
if (existingContainer) {
existingContainer.remove();
} else {
if (isFirstClick || allConversations.length === 0) {
await main();
isFirstClick = false;
} else {
renderUI(allConversations);
}
}
};
document.body.appendChild(toggleButton);
console.log('🔌 对话管理切换按钮已创建。');
}
async function deleteSingleConversation(conversationId) {
if (!conversationId) throw new Error('无效的 conversation ID');
console.log(`⏳ 准备删除对话: ${conversationId}`);
const headers = getAuthHeaders();
if (!headers.Authorization) throw new Error('无法获取 Authorization token');
const url = `/backend-api/conversation/${conversationId}`;
const body = JSON.stringify({ is_visible: false });
try {
const response = await fetch(url, {
method: 'PATCH',
headers: headers,
body: body,
credentials: 'include',
});
if (!response.ok) {
const errorData = await response.text();
console.error(`删除 ${conversationId} 请求失败: ${response.status} ${response.statusText}`, errorData);
throw new Error(`API 请求失败: ${response.status} ${response.statusText}`);
}
const result = await response.json();
console.log(`✅ 对话 ${conversationId} 删除成功:`, result);
return result;
} catch (error) {
console.error(`❌ 执行删除对话 ${conversationId} 的 fetch 时出错:`, error);
throw error;
}
}
console.log('ChatGPT Cleaner 脚本已注入,等待 API 请求以捕获 Token...');
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createToggleButton);
} else {
createToggleButton();
}
})();