ChatGPT 批量对话删除器 v1.2 (分页修复 + JSON 导出 + 加载进度)

自动分页抽取全部对话,按时间分组,支持选中删除和导出JSON备份,增加加载进度显示。

目前為 2025-04-13 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ChatGPT 批量对话删除器 v1.2 (分页修复 + JSON 导出 + 加载进度)
// @namespace    https://chatgpt.com/
// @version      1.2
// @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; // 将错误向上抛出
      }
  }

})(); // 脚本立即执行函数结束