Jina Reader - Copy LLM Format

Copy current page as LLM-friendly format using Jina Reader API

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Jina Reader - Copy LLM Format
// @namespace    https://github.com/kouni/jinasnap
// @version      2.1.6
// @description  Copy current page as LLM-friendly format using Jina Reader API
// @author       Kouni
// @license      GPL-2.0-or-later
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @connect      r.jina.ai
// ==/UserScript==

(function() {
    'use strict';
    
    // Configuration
    const CONFIG = {
        API_ENDPOINT: 'https://r.jina.ai/',
        TIMEOUT: 30000,
        NOTIFICATION_DURATION: 3000,
        FADE_DURATION: 300,
        MIN_CONTENT_LENGTH: 10,
        REJECT_PATTERNS: [
            'page not found',
            'error 404',
            'error 403',
            'error 500',
            'access denied',
            'unauthorized',
            'forbidden',
            'protected by recaptcha',
            'captcha verification required',
            'please verify you are human',
            'authentication required',
            'login required'
        ]
    };
    
    // State management
    let isProcessing = false;
    let currentNotification = null;
    
    // Inject CSS styles
    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .jina-reader-notification {
                position: fixed;
                top: 20px;
                right: 20px;
                z-index: 10000;
                padding: 12px 16px;
                border-radius: 6px;
                font-size: 14px;
                font-weight: 500;
                box-shadow: 0 4px 12px rgba(0,0,0,0.2);
                transition: opacity ${CONFIG.FADE_DURATION}ms ease;
                opacity: 1;
            }
            .jina-reader-notification.jina-reader-success {
                background: #d4edda; color: #155724; border: 1px solid #c3e6cb;
            }
            .jina-reader-notification.jina-reader-error {
                background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;
            }
            .jina-reader-notification.jina-reader-warning {
                background: #fff3cd; color: #856404; border: 1px solid #ffeaa7;
            }
            .jina-reader-notification.jina-reader-info {
                background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb;
            }
        `;
        document.head.appendChild(style);
    }

    // Register menu command
    GM_registerMenuCommand('📄 Copy as LLM Format', copyCurrentPage);
    
    // Main function to copy current page
    function copyCurrentPage() {
        // Prevent multiple simultaneous requests
        if (isProcessing) {
            showNotification('⚠️ Request already in progress...', 'warning');
            return;
        }
        
        isProcessing = true;
        const currentUrl = window.top.location.href;
        
        showNotification('🔄 Converting page...', 'info');
        logDebug('Starting conversion for URL:', currentUrl);
        
        GM_xmlhttpRequest({
            method: 'GET',
            url: CONFIG.API_ENDPOINT + currentUrl,
            headers: {
                'Accept': 'text/plain',
                'User-Agent': 'Mozilla/5.0 (compatible; Jina-Reader-UserScript)'
            },
            timeout: CONFIG.TIMEOUT,
            onload: function(response) {
                isProcessing = false;
                handleApiResponse(response);
            },
            onerror: function(error) {
                isProcessing = false;
                logError('Network error:', error);
                showNotification('❌ Network error occurred', 'error');
            },
            ontimeout: function() {
                isProcessing = false;
                logError('Request timeout for URL:', currentUrl);
                showNotification('❌ Request timeout', 'error');
            }
        });
    }
    
    // Handle API response
    function handleApiResponse(response) {
        logDebug('API response status:', response.status);
        
        if (response.status === 200) {
            const content = response.responseText;
            
            // Validate content
            if (!isValidContent(content)) {
                logError('Invalid content received:', content.substring(0, 100));
                showNotification('❌ Invalid or empty response', 'error');
                return;
            }
            
            GM_setClipboard(content);
            showNotification('✅ Content copied to clipboard!', 'success');
            logDebug('Content copied successfully, length:', content.length);
        } else {
            logError('API error, status:', response.status);
            showNotification(`❌ Failed to convert page (${response.status})`, 'error');
        }
    }
    
    // Validate content
    function isValidContent(content) {
        if (!content || typeof content !== 'string') {
            return false;
        }
        
        const trimmed = content.trim();
        const lowerContent = trimmed.toLowerCase();
        
        // Check minimum length
        if (trimmed.length < CONFIG.MIN_CONTENT_LENGTH) {
            return false;
        }
        
        // Only reject obvious error/protected pages
        for (const pattern of CONFIG.REJECT_PATTERNS) {
            if (lowerContent.includes(pattern)) {
                return false;
            }
        }
        
        return true;
    }
    
    // Debug logging
    function logDebug(message, ...args) {
        console.log(`[Jina Reader] ${message}`, ...args);
    }
    
    // Error logging
    function logError(message, ...args) {
        console.error(`[Jina Reader] ${message}`, ...args);
    }
    
    // Show notification with deduplication
    function showNotification(message, type = 'info') {
        // Clear existing notification
        if (currentNotification && document.body.contains(currentNotification)) {
            document.body.removeChild(currentNotification);
        }
        
        const notification = document.createElement('div');
        notification.className = `jina-reader-notification jina-reader-${type}`;
        notification.textContent = message;
        document.body.appendChild(notification);
        
        // Store reference to current notification
        currentNotification = notification;
        
        // Auto-dismiss after configured duration
        setTimeout(() => {
            if (currentNotification === notification) {
                notification.style.opacity = '0';
                setTimeout(() => {
                    if (document.body.contains(notification)) {
                        document.body.removeChild(notification);
                    }
                    if (currentNotification === notification) {
                        currentNotification = null;
                    }
                }, CONFIG.FADE_DURATION);
            }
        }, CONFIG.NOTIFICATION_DURATION);
    }
    
    // Keyboard shortcut (Ctrl/Cmd + Shift + R)
    document.addEventListener('keydown', function(e) {
        if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'R') {
            e.preventDefault();
            copyCurrentPage();
        }
    });

    // Initialize
    injectStyles();
})();