Skip to Content

功能简述

流式渲染(沉浸式模板)是可以在多楼层进行流式回复渲染 UI 的功能(简称流式渲染),概念相对复杂一些(但是不难),如果是初次上手前端卡的作者更推荐使用原生酒馆正则替换代码块的方式(传统方式)

基础操作

要使用流式渲染,就需要在小白 X 扩展菜单里勾选【沉浸式模板】并且编辑模板编辑器。

勾选保存后即可在聊天中启用流式渲染。


插件整体流程

  1. 将你写好的 HTML 前端,放入模板编辑器中并保存
  2. 每当 AI 开始回复时,插件会新建一个 iframe,把模板放进去显示
  3. AI 在回复文本里,用你规定的格式输出(JSON/YAML/自定义正则标签)
  4. 在 AI 回复的流式过程中,插件会从 AI 文本里提取出一个 “vars 对象”,并多次调用 iframe 里的 window.updateTemplateVariables(vars) 传递文本数据给 iframe
  5. 通过你写的 HTML 里的 CSS 或 JS 部分,将文本映射到 UI 显示

简化理解:每回合插件会在楼层产生时先“插入你写的 UI”,在流式生成时通过 window.updateTemplateVariables 不断输入文本数据更新你的 UI,靠 SillyTavern 变量保存跨回合的状态。

简单使用方法

只需要在 HTML 内,使用 [[占位符名]] 格式引用占位符

<div>角色:[[name]],心情:[[mood]]</div>

示例模板:

<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <style> body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; } .box { background: white; padding: 20px; margin: 15px 0; border-radius: 8px; } </style> </head> <body> <div className="box"> <h2>简介</h2> <p>[[profile]]</p> </div> <div className="box"> <h2>故事</h2> <p>[[mood]], [[story]]</p> </div> <script> function updateTime() { document.getElementById('time').textContent = new Date().toLocaleString('zh-TW'); } updateTime(); </script> </body> </html>

对应 AI 也需要输出带有 profile,mood 和 story 的格式,具体请参考示例卡里的开场白及插件默认解析键的三种格式

简单方法的局限性和适用性:简单的文字类 UI 卡,不需要 JS 去更新界面


非简单写法

适用于更为复杂的前端,如需要通过 AI 的回复去更新前端介面,而不是纯文字更新的情况

  1. window.updateTemplateVariables(vars) 会不断把“解析后的对象”传给你(vars)
    • 要做两件事:读取 vars -> 更新你页面上相关的 UI 元素
    • 会被多次调用、在流式生成时,数据不会完整,所以写法要“幂等”(重复调用结果一致,不会越调越乱)
  2. AI 需要配合流式渲染模板,以指定的格式(编辑器内的自定义正则YAMLJSON)去配合插件解析出输出数据
    • 补充:可通过勾选编辑器内的 文本不使用插件预设的正则及格式解析器 来完全自创属于自己的文本格式解析器

window.updateTemplateVariables 的定位

  • 职责:接收当回合来自 AI 的最新数据 vars,并把这些数据幂等地映射到 UI 与本地临时状态
  • 运行特性:它可能被多次调用、数据在流式生成期间不完整,所以写法要“幂等”(重复调用结果一致,不会越调越乱)。
  • 不负责一次性初始化历史状态;不负责长耗时 IO;不负责绑定事件
分层与职责
  • updateTemplateVariables 只管“输入 AI 数据进 iframe -> 更新临时状态 -> 渲染 UI”,
  • 初始化、事件、持久化、复杂运算,都不放在 updateTemplateVariables

放在 updateTemplateVariables 内的函数/逻辑(需要根据 AI 输出执行的函数):

  • 解析 updateTemplateVariables 传递进 iframevars 对象
  • 更新 vars 对象相关的 localState
  • vars 对象相关的 localStaterender()(根据 localState 刷新对应的 UI)

放在 updateTemplateVariables 外的函数/逻辑(生命周期管理、持久化、事件、慢操作):

  • initializeOrSync/getvar 读取历史状态 -> 写入 localState -> render()
  • 事件绑定:按钮点击、键盘回车
  • 用户操作:更新 localState -> render -> /setvar 持久化 -> 可选 /send
  • 复杂数据的编码/解码:Base64、JSON

插件默认解析的三种格式

JSON

{ "profile": "我是一个友善的助手", "mood": "今天心情很好", "story": "这是一个有趣的故事" }

YAML

profile: 我是一个友善的助手 mood: 今天心情很好 story: 这是一个有趣的故事

自定义标签 (默认)

[profile]我是一个友善的助手[/profile] [mood]今天心情很好[/mood] [story]这是一个有趣的故事[/story]

但若勾选了 文本不使用插件预设的正则及格式解析器,上面三种格式都不会被解析为键值对,updateTemplateVariables(vars) 会将 AI 输出的文本完整发过来。需要在前端自行对 updateTemplateVariables(vars) 进行解析






开箱即用模板---可以此模板作为你的流式渲染卡基础,非常简单

<!DOCTYPE html> <html> <head> <title>沉浸式模板</title> <style> /* CSS样式 */ </style> </head> <body> <!-- HTML布局 --> </body> <script> // 关键:所有代码都必须在这个事件监听器里! document.addEventListener('DOMContentLoaded', function() { // 1. 缓存所有会用到的UI元素 const UIElements = { // 示例:element1: document.getElementById('element1'), }; // 2. 本地状态缓存(仅用于单次iframe生命周期内的临时状态) let localState = { // 用于存储:加载状态、防重复计算标志、临时计算结果等 // 注意:这里的数据在iframe重建时会丢失,这是正常的 isLoading: false, turnUpdateApplied: false, // 防止流式输出时重复计算的标志 }; // 3. 核心函数:处理AI的动态数据 window.updateTemplateVariables = function(vars) { if (!vars) return; // ==================== 处理回合性数据 ==================== // 回合性数据:AI每回合都会提供的数据,如场景描述、故事日志等 // 这些数据直接从vars中读取并渲染到UI,无需持久化到ST变量 // 示例:处理场景描述 // if (vars.scene?.description) { // UIElements.sceneText.innerHTML = vars.scene.description.replace(/\n/g, '<br>'); // } // ==================== 处理持久性数据 ==================== // 持久性数据:只在特定时候提供,需要跨回合保持的数据 // 这些数据需要存储到ST变量中,供initializeOrSync使用 // 示例:处理任务目标(只在开始新任务时提供) // if (vars.game?.objective) { // localState.objective = vars.game.objective; // STscript(`/setvar key=game_objective "${vars.game.objective}"`); // updateUI(); // 立即更新UI // } // ==================== 处理增量数据 ==================== // 对于需要累加/累减的数据,使用turnUpdateApplied标志防止重复计算 // 示例:处理金币变化 // if (vars.player?.gold_change !== undefined && !localState.turnUpdateApplied) { // const change = parseInt(vars.player.gold_change); // localState.currentGold += change; // STscript(`/setvar key=player_gold ${localState.currentGold}`); // updateUI(); // localState.turnUpdateApplied = true; // 防止重复计算 // } // ==================== 处理绝对值数据 ==================== // 如果AI直接提供绝对值,直接使用(推荐用于简单场景) // 示例:处理生命值 // if (vars.player?.hp !== undefined) { // localState.currentHP = parseInt(vars.player.hp); // STscript(`/setvar key=player_hp ${localState.currentHP}`); // updateUI(); // } }; // 4. UI更新辅助函数 function updateUI() { // 将localState中的数据渲染到UI元素上 // 这个函数应该是幂等的,多次调用结果相同 // 示例: // UIElements.goldDisplay.textContent = localState.currentGold; // UIElements.hpBar.style.width = (localState.currentHP / localState.maxHP * 100) + '%'; } // 5. 用户操作处理函数 async function onUserClick() { // 在用户操作开始时,重置回合更新标志 localState.turnUpdateApplied = false; // 调用STscript与SillyTavern通信 // 示例: // await STscript('/send 我要购买这个物品'); // await STscript('/trigger'); } // 6. 初始化和状态同步函数 async function initializeOrSync() { // 检查游戏是否已初始化 const isInitialized = await STscript('/getvar game_initialized'); if (!isInitialized) { // 首次运行:初始化默认值 await STscript('/setvar key=game_initialized true'); // await STscript('/setvar key=player_gold 100'); // await STscript('/setvar key=player_hp 100'); // await STscript('/setvar key=game_objective "暂无任务"'); } // 每次iframe加载时:从ST变量同步持久化状态 // const gold = parseInt(await STscript('/getvar player_gold')) || 0; // const hp = parseInt(await STscript('/getvar player_hp')) || 100; // const objective = await STscript('/getvar game_objective') || '暂无任务'; // 更新localState // localState.currentGold = gold; // localState.currentHP = hp; // localState.objective = objective; // 立即更新UI显示 updateUI(); } // 7. 绑定事件监听器 // UIElements.buyButton.addEventListener('click', onUserClick); // 8. 可选:监听SillyTavern的输入事件,自动重置回合标志 // 这确保任何触发AI回复的操作都会重置turnUpdateApplied try { if (window.parent) { const inputElement = window.parent.document.getElementById('send_textarea'); const sendButton = window.parent.document.getElementById('send_button'); if (inputElement) { inputElement.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { localState.turnUpdateApplied = false; } }); } if (sendButton) { sendButton.addEventListener('click', function() { localState.turnUpdateApplied = false; }); } } } catch (error) { // 如果无法访问父窗口,忽略错误 } // 9. 启动初始化 initializeOrSync(); }); </script> </html>