ChatGPT 批量对话删除器

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

// ==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; // 将错误向上抛出
      }
  }

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