ChatGPT GPTs 導出工具

從 ChatGPT 導出你的 GPTs 數據

目前為 2025-02-21 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ChatGPT GPTs Exporter
// @name:zh-CN   ChatGPT GPTs 导出工具
// @name:zh-TW   ChatGPT GPTs 導出工具
// @namespace    https://github.com/lroolle/chatgpt-degraded
// @version      0.1.0
// @description  Export your GPTs data from ChatGPT
// @description:zh-CN  从 ChatGPT 导出你的 GPTs 数据
// @description:zh-TW  從 ChatGPT 導出你的 GPTs 數據
// @author       lroolle
// @license      AGPL-3.0
// @match        *://chat.openai.com/gpts/*
// @match        *://chatgpt.com/gpts/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @grant        unsafeWindow
// @run-at       document-start
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojMmE5ZDhmO3N0b3Atb3BhY2l0eToxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3R5bGU9InN0b3AtY29sb3I6IzJhOWQ4ZjtzdG9wLW9wYWNpdHk6MC44Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Zz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjI4IiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KPCEtLU91dGVyIGNpcmNsZSBtb2RpZmllZCB0byBsb29rIGxpa2UgIkMiLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEyNSA1NSIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjIwIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjQiIGZpbGw9IiNmZmYiLz4KICA8L2c+Cjwvc3ZnPg==
// @homepageURL  https://github.com/lroolle/chatgpt-degraded
// @supportURL   https://github.com/lroolle/chatgpt-degraded/issues
// ==/UserScript==

(function() {
  'use strict';

  // Store GPTs data
  let gptsData = [];
  let isExporting = false;

  // i18n support
  const i18n = {
    'en': {
      exportBtn: 'Export GPTs',
      exportJSON: 'Export as JSON',
      exportCSV: 'Export as CSV',
      exporting: 'Exporting...',
      copied: 'Copied to clipboard!',
      noData: 'No GPTs data found',
      error: 'Error exporting GPTs'
    },
    'zh-CN': {
      exportBtn: '导出 GPTs',
      exportJSON: '导出为 JSON',
      exportCSV: '导出为 CSV',
      exporting: '导出中...',
      copied: '已复制到剪贴板!',
      noData: '未找到 GPTs 数据',
      error: '导出 GPTs 时出错'
    },
    'zh-TW': {
      exportBtn: '導出 GPTs',
      exportJSON: '導出為 JSON',
      exportCSV: '導出為 CSV',
      exporting: '導出中...',
      copied: '已複製到剪貼板!',
      noData: '未找到 GPTs 數據',
      error: '導出 GPTs 時出錯'
    }
  };

  // Get user language
  const userLang = (navigator.language || 'en').toLowerCase();
  const lang = i18n[userLang] ? userLang : 
               userLang.startsWith('zh-tw') ? 'zh-TW' :
               userLang.startsWith('zh') ? 'zh-CN' : 'en';
  const t = key => i18n[lang][key] || i18n.en[key];

  // Intercept fetch requests
  const originalFetch = unsafeWindow.fetch;
  unsafeWindow.fetch = async function(resource, options) {
    const response = await originalFetch(resource, options);
    const url = typeof resource === 'string' ? resource : resource?.url;

    // Check if this is the GPTs list request
    if (url && url.includes('/public-api/gizmos/discovery/mine')) {
      try {
        const clonedResponse = response.clone();
        const data = await clonedResponse.json();
        if (data?.list?.items) {
          gptsData = data.list.items.map(item => {
            const gpt = item.resource.gizmo;
            return {
              id: gpt.id,
              name: gpt.display.name || '',
              description: gpt.display.description || '',
              instructions: gpt.instructions || '',
              created_at: gpt.created_at,
              updated_at: gpt.updated_at,
              version: gpt.version,
              tools: item.resource.tools.map(tool => tool.type),
              prompt_starters: gpt.display.prompt_starters || [],
              share_recipient: gpt.share_recipient,
              num_interactions: gpt.num_interactions
            };
          });
        }
      } catch (error) {
        console.error('Error intercepting GPTs data:', error);
      }
    }
    return response;
  };

  // Convert GPTs data to CSV
  function convertToCSV(data) {
    const headers = [
      'ID',
      'Name',
      'Description',
      'Instructions',
      'Created At',
      'Updated At',
      'Version',
      'Tools',
      'Prompt Starters',
      'Share Recipient',
      'Interactions'
    ];

    const rows = data.map(gpt => [
      gpt.id,
      `"${(gpt.name || '').replace(/"/g, '""')}"`,
      `"${(gpt.description || '').replace(/"/g, '""')}"`,
      `"${(gpt.instructions || '').replace(/"/g, '""')}"`,
      gpt.created_at,
      gpt.updated_at,
      gpt.version,
      `"${(gpt.tools || []).join(', ')}"`,
      `"${(gpt.prompt_starters || []).join(', ').replace(/"/g, '""')}"`,
      gpt.share_recipient,
      gpt.num_interactions
    ]);

    return [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
  }

  // Export GPTs data
  function exportGPTs(format = 'json') {
    if (isExporting) return;
    isExporting = true;

    try {
      if (!gptsData.length) {
        alert(t('noData'));
        isExporting = false;
        return;
      }

      let exportContent, filename, mimeType;

      if (format === 'csv') {
        exportContent = convertToCSV(gptsData);
        filename = `chatgpt-gpts-export-${new Date().toISOString().split('T')[0]}.csv`;
        mimeType = 'text/csv';
      } else {
        const exportData = {
          exported_at: new Date().toISOString(),
          total_gpts: gptsData.length,
          gpts: gptsData
        };
        exportContent = JSON.stringify(exportData, null, 2);
        filename = `chatgpt-gpts-export-${new Date().toISOString().split('T')[0]}.json`;
        mimeType = 'application/json';
      }

      GM_setClipboard(exportContent);

      // Create and download file
      const blob = new Blob([exportContent], { type: mimeType });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);

      alert(t('copied'));
    } catch (error) {
      console.error('Error exporting GPTs:', error);
      alert(t('error'));
    } finally {
      isExporting = false;
    }
  }

  // Register menu commands
  GM_registerMenuCommand(t('exportJSON'), () => exportGPTs('json'));
  GM_registerMenuCommand(t('exportCSV'), () => exportGPTs('csv'));

  // Add export button to UI
  function addExportButton() {
    const styles = document.createElement('style');
    styles.textContent = `
      .gpts-exporter {
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 10000;
        display: flex;
        flex-direction: column;
        gap: 8px;
        background: var(--surface-primary, rgba(255, 255, 255, 0.9));
        padding: 12px;
        border-radius: 8px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        border: 1px solid var(--border-light, rgba(0, 0, 0, 0.1));
        transition: transform 0.2s ease, opacity 0.2s ease;
      }
      .gpts-exporter:hover {
        transform: translateY(-2px);
      }
      .gpts-exporter.collapsed {
        transform: translateX(calc(100% + 20px));
      }
      .gpts-exporter button {
        padding: 8px 16px;
        background-color: var(--success-color, #10a37f);
        color: #fff;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
        font-weight: 500;
        transition: all 0.2s ease;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 6px;
        min-width: 140px;
      }
      .gpts-exporter button:hover {
        opacity: 0.9;
        transform: translateY(-1px);
      }
      .gpts-exporter button:active {
        transform: translateY(0);
      }
      .gpts-exporter .toggle-btn {
        position: absolute;
        left: -32px;
        top: 50%;
        transform: translateY(-50%);
        width: 24px;
        height: 24px;
        background: var(--success-color, #10a37f);
        border: none;
        border-radius: 4px 0 0 4px;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        min-width: unset;
      }
      .gpts-exporter .toggle-btn svg {
        width: 16px;
        height: 16px;
        fill: #fff;
        transition: transform 0.2s ease;
      }
      .gpts-exporter.collapsed .toggle-btn svg {
        transform: rotate(180deg);
      }
      .gpts-exporter button.json-btn {
        background-color: var(--success-color, #10a37f);
      }
      .gpts-exporter button.csv-btn {
        background-color: var(--primary-color, #0ea5e9);
      }
    `;
    document.head.appendChild(styles);

    const container = document.createElement('div');
    container.className = 'gpts-exporter';

    const toggleBtn = document.createElement('button');
    toggleBtn.className = 'toggle-btn';
    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>';
    toggleBtn.addEventListener('click', () => {
      container.classList.toggle('collapsed');
    });

    const jsonBtn = document.createElement('button');
    jsonBtn.className = 'json-btn';
    jsonBtn.innerHTML = `
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
        <polyline points="7 10 12 15 17 10"/>
        <line x1="12" y1="15" x2="12" y2="3"/>
      </svg>
      ${t('exportJSON')}
    `;
    jsonBtn.addEventListener('click', () => exportGPTs('json'));

    const csvBtn = document.createElement('button');
    csvBtn.className = 'csv-btn';
    csvBtn.innerHTML = `
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
        <polyline points="14 2 14 8 20 8"/>
        <line x1="16" y1="13" x2="8" y2="13"/>
        <line x1="16" y1="17" x2="8" y2="17"/>
        <polyline points="10 9 9 9 8 9"/>
      </svg>
      ${t('exportCSV')}
    `;
    csvBtn.addEventListener('click', () => exportGPTs('csv'));

    container.appendChild(toggleBtn);
    container.appendChild(jsonBtn);
    container.appendChild(csvBtn);
    document.body.appendChild(container);
  }

  // Initialize
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', addExportButton);
  } else {
    addExportButton();
  }
})();