您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Unified ZIP export for Personal & Team spaces. Sorts conversations by project folders.
// ==UserScript== // @name ChatGPT Universal Exporter // @version 5.0.0 // @description Unified ZIP export for Personal & Team spaces. Sorts conversations by project folders. // @author Alex Mercer and Hanashiro with Gemini's help // @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 // @namespace https://gf.qytechs.cn/users/1479633 // ==/UserScript== /* ============================================================ v5.0.0 变更 (最终整合版) ------------------------------------------------------------ • [功能整合] 将所有导出模式统一为一个强大的ZIP导出功能。 • [UI简化] 导出选项简化为“个人空间”和“团队空间”。 • [结构优化] 两种模式都会生成结构化的ZIP:项目对话在文件夹内, 项目外的对话在根目录。 • [逻辑重构] 导出流程重构,以分别处理项目内外的对话。 • 这是功能完善的最终版本,感谢用户的清晰需求和耐心协作! ========================================================== */ (function () { 'use strict'; const TEAM_WORKSPACE_ID = ''; const BASE_DELAY = 600; const JITTER = 400; const PAGE_LIMIT = 100; let accessToken = null; (function interceptNetwork() { const rawFetch = window.fetch; window.fetch = async function (res, opt = {}) { tryCapture(opt?.headers); return rawFetch.apply(this, arguments); }; const rawOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function () { this.addEventListener('readystatechange', () => { try { tryCapture(this.getRequestHeader('Authorization')); } catch (_) { } }); return rawOpen.apply(this, arguments); }; })(); async function ensureAccessToken() { if (accessToken) return accessToken; try { const nd = JSON.parse(document.getElementById('__NEXT_DATA__').textContent); accessToken = nd?.props?.pageProps?.accessToken; } catch (_) { } if (accessToken) return accessToken; try { const r = await fetch('/api/auth/session?unstable_client=true'); if (r.ok) accessToken = (await r.json()).accessToken; } catch (_) { } return accessToken; } function tryCapture(header) { if (!header) return; const h = typeof header === 'string' ? header : header instanceof Headers ? header.get('Authorization') : header.Authorization || header.authorization; if (h?.startsWith('Bearer ')) accessToken = h.slice(7); } const sleep = ms => new Promise(r => setTimeout(r, ms)); const jitter = () => BASE_DELAY + Math.random() * JITTER; const sanitizeFilename = (name) => name.replace(/[\/\\?%*:|"<>]/g, '-'); function downloadFile(blob, filename) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href); } // [REWRITTEN in V5.0] Unified export logic async function startExportProcess(mode, workspaceId) { const btn = document.getElementById('gpt-rescue-btn'); btn.disabled = true; if (!await ensureAccessToken()) { alert('尚未捕获 accessToken,请先打开或刷新任意一条对话再试'); btn.disabled = false; btn.textContent = 'Export Conversations'; return; } try { const zip = new JSZip(); // 1. Get conversations NOT in any project btn.textContent = '📂 获取项目外对话…'; const orphanIds = await collectIds(btn, workspaceId, null); for (let i = 0; i < orphanIds.length; i++) { btn.textContent = `📥 根目录 (${i + 1}/${orphanIds.length})`; const convData = await getConversation(orphanIds[i], workspaceId); const filename = sanitizeFilename(convData.title || `conversation-${convData.conversation_id}`) + '.json'; zip.file(filename, JSON.stringify(convData, null, 2)); await sleep(jitter()); } // 2. Get list of projects (Gizmos) btn.textContent = '🔍 获取项目列表…'; const projects = await getProjects(workspaceId); // 3. Get conversations FOR EACH project for (const project of projects) { const projectFolder = zip.folder(sanitizeFilename(project.title)); btn.textContent = `📂 项目: ${project.title}`; const projectConvIds = await collectIds(btn, workspaceId, project.id); if (projectConvIds.length === 0) continue; for (let i = 0; i < projectConvIds.length; i++) { btn.textContent = `📥 ${project.title.substring(0,10)}... (${i + 1}/${projectConvIds.length})`; const convData = await getConversation(projectConvIds[i], workspaceId); const filename = sanitizeFilename(convData.title || `conversation-${convData.conversation_id}`) + '.json'; projectFolder.file(filename, JSON.stringify(convData, null, 2)); await sleep(jitter()); } } // 4. Generate and download the ZIP file 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); } } async function getProjects(workspaceId) { const headers = { 'Authorization': `Bearer ${accessToken}` }; if (workspaceId) { headers['ChatGPT-Account-Id'] = workspaceId; } const r = await fetch(`/backend-api/gizmos/snorlax/sidebar`, { headers: headers }); if (!r.ok) throw new Error(`获取项目(Gizmo)列表失败 (${r.status})`); const data = await r.json(); const projects = []; if (data && Array.isArray(data.items)) { for (const item of data.items) { if (item && item.gizmo && item.gizmo.id && item.gizmo.display && 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 = { 'Authorization': `Bearer ${accessToken}` }; if (workspaceId) { headers['ChatGPT-Account-Id'] = workspaceId; } if (gizmoId) { let cursor = '0'; do { const url = `/backend-api/gizmos/${gizmoId}/conversations?cursor=${cursor}`; const r = await fetch(url, { headers: headers }); if (!r.ok) throw new Error(`列举Gizmo对话列表失败 (${r.status}) for gizmo ${gizmoId}`); const j = await r.json(); if (j.items && j.items.length > 0) { j.items.forEach(it => all.add(it.id)); } cursor = j.cursor; await sleep(jitter()); } while (cursor); } else { const modes = [{ label: 'active', param: '' }, { label: 'archived', param: '&is_archived=true' }]; for (const mode of modes) { let offset = 0, has_more = true, page = 0; do { btn.textContent = `📂 项目外对话 (p${++page})`; let url = `/backend-api/conversations?offset=${offset}&limit=${PAGE_LIMIT}&order=updated${mode.param}`; const r = await fetch(url, { headers: 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)); offset += j.items.length; } else { has_more = false; } if (j.items.length < PAGE_LIMIT) { has_more = false; } await sleep(jitter()); } while (has_more); } } return Array.from(all); } async function getConversation(id, workspaceId) { const headers = { 'Authorization': `Bearer ${accessToken}` }; if (workspaceId) { headers['ChatGPT-Account-Id'] = workspaceId; } const r = await fetch(`/backend-api/conversation/${id}`, { headers: headers }); if (!r.ok) throw new Error(`获取对话详情失败 conv ${id} (${r.status})`); const j = await r.json(); j.__fetched_at = new Date().toISOString(); return j; } // [REWRITTEN in V5.0] Simplified UI 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%', backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: '99998', display: 'flex', alignItems: 'center', justifyContent: 'center' }); const dialog = document.createElement('div'); dialog.id = 'export-dialog'; Object.assign(dialog.style, { background: '#fff', padding: '24px', borderRadius: '12px', boxShadow: '0 5px 15px rgba(0,0,0,.3)', width: '400px', fontFamily: 'sans-serif', color: '#333' }); dialog.innerHTML = ` <h2 style="margin-top:0; margin-bottom: 20px; font-size: 18px;">选择要导出的空间</h2> <div style="margin-bottom: 20px;"> <label style="display: block; margin-bottom: 8px;"><input type="radio" name="export-mode" value="personal" checked> 个人空间</label> <label style="display: block; margin-bottom: 8px;"><input type="radio" name="export-mode" value="team"> 团队空间</label> </div> <div id="team-id-container" style="display: none; margin-bottom: 24px;"> <label for="team-id-input" style="display: block; margin-bottom: 8px; font-weight: bold;">Team Workspace ID:</label> <input type="text" id="team-id-input" placeholder="请粘贴你的 Workspace ID" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #ccc; box-sizing: border-box;"> </div> <div style="display: flex; justify-content: flex-end; gap: 12px;"> <button id="cancel-export-btn" style="padding: 10px 16px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer;">取消</button> <button id="start-export-btn" style="padding: 10px 16px; border: none; border-radius: 8px; background: #10a37f; color: #fff; cursor: pointer; font-weight: bold;">开始导出 (ZIP)</button> </div> `; overlay.appendChild(dialog); document.body.appendChild(overlay); const teamIdContainer = document.getElementById('team-id-container'); const teamIdInput = document.getElementById('team-id-input'); const radios = document.querySelectorAll('input[name="export-mode"]'); teamIdInput.value = TEAM_WORKSPACE_ID; radios.forEach(radio => { radio.onchange = (e) => { teamIdContainer.style.display = e.target.value === 'team' ? 'block' : 'none'; }; }); const closeDialog = () => document.body.removeChild(overlay); document.getElementById('cancel-export-btn').onclick = closeDialog; overlay.onclick = (e) => { if (e.target === overlay) closeDialog(); }; document.getElementById('start-export-btn').onclick = () => { const mode = document.querySelector('input[name="export-mode"]:checked').value; let workspaceId = null; if (mode === 'team') { workspaceId = teamIdInput.value.trim(); if (!workspaceId) { alert('此模式需要输入 Team Workspace ID!'); return; } } closeDialog(); startExportProcess(mode, workspaceId); }; } 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); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址