豆包网页版音频捕获与合并

捕获豆包网页版中的音频数据,支持直接下载、合并下载多个音频

// ==UserScript==
// @name         豆包网页版音频捕获与合并
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  捕获豆包网页版中的音频数据,支持直接下载、合并下载多个音频
// @author       cenglin123
// @match        https://www.doubao.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=doubao.com
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        unsafeWindow
// @require      https://cdnjs.cloudflare.com/ajax/libs/lamejs/1.2.0/lame.min.js
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    
    // 存储捕获的音频数据
    let capturedAudio = [];
    let isMonitoring = false;
    let originalXHR = unsafeWindow.XMLHttpRequest;
    let originalFetch = unsafeWindow.fetch;
    let observer = null;
    
    // 创建UI
    function createMainInterface() {
        // 检查是否已存在UI
        if (document.getElementById('audio-capture-panel')) {
            document.getElementById('audio-capture-panel').style.display = 'block';
            return;
        }
        
        const panel = document.createElement('div');
        panel.id = 'audio-capture-panel';
        panel.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: white;
            border: 1px solid #ccc;
            border-radius: 8px;
            padding: 15px;
            box-shadow: 0 0 10px rgba(0,0,0,0.2);
            z-index: 9999;
            font-family: Arial, sans-serif;
            max-width: 350px;
        `;
        
        panel.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
                <h3 style="margin: 0;">豆包音频捕获工具</h3>
                <button id="close-tool" style="background: none; border: none; cursor: pointer;">✕</button>
            </div>
            <div style="display: grid; grid-template-columns: 1fr; gap: 10px;">
                <button id="monitor-toggle" style="padding: 8px; background: #4285f4; color: white; border: none; border-radius: 4px; cursor: pointer;">
                    ${isMonitoring ? '停止监控网络请求' : '开始监控网络请求'}
                </button>
                <button id="view-captured" style="padding: 8px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
                    查看已捕获的音频 (<span id="audio-count">0</span>)
                </button>
                <button id="merge-download" style="padding: 8px; background: #0f9d58; color: white; border: none; border-radius: 4px; cursor: pointer;">
                    合并并下载
                </button>
                <button id="direct-download" style="padding: 8px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">✳️从data URL直接解析并下载音频数据</button>
                <button id="process-base64" style="padding: 8px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">✳️处理Base64编码的音频数据</button>
            </div>
            <div id="status-area" style="margin-top: 10px; padding: 5px; font-size: 12px; color: #666;"></div>
        `;
        
        document.body.appendChild(panel);
        
        // 更新音频计数
        updateAudioCount();
        
        // 添加事件监听
        document.getElementById('close-tool').addEventListener('click', () => {
            panel.style.display = 'none';
        });
        
        document.getElementById('direct-download').addEventListener('click', downloadFromDataUrl);
        document.getElementById('process-base64').addEventListener('click', handleBase64FromRequest);
        document.getElementById('view-captured').addEventListener('click', showCapturedAudioList);
        document.getElementById('merge-download').addEventListener('click', showMergeOptions);
        
        // 监控网络请求按钮
        document.getElementById('monitor-toggle').addEventListener('click', () => {
            if (isMonitoring) {
                stopMonitoring();
                document.getElementById('monitor-toggle').textContent = '开始监控网络请求';
                document.getElementById('monitor-toggle').style.background = '#4285f4';
                updateStatus('已停止监控网络请求');
            } else {
                startMonitoring();
                document.getElementById('monitor-toggle').textContent = '停止监控网络请求';
                document.getElementById('monitor-toggle').style.background = '#db4437';
                updateStatus('正在监控网络请求...');
            }
        });
    }
    
    // 更新状态区域
    function updateStatus(message) {
        const statusArea = document.getElementById('status-area');
        if (statusArea) {
            statusArea.textContent = message;
        }
    }
    
    // 更新音频计数
    function updateAudioCount() {
        const countElement = document.getElementById('audio-count');
        if (countElement) {
            countElement.textContent = capturedAudio.length;
        }
    }
    
    // 开始监控网络请求
    function startMonitoring() {
        if (isMonitoring) return;
        isMonitoring = true;
        
        // 拦截XHR请求
        unsafeWindow.XMLHttpRequest = function() {
            const xhr = new originalXHR();
            const originalOpen = xhr.open;
            const originalSend = xhr.send;
            
            xhr.open = function() {
                this.method = arguments[0];
                this.url = arguments[1];
                return originalOpen.apply(this, arguments);
            };
            
            xhr.send = function() {
                this.addEventListener('load', function() {
                    try {
                        // 检查是否是音频相关内容
                        const contentType = this.getResponseHeader('Content-Type') || '';
                        const isAudio = contentType.includes('audio') || 
                                       contentType.includes('octet-stream') ||
                                       this.url.match(/\.(mp3|wav|ogg|aac|flac|m4a)($|\?)/i);
                        
                        if (isAudio || contentType.includes('octet-stream')) {
                            captureAudioFromResponse(this.response, contentType, this.url);
                        }
                    } catch (e) {
                        console.error('处理XHR请求时出错:', e);
                    }
                });
                
                return originalSend.apply(this, arguments);
            };
            
            return xhr;
        };
        
        // 拦截Fetch请求
        unsafeWindow.fetch = function() {
            const url = arguments[0] instanceof Request ? arguments[0].url : arguments[0];
            const method = arguments[0] instanceof Request ? arguments[0].method : 'GET';
            
            return originalFetch.apply(this, arguments).then(response => {
                try {
                    const contentType = response.headers.get('Content-Type') || '';
                    const isAudio = contentType.includes('audio') || 
                                   contentType.includes('octet-stream') ||
                                   url.match(/\.(mp3|wav|ogg|aac|flac|m4a)($|\?)/i);
                    
                    if (isAudio || contentType.includes('octet-stream')) {
                        // 克隆响应以不影响原始处理
                        response.clone().arrayBuffer().then(buffer => {
                            captureAudioFromResponse(buffer, contentType, url);
                        });
                    }
                } catch (e) {
                    console.error('处理Fetch请求时出错:', e);
                }
                
                return response;
            });
        };
        
        // 监控DOM变化以捕获新添加的媒体元素
        observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeName === 'AUDIO' || node.nodeName === 'VIDEO') {
                        node.addEventListener('play', () => {
                            if (node.src) {
                                captureAudioFromMediaElement(node);
                            }
                        });
                    }
                });
            });
        });
        
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
        
        // 监控现有的媒体元素
        document.querySelectorAll('audio, video').forEach(mediaElement => {
            mediaElement.addEventListener('play', () => {
                if (mediaElement.src) {
                    captureAudioFromMediaElement(mediaElement);
                }
            });
        });
        
        // 扫描页面中的data URLs
        scanPageForDataUrls();
    }
    
    // 停止监控
    function stopMonitoring() {
        if (!isMonitoring) return;
        isMonitoring = false;
        
        // 恢复原始的XHR和Fetch
        unsafeWindow.XMLHttpRequest = originalXHR;
        unsafeWindow.fetch = originalFetch;
        
        // 停止DOM观察
        if (observer) {
            observer.disconnect();
            observer = null;
        }
    }
    
    // 从响应捕获音频
    function captureAudioFromResponse(response, contentType, url) {
        // 检查是否已捕获
        if (capturedAudio.some(audio => audio.url === url)) {
            return;
        }
        
        const audioItem = {
            id: generateId(),
            source: 'network',
            url: url,
            contentType: contentType,
            timestamp: new Date().toISOString(),
            data: response,
            format: guessAudioFormat(contentType, url),
            size: response ? (response.byteLength || 0) : 0
        };
        
        capturedAudio.push(audioItem);
        updateAudioCount();
        saveAudioData();
        updateStatus(`捕获到新音频: ${getShortUrl(url)}`);
    }
    
    // 从媒体元素捕获音频
    function captureAudioFromMediaElement(mediaElement) {
        if (capturedAudio.some(audio => audio.url === mediaElement.src)) {
            return;
        }
        
        const audioItem = {
            id: generateId(),
            source: 'media',
            url: mediaElement.src,
            contentType: 'audio/media',
            timestamp: new Date().toISOString(),
            mediaElement: mediaElement,
            format: 'mp3',
            size: 'unknown'
        };
        
        capturedAudio.push(audioItem);
        updateAudioCount();
        saveAudioData();
        updateStatus(`捕获到媒体元素音频: ${getShortUrl(mediaElement.src)}`);
    }
    
    // 生成唯一ID
    function generateId() {
        return 'audio_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
    }
    
    // 获取简短URL
    function getShortUrl(url) {
        if (!url) return 'unknown';
        if (url.startsWith('data:')) return 'data:URL';
        try {
            const urlObj = new URL(url);
            const path = urlObj.pathname;
            if (path.length > 20) {
                return path.substr(0, 17) + '...';
            }
            return path;
        } catch (e) {
            return url.substr(0, 20) + '...';
        }
    }
    
    // 猜测音频格式
    function guessAudioFormat(contentType, url) {
        if (contentType.includes('mpeg') || contentType.includes('mp3')) {
            return 'mp3';
        } else if (contentType.includes('wav')) {
            return 'wav';
        } else if (contentType.includes('ogg')) {
            return 'ogg';
        } else if (contentType.includes('aac')) {
            return 'aac';
        } else if (contentType.includes('flac')) {
            return 'flac';
        } else if (url) {
            // 从URL猜测
            if (url.match(/\.mp3($|\?)/i)) return 'mp3';
            if (url.match(/\.wav($|\?)/i)) return 'wav';
            if (url.match(/\.ogg($|\?)/i)) return 'ogg';
            if (url.match(/\.aac($|\?)/i)) return 'aac';
            if (url.match(/\.flac($|\?)/i)) return 'flac';
        }
        return 'mp3'; // 默认值
    }
    
    // 保存音频数据到GM存储
    function saveAudioData() {
        try {
            // 只保存必要的信息,不保存二进制数据
            const serializedData = capturedAudio.map(audio => {
                const { id, source, url, contentType, timestamp, format, size } = audio;
                return { id, source, url, contentType, timestamp, format, size };
            });
            
            GM_setValue('capturedAudioMeta', JSON.stringify(serializedData));
        } catch (e) {
            console.error('保存音频元数据时出错:', e);
        }
    }
    
    // 加载音频元数据
    function loadAudioData() {
        try {
            const data = GM_getValue('capturedAudioMeta');
            if (data) {
                capturedAudio = JSON.parse(data);
                updateAudioCount();
            }
        } catch (e) {
            console.error('加载音频元数据时出错:', e);
        }
    }
    
    // 扫描页面中的data URLs
    function scanPageForDataUrls() {
        const pageContent = document.documentElement.innerHTML;
        const dataUrlRegex = /data:(application\/octet-stream|audio\/[^;]+);base64,([A-Za-z0-9+/=]{100,})/g;
        
        let match;
        while ((match = dataUrlRegex.exec(pageContent)) !== null) {
            const mimeType = match[1];
            const base64Data = match[2];
            const dataUrl = `data:${mimeType};base64,${base64Data}`;
            
            if (!capturedAudio.some(audio => audio.url === dataUrl)) {
                // 验证是否为有效音频
                validateAudioDataUrl(dataUrl, () => {
                    captureDataUrl(dataUrl, mimeType);
                });
            }
        }
    }
    
    // 验证数据URL是否为有效音频
    function validateAudioDataUrl(dataUrl, callback) {
        const audio = new Audio();
        
        audio.onloadedmetadata = function() {
            // 数据加载成功,是有效音频
            if (audio.duration > 0) {
                callback();
            }
        };
        
        audio.onerror = function() {
            // 尝试作为二进制数据处理
            try {
                fetch(dataUrl)
                    .then(response => response.arrayBuffer())
                    .then(buffer => {
                        // 检查二进制标记
                        const isAudioData = checkAudioSignature(buffer);
                        if (isAudioData) {
                            callback();
                        }
                    });
            } catch (e) {
                // 忽略错误
            }
        };
        
        audio.src = dataUrl;
    }
    
    // 从data URL捕获音频
    function captureDataUrl(dataUrl, mimeType) {
        const audioItem = {
            id: generateId(),
            source: 'dataUrl',
            url: dataUrl,
            contentType: mimeType,
            timestamp: new Date().toISOString(),
            format: guessAudioFormat(mimeType, null),
            size: 'embedded'
        };
        
        capturedAudio.push(audioItem);
        updateAudioCount();
        saveAudioData();
        updateStatus('捕获到data URL音频');
    }
    
    // 检查二进制数据是否为音频
    function checkAudioSignature(buffer) {
        if (!buffer || buffer.byteLength < 8) return false;
        
        const view = new Uint8Array(buffer.slice(0, 16));
        const signatures = {
            // MP3 (ID3)
            'ID3': [0x49, 0x44, 0x33],
            // MP3 (no ID3)
            'MP3': [0xFF, 0xFB],
            // WAV
            'RIFF': [0x52, 0x49, 0x46, 0x46],
            // OGG
            'OGG': [0x4F, 0x67, 0x67, 0x53],
            // FLAC
            'FLAC': [0x66, 0x4C, 0x61, 0x43],
            // M4A/AAC
            'M4A': [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70],
            // FLV
            'FLV': [0x46, 0x4C, 0x56, 0x01]
        };
        
        for (const [format, sig] of Object.entries(signatures)) {
            let match = true;
            for (let i = 0; i < sig.length; i++) {
                if (view[i] !== sig[i]) {
                    match = false;
                    break;
                }
            }
            if (match) return true;
        }
        
        // 检查字符串标记
        try {
            const textDecoder = new TextDecoder('utf-8');
            const text = textDecoder.decode(new Uint8Array(buffer.slice(0, 100)));
            return text.includes('Lavf') || text.includes('matroska') || text.includes('webm');
        } catch (e) {
            return false;
        }
    }
    
    // 从data URL直接下载
    function downloadFromDataUrl() {
        const audioDataUrl = prompt(
            "请粘贴data:application/octet-stream;base64,开头的URL:",
            ""
        );
        
        if (!audioDataUrl || !audioDataUrl.startsWith('data:')) {
            alert('请提供有效的data URL');
            return;
        }
        
        try {
            // 创建下载链接
            const a = document.createElement('a');
            a.href = audioDataUrl;
            a.download = `captured_audio_${Date.now()}.mp3`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            
            updateStatus('音频下载已启动');
            
            // 加入捕获列表
            const mimeType = audioDataUrl.split(';')[0].split(':')[1];
            captureDataUrl(audioDataUrl, mimeType);
        } catch (error) {
            console.error('下载失败:', error);
            alert('下载失败: ' + error.message);
        }
    }
    
    // 处理base64编码的音频数据
    function handleBase64FromRequest() {
        const modal = createModal('处理Base64数据');
        
        const content = document.createElement('div');
        content.innerHTML = `
            <textarea id="base64-input" placeholder="在此粘贴base64编码的音频数据" 
                      style="width: 100%; height: 150px; padding: 8px; margin-bottom: 10px; font-family: monospace;"></textarea>
            <div style="margin-bottom: 10px;">
                <label for="format-select">保存格式:</label>
                <select id="format-select" style="padding: 5px;">
                    <option value="mp3">MP3</option>
                    <option value="wav">WAV</option>
                    <option value="ogg">OGG</option>
                    <option value="flac">FLAC</option>
                </select>
            </div>
            <div style="display: flex; justify-content: flex-end; gap: 10px;">
                <button id="cancel-base64" style="padding: 8px 15px;">取消</button>
                <button id="process-base64-btn" style="padding: 8px 15px; background: #4285f4; color: white; border: none;">处理并下载</button>
            </div>
        `;
        
        modal.appendChild(content);
        
        document.getElementById('cancel-base64').addEventListener('click', () => {
            closeModal(modal);
        });
        
        document.getElementById('process-base64-btn').addEventListener('click', () => {
            const base64Data = document.getElementById('base64-input').value.trim();
            if (!base64Data) {
                alert('请输入base64数据');
                return;
            }
            
            // 移除可能的前缀
            let cleanBase64 = base64Data;
            if (cleanBase64.includes('base64,')) {
                cleanBase64 = cleanBase64.split('base64,')[1];
            }
            
            try {
                // 检查是否为有效base64
                atob(cleanBase64.substring(0, 10));
                
                const format = document.getElementById('format-select').value;
                const mimeTypes = {
                    'mp3': 'audio/mpeg',
                    'wav': 'audio/wav',
                    'ogg': 'audio/ogg',
                    'flac': 'audio/flac'
                };
                
                // 创建完整的data URL
                const dataUrl = `data:${mimeTypes[format] || 'application/octet-stream'};base64,${cleanBase64}`;
                
                // 下载文件
                const a = document.createElement('a');
                a.href = dataUrl;
                a.download = `audio_capture_${Date.now()}.${format}`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                
                // 加入捕获列表
                captureDataUrl(dataUrl, mimeTypes[format]);
                
                closeModal(modal);
                updateStatus('音频处理并下载成功');
            } catch (e) {
                alert('无效的base64数据: ' + e.message);
            }
        });
    }
    
    // 显示已捕获的音频列表
    // 在 showCapturedAudioList 函数中,修改创建模态框的部分
    function showCapturedAudioList() {
        if (capturedAudio.length === 0) {
            alert('尚未捕获任何音频');
            return;
        }
        
        const modal = createModal('已捕获的音频列表');
        
        const content = document.createElement('div');
        content.innerHTML = `
            <div style="margin-bottom: 10px;">
                <input type="text" id="search-audio" placeholder="搜索音频..." 
                    style="width: 100%; padding: 8px; margin-bottom: 10px;">
                <button id="clear-all" style="padding: 5px 10px; background: #db4437; color: white; border: none; float: right;">
                    清空列表
                </button>
                <button id="close-audio-list" style="padding: 5px 10px; background: #f0f0f0; border: 1px solid #ccc; float: right; margin-right: 10px;">
                    关闭
                </button>
            </div>
            <div id="audio-list-container" style="max-height: 400px; overflow-y: auto; margin-top: 40px;"></div>
        `;
        
        modal.appendChild(content);
        
        // 添加关闭按钮事件
        document.getElementById('close-audio-list').addEventListener('click', () => {
            closeModal(modal);
        });
        
        // 显示音频列表
        renderAudioList();
        
        // 搜索功能
        document.getElementById('search-audio').addEventListener('input', function() {
            renderAudioList(this.value);
        });
        
        // 清空列表
        document.getElementById('clear-all').addEventListener('click', function() {
            if (confirm('确定要清空所有已捕获的音频吗?')) {
                capturedAudio = [];
                updateAudioCount();
                saveAudioData();
                closeModal(modal);
                updateStatus('已清空音频列表');
            }
        });
        
        // 渲染音频列表
        function renderAudioList(searchTerm = '') {
            const container = document.getElementById('audio-list-container');
            container.innerHTML = '';
            
            const filteredAudio = searchTerm ? 
                capturedAudio.filter(audio => 
                    audio.url.toLowerCase().includes(searchTerm.toLowerCase()) || 
                    audio.format.toLowerCase().includes(searchTerm.toLowerCase())
                ) : 
                capturedAudio;
            
            if (filteredAudio.length === 0) {
                container.innerHTML = '<p>没有匹配的音频</p>';
                return;
            }
            
            filteredAudio.forEach((audio, index) => {
                const item = document.createElement('div');
                item.style.cssText = `
                    border-bottom: 1px solid #eee;
                    padding: 10px;
                    margin-bottom: 5px;
                `;
                
                const date = new Date(audio.timestamp).toLocaleString();
                const size = typeof audio.size === 'number' ? 
                    (audio.size / 1024).toFixed(2) + ' KB' : 
                    audio.size;
                
                item.innerHTML = `
                    <div style="display: flex; justify-content: space-between;">
                        <div>
                            <strong>#${index + 1}</strong> - ${audio.format.toUpperCase()}
                        </div>
                        <div style="font-size: 12px; color: #666;">
                            ${date}
                        </div>
                    </div>
                    <div title="${audio.url}" style="font-size: 12px; word-break: break-all; margin: 5px 0;">
                        ${getShortUrl(audio.url)}
                    </div>
                    <div style="font-size: 12px; color: #666;">
                        来源: ${audio.source} | 大小: ${size}
                    </div>
                    <div style="display: flex; gap: 5px; margin-top: 5px;">
                        <input type="checkbox" class="audio-select" data-id="${audio.id}" style="margin-right: 5px;">
                        <button class="download-btn" data-id="${audio.id}">下载</button>
                        <button class="play-btn" data-id="${audio.id}">播放</button>
                        <button class="remove-btn" data-id="${audio.id}">删除</button>
                    </div>
                `;
                
                container.appendChild(item);
            });
            
            // 绑定按钮事件
            document.querySelectorAll('.download-btn').forEach(btn => {
                btn.addEventListener('click', function() {
                    const id = this.getAttribute('data-id');
                    downloadAudio(id);
                });
            });
            
            document.querySelectorAll('.play-btn').forEach(btn => {
                btn.addEventListener('click', function() {
                    const id = this.getAttribute('data-id');
                    playAudio(id);
                });
            });
            
            document.querySelectorAll('.remove-btn').forEach(btn => {
                btn.addEventListener('click', function() {
                    const id = this.getAttribute('data-id');
                    removeAudio(id);
                    renderAudioList(searchTerm);
                });
            });
        }
        
        // 下载音频
        function downloadAudio(id) {
            const audio = capturedAudio.find(a => a.id === id);
            if (!audio) return;
            
            if (audio.source === 'dataUrl') {
                // 直接下载data URL
                const a = document.createElement('a');
                a.href = audio.url;
                a.download = `audio_${Date.now()}.${audio.format}`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
            } else if (audio.url) {
                // 下载URL
                GM_download({
                    url: audio.url,
                    name: `audio_${Date.now()}.${audio.format}`,
                    onload: () => updateStatus('下载完成'),
                    onerror: (e) => {
                        console.error('下载失败:', e);
                        updateStatus('下载失败');
                    }
                });
            }
        }
        
        // 播放音频
        function playAudio(id) {
            const audio = capturedAudio.find(a => a.id === id);
            if (!audio) return;
            
            if (audio.source === 'dataUrl') {
                const audioElement = new Audio(audio.url);
                audioElement.play();
            } else if (audio.mediaElement) {
                audio.mediaElement.play();
            } else if (audio.url) {
                const audioElement = new Audio(audio.url);
                audioElement.play();
            }
        }
        
        // 删除音频
        function removeAudio(id) {
            const index = capturedAudio.findIndex(a => a.id === id);
            if (index !== -1) {
                capturedAudio.splice(index, 1);
                updateAudioCount();
                saveAudioData();
                updateStatus('已删除音频');
            }
        }
    }
    
    // 显示合并选项
    function showMergeOptions() {
        if (capturedAudio.length === 0) {
            alert('尚未捕获任何音频');
            return;
        }
        
        const modal = createModal('合并下载音频');
        
        const content = document.createElement('div');
        content.innerHTML = `
            <p>当前有 ${capturedAudio.length} 个已捕获的音频,您可以选择要合并的音频范围:</p>
            <div style="margin: 15px 0;">
                <div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
                    <label for="merge-range" style="min-width: 60px;">合并范围:</label>
                    <input type="text" id="merge-range" placeholder="例如: 1-5,7,9-12" style="flex: 1; padding: 6px;">
                    <button id="select-all-btn" style="padding: 6px 10px;">全选</button>
                </div>
                <div style="margin-bottom: 10px;">
                    <label for="merge-format">输出格式:</label>
                    <select id="merge-format" style="padding: 6px; margin-left: 10px;">
                        <option value="mp3">MP3</option>
                        <option value="wav">WAV</option>
                    </select>
                </div>
                <div style="font-size: 12px; color: #666; margin-top: 5px;">
                    * 范围格式: 单个数字(如5)、范围(如1-5)或组合(如1-3,5,7-9)
                </div>
            </div>
            
            <div style="max-height: 300px; overflow-y: auto; margin: 15px 0; border: 1px solid #eee; padding: 10px;">
                <h4 style="margin-top: 0;">可选择的音频列表:</h4>
                <div id="merge-audio-list"></div>
            </div>
            
            <div style="display: flex; justify-content: flex-end; gap: 10px;">
                <button id="cancel-merge" style="padding: 8px 15px;">取消</button>
                <button id="start-merge" style="padding: 8px 15px; background: #0f9d58; color: white; border: none;">开始合并</button>
            </div>
        `;
        
        modal.appendChild(content);
        
        // 显示音频列表
        const audioListContainer = document.getElementById('merge-audio-list');
        capturedAudio.forEach((audio, index) => {
            const item = document.createElement('div');
            item.style.cssText = `
                display: flex;
                align-items: center;
                padding: 5px 0;
                border-bottom: 1px solid #f0f0f0;
            `;
            
            item.innerHTML = `
                <input type="checkbox" class="merge-select" data-index="${index}" id="merge-item-${index}" style="margin-right: 10px;">
                <label for="merge-item-${index}" style="flex: 1; cursor: pointer;">
                    <strong>#${index + 1}</strong> - ${audio.format.toUpperCase()} 
                    <span style="font-size: 12px; color: #666;">(${getShortUrl(audio.url)})</span>
                </label>
            `;
            
            audioListContainer.appendChild(item);
        });
        
        // 取消按钮
        document.getElementById('cancel-merge').addEventListener('click', () => {
            closeModal(modal);
        });
        
        // 全选按钮
        document.getElementById('select-all-btn').addEventListener('click', () => {
            document.getElementById('merge-range').value = `1-${capturedAudio.length}`;
            document.querySelectorAll('.merge-select').forEach(checkbox => {
                checkbox.checked = true;
            });
        });
        
        // 范围输入框事件
        document.getElementById('merge-range').addEventListener('input', function() {
            const range = this.value.trim();
            if (!range) {
                document.querySelectorAll('.merge-select').forEach(checkbox => {
                    checkbox.checked = false;
                });
                return;
            }
            
            // 解析范围
            const indices = parseRangeString(range, capturedAudio.length);
            
            // 更新复选框
            document.querySelectorAll('.merge-select').forEach(checkbox => {
                const index = parseInt(checkbox.getAttribute('data-index'));
                checkbox.checked = indices.includes(index);
            });
        });
        
        // 复选框变化时更新范围
        document.querySelectorAll('.merge-select').forEach(checkbox => {
            checkbox.addEventListener('change', () => {
                // 获取所有选中的索引
                const selectedIndices = [];
                document.querySelectorAll('.merge-select:checked').forEach(cb => {
                    selectedIndices.push(parseInt(cb.getAttribute('data-index')));
                });
                
                // 生成范围字符串
                document.getElementById('merge-range').value = generateRangeString(selectedIndices);
            });
        });
        
        // 开始合并按钮
        document.getElementById('start-merge').addEventListener('click', () => {
            const range = document.getElementById('merge-range').value.trim();
            if (!range) {
                alert('请选择要合并的音频范围');
                return;
            }
            
            const indices = parseRangeString(range, capturedAudio.length);
            if (indices.length === 0) {
                alert('未选择任何有效的音频');
                return;
            }
            
            const format = document.getElementById('merge-format').value;
            
            // 开始合并
            mergeAudio(indices, format);
            closeModal(modal);
        });
    }
    
    // 解析范围字符串,例如 "1-3,5,7-9"
    function parseRangeString(rangeStr, maxValue) {
        const result = [];
        const parts = rangeStr.split(',');
        
        for (const part of parts) {
            const trimmed = part.trim();
            if (!trimmed) continue;
            
            if (trimmed.includes('-')) {
                // 范围
                const [start, end] = trimmed.split('-').map(n => parseInt(n.trim()));
                // 转换为0-based索引
                const startIndex = Math.max(0, start - 1);
                const endIndex = Math.min(maxValue - 1, end - 1);
                
                if (!isNaN(startIndex) && !isNaN(endIndex) && startIndex <= endIndex) {
                    for (let i = startIndex; i <= endIndex; i++) {
                        if (!result.includes(i)) result.push(i);
                    }
                }
            } else {
                // 单个数字
                const index = parseInt(trimmed) - 1; // 转换为0-based索引
                if (!isNaN(index) && index >= 0 && index < maxValue && !result.includes(index)) {
                    result.push(index);
                }
            }
        }
        
        return result.sort((a, b) => a - b);
    }
    
    // 生成范围字符串
    function generateRangeString(indices) {
        if (indices.length === 0) return '';
        
        // 排序
        indices.sort((a, b) => a - b);
        
        const ranges = [];
        let start = indices[0];
        let end = start;
        
        for (let i = 1; i < indices.length; i++) {
            if (indices[i] === end + 1) {
                end = indices[i];
            } else {
                // 结束当前范围
                if (start === end) {
                    ranges.push((start + 1).toString()); // 转换为1-based显示
                } else {
                    ranges.push(`${start + 1}-${end + 1}`); // 转换为1-based显示
                }
                
                // 开始新范围
                start = end = indices[i];
            }
        }
        
        // 添加最后一个范围
        if (start === end) {
            ranges.push((start + 1).toString()); // 转换为1-based显示
        } else {
            ranges.push(`${start + 1}-${end + 1}`); // 转换为1-based显示
        }
        
        return ranges.join(',');
    }
    
    // 合并音频
    function mergeAudio(indices, format) {
        // 检查索引
        if (indices.length === 0) {
            alert('未选择任何音频');
            return;
        }
        
        // 创建进度模态框
        const modal = createModal('音频合并进度');
        
        const content = document.createElement('div');
        content.innerHTML = `
            <div style="text-align: center; margin: 20px 0;">
                <div id="merge-progress-text">准备合并 ${indices.length} 个音频文件...</div>
                <div style="margin: 15px 0; background: #f0f0f0; border-radius: 4px; overflow: hidden;">
                    <div id="merge-progress-bar" style="width: 0%; height: 20px; background: #0f9d58;"></div>
                </div>
                <div id="merge-status">正在初始化...</div>
            </div>
        `;
        
        modal.appendChild(content);
        
        // 开始合并流程
        setTimeout(() => {
            startMergeProcess(indices, format, modal);
        }, 500);
    }
    
    // 开始合并流程
    async function startMergeProcess(indices, format, modal) {
        try {
            updateMergeProgress(5, '开始下载音频数据...');
            
            // 准备音频数据
            const audioBuffers = [];
            let currentIndex = 0;
            
            for (const index of indices) {
                currentIndex++;
                const progress = 5 + Math.floor((currentIndex / indices.length) * 50);
                updateMergeProgress(progress, `正在处理第 ${currentIndex}/${indices.length} 个音频...`);
                
                const audio = capturedAudio[index];
                if (!audio) continue;
                
                try {
                    const buffer = await getAudioBuffer(audio);
                    if (buffer) {
                        // 如果是MP3格式且需要合并为MP3,直接添加
                        if (format === 'mp3' && (audio.format === 'mp3' || isValidMp3(buffer))) {
                            audioBuffers.push(buffer);
                        } else {
                            // 如果需要转换格式,仍需添加
                            audioBuffers.push(buffer);
                        }
                    }
                } catch (e) {
                    console.error(`处理第 ${index + 1} 个音频时出错:`, e);
                    updateMergeStatus(`处理第 ${index + 1} 个音频时出错: ${e.message}`);
                }
            }
            
            if (audioBuffers.length === 0) {
                updateMergeStatus('没有有效的音频数据可合并');
                setTimeout(() => closeModal(modal), 3000);
                return;
            }
            
            updateMergeProgress(60, `已加载 ${audioBuffers.length} 个音频,开始合并...`);
            
            // 合并音频
            const mergedAudio = await mergeAudioBuffers(audioBuffers, format);
            updateMergeProgress(90, '合并完成,准备下载...');
            
            // 下载合并后的文件
            const fileName = `merged_audio_${Date.now()}.${format}`;
            const blob = new Blob([mergedAudio], { type: format === 'mp3' ? 'audio/mpeg' : 'audio/wav' });
            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);
            
            updateMergeProgress(100, '合并完成,已开始下载!');
            updateStatus(`已成功合并 ${audioBuffers.length} 个音频文件并下载`);
            
            // 3秒后关闭窗口
            setTimeout(() => closeModal(modal), 3000);
        } catch (error) {
            console.error('合并音频过程中出错:', error);
            updateMergeStatus(`合并失败: ${error.message}`);
        }
    }
    
    // 获取音频的ArrayBuffer数据
    // 处理base64编码的音频数据
    async function getAudioBuffer(audio) {
        return new Promise(async (resolve, reject) => {
            try {
                if (audio.data instanceof ArrayBuffer) {
                    // 已经有ArrayBuffer数据
                    resolve(audio.data);
                } else if (audio.source === 'dataUrl') {
                    // 如果是data URL,先检查是否为base64编码的MP3
                    if (audio.url.startsWith('data:application/octet-stream;base64,') || 
                        audio.url.startsWith('data:audio/mpeg;base64,')) {
                        // 直接从data URL获取二进制数据
                        const base64Data = audio.url.split('base64,')[1];
                        const binaryString = atob(base64Data);
                        const bytes = new Uint8Array(binaryString.length);
                        for (let i = 0; i < binaryString.length; i++) {
                            bytes[i] = binaryString.charCodeAt(i);
                        }
                        resolve(bytes.buffer);
                    } else {
                        // 其他类型的data URL
                        fetch(audio.url)
                            .then(response => response.arrayBuffer())
                            .then(buffer => resolve(buffer))
                            .catch(reject);
                    }
                } else if (audio.url) {
                    // 从URL获取
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: audio.url,
                        responseType: 'arraybuffer',
                        onload: function(response) {
                            resolve(response.response);
                        },
                        onerror: function(error) {
                            reject(new Error('无法下载音频: ' + error));
                        }
                    });
                } else {
                    reject(new Error('无法获取音频数据'));
                }
            } catch (e) {
                reject(e);
            }
        });
    }
    
    // 合并音频缓冲区
    // 直接合并MP3文件
    async function mergeAudioBuffers(audioBuffers, format) {
        return new Promise(async (resolve, reject) => {
            try {
                // 如果选择的格式不是MP3,仍使用旧方法
                if (format !== 'mp3') {
                    updateMergeStatus('非MP3格式仍需要完整处理,可能需要较长时间...');
                    // 使用之前的方法处理
                    // ...原来的mergeAudioBuffers代码...
                    return;
                }
                
                updateMergeStatus('正在直接合并MP3文件...');
                
                // 检查每个文件的MP3头,确保是有效的MP3
                const validMp3Buffers = [];
                for (let i = 0; i < audioBuffers.length; i++) {
                    const buffer = audioBuffers[i];
                    // 简单检查是否为MP3文件
                    if (isValidMp3(buffer)) {
                        validMp3Buffers.push(buffer);
                    } else {
                        console.warn(`跳过第${i+1}个非MP3格式文件`);
                    }
                    
                    updateMergeProgress(60 + Math.floor((i / audioBuffers.length) * 30), 
                        `正在处理第 ${i + 1}/${audioBuffers.length} 个文件...`);
                }
                
                if (validMp3Buffers.length === 0) {
                    reject(new Error('没有有效的MP3文件可以合并'));
                    return;
                }
                
                updateMergeStatus(`正在合并 ${validMp3Buffers.length} 个MP3文件...`);
                
                // 直接拼接MP3文件内容
                const totalLength = validMp3Buffers.reduce((total, buffer) => total + buffer.byteLength, 0);
                const mergedMp3 = new Uint8Array(totalLength);
                
                let offset = 0;
                for (const buffer of validMp3Buffers) {
                    const data = new Uint8Array(buffer);
                    mergedMp3.set(data, offset);
                    offset += buffer.byteLength;
                    
                    updateMergeProgress(90, `已合并 ${offset} / ${totalLength} 字节...`);
                }
                
                updateMergeProgress(95, '合并完成,准备下载...');
                resolve(mergedMp3.buffer);
                
            } catch (e) {
                reject(e);
            }
        });
    }

    // 简单检查是否为有效的MP3文件
    function isValidMp3(buffer) {
        if (!buffer || buffer.byteLength < 3) return false;
        
        const view = new Uint8Array(buffer);
        
        // 检查ID3v2标记
        if (view[0] === 0x49 && view[1] === 0x44 && view[2] === 0x33) {
            return true;
        }
        
        // 检查MP3帧头标记 (通常以0xFF开头)
        for (let i = 0; i < Math.min(100, view.length); i++) {
            if (view[i] === 0xFF && (view[i+1] & 0xE0) === 0xE0) {
                return true;
            }
        }
        
        return false;
    }
    
    // 编码为MP3
    // 优化的MP3编码
    function encodeOptimizedMp3(audioBuffer, sampleRate, callback) {
        const channels = audioBuffer.numberOfChannels;
        const mp3encoder = new lamejs.Mp3Encoder(channels, sampleRate, 128);
        
        // 获取音频样本,一次性处理以减少循环次数
        const samples = getInterleavedSamples(audioBuffer);
        const mp3Data = [];
        
        // 使用更大的块大小,减少处理次数
        const chunkSize = 1152 * 10; // 增加批量处理量
        
        // 创建工作器函数用于批处理
        const processChunks = (startIndex) => {
            let endTime = Date.now() + 50; // 每50ms让出主线程
            let currentIndex = startIndex;
            
            while (currentIndex < samples.length && Date.now() < endTime) {
                const end = Math.min(currentIndex + chunkSize, samples.length);
                const chunk = samples.subarray(currentIndex, end);
                const mp3buf = mp3encoder.encodeBuffer(chunk);
                
                if (mp3buf.length > 0) {
                    mp3Data.push(mp3buf);
                }
                
                currentIndex = end;
                
                // 仅在处理一定量数据后更新进度,减少DOM操作
                if (currentIndex % (chunkSize * 10) === 0) {
                    const progress = 80 + Math.floor((currentIndex / samples.length) * 10);
                    updateMergeProgress(progress, `正在编码MP3: ${Math.floor(currentIndex / samples.length * 100)}%...`);
                }
            }
            
            // 所有数据处理完毕或时间片用完
            if (currentIndex < samples.length) {
                // 还有数据要处理,安排下一个时间片
                setTimeout(() => processChunks(currentIndex), 0);
            } else {
                // 所有数据处理完毕,结束编码
                finishEncoding();
            }
        };
        
        // 完成编码
        const finishEncoding = () => {
            const end = mp3encoder.flush();
            if (end.length > 0) {
                mp3Data.push(end);
            }
            
            // 合并所有数据
            const totalLength = mp3Data.reduce((acc, buf) => acc + buf.length, 0);
            const mp3Array = new Uint8Array(totalLength);
            let offset = 0;
            
            for (const buf of mp3Data) {
                mp3Array.set(buf, offset);
                offset += buf.length;
            }
            
            callback(mp3Array.buffer);
        };
        
        // 开始处理
        processChunks(0);
    }

    // 优化的交错样本获取(用于MP3编码)
    function getInterleavedSamples(audioBuffer) {
        const channels = audioBuffer.numberOfChannels;
        const length = audioBuffer.length * channels;
        const result = new Int16Array(length);
        
        // 预先获取所有通道数据,避免反复调用getChannelData
        const channelsData = [];
        for (let channel = 0; channel < channels; channel++) {
            channelsData.push(audioBuffer.getChannelData(channel));
        }
        
        // 块处理,减少循环次数
        const blockSize = 8192;
        for (let blockStart = 0; blockStart < audioBuffer.length; blockStart += blockSize) {
            const blockEnd = Math.min(blockStart + blockSize, audioBuffer.length);
            
            for (let i = blockStart; i < blockEnd; i++) {
                for (let channel = 0; channel < channels; channel++) {
                    const sample = Math.max(-1, Math.min(1, channelsData[channel][i]));
                    result[i * channels + channel] = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
                }
            }
        }
        
        return result;
    }

    // 编码为WAV
    // 优化的WAV编码
    function encodeOptimizedWAV(audioBuffer) {
        const numChannels = audioBuffer.numberOfChannels;
        const sampleRate = audioBuffer.sampleRate;
        const format = 1; // PCM格式
        const bitsPerSample = 16;
        const bytesPerSample = bitsPerSample / 8;
        const blockAlign = numChannels * bytesPerSample;
        const bytesPerSecond = sampleRate * blockAlign;
        
        // 一次性获取所有样本数据
        const samplesData = getWavSamples(audioBuffer);
        
        const buffer = new ArrayBuffer(44 + samplesData.length);
        const view = new DataView(buffer);
        
        // WAV头
        writeString(view, 0, 'RIFF');
        view.setUint32(4, 36 + samplesData.length, true);
        writeString(view, 8, 'WAVE');
        writeString(view, 12, 'fmt ');
        view.setUint32(16, 16, true);
        view.setUint16(20, format, true);
        view.setUint16(22, numChannels, true);
        view.setUint32(24, sampleRate, true);
        view.setUint32(28, bytesPerSecond, true);
        view.setUint16(32, blockAlign, true);
        view.setUint16(34, bitsPerSample, true);
        writeString(view, 36, 'data');
        view.setUint32(40, samplesData.length, true);
        
        // 写入样本数据
        const uint8 = new Uint8Array(buffer);
        uint8.set(samplesData, 44);
        
        return buffer;
    }

    // 优化的WAV样本获取
    function getWavSamples(audioBuffer) {
        const numChannels = audioBuffer.numberOfChannels;
        const length = audioBuffer.length;
        const samples = new Uint8Array(length * numChannels * 2);
        let offset = 0;
        
        // 预先获取所有通道数据
        const channelsData = [];
        for (let channel = 0; channel < numChannels; channel++) {
            channelsData.push(audioBuffer.getChannelData(channel));
        }
        
        // 批量处理样本
        const processBlock = 10000;
        
        for (let blockStart = 0; blockStart < length; blockStart += processBlock) {
            const blockEnd = Math.min(blockStart + processBlock, length);
            
            for (let i = blockStart; i < blockEnd; i++) {
                for (let channel = 0; channel < numChannels; channel++) {
                    const sample = Math.max(-1, Math.min(1, channelsData[channel][i]));
                    const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
                    samples[offset++] = value & 0xFF;
                    samples[offset++] = (value >> 8) & 0xFF;
                }
            }
            
            // 只在每个块完成后更新进度
            const progress = 80 + Math.floor((blockEnd / length) * 10);
            updateMergeProgress(progress, `正在编码WAV: ${Math.floor(blockEnd / length * 100)}%...`);
        }
        
        return samples;
    }
    
    // 辅助函数:写入字符串到DataView
    function writeString(view, offset, string) {
        for (let i = 0; i < string.length; i++) {
            view.setUint8(offset + i, string.charCodeAt(i));
        }
    }
    
    // 更新合并进度
    function updateMergeProgress(percent, message) {
        const progressBar = document.getElementById('merge-progress-bar');
        const progressText = document.getElementById('merge-progress-text');
        
        if (progressBar && progressText) {
            progressBar.style.width = `${percent}%`;
            progressText.textContent = message || `进度: ${percent}%`;
        }
    }
    
    // 更新合并状态
    function updateMergeStatus(message) {
        const statusElement = document.getElementById('merge-status');
        if (statusElement) {
            statusElement.textContent = message;
        }
    }
    
    // 辅助函数:创建模态框
    function createModal(title) {
        // 检查是否已存在模态框
        const existingModal = document.querySelector('.audio-capture-modal-backdrop');
        if (existingModal && document.body.contains(existingModal)) {
            document.body.removeChild(existingModal);
        }
        
        // 创建背景
        const modalBackdrop = document.createElement('div');
        modalBackdrop.className = 'audio-capture-modal-backdrop';
        modalBackdrop.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            z-index: 10000;
            display: flex;
            justify-content: center;
            align-items: center;
        `;
        
        // 创建模态框
        const modal = document.createElement('div');
        modal.className = 'audio-capture-modal';
        modal.style.cssText = `
            background: white;
            border-radius: 8px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
            width: 80%;
            max-width: 600px;
            max-height: 80vh;
            overflow-y: auto;
            z-index: 10001;
            padding: 20px;
        `;
        
        // 标题
        const titleElement = document.createElement('h3');
        titleElement.textContent = title;
        titleElement.style.cssText = `
            margin-top: 0;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #eee;
        `;
        
        // 添加到DOM
        modal.appendChild(titleElement);
        modalBackdrop.appendChild(modal);
        document.body.appendChild(modalBackdrop);
        
        return modal;
    }
    
    // 关闭模态框
    // 修改 closeModal 函数
    function closeModal(modal) {
        try {
            const backdrop = modal.parentElement;
            if (backdrop && document.body.contains(backdrop)) {
                document.body.removeChild(backdrop);
            }
        } catch (e) {
            console.error('关闭模态框时出错:', e);
            // 备用方案:查找并移除所有模态框背景
            document.querySelectorAll('.audio-capture-modal-backdrop').forEach(element => {
                if (document.body.contains(element)) {
                    document.body.removeChild(element);
                }
            });
        }
    }
    
    // 注册GM菜单
    GM_registerMenuCommand('打开豆包音频捕获工具', createMainInterface);
    GM_registerMenuCommand('开始/停止监控', function() {
        if (isMonitoring) {
            stopMonitoring();
            updateStatus('已停止监控网络请求');
        } else {
            startMonitoring();
            updateStatus('正在监控网络请求...');
        }
    });
    GM_registerMenuCommand('查看已捕获的音频', showCapturedAudioList);
    GM_registerMenuCommand('合并下载音频', showMergeOptions);
    
    // 加载保存的音频元数据
    loadAudioData();
    
    // 自动创建UI
    window.addEventListener('load', function() {
        setTimeout(createMainInterface, 1000);
    });
})();