ChatGPT GPTs 导出工具

从 ChatGPT 导出你的 GPTs 数据

当前为 2025-02-21 提交的版本,查看 最新版本

  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.0
  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. // i18n support
  32. const i18n = {
  33. 'en': {
  34. exportBtn: 'Export GPTs',
  35. exportJSON: 'Export as JSON',
  36. exportCSV: 'Export as CSV',
  37. exporting: 'Exporting...',
  38. copied: 'Copied to clipboard!',
  39. noData: 'No GPTs data found',
  40. error: 'Error exporting GPTs'
  41. },
  42. 'zh-CN': {
  43. exportBtn: '导出 GPTs',
  44. exportJSON: '导出为 JSON',
  45. exportCSV: '导出为 CSV',
  46. exporting: '导出中...',
  47. copied: '已复制到剪贴板!',
  48. noData: '未找到 GPTs 数据',
  49. error: '导出 GPTs 时出错'
  50. },
  51. 'zh-TW': {
  52. exportBtn: '導出 GPTs',
  53. exportJSON: '導出為 JSON',
  54. exportCSV: '導出為 CSV',
  55. exporting: '導出中...',
  56. copied: '已複製到剪貼板!',
  57. noData: '未找到 GPTs 數據',
  58. error: '導出 GPTs 時出錯'
  59. }
  60. };
  61.  
  62. // Get user language
  63. const userLang = (navigator.language || 'en').toLowerCase();
  64. const lang = i18n[userLang] ? userLang :
  65. userLang.startsWith('zh-tw') ? 'zh-TW' :
  66. userLang.startsWith('zh') ? 'zh-CN' : 'en';
  67. const t = key => i18n[lang][key] || i18n.en[key];
  68.  
  69. // Intercept fetch requests
  70. const originalFetch = unsafeWindow.fetch;
  71. unsafeWindow.fetch = async function(resource, options) {
  72. const response = await originalFetch(resource, options);
  73. const url = typeof resource === 'string' ? resource : resource?.url;
  74.  
  75. // Check if this is the GPTs list request
  76. if (url && url.includes('/public-api/gizmos/discovery/mine')) {
  77. try {
  78. const clonedResponse = response.clone();
  79. const data = await clonedResponse.json();
  80. if (data?.list?.items) {
  81. gptsData = data.list.items.map(item => {
  82. const gpt = item.resource.gizmo;
  83. return {
  84. id: gpt.id,
  85. name: gpt.display.name || '',
  86. description: gpt.display.description || '',
  87. instructions: gpt.instructions || '',
  88. created_at: gpt.created_at,
  89. updated_at: gpt.updated_at,
  90. version: gpt.version,
  91. tools: item.resource.tools.map(tool => tool.type),
  92. prompt_starters: gpt.display.prompt_starters || [],
  93. share_recipient: gpt.share_recipient,
  94. num_interactions: gpt.num_interactions
  95. };
  96. });
  97. }
  98. } catch (error) {
  99. console.error('Error intercepting GPTs data:', error);
  100. }
  101. }
  102. return response;
  103. };
  104.  
  105. // Convert GPTs data to CSV
  106. function convertToCSV(data) {
  107. const headers = [
  108. 'ID',
  109. 'Name',
  110. 'Description',
  111. 'Instructions',
  112. 'Created At',
  113. 'Updated At',
  114. 'Version',
  115. 'Tools',
  116. 'Prompt Starters',
  117. 'Share Recipient',
  118. 'Interactions'
  119. ];
  120.  
  121. const rows = data.map(gpt => [
  122. gpt.id,
  123. `"${(gpt.name || '').replace(/"/g, '""')}"`,
  124. `"${(gpt.description || '').replace(/"/g, '""')}"`,
  125. `"${(gpt.instructions || '').replace(/"/g, '""')}"`,
  126. gpt.created_at,
  127. gpt.updated_at,
  128. gpt.version,
  129. `"${(gpt.tools || []).join(', ')}"`,
  130. `"${(gpt.prompt_starters || []).join(', ').replace(/"/g, '""')}"`,
  131. gpt.share_recipient,
  132. gpt.num_interactions
  133. ]);
  134.  
  135. return [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
  136. }
  137.  
  138. // Export GPTs data
  139. function exportGPTs(format = 'json') {
  140. if (isExporting) return;
  141. isExporting = true;
  142.  
  143. try {
  144. if (!gptsData.length) {
  145. alert(t('noData'));
  146. isExporting = false;
  147. return;
  148. }
  149.  
  150. let exportContent, filename, mimeType;
  151.  
  152. if (format === 'csv') {
  153. exportContent = convertToCSV(gptsData);
  154. filename = `chatgpt-gpts-export-${new Date().toISOString().split('T')[0]}.csv`;
  155. mimeType = 'text/csv';
  156. } else {
  157. const exportData = {
  158. exported_at: new Date().toISOString(),
  159. total_gpts: gptsData.length,
  160. gpts: gptsData
  161. };
  162. exportContent = JSON.stringify(exportData, null, 2);
  163. filename = `chatgpt-gpts-export-${new Date().toISOString().split('T')[0]}.json`;
  164. mimeType = 'application/json';
  165. }
  166.  
  167. GM_setClipboard(exportContent);
  168.  
  169. // Create and download file
  170. const blob = new Blob([exportContent], { type: mimeType });
  171. const url = URL.createObjectURL(blob);
  172. const a = document.createElement('a');
  173. a.href = url;
  174. a.download = filename;
  175. document.body.appendChild(a);
  176. a.click();
  177. document.body.removeChild(a);
  178. URL.revokeObjectURL(url);
  179.  
  180. alert(t('copied'));
  181. } catch (error) {
  182. console.error('Error exporting GPTs:', error);
  183. alert(t('error'));
  184. } finally {
  185. isExporting = false;
  186. }
  187. }
  188.  
  189. // Register menu commands
  190. GM_registerMenuCommand(t('exportJSON'), () => exportGPTs('json'));
  191. GM_registerMenuCommand(t('exportCSV'), () => exportGPTs('csv'));
  192.  
  193. // Add export button to UI
  194. function addExportButton() {
  195. const styles = document.createElement('style');
  196. styles.textContent = `
  197. .gpts-exporter {
  198. position: fixed;
  199. bottom: 20px;
  200. right: 20px;
  201. z-index: 10000;
  202. display: flex;
  203. flex-direction: column;
  204. gap: 8px;
  205. background: var(--surface-primary, rgba(255, 255, 255, 0.9));
  206. padding: 12px;
  207. border-radius: 8px;
  208. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  209. border: 1px solid var(--border-light, rgba(0, 0, 0, 0.1));
  210. transition: transform 0.2s ease, opacity 0.2s ease;
  211. }
  212. .gpts-exporter:hover {
  213. transform: translateY(-2px);
  214. }
  215. .gpts-exporter.collapsed {
  216. transform: translateX(calc(100% + 20px));
  217. }
  218. .gpts-exporter button {
  219. padding: 8px 16px;
  220. background-color: var(--success-color, #10a37f);
  221. color: #fff;
  222. border: none;
  223. border-radius: 4px;
  224. cursor: pointer;
  225. font-size: 14px;
  226. font-weight: 500;
  227. transition: all 0.2s ease;
  228. display: flex;
  229. align-items: center;
  230. justify-content: center;
  231. gap: 6px;
  232. min-width: 140px;
  233. }
  234. .gpts-exporter button:hover {
  235. opacity: 0.9;
  236. transform: translateY(-1px);
  237. }
  238. .gpts-exporter button:active {
  239. transform: translateY(0);
  240. }
  241. .gpts-exporter .toggle-btn {
  242. position: absolute;
  243. left: -32px;
  244. top: 50%;
  245. transform: translateY(-50%);
  246. width: 24px;
  247. height: 24px;
  248. background: var(--success-color, #10a37f);
  249. border: none;
  250. border-radius: 4px 0 0 4px;
  251. cursor: pointer;
  252. display: flex;
  253. align-items: center;
  254. justify-content: center;
  255. padding: 0;
  256. min-width: unset;
  257. }
  258. .gpts-exporter .toggle-btn svg {
  259. width: 16px;
  260. height: 16px;
  261. fill: #fff;
  262. transition: transform 0.2s ease;
  263. }
  264. .gpts-exporter.collapsed .toggle-btn svg {
  265. transform: rotate(180deg);
  266. }
  267. .gpts-exporter button.json-btn {
  268. background-color: var(--success-color, #10a37f);
  269. }
  270. .gpts-exporter button.csv-btn {
  271. background-color: var(--primary-color, #0ea5e9);
  272. }
  273. `;
  274. document.head.appendChild(styles);
  275.  
  276. const container = document.createElement('div');
  277. container.className = 'gpts-exporter';
  278.  
  279. const toggleBtn = document.createElement('button');
  280. toggleBtn.className = 'toggle-btn';
  281. 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>';
  282. toggleBtn.addEventListener('click', () => {
  283. container.classList.toggle('collapsed');
  284. });
  285.  
  286. const jsonBtn = document.createElement('button');
  287. jsonBtn.className = 'json-btn';
  288. jsonBtn.innerHTML = `
  289. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  290. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  291. <polyline points="7 10 12 15 17 10"/>
  292. <line x1="12" y1="15" x2="12" y2="3"/>
  293. </svg>
  294. ${t('exportJSON')}
  295. `;
  296. jsonBtn.addEventListener('click', () => exportGPTs('json'));
  297.  
  298. const csvBtn = document.createElement('button');
  299. csvBtn.className = 'csv-btn';
  300. csvBtn.innerHTML = `
  301. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  302. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
  303. <polyline points="14 2 14 8 20 8"/>
  304. <line x1="16" y1="13" x2="8" y2="13"/>
  305. <line x1="16" y1="17" x2="8" y2="17"/>
  306. <polyline points="10 9 9 9 8 9"/>
  307. </svg>
  308. ${t('exportCSV')}
  309. `;
  310. csvBtn.addEventListener('click', () => exportGPTs('csv'));
  311.  
  312. container.appendChild(toggleBtn);
  313. container.appendChild(jsonBtn);
  314. container.appendChild(csvBtn);
  315. document.body.appendChild(container);
  316. }
  317.  
  318. // Initialize
  319. if (document.readyState === 'loading') {
  320. document.addEventListener('DOMContentLoaded', addExportButton);
  321. } else {
  322. addExportButton();
  323. }
  324. })();