1.18更新
This commit is contained in:
@@ -577,11 +577,15 @@ let defaultVoiceKey = 'female_1';
|
||||
══════════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
||||
return escapeHtmlText(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
function escapeHtmlText(text) {
|
||||
return String(text || '').replace(/[&<>\"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '\"': '"', '\'': ''' }[c]));
|
||||
}
|
||||
|
||||
function renderThinking(text) {
|
||||
return String(text || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
return escapeHtmlText(text)
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>').replace(/^[\\*\\-]\\s+/gm, '• ').replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
@@ -606,8 +610,12 @@ function generateUUID() {
|
||||
});
|
||||
}
|
||||
|
||||
const PARENT_ORIGIN = (() => {
|
||||
try { return new URL(document.referrer).origin; } catch { return window.location.origin; }
|
||||
})();
|
||||
|
||||
function postToParent(payload) {
|
||||
window.parent.postMessage({ source: 'LittleWhiteBox-FourthWall', ...payload }, '*');
|
||||
window.parent.postMessage({ source: 'LittleWhiteBox-FourthWall', ...payload }, PARENT_ORIGIN);
|
||||
}
|
||||
|
||||
function getEmotionIcon(emotion) {
|
||||
@@ -856,7 +864,7 @@ function hydrateVoiceSlots(container) {
|
||||
|
||||
function renderContent(text) {
|
||||
if (!text) return '';
|
||||
let html = String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
let html = escapeHtmlText(text);
|
||||
|
||||
html = html.replace(/\[(?:img|图片)\s*:\s*([^\]]+)\]/gi, (_, inner) => {
|
||||
const tags = parseImageToken(inner);
|
||||
@@ -915,7 +923,7 @@ function renderMessages() {
|
||||
const isEditing = editingIndex === idx;
|
||||
const timeStr = formatTimeDisplay(msg.ts);
|
||||
const bubbleContent = isEditing
|
||||
? `<textarea class="fw-edit-area" data-index="${idx}">${msg.content || ''}</textarea>`
|
||||
? `<textarea class="fw-edit-area" data-index="${idx}">${escapeHtmlText(msg.content || '')}</textarea>`
|
||||
: renderContent(msg.content);
|
||||
const actions = isEditing
|
||||
? `<div class="fw-bubble-actions" style="display:flex;"><button class="fw-bubble-btn fw-save-btn" data-index="${idx}" title="保存"><i class="fa-solid fa-check"></i></button><button class="fw-bubble-btn fw-cancel-btn" data-index="${idx}" title="取消"><i class="fa-solid fa-xmark"></i></button></div>`
|
||||
@@ -1116,7 +1124,9 @@ function regenerate() {
|
||||
消息处理
|
||||
══════════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
// Guarded by origin/source check.
|
||||
window.addEventListener('message', event => {
|
||||
if (event.origin !== PARENT_ORIGIN || event.source !== window.parent) return;
|
||||
const data = event.data;
|
||||
if (!data || data.source !== 'LittleWhiteBox') return;
|
||||
|
||||
@@ -1125,7 +1135,7 @@ window.addEventListener('message', event => {
|
||||
source: 'LittleWhiteBox-FourthWall',
|
||||
type: 'PONG',
|
||||
pingId: data.pingId
|
||||
}, '*');
|
||||
}, PARENT_ORIGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1313,4 +1323,4 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// 次元壁模块 - 主控制器
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
import { extension_settings, getContext, saveMetadataDebounced } from "../../../../../extensions.js";
|
||||
import { saveSettingsDebounced, chat_metadata } from "../../../../../../script.js";
|
||||
import { executeSlashCommand } from "../../core/slash-command.js";
|
||||
import { saveSettingsDebounced, chat_metadata, default_user_avatar, default_avatar } from "../../../../../../script.js";
|
||||
import { EXT_ID, extensionFolderPath } from "../../core/constants.js";
|
||||
import { createModuleEvents, event_types } from "../../core/event-manager.js";
|
||||
import { xbLog } from "../../core/debug-core.js";
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
DEFAULT_META_PROTOCOL
|
||||
} from "./fw-prompt.js";
|
||||
import { initMessageEnhancer, cleanupMessageEnhancer } from "./fw-message-enhancer.js";
|
||||
import { postToIframe, isTrustedMessage, getTrustedOrigin } from "../../core/iframe-messaging.js";
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 常量
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -41,7 +41,6 @@ let streamTimerId = null;
|
||||
let floatBtnResizeHandler = null;
|
||||
let suppressFloatBtnClickUntil = 0;
|
||||
let currentLoadedChatId = null;
|
||||
let isFullscreen = false;
|
||||
let lastCommentaryTime = 0;
|
||||
let commentaryBubbleEl = null;
|
||||
let commentaryBubbleTimer = null;
|
||||
@@ -157,7 +156,7 @@ function getAvatarUrls() {
|
||||
const ch = Array.isArray(ctx.characters) ? ctx.characters[chId] : null;
|
||||
let char = ch?.avatar || (typeof default_avatar !== 'undefined' ? default_avatar : '');
|
||||
if (char && !/^(data:|blob:|https?:)/i.test(char)) {
|
||||
char = /[\/]/.test(char) ? char.replace(/^\/+/, '') : `characters/${char}`;
|
||||
char = String(char).includes('/') ? char.replace(/^\/+/, '') : `characters/${char}`;
|
||||
}
|
||||
return { user: toAbsUrl(user), char: toAbsUrl(char) };
|
||||
}
|
||||
@@ -209,14 +208,14 @@ function postToFrame(payload) {
|
||||
pendingFrameMessages.push(payload);
|
||||
return;
|
||||
}
|
||||
iframe.contentWindow.postMessage({ source: 'LittleWhiteBox', ...payload }, '*');
|
||||
postToIframe(iframe, payload, 'LittleWhiteBox');
|
||||
}
|
||||
|
||||
function flushPendingMessages() {
|
||||
if (!frameReady) return;
|
||||
const iframe = document.getElementById('xiaobaix-fourth-wall-iframe');
|
||||
if (!iframe?.contentWindow) return;
|
||||
pendingFrameMessages.forEach(p => iframe.contentWindow.postMessage({ source: 'LittleWhiteBox', ...p }, '*'));
|
||||
pendingFrameMessages.forEach(p => postToIframe(iframe, p, 'LittleWhiteBox'));
|
||||
pendingFrameMessages = [];
|
||||
}
|
||||
|
||||
@@ -268,7 +267,7 @@ function checkIframeHealth() {
|
||||
recoverIframe('contentWindow 不存在');
|
||||
return;
|
||||
}
|
||||
win.postMessage({ source: 'LittleWhiteBox', type: 'PING', pingId }, '*');
|
||||
win.postMessage({ source: 'LittleWhiteBox', type: 'PING', pingId }, getTrustedOrigin());
|
||||
} catch (e) {
|
||||
recoverIframe('无法访问 iframe: ' + e.message);
|
||||
return;
|
||||
@@ -314,8 +313,9 @@ function recoverIframe(reason) {
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function handleFrameMessage(event) {
|
||||
const iframe = document.getElementById('xiaobaix-fourth-wall-iframe');
|
||||
if (!isTrustedMessage(event, iframe, 'LittleWhiteBox-FourthWall')) return;
|
||||
const data = event.data;
|
||||
if (!data || data.source !== 'LittleWhiteBox-FourthWall') return;
|
||||
|
||||
const store = getFWStore();
|
||||
const settings = getSettings();
|
||||
@@ -463,11 +463,22 @@ async function startGeneration(data) {
|
||||
promptTemplates: getSettings().fourthWallPromptTemplates
|
||||
});
|
||||
|
||||
const top64 = b64UrlEncode(`user={${msg1}};assistant={${msg2}};user={${msg3}};assistant={${msg4}}`);
|
||||
const nonstreamArg = data.settings.stream ? '' : ' nonstream=true';
|
||||
const cmd = `/xbgenraw id=${STREAM_SESSION_ID} top64="${top64}"${nonstreamArg} ""`;
|
||||
|
||||
await executeSlashCommand(cmd);
|
||||
const gen = window.xiaobaixStreamingGeneration;
|
||||
if (!gen?.xbgenrawCommand) throw new Error('xbgenraw 模块不可用');
|
||||
|
||||
const topMessages = [
|
||||
{ role: 'user', content: msg1 },
|
||||
{ role: 'assistant', content: msg2 },
|
||||
{ role: 'user', content: msg3 },
|
||||
];
|
||||
|
||||
await gen.xbgenrawCommand({
|
||||
id: STREAM_SESSION_ID,
|
||||
top64: b64UrlEncode(JSON.stringify(topMessages)),
|
||||
bottomassistant: msg4,
|
||||
nonstream: data.settings.stream ? 'false' : 'true',
|
||||
as: 'user',
|
||||
}, '');
|
||||
|
||||
if (data.settings.stream) {
|
||||
startStreamingPoll();
|
||||
@@ -620,11 +631,24 @@ async function generateCommentary(targetText, type) {
|
||||
|
||||
if (!built) return null;
|
||||
const { msg1, msg2, msg3, msg4 } = built;
|
||||
const top64 = b64UrlEncode(`user={${msg1}};assistant={${msg2}};user={${msg3}};assistant={${msg4}}`);
|
||||
|
||||
const gen = window.xiaobaixStreamingGeneration;
|
||||
if (!gen?.xbgenrawCommand) return null;
|
||||
|
||||
const topMessages = [
|
||||
{ role: 'user', content: msg1 },
|
||||
{ role: 'assistant', content: msg2 },
|
||||
{ role: 'user', content: msg3 },
|
||||
];
|
||||
|
||||
try {
|
||||
const cmd = `/xbgenraw id=xb8 nonstream=true top64="${top64}" ""`;
|
||||
const result = await executeSlashCommand(cmd);
|
||||
const result = await gen.xbgenrawCommand({
|
||||
id: 'xb8',
|
||||
top64: b64UrlEncode(JSON.stringify(topMessages)),
|
||||
bottomassistant: msg4,
|
||||
nonstream: 'true',
|
||||
as: 'user',
|
||||
}, '');
|
||||
return extractMsg(result) || null;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -771,14 +795,14 @@ function createOverlay() {
|
||||
|
||||
$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) {
|
||||
isFullscreen = false;
|
||||
postToFrame({ type: 'FULLSCREEN_STATE', isFullscreen: false });
|
||||
} else {
|
||||
isFullscreen = true;
|
||||
postToFrame({ type: 'FULLSCREEN_STATE', isFullscreen: true });
|
||||
}
|
||||
});
|
||||
@@ -809,7 +833,6 @@ function showOverlay() {
|
||||
function hideOverlay() {
|
||||
$('#xiaobaix-fourth-wall-overlay').hide();
|
||||
if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
|
||||
isFullscreen = false;
|
||||
|
||||
// ═══════════════════════════ 新增:移除可见性监听 ═══════════════════════════
|
||||
if (visibilityHandler) {
|
||||
@@ -826,12 +849,10 @@ function toggleFullscreen() {
|
||||
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().then(() => {
|
||||
isFullscreen = false;
|
||||
postToFrame({ type: 'FULLSCREEN_STATE', isFullscreen: false });
|
||||
}).catch(() => {});
|
||||
} else if (overlay.requestFullscreen) {
|
||||
overlay.requestFullscreen().then(() => {
|
||||
isFullscreen = true;
|
||||
postToFrame({ type: 'FULLSCREEN_STATE', isFullscreen: true });
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -258,6 +258,8 @@ function injectStyles() {
|
||||
function enhanceMessageContent(container) {
|
||||
if (!container) return;
|
||||
|
||||
// Rewrites already-rendered message HTML; no new HTML source is introduced here.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
const html = container.innerHTML;
|
||||
let enhanced = html;
|
||||
let hasChanges = false;
|
||||
@@ -283,7 +285,11 @@ function enhanceMessageContent(container) {
|
||||
return createVoiceBubbleHTML(txt, '');
|
||||
});
|
||||
|
||||
if (hasChanges) container.innerHTML = enhanced;
|
||||
if (hasChanges) {
|
||||
// Replaces existing message HTML with enhanced tokens only.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
container.innerHTML = enhanced;
|
||||
}
|
||||
|
||||
hydrateImageSlots(container);
|
||||
hydrateVoiceSlots(container);
|
||||
@@ -317,6 +323,8 @@ function hydrateImageSlots(container) {
|
||||
slot.dataset.observed = '1';
|
||||
|
||||
if (!slot.dataset.loaded && !slot.dataset.loading && !slot.querySelector('img')) {
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
slot.innerHTML = `<div class="xb-img-placeholder"><i class="fa-regular fa-image"></i><span>滚动加载</span></div>`;
|
||||
}
|
||||
|
||||
@@ -325,18 +333,26 @@ function hydrateImageSlots(container) {
|
||||
}
|
||||
|
||||
async function loadImage(slot, tags) {
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
slot.innerHTML = `<div class="xb-img-loading"><i class="fa-solid fa-spinner"></i> 检查缓存...</div>`;
|
||||
|
||||
try {
|
||||
const base64 = await generateImage(tags, (status, position, delay) => {
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
slot.innerHTML = `<div class="xb-img-loading"><i class="fa-solid fa-clock"></i> 排队中 #${position}</div>`;
|
||||
break;
|
||||
case 'generating':
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
slot.innerHTML = `<div class="xb-img-loading"><i class="fa-solid fa-palette"></i> 生成中${position > 0 ? ` (${position} 排队)` : ''}...</div>`;
|
||||
break;
|
||||
case 'waiting':
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
slot.innerHTML = `<div class="xb-img-loading"><i class="fa-solid fa-clock"></i> 排队中 #${position} (${delay}s)</div>`;
|
||||
break;
|
||||
}
|
||||
@@ -349,12 +365,16 @@ async function loadImage(slot, tags) {
|
||||
slot.dataset.loading = '';
|
||||
|
||||
if (err.message === '队列已清空') {
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
slot.innerHTML = `<div class="xb-img-placeholder"><i class="fa-regular fa-image"></i><span>滚动加载</span></div>`;
|
||||
slot.dataset.loading = '';
|
||||
slot.dataset.observed = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Template-only UI markup with escaped error text.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
slot.innerHTML = `<div class="xb-img-error"><i class="fa-solid fa-exclamation-triangle"></i><div>${escapeHtml(err?.message || '失败')}</div><button class="xb-img-retry" data-tags="${encodeURIComponent(tags)}">重试</button></div>`;
|
||||
bindRetryButton(slot);
|
||||
}
|
||||
@@ -369,12 +389,16 @@ function renderImage(slot, base64, fromCache) {
|
||||
img.className = 'xb-generated-img';
|
||||
img.onclick = () => window.open(img.src, '_blank');
|
||||
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
slot.innerHTML = '';
|
||||
slot.appendChild(img);
|
||||
|
||||
if (fromCache) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'xb-img-badge';
|
||||
// Template-only UI markup.
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
badge.innerHTML = '<i class="fa-solid fa-bolt"></i>';
|
||||
slot.appendChild(badge);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user