Ctrl K
ComunidadGuías…

routes/chatgpt_import.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
routes/chatgpt_import.py — ChatGPT Conversation Import Blueprint

Endpoints:
  POST /api/import/chatgpt              — Upload and parse a ChatGPT export ZIP
  GET  /api/import/chatgpt/conversations — List all imported conversations
  GET  /api/import/chatgpt/conversation/<id> — Get a single conversation markdown
  GET  /api/import/chatgpt/status        — Get import status/stats
"""

import json
import logging
import uuid
from pathlib import Path

from flask import Blueprint, jsonify, request

logger = logging.getLogger(__name__)

chatgpt_import_bp = Blueprint('chatgpt_import', __name__)

# ---------------------------------------------------------------------------
# Paths — imports dir lives alongside uploads in the runtime directory
# ---------------------------------------------------------------------------

from services.paths import UPLOADS_DIR, WORKSPACE_DIR

# Imported conversations go to workspace/memory/chatgpt-import/
# This makes them accessible to the OpenClaw agent for reference
IMPORT_DIR = WORKSPACE_DIR / 'memory' / 'chatgpt-import'

_MAX_UPLOAD_BYTES = 500 * 1024 * 1024  # 500 MB — exports can be large


@chatgpt_import_bp.route('/api/import/chatgpt', methods=['POST'])
def import_chatgpt():
    """Accept a ChatGPT export ZIP, parse it, store conversations."""
    from services.chatgpt_parser import parse_chatgpt_export

    if 'file' not in request.files:
        return jsonify({'error': 'No file provided'}), 400

    f = request.files['file']
    if not f.filename:
        return jsonify({'error': 'Empty filename'}), 400

    ext = Path(f.filename).suffix.lower()
    if ext != '.zip':
        return jsonify({'error': 'Only ZIP files are accepted. Export your data from ChatGPT Settings > Data Controls > Export.'}), 415

    # Size check
    f.stream.seek(0, 2)
    file_size = f.stream.tell()
    f.stream.seek(0)
    if file_size > _MAX_UPLOAD_BYTES:
        return jsonify({'error': 'File too large (500 MB max)'}), 413

    # Save ZIP to uploads temporarily
    UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
    zip_name = f"chatgpt-export-{uuid.uuid4().hex[:8]}.zip"
    zip_path = UPLOADS_DIR / zip_name
    f.save(str(zip_path))

    # Parse and write conversations
    IMPORT_DIR.mkdir(parents=True, exist_ok=True)
    result = parse_chatgpt_export(zip_path, IMPORT_DIR)

    return jsonify({
        'status': 'complete',
        'total': result['total'],
        'imported': result['imported'],
        'skipped': result['skipped'],
        'errors': result['errors'][:20],  # Cap error messages
        'conversations': result['conversations'],
    })


@chatgpt_import_bp.route('/api/import/chatgpt/conversations', methods=['GET'])
def list_imported_conversations():
    """List all imported ChatGPT conversations from the index."""
    index_path = IMPORT_DIR / 'index.json'
    if not index_path.exists():
        return jsonify({'conversations': [], 'total': 0})

    try:
        index = json.loads(index_path.read_text())
    except (json.JSONDecodeError, OSError):
        return jsonify({'conversations': [], 'total': 0})

    conversations = index.get('conversations', [])

    # Support search query
    q = request.args.get('q', '').strip().lower()
    if q:
        conversations = [
            c for c in conversations
            if q in c.get('title', '').lower()
            or q in ' '.join(c.get('topics', []))
        ]

    # Support topic filter
    topic = request.args.get('topic', '').strip().lower()
    if topic:
        conversations = [
            c for c in conversations
            if topic in [t.lower() for t in c.get('topics', [])]
        ]

    return jsonify({
        'conversations': conversations,
        'total': len(conversations),
        'imported_at': index.get('imported_at'),
    })


@chatgpt_import_bp.route('/api/import/chatgpt/conversation/<path:filename>', methods=['GET'])
def get_imported_conversation(filename):
    """Get the markdown content of a single imported conversation."""
    # Prevent path traversal
    safe_name = Path(filename).name
    file_path = IMPORT_DIR / safe_name

    if not file_path.exists() or not file_path.suffix == '.md':
        return jsonify({'error': 'Conversation not found'}), 404

    try:
        content = file_path.read_text(encoding='utf-8')
    except OSError:
        return jsonify({'error': 'Could not read conversation file'}), 500

    return jsonify({
        'filename': safe_name,
        'content': content,
    })


@chatgpt_import_bp.route('/api/import/chatgpt/status', methods=['GET'])
def import_status():
    """Get current import status and stats."""
    index_path = IMPORT_DIR / 'index.json'
    if not index_path.exists():
        return jsonify({
            'has_import': False,
            'total_conversations': 0,
            'imported_at': None,
        })

    try:
        index = json.loads(index_path.read_text())
    except (json.JSONDecodeError, OSError):
        return jsonify({'has_import': False, 'total_conversations': 0, 'imported_at': None})

    conversations = index.get('conversations', [])

    # Aggregate topic counts
    topic_counts = {}
    total_words = 0
    total_messages = 0
    for c in conversations:
        for t in c.get('topics', []):
            topic_counts[t] = topic_counts.get(t, 0) + 1
        total_words += c.get('word_count', 0)
        total_messages += c.get('message_count', 0)

    return jsonify({
        'has_import': True,
        'total_conversations': len(conversations),
        'imported_at': index.get('imported_at'),
        'total_words': total_words,
        'total_messages': total_messages,
        'topics': dict(sorted(topic_counts.items(), key=lambda x: -x[1])),
    })