294 lines
10 KiB
JavaScript
294 lines
10 KiB
JavaScript
// @ts-nocheck
|
||
import { event_types, user_avatar, getCurrentChatId } from "../../../../../script.js";
|
||
import { getContext } from "../../../../st-context.js";
|
||
import { power_user } from "../../../../power-user.js";
|
||
import { createModuleEvents } from "../core/event-manager.js";
|
||
import { xbLog } from "../core/debug-core.js";
|
||
|
||
const SOURCE_TAG = 'xiaobaix-host';
|
||
|
||
/**
|
||
* Context Bridge — 模板 iframe 上下文桥接服务
|
||
*
|
||
* 功能:
|
||
* 1. iframe 发送 iframe-ready / request-context → 插件推送上下文快照
|
||
* 2. 酒馆事件实时转发到所有模板 iframe
|
||
* 3. 延迟投递队列:iframe 销毁后的事件暂存,待下一个 iframe 连接时投递
|
||
*/
|
||
class ContextBridgeService {
|
||
constructor() {
|
||
this._attached = false;
|
||
this._listener = null;
|
||
this._previousChatId = null;
|
||
/** @type {Array<{type: string, event: string, payload: object}>} */
|
||
this._pendingEvents = [];
|
||
this._events = createModuleEvents('contextBridge');
|
||
}
|
||
|
||
// ===== 生命周期 =====
|
||
|
||
init() {
|
||
if (this._attached) return;
|
||
try { xbLog.info('contextBridge', 'init'); } catch {}
|
||
|
||
try {
|
||
this._previousChatId = getCurrentChatId();
|
||
} catch {}
|
||
|
||
const self = this;
|
||
this._listener = function (event) {
|
||
try {
|
||
self._handleMessage(event);
|
||
} catch (e) {
|
||
try { xbLog.error('contextBridge', 'message handler error', e); } catch {}
|
||
}
|
||
};
|
||
|
||
// eslint-disable-next-line no-restricted-syntax -- bridge listener for iframe-ready/request-context
|
||
window.addEventListener('message', this._listener);
|
||
this._attachEventForwarding();
|
||
this._attached = true;
|
||
}
|
||
|
||
cleanup() {
|
||
if (!this._attached) return;
|
||
try { xbLog.info('contextBridge', 'cleanup'); } catch {}
|
||
try { window.removeEventListener('message', this._listener); } catch {}
|
||
this._listener = null;
|
||
this._events.cleanup();
|
||
this._pendingEvents.length = 0;
|
||
this._previousChatId = null;
|
||
this._attached = false;
|
||
}
|
||
|
||
// ===== 消息处理 =====
|
||
|
||
_handleMessage(event) {
|
||
const data = event && event.data;
|
||
if (!data || typeof data !== 'object') return;
|
||
const type = data.type;
|
||
if (type !== 'iframe-ready' && type !== 'request-context') return;
|
||
|
||
// 找到发送消息的 iframe 元素
|
||
const iframe = this._findIframeBySource(event.source);
|
||
if (!iframe) return;
|
||
|
||
const msgIndex = this._getMsgIndexForIframe(iframe);
|
||
if (msgIndex < 0) return;
|
||
|
||
// iframe-ready 时先投递积压的延迟事件
|
||
if (type === 'iframe-ready') {
|
||
while (this._pendingEvents.length > 0) {
|
||
const pending = this._pendingEvents.shift();
|
||
// eslint-disable-next-line no-restricted-syntax -- delivering queued events to newly ready iframe
|
||
try { event.source?.postMessage(pending, '*'); } catch {}
|
||
}
|
||
}
|
||
|
||
// 推送上下文快照
|
||
const snapshot = this._buildContextSnapshot(msgIndex);
|
||
// eslint-disable-next-line no-restricted-syntax -- sending context snapshot to requesting iframe
|
||
try { event.source?.postMessage(snapshot, '*'); } catch {}
|
||
}
|
||
|
||
/**
|
||
* 遍历 DOM 查找 contentWindow 匹配的 iframe
|
||
* @param {Window} source
|
||
* @returns {HTMLIFrameElement|null}
|
||
*/
|
||
_findIframeBySource(source) {
|
||
if (!source) return null;
|
||
const iframes = document.querySelectorAll('iframe.xiaobaix-iframe');
|
||
for (const iframe of iframes) {
|
||
try {
|
||
if (iframe.contentWindow === source) return iframe;
|
||
} catch {}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 从 iframe 的 DOM 位置获取消息楼层索引
|
||
* @param {HTMLIFrameElement} iframe
|
||
* @returns {number}
|
||
*/
|
||
_getMsgIndexForIframe(iframe) {
|
||
const mesBlock = iframe.closest('.mes');
|
||
if (!mesBlock) return -1;
|
||
const mesid = mesBlock.getAttribute('mesid');
|
||
if (mesid == null) return -1;
|
||
return parseInt(mesid, 10);
|
||
}
|
||
|
||
// ===== 上下文快照 =====
|
||
|
||
/**
|
||
* @param {number} msgIndex
|
||
* @returns {object}
|
||
*/
|
||
_buildContextSnapshot(msgIndex) {
|
||
const ctx = getContext();
|
||
const chat = ctx.chat || [];
|
||
const msg = chat[msgIndex];
|
||
|
||
return {
|
||
type: 'st-context',
|
||
chatId: getCurrentChatId() || null,
|
||
characterId: ctx.characterId ?? null,
|
||
characterName: ctx.name2 || '',
|
||
userName: ctx.name1 || '',
|
||
userPersona: power_user?.persona_description || '',
|
||
userAvatar: user_avatar || '',
|
||
msgIndex: msgIndex,
|
||
swipeId: msg?.swipe_id ?? 0,
|
||
totalSwipes: msg?.swipes?.length ?? 1,
|
||
totalMessages: chat.length,
|
||
isGroupChat: !!ctx.groupId,
|
||
groupId: ctx.groupId ?? null,
|
||
};
|
||
}
|
||
|
||
// ===== 事件广播 =====
|
||
|
||
/**
|
||
* 向所有活跃的模板 iframe 广播事件
|
||
* @param {string} eventName
|
||
* @param {object} payload
|
||
*/
|
||
_broadcastToTemplateIframes(eventName, payload) {
|
||
const iframes = document.querySelectorAll('.mes iframe.xiaobaix-iframe');
|
||
const message = { type: 'st-event', source: SOURCE_TAG, event: eventName, payload };
|
||
for (const iframe of iframes) {
|
||
// eslint-disable-next-line no-restricted-syntax -- broadcasting event to template iframes
|
||
try { iframe.contentWindow?.postMessage(message, '*'); } catch {}
|
||
}
|
||
}
|
||
|
||
// ===== 事件转发注册 =====
|
||
|
||
_attachEventForwarding() {
|
||
const self = this;
|
||
|
||
// ---- 消息级事件 ----
|
||
|
||
// 消息删除(截断式):原生 payload = chat.length(删除后剩余消息数)
|
||
this._events.on(event_types.MESSAGE_DELETED, (remainingCount) => {
|
||
self._broadcastToTemplateIframes('message_deleted', {
|
||
fromIndex: remainingCount,
|
||
});
|
||
});
|
||
|
||
// Swipe 切换:原生 payload = chat.length - 1(最后一条消息索引)
|
||
this._events.on(event_types.MESSAGE_SWIPED, (msgIndex) => {
|
||
const ctx = getContext();
|
||
const msg = ctx.chat?.[msgIndex];
|
||
self._broadcastToTemplateIframes('message_swiped', {
|
||
msgIndex: msgIndex,
|
||
newSwipeId: msg?.swipe_id ?? 0,
|
||
totalSwipes: msg?.swipes?.length ?? 1,
|
||
});
|
||
});
|
||
|
||
// 消息发送:原生 payload = insertAt(消息索引)
|
||
this._events.on(event_types.MESSAGE_SENT, (msgIndex) => {
|
||
self._broadcastToTemplateIframes('message_sent', { msgIndex });
|
||
});
|
||
|
||
// AI 回复完成:原生 payload = chat_id(消息索引)
|
||
this._events.on(event_types.MESSAGE_RECEIVED, (msgIndex) => {
|
||
self._broadcastToTemplateIframes('message_received', { msgIndex });
|
||
});
|
||
|
||
// 消息编辑:原生 payload = this_edit_mes_id(消息索引)
|
||
this._events.on(event_types.MESSAGE_EDITED, (msgIndex) => {
|
||
self._broadcastToTemplateIframes('message_edited', { msgIndex });
|
||
});
|
||
|
||
// ---- 聊天级事件 ----
|
||
|
||
// 聊天切换:原生 payload = getCurrentChatId()
|
||
this._events.on(event_types.CHAT_CHANGED, (newChatId) => {
|
||
self._broadcastToTemplateIframes('chat_id_changed', {
|
||
newChatId: newChatId,
|
||
previousChatId: self._previousChatId,
|
||
});
|
||
self._previousChatId = newChatId;
|
||
});
|
||
|
||
// 新聊天创建(含分支检测):原生 payload = 无
|
||
this._events.on(event_types.CHAT_CREATED, () => {
|
||
const ctx = getContext();
|
||
const newLength = (ctx.chat || []).length;
|
||
const isBranch = newLength > 1;
|
||
|
||
self._broadcastToTemplateIframes('chat_created', {
|
||
chatId: getCurrentChatId() || null,
|
||
isBranch: isBranch,
|
||
branchFromChatId: isBranch ? self._previousChatId : null,
|
||
branchPointIndex: isBranch ? newLength - 1 : null,
|
||
});
|
||
});
|
||
|
||
// ---- 延迟投递事件(入队,不广播)----
|
||
|
||
// 聊天删除:原生 payload = 聊天文件名(不含 .jsonl)
|
||
this._events.on(event_types.CHAT_DELETED, (chatFileName) => {
|
||
self._pendingEvents.push({
|
||
type: 'st-event',
|
||
source: SOURCE_TAG,
|
||
event: 'chat_deleted',
|
||
payload: { chatId: chatFileName, timestamp: Date.now() },
|
||
});
|
||
});
|
||
|
||
// 群聊删除
|
||
this._events.on(event_types.GROUP_CHAT_DELETED, (chatFileName) => {
|
||
self._pendingEvents.push({
|
||
type: 'st-event',
|
||
source: SOURCE_TAG,
|
||
event: 'group_chat_deleted',
|
||
payload: { chatId: chatFileName, timestamp: Date.now() },
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// ===== 模块级实例与导出 =====
|
||
|
||
const contextBridgeService = new ContextBridgeService();
|
||
|
||
export function initContextBridge() {
|
||
contextBridgeService.init();
|
||
}
|
||
|
||
export function cleanupContextBridge() {
|
||
contextBridgeService.cleanup();
|
||
}
|
||
|
||
// ===== 自初始化(与 call-generate-service.js 模式一致)=====
|
||
|
||
if (typeof window !== 'undefined') {
|
||
window.LittleWhiteBox = window.LittleWhiteBox || {};
|
||
window.LittleWhiteBox.contextBridge = contextBridgeService;
|
||
|
||
try { initContextBridge(); } catch (e) {}
|
||
|
||
try {
|
||
window.addEventListener('xiaobaixEnabledChanged', (e) => {
|
||
try {
|
||
const enabled = e && e.detail && e.detail.enabled === true;
|
||
if (enabled) initContextBridge(); else cleanupContextBridge();
|
||
} catch {}
|
||
});
|
||
document.addEventListener('xiaobaixEnabledChanged', (e) => {
|
||
try {
|
||
const enabled = e && e.detail && e.detail.enabled === true;
|
||
if (enabled) initContextBridge(); else cleanupContextBridge();
|
||
} catch {}
|
||
});
|
||
window.addEventListener('beforeunload', () => {
|
||
try { cleanupContextBridge(); } catch {}
|
||
});
|
||
} catch {}
|
||
}
|