Greasy Fork is available in English.
自动检测并移除 Xiaomi MiMo Studio 页面中的水印内容(Canvas水印)
// ==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();
}
})();