Inject HTML code into SillyTavern pages.
// ==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.1.2 // @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 // ==/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 = ` <div id="settings-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <span style="font-size: 16px; font-weight: bold;">HTML注入器设置</span> <button id="close-settings" class="close-button">×</button> </div> <div id="settings-content"> <div class="settings-section"> <h3 class="settings-subtitle">边缘控制面板位置</h3> <select id="edge-controls-position" class="settings-select"> <option value="top-right">界面右上角</option> <option value="right-three-quarters">界面右侧3/4位置</option> <option value="right-middle">界面右侧中间</option> </select> </div> <div class="settings-section"> <h3 class="settings-subtitle">显示模式</h3> <label class="settings-option"><input type="radio" name="display-mode" value="1"> 原代码和注入效果一起显示</label> <label class="settings-option"><input type="radio" name="display-mode" value="2"> 原代码以摘要形式显示</label> <label class="settings-option"><input type="radio" name="display-mode" value="3"> 隐藏原代码,只显示注入效果</label> </div> <div class="settings-section"> <h3 class="settings-subtitle">激活楼层</h3> <select id="activation-mode" class="settings-select"> <option value="all">全部楼层</option> <option value="first">第一层</option> <option value="last">最后一层</option> <option value="lastN">最后N层</option> <option value="custom">自定义楼层</option> </select> <div id="custom-floor-settings" class="settings-subsection" style="display: none;"> <label class="settings-option">起始楼层: <input type="number" id="custom-start-floor" min="1" value="1"></label> <label class="settings-option">结束楼层: <input type="number" id="custom-end-floor" min="-1" value="-1"></label> <p class="settings-note">(-1 表示最后一层)</p> </div> <div id="last-n-settings" class="settings-subsection" style="display: none;"> <label class="settings-option">最后 <input type="number" id="last-n-floors" min="1" value="1"> 层</label> </div> </div> </div> <div class="settings-footer"> <p>安全提醒:请仅注入您信任的代码。不安全的代码可能会对您的系统造成潜在风险。</p> <p>注意:要注入的 HTML 代码应该用 \`\`\` 包裹,例如:</p> <pre class="code-example"> \`\`\` <h1>Hello, World!</h1> <p>This is an example.</p> \`\`\` </pre> <p>以下是对应ST酒馆功能的特殊类名及简单的使用方法:</p> <pre class="code-example"> \`\`\` <button class="qr-button">(你的QR按钮名字)</button> <textarea class="st-text">(对应酒馆的输入文本框,输入内容会同步到酒馆的文本框里)</textarea> <button class="st-send-button">(对应酒馆的发送按钮)</button> <audio class="st-audio" controls> <source src="你的音频文件地址" type="audio/类型"> </audio> (st-audio可以添加controls显示控制栏,loop循环播放,autoplay自动播放。同一时间只会播放一个音频) <details> <summary>点击查看 st-audio 的详细用法讲解</summary> 【属性说明】 - class="st-audio" - 用于标识这个音频元素,使其受到我们刚才编写的音频管理系统控制 - loop - 使音频循环播放 - controls - 显示音频控制面板(播放/暂停/进度条等) - autoplay - 尝试自动播放(注意:现代浏览器可能会阻止自动播放) 【type属性的作用】 - 告诉浏览器音频文件的格式,帮助浏览器更快地确定是否支持该格式 - 不同格式对应不同的type值: - .mp3 → type = "audio/mpeg" - .wav → type = "audio/wav" - .ogg → type = "audio/ogg" - .m4a → type = "audio/mp4" 【示例代码】 \`\`\` <audio class="st-audio" loop controls autoplay> <source src="https://tuchuang-93f.pages.dev/img/zeus_bgm3.wav" type="audio/wav"> </audio> \`\`\` </details> \`\`\` </pre> <p>【注意】通过JavaScript动态插入st-text框的内容同步到st酒馆的输入框需要处理时间,如果需要同步,请添加一个小延迟来确保文本有时间进行同步.</p> <a href="https://discord.com/channels/1134557553011998840/1271783456690409554" target="_blank"> →Discord教程帖指路← 有详细说明与gal界面等模版 </a> </div> `; 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 = ` <input type="checkbox" id="${id}"> <span class="slider round"></span> `; toggleSwitch.style.cssText = ` position: relative; display: inline-block; width: 60px; height: 34px; `; 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; min-width: 80px; // 增加最小宽度 `; // 在边缘控制面板中添加开关 const edgeSwitch = createToggleSwitch('edge-injection-toggle'); edgeControls.appendChild(edgeSwitch); // 处理边缘控制面板的位置调整 function updateEdgeControlsPosition(position) { const vh = window.innerHeight / 100; edgeControls.style.transform = 'none'; // 重置transform switch (position) { case 'top-right': edgeControls.style.top = '1vh'; edgeControls.style.bottom = 'auto'; break; case 'right-three-quarters': edgeControls.style.top = '25vh'; edgeControls.style.bottom = 'auto'; break; case 'right-middle': edgeControls.style.top = '50vh'; edgeControls.style.transform = 'translateY(-50%)'; edgeControls.style.bottom = 'auto'; break; } GM_setValue('edgeControlsPosition', position); // 恢复收起/展开状态 updateEdgeControlsDisplay(); } // 位置调整事件监听器 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: 5px; cursor: pointer; font-size: 14px; font-weight: bold; width: 100%; text-align: center; transition: background-color 0.3s; `; togglePanelButton.addEventListener('mouseover', function () { this.style.backgroundColor = '#1177bb'; }); togglePanelButton.addEventListener('mouseout', function () { this.style.backgroundColor = '#0e639c'; }); edgeControls.appendChild(togglePanelButton); // 添加收起/展开按钮 const toggleEdgeControlsButton = document.createElement('button'); toggleEdgeControlsButton.textContent = '<<'; toggleEdgeControlsButton.style.cssText = ` position: absolute; left: -15px; top: 50%; transform: translateY(-50%); background-color: #2d2d2d; color: #ffffff; border: none; border-radius: 3px 0 0 3px; cursor: pointer; padding: 3px; user-select: none; font-size: 10px; `; edgeControls.appendChild(toggleEdgeControlsButton); document.body.appendChild(edgeControls); // 添加收起/展开功能 // let isEdgeControlsCollapsed = false; let isEdgeControlsCollapsed = GM_getValue('isEdgeControlsCollapsed', false); toggleEdgeControlsButton.addEventListener('click', toggleEdgeControls); function toggleEdgeControls() { isEdgeControlsCollapsed = !isEdgeControlsCollapsed; GM_setValue('isEdgeControlsCollapsed', isEdgeControlsCollapsed); updateEdgeControlsDisplay(); } function updateEdgeControlsDisplay() { edgeControls.style.transform = isEdgeControlsCollapsed ? 'translateX(calc(100% - 20px))' : 'translateX(0)'; toggleEdgeControlsButton.textContent = isEdgeControlsCollapsed ? '>>' : '<<'; } // 添加窗口大小变化的监听,确保面板始终在视图内 window.addEventListener('resize', () => { const savedPosition = GM_getValue('edgeControlsPosition', 'top-right'); updateEdgeControlsPosition(savedPosition); }); // 添加样式 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; } // 响应式样式 @media (max-width: 768px) { #edge-controls { font-size: 10px; min-width: 100px; } #edge-controls button { font-size: 12px; padding: 6px 10px; } .switch { width: 50px; height: 28px; } .slider:before { height: 20px; width: 20px; } input:checked + .slider:before { transform: translateX(22px); } } `; 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('.qr--button.menu_button').each(function () { if (jQuery(this).find('.qr--button-label').text().trim() === buttonName) { jQuery(this).click(); return false; // 退出 each 循环 } }); } else if (event.data.type === 'textInput') { // 处理文本输入 const sendTextarea = document.getElementById('send_textarea'); if (sendTextarea) { sendTextarea.value = event.data.text; // 触发 input 事件以确保任何监听器都能捕捉到变化 sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); // 如果需要,也可以触发 change 事件 sendTextarea.dispatchEvent(new Event('change', { bubbles: true })); } } else if (event.data.type === 'sendClick') { // 处理发送按钮点击 const sendButton = document.getElementById('send_but'); if (sendButton) { sendButton.click(); } } }); // 添加一个自定义的 :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(); if (htmlContent.startsWith('<') && htmlContent.endsWith('>')) { // 创建一个iframe来运行HTML代码 const iframe = document.createElement('iframe'); // 确保每个iframe都有唯一的ID iframe.id = 'audio-iframe-' + Math.random().toString(36).substr(2, 9); iframe.style.width = '100%'; iframe.style.height = '100%'; iframe.style.border = 'none'; iframe.style.marginTop = '10px'; // 设置 iframe 的内容 iframe.srcdoc = ` <html> <head> <style> body { margin: 0; padding: 0; } /* 您可以在这里添加默认样式 */ </style> </head> <body> ${htmlContent} <script> // 音频管理系统 class AudioManager { constructor() { this.currentlyPlaying = null; } handlePlay(audio) { // 如果有其他音频在播放,先通知父窗口停止它 if (this.currentlyPlaying && this.currentlyPlaying !== audio) { this.currentlyPlaying.pause(); } // 通知父窗口有新的音频开始播放 window.parent.postMessage({ type: 'audioPlay', iframeId: window.frameElement.id }, '*'); this.currentlyPlaying = audio; } stopAll() { if (this.currentlyPlaying) { this.currentlyPlaying.pause(); this.currentlyPlaying = null; } } } const audioManager = new AudioManager(); window.addEventListener('load', function() { window.parent.postMessage('loaded', '*'); document.querySelectorAll('.qr-button').forEach(button => { button.addEventListener('click', function() { const buttonName = this.textContent.trim(); window.parent.postMessage({type: 'buttonClick', name: buttonName}, '*'); }); }); document.querySelectorAll('.st-text').forEach(textarea => { textarea.addEventListener('input', function() { window.parent.postMessage({type: 'textInput', text: this.value}, '*'); }); // 添加 'change' 事件监听 textarea.addEventListener('change', function() { window.parent.postMessage({type: 'textInput', text: this.value}, '*'); }); // 添加一个 MutationObserver 来监听值的变化 const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'value') { window.parent.postMessage({type: 'textInput', text: textarea.value}, '*'); } }); }); observer.observe(textarea, { attributes: true }); }); document.querySelectorAll('.st-send-button').forEach(button => { button.addEventListener('click', function() { window.parent.postMessage({type: 'sendClick'}, '*'); }); }); // 为所有st-audio类的音频元素添加事件监听 document.querySelectorAll('.st-audio').forEach(audio => { audio.addEventListener('play', function() { audioManager.handlePlay(this); }); }); // 监听来自父窗口的停止音频指令 window.addEventListener('message', function(event) { if (event.data.type === 'stopAudio' && event.data.iframeId !== window.frameElement.id) { audioManager.stopAll(); } }); }); </script> </body> </html> `; // 根据显示模式处理原代码 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); // 每2秒检查一次最后一个 mes_text 的变化 setInterval(checkLastMesTextChange, 2000); // 在主脚本中添加全局音频管理 function createGlobalAudioManager() { let currentPlayingIframeId = null; window.addEventListener('message', function (event) { if (event.data.type === 'audioPlay') { const newIframeId = event.data.iframeId; // 如果有其他iframe在播放音频,发送停止指令 if (currentPlayingIframeId && currentPlayingIframeId !== newIframeId) { document.querySelectorAll('iframe').forEach(iframe => { iframe.contentWindow.postMessage({ type: 'stopAudio', iframeId: newIframeId }, '*'); }); } currentPlayingIframeId = newIframeId; } }); } // 初始化设置 document.querySelector(`input[name="display-mode"][value="${displayMode}"]`).checked = true; // 初始化边缘控制面板状态 updateEdgeControlsDisplay(); // 在脚本初始化时调用全局音频控制 createGlobalAudioManager(); })();