ChatGPT GPTs 导出工具

从 ChatGPT 导出你的 GPTs 数据

  1. // ==UserScript==
  2. // @name ChatGPT GPTs Exporter
  3. // @name:zh-CN ChatGPT GPTs 导出工具
  4. // @name:zh-TW ChatGPT GPTs 導出工具
  5. // @namespace https://github.com/lroolle/chatgpt-degraded
  6. // @version 0.1.1
  7. // @description Export your GPTs data from ChatGPT
  8. // @description:zh-CN 从 ChatGPT 导出你的 GPTs 数据
  9. // @description:zh-TW 從 ChatGPT 導出你的 GPTs 數據
  10. // @author lroolle
  11. // @license AGPL-3.0
  12. // @match *://chat.openai.com/gpts/*
  13. // @match *://chatgpt.com/gpts/*
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_setClipboard
  17. // @grant unsafeWindow
  18. // @run-at document-start
  19. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojMmE5ZDhmO3N0b3Atb3BhY2l0eToxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3R5bGU9InN0b3AtY29sb3I6IzJhOWQ4ZjtzdG9wLW9wYWNpdHk6MC44Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Zz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjI4IiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KPCEtLU91dGVyIGNpcmNsZSBtb2RpZmllZCB0byBsb29rIGxpa2UgIkMiLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEyNSA1NSIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjIwIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjQiIGZpbGw9IiNmZmYiLz4KICA8L2c+Cjwvc3ZnPg==
  20. // @homepageURL https://github.com/lroolle/chatgpt-degraded
  21. // @supportURL https://github.com/lroolle/chatgpt-degraded/issues
  22. // ==/UserScript==
  23.  
  24. (function() {
  25. 'use strict';
  26.  
  27. // Store GPTs data
  28. let gptsData = [];
  29. let isExporting = false;
  30.  
  31. // Storage key for persisted data
  32. const STORAGE_KEY = 'chatgpt_gpts_data';
  33.  
  34. // Load persisted data
  35. function loadPersistedData() {
  36. try {
  37. const stored = localStorage.getItem(STORAGE_KEY);
  38. return stored ? JSON.parse(stored) : [];
  39. } catch (error) {
  40. console.error('Error loading persisted GPTs data:', error);
  41. return [];
  42. }
  43. }
  44.  
  45. // Save data to persistence
  46. function persistData(data) {
  47. try {
  48. // Create a map of existing data by ID
  49. const existingData = loadPersistedData();
  50. const dataMap = new Map(existingData.map(item => [item.id, item]));
  51. // Update or add new data
  52. data.forEach(item => {
  53. dataMap.set(item.id, item);
  54. });
  55. // Convert map back to array and save
  56. const mergedData = Array.from(dataMap.values());
  57. localStorage.setItem(STORAGE_KEY, JSON.stringify(mergedData));
  58. return mergedData;
  59. } catch (error) {
  60. console.error('Error persisting GPTs data:', error);
  61. return data;
  62. }
  63. }
  64.  
  65. // Clear persisted data
  66. function clearPersistedData() {
  67. try {
  68. localStorage.removeItem(STORAGE_KEY);
  69. } catch (error) {
  70. console.error('Error clearing persisted GPTs data:', error);
  71. }
  72. }
  73.  
  74. // i18n support
  75. const i18n = {
  76. 'en': {
  77. exportBtn: 'Export GPTs',
  78. exportJSON: 'Export as JSON',
  79. exportCSV: 'Export as CSV',
  80. clearData: 'Clear Data',
  81. exporting: 'Exporting...',
  82. copied: 'Copied to clipboard!',
  83. noData: 'No GPTs data found',
  84. error: 'Error exporting GPTs',
  85. dataCleared: 'Data cleared successfully',
  86. totalGPTs: 'Total GPTs: '
  87. },
  88. 'zh-CN': {
  89. exportBtn: '导出 GPTs',
  90. exportJSON: '导出为 JSON',
  91. exportCSV: '导出为 CSV',
  92. clearData: '清除数据',
  93. exporting: '导出中...',
  94. copied: '已复制到剪贴板!',
  95. noData: '未找到 GPTs 数据',
  96. error: '导出 GPTs 时出错',
  97. dataCleared: '数据已清除',
  98. totalGPTs: '总计 GPTs: '
  99. },
  100. 'zh-TW': {
  101. exportBtn: '導出 GPTs',
  102. exportJSON: '導出為 JSON',
  103. exportCSV: '導出為 CSV',
  104. clearData: '清除數據',
  105. exporting: '導出中...',
  106. copied: '已複製到剪貼板!',
  107. noData: '未找到 GPTs 數據',
  108. error: '導出 GPTs 時出錯',
  109. dataCleared: '數據已清除',
  110. totalGPTs: '總計 GPTs: '
  111. }
  112. };
  113.  
  114. // Get user language
  115. const userLang = (navigator.language || 'en').toLowerCase();
  116. const lang = i18n[userLang] ? userLang :
  117. userLang.startsWith('zh-tw') ? 'zh-TW' :
  118. userLang.startsWith('zh') ? 'zh-CN' : 'en';
  119. const t = key => i18n[lang][key] || i18n.en[key];
  120.  
  121. // Intercept fetch requests
  122. const originalFetch = unsafeWindow.fetch;
  123. unsafeWindow.fetch = async function(resource, options) {
  124. const response = await originalFetch(resource, options);
  125. const url = typeof resource === 'string' ? resource : resource?.url;
  126.  
  127. // Check if this is the GPTs list request
  128. if (url && url.includes('/public-api/gizmos/discovery/mine')) {
  129. try {
  130. const clonedResponse = response.clone();
  131. const data = await clonedResponse.json();
  132. if (data?.list?.items) {
  133. const newGPTsData = data.list.items.map(item => {
  134. const gpt = item.resource.gizmo;
  135. return {
  136. id: gpt.id,
  137. name: gpt.display.name || '',
  138. description: gpt.display.description || '',
  139. instructions: gpt.instructions || '',
  140. created_at: gpt.created_at,
  141. updated_at: gpt.updated_at,
  142. version: gpt.version,
  143. tools: item.resource.tools.map(tool => tool.type),
  144. prompt_starters: gpt.display.prompt_starters || [],
  145. share_recipient: gpt.share_recipient,
  146. num_interactions: gpt.num_interactions
  147. };
  148. });
  149. // Persist and update the data
  150. gptsData = persistData(newGPTsData);
  151. // Update the counter in UI
  152. updateGPTsCounter(gptsData.length);
  153. }
  154. } catch (error) {
  155. console.error('Error intercepting GPTs data:', error);
  156. }
  157. }
  158. return response;
  159. };
  160.  
  161. // Update GPTs counter in UI
  162. function updateGPTsCounter(count) {
  163. const counter = document.getElementById('gpts-counter');
  164. if (counter) {
  165. counter.textContent = `${t('totalGPTs')}${count}`;
  166. }
  167. }
  168.  
  169. // Convert GPTs data to CSV
  170. function convertToCSV(data) {
  171. const headers = [
  172. 'ID',
  173. 'Name',
  174. 'Description',
  175. 'Instructions',
  176. 'Created At',
  177. 'Updated At',
  178. 'Version',
  179. 'Tools',
  180. 'Prompt Starters',
  181. 'Share Recipient',
  182. 'Interactions'
  183. ];
  184.  
  185. const rows = data.map(gpt => [
  186. gpt.id,
  187. `"${(gpt.name || '').replace(/"/g, '""')}"`,
  188. `"${(gpt.description || '').replace(/"/g, '""')}"`,
  189. `"${(gpt.instructions || '').replace(/"/g, '""')}"`,
  190. gpt.created_at,
  191. gpt.updated_at,
  192. gpt.version,
  193. `"${(gpt.tools || []).join(', ')}"`,
  194. `"${(gpt.prompt_starters || []).join(', ').replace(/"/g, '""')}"`,
  195. gpt.share_recipient,
  196. gpt.num_interactions
  197. ]);
  198.  
  199. return [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
  200. }
  201.  
  202. // Export GPTs data
  203. function exportGPTs(format = 'json') {
  204. if (isExporting) return;
  205. isExporting = true;
  206.  
  207. try {
  208. // Load all persisted data
  209. const allData = loadPersistedData();
  210. if (!allData.length) {
  211. alert(t('noData'));
  212. isExporting = false;
  213. return;
  214. }
  215.  
  216. let exportContent, filename, mimeType;
  217.  
  218. if (format === 'csv') {
  219. exportContent = convertToCSV(allData);
  220. filename = `chatgpt-gpts-export-${new Date().toISOString().split('T')[0]}.csv`;
  221. mimeType = 'text/csv';
  222. } else {
  223. const exportData = {
  224. exported_at: new Date().toISOString(),
  225. total_gpts: allData.length,
  226. gpts: allData
  227. };
  228. exportContent = JSON.stringify(exportData, null, 2);
  229. filename = `chatgpt-gpts-export-${new Date().toISOString().split('T')[0]}.json`;
  230. mimeType = 'application/json';
  231. }
  232.  
  233. GM_setClipboard(exportContent);
  234.  
  235. // Create and download file
  236. const blob = new Blob([exportContent], { type: mimeType });
  237. const url = URL.createObjectURL(blob);
  238. const a = document.createElement('a');
  239. a.href = url;
  240. a.download = filename;
  241. document.body.appendChild(a);
  242. a.click();
  243. document.body.removeChild(a);
  244. URL.revokeObjectURL(url);
  245.  
  246. alert(t('copied'));
  247. } catch (error) {
  248. console.error('Error exporting GPTs:', error);
  249. alert(t('error'));
  250. } finally {
  251. isExporting = false;
  252. }
  253. }
  254.  
  255. // Register menu commands
  256. GM_registerMenuCommand(t('exportJSON'), () => exportGPTs('json'));
  257. GM_registerMenuCommand(t('exportCSV'), () => exportGPTs('csv'));
  258.  
  259. // Add export button to UI
  260. function addExportButton() {
  261. const styles = document.createElement('style');
  262. styles.textContent = `
  263. .gpts-exporter {
  264. position: fixed;
  265. bottom: 20px;
  266. right: 20px;
  267. z-index: 10000;
  268. display: flex;
  269. flex-direction: column;
  270. gap: 8px;
  271. background: var(--surface-primary, rgba(255, 255, 255, 0.9));
  272. padding: 12px;
  273. border-radius: 8px;
  274. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  275. border: 1px solid var(--border-light, rgba(0, 0, 0, 0.1));
  276. transition: transform 0.2s ease, opacity 0.2s ease;
  277. }
  278. .gpts-exporter:hover {
  279. transform: translateY(-2px);
  280. }
  281. .gpts-exporter.collapsed {
  282. transform: translateX(calc(100% + 20px));
  283. }
  284. .gpts-exporter button {
  285. padding: 8px 16px;
  286. background-color: var(--success-color, #10a37f);
  287. color: #fff;
  288. border: none;
  289. border-radius: 4px;
  290. cursor: pointer;
  291. font-size: 14px;
  292. font-weight: 500;
  293. transition: all 0.2s ease;
  294. display: flex;
  295. align-items: center;
  296. justify-content: center;
  297. gap: 6px;
  298. min-width: 140px;
  299. }
  300. .gpts-exporter button:hover {
  301. opacity: 0.9;
  302. transform: translateY(-1px);
  303. }
  304. .gpts-exporter button:active {
  305. transform: translateY(0);
  306. }
  307. .gpts-exporter .toggle-btn {
  308. position: absolute;
  309. left: -32px;
  310. top: 50%;
  311. transform: translateY(-50%);
  312. width: 24px;
  313. height: 24px;
  314. background: var(--success-color, #10a37f);
  315. border: none;
  316. border-radius: 4px 0 0 4px;
  317. cursor: pointer;
  318. display: flex;
  319. align-items: center;
  320. justify-content: center;
  321. padding: 0;
  322. min-width: unset;
  323. }
  324. .gpts-exporter .toggle-btn svg {
  325. width: 16px;
  326. height: 16px;
  327. fill: #fff;
  328. transition: transform 0.2s ease;
  329. }
  330. .gpts-exporter.collapsed .toggle-btn svg {
  331. transform: rotate(180deg);
  332. }
  333. .gpts-exporter button.json-btn {
  334. background-color: var(--success-color, #10a37f);
  335. }
  336. .gpts-exporter button.csv-btn {
  337. background-color: var(--primary-color, #0ea5e9);
  338. }
  339. .gpts-exporter .counter {
  340. font-size: 12px;
  341. color: var(--text-secondary, #666);
  342. text-align: center;
  343. margin-bottom: 4px;
  344. }
  345. .gpts-exporter button.clear-btn {
  346. background-color: var(--error-color, #dc2626);
  347. font-size: 12px;
  348. padding: 4px 8px;
  349. }
  350. `;
  351. document.head.appendChild(styles);
  352.  
  353. const container = document.createElement('div');
  354. container.className = 'gpts-exporter';
  355.  
  356. // Add GPTs counter
  357. const counter = document.createElement('div');
  358. counter.id = 'gpts-counter';
  359. counter.className = 'counter';
  360. counter.textContent = `${t('totalGPTs')}${loadPersistedData().length}`;
  361.  
  362. const toggleBtn = document.createElement('button');
  363. toggleBtn.className = 'toggle-btn';
  364. toggleBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>';
  365. toggleBtn.addEventListener('click', () => {
  366. container.classList.toggle('collapsed');
  367. });
  368.  
  369. const jsonBtn = document.createElement('button');
  370. jsonBtn.className = 'json-btn';
  371. jsonBtn.innerHTML = `
  372. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  373. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  374. <polyline points="7 10 12 15 17 10"/>
  375. <line x1="12" y1="15" x2="12" y2="3"/>
  376. </svg>
  377. ${t('exportJSON')}
  378. `;
  379. jsonBtn.addEventListener('click', () => exportGPTs('json'));
  380.  
  381. const csvBtn = document.createElement('button');
  382. csvBtn.className = 'csv-btn';
  383. csvBtn.innerHTML = `
  384. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  385. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
  386. <polyline points="14 2 14 8 20 8"/>
  387. <line x1="16" y1="13" x2="8" y2="13"/>
  388. <line x1="16" y1="17" x2="8" y2="17"/>
  389. <polyline points="10 9 9 9 8 9"/>
  390. </svg>
  391. ${t('exportCSV')}
  392. `;
  393. csvBtn.addEventListener('click', () => exportGPTs('csv'));
  394.  
  395. // Add clear data button
  396. const clearBtn = document.createElement('button');
  397. clearBtn.className = 'clear-btn';
  398. clearBtn.textContent = t('clearData');
  399. clearBtn.addEventListener('click', () => {
  400. if (confirm(t('clearData') + '?')) {
  401. clearPersistedData();
  402. gptsData = [];
  403. updateGPTsCounter(0);
  404. alert(t('dataCleared'));
  405. }
  406. });
  407.  
  408. container.appendChild(counter);
  409. container.appendChild(toggleBtn);
  410. container.appendChild(jsonBtn);
  411. container.appendChild(csvBtn);
  412. container.appendChild(clearBtn);
  413. document.body.appendChild(container);
  414. }
  415.  
  416. // Initialize
  417. if (document.readyState === 'loading') {
  418. document.addEventListener('DOMContentLoaded', addExportButton);
  419. } else {
  420. addExportButton();
  421. }
  422. })();