回退
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,24 @@
|
||||
* ============================================================================
|
||||
* Story Outline 模块 - 小白板
|
||||
* ============================================================================
|
||||
* 功能:生成和管理RPG式剧情世界,提供地图导航、NPC管理、短信系统、世界推演
|
||||
*
|
||||
* 分区:
|
||||
* 1. 导入与常量
|
||||
* 2. 通用工具
|
||||
* 3. JSON解析
|
||||
* 4. 存储管理
|
||||
* 5. LLM调用
|
||||
* 6. 世界书操作
|
||||
* 7. 剧情注入
|
||||
* 8. iframe通讯
|
||||
* 9. 请求处理器
|
||||
* 10. UI管理
|
||||
* 11. 事件与初始化
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
// ==================== 1. 导入与常量 ====================
|
||||
|
||||
import { extension_settings, saveMetadataDebounced } from "../../../../../extensions.js";
|
||||
import { chat_metadata, name1, processCommands, eventSource, event_types as st_event_types } from "../../../../../../script.js";
|
||||
import { loadWorldInfo, saveWorldInfo, world_names, world_info } from "../../../../../world-info.js";
|
||||
@@ -25,59 +39,53 @@ import {
|
||||
const events = createModuleEvents('storyOutline');
|
||||
const IFRAME_PATH = `${extensionFolderPath}/modules/story-outline/story-outline.html`;
|
||||
const STORAGE_KEYS = { global: 'LittleWhiteBox_StoryOutline_GlobalSettings', comm: 'LittleWhiteBox_StoryOutline_CommSettings' };
|
||||
const SIZE_STORAGE_KEY = 'LittleWhiteBox_StoryOutline_Size';
|
||||
const STORY_OUTLINE_ID = 'lwb_story_outline';
|
||||
const CHAR_CARD_UID = '__CHARACTER_CARD__';
|
||||
const DEBUG_KEY = 'LittleWhiteBox_StoryOutline_Debug';
|
||||
|
||||
let overlayCreated = false, frameReady = false, currentMesId = null, pendingMsgs = [], presetCleanup = null, step1Cache = null;
|
||||
let iframeLoaded = false;
|
||||
|
||||
// ==================== 2. 通用工具 ====================
|
||||
|
||||
/** 移动端检测 */
|
||||
const isMobile = () => window.innerWidth < 550;
|
||||
|
||||
/** 安全执行函数 */
|
||||
const safe = fn => { try { return fn(); } catch { return null; } };
|
||||
const isDebug = () => { try { return localStorage.getItem(DEBUG_KEY) === '1'; } catch { return false; } };
|
||||
const isDebug = () => {
|
||||
try { return localStorage.getItem(DEBUG_KEY) === '1'; } catch { return false; }
|
||||
};
|
||||
|
||||
/** localStorage读写 */
|
||||
const getStore = (k, def) => safe(() => JSON.parse(localStorage.getItem(k))) || def;
|
||||
const setStore = (k, v) => safe(() => localStorage.setItem(k, JSON.stringify(v)));
|
||||
|
||||
/** 随机范围 */
|
||||
const randRange = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a;
|
||||
|
||||
const getStoredSize = (isMob) => {
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem(SIZE_STORAGE_KEY) || '{}');
|
||||
return isMob ? data.mobile : data.desktop;
|
||||
} catch { return null; }
|
||||
};
|
||||
|
||||
const setStoredSize = (isMob, size) => {
|
||||
try {
|
||||
if (!size) return;
|
||||
const data = JSON.parse(localStorage.getItem(SIZE_STORAGE_KEY) || '{}');
|
||||
if (isMob) {
|
||||
if (Number.isFinite(size.height) && size.height > 44) {
|
||||
data.mobile = { height: size.height };
|
||||
}
|
||||
} else {
|
||||
data.desktop = {};
|
||||
if (Number.isFinite(size.width) && size.width > 300) data.desktop.width = size.width;
|
||||
if (Number.isFinite(size.height) && size.height > 200) data.desktop.height = size.height;
|
||||
}
|
||||
localStorage.setItem(SIZE_STORAGE_KEY, JSON.stringify(data));
|
||||
} catch {}
|
||||
};
|
||||
|
||||
// ==================== 3. JSON解析 ====================
|
||||
|
||||
/**
|
||||
* 修复单个 JSON 字符串的语法问题
|
||||
* 仅在已提取的候选上调用,不做全局破坏性操作
|
||||
*/
|
||||
function fixJson(s) {
|
||||
if (!s || typeof s !== 'string') return s;
|
||||
|
||||
let r = s.trim()
|
||||
// 统一引号:只转换弯引号
|
||||
.replace(/[""]/g, '"').replace(/['']/g, "'")
|
||||
// 修复键名后的错误引号:如 "key': → "key":
|
||||
.replace(/"([^"']+)'[\s]*:/g, '"$1":')
|
||||
.replace(/'([^"']+)"[\s]*:/g, '"$1":')
|
||||
// 修复单引号包裹的完整值:: 'value' → : "value"
|
||||
.replace(/:[\s]*'([^']*)'[\s]*([,}\]])/g, ':"$1"$2')
|
||||
// 修复无引号的键名
|
||||
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
|
||||
// 移除尾随逗号
|
||||
.replace(/,[\s\n]*([}\]])/g, '$1')
|
||||
// 修复 undefined 和 NaN
|
||||
.replace(/:\s*undefined\b/g, ': null').replace(/:\s*NaN\b/g, ': null');
|
||||
|
||||
// 补全未闭合的括号
|
||||
let braces = 0, brackets = 0, inStr = false, esc = false;
|
||||
for (const c of r) {
|
||||
if (esc) { esc = false; continue; }
|
||||
@@ -93,8 +101,17 @@ function fixJson(s) {
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从输入中提取 JSON(非破坏性扫描版)
|
||||
* 策略:
|
||||
* 1. 直接在原始字符串中扫描所有 {...} 结构
|
||||
* 2. 对每个候选单独清洗和解析
|
||||
* 3. 按有效属性评分,返回最佳结果
|
||||
*/
|
||||
function extractJson(input, isArray = false) {
|
||||
if (!input) return null;
|
||||
|
||||
// 处理已经是对象的输入
|
||||
if (typeof input === 'object' && input !== null) {
|
||||
if (isArray && Array.isArray(input)) return input;
|
||||
if (!isArray && !Array.isArray(input)) {
|
||||
@@ -106,21 +123,33 @@ function extractJson(input, isArray = false) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 预处理:只做最基本的清理
|
||||
const str = String(input).trim()
|
||||
.replace(/^\uFEFF/, '')
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
||||
.replace(/\r\n?/g, '\n');
|
||||
if (!str) return null;
|
||||
|
||||
const tryParse = s => { try { return JSON.parse(s); } catch { return null; } };
|
||||
const ok = (o, arr) => o != null && (arr ? Array.isArray(o) : typeof o === 'object' && !Array.isArray(o));
|
||||
|
||||
// 评分函数:meta=10, world/maps=5, 其他=3
|
||||
const score = o => (o?.meta ? 10 : 0) + (o?.world ? 5 : 0) + (o?.maps ? 5 : 0) +
|
||||
(o?.truth ? 3 : 0) + (o?.onion_layers ? 3 : 0) + (o?.atmosphere ? 3 : 0) + (o?.trajectory ? 3 : 0) + (o?.user_guide ? 3 : 0);
|
||||
|
||||
// 1. 直接尝试解析(最理想情况)
|
||||
let r = tryParse(str);
|
||||
if (ok(r, isArray) && score(r) > 0) return r;
|
||||
|
||||
// 2. 扫描所有 {...} 或 [...] 结构
|
||||
const open = isArray ? '[' : '{';
|
||||
const candidates = [];
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str[i] !== open) continue;
|
||||
|
||||
// 括号匹配找闭合位置
|
||||
let depth = 0, inStr = false, esc = false;
|
||||
for (let j = i; j < str.length; j++) {
|
||||
const c = str[j];
|
||||
@@ -132,21 +161,29 @@ function extractJson(input, isArray = false) {
|
||||
else if (c === '}' || c === ']') depth--;
|
||||
if (depth === 0) {
|
||||
candidates.push({ start: i, end: j, text: str.slice(i, j + 1) });
|
||||
i = j;
|
||||
i = j; // 跳过已处理的部分
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 按长度排序(大的优先,更可能是完整对象)
|
||||
candidates.sort((a, b) => b.text.length - a.text.length);
|
||||
|
||||
// 4. 尝试解析每个候选,记录最佳结果
|
||||
let best = null, bestScore = -1;
|
||||
|
||||
for (const { text } of candidates) {
|
||||
// 直接解析
|
||||
r = tryParse(text);
|
||||
if (ok(r, isArray)) {
|
||||
const s = score(r);
|
||||
if (s > bestScore) { best = r; bestScore = s; }
|
||||
if (s >= 10) return r;
|
||||
if (s >= 10) return r; // 有 meta 就直接返回
|
||||
continue;
|
||||
}
|
||||
|
||||
// 修复后解析
|
||||
const fixed = fixJson(text);
|
||||
r = tryParse(fixed);
|
||||
if (ok(r, isArray)) {
|
||||
@@ -155,7 +192,11 @@ function extractJson(input, isArray = false) {
|
||||
if (s >= 10) return r;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 返回最佳结果
|
||||
if (best) return best;
|
||||
|
||||
// 6. 最后尝试:取第一个 { 到最后一个 } 之间的内容
|
||||
const firstBrace = str.indexOf('{');
|
||||
const lastBrace = str.lastIndexOf('}');
|
||||
if (!isArray && firstBrace !== -1 && lastBrace > firstBrace) {
|
||||
@@ -163,6 +204,7 @@ function extractJson(input, isArray = false) {
|
||||
r = tryParse(chunk) || tryParse(fixJson(chunk));
|
||||
if (ok(r, isArray)) return r;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -170,8 +212,10 @@ export { extractJson };
|
||||
|
||||
// ==================== 4. 存储管理 ====================
|
||||
|
||||
/** 获取扩展设置 */
|
||||
const getSettings = () => { const e = extension_settings[EXT_ID] ||= {}; e.storyOutline ||= { enabled: true }; return e; };
|
||||
|
||||
/** 获取剧情大纲存储 */
|
||||
function getOutlineStore() {
|
||||
if (!chat_metadata) return null;
|
||||
const ext = chat_metadata.extensions ||= {}, lwb = ext[EXT_ID] ||= {};
|
||||
@@ -182,11 +226,13 @@ function getOutlineStore() {
|
||||
};
|
||||
}
|
||||
|
||||
/** 全局/通讯设置读写 */
|
||||
const getGlobalSettings = () => getStore(STORAGE_KEYS.global, { apiUrl: '', apiKey: '', model: '', mode: 'assist' });
|
||||
const saveGlobalSettings = s => setStore(STORAGE_KEYS.global, s);
|
||||
const getCommSettings = () => ({ historyCount: 50, npcPosition: 0, npcOrder: 100, ...getStore(STORAGE_KEYS.comm, {}) });
|
||||
const saveCommSettings = s => setStore(STORAGE_KEYS.comm, s);
|
||||
|
||||
/** 获取角色卡信息 */
|
||||
function getCharInfo() {
|
||||
const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
|
||||
return {
|
||||
@@ -195,6 +241,7 @@ function getCharInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取角色卡短信历史 */
|
||||
function getCharSmsHistory() {
|
||||
if (!chat_metadata) return null;
|
||||
const root = chat_metadata.LittleWhiteBox_StoryOutline ||= {};
|
||||
@@ -205,8 +252,11 @@ function getCharSmsHistory() {
|
||||
|
||||
// ==================== 5. LLM调用 ====================
|
||||
|
||||
|
||||
/** 调用LLM */
|
||||
async function callLLM(promptOrMsgs, useRaw = false) {
|
||||
const { apiUrl, apiKey, model } = getGlobalSettings();
|
||||
|
||||
const normalize = r => {
|
||||
if (r == null) return '';
|
||||
if (typeof r === 'string') return r;
|
||||
@@ -220,12 +270,18 @@ async function callLLM(promptOrMsgs, useRaw = false) {
|
||||
}
|
||||
return String(r);
|
||||
};
|
||||
|
||||
// 构建基础选项
|
||||
const opts = { nonstream: 'true', lock: 'on' };
|
||||
if (apiUrl?.trim()) Object.assign(opts, { api: 'openai', apiurl: apiUrl.trim(), ...(apiKey && { apipassword: apiKey }), ...(model && { model }) });
|
||||
|
||||
if (useRaw) {
|
||||
const messages = Array.isArray(promptOrMsgs)
|
||||
? promptOrMsgs
|
||||
: [{ role: 'user', content: String(promptOrMsgs || '').trim() }];
|
||||
|
||||
// 直接把消息转成 top 参数格式,不做预处理
|
||||
// {$worldInfo} 和 {$historyN} 由 xbgenrawCommand 内部处理
|
||||
const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
|
||||
const topParts = messages
|
||||
.filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
|
||||
@@ -234,9 +290,13 @@ async function callLLM(promptOrMsgs, useRaw = false) {
|
||||
return `${role}={${m.content}}`;
|
||||
});
|
||||
const topParam = topParts.join(';');
|
||||
|
||||
opts.top = topParam;
|
||||
// 不设置 addon,让 xbgenrawCommand 自己处理 {$worldInfo} 占位符替换
|
||||
|
||||
const raw = await streamingGeneration.xbgenrawCommand(opts, '');
|
||||
const text = normalize(raw).trim();
|
||||
|
||||
if (isDebug()) {
|
||||
try {
|
||||
console.groupCollapsed('[StoryOutline] callLLM(useRaw via xbgenrawCommand)');
|
||||
@@ -248,11 +308,13 @@ async function callLLM(promptOrMsgs, useRaw = false) {
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
opts.as = 'user';
|
||||
opts.position = 'history';
|
||||
return normalize(await streamingGeneration.xbgenCommand(opts, promptOrMsgs)).trim();
|
||||
}
|
||||
|
||||
/** 调用LLM并解析JSON */
|
||||
async function callLLMJson({ messages, useRaw = true, isArray = false, validate }) {
|
||||
try {
|
||||
const result = await callLLM(messages, useRaw);
|
||||
@@ -282,6 +344,7 @@ async function callLLMJson({ messages, useRaw = true, isArray = false, validate
|
||||
|
||||
// ==================== 6. 世界书操作 ====================
|
||||
|
||||
/** 获取角色卡绑定的世界书 */
|
||||
async function getCharWorldbooks() {
|
||||
const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
|
||||
if (!char) return [];
|
||||
@@ -293,6 +356,7 @@ async function getCharWorldbooks() {
|
||||
return books;
|
||||
}
|
||||
|
||||
/** 根据UID查找条目 */
|
||||
async function findEntry(uid) {
|
||||
const uidNum = parseInt(uid, 10);
|
||||
if (isNaN(uidNum)) return null;
|
||||
@@ -303,6 +367,7 @@ async function findEntry(uid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 根据名称搜索条目 */
|
||||
async function searchEntry(name) {
|
||||
const nl = (name || '').toLowerCase().trim();
|
||||
for (const book of await getCharWorldbooks()) {
|
||||
@@ -319,24 +384,32 @@ async function searchEntry(name) {
|
||||
|
||||
// ==================== 7. 剧情注入 ====================
|
||||
|
||||
/** 获取可见洋葱层级 */
|
||||
const getVisibleLayers = stage => ['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].slice(0, Math.min(Math.max(0, stage), 3) + 2);
|
||||
|
||||
/** 格式化剧情数据为提示词 */
|
||||
function formatOutlinePrompt() {
|
||||
const store = getOutlineStore();
|
||||
if (!store?.outlineData) return "";
|
||||
|
||||
const { outlineData: d, dataChecked: c, playerLocation } = store, stage = store.stage ?? 0;
|
||||
let text = "## Story Outline (剧情数据)\n\n", has = false;
|
||||
|
||||
// 世界真相
|
||||
if (c?.meta && d.meta?.truth) {
|
||||
has = true;
|
||||
text += "### 世界真相 (World Truth)\n> 注意:以下信息仅供生成逻辑参考,不可告知玩家。\n";
|
||||
if (d.meta.truth.background) text += `* 背景真相: ${d.meta.truth.background}\n`;
|
||||
const dr = d.meta.truth.driver;
|
||||
if (dr) { if (dr.source) text += `* 驱动: ${dr.source}\n`; if (dr.target_end) text += `* 目的: ${dr.target_end}\n`; if (dr.tactic) text += `* 当前手段: ${dr.tactic}\n`; }
|
||||
|
||||
// 当前气氛
|
||||
const atm = d.meta.atmosphere?.current;
|
||||
if (atm) {
|
||||
if (atm.environmental) text += `* 当前气氛: ${atm.environmental}\n`;
|
||||
if (atm.npc_attitudes) text += `* NPC态度: ${atm.npc_attitudes}\n`;
|
||||
}
|
||||
|
||||
const onion = d.meta.onion_layers || d.meta.truth.onion_layers;
|
||||
if (onion) {
|
||||
text += "* 当前可见层级:\n";
|
||||
@@ -348,7 +421,11 @@ function formatOutlinePrompt() {
|
||||
}
|
||||
text += "\n";
|
||||
}
|
||||
|
||||
// 世界资讯
|
||||
if (c?.world && d.world?.news?.length) { has = true; text += "### 世界资讯 (News)\n"; d.world.news.forEach(n => { text += `* ${n.title}: ${n.content}\n`; }); text += "\n"; }
|
||||
|
||||
// 环境信息
|
||||
let mapC = "", locNode = null;
|
||||
if (c?.outdoor && d.outdoor) {
|
||||
if (d.outdoor.description) mapC += `> 大地图环境: ${d.outdoor.description}\n`;
|
||||
@@ -360,14 +437,20 @@ function formatOutlinePrompt() {
|
||||
if (playerLocation && locText) mapC += `\n> 当前地点 (${playerLocation}):\n${locText}\n`;
|
||||
if (c?.indoor && d.indoor && !locNode && !indoorMap && d.indoor.description) { mapC += d.indoor.name ? `\n> 当前地点: ${d.indoor.name}\n` : "\n> 局部区域:\n"; mapC += `${d.indoor.description}\n`; }
|
||||
if (mapC) { has = true; text += `### 环境信息 (Environment)\n${mapC}\n`; }
|
||||
|
||||
// 周边人物
|
||||
let charC = "";
|
||||
if (c?.contacts && d.contacts?.length) { charC += "* 联络人:\n"; d.contacts.forEach(p => charC += ` - ${p.name}${p.location ? ` @ ${p.location}` : ''}: ${p.info || ''}\n`); }
|
||||
if (c?.strangers && d.strangers?.length) { charC += "* 陌路人:\n"; d.strangers.forEach(p => charC += ` - ${p.name}${p.location ? ` @ ${p.location}` : ''}: ${p.info || ''}\n`); }
|
||||
if (charC) { has = true; text += `### 周边人物 (Characters)\n${charC}\n`; }
|
||||
|
||||
// 当前剧情
|
||||
if (c?.sceneSetup && d.sceneSetup) {
|
||||
const ss = d.sceneSetup.sideStory || d.sceneSetup.side_story || d.sceneSetup;
|
||||
if (ss && (ss.surface || ss.inner)) { has = true; text += "### 当前剧情 (Current Scene)\n"; if (ss.surface) text += `* 表象: ${ss.surface}\n`; if (ss.inner) text += `* 里层 (潜台词): ${ss.inner}\n`; text += "\n"; }
|
||||
}
|
||||
|
||||
// 角色卡短信
|
||||
if (c?.characterContactSms) {
|
||||
const { name: charName } = getCharInfo(), hist = getCharSmsHistory();
|
||||
const sums = hist?.summaries || {}, sumKeys = Object.keys(sums).filter(k => k !== '_count').sort((a, b) => a - b);
|
||||
@@ -379,9 +462,11 @@ function formatOutlinePrompt() {
|
||||
text += "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return has ? text.trim() : "";
|
||||
}
|
||||
|
||||
/** 确保剧情大纲Prompt存在 */
|
||||
function ensurePrompt() {
|
||||
if (!promptManager) return false;
|
||||
let prompt = promptManager.getPromptById(STORY_OUTLINE_ID);
|
||||
@@ -399,6 +484,7 @@ function ensurePrompt() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 更新剧情大纲Prompt内容 */
|
||||
function updatePromptContent() {
|
||||
if (!promptManager) return;
|
||||
if (!getSettings().storyOutline?.enabled) { removePrompt(); return; }
|
||||
@@ -411,6 +497,7 @@ function updatePromptContent() {
|
||||
promptManager.render?.(false);
|
||||
}
|
||||
|
||||
/** 移除剧情大纲Prompt */
|
||||
function removePrompt() {
|
||||
if (!promptManager) return;
|
||||
const prompts = promptManager.serviceSettings?.prompts;
|
||||
@@ -420,6 +507,7 @@ function removePrompt() {
|
||||
promptManager.render?.(false);
|
||||
}
|
||||
|
||||
/** 设置ST预设事件监听 */
|
||||
function setupSTEvents() {
|
||||
if (presetCleanup) return;
|
||||
const onChanged = () => { if (getSettings().storyOutline?.enabled) setTimeout(() => { ensurePrompt(); updatePromptContent(); }, 100); };
|
||||
@@ -437,6 +525,7 @@ const injectOutline = () => updatePromptContent();
|
||||
|
||||
// ==================== 8. iframe通讯 ====================
|
||||
|
||||
/** 发送消息到iframe */
|
||||
function postFrame(payload) {
|
||||
const iframe = document.getElementById("xiaobaix-story-outline-iframe");
|
||||
if (!iframe?.contentWindow || !frameReady) { pendingMsgs.push(payload); return; }
|
||||
@@ -445,6 +534,7 @@ function postFrame(payload) {
|
||||
|
||||
const flushPending = () => { if (!frameReady) return; const f = document.getElementById("xiaobaix-story-outline-iframe"); pendingMsgs.forEach(p => f?.contentWindow?.postMessage({ source: "LittleWhiteBox", ...p }, "*")); pendingMsgs = []; };
|
||||
|
||||
/** 发送设置到iframe */
|
||||
function sendSettings() {
|
||||
const store = getOutlineStore(), { name: charName, desc: charDesc } = getCharInfo();
|
||||
postFrame({
|
||||
@@ -464,10 +554,12 @@ const loadAndSend = () => { const s = getOutlineStore(); if (s?.mapData) postFra
|
||||
const reply = (type, reqId, data) => postFrame({ type, requestId: reqId, ...data });
|
||||
const replyErr = (type, reqId, err) => reply(type, reqId, { error: err });
|
||||
|
||||
/** 获取当前气氛 */
|
||||
function getAtmosphere(store) {
|
||||
return store?.outlineData?.meta?.atmosphere?.current || null;
|
||||
}
|
||||
|
||||
/** 合并世界推演数据 */
|
||||
function mergeSimData(orig, upd) {
|
||||
if (!upd) return orig;
|
||||
const r = JSON.parse(JSON.stringify(orig || {}));
|
||||
@@ -477,13 +569,16 @@ function mergeSimData(orig, upd) {
|
||||
if (ut?.driver?.tactic) r.meta.truth.driver = { ...r.meta.truth.driver, tactic: ut.driver.tactic };
|
||||
if (uo) { ['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].forEach(l => { const v = uo[l]; if (Array.isArray(v) && v.length) { r.meta.onion_layers = r.meta.onion_layers || {}; r.meta.onion_layers[l] = v; } }); if (r.meta.truth?.onion_layers) delete r.meta.truth.onion_layers; }
|
||||
if (um.user_guide || upd?.user_guide) r.meta.user_guide = um.user_guide || upd.user_guide;
|
||||
// 更新 atmosphere
|
||||
if (ua) { r.meta.atmosphere = ua; }
|
||||
// 更新 trajectory
|
||||
if (utr) { r.meta.trajectory = utr; }
|
||||
if (upd?.world) r.world = upd.world;
|
||||
if (upd?.maps?.outdoor) { r.maps = r.maps || {}; r.maps.outdoor = r.maps.outdoor || {}; if (upd.maps.outdoor.description) r.maps.outdoor.description = upd.maps.outdoor.description; if (Array.isArray(upd.maps.outdoor.nodes)) { const on = r.maps.outdoor.nodes || []; upd.maps.outdoor.nodes.forEach(n => { const i = on.findIndex(x => x.name === n.name); if (i >= 0) on[i] = { ...n }; else on.push(n); }); r.maps.outdoor.nodes = on; } }
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 检查自动推演 */
|
||||
async function checkAutoSim(reqId) {
|
||||
const store = getOutlineStore();
|
||||
if (!store || (store.simulationProgress || 0) < (store.simulationTarget ?? 5)) return;
|
||||
@@ -491,17 +586,20 @@ async function checkAutoSim(reqId) {
|
||||
await handleSimWorld({ requestId: `wsim_auto_${Date.now()}`, currentData: JSON.stringify(data), isAuto: true });
|
||||
}
|
||||
|
||||
// 验证器
|
||||
const V = {
|
||||
sum: o => o?.summary, npc: o => o?.name && o?.aliases, arr: o => Array.isArray(o),
|
||||
scene: o => !!o?.review?.deviation && !!(o?.local_map || o?.scene_setup?.local_map),
|
||||
lscene: o => !!o?.side_story, inv: o => typeof o?.invite === 'boolean' && o?.reply,
|
||||
sms: o => typeof o?.reply === 'string' && o.reply.length > 0,
|
||||
wg1: d => !!d && typeof d === 'object',
|
||||
wg1: d => !!d && typeof d === 'object', // 只要是对象就行,后续会 normalize
|
||||
wg2: d => !!(d?.world && (d?.maps || d?.world?.maps)?.outdoor),
|
||||
wga: d => !!(d?.world && d?.maps?.outdoor), ws: d => !!d, w: o => !!o && typeof o === 'object',
|
||||
lm: o => !!o?.inside?.name && !!o?.inside?.description
|
||||
};
|
||||
|
||||
// --- 处理器 ---
|
||||
|
||||
async function handleFetchModels({ apiUrl, apiKey }) {
|
||||
try {
|
||||
let models = [];
|
||||
@@ -550,6 +648,7 @@ async function handleSendSms({ requestId, contactName, worldbookUid, userMessage
|
||||
try {
|
||||
const ctx = getContext(), userName = name1 || ctx.name1 || '用户';
|
||||
let charContent = '', existSum = {}, sc = summarizedCount || 0;
|
||||
|
||||
if (worldbookUid === CHAR_CARD_UID) {
|
||||
charContent = getCharInfo().desc;
|
||||
const h = getCharSmsHistory(); existSum = h?.summaries || {}; sc = summarizedCount ?? h?.summarizedCount ?? 0;
|
||||
@@ -562,10 +661,12 @@ async function handleSendSms({ requestId, contactName, worldbookUid, userMessage
|
||||
if (s !== -1 && ed !== -1) { const p = safe(() => JSON.parse(c.substring(s + 19, ed).trim())); const si = p?.find?.(i => typeof i === 'string' && i.startsWith('SMS_summary:')); if (si) existSum = safe(() => JSON.parse(si.substring(12))) || {}; }
|
||||
}
|
||||
}
|
||||
|
||||
let histText = '';
|
||||
const sumKeys = Object.keys(existSum).filter(k => k !== '_count').sort((a, b) => a - b);
|
||||
if (sumKeys.length) histText = `[之前的对话摘要] ${sumKeys.map(k => existSum[k]).join(';')}\n\n`;
|
||||
if (chatHistory?.length > 1) { const msgs = chatHistory.slice(sc, -1); if (msgs.length) histText += msgs.map(m => `${m.type === 'sent' ? userName : contactName}:${m.text}`).join('\n'); }
|
||||
|
||||
const msgs = buildSmsMessages({ contactName, userName, storyOutline: formatOutlinePrompt(), historyCount: getCommSettings().historyCount || 50, smsHistoryContent: buildSmsHistoryContent(histText), userMessage, characterContent: charContent });
|
||||
const parsed = await callLLMJson({ messages: msgs, validate: V.sms });
|
||||
reply('SMS_RESULT', requestId, parsed?.reply ? { reply: parsed.reply } : { error: '生成回复失败,请调整重试' });
|
||||
@@ -596,6 +697,7 @@ async function handleCompressSms({ requestId, worldbookUid, messages, contactNam
|
||||
try {
|
||||
const ctx = getContext(), userName = name1 || ctx.name1 || '用户';
|
||||
let e = null, existSum = {};
|
||||
|
||||
if (worldbookUid === CHAR_CARD_UID) {
|
||||
const h = getCharSmsHistory(); existSum = h?.summaries || {};
|
||||
const keep = 4, toEnd = Math.max(sc, (messages?.length || 0) - keep);
|
||||
@@ -611,8 +713,10 @@ async function handleCompressSms({ requestId, worldbookUid, messages, contactNam
|
||||
if (h) { h.messages = Array.isArray(messages) ? messages : (h.messages || []); h.summarizedCount = toEnd; h.summaries = existSum; saveMetadataDebounced?.(); }
|
||||
return reply('COMPRESS_SMS_RESULT', requestId, { summary: sum, newSummarizedCount: toEnd });
|
||||
}
|
||||
|
||||
e = await findEntry(worldbookUid);
|
||||
if (e?.entry) { const c = e.entry.content || '', [s, ed] = [c.indexOf('[SMS_HISTORY_START]'), c.indexOf('[SMS_HISTORY_END]')]; if (s !== -1 && ed !== -1) { const p = safe(() => JSON.parse(c.substring(s + 19, ed).trim())); const si = p?.find?.(i => typeof i === 'string' && i.startsWith('SMS_summary:')); if (si) existSum = safe(() => JSON.parse(si.substring(12))) || {}; } }
|
||||
|
||||
const keep = 4, toEnd = Math.max(sc, messages.length - keep);
|
||||
if (toEnd <= sc) return replyErr('COMPRESS_SMS_RESULT', requestId, '没有足够的新消息需要总结');
|
||||
const toSum = messages.slice(sc, toEnd); if (toSum.length < 2) return replyErr('COMPRESS_SMS_RESULT', requestId, '需要至少2条消息才能进行总结');
|
||||
@@ -622,6 +726,7 @@ async function handleCompressSms({ requestId, worldbookUid, messages, contactNam
|
||||
const parsed = await callLLMJson({ messages: buildSummaryMessages({ existingSummaryContent: buildExistingSummaryContent(existText), conversationText: convText }), validate: V.sum });
|
||||
const sum = parsed?.summary?.trim?.(); if (!sum) return replyErr('COMPRESS_SMS_RESULT', requestId, 'ECHO:总结生成出错,请重试');
|
||||
const newSc = toEnd;
|
||||
|
||||
if (e) {
|
||||
const { bookName, entry: en, worldData } = e; let c = en.content || ''; const cn = contactName || en.key?.[0] || '角色';
|
||||
const [s, ed] = [c.indexOf('[SMS_HISTORY_START]'), c.indexOf('[SMS_HISTORY_END]')]; if (s !== -1 && ed !== -1) c = c.substring(0, s).trimEnd() + c.substring(ed + 17);
|
||||
@@ -742,6 +847,8 @@ async function handleGenLocalScene({ requestId, locationName, locationInfo }) {
|
||||
async function handleGenWorld({ requestId, playerRequests }) {
|
||||
try {
|
||||
const comm = getCommSettings(), mode = getGlobalSettings().mode || 'story', store = getOutlineStore();
|
||||
|
||||
// 递归查找函数 - 在任意层级找到目标键
|
||||
const deepFind = (obj, key) => {
|
||||
if (!obj || typeof obj !== 'object') return null;
|
||||
if (obj[key] !== undefined) return obj[key];
|
||||
@@ -751,24 +858,42 @@ async function handleGenWorld({ requestId, playerRequests }) {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const normalizeStep1Data = (data) => {
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
|
||||
// 构建标准化结构,从任意位置提取数据
|
||||
const result = { meta: {} };
|
||||
|
||||
// 提取 truth(可能在 meta.truth, data.truth, 或者 data 本身就是 truth)
|
||||
result.meta.truth = deepFind(data, 'truth')
|
||||
|| (data.background && data.driver ? data : null)
|
||||
|| { background: deepFind(data, 'background'), driver: deepFind(data, 'driver') };
|
||||
|
||||
// 提取 onion_layers
|
||||
result.meta.onion_layers = deepFind(data, 'onion_layers') || {};
|
||||
|
||||
// 统一洋葱层级为数组格式
|
||||
['L1_The_Veil', 'L2_The_Distortion', 'L3_The_Law', 'L4_The_Agent', 'L5_The_Axiom'].forEach(k => {
|
||||
const v = result.meta.onion_layers[k];
|
||||
if (v && !Array.isArray(v) && typeof v === 'object') {
|
||||
result.meta.onion_layers[k] = [v];
|
||||
}
|
||||
});
|
||||
|
||||
// 提取 atmosphere
|
||||
result.meta.atmosphere = deepFind(data, 'atmosphere') || { reasoning: '', current: { environmental: '', npc_attitudes: '' } };
|
||||
|
||||
// 提取 trajectory
|
||||
result.meta.trajectory = deepFind(data, 'trajectory') || { reasoning: '', ending: '' };
|
||||
|
||||
// 提取 user_guide
|
||||
result.meta.user_guide = deepFind(data, 'user_guide') || { current_state: '', guides: [] };
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 辅助模式
|
||||
if (mode === 'assist') {
|
||||
const msgs = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests, mode: 'assist' });
|
||||
const wd = await callLLMJson({ messages: msgs, validate: V.wga });
|
||||
@@ -776,20 +901,28 @@ async function handleGenWorld({ requestId, playerRequests }) {
|
||||
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = { ...wd }; saveMetadataDebounced?.(); }
|
||||
return reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: wd });
|
||||
}
|
||||
|
||||
// Step 1
|
||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在构思世界大纲 (Step 1/2)...' });
|
||||
const s1m = buildWorldGenStep1Messages({ historyCount: comm.historyCount || 50, playerRequests });
|
||||
const s1d = normalizeStep1Data(await callLLMJson({ messages: s1m, validate: V.wg1 }));
|
||||
|
||||
// 简化验证 - 只要有基本数据就行
|
||||
if (!s1d?.meta) {
|
||||
return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 1 失败:无法解析大纲数据,请重试');
|
||||
}
|
||||
step1Cache = { step1Data: s1d, playerRequests: playerRequests || '' };
|
||||
|
||||
// Step 2
|
||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: 'Step 1 完成,1 秒后开始构建世界细节 (Step 2/2)...' });
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在构建世界细节 (Step 2/2)...' });
|
||||
|
||||
const s2m = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests, step1Data: s1d });
|
||||
const s2d = await callLLMJson({ messages: s2m, validate: V.wg2 });
|
||||
if (s2d?.world?.maps && !s2d?.maps) { s2d.maps = s2d.world.maps; delete s2d.world.maps; }
|
||||
if (!s2d?.world || !s2d?.maps) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 失败:无法生成有效的地图');
|
||||
|
||||
const final = { meta: s1d.meta, world: s2d.world, maps: s2d.maps, playerLocation: s2d.playerLocation };
|
||||
step1Cache = null;
|
||||
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); }
|
||||
@@ -801,13 +934,16 @@ async function handleRetryStep2({ requestId }) {
|
||||
try {
|
||||
if (!step1Cache?.step1Data?.meta) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 重试失败:缺少 Step 1 数据,请重新开始生成');
|
||||
const comm = getCommSettings(), store = getOutlineStore(), s1d = step1Cache.step1Data, pr = step1Cache.playerRequests || '';
|
||||
|
||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '1 秒后重试构建世界细节 (Step 2/2)...' });
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
postFrame({ type: 'GENERATE_WORLD_STATUS', requestId, message: '正在重试构建世界细节 (Step 2/2)...' });
|
||||
|
||||
const s2m = buildWorldGenStep2Messages({ historyCount: comm.historyCount || 50, playerRequests: pr, step1Data: s1d });
|
||||
const s2d = await callLLMJson({ messages: s2m, validate: V.wg2 });
|
||||
if (s2d?.world?.maps && !s2d?.maps) { s2d.maps = s2d.world.maps; delete s2d.world.maps; }
|
||||
if (!s2d?.world || !s2d?.maps) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 失败:无法生成有效的地图');
|
||||
|
||||
const final = { meta: s1d.meta, world: s2d.world, maps: s2d.maps, playerLocation: s2d.playerLocation };
|
||||
step1Cache = null;
|
||||
if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); }
|
||||
@@ -844,10 +980,7 @@ function handleSaveSettings(d) {
|
||||
function handleSavePrompts(d) {
|
||||
if (!d?.promptConfig) return;
|
||||
setPromptConfig?.(d.promptConfig, true);
|
||||
postFrame({
|
||||
type: "PROMPT_CONFIG_UPDATED",
|
||||
promptConfig: getPromptConfigPayload?.()
|
||||
});
|
||||
postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
|
||||
}
|
||||
|
||||
function handleSaveContacts(d) {
|
||||
@@ -881,6 +1014,7 @@ function handleSaveCharSmsHistory(d) {
|
||||
injectOutline();
|
||||
}
|
||||
|
||||
// 处理器映射
|
||||
const handlers = {
|
||||
FRAME_READY: () => { frameReady = true; flushPending(); loadAndSend(); },
|
||||
CLOSE_PANEL: hideOverlay,
|
||||
@@ -916,6 +1050,7 @@ const handleMsg = ({ data }) => { if (data?.source === "LittleWhiteBox-OutlineFr
|
||||
|
||||
// ==================== 10. UI管理 ====================
|
||||
|
||||
/** 指针拖拽 */
|
||||
function setupDrag(el, { onStart, onMove, onEnd, shouldHandle }) {
|
||||
if (!el) return;
|
||||
let state = null;
|
||||
@@ -925,6 +1060,7 @@ function setupDrag(el, { onStart, onMove, onEnd, shouldHandle }) {
|
||||
['pointerup', 'pointercancel', 'lostpointercapture'].forEach(ev => el.addEventListener(ev, end));
|
||||
}
|
||||
|
||||
/** 创建Overlay */
|
||||
function createOverlay() {
|
||||
if (overlayCreated) return;
|
||||
overlayCreated = true;
|
||||
@@ -932,6 +1068,7 @@ function createOverlay() {
|
||||
const overlay = document.getElementById("xiaobaix-story-outline-overlay"), wrap = overlay.querySelector(".xb-so-frame-wrap"), iframe = overlay.querySelector("iframe");
|
||||
const setPtr = v => iframe && (iframe.style.pointerEvents = v);
|
||||
|
||||
// 拖拽
|
||||
setupDrag(overlay.querySelector(".xb-so-drag-handle"), {
|
||||
shouldHandle: () => !isMobile(),
|
||||
onStart(e) { const r = wrap.getBoundingClientRect(), ro = overlay.getBoundingClientRect(); wrap.style.left = (r.left - ro.left) + 'px'; wrap.style.top = (r.top - ro.top) + 'px'; wrap.style.transform = ''; setPtr('none'); return { sx: e.clientX, sy: e.clientY, sl: parseFloat(wrap.style.left), st: parseFloat(wrap.style.top) }; },
|
||||
@@ -939,18 +1076,20 @@ function createOverlay() {
|
||||
onEnd: () => setPtr('')
|
||||
});
|
||||
|
||||
// 缩放
|
||||
setupDrag(overlay.querySelector(".xb-so-resize-handle"), {
|
||||
shouldHandle: () => !isMobile(),
|
||||
onStart(e) { const r = wrap.getBoundingClientRect(), ro = overlay.getBoundingClientRect(); wrap.style.left = (r.left - ro.left) + 'px'; wrap.style.top = (r.top - ro.top) + 'px'; wrap.style.transform = ''; setPtr('none'); return { sx: e.clientX, sy: e.clientY, sw: wrap.offsetWidth, sh: wrap.offsetHeight, ratio: wrap.offsetWidth / wrap.offsetHeight }; },
|
||||
onMove(e, s) { const dx = e.clientX - s.sx, dy = e.clientY - s.sy, delta = Math.abs(dx) > Math.abs(dy) ? dx : dy * s.ratio; let w = Math.max(400, Math.min(window.innerWidth * 0.95, s.sw + delta)), h = w / s.ratio; if (h > window.innerHeight * 0.9) { h = window.innerHeight * 0.9; w = h * s.ratio; } if (h < 300) { h = 300; w = h * s.ratio; } wrap.style.width = w + 'px'; wrap.style.height = h + 'px'; },
|
||||
onEnd: () => { setPtr(''); setStoredSize(false, { width: wrap.offsetWidth, height: wrap.offsetHeight }); }
|
||||
onEnd: () => setPtr('')
|
||||
});
|
||||
|
||||
// 移动端
|
||||
setupDrag(overlay.querySelector(".xb-so-resize-mobile"), {
|
||||
shouldHandle: () => isMobile(),
|
||||
onStart(e) { setPtr('none'); return { sy: e.clientY, sh: wrap.offsetHeight }; },
|
||||
onMove(e, s) { wrap.style.height = Math.max(44, Math.min(window.innerHeight * 0.9, s.sh + e.clientY - s.sy)) + 'px'; },
|
||||
onEnd: () => { setPtr(''); setStoredSize(true, { height: wrap.offsetHeight }); }
|
||||
onEnd: () => setPtr('')
|
||||
});
|
||||
|
||||
window.addEventListener("message", handleMsg);
|
||||
@@ -959,53 +1098,17 @@ function createOverlay() {
|
||||
function updateLayout() {
|
||||
const wrap = document.querySelector(".xb-so-frame-wrap"); if (!wrap) return;
|
||||
const drag = document.querySelector(".xb-so-drag-handle"), resize = document.querySelector(".xb-so-resize-handle"), mobile = document.querySelector(".xb-so-resize-mobile");
|
||||
if (isMobile()) {
|
||||
if (drag) drag.style.display = 'none';
|
||||
if (resize) resize.style.display = 'none';
|
||||
if (mobile) mobile.style.display = 'flex';
|
||||
wrap.style.cssText = MOBILE_LAYOUT_STYLE;
|
||||
const maxHeight = window.innerHeight * 1;
|
||||
const stored = getStoredSize(true);
|
||||
const height = stored?.height ? Math.min(stored.height, maxHeight) : maxHeight;
|
||||
wrap.style.height = Math.max(44, height) + 'px';
|
||||
wrap.style.top = '0px';
|
||||
}
|
||||
else {
|
||||
if (drag) drag.style.display = 'block';
|
||||
if (resize) resize.style.display = 'block';
|
||||
if (mobile) mobile.style.display = 'none';
|
||||
wrap.style.cssText = DESKTOP_LAYOUT_STYLE;
|
||||
const stored = getStoredSize(false);
|
||||
if (stored) {
|
||||
const maxW = window.innerWidth * 0.95;
|
||||
const maxH = window.innerHeight * 0.9;
|
||||
if (stored.width) wrap.style.width = Math.max(400, Math.min(stored.width, maxW)) + 'px';
|
||||
if (stored.height) wrap.style.height = Math.max(300, Math.min(stored.height, maxH)) + 'px';
|
||||
}
|
||||
}
|
||||
if (isMobile()) { if (drag) drag.style.display = 'none'; if (resize) resize.style.display = 'none'; if (mobile) mobile.style.display = 'flex'; wrap.style.cssText = MOBILE_LAYOUT_STYLE; const fixedHeight = window.innerHeight * 0.4; wrap.style.height = Math.max(44, fixedHeight) + 'px'; wrap.style.top = '0px'; }
|
||||
else { if (drag) drag.style.display = 'block'; if (resize) resize.style.display = 'block'; if (mobile) mobile.style.display = 'none'; wrap.style.cssText = DESKTOP_LAYOUT_STYLE; }
|
||||
}
|
||||
|
||||
function showOverlay() {
|
||||
if (!overlayCreated) createOverlay();
|
||||
|
||||
if (!iframeLoaded) {
|
||||
frameReady = false;
|
||||
const f = document.getElementById("xiaobaix-story-outline-iframe");
|
||||
if (f) f.src = IFRAME_PATH;
|
||||
iframeLoaded = true;
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
$("#xiaobaix-story-outline-overlay").show();
|
||||
}
|
||||
|
||||
function hideOverlay() {
|
||||
$("#xiaobaix-story-outline-overlay").hide();
|
||||
}
|
||||
function showOverlay() { if (!overlayCreated) createOverlay(); frameReady = false; const f = document.getElementById("xiaobaix-story-outline-iframe"); if (f) f.src = IFRAME_PATH; updateLayout(); $("#xiaobaix-story-outline-overlay").show(); }
|
||||
function hideOverlay() { $("#xiaobaix-story-outline-overlay").hide(); }
|
||||
|
||||
let lastIsMobile = isMobile();
|
||||
window.addEventListener('resize', () => { const nowIsMobile = isMobile(); if (nowIsMobile !== lastIsMobile) { lastIsMobile = nowIsMobile; updateLayout(); } });
|
||||
|
||||
|
||||
// ==================== 11. 事件与初始化 ====================
|
||||
|
||||
let eventsRegistered = false;
|
||||
@@ -1032,13 +1135,17 @@ function initBtns() {
|
||||
function registerEvents() {
|
||||
if (eventsRegistered) return;
|
||||
eventsRegistered = true;
|
||||
|
||||
initBtns();
|
||||
|
||||
events.on(event_types.CHAT_CHANGED, () => { setTimeout(initBtns, 80); setTimeout(injectOutline, 100); });
|
||||
events.on(event_types.GENERATION_STARTED, injectOutline);
|
||||
|
||||
const handler = d => setTimeout(() => {
|
||||
const id = d?.element ? $(d.element).attr("mesid") : d?.messageId;
|
||||
id == null ? initBtns() : addBtnToMsg(id);
|
||||
}, 50);
|
||||
|
||||
events.onMany([
|
||||
event_types.USER_MESSAGE_RENDERED,
|
||||
event_types.CHARACTER_MESSAGE_RENDERED,
|
||||
@@ -1047,6 +1154,7 @@ function registerEvents() {
|
||||
event_types.MESSAGE_SWIPED,
|
||||
event_types.MESSAGE_EDITED
|
||||
], handler);
|
||||
|
||||
setupSTEvents();
|
||||
}
|
||||
|
||||
@@ -1056,13 +1164,14 @@ function cleanup() {
|
||||
$(".xiaobaix-story-outline-btn").remove();
|
||||
hideOverlay();
|
||||
overlayCreated = false; frameReady = false; pendingMsgs = [];
|
||||
iframeLoaded = false;
|
||||
window.removeEventListener("message", handleMsg);
|
||||
document.getElementById("xiaobaix-story-outline-overlay")?.remove();
|
||||
removePrompt();
|
||||
if (presetCleanup) { presetCleanup(); presetCleanup = null; }
|
||||
}
|
||||
|
||||
// ==================== Toggle 监听(始终注册)====================
|
||||
|
||||
$(document).on("xiaobaix:storyOutline:toggle", (_e, enabled) => {
|
||||
if (enabled) {
|
||||
registerEvents();
|
||||
@@ -1083,6 +1192,8 @@ document.addEventListener('xiaobaixEnabledChanged', e => {
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
jQuery(() => {
|
||||
if (!getSettings().storyOutline?.enabled) return;
|
||||
registerEvents();
|
||||
|
||||
Reference in New Issue
Block a user