// ==UserScript== // @name 【SillyTavern / ST酒馆】html代码注入器 // @name:zh 【ST酒馆】html代码注入器 // @name:zh-CN 【ST酒馆】html代码注入器 // @name:en 【SillyTavern】 HTML Code Injector // @namespace https://greasyfork.org/users/Qianzhuo // @version 1.0.22 // @description 可以让ST酒馆独立运行html代码 (Inject HTML code into SillyTavern pages.) // @description:zh 可以让ST酒馆独立运行html代码 // @description:zh-CN 可以让ST酒馆独立运行html代码 // @description:en Inject HTML code into SillyTavern pages. // @author Qianzhuo // @match *://localhost:8000/* // @match *://127.0.0.1:8000/* // @match *://*/*:8000/* // @include /^https?:\/\/.*:8000\// // @grant GM_setValue // @grant GM_getValue // @require https://code.jquery.com/jquery-3.6.0.min.js // @license CC BY-NC 4.0 // @downloadURL none // ==/UserScript== /* 【SillyTavern / ST酒馆】html代码注入器 © 2024 by Qianzhuo is licensed under CC BY-NC 4.0. To view a copy of this license, visit https://creativecommons.org/licenses/by-nc/4.0/ */ (function () { 'use strict'; let isInjectionEnabled = false; let displayMode = GM_getValue('displayMode', 1); // 从存储中获取,默认为1 let lastMesTextContent = ''; // 存储激活楼层的设置 let activationMode = GM_getValue('activationMode', 'all'); // 默认激活所有楼层 let customStartFloor = GM_getValue('customStartFloor', 1); let customEndFloor = GM_getValue('customEndFloor', -1); // -1 表示最后一层 // 创建设置面板 const settingsPanel = document.createElement('div'); settingsPanel.innerHTML = `
HTML注入器设置

边缘控制面板位置

显示模式

激活楼层

`; settingsPanel.id = 'html-injector-settings'; settingsPanel.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; background-color: #1e1e1e; border-bottom: 1px solid #454545; padding: 20px; z-index: 9999; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; box-shadow: 0 2px 10px rgba(0,0,0,0.3); display: none; color: #d4d4d4; overflow-y: auto; max-height: 50vh; `; document.body.appendChild(settingsPanel); // 处理激活楼层的设置 document.getElementById('activation-mode').addEventListener('change', function () { const customSettings = document.getElementById('custom-floor-settings'); const lastNSettings = document.getElementById('last-n-settings'); customSettings.style.display = this.value === 'custom' ? 'block' : 'none'; lastNSettings.style.display = this.value === 'lastN' ? 'block' : 'none'; activationMode = this.value; GM_setValue('activationMode', activationMode); if (isInjectionEnabled) { removeInjectedIframes(); injectHtmlCode(); } }); document.getElementById('custom-start-floor').addEventListener('change', function () { customStartFloor = parseInt(this.value); GM_setValue('customStartFloor', customStartFloor); if (isInjectionEnabled) { removeInjectedIframes(); injectHtmlCode(); } }); document.getElementById('custom-end-floor').addEventListener('change', function () { customEndFloor = parseInt(this.value); GM_setValue('customEndFloor', customEndFloor); if (isInjectionEnabled) { removeInjectedIframes(); injectHtmlCode(); } }); document.getElementById('last-n-floors').addEventListener('change', function () { customEndFloor = parseInt(this.value); GM_setValue('customEndFloor', customEndFloor); if (isInjectionEnabled) { removeInjectedIframes(); injectHtmlCode(); } }); // 创建开关 function createToggleSwitch(id) { const toggleSwitch = document.createElement('label'); toggleSwitch.className = 'switch'; toggleSwitch.innerHTML = ` `; toggleSwitch.style.cssText = ` position: relative; display: inline-block; width: 52px; height: 28px; `; return toggleSwitch; } // 创建边缘控制面板 const edgeControls = document.createElement('div'); edgeControls.id = 'edge-controls'; edgeControls.style.cssText = ` position: fixed; right: 0; background-color: #2d2d2d; border: 1px solid #454545; border-right: none; border-radius: 5px 0 0 5px; padding: 10px; z-index: 9998; display: flex; flex-direction: column; align-items: center; `; // 在边缘控制面板中添加开关 const edgeSwitch = createToggleSwitch('edge-injection-toggle'); edgeControls.appendChild(edgeSwitch); // 处理边缘控制面板的位置调整 function updateEdgeControlsPosition(position) { const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; const panelHeight = edgeControls.offsetHeight; const isSmallScreen = windowWidth <= 768; // 定义小屏幕的阈值 // 重置之前的样式 edgeControls.style.top = 'auto'; edgeControls.style.bottom = 'auto'; edgeControls.style.transform = 'none'; switch (position) { case 'top-right': edgeControls.style.top = isSmallScreen ? '5px' : '10px'; break; case 'right-three-quarters': edgeControls.style.top = `${windowHeight * 0.25 - panelHeight / 2}px`; break; case 'right-middle': edgeControls.style.top = '50%'; edgeControls.style.transform = 'translateY(-50%)'; break; case 'right-one-quarter': edgeControls.style.bottom = `${windowHeight * 0.25 - panelHeight / 2}px`; break; case 'bottom-right': edgeControls.style.bottom = isSmallScreen ? '5px' : '10px'; break; } // 调整右侧距离 edgeControls.style.right = isSmallScreen ? '5px' : '0'; // 如果是小屏幕,确保面板不会超出屏幕 if (isSmallScreen) { const rect = edgeControls.getBoundingClientRect(); if (rect.top < 0) edgeControls.style.top = '5px'; if (rect.bottom > windowHeight) edgeControls.style.bottom = '5px'; } GM_setValue('edgeControlsPosition', position); } // 处理屏幕尺寸变化 function handleScreenSizeChange() { const savedPosition = GM_getValue('edgeControlsPosition', 'top-right'); updateEdgeControlsPosition(savedPosition); // 调整设置面板的最大高度 const windowHeight = window.innerHeight; settingsPanel.style.maxHeight = `${windowHeight * 0.8}px`; } // 位置调整事件监听器 document.getElementById('edge-controls-position').addEventListener('change', function () { updateEdgeControlsPosition(this.value); }); // 添加显示/隐藏面板的按钮 const togglePanelButton = document.createElement('button'); togglePanelButton.textContent = '显示面板'; togglePanelButton.style.cssText = ` margin-top: 10px; padding: 8px 12px; background-color: #0e639c; color: #ffffff; border: none; border-radius: 3px; cursor: pointer; font-size: 14px; min-width: 80px; // 确保按钮有足够的点击区域 `; edgeControls.appendChild(togglePanelButton); // 添加收起/展开按钮 const toggleEdgeControlsButton = document.createElement('button'); toggleEdgeControlsButton.textContent = '<<'; toggleEdgeControlsButton.style.cssText = ` position: absolute; left: -20px; top: 50%; transform: translateY(-50%); background-color: #2d2d2d; color: #ffffff; border: none; border-radius: 3px 0 0 3px; cursor: pointer; /* 改变鼠标样式,表明可点击 */ padding: 5px; user-select: none; /* 防止文本被选中 */ `; edgeControls.appendChild(toggleEdgeControlsButton); document.body.appendChild(edgeControls); // 添加收起/展开功能 let isEdgeControlsCollapsed = false; toggleEdgeControlsButton.addEventListener('click', function () { if (isEdgeControlsCollapsed) { edgeControls.style.transform = 'translateX(0)'; toggleEdgeControlsButton.textContent = '<<'; } else { edgeControls.style.transform = 'translateX(calc(100% - 20px))'; toggleEdgeControlsButton.textContent = '>>'; } isEdgeControlsCollapsed = !isEdgeControlsCollapsed; }); function toggleEdgeControls() { if (isEdgeControlsCollapsed) { edgeControls.style.transform = 'translateX(0)'; toggleEdgeControlsButton.textContent = '<<'; } else { edgeControls.style.transform = 'translateX(calc(100% - 20px))'; toggleEdgeControlsButton.textContent = '>>'; } isEdgeControlsCollapsed = !isEdgeControlsCollapsed; } // 添加窗口大小变化的监听,确保面板始终在视图内 window.addEventListener('resize', handleScreenSizeChange); // 添加样式 const style = document.createElement('style'); style.textContent = ` .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #3a3a3a; transition: .4s; border-radius: 34px; } .slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: #d4d4d4; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: #0e639c; } input:checked + .slider:before { transform: translateX(26px); } #settings-content label { display: block; margin: 10px 0; color: #d4d4d4; } .close-button { width: 30px; height: 30px; background-color: #e81123; border: none; color: white; font-size: 20px; font-weight: bold; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background-color 0.3s; } .close-button:hover { background-color: #f1707a; } #settings-header { padding-bottom: 10px; border-bottom: 1px solid #454545; margin-bottom: 15px; } #settings-content input[type="radio"] { margin-right: 5px; } #settings-content input[type="number"] { background-color: #2d2d2d; color: #d4d4d4; border: 1px solid #454545; padding: 5px; border-radius: 3px; width: 50px; margin: 0 5px; } #settings-content input[type="number"]:focus { outline: none; border-color: #0e639c; } #activation-mode { background-color: #2d2d2d; color: #d4d4d4; border: 1px solid #454545; padding: 5px; border-radius: 3px; } #activation-mode:focus { outline: none; border-color: #0e639c; } .settings-section { margin-bottom: 15px; } .settings-subtitle { font-size: 14px; margin: 0 0 5px 0; color: #d4d4d4; } .settings-option { display: block; margin: 5px 0; font-size: 13px; } .settings-select { width: 100%; margin-bottom: 5px; } .settings-subsection { margin-top: 5px; padding-left: 10px; } .settings-note { font-size: 12px; color: #858585; margin: 2px 0; } .settings-footer { font-size: 12px; color: #858585; margin-top: 15px; } .code-example { background-color: #2d2d2d; padding: 10px; border-radius: 3px; overflow-x: auto; font-size: 12px; } `; style.textContent += ` .slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 4px; background-color: #d4d4d4; transition: .4s; border-radius: 50%; } input:checked + .slider:before { transform: translateX(24px); } `; document.head.appendChild(style); // 监听开关变化 function handleToggleChange(e) { isInjectionEnabled = e.target.checked; document.getElementById('edge-injection-toggle').checked = isInjectionEnabled; if (isInjectionEnabled) { injectHtmlCode(); } else { removeInjectedIframes(); } } document.getElementById('edge-injection-toggle').addEventListener('change', handleToggleChange); // 监听显示模式变化 document.getElementsByName('display-mode').forEach(radio => { radio.addEventListener('change', function () { displayMode = parseInt(this.value); GM_setValue('displayMode', displayMode); // 保存设置 if (isInjectionEnabled) { removeInjectedIframes(); injectHtmlCode(); } }); }); // 显示/隐藏面板按钮 togglePanelButton.addEventListener('click', function () { if (settingsPanel.style.display === 'none') { settingsPanel.style.display = 'block'; this.textContent = '隐藏面板'; } else { settingsPanel.style.display = 'none'; this.textContent = '显示面板'; } }); // 关闭设置面板 document.getElementById('close-settings').addEventListener('click', function () { settingsPanel.style.display = 'none'; }); // 全局消息监听器 window.addEventListener('message', function (event) { if (event.data === 'loaded') { // 处理 iframe 加载完成的消息 const iframes = document.querySelectorAll('.mes_text iframe'); iframes.forEach(iframe => { if (iframe.contentWindow === event.source) { adjustIframeHeight(iframe); } }); } else if (event.data.type === 'buttonClick') { // 处理按钮点击事件 const buttonName = event.data.name; // 使用 jQuery 来查找和触发按钮 jQuery('.qr--button.menu_button').each(function () { if (jQuery(this).find('.qr--button-label').text().trim() === buttonName) { jQuery(this).click(); return false; // 退出 each 循环 } }); } }); // 添加一个自定义的 :contains 选择器 jQuery.expr[':'].contains = function (a, i, m) { return jQuery(a).text().toUpperCase().indexOf(m[3].toUpperCase()) >= 0; }; // 调整 iframe 高度的函数 function adjustIframeHeight(iframe) { if (iframe.contentWindow.document.body) { const height = iframe.contentWindow.document.documentElement.scrollHeight; iframe.style.height = (height + 5) + 'px'; // 添加一些额外的高度 } } // 主要的注入函数 function injectHtmlCode(specificMesText = null) { let mesTextElements = specificMesText ? [specificMesText] : Array.from(document.getElementsByClassName('mes_text')); // 根据激活楼层设置筛选要处理的元素 let targetElements; switch (activationMode) { case 'first': targetElements = mesTextElements.slice(0, 1); break; case 'last': targetElements = mesTextElements.slice(-1); break; case 'lastN': targetElements = mesTextElements.slice(-customEndFloor); break; case 'custom': const start = customStartFloor - 1; const end = customEndFloor === -1 ? undefined : customEndFloor; targetElements = mesTextElements.slice(start, end); break; default: // 'all' targetElements = mesTextElements; } // 原有的注入逻辑 for (const mesText of targetElements) { const codeElements = mesText.getElementsByTagName('code'); for (const codeElement of codeElements) { let htmlContent = codeElement.innerText.trim(); // 处理 标签 htmlContent = htmlContent.replace(/(.*?)<\/qr--button>/g, ''); if (htmlContent.startsWith('<') && htmlContent.endsWith('>')) { // 创建一个iframe来运行HTML代码 const iframe = document.createElement('iframe'); iframe.style.width = '100%'; iframe.style.border = 'none'; iframe.style.marginTop = '10px'; // 设置 iframe 的内容,包括消息发送脚本和按钮点击处理 iframe.srcdoc = ` ${htmlContent} `; // 根据显示模式处理原代码 if (displayMode === 2) { const details = document.createElement('details'); const summary = document.createElement('summary'); summary.textContent = '[原代码]'; details.appendChild(summary); codeElement.parentNode.insertBefore(details, codeElement); details.appendChild(codeElement); } else if (displayMode === 3) { codeElement.style.display = 'none'; } // 将iframe插入到code元素后面 codeElement.parentNode.insertBefore(iframe, codeElement.nextSibling); // 初始调整iframe高度 iframe.onload = function () { adjustIframeHeight(iframe); // 再次调整高度,以防有延迟加载的内容 setTimeout(() => adjustIframeHeight(iframe), 500); }; // 监听 iframe 内容变化 if (iframe.contentWindow) { const resizeObserver = new ResizeObserver(() => adjustIframeHeight(iframe)); resizeObserver.observe(iframe.contentWindow.document.body); } } } } } // 楼层初始化设置 document.querySelector(`input[name="display-mode"][value="${displayMode}"]`).checked = true; document.getElementById('activation-mode').value = activationMode; document.getElementById('custom-start-floor').value = customStartFloor; document.getElementById('custom-end-floor').value = customEndFloor; document.getElementById('last-n-floors').value = customEndFloor; if (activationMode === 'custom') { document.getElementById('custom-floor-settings').style.display = 'block'; } else if (activationMode === 'lastN') { document.getElementById('last-n-settings').style.display = 'block'; } function removeInjectedIframes() { const iframes = document.querySelectorAll('.mes_text iframe'); iframes.forEach(iframe => iframe.remove()); // 恢复原代码显示 const codeElements = document.querySelectorAll('.mes_text code'); codeElements.forEach(code => { code.style.display = ''; const details = code.closest('details'); if (details) { details.parentNode.insertBefore(code, details); details.remove(); } }); } function checkLastMesTextChange() { const mesTextElements = document.getElementsByClassName('mes_text'); if (mesTextElements.length > 0) { const lastMesText = mesTextElements[mesTextElements.length - 1]; const codeElement = lastMesText.querySelector('code'); if (codeElement) { const currentContent = codeElement.innerText.trim(); const injectedIframe = lastMesText.querySelector('iframe'); // 检查是否有变化或者没有注入的iframe if (currentContent !== lastMesTextContent || (isInjectionEnabled && !injectedIframe)) { lastMesTextContent = currentContent; if (isInjectionEnabled) { // 如果已经有iframe,先移除 if (injectedIframe) { injectedIframe.remove(); } // 重新注入 injectHtmlCode(lastMesText); } } } else { // 如果没有code标签,但之前有内容,清除lastMesTextContent if (lastMesTextContent !== '') { lastMesTextContent = ''; // 如果有之前注入的iframe,移除它 const injectedIframe = lastMesText.querySelector('iframe'); if (injectedIframe) { injectedIframe.remove(); } } } } } // 监听DOM变化,处理动态加载的内容 const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && (node.classList.contains('mes_text') || node.querySelector('.mes_text'))) { if (isInjectionEnabled) { injectHtmlCode(); } break; } } } } }); observer.observe(document.body, { childList: true, subtree: true }); // 边缘控制面板位置 const savedPosition = GM_getValue('edgeControlsPosition', 'top-right'); document.getElementById('edge-controls-position').value = savedPosition; updateEdgeControlsPosition(savedPosition); handleScreenSizeChange(); // 每2秒检查一次最后一个 mes_text 的变化 setInterval(checkLastMesTextChange, 2000); // 初始化设置 document.querySelector(`input[name="display-mode"][value="${displayMode}"]`).checked = true; })();