网页列表数据提取器 (增强版 - 带预览功能)

在任何页面上通过一个可交互的、可拖动的界面,输入多个CSS选择器,预览提取的数据,并将匹配到的内容导出为CSV表格文件。

// ==UserScript==
// @name         网页列表数据提取器 (增强版 - 带预览功能)
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  在任何页面上通过一个可交互的、可拖动的界面,输入多个CSS选择器,预览提取的数据,并将匹配到的内容导出为CSV表格文件。
// @author       Kamjin3086
// @license      MIT
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // 1. 注册一个菜单命令,用来打开提取器界面
    GM_registerMenuCommand('打开内容提取器', toggleExtractorPanel);

    // 2. 为界面添加CSS样式
    GM_addStyle(`
        #gm-extractor-panel {
            position: fixed;
            top: 60px;
            right: 15px;
            width: 450px;
            background-color: #ffffff;
            border: 2px solid #4A90E2;
            border-radius: 10px;
            z-index: 99999;
            box-shadow: 0 6px 20px rgba(0,0,0,0.15);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            display: none;
            flex-direction: column;
            max-height: 90vh;
            overflow: hidden;
        }
        
        #gm-extractor-panel .extractor-header {
            padding: 12px 16px;
            background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
            color: white;
            font-weight: 600;
            font-size: 15px;
            border-top-left-radius: 8px;
            border-top-right-radius: 8px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            cursor: move;
            user-select: none;
        }
        
        #gm-extractor-panel .header-buttons {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        #gm-extractor-panel .header-btn {
            background-color: rgba(255, 255, 255, 0.2);
            border: none;
            color: white;
            padding: 4px 8px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            transition: background-color 0.2s ease;
        }
        
        #gm-extractor-panel .header-btn:hover {
            background-color: rgba(255, 255, 255, 0.3);
        }
        
        #gm-extractor-panel .extractor-body {
            padding: 12px 16px;
            flex: 1;
            overflow-y: auto;
            background-color: #fafafa;
        }
        
        #gm-extractor-panel details {
            border: 1px solid #e0e0e0;
            border-radius: 6px;
            margin-bottom: 12px;
            background-color: white;
            box-shadow: 0 1px 3px rgba(0,0,0,0.05);
        }
        
        #gm-extractor-panel summary {
            padding: 10px 12px;
            font-weight: 600;
            background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
            cursor: pointer;
            outline: none;
            border-radius: 6px;
            transition: all 0.2s ease;
            font-size: 14px;
        }
        
        #gm-extractor-panel summary:hover {
            background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
        }
        
        #gm-extractor-panel .instructions {
            padding: 16px;
            font-size: 14px;
            line-height: 1.6;
            background: white;
            border-bottom-left-radius: 8px;
            border-bottom-right-radius: 8px;
        }
        
        #gm-extractor-panel .instructions ul {
            padding-left: 20px;
            margin: 0;
        }
        
        #gm-extractor-panel .instructions code {
            background-color: #f1f3f4;
            padding: 3px 6px;
            border-radius: 4px;
            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
            font-size: 13px;
            color: #d63384;
        }
        
        #gm-extractor-panel .instructions li {
            margin-bottom: 10px;
        }
        
        #gm-extractor-panel .instructions .pro-tip {
            border-top: 1px dashed #dee2e6;
            margin-top: 15px;
            padding-top: 15px;
            font-size: 13px;
            color: #6c757d;
            background-color: #f8f9fa;
            padding: 12px;
            border-radius: 6px;
        }
        
        #gm-extractor-panel .selector-optimize {
            margin-top: 8px;
            padding: 10px 12px;
            background-color: #fff3cd;
            border: 1px solid #ffeaa7;
            border-radius: 6px;
            font-size: 12px;
            color: #856404;
        }
        
        #gm-extractor-panel .optimize-btn {
            background-color: #17a2b8;
            color: white;
            border: none;
            padding: 4px 8px;
            border-radius: 3px;
            cursor: pointer;
            font-size: 11px;
            margin-left: 8px;
        }
        
        #gm-extractor-panel .optimize-btn:hover {
            background-color: #138496;
        }
        
        #gm-extractor-panel .optimize-options {
            margin-top: 8px;
            padding: 8px;
            background-color: #f8f9fa;
            border-radius: 4px;
            border: 1px solid #dee2e6;
        }
        
        #gm-extractor-panel .optimize-option {
            display: inline-block;
            margin: 2px 4px 2px 0;
            padding: 4px 8px;
            background-color: #e9ecef;
            border: 1px solid #ced4da;
            border-radius: 3px;
            cursor: pointer;
            font-size: 11px;
            transition: all 0.2s ease;
        }
        
        #gm-extractor-panel .optimize-option:hover {
            background-color: #17a2b8;
            color: white;
        }
        
        #gm-extractor-panel .optimize-option.selected {
            background-color: #007bff;
            color: white;
        }
        
        #gm-extractor-panel .validation-result {
            margin-top: 8px;
            padding: 6px 10px;
            border-radius: 4px;
            font-size: 12px;
        }
        
        #gm-extractor-panel .validation-success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        
        #gm-extractor-panel .validation-warning {
            background-color: #fff3cd;
            color: #856404;
            border: 1px solid #ffeaa7;
        }
        
        #gm-extractor-panel .validation-error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        
        #gm-extractor-panel .selector-row {
            margin-bottom: 10px;
            padding: 10px;
            background-color: white;
            border-radius: 6px;
            border: 1px solid #e0e0e0;
            box-shadow: 0 1px 2px rgba(0,0,0,0.05);
        }
        
        #gm-extractor-panel .selector-input-row {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        #gm-extractor-panel .selector-input {
            flex-grow: 1;
            padding: 8px 10px;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            font-size: 13px;
            transition: border-color 0.2s ease;
            background-color: white;
        }
        
        #gm-extractor-panel .selector-input:focus {
            outline: none;
            border-color: #4A90E2;
            box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
        }
        
        #gm-extractor-panel .add-btn {
            padding: 6px 8px;
            cursor: pointer;
            background-color: #28a745;
            color: white;
            border: none;
            border-radius: 4px;
            font-weight: 600;
            transition: background-color 0.2s ease;
            min-width: 32px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 16px;
        }
        
        #gm-extractor-panel .add-btn:hover {
            background-color: #218838;
        }
        
        #gm-extractor-panel .remove-btn {
            padding: 6px 8px;
            cursor: pointer;
            background-color: #dc3545;
            color: white;
            border: none;
            border-radius: 4px;
            font-weight: 600;
            transition: background-color 0.2s ease;
            min-width: 32px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 16px;
        }
        
        #gm-extractor-panel .remove-btn:hover {
            background-color: #c82333;
        }
        
        #gm-extractor-panel .preview-section {
            margin-top: 12px;
            padding: 12px;
            background-color: white;
            border: 1px solid #e0e0e0;
            border-radius: 6px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.05);
        }
        
        #gm-extractor-panel .preview-title {
            font-weight: 600;
            margin-bottom: 8px;
            color: #495057;
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 14px;
        }
        
        #gm-extractor-panel .preview-table {
            width: 100%;
            border-collapse: collapse;
            font-size: 12px;
            background-color: white;
        }
        
        #gm-extractor-panel .preview-table th {
            background-color: #f8f9fa;
            padding: 6px 8px;
            text-align: left;
            font-weight: 600;
            border: 1px solid #dee2e6;
            color: #495057;
            font-size: 11px;
        }
        
        #gm-extractor-panel .preview-table td {
            padding: 6px 8px;
            border: 1px solid #dee2e6;
            max-width: 120px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            font-size: 11px;
        }
        
        #gm-extractor-panel .preview-table tr:nth-child(even) {
            background-color: #f8f9fa;
        }
        
        #gm-extractor-panel .preview-info {
            margin-top: 10px;
            padding: 8px 12px;
            background-color: #e3f2fd;
            border-radius: 4px;
            font-size: 12px;
            color: #1976d2;
        }
        
        #gm-extractor-panel .extractor-footer {
            padding: 12px 16px;
            border-top: 1px solid #e0e0e0;
            background-color: white;
            border-bottom-left-radius: 8px;
            border-bottom-right-radius: 8px;
        }
        
        #gm-extractor-panel .footer-actions {
            display: flex;
            gap: 8px;
            justify-content: center;
        }
        
        #gm-extractor-panel button {
            padding: 8px 14px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: 600;
            font-size: 13px;
            transition: all 0.2s ease;
            min-width: 90px;
        }
        
        #gm-extractor-panel .footer-actions button {
            min-width: 100px;
            padding: 10px 16px;
        }
        
        #gm-extractor-panel #add-selector-btn {
            background-color: #28a745;
            color: white;
        }
        
        #gm-extractor-panel #add-selector-btn:hover {
            background-color: #218838;
            transform: translateY(-1px);
        }
        
        #gm-extractor-panel #preview-btn {
            background-color: #ffc107;
            color: #212529;
        }
        
        #gm-extractor-panel #preview-btn:hover {
            background-color: #e0a800;
            transform: translateY(-1px);
        }
        
        #gm-extractor-panel #export-csv-btn {
            background-color: #007bff;
            color: white;
        }
        
        #gm-extractor-panel #export-csv-btn:hover {
            background-color: #0056b3;
            transform: translateY(-1px);
        }
        
        #gm-extractor-panel #close-panel-btn {
            cursor: pointer;
            font-size: 24px;
            font-weight: bold;
            opacity: 0.8;
            transition: opacity 0.2s ease;
            padding: 0;
            min-width: auto;
            background: none;
            color: white;
        }
        
        #gm-extractor-panel #close-panel-btn:hover {
            opacity: 1;
        }
        
        .status-indicator {
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 6px;
        }
        
        .status-success { background-color: #28a745; }
        .status-warning { background-color: #ffc107; }
        .status-error { background-color: #dc3545; }
        
        #gm-extractor-panel .accumulation-controls {
            margin-bottom: 12px;
            padding: 10px;
            background-color: #e3f2fd;
            border: 1px solid #bbdefb;
            border-radius: 6px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        #gm-extractor-panel .accumulation-toggle {
            display: flex;
            align-items: center;
            gap: 6px;
            font-size: 13px;
            color: #1976d2;
        }
        
        #gm-extractor-panel .toggle-switch {
            position: relative;
            width: 40px;
            height: 20px;
            background-color: #ccc;
            border-radius: 20px;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        
        #gm-extractor-panel .toggle-switch.active {
            background-color: #4caf50;
        }
        
        #gm-extractor-panel .toggle-slider {
            position: absolute;
            top: 2px;
            left: 2px;
            width: 16px;
            height: 16px;
            background-color: white;
            border-radius: 50%;
            transition: transform 0.3s;
        }
        
        #gm-extractor-panel .toggle-switch.active .toggle-slider {
            transform: translateX(20px);
        }
        
        #gm-extractor-panel .accumulation-info {
            flex: 1;
            font-size: 12px;
            color: #666;
        }
        
        #gm-extractor-panel .clear-btn {
            background-color: #ff9800;
            color: white;
            border: none;
            padding: 4px 8px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 11px;
            font-weight: 600;
        }
        
        #gm-extractor-panel .clear-btn:hover {
            background-color: #f57c00;
        }
        
        #gm-extractor-panel .group-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            display: none;
            justify-content: center;
            align-items: center;
            z-index: 10000;
        }
        
        #gm-extractor-panel .group-modal.open {
            display: flex;
        }
        
        #gm-extractor-panel .group-modal-content {
            background-color: white;
            border-radius: 8px;
            padding: 20px;
            max-width: 500px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
        }
        
        #gm-extractor-panel .group-modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 1px solid #e0e0e0;
        }
        
        #gm-extractor-panel .group-modal-title {
            font-size: 18px;
            font-weight: 600;
            color: #333;
        }
        
        #gm-extractor-panel .group-modal-close {
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            color: #666;
            padding: 0;
            width: 30px;
            height: 30px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        #gm-extractor-panel .group-modal-close:hover {
            color: #333;
        }
        
        #gm-extractor-panel .group-form {
            margin-bottom: 20px;
        }
        
        #gm-extractor-panel .group-form input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
            margin-bottom: 10px;
        }
        
        #gm-extractor-panel .group-form button {
            background-color: #4A90E2;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        
        #gm-extractor-panel .group-form button:hover {
            background-color: #357ABD;
        }
        
        #gm-extractor-panel .group-list {
            max-height: 300px;
            overflow-y: auto;
        }
        
        #gm-extractor-panel .group-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            margin-bottom: 8px;
            background-color: #f9f9f9;
        }
        
        #gm-extractor-panel .group-item:hover {
            background-color: #f0f0f0;
        }
        
        #gm-extractor-panel .group-item-info {
            flex: 1;
        }
        
        #gm-extractor-panel .group-item-name {
            font-weight: 600;
            color: #333;
            margin-bottom: 4px;
        }
        
        #gm-extractor-panel .group-item-path {
            font-size: 12px;
            color: #666;
        }
        
        #gm-extractor-panel .group-item-actions {
            display: flex;
            gap: 8px;
        }
        
        #gm-extractor-panel .group-item-btn {
            padding: 4px 8px;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
        }
        
        #gm-extractor-panel .group-load-btn {
            background-color: #28a745;
            color: white;
        }
        
        #gm-extractor-panel .group-load-btn:hover {
            background-color: #218838;
        }
        
        #gm-extractor-panel .group-delete-btn {
            background-color: #dc3545;
            color: white;
        }
        
        #gm-extractor-panel .group-delete-btn:hover {
            background-color: #c82333;
        }
        
        #gm-extractor-panel .groups-sidebar {
            position: absolute;
            left: -180px;
            top: 0;
            width: 180px;
            height: 100%;
            background-color: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 8px 0 0 8px;
            padding: 12px;
            overflow-y: auto;
            transition: left 0.3s ease;
            z-index: 1;
        }
        
        #gm-extractor-panel .groups-sidebar.open {
            left: 0;
        }
        
        #gm-extractor-panel .groups-toggle {
            position: absolute;
            left: -20px;
            top: 20px;
            width: 20px;
            height: 40px;
            background-color: #6c757d;
            border-radius: 4px 0 0 4px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 12px;
            transition: background-color 0.3s;
        }
        
        #gm-extractor-panel .groups-toggle:hover {
            background-color: #5a6268;
        }
        
        #gm-extractor-panel .groups-header {
            font-weight: 600;
            margin-bottom: 10px;
            color: #495057;
            font-size: 14px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        
        #gm-extractor-panel .group-item {
            background-color: white;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
            padding: 8px;
            margin-bottom: 6px;
            cursor: pointer;
            transition: all 0.2s ease;
            position: relative;
        }
        
        #gm-extractor-panel .group-item:hover {
            background-color: #e3f2fd;
            border-color: #4A90E2;
        }
        
        #gm-extractor-panel .group-name {
            font-weight: 600;
            font-size: 12px;
            color: #495057;
            margin-bottom: 2px;
        }
        
        #gm-extractor-panel .group-path {
            font-size: 10px;
            color: #6c757d;
            margin-bottom: 2px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        
        #gm-extractor-panel .group-info {
            font-size: 10px;
            color: #28a745;
        }
        
        #gm-extractor-panel .group-delete {
            position: absolute;
            top: 4px;
            right: 4px;
            width: 16px;
            height: 16px;
            background-color: #dc3545;
            color: white;
            border: none;
            border-radius: 2px;
            cursor: pointer;
            font-size: 10px;
            display: none;
            align-items: center;
            justify-content: center;
        }
        
        #gm-extractor-panel .group-item:hover .group-delete {
            display: flex;
        }
        
        #gm-extractor-panel .save-group-btn {
            background-color: #17a2b8;
            color: white;
            border: none;
            padding: 6px 10px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 11px;
            font-weight: 600;
            margin-bottom: 10px;
            width: 100%;
        }
        
        #gm-extractor-panel .save-group-btn:hover {
            background-color: #138496;
        }
        
        #gm-extractor-panel .group-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.5);
            z-index: 100000;
            display: none;
            align-items: center;
            justify-content: center;
        }
        
        #gm-extractor-panel .group-modal-content {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            width: 300px;
            max-width: 90%;
        }
        
        #gm-extractor-panel .group-modal h3 {
            margin: 0 0 15px 0;
            color: #495057;
            font-size: 16px;
        }
        
        #gm-extractor-panel .group-modal input {
            width: 100%;
            padding: 8px 10px;
            border: 1px solid #ced4da;
            border-radius: 4px;
            margin-bottom: 15px;
            font-size: 14px;
        }
        
        #gm-extractor-panel .group-modal-buttons {
            display: flex;
            gap: 10px;
            justify-content: flex-end;
        }
        
        #gm-extractor-panel .group-modal button {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: 600;
        }
        
        #gm-extractor-panel .group-modal .btn-primary {
            background-color: #007bff;
            color: white;
        }
        
        #gm-extractor-panel .group-modal .btn-secondary {
            background-color: #6c757d;
            color: white;
        }
    `);

    // 3. 定义所有函数(在HTML创建之前)
    
    // 智能选择器优化函数
    function optimizeSelector(button) {
        const row = button.closest('.selector-row');
        const input = row.querySelector('.selector-input');
        const originalSelector = input.value.trim();
        
        if (!originalSelector) return;
        
        // 显示优化选项
        showOptimizeOptions(row, originalSelector);
    }
    
    // 立即暴露到全局作用域
    window.optimizeSelector = optimizeSelector;
    
    // 显示优化选项
    function showOptimizeOptions(row, originalSelector) {
        const optimizeDiv = row.querySelector('.selector-optimize');
        
        // 分析选择器,生成多种优化策略
        const strategies = analyzeSelector(originalSelector);
        
        if (strategies.length === 0) {
            showValidationResult(row, 'error', '未检测到可优化的行号选择器');
            return;
        }
        
        // 创建优化选项界面
        const optionsHtml = `
            <div class="optimize-options">
                <div style="font-weight: 600; margin-bottom: 6px; color: #495057;">选择优化策略:</div>
                ${strategies.map((strategy, index) => `
                    <span class="optimize-option" data-strategy="${index}" data-original="${originalSelector.replace(/"/g, '&quot;')}">
                        ${strategy.name}
                    </span>
                `).join('')}
                <div style="margin-top: 6px; font-size: 10px; color: #6c757d;">
                    点击策略后会自动验证并显示结果
                </div>
            </div>
        `;
        
        optimizeDiv.innerHTML = optionsHtml;
        
        // 为每个策略按钮添加事件监听器
        optimizeDiv.querySelectorAll('.optimize-option').forEach(option => {
            option.addEventListener('click', () => {
                const strategyIndex = parseInt(option.dataset.strategy);
                const original = option.dataset.original;
                applyOptimization(strategyIndex, original, row);
            });
        });
    }
    
    // 分析选择器,生成优化策略
    function analyzeSelector(selector) {
        const strategies = [];
        
        // 策略1: 移除所有行号选择器(激进)
        if (selector.includes(':nth-child(') || selector.includes(':nth-of-type(') || 
            selector.includes(':first-child') || selector.includes(':last-child')) {
            strategies.push({
                name: '移除所有行号',
                description: '移除所有 :nth-child, :first-child 等选择器',
                transform: (sel) => {
                    return sel.replace(/:nth-child\(\d+\)/g, '')
                             .replace(/:nth-of-type\(\d+\)/g, '')
                             .replace(/:first-child/g, '')
                             .replace(/:last-child/g, '')
                             .replace(/:first-of-type/g, '')
                             .replace(/:last-of-type/g, '')
                             .replace(/\s*>\s*/g, ' > ')
                             .replace(/\s+/g, ' ')
                             .trim();
                }
            });
        }
        
        // 策略2: 只移除最后一个行号选择器(保守)
        if (selector.includes(':nth-child(') || selector.includes(':nth-of-type(')) {
            strategies.push({
                name: '移除最后行号',
                description: '只移除选择器末尾的行号选择器',
                transform: (sel) => {
                    return sel.replace(/:nth-child\(\d+\)$/, '')
                             .replace(/:nth-of-type\(\d+\)$/, '')
                             .replace(/:first-child$/, '')
                             .replace(/:last-child$/, '')
                             .trim();
                }
            });
        }
        
        // 策略3: 替换为通用选择器
        if (selector.includes(':nth-child(')) {
            strategies.push({
                name: '替换为通用',
                description: '将 :nth-child(n) 替换为通用选择器',
                transform: (sel) => {
                    return sel.replace(/:nth-child\(\d+\)/g, ':not(:empty)')
                             .trim();
                }
            });
        }
        
        // 策略4: 基于父元素优化
        if (selector.includes(' > ')) {
            strategies.push({
                name: '基于父元素',
                description: '保留父元素结构,移除子元素行号',
                transform: (sel) => {
                    const parts = sel.split(' > ');
                    if (parts.length > 1) {
                        const lastPart = parts[parts.length - 1];
                        const optimizedLast = lastPart.replace(/:nth-child\(\d+\)/g, '')
                                                     .replace(/:nth-of-type\(\d+\)/g, '');
                        parts[parts.length - 1] = optimizedLast;
                        return parts.join(' > ');
                    }
                    return sel;
                }
            });
        }
        
        return strategies;
    }
    
    // 应用优化策略
    function applyOptimization(strategyIndex, originalSelector, row) {
        const input = row.querySelector('.selector-input');
        
        // 获取策略
        const strategies = analyzeSelector(originalSelector);
        const strategy = strategies[strategyIndex];
        
        if (!strategy) return;
        
        // 应用优化
        const optimizedSelector = strategy.transform(originalSelector);
        
        // 验证优化结果
        const validation = validateOptimizedSelector(originalSelector, optimizedSelector);
        
        // 显示验证结果
        showValidationResult(row, validation.type, validation.message, optimizedSelector);
        
        // 如果验证成功,应用选择器
        if (validation.type === 'success') {
            input.value = optimizedSelector;
            input.style.backgroundColor = '#d4edda';
            setTimeout(() => {
                input.style.backgroundColor = '';
            }, 2000);
        }
    }
    
    // 验证优化后的选择器
    function validateOptimizedSelector(originalSelector, optimizedSelector) {
        try {
            // 测试原始选择器
            const originalElements = document.querySelectorAll(originalSelector);
            const originalCount = originalElements.length;
            
            // 测试优化后的选择器
            const optimizedElements = document.querySelectorAll(optimizedSelector);
            const optimizedCount = optimizedElements.length;
            
            if (optimizedCount === 0) {
                return {
                    type: 'error',
                    message: '优化后的选择器无法匹配任何元素'
                };
            }
            
            if (optimizedCount === originalCount) {
                return {
                    type: 'warning',
                    message: `优化后仍只匹配 ${originalCount} 个元素,可能没有扩展匹配范围`
                };
            }
            
            if (optimizedCount > originalCount) {
                return {
                    type: 'success',
                    message: `✅ 从 ${originalCount} 扩展到 ${optimizedCount} 个元素`
                };
            }
            
            return {
                type: 'warning',
                message: `优化后匹配 ${optimizedCount} 个元素,请检查是否为目标数据`
            };
            
        } catch (error) {
            return {
                type: 'error',
                message: `选择器语法错误: ${error.message}`
            };
        }
    }
    
    // 显示验证结果
    function showValidationResult(row, type, message, optimizedSelector = null) {
        // 移除现有的验证结果
        const existingResult = row.querySelector('.validation-result');
        if (existingResult) {
            existingResult.remove();
        }
        
        // 创建新的验证结果
        const resultDiv = document.createElement('div');
        resultDiv.className = `validation-result validation-${type}`;
        resultDiv.innerHTML = `
            <div>${message}</div>
            ${optimizedSelector ? `<div style="margin-top: 4px; font-family: monospace; font-size: 11px; opacity: 0.8;">${optimizedSelector}</div>` : ''}
        `;
        
        // 插入到选择器行中
        const optimizeDiv = row.querySelector('.selector-optimize');
        optimizeDiv.appendChild(resultDiv);
        
        // 如果是成功或错误,3秒后隐藏优化区域
        if (type === 'success' || type === 'error') {
            setTimeout(() => {
                optimizeDiv.style.display = 'none';
            }, 3000);
        }
    }

    // 4. 创建HTML界面
    const panel = document.createElement('div');
    panel.id = 'gm-extractor-panel';
    panel.innerHTML = `
        <div class="extractor-header">
            <span>🔍 内容提取器</span>
            <div class="header-buttons">
                <button id="save-group-btn" class="header-btn" title="保存当前选择器组">💾</button>
                <button id="load-group-btn" class="header-btn" title="加载选择器组">📂</button>
                <span id="close-panel-btn" title="关闭">&times;</span>
            </div>
        </div>
        <div class="extractor-body">
            <details>
                <summary>📖 如何获取选择器 (点击展开)</summary>
                <div class="instructions">
                    <ul>
                        <li><b>第1步:</b> 在您想提取的文字上 (例如标题或名字),点击鼠标<b>右键</b>,在弹出的菜单中选择 <strong>检查 (Inspect)</strong>。</li>
                        <li><b>第2步:</b> 浏览器下方或右侧会弹出开发者工具,并且有一行代码是<b>高亮</b>状态。</li>
                        <li><b>第3步:</b> 在这行<b>高亮的代码上</b>,再次点击鼠标<b>右键</b>。</li>
                        <li><b>第4步:</b> 在弹出的新菜单中,依次选择 <strong>复制 (Copy)</strong> > <strong>复制选择器 (Copy selector)</strong>。</li>
                        <li><b>第5步:</b> 回到本面板,将刚刚复制的内容粘贴到下面的输入框里即可。</li>
                    </ul>
                    <div class="pro-tip">
                        <b>💡 小提示:</b> 
                        <ul style="margin: 8px 0; padding-left: 20px;">
                            <li><b>提取单行数据:</b> 右键点击某一行 → 复制选择器</li>
                            <li><b>提取列表数据:</b> 右键点击某一行 → 复制选择器 → 点击"优化选择器"按钮,脚本会自动移除行号限制</li>
                            <li><b>手动优化:</b> 如果选择器包含 <code>:nth-child(1)</code> 等行号,删除这部分即可获取所有行</li>
                        </ul>
                    </div>
                </div>
            </details>
            <div class="accumulation-controls">
                <div class="accumulation-toggle">
                    <span>积累模式</span>
                    <div class="toggle-switch" id="accumulation-toggle">
                        <div class="toggle-slider"></div>
                    </div>
                </div>
                <div class="accumulation-info" id="accumulation-info">
                    关闭 - 点击"预览和积累"只显示当前页面数据
                </div>
                <button class="clear-btn" id="clear-accumulated-btn" style="display: none;">清空</button>
            </div>
            <div id="selectors-container">
            </div>
            <div class="preview-section" id="preview-section" style="display: none;">
                <div class="preview-title">
                    <span class="status-indicator status-success"></span>
                    数据预览 (前3行)
                </div>
                <div id="preview-content"></div>
                <div class="preview-info" id="preview-info"></div>
            </div>
        </div>
        <div class="extractor-footer">
            <div class="footer-actions">
                <button id="preview-btn">👁️ 预览和积累</button>
                <button id="export-csv-btn">📥 导出CSV</button>
            </div>
        </div>
        
        <!-- 组管理模态框 -->
        <div class="group-modal" id="group-modal">
            <div class="group-modal-content">
                <div class="group-modal-header">
                    <div class="group-modal-title">选择器组管理</div>
                    <button class="group-modal-close" id="group-modal-close">&times;</button>
                </div>
                
                <div class="group-form">
                    <input type="text" id="group-name-input" placeholder="输入组名称(例如:商品列表)" maxlength="50">
                    <button id="save-current-group-btn">💾 保存当前选择器组</button>
                </div>
                
                <div class="group-list" id="group-list">
                    <!-- 组列表将在这里动态生成 -->
                </div>
            </div>
        </div>
    `;
    document.body.appendChild(panel);

    const selectorsContainer = panel.querySelector('#selectors-container');
    const previewSection = panel.querySelector('#preview-section');
    const previewContent = panel.querySelector('#preview-content');
    const previewInfo = panel.querySelector('#preview-info');
    
    // 数据积累相关变量
    let accumulatedData = [];
    let isAccumulationMode = false;
    
    // 选择器组相关变量
    let savedGroups = [];
    const STORAGE_KEY = 'content_extractor_groups';

    // 4. 界面的交互逻辑
    
    // 选择器组管理函数
    function loadSavedGroups() {
        try {
            const stored = localStorage.getItem(STORAGE_KEY);
            if (stored) {
                savedGroups = JSON.parse(stored);
            }
        } catch (e) {
            console.error('加载保存的组失败:', e);
            savedGroups = [];
        }
        renderGroupsList();
    }
    
    function saveGroupsToStorage() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(savedGroups));
        } catch (e) {
            console.error('保存组失败:', e);
            alert('保存失败,可能是存储空间不足');
        }
    }
    
    function renderGroupsList() {
        const groupsList = panel.querySelector('#groups-list');
        if (savedGroups.length === 0) {
            groupsList.innerHTML = '<div style="text-align: center; color: #6c757d; font-size: 12px; padding: 20px;">暂无保存的组</div>';
            return;
        }
        
        groupsList.innerHTML = savedGroups.map((group, index) => `
            <div class="group-item" data-index="${index}">
                <div class="group-name">${group.name || '未命名组'}</div>
                <div class="group-path" title="${group.path}">${group.path}</div>
                <div class="group-info">${group.selectors.length} 个选择器</div>
                <button class="group-delete" onclick="deleteGroup(${index})" title="删除此组">×</button>
            </div>
        `).join('');
        
        // 为每个组项添加点击事件
        groupsList.querySelectorAll('.group-item').forEach(item => {
            item.addEventListener('click', (e) => {
                if (e.target.classList.contains('group-delete')) return;
                const index = parseInt(item.dataset.index);
                loadGroup(index);
            });
        });
    }
    
    function showSaveGroupModal() {
        const modal = document.createElement('div');
        modal.className = 'group-modal';
        modal.innerHTML = `
            <div class="group-modal-content">
                <h3>保存选择器组</h3>
                <input type="text" id="group-name-input" placeholder="组名称(可选)" maxlength="50">
                <div class="group-modal-buttons">
                    <button class="btn-secondary" onclick="closeSaveModal()">取消</button>
                    <button class="btn-primary" onclick="saveCurrentGroup()">保存</button>
                </div>
            </div>
        `;
        document.body.appendChild(modal);
        modal.style.display = 'flex';
        
        // 自动聚焦到输入框
        setTimeout(() => {
            modal.querySelector('#group-name-input').focus();
        }, 100);
        
        // 回车保存
        modal.querySelector('#group-name-input').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                saveCurrentGroup();
            }
        });
    }
    
    function closeSaveModal() {
        const modal = document.querySelector('.group-modal');
        if (modal) {
            modal.remove();
        }
    }
    
    
    // 切换积累模式
    function toggleAccumulationMode() {
        isAccumulationMode = !isAccumulationMode;
        const toggle = panel.querySelector('#accumulation-toggle');
        const info = panel.querySelector('#accumulation-info');
        const clearBtn = panel.querySelector('#clear-accumulated-btn');
        
        if (isAccumulationMode) {
            toggle.classList.add('active');
            info.textContent = `开启 - 已积累 ${accumulatedData.length} 行数据,点击"预览和积累"会继续积累`;
            clearBtn.style.display = 'block';
        } else {
            toggle.classList.remove('active');
            info.textContent = '关闭 - 点击"预览和积累"只显示当前页面数据';
            clearBtn.style.display = 'none';
        }
    }
    
    // 清空积累的数据
    function clearAccumulatedData() {
        accumulatedData = [];
        const info = panel.querySelector('#accumulation-info');
        info.textContent = '开启 - 已积累 0 行数据,点击"预览和积累"会开始积累';
        
        // 如果预览区域显示的是积累数据,需要重新预览当前页面
        if (isAccumulationMode && previewSection.style.display !== 'none') {
            previewData();
        }
    }
    
    // 检查数据是否重复
    function isDuplicateData(newRow, existingData) {
        return existingData.some(existingRow => {
            return newRow.every((cell, index) => {
                return cell === (existingRow[index] || '');
            });
        });
    }
    
    // 添加数据到积累池
    function addToAccumulatedData(newData) {
        let addedCount = 0;
        newData.forEach(row => {
            if (!isDuplicateData(row, accumulatedData)) {
                accumulatedData.push(row);
                addedCount++;
            }
        });
        return addedCount;
    }

    function toggleExtractorPanel() {
        panel.style.display = (panel.style.display === 'flex') ? 'none' : 'flex';
        if (selectorsContainer.children.length === 0) {
            addSelectorRow();
        }
    }

    function addSelectorRow(initialValue = '') {
        const row = document.createElement('div');
        row.className = 'selector-row';
        row.innerHTML = `
            <div class="selector-input-row">
                <button class="add-btn" title="在此行前添加选择器">+</button>
                <input type="text" class="selector-input" placeholder="请从控制台复制选择器并粘贴" value="${initialValue}">
                <button class="remove-btn" title="移除此行">-</button>
            </div>
        `;
        selectorsContainer.appendChild(row);
        
        // 添加选择器优化提示
        const optimizeDiv = document.createElement('div');
        optimizeDiv.className = 'selector-optimize';
        optimizeDiv.style.display = 'none';
        optimizeDiv.innerHTML = `
            <span>检测到行号选择器,点击优化可获取所有行数据</span>
            <button class="optimize-btn">优化选择器</button>
        `;
        row.appendChild(optimizeDiv);
        
        // 为加号按钮添加事件监听器
        const addBtn = row.querySelector('.add-btn');
        addBtn.addEventListener('click', () => {
            const newRow = addSelectorRow();
            row.parentNode.insertBefore(newRow, row);
        });
        
        // 为优化按钮添加事件监听器
        const optimizeBtn = optimizeDiv.querySelector('.optimize-btn');
        optimizeBtn.addEventListener('click', () => {
            optimizeSelector(optimizeBtn);
        });
        
        // 监听输入变化,检测是否需要优化
        const input = row.querySelector('.selector-input');
        input.addEventListener('input', () => {
            const selector = input.value.trim();
            if (selector && (selector.includes(':nth-child(') || selector.includes(':nth-of-type('))) {
                optimizeDiv.style.display = 'block';
            } else {
                optimizeDiv.style.display = 'none';
            }
        });
        
        row.querySelector('.remove-btn').addEventListener('click', () => {
            row.remove();
            // 如果移除了所有选择器,隐藏预览区域
            if (selectorsContainer.children.length === 0) {
                previewSection.style.display = 'none';
            }
        });
        
        return row;
    }

    function previewData() {
        const selectorInputs = panel.querySelectorAll('.selector-input');
        const selectors = Array.from(selectorInputs).map(input => input.value.trim()).filter(Boolean);

        if (selectors.length === 0) {
            alert('请输入至少一个CSS选择器!');
            return;
        }

        const columnsData = [];
        const validSelectors = [];
        let hasError = false;

        selectors.forEach((selector, index) => {
            try {
                const elements = Array.from(document.querySelectorAll(selector));
                const data = elements.map(el => el.innerText.trim());
                columnsData.push(data);
                validSelectors.push(selector);
            } catch (e) {
                alert(`选择器 "${selector}" 无效,请检查语法!`);
                hasError = true;
            }
        });

        if (hasError) return;

        const maxRows = Math.max(0, ...columnsData.map(col => col.length));
        if (maxRows === 0) {
            previewContent.innerHTML = '<div style="text-align: center; color: #6c757d; padding: 20px;">没有找到任何数据</div>';
            previewInfo.innerHTML = '根据您提供的选择器,没有在页面上找到任何内容。';
            previewSection.style.display = 'block';
            return;
        }

        // 处理数据积累
        let displayData = [];
        let totalRows = maxRows;
        let addedCount = 0;

        if (isAccumulationMode) {
            // 将当前页面数据转换为行格式
            const currentPageData = [];
            for (let i = 0; i < maxRows; i++) {
                const row = columnsData.map(col => col[i] || '');
                currentPageData.push(row);
            }
            
            // 添加到积累池
            addedCount = addToAccumulatedData(currentPageData);
            
            // 使用积累的数据进行显示
            displayData = accumulatedData;
            totalRows = accumulatedData.length;
            
            // 更新积累信息
            const info = panel.querySelector('#accumulation-info');
            info.textContent = `开启 - 已积累 ${accumulatedData.length} 行数据 (本次新增 ${addedCount} 行),点击"预览和积累"会继续积累`;
        } else {
            // 普通模式,直接使用当前页面数据
            for (let i = 0; i < maxRows; i++) {
                const row = columnsData.map(col => col[i] || '');
                displayData.push(row);
            }
        }

        // 创建预览表格
        let tableHTML = '<table class="preview-table"><thead><tr>';
        validSelectors.forEach(selector => {
            tableHTML += `<th>${selector.length > 20 ? selector.substring(0, 20) + '...' : selector}</th>`;
        });
        tableHTML += '</tr></thead><tbody>';

        // 只显示前3行数据
        const previewRows = Math.min(3, totalRows);
        for (let i = 0; i < previewRows; i++) {
            tableHTML += '<tr>';
            displayData[i].forEach(cellData => {
                const displayData = cellData.length > 30 ? cellData.substring(0, 30) + '...' : cellData;
                tableHTML += `<td title="${cellData.replace(/"/g, '&quot;')}">${displayData}</td>`;
            });
            tableHTML += '</tr>';
        }
        tableHTML += '</tbody></table>';

        previewContent.innerHTML = tableHTML;
        
        // 更新预览信息
        let infoText = `共找到 ${totalRows} 行数据,显示前 ${previewRows} 行。列数: ${validSelectors.length}`;
        if (isAccumulationMode && addedCount > 0) {
            infoText += ` (本次新增 ${addedCount} 行)`;
        }
        previewInfo.innerHTML = infoText;
        previewSection.style.display = 'block';
    }

    function exportToCsv() {
        const selectorInputs = panel.querySelectorAll('.selector-input');
        const selectors = Array.from(selectorInputs).map(input => input.value.trim()).filter(Boolean);

        if (selectors.length === 0) {
            alert('请输入至少一个CSS选择器!');
            return;
        }

        let exportData = [];
        let fileName = 'content_export';

        if (isAccumulationMode && accumulatedData.length > 0) {
            // 导出积累的数据
            exportData = accumulatedData;
            fileName = `accumulated_export_${accumulatedData.length}rows`;
        } else {
            // 导出当前页面数据
            const columnsData = selectors.map(selector => {
                try {
                    const elements = Array.from(document.querySelectorAll(selector));
                    const data = elements.map(el => el.innerText.trim());
                    return data;
                } catch (e) {
                    alert('选择器 "' + selector + '" 无效,请检查语法!');
                    return null;
                }
            });

            if (columnsData.some(col => col === null)) return;

            const maxRows = Math.max(0, ...columnsData.map(col => col.length));
            if (maxRows === 0) {
                alert('根据您提供的选择器,没有在页面上找到任何内容。');
                return;
            }

            // 将列数据转换为行数据
            for (let i = 0; i < maxRows; i++) {
                const row = columnsData.map(col => col[i] || '');
                exportData.push(row);
            }
            fileName = 'current_page_export';
        }

        if (exportData.length === 0) {
            alert('没有数据可以导出!');
            return;
        }

        // 生成CSV内容
        let csvContent = '';
        const header = selectors.map(s => `"${s.replace(/"/g, '""')}"`).join(',');
        csvContent += header + '\r\n';

        exportData.forEach(row => {
            const rowData = row.map(cellData => `"${String(cellData).replace(/"/g, '""')}"`);
            csvContent += rowData.join(',') + '\r\n';
        });

        // 使用Blob方式导出,避免URL长度限制
        const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
        const link = document.createElement("a");
        
        try {
            if (link.download !== undefined) {
                const url = URL.createObjectURL(blob);
                link.setAttribute("href", url);
                link.setAttribute("download", `${fileName}_${new Date().toISOString().slice(0,10)}.csv`);
                link.style.visibility = 'hidden';
                document.body.appendChild(link);
                
                // 使用setTimeout避免同步点击问题
                setTimeout(() => {
                    try {
                        link.click();
                    } catch (e) {
                        console.warn('点击下载链接失败,尝试降级方案:', e);
                        // 降级方案
                        const dataUrl = 'data:text/csv;charset=utf-8,' + encodeURIComponent('\ufeff' + csvContent);
                        window.open(dataUrl);
                    }
                    document.body.removeChild(link);
                    URL.revokeObjectURL(url);
                }, 100);
            } else {
                // 降级方案:使用data URL
                const dataUrl = 'data:text/csv;charset=utf-8,' + encodeURIComponent('\ufeff' + csvContent);
                window.open(dataUrl);
            }
        } catch (e) {
            console.error('导出CSV失败:', e);
            // 提供备用方案:复制到剪贴板
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(csvContent).then(() => {
                    alert('导出失败,但数据已复制到剪贴板,您可以粘贴到Excel或其他应用中');
                }).catch(() => {
                    alert('导出失败,请检查浏览器设置');
                });
            } else {
                alert('导出失败,请检查浏览器设置');
            }
        }

        // 显示成功提示
        const originalText = panel.querySelector('#export-csv-btn').textContent;
        const exportBtn = panel.querySelector('#export-csv-btn');
        exportBtn.textContent = `✅ 已导出 ${exportData.length} 行`;
        exportBtn.style.backgroundColor = '#28a745';
        setTimeout(() => {
            exportBtn.textContent = originalText;
            exportBtn.style.backgroundColor = '#007bff';
        }, 2000);
    }

    // 5. 绑定事件
    panel.querySelector('#preview-btn').addEventListener('click', previewData);
    panel.querySelector('#export-csv-btn').addEventListener('click', exportToCsv);
    panel.querySelector('#close-panel-btn').addEventListener('click', toggleExtractorPanel);
    
    // 绑定积累模式相关事件
    panel.querySelector('#accumulation-toggle').addEventListener('click', toggleAccumulationMode);
    panel.querySelector('#clear-accumulated-btn').addEventListener('click', clearAccumulatedData);
    
    // 绑定组管理相关事件
    panel.querySelector('#save-group-btn').addEventListener('click', showGroupModal);
    panel.querySelector('#load-group-btn').addEventListener('click', showGroupModal);
    panel.querySelector('#group-modal-close').addEventListener('click', closeGroupModal);
    panel.querySelector('#save-current-group-btn').addEventListener('click', saveCurrentGroup);
    
    // 使用事件委托处理动态生成的按钮
    panel.addEventListener('click', function(e) {
        if (e.target.classList.contains('group-item-btn')) {
            const action = e.target.getAttribute('data-action');
            const index = parseInt(e.target.getAttribute('data-index'));
            
            if (action === 'load') {
                loadGroup(index);
            } else if (action === 'delete') {
                deleteGroup(index);
            }
        } else if (e.target.classList.contains('add-btn')) {
            // 处理"+"按钮点击
            e.preventDefault();
            addSelectorRow();
        } else if (e.target.classList.contains('remove-btn')) {
            // 处理"-"按钮点击
            e.preventDefault();
            const row = e.target.closest('.selector-row');
            if (row) {
                row.remove();
            }
        }
    });
    
    // 初始化加载保存的组
    loadSavedGroups();
    
    // 组管理相关函数
    function showGroupModal() {
        const modal = panel.querySelector('#group-modal');
        modal.classList.add('open');
        renderGroupList();
    }
    
    function closeGroupModal() {
        const modal = panel.querySelector('#group-modal');
        modal.classList.remove('open');
    }
    
    function saveCurrentGroup() {
        const nameInput = panel.querySelector('#group-name-input');
        const groupName = nameInput.value.trim();
        
        if (!groupName) {
            alert('请输入组名称!');
            return;
        }
        
        const currentSelectors = Array.from(panel.querySelectorAll('.selector-input'))
            .map(input => input.value.trim())
            .filter(selector => selector);
        
        if (currentSelectors.length === 0) {
            alert('当前没有选择器可以保存!');
            return;
        }
        
        const groupData = {
            name: groupName,
            selectors: currentSelectors,
            url: window.location.href,
            timestamp: new Date().toISOString()
        };
        
        const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]');
        savedGroups.push(groupData);
        localStorage.setItem('extractorGroups', JSON.stringify(savedGroups));
        
        nameInput.value = '';
        renderGroupList();
        alert(`组 "${groupName}" 保存成功!`);
    }
    
    function renderGroupList() {
        const groupList = panel.querySelector('#group-list');
        const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]');
        
        if (savedGroups.length === 0) {
            groupList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">暂无保存的组</div>';
            return;
        }
        
        groupList.innerHTML = savedGroups.map((group, index) => `
            <div class="group-item">
                <div class="group-item-info">
                    <div class="group-item-name">${group.name}</div>
                    <div class="group-item-path">${new URL(group.url).pathname}</div>
                </div>
                <div class="group-item-actions">
                    <button class="group-item-btn group-load-btn" data-action="load" data-index="${index}">加载</button>
                    <button class="group-item-btn group-delete-btn" data-action="delete" data-index="${index}">删除</button>
                </div>
            </div>
        `).join('');
    }
    
    function loadGroup(index) {
        const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]');
        const group = savedGroups[index];
        
        if (!group) {
            alert('组不存在!');
            return;
        }
        
        // 清空当前选择器
        const selectorsContainer = panel.querySelector('#selectors-container');
        selectorsContainer.innerHTML = '';
        
        // 添加新的选择器
        group.selectors.forEach(selector => {
            addSelectorRow(selector);
        });
        
        closeGroupModal();
        alert(`已加载组 "${group.name}",包含 ${group.selectors.length} 个选择器`);
    }
    
    function deleteGroup(index) {
        const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]');
        const group = savedGroups[index];
        
        if (!group) {
            alert('组不存在!');
            return;
        }
        
        if (confirm(`确定要删除组 "${group.name}" 吗?`)) {
            savedGroups.splice(index, 1);
            localStorage.setItem('extractorGroups', JSON.stringify(savedGroups));
            renderGroupList();
            alert('组已删除!');
        }
    }
    
    // 6. 添加拖拽功能
    (function makeDraggable(panel) {
        const header = panel.querySelector('.extractor-header');
        let isDragging = false;
        let offsetX, offsetY;

        header.addEventListener('mousedown', (e) => {
            if (e.button !== 0) return;
            if (e.target.id === 'close-panel-btn') return;

            isDragging = true;
            const panelRect = panel.getBoundingClientRect();
            offsetX = e.clientX - panelRect.left;
            offsetY = e.clientY - panelRect.top;

            document.body.style.userSelect = 'none';
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });

        function onMouseMove(e) {
            if (!isDragging) return;

            let newLeft = e.clientX - offsetX;
            let newTop = e.clientY - offsetY;

            newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - panel.offsetWidth));
            newTop = Math.max(0, Math.min(newTop, window.innerHeight - panel.offsetHeight));

            panel.style.left = newLeft + 'px';
            panel.style.top = newTop + 'px';
            panel.style.right = '';
        }

        function onMouseUp() {
            isDragging = false;
            document.body.style.userSelect = '';
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
        }
    })(panel);

})();

// 暴露函数到全局作用域
window.loadGroup = function(index) {
    const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]');
    const group = savedGroups[index];
    
    if (!group) {
        alert('组不存在!');
        return;
    }
    
    // 清空当前选择器
    const selectorsContainer = document.querySelector('#selectors-container');
    if (selectorsContainer) {
        selectorsContainer.innerHTML = '';
        
        // 添加新的选择器
        group.selectors.forEach(selector => {
            // 创建新的选择器行
            const row = document.createElement('div');
            row.className = 'selector-row';
            row.innerHTML = `
                <div class="selector-input-row">
                    <button class="add-btn" onclick="addSelectorRow()">+</button>
                    <input type="text" class="selector-input" placeholder="请从控制台复制选择器并粘贴" value="${selector}">
                    <button class="remove-btn" onclick="this.parentElement.parentElement.remove()">-</button>
                </div>
                <div class="selector-optimize" style="display: none;">
                    <button class="optimize-btn" onclick="optimizeSelector(this)">🔧 优化选择器</button>
                </div>
            `;
            selectorsContainer.appendChild(row);
        });
        
        // 关闭模态框
        const modal = document.querySelector('#group-modal');
        if (modal) {
            modal.classList.remove('open');
        }
        
        alert(`已加载组 "${group.name}",包含 ${group.selectors.length} 个选择器`);
    }
};

window.deleteGroup = function(index) {
    const savedGroups = JSON.parse(localStorage.getItem('extractorGroups') || '[]');
    const group = savedGroups[index];
    
    if (!group) {
        alert('组不存在!');
        return;
    }
    
    if (confirm(`确定要删除组 "${group.name}" 吗?`)) {
        savedGroups.splice(index, 1);
        localStorage.setItem('extractorGroups', JSON.stringify(savedGroups));
        
        // 重新渲染组列表
        const groupList = document.querySelector('#group-list');
        if (groupList) {
            if (savedGroups.length === 0) {
                groupList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">暂无保存的组</div>';
            } else {
                groupList.innerHTML = savedGroups.map((group, index) => `
                    <div class="group-item">
                        <div class="group-item-info">
                            <div class="group-item-name">${group.name}</div>
                            <div class="group-item-path">${new URL(group.url).pathname}</div>
                        </div>
                        <div class="group-item-actions">
                            <button class="group-item-btn group-load-btn" data-action="load" data-index="${index}">加载</button>
                            <button class="group-item-btn group-delete-btn" data-action="delete" data-index="${index}">删除</button>
                        </div>
                    </div>
                `).join('');
            }
        }
        
        alert('组已删除!');
    }
};

// 暴露其他必要的函数到全局作用域
window.addSelectorRow = function(value = '') {
    const selectorsContainer = document.querySelector('#selectors-container');
    if (selectorsContainer) {
        const row = document.createElement('div');
        row.className = 'selector-row';
        row.innerHTML = `
            <div class="selector-input-row">
                <button class="add-btn" onclick="addSelectorRow()">+</button>
                <input type="text" class="selector-input" placeholder="请从控制台复制选择器并粘贴" value="${value}">
                <button class="remove-btn" onclick="this.parentElement.parentElement.remove()">-</button>
            </div>
            <div class="selector-optimize" style="display: none;">
                <button class="optimize-btn" onclick="optimizeSelector(this)">🔧 优化选择器</button>
            </div>
        `;
        selectorsContainer.appendChild(row);
        return row;
    }
};

window.optimizeSelector = function(button) {
    const row = button.closest('.selector-row');
    const input = row.querySelector('.selector-input');
    const optimizeDiv = row.querySelector('.selector-optimize');
    
    if (!input.value.trim()) {
        alert('请先输入选择器!');
        return;
    }
    
    // 显示优化选项
    optimizeDiv.style.display = 'block';
    optimizeDiv.innerHTML = `
        <div class="optimize-options">
            <button class="optimize-option" onclick="applyOptimization(this, 'remove-nth')">移除行号限制</button>
            <button class="optimize-option" onclick="applyOptimization(this, 'generalize')">通用化选择器</button>
            <button class="optimize-option" onclick="applyOptimization(this, 'parent')">使用父级选择器</button>
            <button class="optimize-option" onclick="applyOptimization(this, 'class-only')">仅保留类名</button>
        </div>
    `;
};

window.applyOptimization = function(button, type) {
    const row = button.closest('.selector-row');
    const input = row.querySelector('.selector-input');
    const originalSelector = input.value.trim();
    
    let optimizedSelector = originalSelector;
    
    switch (type) {
        case 'remove-nth':
            optimizedSelector = originalSelector.replace(/:nth-child\(\d+\)/g, '');
            optimizedSelector = optimizedSelector.replace(/:nth-of-type\(\d+\)/g, '');
            break;
        case 'generalize':
            optimizedSelector = originalSelector.replace(/\d+/g, '');
            break;
        case 'parent': {
            const parts = originalSelector.split(' > ');
            if (parts.length > 1) {
                optimizedSelector = parts.slice(0, -1).join(' > ');
            }
            break;
        }
        case 'class-only': {
            const classMatch = originalSelector.match(/\.[\w-]+/g);
            if (classMatch) {
                optimizedSelector = classMatch.join('');
            }
            break;
        }
    }
    
    input.value = optimizedSelector;
    
    // 验证优化结果
    try {
        const elements = document.querySelectorAll(optimizedSelector);
        const originalElements = document.querySelectorAll(originalSelector);
        
        if (elements.length > originalElements.length) {
            alert(`✅ 优化成功!从 ${originalElements.length} 个元素扩展到 ${elements.length} 个元素`);
        } else if (elements.length === originalElements.length) {
            alert(`⚠️ 优化后仍匹配 ${elements.length} 个元素,可能没有扩展匹配范围`);
        } else {
            alert(`❌ 优化后只匹配 ${elements.length} 个元素,请检查选择器`);
        }
    } catch (e) {
        alert(`❌ 选择器语法错误: ${e.message}`);
    }
    
    // 隐藏优化选项
    const optimizeDiv = row.querySelector('.selector-optimize');
    optimizeDiv.style.display = 'none';
};