Greasy Fork

来自缓存

Greasy Fork is available in English.

Xiaomi MiMo Studio 去水印

自动检测并移除 Xiaomi MiMo Studio 页面中的水印内容(Canvas水印)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Xiaomi MiMo Studio 去水印
// @namespace    https://github.com/wang93wei/Xiaomi-MiMo-Studio-Watermark-Remover
// @version      1.4.0
// @description  自动检测并移除 Xiaomi MiMo Studio 页面中的水印内容(Canvas水印)
// @author       AlanWang
// @license      MIT
// @homepageURL  https://github.com/wang93wei/Xiaomi-MiMo-Studio-Watermark-Remover
// @supportURL   https://github.com/wang93wei/Xiaomi-MiMo-Studio-Watermark-Remover/issues
// @match        https://aistudio.xiaomimimo.com/*
// @match        https://aistudio.xiaomimimo.com/#/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ========== 配置常量 ==========
    const CONFIG = {
        // 日志配置
        ENABLE_LOG: false,

        // API配置
        API_URL: 'https://aistudio.xiaomimimo.com/open-apis/user/mi/get',
        DEFAULT_TIMEZONE: 'Asia/Shanghai',
        FETCH_TIMEOUT: 10000,

        // 水印文本配置
        MAX_WATERMARK_LENGTH: 500,
        MIN_WATERMARK_LENGTH: 1,
        BASE64_MATCH_MIN_LENGTH: 20,
        BASE64_MATCH_MAX_LENGTH: 50,

        // Canvas检测配置
        VIEWPORT_COVERAGE_THRESHOLD: 0.9,

        // 重试配置
        MAX_RETRIES: 5,
        PAGE_LOAD_RETRIES: 3,
        INITIAL_RETRY_DELAY: 1000,
        RETRY_BACKOFF_MULTIPLIER: 1.5,

        // 轮询配置
        MAX_POLL_COUNT: 20,
        POLL_INTERVAL: 500,

        // 页面加载配置
        PAGE_LOAD_WAIT_TIME: 2000,

        // 安全配置
        MAX_REPEATED_CHARS: 10,
        MAX_REPEATED_SUBSTRINGS: 5,
        MAX_NESTED_BRACKETS: 20,

        // Canvas拦截配置
        ENABLE_CANVAS_INTERCEPT: true,
    };

    // ========== 错误统计 ==========
    const errorStats = {
        fetchErrors: 0,
        canvasErrors: 0,
    };

    // ========== 日志工具 ==========
    const logger = {
        log: (...args) => {
            if (CONFIG.ENABLE_LOG) {
                console.log('[去水印脚本]', ...args);
            }
        },
        warn: (...args) => {
            if (CONFIG.ENABLE_LOG) {
                console.warn('[去水印脚本]', ...args);
            }
        },
        error: (...args) => {
            console.error('[去水印脚本]', ...args);
        },
        stat: (type) => {
            if (Object.prototype.hasOwnProperty.call(errorStats, type)) {
                errorStats[type]++;
            }
        },
        getStats: () => ({ ...errorStats }),
    };

    // ========== 状态管理 ==========
    const state = {
        watermarkText: null,
        watermarkCandidates: [],
        processedElements: new WeakSet(),
        pollTimer: null,
        resizeHandler: null,
    };

    // ========== 工具函数 ==========
    const formatErrorContext = (error, additionalContext = {}) => ({
        error: error?.message,
        stack: error?.stack,
        timestamp: new Date().toISOString(),
        url: window.location.href,
        userAgent: navigator.userAgent,
        ...additionalContext,
    });

    const containsWatermark = (text) => {
        if (text == null || typeof text !== 'string') return false;
        if (!Array.isArray(state.watermarkCandidates) || state.watermarkCandidates.length === 0) return false;
        return state.watermarkCandidates.some((candidate) => text.includes(candidate));
    };

    const isSafeWatermarkText = (text) => {
        if (!text || typeof text !== 'string') return false;
        if (text.length === 0 || text.length > CONFIG.MAX_WATERMARK_LENGTH) return false;

        const dangerousPatterns = [
            new RegExp(`(.+)\\1{${CONFIG.MAX_REPEATED_CHARS},}`),
            new RegExp(`(.+?)(\\1+){${CONFIG.MAX_REPEATED_SUBSTRINGS},}`),
            new RegExp(`[\\(\\)\\[\\]\\{\\}]{${CONFIG.MAX_NESTED_BRACKETS},}`),
        ];

        return !dangerousPatterns.some((pattern) => pattern.test(text));
    };

    const rebuildWatermarkCandidates = () => {
        const candidates = [];

        if (!state.watermarkText || typeof state.watermarkText !== 'string') {
            state.watermarkCandidates = [];
            logger.warn('WATERMARK_TEXT 无效,清空候选列表');
            return;
        }

        candidates.push(state.watermarkText);

        try {
            const decoded = atob(state.watermarkText);
            if (decoded && typeof decoded === 'string') {
                candidates.push(decoded);

                try {
                    const bytes = new Uint8Array(decoded.length);
                    for (let i = 0; i < decoded.length; i++) {
                        bytes[i] = decoded.charCodeAt(i);
                    }
                    const utf8 = new TextDecoder('utf-8').decode(bytes);
                    if (utf8 && typeof utf8 === 'string' && utf8 !== decoded) {
                        candidates.push(utf8);
                    }
                } catch {
                    // UTF-8 解码失败,忽略
                }
            }
        } catch {
            // Base64 解码失败,忽略
        }

        state.watermarkCandidates = Array.from(
            new Set(candidates.filter((c) => c && typeof c === 'string' && c.length > 0))
        );
        logger.log('水印匹配候选:', state.watermarkCandidates);
    };

    const cleanup = () => {
        if (state.pollTimer) {
            clearInterval(state.pollTimer);
            state.pollTimer = null;
        }
        if (state.resizeHandler) {
            window.removeEventListener('resize', state.resizeHandler);
            state.resizeHandler = null;
        }
    };

    // ========== Canvas拦截 ==========
    const interceptCanvas = () => {
        if (!CONFIG.ENABLE_CANVAS_INTERCEPT) {
            logger.log('Canvas 拦截已禁用,跳过');
            return;
        }

        if (CanvasRenderingContext2D.prototype._watermarkIntercepted) {
            logger.log('Canvas 拦截器已安装,跳过重复安装');
            return;
        }

        const interceptMethod = (prototype, methodName, originalMethod) => {
            Object.defineProperty(prototype, methodName, {
                value: function(...args) {
                    try {
                        const text = args[0];
                        if (text && containsWatermark(String(text))) {
                            return;
                        }
                        return originalMethod.apply(this, args);
                    } catch (e) {
                        logger.warn(`${methodName} 拦截出错,使用原始实现:`, e);
                        logger.stat('canvasErrors');
                        return originalMethod.apply(this, args);
                    }
                },
                writable: true,
                configurable: true,
            });
        };

        const interceptDrawImage = (prototype, methodName, originalMethod) => {
            Object.defineProperty(prototype, methodName, {
                value: function(...args) {
                    try {
                        const image = args[0];
                        if (image?.src && containsWatermark(image.src)) {
                            return;
                        }
                        return originalMethod.apply(this, args);
                    } catch (e) {
                        logger.warn(`${methodName} 拦截出错,使用原始实现:`, e);
                        logger.stat('canvasErrors');
                        return originalMethod.apply(this, args);
                    }
                },
                writable: true,
                configurable: true,
            });
        };

        try {
            const originalFillText = CanvasRenderingContext2D.prototype.fillText;
            const originalStrokeText = CanvasRenderingContext2D.prototype.strokeText;
            const originalDrawImage = CanvasRenderingContext2D.prototype.drawImage;

            interceptMethod(CanvasRenderingContext2D.prototype, 'fillText', originalFillText);
            interceptMethod(CanvasRenderingContext2D.prototype, 'strokeText', originalStrokeText);
            interceptDrawImage(CanvasRenderingContext2D.prototype, 'drawImage', originalDrawImage);

            Object.defineProperty(CanvasRenderingContext2D.prototype, '_watermarkIntercepted', {
                value: true,
                writable: true,
                configurable: true,
            });
        } catch (e) {
            logger.error('拦截 CanvasRenderingContext2D 失败:', e);
            logger.stat('canvasErrors');
        }

        try {
            if (typeof OffscreenCanvasRenderingContext2D !== 'undefined' &&
                !OffscreenCanvasRenderingContext2D.prototype._watermarkIntercepted) {
                const offscreenOriginalFillText = OffscreenCanvasRenderingContext2D.prototype.fillText;
                const offscreenOriginalStrokeText = OffscreenCanvasRenderingContext2D.prototype.strokeText;
                const offscreenOriginalDrawImage = OffscreenCanvasRenderingContext2D.prototype.drawImage;

                interceptMethod(OffscreenCanvasRenderingContext2D.prototype, 'fillText', offscreenOriginalFillText);
                interceptMethod(OffscreenCanvasRenderingContext2D.prototype, 'strokeText', offscreenOriginalStrokeText);
                interceptDrawImage(OffscreenCanvasRenderingContext2D.prototype, 'drawImage', offscreenOriginalDrawImage);

                Object.defineProperty(OffscreenCanvasRenderingContext2D.prototype, '_watermarkIntercepted', {
                    value: true,
                    writable: true,
                    configurable: true,
                });
            }
        } catch (e) {
            logger.warn('拦截 OffscreenCanvas 失败:', e);
            logger.stat('canvasErrors');
        }
    };

    const clearSuspectedWatermarkCanvases = () => {
        const canvases = document.querySelectorAll('canvas');
        if (!canvases?.length) return;

        canvases.forEach((canvas) => {
            if (!canvas || state.processedElements.has(canvas)) return;

            try {
                const style = window.getComputedStyle(canvas);
                if (!style) return;

                const positionLikely = style.position === 'fixed' || style.position === 'absolute';
                const pointerEventsNone = style.pointerEvents === 'none';

                const rect = canvas.getBoundingClientRect();
                const vw = window.innerWidth ?? 0;
                const vh = window.innerHeight ?? 0;
                const coversMostViewport = vw > 0 && vh > 0 &&
                    rect.width >= vw * CONFIG.VIEWPORT_COVERAGE_THRESHOLD &&
                    rect.height >= vh * CONFIG.VIEWPORT_COVERAGE_THRESHOLD;

                if (coversMostViewport && (positionLikely || pointerEventsNone)) {
                    const ctx2d = canvas.getContext('2d');
                    ctx2d?.clearRect(0, 0, canvas.width, canvas.height);

                    if (canvas.style) {
                        canvas.style.backgroundImage = 'none';
                        canvas.style.background = 'none';
                        canvas.style.opacity = '0';
                        canvas.style.visibility = 'hidden';
                        canvas.style.display = 'none';
                    }
                    state.processedElements.add(canvas);
                }
            } catch (e) {
                logger.warn('清理Canvas水印时出错:', e);
            }
        });
    };

    // ========== API请求 ==========
    const fetchWatermark = async () => {
        try {
            logger.log('开始获取水印内容...');
            const browserTimeZone = typeof Intl !== 'undefined' && Intl.DateTimeFormat
                ? (Intl.DateTimeFormat().resolvedOptions().timeZone ?? CONFIG.DEFAULT_TIMEZONE)
                : CONFIG.DEFAULT_TIMEZONE;

            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), CONFIG.FETCH_TIMEOUT);

            let response;
            try {
                response = await fetch(CONFIG.API_URL, {
                    method: 'GET',
                    headers: {
                        accept: 'application/json',
                        'content-type': 'application/json',
                        'x-timezone': browserTimeZone,
                    },
                    credentials: 'include',
                    signal: controller.signal,
                });
            } catch (networkError) {
                clearTimeout(timeoutId);

                const errorContext = formatErrorContext(networkError, {
                    url: CONFIG.API_URL,
                    timeout: CONFIG.FETCH_TIMEOUT,
                });

                if (networkError.name === 'AbortError') {
                    logger.error('获取水印请求超时', errorContext);
                } else if (networkError.name === 'TypeError') {
                    logger.error('网络错误,请检查网络连接', errorContext);
                } else if (networkError.name === 'SecurityError') {
                    logger.error('安全错误,可能是跨域问题', errorContext);
                } else {
                    logger.error('网络请求失败', errorContext);
                }
                logger.stat('fetchErrors');
                return false;
            } finally {
                clearTimeout(timeoutId);
            }

            if (!response.ok) {
                logger.error(`HTTP error! status: ${response.status}`);
                logger.stat('fetchErrors');
                return false;
            }

            let result;
            try {
                result = await response.json();
            } catch (jsonError) {
                try {
                    const responseText = await response.text();
                    logger.error('解析 API 响应失败:', jsonError, '响应内容:', responseText);
                } catch (textError) {
                    logger.error('无法读取响应内容:', textError);
                }
                logger.stat('fetchErrors');
                return false;
            }
            logger.log('API 响应:', result);

            if (!result || typeof result !== 'object') {
                logger.warn('API 响应格式无效');
                return false;
            }

            if (typeof result.code !== 'number') {
                logger.warn('API 响应缺少 code 字段');
                return false;
            }

            if (result.code !== 0) {
                logger.warn('API 返回错误码:', result.code);
                return false;
            }

            if (!result.data || typeof result.data !== 'object') {
                logger.warn('API 响应缺少 data 字段');
                return false;
            }

            if (!result.data.watermark || typeof result.data.watermark !== 'string') {
                logger.warn('API 响应缺少 watermark 字段');
                return false;
            }

            state.watermarkText = result.data.watermark;
            rebuildWatermarkCandidates();
            logger.log('成功获取水印内容:', state.watermarkText);
            return true;
        } catch (error) {
            logger.error('获取水印内容时发生未知错误:', formatErrorContext(error));
            logger.stat('fetchErrors');
            return false;
        }
    };

    const detectWatermarkFromPage = () => {
        try {
            if (state.watermarkText) return true;

            logger.log('尝试从页面中检测水印...');

            if (!document.body) return false;

            const base64Pattern = new RegExp(`[A-Za-z0-9+/]{${CONFIG.BASE64_MATCH_MIN_LENGTH},}={0,2}`, 'g');
            const allText = document.body.innerText ?? '';
            const matches = allText.match(base64Pattern);

            if (!matches?.length) return false;

            for (const match of matches) {
                if (match.length >= CONFIG.MIN_WATERMARK_LENGTH &&
                    match.length <= CONFIG.BASE64_MATCH_MAX_LENGTH) {
                    const walker = document.createTreeWalker(
                        document.body,
                        NodeFilter.SHOW_TEXT,
                        null
                    );

                    let node;
                    while ((node = walker.nextNode())) {
                        const textContent = node.textContent ?? '';
                        if (textContent.includes(match)) {
                            state.watermarkText = match;
                            rebuildWatermarkCandidates();
                            logger.log('从页面检测到水印:', state.watermarkText);
                            return true;
                        }
                    }
                }
            }

            return false;
        } catch (error) {
            logger.error('从页面检测水印时出错:', formatErrorContext(error));
            return false;
        }
    };

    const fetchWatermarkWithRetry = async (maxRetries = CONFIG.MAX_RETRIES, delay = CONFIG.INITIAL_RETRY_DELAY) => {
        for (let i = 0; i < maxRetries; i++) {
            logger.log(`尝试获取水印 (${i + 1}/${maxRetries})...`);

            const success = await fetchWatermark();
            if (success && state.watermarkText) {
                return true;
            }

            if (i < maxRetries - 1) {
                logger.log(`等待 ${delay}ms 后重试...`);
                await new Promise((resolve) => setTimeout(resolve, delay));
                delay *= CONFIG.RETRY_BACKOFF_MULTIPLIER;
            }
        }

        logger.log('API 获取失败,尝试从页面检测水印...');
        if (detectWatermarkFromPage()) {
            return true;
        }

        logger.error('无法获取水印内容,脚本将无法正常工作');
        return false;
    };

    // ========== 启动水印移除 ==========
    const startWatermarkRemoval = () => {
        if (!state.watermarkText) {
            logger.warn('水印内容为空,无法启动移除功能');
            return false;
        }

        logger.log('启动水印移除功能,水印内容:', state.watermarkText);

        try {
            interceptCanvas();
            clearSuspectedWatermarkCanvases();
        } catch (error) {
            logger.error('启动水印移除功能时出错:', error);
            return false;
        }

        cleanup();

        let pollCount = 0;

        state.pollTimer = setInterval(() => {
            pollCount++;
            if (pollCount > CONFIG.MAX_POLL_COUNT) {
                clearInterval(state.pollTimer);
                state.pollTimer = null;
                logger.log('轮询检测完成');
                return;
            }

            clearSuspectedWatermarkCanvases();
        }, CONFIG.POLL_INTERVAL);

        state.resizeHandler = () => {
            logger.log('窗口大小改变,重新检测水印');
            clearSuspectedWatermarkCanvases();
        };
        window.addEventListener('resize', state.resizeHandler);

        return true;
    };

    // ========== 主函数 ==========
    const main = async () => {
        logger.log('脚本开始运行...');

        const watermarkFetched = await fetchWatermarkWithRetry();

        if (watermarkFetched && state.watermarkText) {
            startWatermarkRemoval();
        } else {
            logger.log('等待页面完全加载后重试...');
            window.addEventListener('load', async () => {
                await new Promise((resolve) => setTimeout(resolve, CONFIG.PAGE_LOAD_WAIT_TIME));
                const retrySuccess = await fetchWatermarkWithRetry(CONFIG.PAGE_LOAD_RETRIES, CONFIG.INITIAL_RETRY_DELAY * 2);
                if (retrySuccess && state.watermarkText) {
                    startWatermarkRemoval();
                } else if (detectWatermarkFromPage()) {
                    startWatermarkRemoval();
                }
            });
        }

        const stats = logger.getStats();
        const totalErrors = stats.fetchErrors + stats.canvasErrors;
        if (totalErrors > 0) {
            console.warn('[去水印脚本] 错误统计:', stats);
        }
    };

    // ========== 启动脚本 ==========
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }

})();