// ==UserScript==
// @name ChatGPT 批量对话删除器
// @namespace https://chatgpt.com/
// @version 1.3
// @description 自动分页抽取全部对话,按时间分组,支持选中删除和导出JSON备份,增加加载进度显示。
// @author 猫猫 & AI Assistant
// @match https://chatgpt.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
let cachedToken = null;
let alreadyLoaded = false;
const checkboxRefs = {};
let allConversationsRaw = [];
let progressElement = null; // 用于存储显示进度的 DOM 元素
let panelContainer = null; // 存储面板容器的引用
let toggleButton = null; // 存储切换按钮的引用
function parseDateGroups(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 "更早";
}
const categoryOrder = ["今天", "昨天", "7天内", "30天内", "更早"];
function formatDateLocal(isoStr) {
const d = new Date(isoStr);
return `${d.toLocaleDateString()} ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
}
const realFetch = window.fetch;
window.fetch = async function (...args) {
const [url, options] = args;
if (typeof url === 'string' && url.includes('/backend-api/') && options?.headers?.Authorization && !cachedToken) {
cachedToken = options.headers.Authorization;
console.log(`🎯 抓到 token: ${cachedToken ? '成功' : '失败'} (来自: ${url})`);
// !! 不再自动调用 main !!
// Token 捕获后,可以更新切换按钮的状态或提示用户可以点击了(可选)
if (toggleButton && cachedToken) {
toggleButton.disabled = false;
toggleButton.title = '打开对话管理器';
}
}
return realFetch.apply(this, args);
};
function getCookie(name) {
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
return match ? match[2] : null;
}
function getAuthHeaders() {
const token = cachedToken;
const deviceId = getCookie("oai-did");
if (!token) {
// 如果 token 仍然未获取,可以尝试从 localStorage 或其他地方获取,或者提示用户
console.warn("⚠️ Token 未通过 fetch 拦截获取,尝试其他方法或等待...");
// throw new Error("❌ 无法获取 Authorization token"); // 或者先不抛出错误,看后续是否能拿到
}
if (!deviceId) throw new Error("❌ 缺少 oai-did cookie");
// 返回头部,即使 token 暂时为空,让 fetchAllConversations 内部处理重试或失败
return {
"Authorization": token || '', // 提供空字符串如果未获取到
"OAI-Device-Id": deviceId,
"Content-Type": "application/json"
};
}
// 新增:更新进度的函数
function updateProgressUI(loaded, total) {
if (progressElement) {
progressElement.textContent = `⏳ 正在加载... ${loaded}${total !== null ? ` / ${total}` : ''} 条对话`;
}
}
// 修改 showFinalUI 确保面板显示
function showFinalUI(conversations, exportFn) {
// 隐藏加载提示
if (progressElement) progressElement.style.display = 'none';
console.log("🎉 会话总数:", conversations.length);
// 容器获取与样式设置
panelContainer = document.getElementById('chatgpt-cleaner-container');
if (!panelContainer) {
panelContainer = document.createElement('div');
panelContainer.id = 'chatgpt-cleaner-container';
document.body.appendChild(panelContainer);
// --- 容器基础样式 ---
panelContainer.style.position = 'fixed';
panelContainer.style.top = '60px'; // 稍微往下一点,避免遮挡顶部元素
panelContainer.style.right = '15px';
panelContainer.style.width = '350px'; // 稍微宽一点
panelContainer.style.maxHeight = 'calc(100vh - 80px)'; // 限制最大高度
panelContainer.style.overflowY = 'auto';
panelContainer.style.background = '#ffffff'; // 明确白色背景
panelContainer.style.color = '#333333'; // 明确深色文字
panelContainer.style.border = '1px solid #e0e0e0';
panelContainer.style.borderRadius = '8px'; // 圆角
panelContainer.style.padding = '15px'; // 内边距
panelContainer.style.zIndex = '9998';
panelContainer.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; // 改进阴影
panelContainer.style.fontFamily = 'sans-serif'; // 使用通用字体
panelContainer.style.fontSize = '14px';
// panelContainer.style.display = 'none'; // 初始隐藏状态由 toggle 控制
}
panelContainer.innerHTML = ''; // 清空旧内容
panelContainer.style.display = 'block'; // 确保显示
// --- 标题 ---
const title = document.createElement('h2');
title.textContent = '对话批量管理';
title.style.fontSize = '1.2em';
title.style.fontWeight = '600';
title.style.color = '#111'; // 标题深色
title.style.marginTop = '0';
title.style.marginBottom = '15px';
title.style.borderBottom = '1px solid #eee';
title.style.paddingBottom = '10px';
panelContainer.appendChild(title);
// --- 按钮区域 ---
const buttonGroup = document.createElement('div');
buttonGroup.style.marginBottom = '15px';
buttonGroup.style.display = 'flex';
buttonGroup.style.flexWrap = 'wrap'; // 允许按钮换行
buttonGroup.style.gap = '8px'; // 按钮间距
// 统一样式函数
const styleButton = (btn, primary = true) => {
btn.style.padding = '8px 12px';
btn.style.border = 'none';
btn.style.borderRadius = '5px';
btn.style.cursor = 'pointer';
btn.style.fontSize = '0.9em';
btn.style.transition = 'background-color 0.2s ease, box-shadow 0.2s ease';
if (primary) {
btn.style.background = '#0d6efd'; // Bootstrap primary blue
btn.style.color = 'white';
} else {
btn.style.background = '#6c757d'; // Bootstrap secondary gray
btn.style.color = 'white';
}
btn.onmouseover = () => {
btn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
btn.style.filter = 'brightness(1.1)';
};
btn.onmouseout = () => {
btn.style.boxShadow = 'none';
btn.style.filter = 'brightness(1)';
};
};
// 添加导出按钮
const exportButton = document.createElement('button');
exportButton.textContent = '导出 JSON';
exportButton.onclick = exportFn;
styleButton(exportButton);
buttonGroup.appendChild(exportButton);
// 添加全选按钮 (占位)
const selectAllButton = document.createElement('button');
selectAllButton.textContent = '全选';
selectAllButton.onclick = () => {
Object.values(checkboxRefs).forEach(cb => cb.checked = true);
console.log("全选操作 (待实现)");
};
styleButton(selectAllButton, false); // 使用次要样式
buttonGroup.appendChild(selectAllButton);
// 添加取消全选按钮 (占位)
const deselectAllButton = document.createElement('button');
deselectAllButton.textContent = '取消全选';
deselectAllButton.onclick = () => {
Object.values(checkboxRefs).forEach(cb => cb.checked = false);
console.log("取消全选操作 (待实现)");
};
styleButton(deselectAllButton, false);
buttonGroup.appendChild(deselectAllButton);
// 添加删除按钮 (占位)
const deleteButton = document.createElement('button');
deleteButton.textContent = '删除选中';
deleteButton.style.background = '#dc3545'; // Bootstrap danger red
deleteButton.onclick = async () => { // 改为 async 函数
const selectedCheckboxes = Object.values(checkboxRefs).filter(cb => cb.checked);
const selectedIds = selectedCheckboxes.map(cb => cb.value);
if (selectedIds.length === 0) {
alert('请先选择要删除的对话。');
return;
}
// --- 重要:用户确认 ---
if (!confirm(`确定要删除 ${selectedIds.length} 个选中的对话吗?\n(注意:这是永久删除,而不是将其设置为不可见)`)) {
return;
}
// --- 开始删除流程 ---
console.log("开始删除选中的对话:", selectedIds);
// 禁用按钮防止重复点击
const allButtons = panelContainer.querySelectorAll('button');
allButtons.forEach(btn => btn.disabled = true);
deleteButton.textContent = `正在删除(0/${selectedIds.length})...`;
let successCount = 0;
let failCount = 0;
const failedIds = [];
for (let i = 0; i < selectedIds.length; i++) {
const id = selectedIds[i];
deleteButton.textContent = `正在删除(${i + 1}/${selectedIds.length})...`;
try {
await deleteSingleConversation(id);
successCount++;
// 从 UI 移除成功的条目
const checkbox = checkboxRefs[id];
if (checkbox) {
checkbox.closest('div').remove(); // 移除包含 checkbox 的整行 div
delete checkboxRefs[id]; // 从引用中删除
}
} catch (error) {
console.error(`删除对话 ${id} 失败:`, error);
failCount++;
failedIds.push(id);
}
// 稍微延时,避免请求过于频繁
await sleep(200); // 200ms 延迟
}
// --- 删除完成,恢复 UI ---
allButtons.forEach(btn => btn.disabled = false);
deleteButton.textContent = '删除选中'; // 恢复按钮文字
// 显示结果
let message = `删除操作完成。
成功删除: ${successCount} 个`;
if (failCount > 0) {
message += `
失败: ${failCount} 个 (ID: ${failedIds.join(', ')})\n请检查控制台获取详细错误信息。`;
console.error("以下对话删除失败:", failedIds);
}
alert(message);
// 可选:更新 allConversationsRaw (如果需要精确的备份)
allConversationsRaw = allConversationsRaw.filter(convo => !selectedIds.includes(convo.id) || failedIds.includes(convo.id));
console.log("内存中的原始数据已更新 (移除了成功的对话)");
};
styleButton(deleteButton);
deleteButton.style.background = '#dc3545'; // 覆盖默认主色
buttonGroup.appendChild(deleteButton);
panelContainer.appendChild(buttonGroup);
// --- 对话列表区域 ---
const listContainer = document.createElement('div');
listContainer.id = 'chatgpt-cleaner-list';
// 可以给列表容器也加点样式,比如内边距
// listContainer.style.paddingTop = '10px';
// 按日期分组对话
const grouped = conversations.reduce((acc, convo) => {
const group = parseDateGroups(convo.update_time);
if (!acc[group]) acc[group] = [];
acc[group].push(convo);
return acc;
}, {});
// 按预定顺序显示分组
let itemCount = 0;
categoryOrder.forEach(groupName => {
if (grouped[groupName] && grouped[groupName].length > 0) {
itemCount += grouped[groupName].length;
const groupDiv = document.createElement('div');
groupDiv.style.marginBottom = '15px';
// --- 分组标题 ---
const groupTitle = document.createElement('h3');
groupTitle.textContent = groupName;
groupTitle.style.fontSize = '1em';
groupTitle.style.fontWeight = '600';
groupTitle.style.color = '#555'; // 分组标题颜色
groupTitle.style.marginTop = '15px'; // 与上一组的间距
groupTitle.style.marginBottom = '8px';
groupTitle.style.paddingBottom = '5px';
groupTitle.style.borderBottom = '1px solid #f0f0f0'; // 分组标题下划线
groupDiv.appendChild(groupTitle);
grouped[groupName].forEach(convo => {
// --- 单个对话行 ---
const convoDiv = document.createElement('div');
convoDiv.style.display = 'flex';
convoDiv.style.alignItems = 'center';
convoDiv.style.padding = '6px 4px'; // 行内上下间距
convoDiv.style.marginBottom = '2px';
convoDiv.style.cursor = 'pointer';
convoDiv.style.borderRadius = '4px';
convoDiv.style.transition = 'background-color 0.15s ease';
convoDiv.onmouseover = () => { convoDiv.style.backgroundColor = '#f0f0f0'; };
convoDiv.onmouseout = () => { convoDiv.style.backgroundColor = 'transparent'; };
// --- 复选框 ---
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = convo.id;
checkbox.style.marginRight = '10px'; // 与文字间距
checkbox.style.marginLeft = '4px';
checkbox.style.flexShrink = '0';
checkbox.style.cursor = 'pointer';
checkboxRefs[convo.id] = checkbox; // 存储引用
convoDiv.appendChild(checkbox);
// --- 标题和日期容器 ---
const textWrapper = document.createElement('div');
textWrapper.style.flexGrow = '1';
textWrapper.style.overflow = 'hidden'; // 防止文字溢出容器
textWrapper.style.display = 'flex';
textWrapper.style.flexDirection = 'column'; // 标题和日期垂直排列
// --- 标题文字 ---
const titleSpan = document.createElement('span');
titleSpan.textContent = `${convo.title || '无标题'}`;
titleSpan.title = `ID: ${convo.id}
点击跳转到对话`;
titleSpan.style.color = '#333'; // 确保标题文字是深色
titleSpan.style.overflow = 'hidden';
titleSpan.style.textOverflow = 'ellipsis';
titleSpan.style.whiteSpace = 'nowrap';
titleSpan.style.marginBottom = '3px'; // 与日期间距
titleSpan.onclick = (e) => {
e.stopPropagation(); // 阻止事件冒泡到 convoDiv 的点击
window.location.href = `/c/${convo.id}`;
};
textWrapper.appendChild(titleSpan);
// --- 日期文字 ---
const dateSpan = document.createElement('span');
dateSpan.textContent = `${formatDateLocal(convo.update_time)}`;
dateSpan.style.fontSize = '0.85em';
dateSpan.style.color = '#666'; // 日期颜色稍浅
dateSpan.style.whiteSpace = 'nowrap';
textWrapper.appendChild(dateSpan);
convoDiv.appendChild(textWrapper);
// --- 整行点击切换复选框 ---
convoDiv.onclick = (e) => {
// 只有当点击目标不是 checkbox 或 titleSpan 时才切换 checkbox
if (e.target !== checkbox && !titleSpan.contains(e.target)) {
checkbox.checked = !checkbox.checked;
}
};
groupDiv.appendChild(convoDiv);
});
listContainer.appendChild(groupDiv);
}
});
// 如果没有加载到任何对话,显示提示
if (itemCount === 0) {
const noDataDiv = document.createElement('div');
noDataDiv.textContent = '未找到任何对话记录。';
noDataDiv.style.textAlign = 'center';
noDataDiv.style.padding = '20px';
noDataDiv.style.color = '#888';
listContainer.appendChild(noDataDiv);
}
panelContainer.appendChild(listContainer);
// 添加一个关闭按钮到面板内部(可选,但更方便)
const closeButton = document.createElement('button');
closeButton.textContent = '×'; // 使用乘号作为关闭图标
closeButton.style.position = 'absolute';
closeButton.style.top = '10px';
closeButton.style.right = '10px';
closeButton.style.background = 'transparent';
closeButton.style.border = 'none';
closeButton.style.fontSize = '1.5em';
closeButton.style.color = '#888';
closeButton.style.cursor = 'pointer';
closeButton.style.lineHeight = '1';
closeButton.style.padding = '5px';
closeButton.title = '关闭面板';
closeButton.onclick = () => {
if (panelContainer) {
panelContainer.style.display = 'none';
}
if (toggleButton) {
toggleButton.textContent = '管理'; // 或图标
toggleButton.title = '打开对话管理器';
}
};
panelContainer.insertBefore(closeButton, panelContainer.firstChild); // 插入到标题前
console.log("✅ UI 渲染完成,样式已更新");
}
// 修改 fetchAllConversations
async function fetchAllConversations(progressCallback) {
let headers = getAuthHeaders();
// 如果第一次获取 headers 时 token 为空,可能需要稍等并重试获取
if (!headers.Authorization) {
console.log("⏳ Token 尚未捕获,等待 1 秒后重试获取 headers...");
await sleep(1000);
headers = getAuthHeaders();
if (!headers.Authorization) {
throw new Error("❌ 无法获取 Authorization token,请尝试刷新页面或重新登录。");
}
}
let conversations = [];
let offset = 0;
const limit = 50; // 稍微调大limit,减少请求次数,但注意不要超过API限制
let done = false;
let total = null; // 初始化 total 为 null
while (!done) {
console.log(`⏳ 正在获取对话: offset=${offset}, limit=${limit}${total !== null ? `, total=${total}`:''}`);
try {
const res = await fetch(`/backend-api/conversations?offset=${offset}&limit=${limit}&order=updated`, {
credentials: "include",
headers // 使用更新后的 headers
});
if (!res.ok) {
const errorText = await res.text();
console.error(`❌ 获取对话失败: ${res.status} ${res.statusText}`, errorText);
// 特别处理 401 Unauthorized
if (res.status === 401) {
throw new Error(`API 请求失败: 401 Unauthorized. Token 可能已过期,请刷新页面或重新登录。`);
}
throw new Error(`API 请求失败: ${res.status} ${res.statusText}`);
}
const json = await res.json();
// --- 关键修改:使用 'items' 和 'total' 字段 ---
const page = json.items || []; // 假设对话列表在 'items' 字段
// 只有在 total 尚未确定时更新 total
if (total === null && typeof json.total === 'number') {
total = json.total;
console.log(`ℹ️ 获取到对话总数: ${total}`);
}
// --- 结束修改 ---
if (page.length === 0) {
// 如果API不返回total,或者返回的total不准,这仍然是最后的保障
console.log("✅ 获取完成(API返回空列表或已达末页)");
done = true;
} else {
conversations = conversations.concat(page);
offset += limit; // 按 limit 增加 offset 请求下一页
// 如果获取的数量已达到或超过 total(如果 total 可知),也视为完成
if (total !== null && conversations.length >= total) {
console.log(`✅ 获取完成(已达到或超过声明的总数 ${total})`);
// 可能获取了比 total 稍多的数据,如果 API 返回不精确或并发请求导致
// 为确保准确,最好还是依赖 page.length === 0 作为主要结束条件,或者 offset 足够大
// conversations = conversations.slice(0, total); // 暂时不裁剪,以防total不准
done = true; // 标记完成
}
// 调用进度回调
if (progressCallback) {
// 传递当前获取的数量和已知的总数
progressCallback(conversations.length, total);
}
await sleep(300); // 保持适当的 API 请求间隔
}
} catch (error) {
console.error("❌ 获取对话分页时出错:", error);
// 通知用户并停止
if (progressElement) progressElement.textContent = `❌ 加载出错: ${error.message}. 请检查控制台。`;
throw error; // 重新抛出错误,中断流程
}
}
allConversationsRaw = conversations; // 用于导出备份
console.log(`📊 最终获取 ${conversations.length} 条对话`);
// 按更新时间降序排序
return conversations.sort((a, b) => new Date(b.update_time) - new Date(a.update_time));
}
// 修改 __chatCleanerInject 以处理加载状态
window.__chatCleanerInject = function (conversations, exportFn, isLoading = false) {
if (isLoading) {
// 如果是加载状态,创建或显示进度元素
if (!progressElement) {
progressElement = document.createElement('div');
progressElement.id = 'chatgpt-cleaner-progress';
// --- 设置进度条样式 ---
progressElement.style.position = 'fixed';
progressElement.style.top = '10px';
progressElement.style.right = '10px';
progressElement.style.background = 'rgba(0,0,0,0.7)';
progressElement.style.color = 'white';
progressElement.style.padding = '5px 10px';
progressElement.style.borderRadius = '5px';
progressElement.style.zIndex = '9999';
progressElement.style.fontSize = '12px'; // 小一点
progressElement.style.fontFamily = 'sans-serif'; // Consistent font
// --- 结束样式设置 ---
document.body.appendChild(progressElement);
}
progressElement.style.display = 'block';
progressElement.style.backgroundColor = 'rgba(0,0,0,0.7)'; // Reset background color
progressElement.textContent = '⏳ 正在准备加载...'; // 初始文本
} else {
// 如果不是加载状态 (即加载完成),调用渲染最终 UI 的函数
if (Array.isArray(conversations)) {
showFinalUI(conversations, exportFn);
} else {
console.error("❌ 渲染最终 UI 失败:对话数据不是数组。");
if (progressElement) {
progressElement.textContent = '❌ 加载数据格式错误';
progressElement.style.backgroundColor = 'red';
}
}
}
};
function downloadJSON() {
if (!allConversationsRaw || allConversationsRaw.length === 0) {
alert("没有对话数据可导出。");
return;
}
try {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(allConversationsRaw, null, 2));
const a = document.createElement('a');
a.setAttribute("href", dataStr);
// 更安全的文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
a.setAttribute("download", `chatgpt_backup_${timestamp}.json`);
document.body.appendChild(a); // Required for Firefox
a.click();
document.body.removeChild(a); // Clean up
console.log("📄 JSON 文件已触发下载");
} catch (error) {
console.error("❌ 导出 JSON 时出错:", error);
alert(`导出失败: ${error.message}`);
}
}
// 修改 main 函数,不再自动执行,由按钮触发
async function main() {
// 防止重复加载数据
if (alreadyLoaded) {
console.log("🚫 数据已加载,无需重复执行 main。");
// 如果面板已存在但被隐藏,直接显示它
if (panelContainer && panelContainer.style.display === 'none') {
panelContainer.style.display = 'block';
if (toggleButton) {
toggleButton.textContent = '隐藏'; // 更新按钮文本
toggleButton.title = '隐藏对话管理器';
}
}
return;
}
console.log("🚀 ChatGPT 清理器启动 main 函数 (由按钮触发)... ");
// 1. 显示加载 UI
window.__chatCleanerInject(null, downloadJSON, true);
try {
// 2. 获取所有对话
console.log("📡 开始获取对话列表...");
const conversations = await fetchAllConversations(updateProgressUI);
alreadyLoaded = true; // 标记数据加载完成
if (toggleButton) toggleButton.disabled = false; // 确保按钮可用
// 3. 渲染最终的对话列表 UI
console.log("🎨 准备渲染最终 UI...");
window.__chatCleanerInject(conversations, downloadJSON, false); // false 表示非加载状态
if (toggleButton) {
toggleButton.textContent = '隐藏'; // 更新按钮文本
toggleButton.title = '隐藏对话管理器';
}
} catch (error) {
console.error("❌ 主流程执行失败:", error);
if (progressElement) {
progressElement.textContent = `❌ 加载失败: ${error.message}`; // 简化信息
progressElement.style.backgroundColor = 'red';
}
// 出错时也解锁按钮,允许用户重试
if (toggleButton) toggleButton.disabled = false;
alreadyLoaded = false; // 允许重试加载
}
}
// --- 创建切换按钮并添加到页面 ---
function createToggleButton() {
toggleButton = document.createElement('button');
toggleButton.id = 'chatgpt-cleaner-toggle';
toggleButton.textContent = '管理'; // 初始文本,或用图标
toggleButton.title = '等待 Token...'; // 初始提示
toggleButton.disabled = true; // 初始禁用,等待 Token
// --- 切换按钮样式 ---
toggleButton.style.position = 'fixed';
toggleButton.style.bottom = '20px';
toggleButton.style.right = '20px';
toggleButton.style.padding = '10px 15px';
toggleButton.style.background = '#0d6efd';
toggleButton.style.color = 'white';
toggleButton.style.border = 'none';
toggleButton.style.borderRadius = '50px'; // 圆形或胶囊形
toggleButton.style.cursor = 'pointer';
toggleButton.style.zIndex = '9997'; // 比面板低一点
toggleButton.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)';
toggleButton.style.fontSize = '14px';
toggleButton.style.transition = 'background-color 0.2s ease, transform 0.2s ease';
toggleButton.onmouseover = () => { toggleButton.style.filter = 'brightness(1.1)'; };
toggleButton.onmouseout = () => { toggleButton.style.filter = 'brightness(1)'; };
toggleButton.onclick = () => {
toggleButton.disabled = true; // 防止快速重复点击
panelContainer = document.getElementById('chatgpt-cleaner-container');
if (!alreadyLoaded) {
// 首次点击,执行 main 加载数据
console.log("🖱️ 切换按钮点击:首次加载数据...");
main().finally(() => {
// 无论成功失败,最终都解锁按钮
// 状态更新由 main 内部处理
// toggleButton.disabled = false; // main内部处理了
});
} else if (panelContainer) {
// 数据已加载,切换面板显示状态
const isHidden = panelContainer.style.display === 'none';
console.log(`🖱️ 切换按钮点击:切换面板显示为 ${isHidden ? '可见' : '隐藏'}`);
panelContainer.style.display = isHidden ? 'block' : 'none';
toggleButton.textContent = isHidden ? '隐藏' : '管理';
toggleButton.title = isHidden ? '隐藏对话管理器' : '打开对话管理器';
toggleButton.disabled = false; // 操作完成,解锁按钮
} else {
// 数据已加载但面板丢失?理论上不应发生,但作为保险
console.warn("面板容器丢失,尝试重新加载...");
alreadyLoaded = false; // 重置状态以允许重新加载
main().finally(() => { /* toggleButton.disabled = false; */ });
}
};
document.body.appendChild(toggleButton);
console.log("🔌 对话管理切换按钮已创建。");
}
// 脚本主体逻辑:立即创建按钮,等待 Token
console.log("ChatGPT Cleaner 脚本已注入,等待 API 请求以捕获 Token...");
// 确保 DOM 加载完成后创建按钮
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createToggleButton);
} else {
createToggleButton();
}
// --- 新增:单个对话删除函数 ---
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' // 确保包含 credentials
});
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(); // 通常 PATCH 成功会返回 {success: true}
console.log(`✅ 对话 ${conversationId} 删除成功:`, result);
return result;
} catch (error) {
console.error(`❌ 执行删除对话 ${conversationId} 的 fetch 时出错:`, error);
throw error; // 将错误向上抛出
}
}
})(); // 脚本立即执行函数结束