即梦AI全能助手

支持图片/视频下载的即梦AI增强工具(悬浮按钮版+优化图片预览+视频缩略图)

// ==UserScript==
// @name         即梦AI全能助手
// @namespace    http://tampermonkey.net/
// @version      3.31
// @description  支持图片/视频下载的即梦AI增强工具(悬浮按钮版+优化图片预览+视频缩略图)
// @author       Jackey
// @license      GPL-3.0
// @match        https://jimeng.jianying.com/*
// @icon         https://lf3-lv-buz.vlabstatic.com/obj/image-lvweb-buz/common/images/dreamina-v5.ico
// @grant        GM.xmlHttpRequest
// @grant        GM_download
// @grant        GM_addStyle
// @grant        window.open
// @connect      byteimg.com
// @connect      *.byteimg.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        FLOATING_CONTAINER_STYLE: `
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 9999;
            display: flex;
            flex-direction: column;
            gap: 8px;
        `,
        BUTTON_STYLE: `
            cursor: pointer;
            border-radius: 8px;
            padding: 8px 16px;
            transition: all 0.2s;
            background: rgba(255,255,255,0.9);
            border: 1px solid #d9d9d9;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            display: flex;
            align-items: center;
            gap: 8px;
        `,
        HOVER_STYLE: `
            background: #e6f7ff;
            border-color: #91d5ff;
        `,
        VIDEO_PANEL_STYLE: `
            position: fixed;
            bottom: 80px;
            right: 20px;
            background: rgba(255,255,255,0.95);
            border: 1px solid #ddd;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            width: 400px;
            max-height: 70vh;
            overflow: hidden;
            z-index: 9999;
            display: flex;
            flex-direction: column;
        `,
        VIDEO_ITEM_STYLE: `
            display: flex;
            align-items: center;
            padding: 8px;
            border-bottom: 1px solid #eee;
            gap: 12px;
        `,
        PREVIEW_STYLE: `
            width: 120px;
            height: 120px;
            background: rgba(255,255,255,0.9);
            border: 1px solid #ddd;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 8px;
        `,
        VIDEO_SCAN_INTERVAL: 3000,
        PREVIEW_UPDATE_INTERVAL: 1000,
        DEFAULT_THUMBNAIL: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAAA8CAMAAABTa4nrAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAC1QTFRF4eHh+/v78PDw9fX17u7u/f396Ojo5OTk/v7+/Pz8+fn59vb25+fn8/Pz5ubmBUUEgAAAAPFJREFUeNrs1kEKg0AMBND4n0ST+5+3ZSibQltJJFPnbQRxHsOoRj+J8bWzbQYQIECA/wFGwm2tl4CmQvECNCqw/fYCNCCwBSBAgAABAnyE71kKM1sCmlKgBKBRgbrqCmgqB1oCGpXIVlOA3VIIrQO7HBMuAeUoqA4UGymgObDH3pXAJaBQoEGBfSG0CWhuBWICFQNFQDWQsLjVQMJ6qgYSZlY10N1DiS/Ag4Hx8M7hIyYQIECAAAECBAgQ4Ba47zfFMQuBlM9WJiCHlc9ZJqC8YO0EysvWTqC8cO0EykvXTqD8AfRkK0APcKO1AK9/J/vQ2AH8FeA7Ft9K2QAAAABJRU5ErkJggg=='
    };

    class JimengEnhancer {
        constructor() {
            this.videoList = new Map();
            this.floatingContainer = null;
            this.videoPanel = null;
            this.previewArea = null;
            this.isVideoPanelExpanded = false;
            this.currentImageSrc = null;
            this.init();
        }

        init() {
            this.setupDOMObserver();
            this.setupGlobalListener();
            this.initVideoMonitor();
            this.createFloatingButtons();
            this.createVideoPanel();
            this.startPreviewUpdate();
        }

        setupDOMObserver() {
            const Observer = window.MutationObserver || window.WebKitMutationObserver;
            this.observer = new Observer(mutations => {
                mutations.forEach(mutation => {
                    if (mutation.type === 'childList') {
                        this.scanVideos();
                    }
                });
            });

            this.observer.observe(document, {
                childList: true,
                subtree: true
            });
        }

        setupGlobalListener() {
            document.addEventListener('click', this.handleGlobalClick.bind(this));
        }

        createFloatingButtons() {
            if (this.floatingContainer) return;

            this.floatingContainer = document.createElement('div');
            this.floatingContainer.style = CONFIG.FLOATING_CONTAINER_STYLE;

            this.previewArea = document.createElement('div');
            this.previewArea.style = CONFIG.PREVIEW_STYLE;
            this.updatePreview();
            this.floatingContainer.appendChild(this.previewArea);

            ['download', 'video', 'open'].forEach(type => {
                const button = this.generateButton(type);
                this.floatingContainer.appendChild(button);
            });

            document.body.appendChild(this.floatingContainer);
            this.setupDrag(this.floatingContainer);
        }

        generateButton(type) {
            const button = document.createElement('div');
            button.className = `jimeng-${type}-btn`;
            button.style = CONFIG.BUTTON_STYLE;
            button.onmouseover = () => button.style = `${CONFIG.BUTTON_STYLE}${CONFIG.HOVER_STYLE}`;
            button.onmouseout = () => button.style = CONFIG.BUTTON_STYLE;

            const templates = {
                download: () => `
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="#1890ff">
                        <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
                    </svg>
                    <span style="color:#1890ff">下载PNG</span>
                `,
                video: () => `
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="#eb2f96">
                        <path d="M18 3v2h-2V3H8v2H6V3H4v18h2v-2h2v2h8v-2h2v2h2V3h-2zM8 17H6v-2h2v2zm0-4H6v-2h2v2zm0-4H6V7h2v2zm10 8h-2v-2h2v2zm0-4h-2v-2h2v2zm0-4h-2V7h2v2z"/>
                    </svg>
                    <span style="color:#eb2f96">
                        视频下载
                        <span class="video-count" style="margin-left:4px;background:#ff4d4f;color:white;border-radius:10px;padding:0 6px;">
                            ${this.videoList.size}
                        </span>
                    </span>
                `,
                open: () => `
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="#52c41a">
                        <path d="M19 19H5V5h7V3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
                    </svg>
                    <span style="color:#52c41a">打开图片</span>
                `
            };

            button.innerHTML = templates[type]();
            button.onclick = {
                download: () => this.handleDownload(),
                video: () => this.toggleVideoPanel(),
                open: () => this.handleOpen()
            }[type];

            return button;
        }

        getCurrentImage() {
            try {
                const activeImage = document.querySelector('.selected img, .active img, .current img') ||
                    document.querySelector('.imageResult img, .generatedImage img, .previewResult img') ||
                    Array.from(document.querySelectorAll('img'))
                        .filter(img => img.src?.includes('byteimg.com') && img.width > 200 && img.height > 200)
                        .sort((a, b) => (b.width * b.height) - (a.width * a.height))[0];
                return activeImage;
            } catch (error) {
                console.error('图片查找失败:', error);
                return null;
            }
        }

        async updatePreview() {
            const img = this.getCurrentImage();
            const newSrc = img?.src || null;

            if (newSrc !== this.currentImageSrc) {
                this.currentImageSrc = newSrc;
                if (newSrc) {
                    try {
                        const blob = await this.fetchImageBlob(newSrc);
                        const url = URL.createObjectURL(blob);
                        const imgElement = document.createElement('img');
                        imgElement.src = url;
                        imgElement.style = 'max-width:100%;max-height:100%;object-fit:contain;';
                        imgElement.alt = '预览图片';
                        imgElement.onload = () => {
                            this.previewArea.innerHTML = '';
                            this.previewArea.appendChild(imgElement);
                            if (this.lastPreviewUrl && this.lastPreviewUrl !== url) {
                                URL.revokeObjectURL(this.lastPreviewUrl);
                            }
                            this.lastPreviewUrl = url;
                        };
                        imgElement.onerror = () => {
                            this.previewArea.innerHTML = `<span style="color:#888;font-size:12px;text-align:center;">图片加载失败</span>`;
                            URL.revokeObjectURL(url);
                        };
                    } catch (error) {
                        console.error('预览图片加载失败:', error);
                        this.previewArea.innerHTML = `<span style="color:#888;font-size:12px;text-align:center;">图片加载失败</span>`;
                    }
                } else {
                    this.previewArea.innerHTML = `<span style="color:#888;font-size:12px;text-align:center;">未找到有效图片</span>`;
                    if (this.lastPreviewUrl) {
                        URL.revokeObjectURL(this.lastPreviewUrl);
                        this.lastPreviewUrl = null;
                    }
                }
            }
        }

        startPreviewUpdate() {
            setInterval(() => this.updatePreview(), CONFIG.PREVIEW_UPDATE_INTERVAL);
        }

        async handleDownload() {
            const img = this.getCurrentImage();
            if (!img?.src) {
                alert('未找到有效图片');
                return;
            }

            try {
                const { blob, fileName } = await this.convertWebpToPng(img.src);
                const url = URL.createObjectURL(blob);
                const link = document.createElement('a');
                link.href = url;
                link.download = fileName;
                link.click();
                URL.revokeObjectURL(url);
            } catch (error) {
                alert(`下载失败: ${error.message}`);
            }
        }

        handleOpen() {
            const img = this.getCurrentImage();
            if (img?.src) window.open(img.src, '_blank');
        }

        async convertWebpToPng(src) {
            const blob = await this.fetchImageBlob(src);
            return new Promise((resolve) => {
                const img = new Image();
                img.onload = () => {
                    const canvas = document.createElement('canvas');
                    canvas.width = img.width;
                    canvas.height = img.height;
                    canvas.getContext('2d').drawImage(img, 0, 0);
                    canvas.toBlob(pngBlob => resolve({
                        blob: pngBlob,
                        fileName: `jimeng-ai-${Date.now()}.png`
                    }), 'image/png');
                };
                img.src = URL.createObjectURL(blob);
            });
        }

        fetchImageBlob(url) {
            return new Promise((resolve, reject) => {
                GM.xmlHttpRequest({
                    method: 'GET',
                    url: url,
                    responseType: 'blob',
                    onload: res => resolve(res.response),
                    onerror: err => reject(new Error('图片请求失败: ' + err.statusText))
                });
            });
        }

        initVideoMonitor() {
            setInterval(() => this.scanVideos(), CONFIG.VIDEO_SCAN_INTERVAL);
        }

        scanVideos() {
            const videos = document.querySelectorAll('video');
            videos.forEach(video => {
                const src = this.cleanVideoUrl(video.src || video.querySelector('source')?.src);
                if (src && !this.videoList.has(src)) {
                    // 保存视频元素的引用,方便后续获取poster或截图
                    this.videoList.set(src, { 
                        timestamp: Date.now(),
                        videoElement: video,
                        poster: video.poster || null
                    });
                    this.updateVideoCount();
                }
            });
        }

        cleanVideoUrl(url) {
            if (!url || url === 'about:blank') return '';
            try {
                const u = new URL(url.startsWith('/') ? window.location.origin + url : url);
                ['watermark', 'logo', 'w', 'h', 'quality'].forEach(param => u.searchParams.delete(param));
                return u.toString();
            } catch (e) {
                return url;
            }
        }

        createVideoPanel() {
            this.videoPanel = document.createElement('div');
            this.videoPanel.style = `${CONFIG.VIDEO_PANEL_STYLE};display:none;`;
            this.updateVideoPanelContent();
            document.body.appendChild(this.videoPanel);
            this.setupDrag(this.videoPanel);
        }

        updateVideoPanelContent() {
            this.videoPanel.innerHTML = `
                <div style="padding:12px;border-bottom:1px solid #eee;background:#f8f9fa;display:flex;justify-content:space-between;align-items:center;">
                    <span style="font-weight:500;">检测到 ${this.videoList.size} 个视频</span>
                    <button class="collapse-btn" style="padding:4px 8px;background:#ff4d4f;color:white;border:none;border-radius:4px;cursor:pointer;">收起</button>
                </div>
                <div style="flex:1;overflow-y:auto;padding:8px;">
                    ${Array.from(this.videoList.entries()).map(([url, info], index) => `
                        <div style="${CONFIG.VIDEO_ITEM_STYLE}">
                            <img class="video-thumbnail" data-url="${url}" 
                                 src="${info.poster || CONFIG.DEFAULT_THUMBNAIL}" 
                                 style="width:80px;height:60px;object-fit:cover;border-radius:4px;" 
                                 alt="视频缩略图">
                            <div style="flex:1;min-width:0;">
                                <div style="font-size:12px;color:#666;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
                                    视频 ${index + 1} - ${new Date(info.timestamp).toLocaleTimeString()}
                                </div>
                            </div>
                            <button class="download-btn" data-url="${url}" style="padding:4px 8px;background:#007bff;color:white;border:none;border-radius:4px;cursor:pointer;white-space:nowrap;">
                                下载无水印版
                            </button>
                        </div>
                    `).join('')}
                </div>
            `;
            this.videoPanel.className = 'jimeng-enhancer-video-panel';

            const collapseBtn = this.videoPanel.querySelector('.collapse-btn');
            collapseBtn.addEventListener('click', () => this.toggleVideoPanel());

            const downloadBtns = this.videoPanel.querySelectorAll('.download-btn');
            downloadBtns.forEach(btn => {
                btn.addEventListener('click', () => {
                    const url = btn.getAttribute('data-url');
                    GM_download({
                        url: url,
                        name: `即梦视频_${Date.now()}.mp4`,
                        onload: () => {
                            this.videoList.delete(url);
                            this.updateVideoCount();
                            this.updateVideoPanelContent();
                        }
                    });
                });
            });

            const thumbnails = this.videoPanel.querySelectorAll('.video-thumbnail');
            thumbnails.forEach(thumbnail => {
                const url = thumbnail.getAttribute('data-url');
                const videoInfo = this.videoList.get(url);
                
                // 如果没有poster,尝试生成缩略图
                if (!videoInfo.poster) {
                    this.generateVideoThumbnail(url, thumbnail, videoInfo.videoElement);
                }
            });
        }

        generateVideoThumbnail(url, imgElement, videoElement) {
            // 优先使用视频元素生成缩略图
            if (videoElement && !videoElement.seeking && videoElement.readyState >= 2) {
                try {
                    // 直接使用已加载的视频元素生成缩略图
                    const canvas = document.createElement('canvas');
                    canvas.width = 80;
                    canvas.height = 60;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
                    imgElement.src = canvas.toDataURL('image/jpeg', 0.7);
                    
                    // 更新视频列表中的poster信息
                    const videoInfo = this.videoList.get(url);
                    if (videoInfo) {
                        videoInfo.poster = imgElement.src;
                    }
                    return;
                } catch (e) {
                    console.warn('使用已有视频元素生成缩略图失败:', e);
                    // 失败后继续尝试下面的方法
                }
            }
            
            // 创建新的视频元素加载视频
            const tempVideo = document.createElement('video');
            tempVideo.muted = true;
            tempVideo.crossOrigin = "anonymous"; // 允许跨域访问
            tempVideo.preload = "metadata";
            
            // 设置超时以避免长时间加载
            const timeoutId = setTimeout(() => {
                cleanupAndShowFallback();
            }, 5000);
            
            const cleanupAndShowFallback = () => {
                clearTimeout(timeoutId);
                if (document.body.contains(tempVideo)) {
                    document.body.removeChild(tempVideo);
                }
                if (imgElement.src === CONFIG.DEFAULT_THUMBNAIL) {
                    // 只有当图片还是默认图时才需要更新
                    imgElement.src = CONFIG.DEFAULT_THUMBNAIL;
                }
            };
            
            tempVideo.onloadedmetadata = function() {
                try {
                    tempVideo.currentTime = Math.min(1, tempVideo.duration / 3);
                } catch (e) {
                    cleanupAndShowFallback();
                }
            };
            
            tempVideo.onseeked = function() {
                try {
                    // 视频已经跳转到指定时间点,创建缩略图
                    const canvas = document.createElement('canvas');
                    canvas.width = 80;
                    canvas.height = 60;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(tempVideo, 0, 0, canvas.width, canvas.height);
                    imgElement.src = canvas.toDataURL('image/jpeg', 0.7);
                    
                    // 更新视频列表中的poster信息
                    const videoInfo = this.videoList?.get?.(url);
                    if (videoInfo) {
                        videoInfo.poster = imgElement.src;
                    }
                    
                    clearTimeout(timeoutId);
                    document.body.removeChild(tempVideo);
                } catch (e) {
                    cleanupAndShowFallback();
                }
            }.bind(this);
            
            tempVideo.onerror = cleanupAndShowFallback;
            
            // 隐藏视频元素并添加到DOM
            tempVideo.style.display = 'none';
            document.body.appendChild(tempVideo);
            
            // 最后设置src以开始加载
            tempVideo.src = url;
        }

        updateVideoCount() {
            const countBadge = this.floatingContainer?.querySelector('.video-count');
            if (countBadge) countBadge.textContent = this.videoList.size;
            if (this.isVideoPanelExpanded) this.updateVideoPanelContent();
        }

        toggleVideoPanel() {
            this.isVideoPanelExpanded = !this.isVideoPanelExpanded;
            this.videoPanel.style.display = this.isVideoPanelExpanded ? 'flex' : 'none';
            const videoBtn = this.floatingContainer.querySelector('.jimeng-video-btn');
            if (videoBtn) videoBtn.style.display = this.isVideoPanelExpanded ? 'none' : 'flex';
            if (this.isVideoPanelExpanded) this.updateVideoPanelContent();
        }

        setupDrag(element) {
            let isDragging = false, startX, startY, initialX, initialY;
            const header = element.querySelector('div:first-child');

            header.addEventListener('mousedown', e => {
                isDragging = true;
                startX = e.clientX;
                startY = e.clientY;
                initialX = element.offsetLeft;
                initialY = element.offsetTop;
            });

            document.addEventListener('mousemove', e => {
                if (!isDragging) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                element.style.right = 'auto';
                element.style.bottom = 'auto';
                element.style.left = `${initialX + dx}px`;
                element.style.top = `${initialY + dy}px`;
            });

            document.addEventListener('mouseup', () => {
                isDragging = false;
            });
        }

        handleGlobalClick(event) {
            if (!event.target.closest('.jimeng-enhancer-video-panel, .jimeng-download-btn, .jimeng-video-btn, .jimeng-open-btn')) {
                return;
            }
            event.stopImmediatePropagation();
        }
    }

    new JimengEnhancer();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址