四次元壁鉴权模式,claude4.6预填充等等
This commit is contained in:
@@ -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 { }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user