Skip to content

Instantly share code, notes, and snippets.

@CodeBoy2006
Last active August 2, 2025 02:52
Show Gist options
  • Select an option

  • Save CodeBoy2006/5b5bf6273cede317fce287b2fdcc244f to your computer and use it in GitHub Desktop.

Select an option

Save CodeBoy2006/5b5bf6273cede317fce287b2fdcc244f to your computer and use it in GitHub Desktop.
SillyTavern 3D视野插件 - 让角色的视野可视化 (通过酒馆助手加载)
<vision-renderer-guide>
<rule>
**启用附属模块[视觉投影仪] - 强制执行**
视觉投影仪 (Scene Projector) 被激活为一个强制性子模块,用于处理所有相关的三维空间布局和视野渲染。其功能至关重要,**绝不能被跳过**。
**核心职责:** 在任何需要以三维空间布局来展示角色视野的回合末尾,视觉投影仪 **必须** 按顺序执行以下两个动作:
1. **`<visionThink>` 块:**
* 目的: 提供一个**强制性的、详尽的**思考过程,解释为何需要生成视觉布局,以及如何根据**当前回合的事件**来构建它。
* 内容要求:
* **精确识别**:列出所有需要被渲染的对象(角色、物品、地形等)。
* **严谨定位**:根据当前回合的叙述,为每个对象确定其精确的坐标 (`pos`) 和尺寸 (`dim`)。所有坐标均以**`<user>`的位置为原点(0,0,0)**。
* **合理分类**:为每个对象分配合适的类型 (`type`)。
* **数据来源**: 所有渲染信息必须**严格源于**本回合的叙述。**禁止凭空捏造**场景细节。
2. **`<vision3d>` 块:**
* 目的: 提供**精确的、可执行的**标签块,用于实际渲染在 `<visionThink>` 中规划好的场景。如果 `<visionThink>` 确定需要渲染,则此块**不得省略**。
* 操作限制: **只能**使用在 `<vision-syntax>` 中定义的标签和格式。
* 严格格式化规则:
* **完全遵守**指定的标签和参数语法。
* 所有字符串值**必须**使用**双引号**包围 (例如, `content:"哥布林"`).
* 所有参数名和类型名**必须**使用**英文** (例如, `pos`, `dim`, `type`).
* 坐标和尺寸的数值之间用逗号 `,` 分隔。
* 参数之间用分号 `;` 分隔。
**关键放置:** `<visionThink>` 和 `<vision3d>` 块**必须总是成对出现**,并且**只能出现在回复的绝对末尾**,在所有叙事或情节内容之后。它们**绝不能**出现在初步思考或预计算阶段。
**失败条款:** 在回合事件明显需要进行视觉布局更新时,省略 `<visionThink>` 或 `<vision3d>` 块,是对视觉投影仪模块的**一次严重失败**。勤勉和完整性至关重要。
</rule>
<vision-syntax>
<!-- 定义三维视觉渲染的精确语法 -->
<!-- 3D 空间视角 -->
<vision3d size="数字">
<item>pos:X,Y,Z; dim:W,D,H; content:"文本"; type:"分类";</item>
<!-- ... more items ... -->
</vision3d>
</vision-syntax>
<coordinate-system-and-types>
<!-- 定义坐标系和对象类型 -->
<!-- 原点: <user>的位置永远是坐标系的原点 (0,0,0) -->
**3D坐标系 (空间):**
- `pos:X,Y,Z`: 对象的中心点坐标。
- `X`: 左右。正右负左。
- `Y`: 前后。正前负后。
- `Z`: 上下。正上负下。
- `dim:W,D,H`: 对象的尺寸。
- `W (Width)`: 沿X轴的宽度。
- `D (Depth)`: 沿Y轴的深度。
- `H (Height)`: 沿Z轴的高度。
**对象类型 (type):**
- `友方 (Friendly)`: 盟友、友好的NPC等。
- `敌方 (Hostile)`: 敌人、具有威胁的实体。
- `中立 (Neutral)`: 不具有明显立场的NPC、动物等。
- `物品 (Item)`: 可交互的物品、道具、宝箱等。
- `地形 (Terrain)`: 静态的环境元素,如墙壁、柱子、障碍物等。
</coordinate-system-and-types>
<example>
<!-- 示例:演示了思考过程和对应的渲染标签 -->
<!-- 场景叙述: 你(代号“凤凰”)正蹲伏在一张翻倒的重金属桌后,这里是“奇点实验室”被毁坏的服务器机房。在你正前方8米处,一名敌对士兵正以一个高大的服务器机柜为掩护。你的右侧,一条金属 catwalk(猫道)部分坍塌,形成了一个斜坡,另一名狙击手正占据着4米高的平台残余部分。你的盟友,阿里斯博士,正蜷缩在左前方远处的一组服务器后方,他身旁的墙壁上,一个破损的动力导管正迸射出危险的电火花。一个至关重要的“数据核心”掉落在你左手边的地板上。 -->
<visionThink>
<!--
基于当前场景分析:
1. **场景设置**: 这是一个复杂的多层次战斗场景,包含高低差(地面、坍塌的猫道)、多种类型的实体(友方、敌方、物品)和掩体。必须使用 `<vision3d>` 进行渲染。设定视野范围 `size` 为25米,以清晰包含所有关键元素。
2. **识别并定位对象**:
- **观察者角色 (在这个场景是“凤凰”)**: 位于原点(0,0,0),作为所有坐标的参考。
- **翻倒的金属桌 (地形)**: 你正前方的掩体,假设它高1米,宽2米。`pos:0,1.5,0.5; dim:2,1,1;`
- **地面士兵 (敌方)**: 正前方8米(Y=8)。假设他在桌子左侧探头,所以X为负。身高1.8米。`pos:-1,8,0.9; dim:0.8,0.8,1.8;`
- **士兵掩体-服务器机柜 (地形)**: 在地面士兵前方,提供掩护。`pos:-1,9,1.5; dim:1,2,3;`
- **猫道平台 (地形)**: 右侧,4米高。假设平台中心点在右侧10米(X=10),前方6米(Y=6)处。`pos:10,6,3.75; dim:6,8,0.5;`
- **坍塌的斜坡 (地形)**: 连接地面和猫道平台,用一个倾斜的长方体来近似表示。`pos:6,3,1.75; dim:4,6,3.5;`
- **狙击手 (敌方)**: 在猫道平台上。`pos:10,7,4.9; dim:0.8,0.8,1.8;` (4米平台+0.9米身高中心)
- **阿里斯博士 (友方)**: 左前方远处,蜷缩着。`pos:-10,14,0.5; dim:0.8,0.8,1;` (蜷缩高度1米)
- **博士掩体-服务器组 (地形)**: 在博士前方。`pos:-10,15,1.5; dim:4,2,3;`
- **电火花导管 (地形)**: 博士旁边的墙上,设定墙在X=-12处。`pos:-12,14,2; dim:0.2,1,1;`
- **数据核心 (物品)**: 在你左手边地板上。`pos:-2,1,0.05; dim:0.2,0.2,0.1;`
-->
</visionThink>
<vision3d size="25">
<!-- Your Cover -->
<item>pos:0,1.5,0.5; dim:2,1,1; content:"翻倒的桌子"; type:"地形";</item>
<!-- Hostile Soldier on the ground floor -->
<item>pos:-1,8,0.9; dim:0.8,0.8,1.8; content:"士兵"; type:"敌方";</item>
<!-- Server Rack cover for the ground soldier -->
<item>pos:-1,9,1.5; dim:1,2,3; content:"服务器机柜"; type:"地形";</item>
<!-- Elevated Catwalk Platform -->
<item>pos:10,6,3.75; dim:6,8,0.5; content:"猫道平台"; type:"地形";</item>
<!-- Collapsed Ramp leading to the catwalk -->
<item>pos:6,3,1.75; dim:4,6,3.5; content:"坍塌的斜坡"; type:"地形";</item>
<!-- Hostile Sniper on the catwalk -->
<item>pos:10,7,4.9; dim:0.8,0.8,1.8; content:"狙击手"; type:"敌方";</item>
<!-- Dr. Aris, your ally -->
<item>pos:-10,14,0.5; dim:0.8,0.8,1; content:"阿里斯博士"; type:"友方";</item>
<!-- Server Racks providing cover for the doctor -->
<item>pos:-10,15,1.5; dim:4,2,3; content:"服务器组"; type:"地形";</item>
<!-- Damaged Power Conduit -->
<item>pos:-12,14,2; dim:0.2,1,1; content:"电火花"; type:"地形";</item>
<!-- The objective item -->
<item>pos:-2,1,0.05; dim:0.2,0.2,0.1; content:"数据核心"; type:"物品";</item>
</vision3d>
</example>
</dataTable-operatior-guide>
// ==UserScript==
// @name 全局脚本 - 3D视野组件 (单 Canvas v10.1)
// @version 10.1
// @description 将标签改为 3D Sprite,仅用一张 Canvas;解决多次 scale 累积问题;优化 resize、hover 效果;完善事件触发逻辑
// @author codeboy
// @match */*
// @grant none
// ==/UserScript==
(function () {
'use strict';
/* -------------------- 配置 -------------------- */
const WIDGET_NAME = 'GlobalScript_Vision3D_SingleCanvas';
const VISION_TYPES = {
'友方': { color: 0x28a745 },
'敌方': { color: 0xdc3545 },
'中立': { color: 0x6c757d },
'物品': { color: 0x007bff },
'地形': { color: 0x8B4513 }
};
const USER_COLOR = 0xffd700;
const FONT_FAMILY = 'Arial, "PingFang SC", "Microsoft YaHei", sans-serif';
/* -------------------- 调试日志 -------------------- */
let debugMode = false; // 可通过控制台设置 window.vision3dDebug = true
function logDebug(...args) {
if (debugMode || window.vision3dDebug) {
console.log(`[${WIDGET_NAME}]`, ...args);
}
}
/* ------------------ 依赖加载 ------------------ */
let libsReady = false;
async function ensureThree() {
if (libsReady) return true;
const load = src => new Promise((r, j) => {
if (document.querySelector(`script[src="${src}"]`)) return r();
const s = Object.assign(document.createElement('script'), { src });
s.onload = r; s.onerror = () => j(new Error(`load fail: ${src}`));
document.head.appendChild(s);
});
try {
await load('https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js');
await load('https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js');
libsReady = true; return true;
} catch (e) {
console.error(WIDGET_NAME, e); return false;
}
}
/* -------------------- 样式 -------------------- */
function injectCss() {
if (document.getElementById(`${WIDGET_NAME}-style`)) return;
const style = document.createElement('style');
style.id = `${WIDGET_NAME}-style`;
style.textContent = `
.vision3d-container{position:relative;width:100%;max-width:600px;border:1px solid #444;border-radius:8px;margin:10px auto;aspect-ratio:1/1;overflow:hidden;cursor:grab}
.vision3d-container:active{cursor:grabbing}
.vision3d-canvas{position:absolute;top:0;left:0;width:100%!important;height:100%!important;display:block}
`;
document.head.appendChild(style);
}
/* ------------ 工具:文字转 Sprite ------------ */
function makeTextSprite(text, { baseScale = 0.8, color = '#f0f0f0', bg = 'rgba(0,0,0,.75)', padding = 6 } = {}) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `bold 48px ${FONT_FAMILY}`; // 先用大字号测宽
const textW = ctx.measureText(text).width;
const textH = 48;
canvas.width = textW + padding * 2;
canvas.height = textH + padding * 2;
// 重新以正确尺寸绘制
ctx.font = `bold 48px ${FONT_FAMILY}`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = bg;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = color;
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter; // 防止离屏抽样模糊
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false });
const sprite = new THREE.Sprite(material);
// 基础缩放:用像素转化为世界单位;此比例可按需调整
const pxRatio = 0.01; // 1px ≈ 0.01 world unit
sprite.scale.set(canvas.width * pxRatio, canvas.height * pxRatio, 1).multiplyScalar(baseScale);
// 附加自定义字段以便 hover 动态调整
sprite.userData = { baseScale: sprite.scale.clone(), canvas };
return sprite;
}
/* ------------ 运行时存储 & 清理 -------------- */
const active = new Map();
const renderTimers = new Map(); // 防抖计时器
function cleanup(id) {
const st = active.get(id); if (!st) return;
// 清理防抖计时器
const timer = renderTimers.get(id);
if (timer) {
clearTimeout(timer);
renderTimers.delete(id);
}
cancelAnimationFrame(st.raf);
st.container.removeEventListener('mousemove', st.onMouseMove);
st.resizeObserver.disconnect();
st.renderer.dispose();
st.scene.traverse(o => {
if (o.isMesh) {
o.geometry?.dispose();
(Array.isArray(o.material) ? o.material : [o.material]).forEach(m => m?.dispose?.());
}
if (o.isSprite) {
o.material?.map?.dispose();
o.material?.dispose();
}
});
active.delete(id);
logDebug(`清理 3D 视野组件: ${id}`);
}
/* --------------- 渲染主逻辑 ------------------ */
async function render3D(id, $placeholder, worldSize, xml) {
if (!await ensureThree()) return;
/* ---------- 解析 <vision3d> 数据 ---------- */
const items = [...xml.matchAll(/<item>(.*?)<\/item>/gis)].map(m => {
const obj = {}; m[1].split(';').forEach(p => {
const [k, v] = p.split(':').map(s => s.trim());
if (k && v) obj[k] = v.replace(/"/g, '');
});
const [x, y, z] = (obj.pos || '0,0,0').split(',').map(Number);
const [w, d, h] = (obj.dim || '1,1,1').split(',').map(Number);
return { pos: { x, y, z }, dim: { w, d, h }, label: obj.content, type: obj.type || '中立' };
});
/* ------------------- DOM ------------------ */
$placeholder.html(`<div class="vision3d-container"><canvas class="vision3d-canvas"></canvas></div>`);
const container = $placeholder.find('.vision3d-container')[0];
const canvas3D = container.querySelector('canvas');
/* ------------------- Three ---------------- */
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 1000);
camera.position.set(worldSize * .8, worldSize * .8, worldSize * .8);
const renderer = new THREE.WebGLRenderer({ canvas: canvas3D, antialias: true, alpha: true });
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.target.set(0, 0, 0);
/* 网格与玩家 */
scene.add(new THREE.GridHelper(worldSize, worldSize, 0x555555, 0x555555));
const playerGeo = new THREE.ConeGeometry(.5, 2, 8).translate(0, 1, 0);
const playerMesh = new THREE.Mesh(playerGeo, new THREE.MeshBasicMaterial({ color: USER_COLOR }));
scene.add(playerMesh);
const playerLabel = makeTextSprite('玩家', { baseScale: 1 });
playerLabel.position.set(0, 2.5, 0);
scene.add(playerLabel);
/* items mesh & label */
const meshes = [];
items.forEach(it => {
const color = VISION_TYPES[it.type]?.color || VISION_TYPES['中立'].color;
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(it.dim.w, it.dim.h, it.dim.d),
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.9 })
);
mesh.position.set(it.pos.x, it.pos.z + it.dim.h / 2, -it.pos.y);
scene.add(mesh);
const labelSprite = makeTextSprite(it.label, { baseScale: 0.9 });
labelSprite.position.copy(mesh.position).add(new THREE.Vector3(0, it.dim.h / 2 + .5, 0));
scene.add(labelSprite);
mesh.userData = { labelSprite };
meshes.push(mesh);
});
/* 自适应尺寸 */
function syncSize() {
const { width, height } = container.getBoundingClientRect();
if (!width || !height) return;
const s = Math.min(width, height);
camera.aspect = 1; camera.updateProjectionMatrix();
renderer.setSize(s, s, false);
}
const resizeObserver = new ResizeObserver(syncSize); resizeObserver.observe(container);
syncSize();
/* Hover 光线检测 */
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoverMesh = null;
function onMouseMove(e) {
const r = container.getBoundingClientRect();
mouse.x = ((e.clientX - r.left) / r.width) * 2 - 1;
mouse.y = -((e.clientY - r.top) / r.height) * 2 + 1;
}
container.addEventListener('mousemove', onMouseMove);
/* 视距缩放函数 */
function updateLabelScale(sprite) {
const camDist = camera.position.distanceTo(sprite.position);
const scaleFactor = camDist * 0.04; // 调节粗细可见度;经验参数
sprite.scale.copy(sprite.userData.baseScale).multiplyScalar(scaleFactor);
}
/* 渲染循环 */
function loop() {
controls.update();
// hover
raycaster.setFromCamera(mouse, camera);
const intersect = raycaster.intersectObjects(meshes)[0]?.object || null;
if (intersect !== hoverMesh) {
if (hoverMesh) {
hoverMesh.material.opacity = 0.9;
hoverMesh.userData.labelSprite.material.opacity = 1.0;
hoverMesh.userData.labelSprite.scale.copy(hoverMesh.userData.labelSprite.userData.baseScale);
}
hoverMesh = intersect;
if (hoverMesh) {
hoverMesh.material.opacity = 0.5;
hoverMesh.userData.labelSprite.material.opacity = 1.2;
hoverMesh.userData.labelSprite.scale.multiplyScalar(1.25);
}
}
// 每帧动态调整文字大小
updateLabelScale(playerLabel);
meshes.forEach(m => updateLabelScale(m.userData.labelSprite));
renderer.render(scene, camera);
active.get(id).raf = requestAnimationFrame(loop);
}
/* 注册清理对象 */
active.set(id, {
scene, renderer, container, onMouseMove, resizeObserver, raf: requestAnimationFrame(loop)
});
logDebug(`创建 3D 视野组件: ${id}, items: ${items.length}, worldSize: ${worldSize}`);
}
/* --------------- 防抖渲染函数 --------------- */
function renderVisionMessageDebounced(messageId, $mesText, delay = 150) {
// 清除之前的计时器
const existingTimer = renderTimers.get(messageId);
if (existingTimer) {
clearTimeout(existingTimer);
}
// 设置新的防抖计时器
const timer = setTimeout(() => {
renderVisionMessage(messageId, $mesText, false);
renderTimers.delete(messageId);
}, delay);
renderTimers.set(messageId, timer);
logDebug(`防抖渲染消息: ${messageId}, 延迟: ${delay}ms`);
}
/* --------------- 立即渲染函数 --------------- */
function renderVisionMessageImmediate(messageId, $mesText) {
// 清除防抖计时器(如果存在)
const existingTimer = renderTimers.get(messageId);
if (existingTimer) {
clearTimeout(existingTimer);
renderTimers.delete(messageId);
}
renderVisionMessage(messageId, $mesText, true);
logDebug(`立即渲染消息: ${messageId}`);
}
/* --------------- 核心渲染函数 --------------- */
function renderVisionMessage(messageId, $mesText, isImmediate = false) {
const msgObj = SillyTavern.chat[messageId];
if (!msgObj) {
logDebug(`消息不存在: ${messageId}`);
return;
}
const reg = /<vision3d size="([\d.]+)">(.*?)<\/vision3d>/s;
const match = msgObj.mes.match(reg);
// 如果消息已有组件但不再包含vision3d标签,清理旧实例
if (!match && active.has(messageId)) {
cleanup(messageId);
}
// 如果找到vision3d标签,先清理旧实例再创建新实例
if (match) {
cleanup(messageId);
let html = msgObj.mes;
const placeholderId = `v3d-${messageId}-${Date.now()}`;
html = html.replace(reg, `<div id="${placeholderId}"></div>`);
// 如果没有传入$mesText,尝试获取
if (!$mesText) {
const mesElement = $(`#chat .mes[mesid="${messageId}"]`);
if (mesElement.length) {
$mesText = mesElement.find('.mes_text');
}
}
if ($mesText && $mesText.length) {
$mesText.html(SillyTavern.messageFormatting(html, msgObj.name, msgObj.is_system, msgObj.is_user, messageId));
const renderDelay = isImmediate ? 0 : 50; // 立即渲染时不延迟
setTimeout(() => {
const $placeholder = $(`#${placeholderId}`, $mesText);
if ($placeholder.length) {
render3D(messageId, $placeholder, +match[1], match[2]);
}
}, renderDelay);
}
}
}
/* --------------- 批量渲染函数 --------------- */
function renderAllVisionMessages() {
logDebug('开始批量渲染所有vision3d消息');
$('#chat .mes[is_user="false"][is_system="false"]').each((_, el) => {
const messageId = +el.getAttribute('mesid');
const $mesText = $('.mes_text', el);
if (messageId && $mesText.length) {
renderVisionMessageDebounced(messageId, $mesText, 100);
}
});
}
/* --------------- 优化的事件监听设置 --------------- */
function setupOptimizedEventListeners() {
if (!SillyTavern.eventTypes || !SillyTavern.eventSource) {
logDebug('SillyTavern 事件系统未就绪');
return;
}
const eventTypes = SillyTavern.eventTypes;
const eventSource = SillyTavern.eventSource;
logDebug('设置优化的事件监听器');
// 消息生成状态追踪
let messageSentRecently = false;
let messageSentResetTimer = null;
// 监听消息发送事件
if (eventTypes.MESSAGE_SENT) {
eventSource.on(eventTypes.MESSAGE_SENT, () => {
logDebug('Event: MESSAGE_SENT');
messageSentRecently = true;
if (messageSentResetTimer) {
clearTimeout(messageSentResetTimer);
}
// 1秒后自动重置标志
messageSentResetTimer = setTimeout(() => {
messageSentRecently = false;
messageSentResetTimer = null;
logDebug('自动重置 messageSentRecently 标志');
}, 1000);
});
}
// 生成开始时的条件渲染
if (eventTypes.GENERATION_STARTED) {
eventSource.on(eventTypes.GENERATION_STARTED, () => {
logDebug(`Event: GENERATION_STARTED (messageSentRecently: ${messageSentRecently})`);
if (messageSentRecently) {
// 获取最新消息进行防抖渲染
const latestMessage = SillyTavern.chat[SillyTavern.chat.length - 1];
if (latestMessage && !latestMessage.is_user) {
renderVisionMessageDebounced(SillyTavern.chat.length - 1);
}
messageSentRecently = false;
if (messageSentResetTimer) {
clearTimeout(messageSentResetTimer);
messageSentResetTimer = null;
}
} else {
logDebug('跳过 GENERATION_STARTED 事件的渲染,因为不是由消息发送触发的');
}
});
}
// 生成结束时的立即渲染
if (eventTypes.GENERATION_ENDED) {
eventSource.on(eventTypes.GENERATION_ENDED, () => {
logDebug('Event: GENERATION_ENDED');
// 重置消息发送标志
messageSentRecently = false;
if (messageSentResetTimer) {
clearTimeout(messageSentResetTimer);
messageSentResetTimer = null;
}
// 立即渲染最新的非用户消息
for (let i = SillyTavern.chat.length - 1; i >= 0; i--) {
const msg = SillyTavern.chat[i];
if (msg && !msg.is_user && !msg.is_system) {
renderVisionMessageImmediate(i);
break;
}
}
});
}
// 消息接收时的立即渲染
if (eventTypes.MESSAGE_RECEIVED) {
eventSource.on(eventTypes.MESSAGE_RECEIVED, (messageId) => {
logDebug(`Event: MESSAGE_RECEIVED, messageId: ${messageId}`);
const msg = SillyTavern.chat[messageId];
if (msg && !msg.is_user && !msg.is_system) {
renderVisionMessageImmediate(messageId);
}
});
}
// 消息切换事件 - 防抖渲染
if (eventTypes.MESSAGE_SWIPED) {
eventSource.on(eventTypes.MESSAGE_SWIPED, (messageId) => {
logDebug(`Event: MESSAGE_SWIPED, messageId: ${messageId}`);
const msg = SillyTavern.chat[messageId];
if (msg && !msg.is_user && !msg.is_system) {
renderVisionMessageDebounced(messageId, null, 200);
}
});
}
// 消息更新事件 - 防抖渲染
if (eventTypes.MESSAGE_UPDATED) {
eventSource.on(eventTypes.MESSAGE_UPDATED, (messageId) => {
logDebug(`Event: MESSAGE_UPDATED, messageId: ${messageId}`);
const msg = SillyTavern.chat[messageId];
if (msg && !msg.is_user && !msg.is_system) {
renderVisionMessageDebounced(messageId, null, 150);
}
});
}
// 消息删除事件 - 立即清理
if (eventTypes.MESSAGE_DELETED) {
eventSource.on(eventTypes.MESSAGE_DELETED, (messageId) => {
logDebug(`Event: MESSAGE_DELETED, messageId: ${messageId}`);
cleanup(messageId);
});
}
// 批量消息删除事件
if (eventTypes.MESSAGES_DELETED) {
eventSource.on(eventTypes.MESSAGES_DELETED, () => {
logDebug('Event: MESSAGES_DELETED');
// 清理所有实例,稍后重新渲染
active.forEach((_, id) => cleanup(id));
setTimeout(renderAllVisionMessages, 300);
});
}
// 角色切换事件
if (eventTypes.CHARACTER_FIRST_MESSAGE_SELECTED) {
eventSource.on(eventTypes.CHARACTER_FIRST_MESSAGE_SELECTED, () => {
logDebug('Event: CHARACTER_FIRST_MESSAGE_SELECTED');
// 清理所有旧实例
active.forEach((_, id) => cleanup(id));
// 重新渲染所有消息
setTimeout(renderAllVisionMessages, 200);
});
}
// 其他需要重新渲染的事件 - 防抖处理
const otherDebouncedEvents = [
eventTypes.IMAGE_SWIPED,
eventTypes.MESSAGE_FILE_EMBEDDED,
eventTypes.MESSAGE_REASONING_EDITED,
eventTypes.MESSAGE_REASONING_DELETED,
eventTypes.FILE_ATTACHMENT_DELETED,
eventTypes.GROUP_UPDATED
].filter(Boolean);
otherDebouncedEvents.forEach(eventType => {
if (eventType) {
eventSource.on(eventType, () => {
logDebug(`Event (Other Debounced): ${eventType}`);
setTimeout(renderAllVisionMessages, 300);
});
}
});
logDebug('优化的事件监听器设置完成');
}
/* --------------- 页面卸载清理 --------------- */
function setupCleanupOnUnload() {
window.addEventListener('beforeunload', () => {
logDebug('页面卸载,清理所有3D视野组件');
active.forEach((_, id) => cleanup(id));
renderTimers.forEach(timer => clearTimeout(timer));
renderTimers.clear();
});
}
/* ------------------ 启动脚本 ------------------ */
function waitReady() {
if (typeof SillyTavern !== 'undefined' &&
SillyTavern.chat &&
SillyTavern.eventSource &&
SillyTavern.eventTypes) {
logDebug('SillyTavern 环境就绪,初始化3D视野组件');
injectCss();
setupOptimizedEventListeners();
setupCleanupOnUnload();
// 初始渲染现有消息
setTimeout(renderAllVisionMessages, 500);
// 暴露调试接口到全局
window.vision3dDebug = false;
window.vision3dCleanup = (id) => {
if (id !== undefined) {
cleanup(id);
} else {
active.forEach((_, id) => cleanup(id));
}
};
window.vision3dRenderAll = renderAllVisionMessages;
logDebug('3D视野组件初始化完成');
} else {
setTimeout(waitReady, 300);
}
}
waitReady();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment