【SillyTavern / ST酒馆】html代码注入器

可以让ST酒馆独立运行html代码 (Inject HTML code into SillyTavern pages.)

目前為 2024-08-12 提交的版本,檢視 最新版本

// ==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.6
// @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
// @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;">
\`\`\`
&lt;h1&gt;Hello, World!&lt;/h1&gt;
&lt;p&gt;This is an example.&lt;/p&gt;
\`\`\`
        </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);
        }
      });
    }
  });

  // 调整 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) {
        const htmlContent = codeElement.innerText.trim();

        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; }
                            </style>
                        </head>
                        <body>
                            ${htmlContent}
                            <script>
                                window.addEventListener('load', function() {
                                    window.parent.postMessage('loaded', '*');
                                });
                            </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;
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址