- // ==UserScript==
- // @name 【SillyTavern / ST酒馆】html代码注入器
- // @name:zh 【ST酒馆】html代码注入器
- // @name:zh-CN 【ST酒馆】html代码注入器
- // @name:en 【SillyTavern】 HTML Code Injector
- // @namespace https://gf.qytechs.cn/users/Qianzhuo
- // @version 1.0.7
- // @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/*
- // @grant GM_setValue
- // @grant GM_getValue
- // @require https://code.jquery.com/jquery-3.6.0.min.js
- // @license MIT
- // ==/UserScript==
-
- /* MIT License
-
- Copyright (c) 2024 Qianzhuo
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE. */
-
- (function () {
- 'use strict';
-
- let isInjectionEnabled = false;
- let displayMode = GM_getValue('displayMode', 1); // 从存储中获取,默认为1
- let lastMesTextContent = '';
-
-
- // 创建设置面板
- const settingsPanel = document.createElement('div');
- settingsPanel.innerHTML = `
- <div id="settings-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
- <span style="font-size: 16px; font-weight: bold;">HTML注入器设置</span>
- <button id="close-settings" class="close-button">×</button>
- </div>
- <div id="settings-content">
- <label><input type="radio" name="display-mode" value="1"> 原代码和注入效果一起显示</label><br>
- <label><input type="radio" name="display-mode" value="2"> 原代码以摘要形式显示</label><br>
- <label><input type="radio" name="display-mode" value="3"> 隐藏原代码,只显示注入效果</label>
- </div>
- <div style="margin-top: 20px; font-size: 1em; color: #858585;">
- <p>安全提醒:请仅注入您信任的代码。不安全的代码可能会对您的系统造成潜在风险。</p>
- <p>注意:要注入的 HTML 代码应该用 \`\`\` 包裹,例如:</p>
- <pre style="background-color: #2d2d2d; padding: 10px; border-radius: 3px; overflow-x: auto;">
- \`\`\`
- <h1>Hello, World!</h1>
- <p>This is an example.</p>
- \`\`\`
- </pre>
- </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);
-
- // 创建开关
- 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;
- top: 10px;
- 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;
- transition: transform 0.3s ease-in-out;
- `;
-
- // 在边缘控制面板中添加开关
- const edgeSwitch = createToggleSwitch('edge-injection-toggle');
- edgeControls.appendChild(edgeSwitch);
-
- // 添加显示/隐藏面板的按钮
- const togglePanelButton = document.createElement('button');
- togglePanelButton.textContent = '显示面板';
- togglePanelButton.style.cssText = `
- margin-top: 10px;
- padding: 5px 10px;
- background-color: #0e639c;
- color: #ffffff;
- border: none;
- border-radius: 3px;
- cursor: pointer;
- font-size: 12px;
- `;
- 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: move; /* 改变鼠标样式,表明可拖动 */
- 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;
- });
-
- let isDragging = false;
- let startY;
- let startTop;
-
- toggleEdgeControlsButton.addEventListener('mousedown', startDragging);
- document.addEventListener('mousemove', drag);
- document.addEventListener('mouseup', stopDragging);
-
- function startDragging(e) {
- isDragging = true;
- startY = e.clientY;
- startTop = edgeControls.offsetTop;
- e.preventDefault(); // 防止文本被选中
- }
-
- function drag(e) {
- if (!isDragging) return;
-
- let newTop = startTop + (e.clientY - startY);
-
- // 限制面板在页面内
- newTop = Math.max(0, Math.min(newTop, window.innerHeight - edgeControls.offsetHeight));
-
- edgeControls.style.top = `${newTop}px`;
- }
-
- function stopDragging(e) {
- if (!isDragging) return;
-
- isDragging = false;
-
- // 检查鼠标释放时是否在按钮上
- const rect = toggleEdgeControlsButton.getBoundingClientRect();
- if (
- e.clientX >= rect.left &&
- e.clientX <= rect.right &&
- e.clientY >= rect.top &&
- e.clientY <= rect.bottom
- ) {
- toggleEdgeControls();
- }
- }
-
- function toggleEdgeControls() {
- if (isEdgeControlsCollapsed) {
- edgeControls.style.transform = 'translateX(0)';
- toggleEdgeControlsButton.textContent = '<<';
- } else {
- edgeControls.style.transform = 'translateX(calc(100% - 20px))';
- toggleEdgeControlsButton.textContent = '>>';
- }
- isEdgeControlsCollapsed = !isEdgeControlsCollapsed;
- }
-
- // 单独为按钮添加点击事件
- toggleEdgeControlsButton.addEventListener('click', function (e) {
- if (!isDragging) {
- toggleEdgeControls();
- }
- e.stopPropagation(); // 阻止事件冒泡
- });
-
- // 添加窗口大小变化的监听,确保面板始终在视图内
- window.addEventListener('resize', () => {
- const maxTop = window.innerHeight - edgeControls.offsetHeight;
- if (edgeControls.offsetTop > maxTop) {
- edgeControls.style.top = `${maxTop}px`;
- }
- });
-
- // 添加样式
- 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;
- }
- #settings-content input[type="radio"] {
- margin-right: 10px;
- }
-
- .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 label {
- display: block;
- margin: 10px 0;
- color: #d4d4d4;
- }
-
- #settings-content input[type="radio"] {
- margin-right: 10px;
- }
- `;
- 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) {
- const mesTextElements = specificMesText ? [specificMesText] : document.getElementsByClassName('mes_text');
-
- for (const mesText of mesTextElements) {
- const codeElements = mesText.getElementsByTagName('code');
-
- for (const codeElement of codeElements) {
- let htmlContent = codeElement.innerText.trim();
-
- // 处理 <qr--button> 标签
- htmlContent = htmlContent.replace(/<qr--button>(.*?)<\/qr--button>/g,
- '<button class="qr-button" data-button-name="$1">$1</button>');
-
- 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 = `
- <html>
- <head>
- <style>
- body { margin: 0; padding: 0; }
- .qr-button {
- padding: 5px 10px;
- margin: 5px;
- background-color: #4CAF50;
- color: white;
- border: none;
- cursor: pointer;
- }
- </style>
- </head>
- <body>
- ${htmlContent}
- <script>
- window.addEventListener('load', function() {
- window.parent.postMessage('loaded', '*');
-
- document.querySelectorAll('.qr-button').forEach(button => {
- button.addEventListener('click', function() {
- const buttonName = this.getAttribute('data-button-name');
- window.parent.postMessage({type: 'buttonClick', name: buttonName}, '*');
- });
- });
- });
- </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);
- }
- }
- }
- }
- }
-
-
- 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 });
-
- // 每2秒检查一次最后一个 mes_text 的变化
- setInterval(checkLastMesTextChange, 2000);
-
- // 初始化设置
- document.querySelector(`input[name="display-mode"][value="${displayMode}"]`).checked = true;
- })();