// ==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.26
// @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">
\`\`\`
<qr--button>(你的QR按钮名字)</qr--button>
<st_text>(对应酒馆的输入文本框,输入内容会同步到酒馆的文本框里)</st_text>
<st_send_but>(对应酒馆的发送按钮)</st_send_but>
\`\`\`
</pre>
<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 }));
}
} 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();
// 处理 <qr--button> 标签
htmlContent = htmlContent.replace(/<qr--button>(.*?)<\/qr--button>/g,
'<button class="qr-button" data-button-name="$1">$1</button>');
// 处理 <st_text> 标签
htmlContent = htmlContent.replace(/<st_text>(.*?)<\/st_text>/g,
'<textarea class="st-text">$1</textarea>');
// 处理 <st_send_but> 标签
htmlContent = htmlContent.replace(/<st_send_but><\/st_send_but>/g,
'<button class="st-send-button">发送</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; }
</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}, '*');
});
});
document.querySelectorAll('.st-text').forEach(textarea => {
textarea.addEventListener('input', function() {
window.parent.postMessage({type: 'textInput', text: this.value}, '*');
});
});
document.querySelectorAll('.st-send-button').forEach(button => {
button.addEventListener('click', function() {
window.parent.postMessage({type: 'sendClick'}, '*');
});
});
});
</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);
// 初始化设置
document.querySelector(`input[name="display-mode"][value="${displayMode}"]`).checked = true;
// 初始化边缘控制面板状态
updateEdgeControlsDisplay();
})();