您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
OpenWebUI 强制 HTML 图片渲染, 识别 <img> 标签,基于 OpenWebUI v0.6.10 开发。
当前为
// ==UserScript== // @name OpenWebUI Force HTML Image Renderer // @version 1.0.0 // @description OpenWebUI 强制 HTML 图片渲染, 识别 <img> 标签,基于 OpenWebUI v0.6.10 开发。 // @author B3000Kcn // @match https://your.openwebui.url/* // @run-at document-idle // @license MIT // @namespace https://greasyfork.org/users/1474401 // ==/UserScript== (function() { 'use strict'; const SCRIPT_VERSION = '1.0.0'; const TARGET_DIV_DEPTH = 22; // 移除了启动日志,如果需要可以取消下面一行的注释 // console.log(`OpenWebUI 图片强制渲染脚本 v${SCRIPT_VERSION} 已启动。`); const PROCESSED_CLASS = `html-img-rendered-v1-0-0`; // 建议与脚本版本匹配 const ALLOWED_INNERHTML_MOD_TAGS = ['P', 'SPAN', 'PRE', 'CODE', 'LI', 'BLOCKQUOTE', 'FIGCAPTION', 'LABEL', 'SMALL', 'STRONG', 'EM', 'B', 'I', 'U', 'SUB', 'SUP', 'MARK', 'DEL', 'INS', 'TD', 'TH', 'DT', 'DD']; const TAGS_TO_SKIP_PROCESSING = ['SCRIPT', 'STYLE', 'HEAD', 'META', 'LINK', 'TITLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'BUTTON', 'SELECT', 'FORM', 'IFRAME', 'VIDEO', 'AUDIO', 'CANVAS', 'SVG', 'IMG']; const escapedImgTagRegex = /<img\s+([^&]*?)>/gi; const directImgTagRegex = /<img\s+([^>]*)>/gi; function getElementDepth(element) { let depth = 0; let el = element; while (el) { depth++; el = el.parentElement; } return depth; } function getAttributeValue(attributesString, attributeName) { if (!attributesString || !attributeName) return null; try { const regex = new RegExp(`${attributeName}\\s*=\\s*(["'])(.*?)\\1`, 'i'); const match = attributesString.match(regex); return match ? match[2] : null; } catch (e) { // 在生产版本中,可以选择只在内部处理错误,不向控制台输出,或者保留关键错误输出 // console.error(`${SCRIPT_VERSION} 错误: getAttributeValue 执行失败 for ${attributeName}`, e); return null; } } function processImageTag(match, attributesStringOriginal, isEscaped) { let matchPreview = ''; // 主要用于日志,生产版中可以考虑移除或简化 if (typeof match === 'string' && match.length > 0) { matchPreview = match.substring(0, 60).replace(/\s+/g,' ') + (match.length > 60 ? "..." : ""); } else { return match; // 如果match无效,直接返回 } if (!isEscaped && match.startsWith("<img") && match.includes('data-force-rendered="true"')) { return match; } if (!isEscaped && match.startsWith("<div class=\"userscript-image-paragraph\"") && match.includes('data-force-rendered="true"')) { return match; } const attributesString = attributesStringOriginal.trim(); const src = getAttributeValue(attributesString, 'src'); try { let isSrcInvalid = true; if (src && typeof src === 'string') { if (src.toLowerCase().startsWith('javascript:')) { // console.warn(`警告: 检测到 javascript: SRC 并已阻止.`); // 移除具体日志前缀 } else { isSrcInvalid = false; } } else { // if (src !== null || (attributesString && attributesString.includes("src="))) { // console.warn(`警告: 无效或缺失 src. src: ${src}, 类型: ${typeof src}.`); // } } if (isSrcInvalid) { return match; } let alt, width, height; try { alt = getAttributeValue(attributesString, 'alt'); width = getAttributeValue(attributesString, 'width'); height = getAttributeValue(attributesString, 'height'); const img = document.createElement('img'); img.src = src; img.alt = alt ? alt : `用户HTML图片 (${isEscaped ? '转义' : '直接'}) v${SCRIPT_VERSION}`; img.setAttribute('data-force-rendered', 'true'); if (width) { if (width.includes('%')) { img.style.width = width; } else { img.setAttribute('width', width.replace(/px$/i, '')); } } if (height) { if (height.includes('%')) { img.style.height = height; } else { img.setAttribute('height', height.replace(/px$/i, '')); } } img.style.maxWidth = '100%'; img.style.maxHeight = '300px'; img.style.display = 'block'; img.onerror = function() { // console.warn(`警告: 图片加载失败: ${this.src}. 将隐藏元素.`); // 移除具体日志前缀 this.style.display = 'none'; // 保留隐藏功能 }; const imgHTML = img.outerHTML; const pWrappedHTML = `<p class="userscript-image-paragraph" style="margin: 0.5em 0; line-height: normal;">${imgHTML}</p>`; return pWrappedHTML; } catch (eInner) { // console.error(`错误: 在主 try 块中发生错误, 返回原始match.`, eInner); return match; } } catch (eOuter) { // console.error(`错误: 在最外层 try 块中发生错误, 返回原始match.`, eOuter); return match; } } function renderImagesInElement(element, forcedByObserver = false) { if (!element || typeof element.classList === 'undefined' || TAGS_TO_SKIP_PROCESSING.includes(element.tagName.toUpperCase()) || element.isContentEditable) { return; } const isTargetDepthDiv = (element.tagName.toUpperCase() === 'DIV' && getElementDepth(element) === TARGET_DIV_DEPTH); let isAllowedNonDiv = ALLOWED_INNERHTML_MOD_TAGS.includes(element.tagName.toUpperCase()); if (element.classList.contains(PROCESSED_CLASS)) { if ( (isTargetDepthDiv && forcedByObserver) || (isAllowedNonDiv && forcedByObserver) ) { element.classList.remove(PROCESSED_CLASS); } else { return; } } if (typeof element.innerHTML !== 'string') { return; } let htmlContent = element.innerHTML; let madeChange = false; const originalHtmlContent = htmlContent; const replacerFunc = (type) => (fullMatch, attrs) => { const newImgHtml = processImageTag(fullMatch, attrs, type === 'escaped'); if (newImgHtml !== fullMatch) { madeChange = true; } return newImgHtml; }; escapedImgTagRegex.lastIndex = 0; if (htmlContent.includes('<img')) { if (escapedImgTagRegex.test(htmlContent)) { escapedImgTagRegex.lastIndex = 0; htmlContent = htmlContent.replace(escapedImgTagRegex, replacerFunc('escaped')); } } directImgTagRegex.lastIndex = 0; if (htmlContent.includes('<img')) { if (directImgTagRegex.test(htmlContent)) { directImgTagRegex.lastIndex = 0; const tempHtmlContent = htmlContent.replace(directImgTagRegex, replacerFunc('direct')); if (htmlContent !== tempHtmlContent) { htmlContent = tempHtmlContent; } } } let allowFinalModification = isAllowedNonDiv || isTargetDepthDiv; if (madeChange) { if (allowFinalModification) { // if (isTargetDepthDiv && !isAllowedNonDiv) { // console.info(`DIV 在目标深度 ${TARGET_DIV_DEPTH},允许修改.`); // } if (originalHtmlContent !== htmlContent) { element.innerHTML = htmlContent; } if (!isTargetDepthDiv) { if (!element.classList.contains(PROCESSED_CLASS)) { element.classList.add(PROCESSED_CLASS); } } } else { if (!isTargetDepthDiv && !element.classList.contains(PROCESSED_CLASS)) { element.classList.add(PROCESSED_CLASS); } } } else if (!element.classList.contains(PROCESSED_CLASS) && (originalHtmlContent.includes('<img') || originalHtmlContent.includes('<img'))) { if (!isTargetDepthDiv) { element.classList.add(PROCESSED_CLASS); } } } // 单独、正确地定义 observerCallback, initialScan, activateScript const observerCallback = (mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { renderImagesInElement(node, true); if (typeof node.querySelectorAll === 'function') { node.querySelectorAll(ALLOWED_INNERHTML_MOD_TAGS.join(',')).forEach(child => { if (child.nodeType === Node.ELEMENT_NODE) renderImagesInElement(child, true); }); node.querySelectorAll('div').forEach(childDiv => { if (childDiv.nodeType === Node.ELEMENT_NODE && getElementDepth(childDiv) === TARGET_DIV_DEPTH) { renderImagesInElement(childDiv, true); } }); } } }); } else if (mutation.type === 'characterData') { if (mutation.target && mutation.target.parentElement) { renderImagesInElement(mutation.target.parentElement, true); } } } }; const observer = new MutationObserver(observerCallback); const observerConfig = { childList: true, subtree: true, characterData: true }; function initialScan() { const allRelevantSelectors = ALLOWED_INNERHTML_MOD_TAGS.join(',') + ',div'; document.querySelectorAll(allRelevantSelectors).forEach(el => { if (el.offsetParent !== null || document.body.contains(el)) { renderImagesInElement(el, false); } }); } function activateScript() { setTimeout(() => { initialScan(); observer.observe(document.body, observerConfig); }, 2000); } if (document.readyState === 'complete' || document.readyState === 'interactive') { activateScript(); } else { document.addEventListener('DOMContentLoaded', activateScript, { once: true }); } })();