1.18更新

This commit is contained in:
RT15548
2026-01-18 20:04:43 +08:00
committed by GitHub
parent be142640c0
commit 03ba508a31
62 changed files with 18838 additions and 7264 deletions

View File

@@ -577,11 +577,15 @@ let defaultVoiceKey = 'female_1';
══════════════════════════════════════════════════════════════════════════════ */
function escapeHtml(text) {
return String(text || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
return escapeHtmlText(text).replace(/\n/g, '<br>');
}
function escapeHtmlText(text) {
return String(text || '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', '\'': '&#39;' }[c]));
}
function renderThinking(text) {
return String(text || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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>

View File

@@ -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(() => {});
}

View File

@@ -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);
}