// ==UserScript== // @name ChatGPT Universal Exporter Enhanced (Fixed) // @description Robust ZIP exporter with JSON/Markdown/HTML, safer intercept, full-thread export, and retries. // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @grant none // @license MIT // @run-at document-start // ==/UserScript== (function () { 'use strict'; // --- 配置与全局变量 Config & globals --- const BASE_DELAY = 600; const JITTER = 400; const PAGE_LIMIT = 100; let accessToken = null; let capturedWorkspaceIds = new Set(); let stopNetworkIntercept = () => {}; let interceptStopped = false; // 导出格式配置 Export formats let exportFormats = { json: true, markdown: true, html: true }; // 安全恢复拦截,防止长期劫持 fetch 影响正常聊天 function maybeStopIntercept() { if (interceptStopped) return; interceptStopped = true; try { stopNetworkIntercept(); } catch (_) {} } // --- 网络拦截与信息捕获 Network intercept (minimal & safe) --- (function interceptNetwork() { const rawFetch = window.fetch; const rawOpen = XMLHttpRequest.prototype.open; stopNetworkIntercept = () => { window.fetch = rawFetch; XMLHttpRequest.prototype.open = rawOpen; }; function isSameOriginResource(res) { try { const url = typeof res === 'string' ? new URL(res, location.href) : new URL(res.url, location.href); return url.origin === location.origin; } catch (_) { return true; } } function getHeaderValueFromAny(hLike, name) { if (!hLike) return null; try { if (hLike instanceof Headers) return hLike.get(name) || hLike.get(name.toLowerCase()); if (Array.isArray(hLike)) { const found = hLike.find(p => Array.isArray(p) && (String(p[0]).toLowerCase() === name.toLowerCase())); return found ? found[1] : null; } if (typeof hLike === 'object') return hLike[name] || hLike[name.toLowerCase()] || null; if (typeof hLike === 'string' && name.toLowerCase() === 'authorization') return hLike; } catch (_) {} return null; } window.fetch = function(resource, options) { try { if (isSameOriginResource(resource)) { const headerCandidates = []; if (resource && typeof Request !== 'undefined' && resource instanceof Request) { headerCandidates.push(resource.headers); } if (options && options.headers) { headerCandidates.push(options.headers); } for (const hc of headerCandidates) { tryCaptureToken(getHeaderValueFromAny(hc, 'Authorization')); const wid = getHeaderValueFromAny(hc, 'ChatGPT-Account-Id'); if (wid && !capturedWorkspaceIds.has(wid)) { capturedWorkspaceIds.add(wid); try { console.log('🎯 [Fetch] 捕获 Workspace ID:', wid); } catch(_){} } } } } catch (_) {} return rawFetch.apply(this, arguments); }; XMLHttpRequest.prototype.open = function () { this.addEventListener('readystatechange', () => { if (this.readyState === 4) { try { const auth = this.getRequestHeader && this.getRequestHeader('Authorization'); tryCaptureToken(auth); const id = this.getRequestHeader && this.getRequestHeader('ChatGPT-Account-Id'); if (id && !capturedWorkspaceIds.has(id)) { capturedWorkspaceIds.add(id); try { console.log('🎯 [XHR] 捕获 Workspace ID:', id); } catch(_){} } } catch (_) {} } }); return rawOpen.apply(this, arguments); }; })(); function tryCaptureToken(headerLike) { let h = null; try { if (!headerLike) { h = null; } else if (typeof headerLike === 'string') { h = headerLike; } else if (headerLike instanceof Headers) { h = headerLike.get('Authorization') || headerLike.get('authorization'); } else if (Array.isArray(headerLike)) { const found = headerLike.find(e => Array.isArray(e) && String(e[0]).toLowerCase() === 'authorization'); h = found ? found[1] : null; } else if (typeof headerLike === 'object') { h = headerLike.Authorization || headerLike.authorization || null; } } catch (_) {} if (h && /^Bearer\s+(.+)/i.test(h)) { const token = h.replace(/^Bearer\s+/i, ''); if (token && token.toLowerCase() !== 'dummy') { accessToken = token; maybeStopIntercept(); } } } async function ensureAccessToken() { if (accessToken) return accessToken; try { const session = await (await fetch('/api/auth/session?unstable_client=true')).json(); if (session.accessToken) { accessToken = session.accessToken; maybeStopIntercept(); return accessToken; } } catch (_) {} alert('无法获取 Access Token。请刷新页面或打开任意一个对话后再试。'); return null; } // --- 辅助函数 Helpers --- const sleep = ms => new Promise(r => setTimeout(r, ms)); const jitter = () => BASE_DELAY + Math.random() * JITTER; const sanitizeFilename = (name) => name.replace(/[\/\\?%*:|"<>]/g, '-').trim(); function getOaiDeviceId() { const cookieString = document.cookie; const match = cookieString.match(/oai-did=([^;]+)/); return match ? match[1] : null; } async function fetchWithRetry(input, init = {}, retries = 3) { let attempt = 0; while (true) { try { const res = await fetch(input, init); if (res.ok) return res; if (attempt < retries && (res.status === 429 || res.status >= 500)) { await sleep(BASE_DELAY * Math.pow(2, attempt) + Math.random() * JITTER); attempt++; continue; } return res; } catch (err) { if (attempt < retries) { await sleep(BASE_DELAY * Math.pow(2, attempt) + Math.random() * JITTER); attempt++; continue; } throw err; } } } function buildHeaders(workspaceId) { const headers = { 'Authorization': `Bearer ${accessToken}` }; const did = getOaiDeviceId(); if (did) headers['oai-device-id'] = did; if (workspaceId) headers['ChatGPT-Account-Id'] = workspaceId; return headers; } function generateUniqueFilename(convData, extension = 'json') { const convId = String(convData.conversation_id || '').trim(); const idPart = convId || Math.random().toString(36).slice(2, 10); const ts = convData.create_time ? new Date(convData.create_time * 1000) : new Date(); const tsPart = `${ts.getFullYear()}${String(ts.getMonth() + 1).padStart(2, '0')}${String(ts.getDate()).padStart(2, '0')}_${String(ts.getHours()).padStart(2, '0')}${String(ts.getMinutes()).padStart(2, '0')}${String(ts.getSeconds()).padStart(2, '0')}`; let baseName = convData.title; if (!baseName || baseName.trim().toLowerCase() === 'new chat') { baseName = 'Untitled Conversation'; } return `${sanitizeFilename(baseName)}_${idPart}_${tsPart}.${extension}`; } function downloadFile(blob, filename) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); } // --- Conversation parsing (full mapping sorted by time) --- function parseConversation(convData) { const mapping = convData.mapping || {}; const msgs = []; for (const key in mapping) { const node = mapping[key]; const message = node && node.message; if (!message || !message.content || !message.content.parts) continue; const role = message.author && message.author.role; if (role !== 'user' && role !== 'assistant') continue; const content = message.content.parts.join('\n'); if (!content || !content.trim()) continue; msgs.push({ role, content, createTime: message.create_time, model: (message.metadata && message.metadata.model_slug) || '' }); } msgs.sort((a, b) => (a.createTime || 0) - (b.createTime || 0)); return { title: convData.title || 'Untitled Conversation', createTime: convData.create_time, updateTime: convData.update_time, conversationId: convData.conversation_id, model: convData.default_model_slug || '', messages: msgs }; } // --- Markdown 转换函数 Markdown converter --- function convertToMarkdown(convData) { const parsed = parseConversation(convData); let md = ''; md += `# ${parsed.title}\n\n`; md += `**Conversation ID:** \`${parsed.conversationId}\`\n\n`; if (parsed.model) md += `**Model:** ${parsed.model}\n\n`; if (parsed.createTime) md += `**Created:** ${new Date(parsed.createTime * 1000).toLocaleString()}\n\n`; if (parsed.updateTime) md += `**Last Updated:** ${new Date(parsed.updateTime * 1000).toLocaleString()}\n\n`; md += `---\n\n`; parsed.messages.forEach((msg, index) => { const roleLabel = msg.role === 'user' ? '👤 User' : '🤖 Assistant'; const timestamp = msg.createTime ? ` (${new Date(msg.createTime * 1000).toLocaleString()})` : ''; md += `## ${roleLabel}${timestamp}\n\n`; md += `${msg.content}\n\n`; if (index < parsed.messages.length - 1) md += `---\n\n`; }); return md; } // --- HTML 转换函数 HTML converter (code-block safe) --- function convertToHTML(convData) { const parsed = parseConversation(convData); const escapeHtml = (text) => { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }; const renderContent = (content) => { let html = escapeHtml(content); const blocks = []; html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { const idx = blocks.length; const blockHtml = `
${code.trim()}
`; blocks.push(blockHtml); return `[[[CODE_BLOCK_${idx}]]]`; }); html = html.replace(/`([^`]+)`/g, '$1'); html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); html = html.replace(/\n/g, '
'); html = html.replace(/\[\[\[CODE_BLOCK_(\d+)]]]/g, (_, i) => blocks[Number(i)]); return html; }; let html = ` ${escapeHtml(parsed.title)}

${escapeHtml(parsed.title)}

`; parsed.messages.forEach((msg) => { const roleClass = msg.role; const roleIcon = msg.role === 'user' ? '👤' : '🤖'; const roleLabel = msg.role === 'user' ? 'User' : 'Assistant'; const timestamp = msg.createTime ? new Date(msg.createTime * 1000).toLocaleString() : ''; html += `
${roleIcon} ${roleLabel} ${timestamp ? `${timestamp}` : ''}
${renderContent(msg.content)}
`; }); html += `
`; return html; } // --- 导出流程 Export process --- async function startExportProcess(mode, workspaceId, formats, selectedConversations = []) { const btn = document.getElementById('gpt-rescue-btn'); btn.disabled = true; if (!await ensureAccessToken()) { btn.disabled = false; btn.textContent = 'Export Conversations'; return; } try { const zip = new JSZip(); if (!selectedConversations.length) throw new Error('没有需要导出的对话。'); const rootConvs = selectedConversations.filter(c => !c.projectId); const projectMap = {}; selectedConversations.filter(c => c.projectId).forEach(c => { if (!projectMap[c.projectId]) projectMap[c.projectId] = { title: c.projectTitle || c.projectId, items: [] }; projectMap[c.projectId].items.push(c); }); btn.textContent = '📂 导出项目外对话…'; for (let i = 0; i < rootConvs.length; i++) { const conv = rootConvs[i]; btn.textContent = `📥 根目录 (${i + 1}/${rootConvs.length})`; const convData = await getConversation(conv.id, workspaceId); if (formats.json) zip.file(generateUniqueFilename(convData, 'json'), JSON.stringify(convData, null, 2)); if (formats.markdown) zip.file(generateUniqueFilename(convData, 'md'), convertToMarkdown(convData)); if (formats.html) zip.file(generateUniqueFilename(convData, 'html'), convertToHTML(convData)); await sleep(jitter()); } const projectEntries = Object.entries(projectMap); for (const [projectId, detail] of projectEntries) { const folderName = sanitizeFilename(detail.title || projectId); const projectFolder = zip.folder(folderName); btn.textContent = `📂 项目: ${folderName}`; const list = detail.items; for (let i = 0; i < list.length; i++) { const conv = list[i]; btn.textContent = `📥 ${folderName.substring(0,10)}... (${i + 1}/${list.length})`; const convData = await getConversation(conv.id, workspaceId); if (formats.json) projectFolder.file(generateUniqueFilename(convData, 'json'), JSON.stringify(convData, null, 2)); if (formats.markdown) projectFolder.file(generateUniqueFilename(convData, 'md'), convertToMarkdown(convData)); if (formats.html) projectFolder.file(generateUniqueFilename(convData, 'html'), convertToHTML(convData)); await sleep(jitter()); } } btn.textContent = '📦 生成 ZIP 文件…'; const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE" }); const date = new Date().toISOString().slice(0, 10); const filename = mode === 'team' ? `chatgpt_team_backup_${workspaceId}_${date}.zip` : `chatgpt_personal_backup_${date}.zip`; downloadFile(blob, filename); alert(`✅ 导出完成!`); btn.textContent = '✅ 完成'; } catch (e) { console.error("导出过程中发生严重错误", e); alert(`导出失败: ${e.message}。详情请查看控制台(F12 -> Console)。`); btn.textContent = '⚠️ Error'; } finally { setTimeout(() => { btn.disabled = false; btn.textContent = 'Export Conversations'; }, 3000); } } // --- API 调用函数 API helpers --- async function getProjects(workspaceId) { if (!workspaceId) return []; const r = await fetchWithRetry(`/backend-api/gizmos/snorlax/sidebar`, { headers: buildHeaders(workspaceId) }); if (!r.ok) { console.warn(`获取项目(Gizmo)列表失败 (${r.status})`); return []; } const data = await r.json(); const projects = []; data.items?.forEach(item => { if (item?.gizmo?.id && item?.gizmo?.display?.name) { projects.push({ id: item.gizmo.id, title: item.gizmo.display.name }); } }); return projects; } async function collectIds(btn, workspaceId, gizmoId) { const all = new Set(); const headers = buildHeaders(workspaceId); if (gizmoId) { let cursor = '0'; do { const r = await fetchWithRetry(`/backend-api/gizmos/${gizmoId}/conversations?cursor=${cursor}`, { headers }); if (!r.ok) throw new Error(`列举项目对话列表失败 (${r.status})`); const j = await r.json(); j.items?.forEach(it => all.add(it.id)); cursor = j.cursor; await sleep(jitter()); } while (cursor); } else { for (const is_archived of [false, true]) { let offset = 0, has_more = true, page = 0; do { btn.textContent = `📂 项目外对话 (${is_archived ? 'Archived' : 'Active'} p${++page})`; const r = await fetchWithRetry(`/backend-api/conversations?offset=${offset}&limit=${PAGE_LIMIT}&order=updated${is_archived ? '&is_archived=true' : ''}`, { headers }); if (!r.ok) throw new Error(`列举项目外对话列表失败 (${r.status})`); const j = await r.json(); if (j.items && j.items.length > 0) { j.items.forEach(it => all.add(it.id)); has_more = j.items.length === PAGE_LIMIT; offset += j.items.length; } else { has_more = false; } await sleep(jitter()); } while (has_more); } } return Array.from(all); } async function listConversationMetas(mode, workspaceId, progressCb = () => {}) { if (!await ensureAccessToken()) throw new Error('无法获取 Access Token,无法列出对话。'); const headers = buildHeaders(workspaceId); const all = []; const seen = new Set(); const pushItem = (item, projectInfo = {}) => { if (!item || !item.id || seen.has(item.id)) return; seen.add(item.id); all.push({ id: item.id, title: item.title || '未命名对话', projectId: projectInfo.id || null, projectTitle: projectInfo.title || '' }); }; // 根目录对话 progressCb('加载项目外对话列表…'); for (const is_archived of [false, true]) { let offset = 0, has_more = true, page = 0; do { progressCb(`加载项目外对话 ${is_archived ? 'Archived' : 'Active'} 第 ${++page} 页…`); const r = await fetchWithRetry(`/backend-api/conversations?offset=${offset}&limit=${PAGE_LIMIT}&order=updated${is_archived ? '&is_archived=true' : ''}`, { headers }); if (!r.ok) throw new Error(`列举项目外对话列表失败 (${r.status})`); const j = await r.json(); if (j.items && j.items.length > 0) { j.items.forEach(it => pushItem(it)); has_more = j.items.length === PAGE_LIMIT; offset += j.items.length; } else { has_more = false; } await sleep(jitter()); } while (has_more); } // 项目内对话(仅团队空间有) if (workspaceId) { const projects = await getProjects(workspaceId); for (const project of projects) { let cursor = '0'; do { progressCb(`加载项目 ${project.title}…`); const r = await fetchWithRetry(`/backend-api/gizmos/${project.id}/conversations?cursor=${cursor}`, { headers }); if (!r.ok) throw new Error(`列举项目 ${project.title} 对话列表失败 (${r.status})`); const j = await r.json(); j.items?.forEach(it => pushItem(it, { id: project.id, title: project.title })); cursor = j.cursor; await sleep(jitter()); } while (cursor); } } progressCb(`已加载 ${all.length} 个对话,可勾选导出。`); return all; } async function getConversation(id, workspaceId) { const headers = buildHeaders(workspaceId); const r = await fetchWithRetry(`/backend-api/conversation/${id}`, { headers }); if (!r.ok) throw new Error(`获取对话详情失败 conv ${id} (${r.status})`); const j = await r.json(); j.__fetched_at = new Date().toISOString(); return j; } // --- 工作空间自动检测 Workspace detection --- function detectAllWorkspaceIds() { const foundIds = new Set(capturedWorkspaceIds); try { const data = JSON.parse(document.getElementById('__NEXT_DATA__')?.textContent || '{}'); const accounts = data?.props?.pageProps?.user?.accounts; if (accounts) { Object.values(accounts).forEach(acc => { if (acc?.account?.id) foundIds.add(acc.account.id); }); } } catch (e) {} try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (!key) continue; if (key.includes('account') || key.includes('workspace')) { const value = localStorage.getItem(key); if (!value) continue; if (/^ws-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value.replace(/"/g, ''))) { foundIds.add(value.replace(/"/g, '')); } else if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value.replace(/"/g, ''))) { foundIds.add(value.replace(/"/g, '')); } } } } catch(e) {} try { console.log('🔍 检测到以下 Workspace IDs:', Array.from(foundIds)); } catch(_){} return Array.from(foundIds); } // --- 对话框UI函数 Simple export dialog --- function showExportDialog() { if (document.getElementById('export-dialog-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'export-dialog-overlay'; Object.assign(overlay.style, { position:'fixed', top:'0', left:'0', width:'100%', height:'100%', background:'rgba(0,0,0,0.45)', zIndex:'99998', display:'flex', alignItems:'center', justifyContent:'center' }); const dialog = document.createElement('div'); Object.assign(dialog.style, { background:'#fff', borderRadius:'10px', padding:'18px', width:'460px', boxShadow:'0 6px 24px rgba(0,0,0,.2)', fontFamily:'sans-serif', color:'#333' }); dialog.innerHTML = `

导出会话

选择要导出的对话
加载中…
`; overlay.appendChild(dialog); document.body.appendChild(overlay); const radioPersonal = dialog.querySelector('input[name="mode"][value="personal"]'); const radioTeam = dialog.querySelector('input[name="mode"][value="team"]'); const teamArea = dialog.querySelector('#team-area'); const detectedDiv = dialog.querySelector('#detected'); const teamInput = dialog.querySelector('#team-id'); const convStatus = dialog.querySelector('#conv-select-status'); const convListEl = dialog.querySelector('#conv-list'); const convRefresh = dialog.querySelector('#conv-refresh'); const convSelectAll = dialog.querySelector('#conv-select-all'); const convCache = {}; let loadToken = 0; const renderConversationList = (items) => { convListEl.innerHTML = ''; if (!items.length) { convListEl.innerHTML = '
暂无可用对话
'; convSelectAll.checked = false; return; } const roots = []; const projectMap = {}; const projectOrder = []; items.forEach(it => { if (it.projectId) { if (!projectMap[it.projectId]) { projectMap[it.projectId] = { title: it.projectTitle || it.projectId, list: [] }; projectOrder.push(it.projectId); } projectMap[it.projectId].list.push(it); } else { roots.push(it); } }); const frag = document.createDocumentFragment(); // 项目分组 projectOrder.forEach(pid => { const detail = projectMap[pid]; if (!detail.list.length) return; const wrap = document.createElement('details'); wrap.style.margin = '6px 0'; const summary = document.createElement('summary'); summary.style.cursor = 'pointer'; summary.style.fontWeight = '600'; summary.textContent = `项目 ${sanitizeFilename(detail.title)} (${detail.list.length})`; wrap.appendChild(summary); detail.list.forEach(item => { const row = document.createElement('label'); Object.assign(row.style, { display:'flex', alignItems:'center', justifyContent:'space-between', padding:'4px 2px 4px 16px', borderBottom:'1px solid #eee' }); row.innerHTML = ` ${sanitizeFilename(item.title)} `; wrap.appendChild(row); }); frag.appendChild(wrap); }); // 根目录对话 roots.forEach(item => { const row = document.createElement('label'); Object.assign(row.style, { display:'flex', alignItems:'center', justifyContent:'space-between', padding:'4px 2px', borderBottom:'1px solid #eee' }); row.innerHTML = ` ${sanitizeFilename(item.title)} `; frag.appendChild(row); }); convListEl.appendChild(frag); convSelectAll.checked = true; }; const determineWorkspace = () => { const mode = radioTeam.checked ? 'team' : 'personal'; if (mode === 'personal') return { mode, workspaceId: null }; const manual = teamInput.value.trim(); const workspaceId = manual || ids[0] || ''; return { mode, workspaceId }; }; const loadConversationList = async (showWarnOnMissing = false) => { const { mode, workspaceId } = determineWorkspace(); if (mode === 'team' && !workspaceId) { convStatus.textContent = '请输入有效的 Team Workspace ID 后再刷新列表。'; convListEl.innerHTML = ''; convSelectAll.checked = false; if (showWarnOnMissing) alert('请输入一个有效的 Team Workspace ID 再加载对话列表。'); return; } const cacheKey = `${mode}:${workspaceId || 'personal'}`; if (convCache[cacheKey]) { convStatus.textContent = `已加载 ${convCache[cacheKey].length} 个对话(缓存)`; renderConversationList(convCache[cacheKey]); return; } const token = ++loadToken; convStatus.textContent = '加载对话列表中…'; convListEl.innerHTML = ''; try { const data = await listConversationMetas(mode, workspaceId || null, (msg) => { if (token === loadToken) convStatus.textContent = msg; }); if (token !== loadToken) return; convCache[cacheKey] = data; renderConversationList(data); } catch (e) { if (token !== loadToken) return; convStatus.textContent = `加载失败: ${e.message}`; convListEl.innerHTML = '
无法加载对话列表
'; convSelectAll.checked = false; } }; convSelectAll.addEventListener('change', () => { convListEl.querySelectorAll('.conv-check').forEach(cb => { cb.checked = convSelectAll.checked; }); }); convListEl.addEventListener('change', () => { const checks = Array.from(convListEl.querySelectorAll('.conv-check')); convSelectAll.checked = checks.length > 0 && checks.every(cb => cb.checked); }); convRefresh.onclick = () => { loadConversationList(true); }; const ids = detectAllWorkspaceIds(); if (ids.length) { detectedDiv.textContent = ids.join(' , '); radioTeam.checked = true; radioPersonal.checked = false; } teamArea.style.display = 'block'; radioTeam.addEventListener('change', () => { loadConversationList(); }); radioPersonal.addEventListener('change', () => { loadConversationList(); }); teamInput.addEventListener('change', () => { loadConversationList(); }); loadConversationList(); dialog.querySelector('#dlg-cancel').onclick = () => document.body.removeChild(overlay); dialog.querySelector('#dlg-start').onclick = async () => { const formats = { json: dialog.querySelector('#fmt-json').checked, markdown: dialog.querySelector('#fmt-md').checked, html: dialog.querySelector('#fmt-html').checked }; if (!formats.json && !formats.markdown && !formats.html) { alert('请至少选择一种导出格式!'); return; } const mode = radioTeam.checked ? 'team' : 'personal'; let workspaceId = null; if (mode === 'team') { const manual = teamInput.value.trim(); workspaceId = manual || ids[0] || ''; if (!workspaceId) { alert('请选择或输入一个有效的 Team Workspace ID!'); return; } } const selected = Array.from(dialog.querySelectorAll('.conv-check:checked')).map(cb => ({ id: cb.getAttribute('data-id'), projectId: cb.getAttribute('data-project') || null, projectTitle: cb.getAttribute('data-project-title') || '', title: cb.getAttribute('data-title') || '' })); if (!selected.length) { await loadConversationList(true); const retrySelected = Array.from(dialog.querySelectorAll('.conv-check:checked')).map(cb => ({ id: cb.getAttribute('data-id'), projectId: cb.getAttribute('data-project') || null, projectTitle: cb.getAttribute('data-project-title') || '', title: cb.getAttribute('data-title') || '' })); if (!retrySelected.length) { alert('请至少勾选一个要导出的对话。'); return; } selected.splice(0, selected.length, ...retrySelected); } document.body.removeChild(overlay); exportFormats.mode = mode; exportFormats.workspaceId = workspaceId; startExportProcess(mode, workspaceId, formats, selected); }; overlay.onclick = (e) => { if (e.target === overlay) document.body.removeChild(overlay); }; } function addBtn() { if (document.getElementById('gpt-rescue-btn')) return; const b = document.createElement('button'); b.id = 'gpt-rescue-btn'; b.textContent = 'Export Conversations'; Object.assign(b.style, { position:'fixed', bottom:'24px', right:'24px', zIndex:'99997', padding:'10px 14px', borderRadius:'8px', border:'none', cursor:'pointer', fontWeight:'bold', background:'#10a37f', color:'#fff', fontSize:'14px', boxShadow:'0 3px 12px rgba(0,0,0,.15)', userSelect:'none' }); b.onclick = showExportDialog; document.body.appendChild(b); } setTimeout(addBtn, 2000); })();