Unify fourth-wall voice playback with TTS synth runtime

This commit is contained in:
2026-02-18 23:08:44 +08:00
parent 65e26e4c72
commit 37ae0a9769
6 changed files with 557 additions and 472 deletions

View File

@@ -1,5 +1,5 @@
// ════════════════════════════════════════════════════════════════════════════
// 次元壁模块 - 主控制器
// ════════════════════════════════════════════
// Fourth Wall Module - Main Controller
// ════════════════════════════════════════════════════════════════════════════
import { extension_settings, getContext, saveMetadataDebounced } from "../../../../../extensions.js";
import { saveSettingsDebounced, chat_metadata, default_user_avatar, default_avatar } from "../../../../../../script.js";
@@ -8,19 +8,20 @@ import { createModuleEvents, event_types } from "../../core/event-manager.js";
import { xbLog } from "../../core/debug-core.js";
import { handleCheckCache, handleGenerate, clearExpiredCache } from "./fw-image.js";
import { DEFAULT_VOICE, DEFAULT_SPEED } from "./fw-voice.js";
import {
buildPrompt,
buildCommentaryPrompt,
import { synthesizeAndPlay, stopCurrent as stopCurrentVoice } from "./fw-voice-runtime.js";
import {
buildPrompt,
buildCommentaryPrompt,
DEFAULT_TOPUSER,
DEFAULT_CONFIRM,
DEFAULT_BOTTOM,
DEFAULT_META_PROTOCOL
DEFAULT_META_PROTOCOL
} from "./fw-prompt.js";
import { initMessageEnhancer, cleanupMessageEnhancer } from "./fw-message-enhancer.js";
import { postToIframe, isTrustedMessage, getTrustedOrigin } from "../../core/iframe-messaging.js";
// ════════════════════════════════════════════════════════════════════════════
// 常量
// ════════════════════════════════════════════
// Constants
// ════════════════════════════════════════════════════════════════════════════
const events = createModuleEvents('fourthWall');
@@ -30,7 +31,7 @@ const COMMENTARY_COOLDOWN = 180000;
const IFRAME_PING_TIMEOUT = 800;
// ════════════════════════════════════════════════════════════════════════════
// 状态
// State
// ════════════════════════════════════════════════════════════════════════════
let overlayCreated = false;
@@ -44,37 +45,36 @@ let currentLoadedChatId = null;
let lastCommentaryTime = 0;
let commentaryBubbleEl = null;
let commentaryBubbleTimer = null;
let currentVoiceRequestId = null;
// ═══════════════════════════════ 新增 ═══════════════════════════════
let visibilityHandler = null;
let pendingPingId = null;
// ════════════════════════════════════════════════════════════════════
// ════════════════════════════════════════════════════════════════════════════
// 设置管理(保持不变)
// Settings
// ════════════════════════════════════════════════════════════════════════════
function getSettings() {
extension_settings[EXT_ID] ||= {};
const s = extension_settings[EXT_ID];
s.fourthWall ||= { enabled: true };
s.fourthWallImage ||= { enablePrompt: false };
s.fourthWallVoice ||= { enabled: false, voice: DEFAULT_VOICE, speed: DEFAULT_SPEED };
s.fourthWallVoice ||= { enabled: false };
s.fourthWallCommentary ||= { enabled: false, probability: 30 };
s.fourthWallPromptTemplates ||= {};
const t = s.fourthWallPromptTemplates;
if (t.topuser === undefined) t.topuser = DEFAULT_TOPUSER;
if (t.confirm === undefined) t.confirm = DEFAULT_CONFIRM;
if (t.bottom === undefined) t.bottom = DEFAULT_BOTTOM;
if (t.metaProtocol === undefined) t.metaProtocol = DEFAULT_META_PROTOCOL;
return s;
}
// ════════════════════════════════════════════════════════════════════════════
// 工具函数(保持不变)
// Utilities
// ════════════════════════════════════════════════════════════════════════════
function b64UrlEncode(str) {
@@ -162,7 +162,7 @@ function getAvatarUrls() {
}
// ════════════════════════════════════════════════════════════════════════════
// 存储管理(保持不变)
// Storage
// ════════════════════════════════════════════════════════════════════════════
function getFWStore(chatId = getCurrentChatIdSafe()) {
@@ -171,17 +171,17 @@ function getFWStore(chatId = getCurrentChatIdSafe()) {
chat_metadata[chatId].extensions ||= {};
chat_metadata[chatId].extensions[EXT_ID] ||= {};
chat_metadata[chatId].extensions[EXT_ID].fw ||= {};
const fw = chat_metadata[chatId].extensions[EXT_ID].fw;
fw.settings ||= { maxChatLayers: 9999, maxMetaTurns: 9999, stream: true };
if (!fw.sessions) {
const oldHistory = Array.isArray(fw.history) ? fw.history.slice() : [];
fw.sessions = [{ id: 'default', name: '默认记录', createdAt: Date.now(), history: oldHistory }];
fw.sessions = [{ id: 'default', name: 'Default', createdAt: Date.now(), history: oldHistory }];
fw.activeSessionId = 'default';
if (Object.prototype.hasOwnProperty.call(fw, 'history')) delete fw.history;
}
if (!fw.activeSessionId || !fw.sessions.find(s => s.id === fw.activeSessionId)) {
fw.activeSessionId = fw.sessions[0]?.id || null;
}
@@ -199,7 +199,7 @@ function saveFWStore() {
}
// ════════════════════════════════════════════════════════════════════════════
// iframe 通讯
// iframe Communication
// ════════════════════════════════════════════════════════════════════════════
function postToFrame(payload) {
@@ -224,7 +224,7 @@ function sendInitData() {
const settings = getSettings();
const session = getActiveSession();
const avatars = getAvatarUrls();
postToFrame({
type: 'INIT_DATA',
settings: store?.settings || {},
@@ -240,86 +240,128 @@ function sendInitData() {
}
// ════════════════════════════════════════════════════════════════════════════
// iframe 健康检测与恢复(新增)
// iframe Health Check & Recovery
// ════════════════════════════════════════════════════════════════════════════
function handleVisibilityChange() {
if (document.visibilityState !== 'visible') return;
const overlay = document.getElementById('xiaobaix-fourth-wall-overlay');
if (!overlay || overlay.style.display === 'none') return;
checkIframeHealth();
}
function checkIframeHealth() {
const iframe = document.getElementById('xiaobaix-fourth-wall-iframe');
if (!iframe) return;
// 生成唯一 ping ID
const pingId = 'ping_' + Date.now();
pendingPingId = pingId;
// 尝试发送 PING
try {
const win = iframe.contentWindow;
if (!win) {
recoverIframe('contentWindow 不存在');
recoverIframe('contentWindow missing');
return;
}
win.postMessage({ source: 'LittleWhiteBox', type: 'PING', pingId }, getTrustedOrigin());
} catch (e) {
recoverIframe('无法访问 iframe: ' + e.message);
recoverIframe('Cannot access iframe: ' + e.message);
return;
}
// 设置超时检测
setTimeout(() => {
if (pendingPingId === pingId) {
// 没有收到 PONG 响应
recoverIframe('PING 超时无响应');
recoverIframe('PING timeout');
}
}, IFRAME_PING_TIMEOUT);
}
function handlePongResponse(pingId) {
if (pendingPingId === pingId) {
pendingPingId = null; // 清除,表示收到响应
pendingPingId = null;
}
}
function recoverIframe(reason) {
const iframe = document.getElementById('xiaobaix-fourth-wall-iframe');
if (!iframe) return;
try { xbLog.warn('fourthWall', `iframe 恢复中: ${reason}`); } catch {}
// 重置状态
try { xbLog.warn('fourthWall', `iframe recovery: ${reason}`); } catch { }
frameReady = false;
pendingFrameMessages = [];
pendingPingId = null;
// 如果正在流式生成,取消
if (isStreaming) {
cancelGeneration();
}
// 重新加载 iframe
iframe.src = iframePath;
}
// ════════════════════════════════════════════════════════════════════════════
// 消息处理(添加 PONG 处理)
// Voice Handling
// ════════════════════════════════════════════════════════════════════════════
function handlePlayVoice(data) {
const { text, emotion, voiceRequestId } = data;
if (!text?.trim()) {
postToFrame({ type: 'VOICE_STATE', voiceRequestId, state: 'error', message: 'Voice text is empty' });
return;
}
// Notify old request as stopped
if (currentVoiceRequestId && currentVoiceRequestId !== voiceRequestId) {
postToFrame({ type: 'VOICE_STATE', voiceRequestId: currentVoiceRequestId, state: 'stopped' });
}
currentVoiceRequestId = voiceRequestId;
synthesizeAndPlay(text, emotion, {
requestId: voiceRequestId,
onState(state, info) {
if (currentVoiceRequestId !== voiceRequestId) return;
postToFrame({
type: 'VOICE_STATE',
voiceRequestId,
state,
duration: info?.duration,
message: info?.message,
});
},
});
}
function handleStopVoice(data) {
const targetId = data?.voiceRequestId || currentVoiceRequestId;
stopCurrentVoice();
if (targetId) {
postToFrame({ type: 'VOICE_STATE', voiceRequestId: targetId, state: 'stopped' });
}
currentVoiceRequestId = null;
}
function stopVoiceAndNotify() {
if (currentVoiceRequestId) {
postToFrame({ type: 'VOICE_STATE', voiceRequestId: currentVoiceRequestId, state: 'stopped' });
}
stopCurrentVoice();
currentVoiceRequestId = null;
}
// ════════════════════════════════════════════════════════════════════════════
// Frame Message Handler
// ════════════════════════════════════════════════════════════════════════════
function handleFrameMessage(event) {
const iframe = document.getElementById('xiaobaix-fourth-wall-iframe');
if (!isTrustedMessage(event, iframe, 'LittleWhiteBox-FourthWall')) return;
const data = event.data;
const store = getFWStore();
const settings = getSettings();
switch (data.type) {
case 'FRAME_READY':
frameReady = true;
@@ -327,11 +369,9 @@ function handleFrameMessage(event) {
sendInitData();
break;
// ═══════════════════════════ 新增 ═══════════════════════════
case 'PONG':
handlePongResponse(data.pingId);
break;
// ════════════════════════════════════════════════════════════
case 'TOGGLE_FULLSCREEN':
toggleFullscreen();
@@ -340,29 +380,29 @@ function handleFrameMessage(event) {
case 'SEND_MESSAGE':
handleSendMessage(data);
break;
case 'REGENERATE':
handleRegenerate(data);
break;
case 'CANCEL_GENERATION':
cancelGeneration();
break;
case 'SAVE_SETTINGS':
if (store) {
Object.assign(store.settings, data.settings);
saveFWStore();
}
break;
case 'SAVE_IMG_SETTINGS':
Object.assign(settings.fourthWallImage, data.imgSettings);
saveSettingsDebounced();
break;
case 'SAVE_VOICE_SETTINGS':
Object.assign(settings.fourthWallVoice, data.voiceSettings);
settings.fourthWallVoice.enabled = !!data.voiceSettings?.enabled;
saveSettingsDebounced();
break;
@@ -370,7 +410,7 @@ function handleFrameMessage(event) {
Object.assign(settings.fourthWallCommentary, data.commentarySettings);
saveSettingsDebounced();
break;
case 'SAVE_PROMPT_TEMPLATES':
settings.fourthWallPromptTemplates = data.templates;
saveSettingsDebounced();
@@ -382,7 +422,7 @@ function handleFrameMessage(event) {
saveSettingsDebounced();
sendInitData();
break;
case 'SAVE_HISTORY': {
const session = getActiveSession();
if (session) {
@@ -391,7 +431,7 @@ function handleFrameMessage(event) {
}
break;
}
case 'RESET_HISTORY': {
const session = getActiveSession();
if (session) {
@@ -400,7 +440,7 @@ function handleFrameMessage(event) {
}
break;
}
case 'SWITCH_SESSION':
if (store) {
store.activeSessionId = data.sessionId;
@@ -408,7 +448,7 @@ function handleFrameMessage(event) {
sendInitData();
}
break;
case 'ADD_SESSION':
if (store) {
const newId = 'sess_' + Date.now();
@@ -418,14 +458,14 @@ function handleFrameMessage(event) {
sendInitData();
}
break;
case 'RENAME_SESSION':
if (store) {
const sess = store.sessions.find(s => s.id === data.sessionId);
if (sess) { sess.name = data.name; saveFWStore(); sendInitData(); }
}
break;
case 'DELETE_SESSION':
if (store && store.sessions.length > 1) {
store.sessions = store.sessions.filter(s => s.id !== data.sessionId);
@@ -446,11 +486,19 @@ function handleFrameMessage(event) {
case 'GENERATE_IMAGE':
handleGenerate(data, postToFrame);
break;
case 'PLAY_VOICE':
handlePlayVoice(data);
break;
case 'STOP_VOICE':
handleStopVoice(data);
break;
}
}
// ════════════════════════════════════════════════════════════════════════════
// 生成处理(保持不变)
// Generation
// ════════════════════════════════════════════════════════════════════════════
async function startGeneration(data) {
@@ -462,9 +510,9 @@ async function startGeneration(data) {
voiceSettings: data.voiceSettings,
promptTemplates: getSettings().fourthWallPromptTemplates
});
const gen = window.xiaobaixStreamingGeneration;
if (!gen?.xbgenrawCommand) throw new Error('xbgenraw 模块不可用');
if (!gen?.xbgenrawCommand) throw new Error('xbgenraw module unavailable');
const topMessages = [
{ role: 'user', content: msg1 },
@@ -479,7 +527,7 @@ async function startGeneration(data) {
nonstream: data.settings.stream ? 'false' : 'true',
as: 'user',
}, '');
if (data.settings.stream) {
startStreamingPoll();
} else {
@@ -490,13 +538,13 @@ async function startGeneration(data) {
async function handleSendMessage(data) {
if (isStreaming) return;
isStreaming = true;
const session = getActiveSession();
if (session) {
session.history = data.history;
saveFWStore();
}
try {
await startGeneration(data);
} catch {
@@ -509,13 +557,13 @@ async function handleSendMessage(data) {
async function handleRegenerate(data) {
if (isStreaming) return;
isStreaming = true;
const session = getActiveSession();
if (session) {
session.history = data.history;
saveFWStore();
}
try {
await startGeneration(data);
} catch {
@@ -535,7 +583,7 @@ function startStreamingPoll() {
const thinking = extractThinkingPartial(raw);
const msg = extractMsg(raw) || extractMsgPartial(raw);
postToFrame({ type: 'STREAM_UPDATE', text: msg || '...', thinking: thinking || undefined });
const st = gen.getStatus?.(STREAM_SESSION_ID);
if (st && st.isStreaming === false) finalizeGeneration();
}, 80);
@@ -560,18 +608,18 @@ function stopStreamingPoll() {
function finalizeGeneration() {
stopStreamingPoll();
const gen = window.xiaobaixStreamingGeneration;
const rawText = gen?.getLastGeneration?.(STREAM_SESSION_ID) || '(无响应)';
const finalText = extractMsg(rawText) || '(无响应)';
const rawText = gen?.getLastGeneration?.(STREAM_SESSION_ID) || '(no response)';
const finalText = extractMsg(rawText) || '(no response)';
const thinkingText = extractThinking(rawText);
isStreaming = false;
const session = getActiveSession();
if (session) {
session.history.push({ role: 'ai', content: finalText, thinking: thinkingText || undefined, ts: Date.now() });
saveFWStore();
}
postToFrame({ type: 'STREAM_COMPLETE', finalText, thinking: thinkingText });
}
@@ -579,12 +627,12 @@ function cancelGeneration() {
const gen = window.xiaobaixStreamingGeneration;
stopStreamingPoll();
isStreaming = false;
try { gen?.cancel?.(STREAM_SESSION_ID); } catch {}
try { gen?.cancel?.(STREAM_SESSION_ID); } catch { }
postToFrame({ type: 'GENERATION_CANCELLED' });
}
// ════════════════════════════════════════════════════════════════════════════
// 实时吐槽(保持不变,省略...
// Commentary
// ════════════════════════════════════════════════════════════════════════════
function shouldTriggerCommentary() {
@@ -669,7 +717,7 @@ async function handleAIMessageForCommentary(data) {
if (!commentary) return;
const session = getActiveSession();
if (session) {
session.history.push({ role: 'ai', content: `(瞄了眼刚才的台词)${commentary}`, ts: Date.now(), type: 'commentary' });
session.history.push({ role: 'ai', content: `(glanced at the last line) ${commentary}`, ts: Date.now(), type: 'commentary' });
saveFWStore();
}
showCommentaryBubble(commentary);
@@ -678,22 +726,22 @@ async function handleAIMessageForCommentary(data) {
async function handleEditForCommentary(data) {
if ($('#xiaobaix-fourth-wall-overlay').is(':visible')) return;
if (!shouldTriggerCommentary()) return;
const ctx = getContext?.() || {};
const messageId = typeof data === 'object' ? (data.messageId ?? data.id ?? data.index) : data;
const msgObj = Number.isFinite(messageId) ? ctx?.chat?.[messageId] : null;
const messageText = getMessageTextFromEventArg(data);
if (!String(messageText).trim()) return;
await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
const editType = msgObj?.is_user ? 'edit_own' : 'edit_ai';
const commentary = await generateCommentary(messageText, editType);
if (!commentary) return;
const session = getActiveSession();
if (session) {
const prefix = editType === 'edit_ai' ? '(发现你改了我的台词)' : '(发现你偷偷改台词)';
const prefix = editType === 'edit_ai' ? '(noticed you edited my line) ' : '(caught you sneaking edits) ';
session.history.push({ role: 'ai', content: `${prefix}${commentary}`, ts: Date.now(), type: 'commentary' });
saveFWStore();
}
@@ -705,7 +753,7 @@ function getFloatBtnPosition() {
if (!btn) return null;
const rect = btn.getBoundingClientRect();
let stored = {};
try { stored = JSON.parse(localStorage.getItem(`${EXT_ID}:fourthWallFloatBtnPos`) || '{}') || {}; } catch {}
try { stored = JSON.parse(localStorage.getItem(`${EXT_ID}:fourthWallFloatBtnPos`) || '{}') || {}; } catch { }
return { top: rect.top, left: rect.left, width: rect.width, height: rect.height, side: stored.side || 'right' };
}
@@ -771,19 +819,19 @@ function cleanupCommentary() {
lastCommentaryTime = 0;
}
// ════════════════════════════════════════════════════════════════════════════
// Overlay 管理(添加可见性监听)
// ════════════════════════════════════════════
// Overlay
// ════════════════════════════════════════════════════════════════════════════
function createOverlay() {
if (overlayCreated) return;
overlayCreated = true;
const isMobile = window.innerWidth <= 768;
const frameInset = isMobile ? '0px' : '12px';
const iframeRadius = isMobile ? '0px' : '12px';
const framePadding = isMobile ? 'padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left) !important;' : '';
const $overlay = $(`
<div id="xiaobaix-fourth-wall-overlay" style="position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;height:100dvh!important;z-index:99999!important;display:none;overflow:hidden!important;background:#000!important;">
<div class="fw-backdrop" style="position:absolute!important;inset:0!important;background:rgba(0,0,0,.55)!important;backdrop-filter:blur(4px)!important;"></div>
@@ -792,13 +840,13 @@ function createOverlay() {
</div>
</div>
`);
$overlay.on('click', '.fw-backdrop', hideOverlay);
document.body.appendChild($overlay[0]);
// Guarded by isTrustedMessage (origin + source).
// eslint-disable-next-line no-restricted-syntax
window.addEventListener('message', handleFrameMessage);
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
postToFrame({ type: 'FULLSCREEN_STATE', isFullscreen: false });
@@ -821,26 +869,23 @@ function showOverlay() {
sendInitData();
postToFrame({ type: 'FULLSCREEN_STATE', isFullscreen: !!document.fullscreenElement });
// ═══════════════════════════ 新增:添加可见性监听 ═══════════════════════════
if (!visibilityHandler) {
visibilityHandler = handleVisibilityChange;
document.addEventListener('visibilitychange', visibilityHandler);
}
// ════════════════════════════════════════════════════════════════════════════
}
function hideOverlay() {
$('#xiaobaix-fourth-wall-overlay').hide();
if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
// ═══════════════════════════ 新增:移除可见性监听 ═══════════════════════════
if (document.fullscreenElement) document.exitFullscreen().catch(() => { });
stopVoiceAndNotify();
if (visibilityHandler) {
document.removeEventListener('visibilitychange', visibilityHandler);
visibilityHandler = null;
}
pendingPingId = null;
// ════════════════════════════════════════════════════════════════════════════
}
function toggleFullscreen() {
@@ -850,16 +895,16 @@ function toggleFullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen().then(() => {
postToFrame({ type: 'FULLSCREEN_STATE', isFullscreen: false });
}).catch(() => {});
}).catch(() => { });
} else if (overlay.requestFullscreen) {
overlay.requestFullscreen().then(() => {
postToFrame({ type: 'FULLSCREEN_STATE', isFullscreen: true });
}).catch(() => {});
}).catch(() => { });
}
}
// ════════════════════════════════════════════════════════════════════════════
// 悬浮按钮(保持不变,省略...
// Floating Button
// ════════════════════════════════════════════════════════════════════════════
function createFloatingButton() {
@@ -871,7 +916,7 @@ function createFloatingButton() {
const clamp = (v, min, max) => Math.min(Math.max(v, min), max);
const readPos = () => { try { return JSON.parse(localStorage.getItem(POS_KEY) || 'null'); } catch { return null; } };
const writePos = (pos) => { try { localStorage.setItem(POS_KEY, JSON.stringify(pos)); } catch {} };
const writePos = (pos) => { try { localStorage.setItem(POS_KEY, JSON.stringify(pos)); } catch { } };
const calcDockLeft = (side, w) => (side === 'left' ? -Math.round(w / 2) : (window.innerWidth - Math.round(w / 2)));
const applyDocked = (side, topRatio) => {
const btn = document.getElementById('xiaobaix-fw-float-btn');
@@ -885,20 +930,20 @@ function createFloatingButton() {
};
const $btn = $(`
<button id="xiaobaix-fw-float-btn" title="皮下交流" style="position:fixed!important;left:0px!important;top:0px!important;z-index:9999!important;width:${size}px!important;height:${size}px!important;border-radius:50%!important;border:none!important;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%)!important;color:#fff!important;font-size:${Math.round(size * 0.45)}px!important;cursor:pointer!important;box-shadow:0 4px 15px rgba(102,126,234,0.4)!important;display:flex!important;align-items:center!important;justify-content:center!important;transition:left 0.2s,top 0.2s,transform 0.2s,box-shadow 0.2s!important;touch-action:none!important;user-select:none!important;">
<button id="xiaobaix-fw-float-btn" title="Fourth Wall" style="position:fixed!important;left:0px!important;top:0px!important;z-index:9999!important;width:${size}px!important;height:${size}px!important;border-radius:50%!important;border:none!important;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%)!important;color:#fff!important;font-size:${Math.round(size * 0.45)}px!important;cursor:pointer!important;box-shadow:0 4px 15px rgba(102,126,234,0.4)!important;display:flex!important;align-items:center!important;justify-content:center!important;transition:left 0.2s,top 0.2s,transform 0.2s,box-shadow 0.2s!important;touch-action:none!important;user-select:none!important;">
<i class="fa-solid fa-comments"></i>
</button>
`);
$btn.on('click', () => {
if (Date.now() < suppressFloatBtnClickUntil) return;
if (!getSettings().fourthWall?.enabled) return;
showOverlay();
});
$btn.on('mouseenter', function() { $(this).css({ 'transform': 'scale(1.08)', 'box-shadow': '0 6px 20px rgba(102, 126, 234, 0.5)' }); });
$btn.on('mouseleave', function() { $(this).css({ 'transform': 'none', 'box-shadow': '0 4px 15px rgba(102, 126, 234, 0.4)' }); });
$btn.on('mouseenter', function () { $(this).css({ 'transform': 'scale(1.08)', 'box-shadow': '0 6px 20px rgba(102, 126, 234, 0.5)' }); });
$btn.on('mouseleave', function () { $(this).css({ 'transform': 'none', 'box-shadow': '0 4px 15px rgba(102, 126, 234, 0.4)' }); });
document.body.appendChild($btn[0]);
const initial = readPos();
@@ -911,7 +956,7 @@ function createFloatingButton() {
if (e.button !== undefined && e.button !== 0) return;
const btn = e.currentTarget;
pointerId = e.pointerId;
try { btn.setPointerCapture(pointerId); } catch {}
try { btn.setPointerCapture(pointerId); } catch { }
const rect = btn.getBoundingClientRect();
startX = e.clientX; startY = e.clientY; startLeft = rect.left; startTop = rect.top;
dragging = false;
@@ -937,7 +982,7 @@ function createFloatingButton() {
const onPointerUp = (e) => {
if (pointerId === null || e.pointerId !== pointerId) return;
const btn = e.currentTarget;
try { btn.releasePointerCapture(pointerId); } catch {}
try { btn.releasePointerCapture(pointerId); } catch { }
pointerId = null;
btn.style.transition = '';
const rect = btn.getBoundingClientRect();
@@ -976,20 +1021,20 @@ function removeFloatingButton() {
}
}
// ════════════════════════════════════════════════════════════════════════════
// 初始化和清理
// ════════════════════════════════════════════════════════════════════════════
// ════════════════════════════════════════════
// Init & Cleanup
// ════════════════════════════════════════════
function initFourthWall() {
try { xbLog.info('fourthWall', 'initFourthWall'); } catch {}
try { xbLog.info('fourthWall', 'initFourthWall'); } catch { }
const settings = getSettings();
if (!settings.fourthWall?.enabled) return;
createFloatingButton();
initCommentary();
clearExpiredCache();
clearExpiredCache();
initMessageEnhancer();
events.on(event_types.CHAT_CHANGED, () => {
cancelGeneration();
currentLoadedChatId = null;
@@ -999,24 +1044,26 @@ function initFourthWall() {
}
function fourthWallCleanup() {
try { xbLog.info('fourthWall', 'fourthWallCleanup'); } catch {}
try { xbLog.info('fourthWall', 'fourthWallCleanup'); } catch { }
events.cleanup();
cleanupCommentary();
removeFloatingButton();
hideOverlay();
cancelGeneration();
cleanupMessageEnhancer();
stopCurrentVoice();
currentVoiceRequestId = null;
frameReady = false;
pendingFrameMessages = [];
overlayCreated = false;
currentLoadedChatId = null;
pendingPingId = null;
if (visibilityHandler) {
document.removeEventListener('visibilitychange', visibilityHandler);
visibilityHandler = null;
}
$('#xiaobaix-fourth-wall-overlay').remove();
window.removeEventListener('message', handleFrameMessage);
}
@@ -1026,10 +1073,10 @@ export { initFourthWall, fourthWallCleanup, showOverlay as showFourthWallPopup }
if (typeof window !== 'undefined') {
window.fourthWallCleanup = fourthWallCleanup;
window.showFourthWallPopup = showOverlay;
document.addEventListener('xiaobaixEnabledChanged', e => {
if (e?.detail?.enabled === false) {
try { fourthWallCleanup(); } catch {}
try { fourthWallCleanup(); } catch { }
}
});
}