/* eslint-disable no-restricted-syntax */
/**
 * ============================================================================
 * 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";
import { getContext } from "../../../../../st-context.js";
import { streamingGeneration } from "../streaming-generation.js";
import { EXT_ID, extensionFolderPath } from "../../core/constants.js";
import { createModuleEvents, event_types } from "../../core/event-manager.js";
import { StoryOutlineStorage } from "../../core/server-storage.js";
import { promptManager } from "../../../../../openai.js";
import {
    buildSmsMessages, buildSummaryMessages, buildSmsHistoryContent, buildExistingSummaryContent,
    buildNpcGenerationMessages, formatNpcToWorldbookContent, buildExtractStrangersMessages,
    buildWorldGenStep1Messages, buildWorldGenStep2Messages, buildWorldSimMessages, buildSceneSwitchMessages,
    buildInviteMessages, buildLocalMapGenMessages, buildLocalMapRefreshMessages, buildLocalSceneGenMessages,
    buildOverlayHtml, MOBILE_LAYOUT_STYLE, DESKTOP_LAYOUT_STYLE, getPromptConfigPayload, setPromptConfig
} from "./story-outline-prompt.js";
import { postToIframe, isTrustedMessage } from "../../core/iframe-messaging.js";

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 STORY_OUTLINE_ID = 'lwb_story_outline';
const CHAR_CARD_UID = '__CHARACTER_CARD__';
const DEBUG_KEY = 'LittleWhiteBox_StoryOutline_Debug';

let overlayCreated = false, frameReady = false, pendingMsgs = [], presetCleanup = null, step1Cache = null;

// ==================== 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; }
};

/** 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;

/**
 * 修复单个 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; }
        if (c === '\\' && inStr) { esc = true; continue; }
        if (c === '"') { inStr = !inStr; continue; }
        if (!inStr) {
            if (c === '{') braces++; else if (c === '}') braces--;
            if (c === '[') brackets++; else if (c === ']') brackets--;
        }
    }
    while (braces-- > 0) r += '}';
    while (brackets-- > 0) r += ']';
    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)) {
            const content = input.choices?.[0]?.message?.content
                ?? input.choices?.[0]?.message?.reasoning_content
                ?? input.content ?? input.reasoning_content;
            if (content != null) return extractJson(String(content).trim(), isArray);
            if (!input.choices) return input;
        }
        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];
            if (esc) { esc = false; continue; }
            if (c === '\\' && inStr) { esc = true; continue; }
            if (c === '"') { inStr = !inStr; continue; }
            if (inStr) continue;
            if (c === '{' || c === '[') depth++;
            else if (c === '}' || c === ']') depth--;
            if (depth === 0) {
                candidates.push({ start: i, end: j, text: str.slice(i, j + 1) });
                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; // 有 meta 就直接返回
            continue;
        }

        // 修复后解析
        const fixed = fixJson(text);
        r = tryParse(fixed);
        if (ok(r, isArray)) {
            const s = score(r);
            if (s > bestScore) { best = r; bestScore = s; }
            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) {
        const chunk = str.slice(firstBrace, lastBrace + 1);
        r = tryParse(chunk) || tryParse(fixJson(chunk));
        if (ok(r, isArray)) return r;
    }

    return null;
}

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] ||= {};
    return lwb.storyOutline ||= {
        mapData: null, stage: 0, deviationScore: 0, simulationTarget: 5, playerLocation: '家',
        outlineData: { meta: null, world: null, outdoor: null, indoor: null, sceneSetup: null, strangers: null, contacts: null },
        dataChecked: { meta: true, world: true, outdoor: true, indoor: true, sceneSetup: true, strangers: false, contacts: false, characterContactSms: false }
    };
}

/** 全局/通讯设置读写 */
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, stream: false, ...getStore(STORAGE_KEYS.comm, {}) });
const saveCommSettings = s => setStore(STORAGE_KEYS.comm, s);

/** 获取角色卡信息 */
function getCharInfo() {
    const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
    return {
        name: char?.name || char?.data?.name || char?.avatar || '角色卡',
        desc: String((char?.description ?? char?.data?.description ?? '') || '').trim() || '{{description}}'
    };
}

/** 获取角色卡短信历史 */
function getCharSmsHistory() {
    if (!chat_metadata) return null;
    const root = chat_metadata.LittleWhiteBox_StoryOutline ||= {};
    const h = root.characterContactSmsHistory ||= { messages: [], summarizedCount: 0, summaries: {} };
    h.messages ||= []; h.summarizedCount ||= 0; h.summaries ||= {};
    return h;
}

// ==================== 5. LLM调用 ====================

const STREAM_DONE_EVT = 'xiaobaix_streaming_completed';
let streamLlmQueue = Promise.resolve();

function createStreamingWaiter(sessionId, timeoutMs = 180000) {
    let done = false;
    let timer = null;
    let handler = null;

    const cleanup = () => {
        if (done) return;
        done = true;
        try { if (timer) clearTimeout(timer); } catch { }
        try { eventSource.removeListener?.(STREAM_DONE_EVT, handler); } catch { }
    };

    const promise = new Promise((resolve, reject) => {
        handler = (payload) => {
            if (!payload || payload.sessionId !== sessionId) return;
            cleanup();
            resolve(String(payload.finalText ?? ''));
        };
        timer = setTimeout(() => {
            cleanup();
            reject(new Error('Streaming timeout'));
        }, timeoutMs);
        try { eventSource.on?.(STREAM_DONE_EVT, handler); } catch (e) {
            cleanup();
            reject(e);
        }
    });

    return { promise, cleanup };
}

/** 调用LLM */
async function callLLM(promptOrMsgs, useRaw = false) {
    const { apiUrl, apiKey, model } = getGlobalSettings();
    const useStream = !!getCommSettings()?.stream;

    const normalize = r => {
        if (r == null) return '';
        if (typeof r === 'string') return r;
        if (typeof r === 'object') {
            if (r.data && typeof r.data === 'object') return normalize(r.data);
            if (typeof r.text === 'string') return r.text;
            if (typeof r.response === 'string') return r.response;
            const inner = r.content?.trim?.() || r.reasoning_content?.trim?.() || r.choices?.[0]?.message?.content?.trim?.() || r.choices?.[0]?.message?.reasoning_content?.trim?.() || null;
            if (inner != null) return String(inner);
            return safe(() => JSON.stringify(r)) || String(r);
        }
        return String(r);
    };

    const baseOpts = { lock: 'on' };
    if (!useStream) baseOpts.nonstream = 'true';
    if (apiUrl?.trim()) Object.assign(baseOpts, { api: 'openai', apiurl: apiUrl.trim(), ...(apiKey && { apipassword: apiKey }), ...(model && { model }) });

    if (!useStream) {
        const opts = { ...baseOpts };

        if (useRaw) {
            const messages = Array.isArray(promptOrMsgs)
                ? promptOrMsgs
                : [{ role: 'user', content: String(promptOrMsgs || '').trim() }];

            const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
            const topParts = messages
                .filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
                .map(m => {
                    const role = roleMap[m.role] || m.role;
                    return `${role}={${m.content}}`;
                });
            const topParam = topParts.join(';');
            opts.top = topParam;

            const raw = await streamingGeneration.xbgenrawCommand(opts, '');
            const text = normalize(raw).trim();

            if (isDebug()) {
                try {
                    console.groupCollapsed('[StoryOutline] callLLM(useRaw via xbgenrawCommand)');
                    console.log('opts.top.length', topParam.length);
                    console.log('raw', raw);
                    console.log('normalized.length', text.length);
                    console.groupEnd();
                } catch { }
            }
            return text;
        }

        opts.as = 'user';
        opts.position = 'history';
        return normalize(await streamingGeneration.xbgenCommand(opts, promptOrMsgs)).trim();
    }

    const runStreaming = async () => {
        const sessionId = 'xb10';
        const waiter = createStreamingWaiter(sessionId);
        const opts = { ...baseOpts, id: sessionId };
        try {
            if (useRaw) {
                const messages = Array.isArray(promptOrMsgs)
                    ? promptOrMsgs
                    : [{ role: 'user', content: String(promptOrMsgs || '').trim() }];

                const roleMap = { user: 'user', assistant: 'assistant', system: 'sys' };
                const topParts = messages
                    .filter(m => m?.role && typeof m.content === 'string' && m.content.trim())
                    .map(m => {
                        const role = roleMap[m.role] || m.role;
                        return `${role}={${m.content}}`;
                    });
                opts.top = topParts.join(';');
                await streamingGeneration.xbgenrawCommand(opts, '');
                return (await waiter.promise).trim();
            }

            opts.as = 'user';
            opts.position = 'history';
            await streamingGeneration.xbgenCommand(opts, promptOrMsgs);
            return (await waiter.promise).trim();
        } finally {
            waiter.cleanup();
        }
    };

    streamLlmQueue = streamLlmQueue.then(runStreaming, runStreaming);
    return streamLlmQueue;
}

/** 调用LLM并解析JSON */
async function callLLMJson({ messages, useRaw = true, isArray = false, validate }) {
    try {
        const result = await callLLM(messages, useRaw);
        if (isDebug()) {
            try {
                const s = String(result ?? '');
                console.groupCollapsed('[StoryOutline] callLLMJson');
                console.log({ useRaw, isArray, length: s.length });
                console.log('result.head', s.slice(0, 500));
                console.log('result.tail', s.slice(Math.max(0, s.length - 500)));
                console.groupEnd();
            } catch { }
        }
        const parsed = extractJson(result, isArray);
        if (isDebug()) {
            try {
                console.groupCollapsed('[StoryOutline] extractJson');
                console.log('parsed', parsed);
                console.log('validate', !!(parsed && validate?.(parsed)));
                console.groupEnd();
            } catch { }
        }
        if (parsed && validate(parsed)) return parsed;
    } catch { }
    return null;
}

// ==================== 6. 世界书操作 ====================

/** 获取角色卡绑定的世界书 */
async function getCharWorldbooks() {
    const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
    if (!char) return [];
    const books = [], primary = char.data?.extensions?.world;
    if (primary && world_names?.includes(primary)) books.push(primary);
    (world_info?.charLore || []).find(e => e.name === char.avatar)?.extraBooks?.forEach(b => {
        if (world_names?.includes(b) && !books.includes(b)) books.push(b);
    });
    return books;
}

/** 根据UID查找条目 */
async function findEntry(uid) {
    const uidNum = parseInt(uid, 10);
    if (isNaN(uidNum)) return null;
    for (const book of await getCharWorldbooks()) {
        const data = await loadWorldInfo(book);
        if (data?.entries?.[uidNum]) return { bookName: book, entry: data.entries[uidNum], uidNumber: uidNum, worldData: data };
    }
    return null;
}

/** 根据名称搜索条目 */
async function searchEntry(name) {
    const nl = (name || '').toLowerCase().trim();
    for (const book of await getCharWorldbooks()) {
        const data = await loadWorldInfo(book);
        if (!data?.entries) continue;
        for (const [uid, entry] of Object.entries(data.entries)) {
            const keys = Array.isArray(entry.key) ? entry.key : [];
            if (keys.some(k => { const kl = (k || '').toLowerCase().trim(); return kl === nl || kl.includes(nl) || nl.includes(kl); }))
                return { uid: String(uid), bookName: book, entry };
        }
    }
    return null;
}

// ==================== 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";
            getVisibleLayers(stage).forEach(k => {
                const l = onion[k]; if (!l || !Array.isArray(l) || !l.length) return;
                const name = k.replace(/_/g, ' - ');
                l.forEach(i => { text += `  - [${name}] ${i.desc}: ${i.logic}\n`; });
            });
        }
        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`;
        if (playerLocation && d.outdoor.nodes?.length) locNode = d.outdoor.nodes.find(n => n.name === playerLocation);
    }
    if (!locNode && c?.indoor && d.indoor?.nodes?.length && playerLocation) locNode = d.indoor.nodes.find(n => n.name === playerLocation);
    const indoorMap = (c?.indoor && playerLocation && d.indoor && typeof d.indoor === 'object' && !Array.isArray(d.indoor)) ? d.indoor[playerLocation] : null;
    const locText = indoorMap?.description || locNode?.info || '';
    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.Facade || ss.Undercurrent)) {
            has = true;
            text += "### 当前剧情 (Current Scene)\n";
            if (ss.Facade) text += `* 表现: ${ss.Facade}\n`;
            if (ss.Undercurrent) text += `* 暗流: ${ss.Undercurrent}\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);
        const msgs = hist?.messages || [], sc = hist?.summarizedCount || 0, rem = msgs.slice(sc);
        if (sumKeys.length || rem.length) {
            has = true; text += `### ${charName}短信记录\n`;
            if (sumKeys.length) text += `[摘要] ${sumKeys.map(k => sums[k]).join('；')}\n`;
            if (rem.length) text += rem.map(m => `${m.type === 'sent' ? '{{user}}' : charName}：${m.text}`).join('\n') + "\n";
            text += "\n";
        }
    }

    return has ? text.trim() : "";
}

/** 确保剧情大纲Prompt存在 */
function ensurePrompt() {
    if (!promptManager) return false;
    let prompt = promptManager.getPromptById(STORY_OUTLINE_ID);
    if (!prompt) {
        promptManager.addPrompt({ identifier: STORY_OUTLINE_ID, name: '剧情地图', role: 'system', content: '', system_prompt: false, marker: false, extension: true }, STORY_OUTLINE_ID);
        prompt = promptManager.getPromptById(STORY_OUTLINE_ID);
    }
    const char = promptManager.activeCharacter;
    if (!char) return true;
    const order = promptManager.getPromptOrderForCharacter(char);
    const exists = order.some(e => e.identifier === STORY_OUTLINE_ID);
    if (!exists) { const idx = order.findIndex(e => e.identifier === 'charDescription'); order.splice(idx !== -1 ? idx : 0, 0, { identifier: STORY_OUTLINE_ID, enabled: true }); }
    else { const entry = order.find(e => e.identifier === STORY_OUTLINE_ID); if (entry && !entry.enabled) entry.enabled = true; }
    promptManager.render?.(false);
    return true;
}

/** 更新剧情大纲Prompt内容 */
function updatePromptContent() {
    if (!promptManager) return;
    if (!getSettings().storyOutline?.enabled) { removePrompt(); return; }
    ensurePrompt();
    const store = getOutlineStore(), prompt = promptManager.getPromptById(STORY_OUTLINE_ID);
    if (!prompt) return;
    const { dataChecked } = store || {};
    const hasAny = dataChecked && Object.values(dataChecked).some(v => v === true);
    prompt.content = (!hasAny || !store) ? '' : (formatOutlinePrompt() || '');
    promptManager.render?.(false);
}

/** 移除剧情大纲Prompt */
function removePrompt() {
    if (!promptManager) return;
    const prompts = promptManager.serviceSettings?.prompts;
    if (prompts) { const idx = prompts.findIndex(p => p?.identifier === STORY_OUTLINE_ID); if (idx !== -1) prompts.splice(idx, 1); }
    const orders = promptManager.serviceSettings?.prompt_order;
    if (orders) orders.forEach(cfg => { if (cfg?.order) { const idx = cfg.order.findIndex(e => e?.identifier === STORY_OUTLINE_ID); if (idx !== -1) cfg.order.splice(idx, 1); } });
    promptManager.render?.(false);
}

/** 设置ST预设事件监听 */
function setupSTEvents() {
    if (presetCleanup) return;
    const onChanged = () => { if (getSettings().storyOutline?.enabled) setTimeout(() => { ensurePrompt(); updatePromptContent(); }, 100); };
    const onExport = preset => {
        if (!preset) return;
        if (preset.prompts) { const i = preset.prompts.findIndex(p => p?.identifier === STORY_OUTLINE_ID); if (i !== -1) preset.prompts.splice(i, 1); }
        if (preset.prompt_order) preset.prompt_order.forEach(c => { if (c?.order) { const i = c.order.findIndex(e => e?.identifier === STORY_OUTLINE_ID); if (i !== -1) c.order.splice(i, 1); } });
    };
    eventSource.on(st_event_types.OAI_PRESET_CHANGED_AFTER, onChanged);
    eventSource.on(st_event_types.OAI_PRESET_EXPORT_READY, onExport);
    presetCleanup = () => { try { eventSource.removeListener(st_event_types.OAI_PRESET_CHANGED_AFTER, onChanged); } catch { } try { eventSource.removeListener(st_event_types.OAI_PRESET_EXPORT_READY, onExport); } catch { } };
}

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; }
    postToIframe(iframe, payload, "LittleWhiteBox");
}

const flushPending = () => { if (!frameReady) return; const f = document.getElementById("xiaobaix-story-outline-iframe"); pendingMsgs.forEach(p => { if (f) postToIframe(f, p, "LittleWhiteBox"); }); pendingMsgs = []; };

/** 发送设置到iframe */
function sendSettings() {
    const store = getOutlineStore(), { name: charName, desc: charDesc } = getCharInfo();
    postFrame({
        type: "LOAD_SETTINGS", globalSettings: getGlobalSettings(), commSettings: getCommSettings(),
        stage: store?.stage ?? 0, deviationScore: store?.deviationScore ?? 0,
        simulationTarget: store?.simulationTarget ?? 5, playerLocation: store?.playerLocation ?? '家',
        dataChecked: store?.dataChecked || {}, outlineData: store?.outlineData || {}, promptConfig: getPromptConfigPayload?.(),
        characterCardName: charName, characterCardDescription: charDesc,
        characterContactSmsHistory: getCharSmsHistory()
    });
}

const loadAndSend = () => { const s = getOutlineStore(); if (s?.mapData) postFrame({ type: "LOAD_MAP_DATA", mapData: s.mapData }); sendSettings(); };

function sendSimStateOnly() {
    const store = getOutlineStore();
    postFrame({
        type: "LOAD_SETTINGS",
        commSettings: getCommSettings(),
        stage: store?.stage ?? 0,
        deviationScore: store?.deviationScore ?? 0,
        simulationTarget: store?.simulationTarget ?? 5,
        playerLocation: store?.playerLocation ?? '家',
    });
}

// ==================== 9. 请求处理器 ====================

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 getCommonPromptVars(extra = {}) {
    const store = getOutlineStore();
    const comm = getCommSettings();
    const mode = getGlobalSettings().mode || 'story';
    const playerLocation = store?.playerLocation || store?.outlineData?.playerLocation || '未知';
    return {
        storyOutline: formatOutlinePrompt(),
        historyCount: comm.historyCount || 50,
        mode,
        stage: store?.stage || 0,
        deviationScore: store?.deviationScore || 0,
        simulationTarget: store?.simulationTarget ?? 5,
        playerLocation,
        currentAtmosphere: getAtmosphere(store),
        existingContacts: Array.isArray(store?.outlineData?.contacts) ? store.outlineData.contacts : [],
        existingStrangers: Array.isArray(store?.outlineData?.strangers) ? store.outlineData.strangers : [],
        ...(extra || {}),
    };
}

/** 合并世界推演数据 */
function mergeSimData(orig, upd) {
    if (!upd) return orig;
    const r = JSON.parse(JSON.stringify(orig || {}));
    const um = upd?.meta || {}, ut = um.truth || upd?.truth, uo = um.onion_layers || ut?.onion_layers;
    const ua = um.atmosphere || upd?.atmosphere, utr = um.trajectory || upd?.trajectory;
    r.meta = r.meta || {}; r.meta.truth = r.meta.truth || {};
    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;
}

function tickSimCountdown(store) {
    if (!store) return;
    const prevRaw = Number(store.simulationTarget);
    const prev = Number.isFinite(prevRaw) ? prevRaw : 5;
    const next = prev - 1;
    store.simulationTarget = next;
    store.updatedAt = Date.now();
    saveMetadataDebounced?.();
    sendSimStateOnly();
    if (prev > 0 && next <= 0) {
        try { processCommands?.('/echo 该进行世界推演啦！'); } catch { }
    }
}

// 验证器
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?.Incident && o?.side_story?.Facade && o?.side_story?.Undercurrent),
    inv: o => typeof o?.invite === 'boolean' && o?.reply,
    sms: o => typeof o?.reply === 'string' && o.reply.length > 0,
    wg1: d => !!d && typeof d === 'object',  // 只要是对象就行，后续会 normalize
    wg2: d => !!((d?.world && (d?.maps || d?.world?.maps)?.outdoor) || (d?.outdoor && d?.inside)),
    wga: d => !!((d?.world && d?.maps?.outdoor) || d?.outdoor), ws: d => !!d, w: o => !!o && typeof o === 'object',
    lm: o => !!o?.inside?.name && !!o?.inside?.description
};

function normalizeStep2Maps(data) {
    if (!data || typeof data !== 'object') return data;
    if (data.maps || data?.world?.maps) return data;
    if (!data.outdoor && !data.inside) return data;
    const out = { ...data };
    out.maps = { outdoor: data.outdoor, inside: data.inside };
    if (!out.world || typeof out.world !== 'object') out.world = { news: [] };
    delete out.outdoor;
    delete out.inside;
    return out;
}

// --- 处理器 ---

async function handleFetchModels({ apiUrl, apiKey }) {
    try {
        let models = [];
        if (!apiUrl) {
            for (const ep of ['/api/backends/chat-completions/models', '/api/openai/models']) {
                try { const r = await fetch(ep, { headers: { 'Content-Type': 'application/json' } }); if (r.ok) { const j = await r.json(); models = (j.data || j || []).map(m => m.id || m.name || m).filter(m => typeof m === 'string'); if (models.length) break; } } catch { }
            }
            if (!models.length) throw new Error('无法从酒馆获取模型列表');
        } else {
            const h = { 'Content-Type': 'application/json', ...(apiKey && { Authorization: `Bearer ${apiKey}` }) };
            const r = await fetch(apiUrl.replace(/\/$/, '') + '/models', { headers: h });
            if (!r.ok) throw new Error(`HTTP ${r.status}`);
            const j = await r.json();
            models = (j.data || j || []).map(m => m.id || m.name || m).filter(m => typeof m === 'string');
        }
        postFrame({ type: "FETCH_MODELS_RESULT", models });
    } catch (e) { postFrame({ type: "FETCH_MODELS_RESULT", error: e.message }); }
}

async function handleTestConn({ apiUrl, apiKey, model }) {
    try {
        if (!apiUrl) { for (const ep of ['/api/backends/chat-completions/status', '/api/openai/models', '/api/backends/chat-completions/models']) { try { if ((await fetch(ep, { headers: { 'Content-Type': 'application/json' } })).ok) { postFrame({ type: "TEST_CONN_RESULT", success: true, message: `连接成功${model ? ` (模型: ${model})` : ''}` }); return; } } catch { } } throw new Error('无法连接到酒馆API'); }
        const h = { 'Content-Type': 'application/json', ...(apiKey && { Authorization: `Bearer ${apiKey}` }) };
        if (!(await fetch(apiUrl.replace(/\/$/, '') + '/models', { headers: h })).ok) throw new Error('连接失败');
        postFrame({ type: "TEST_CONN_RESULT", success: true, message: `连接成功${model ? ` (模型: ${model})` : ''}` });
    } catch (e) { postFrame({ type: "TEST_CONN_RESULT", success: false, message: `连接失败: ${e.message}` }); }
}

async function handleCheckUid({ uid, requestId }) {
    const num = parseInt(uid, 10);
    if (!uid?.trim() || isNaN(num)) return replyErr("CHECK_WORLDBOOK_UID_RESULT", requestId, isNaN(num) ? 'UID必须是数字' : '请输入有效的UID');
    const books = await getCharWorldbooks();
    if (!books.length) return replyErr("CHECK_WORLDBOOK_UID_RESULT", requestId, '当前角色卡没有绑定世界书');
    for (const book of books) {
        const data = await loadWorldInfo(book), entry = data?.entries?.[num];
        if (entry) {
            const keys = Array.isArray(entry.key) ? entry.key : [];
            if (!keys.length) return replyErr("CHECK_WORLDBOOK_UID_RESULT", requestId, `在「${book}」中找到条目 UID ${uid}，但没有主要关键字`);
            return reply("CHECK_WORLDBOOK_UID_RESULT", requestId, { primaryKeys: keys, worldbook: book, comment: entry.comment || '' });
        }
    }
    replyErr("CHECK_WORLDBOOK_UID_RESULT", requestId, `在角色卡绑定的世界书中未找到 UID 为 ${uid} 的条目`);
}

async function handleSendSms({ requestId, contactName, worldbookUid, userMessage, chatHistory, summarizedCount }) {
    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;
        } else if (worldbookUid) {
            const e = await findEntry(worldbookUid);
            if (e?.entry) {
                const c = e.entry.content || '', si = c.indexOf('[SMS_HISTORY_START]');
                charContent = si !== -1 ? c.substring(0, si).trim() : c;
                const [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))) || {}; }
            }
        }

        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(getCommonPromptVars({ contactName, userName, 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: '生成回复失败，请调整重试' });
    } catch (e) { replyErr('SMS_RESULT', requestId, `生成失败: ${e.message}`); }
}

async function handleLoadSmsHistory({ worldbookUid }) {
    if (worldbookUid === CHAR_CARD_UID) { const h = getCharSmsHistory(); return postFrame({ type: 'LOAD_SMS_HISTORY_RESULT', worldbookUid, messages: h?.messages || [], summarizedCount: h?.summarizedCount || 0 }); }
    const store = getOutlineStore(), contact = store?.outlineData?.contacts?.find(c => c.worldbookUid === worldbookUid);
    if (contact?.smsHistory?.messages?.length) return postFrame({ type: 'LOAD_SMS_HISTORY_RESULT', worldbookUid, messages: contact.smsHistory.messages, summarizedCount: contact.smsHistory.summarizedCount || 0 });
    const e = await findEntry(worldbookUid); let msgs = [];
    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())); p?.forEach?.(i => { if (typeof i === 'string' && !i.startsWith('SMS_summary:')) { const idx = i.indexOf(':'); if (idx > 0) msgs.push({ type: i.substring(0, idx) === '{{user}}' ? 'sent' : 'received', text: i.substring(idx + 1) }); } }); } }
    postFrame({ type: 'LOAD_SMS_HISTORY_RESULT', worldbookUid, messages: msgs, summarizedCount: 0 });
}

async function handleSaveSmsHistory({ worldbookUid, messages, contactName, summarizedCount }) {
    if (worldbookUid === CHAR_CARD_UID) { const h = getCharSmsHistory(); if (!h) return; h.messages = Array.isArray(messages) ? messages : []; h.summarizedCount = summarizedCount || 0; if (!h.messages.length) { h.summarizedCount = 0; h.summaries = {}; } saveMetadataDebounced?.(); return; }
    const e = await findEntry(worldbookUid); if (!e) return;
    const { bookName, entry: en, worldData } = e; let c = en.content || ''; const cn = contactName || en.key?.[0] || '角色'; let existSum = '';
    const [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())); existSum = p?.find?.(i => typeof i === 'string' && i.startsWith('SMS_summary:')) || ''; c = c.substring(0, s).trimEnd() + c.substring(ed + 17); }
    if (messages?.length) { const sc = summarizedCount || 0, simp = messages.slice(sc).map(m => `${m.type === 'sent' ? '{{user}}' : cn}:${m.text}`); const arr = existSum ? [existSum, ...simp] : simp; c = c.trimEnd() + `\n\n[SMS_HISTORY_START]\n${JSON.stringify(arr)}\n[SMS_HISTORY_END]`; }
    en.content = c.trim(); await saveWorldInfo(bookName, worldData);
}

async function handleCompressSms({ requestId, worldbookUid, messages, contactName, summarizedCount }) {
    const sc = summarizedCount || 0;
    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);
            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条消息才能进行总结');
            const convText = toSum.map(m => `${m.type === 'sent' ? userName : contactName}：${m.text}`).join('\n');
            const sumKeys = Object.keys(existSum).filter(k => k !== '_count').sort((a, b) => a - b);
            const existText = sumKeys.map(k => `${k}. ${existSum[k]}`).join('\n');
            const parsed = await callLLMJson({ messages: buildSummaryMessages(getCommonPromptVars({ existingSummaryContent: buildExistingSummaryContent(existText), conversationText: convText })), validate: V.sum });
            const sum = parsed?.summary?.trim?.(); if (!sum) return replyErr('COMPRESS_SMS_RESULT', requestId, 'ECHO：总结生成出错，请重试');
            const nextK = Math.max(0, ...Object.keys(existSum).filter(k => k !== '_count').map(k => parseInt(k, 10)).filter(n => !isNaN(n))) + 1;
            existSum[String(nextK)] = sum;
            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条消息才能进行总结');
        const convText = toSum.map(m => `${m.type === 'sent' ? userName : contactName}：${m.text}`).join('\n');
        const sumKeys = Object.keys(existSum).filter(k => k !== '_count').sort((a, b) => a - b);
        const existText = sumKeys.map(k => `${k}. ${existSum[k]}`).join('\n');
        const parsed = await callLLMJson({ messages: buildSummaryMessages(getCommonPromptVars({ 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);
            const nextK = Math.max(0, ...Object.keys(existSum).filter(k => k !== '_count').map(k => parseInt(k, 10)).filter(n => !isNaN(n))) + 1;
            existSum[String(nextK)] = sum;
            const rem = messages.slice(toEnd).map(m => `${m.type === 'sent' ? '{{user}}' : cn}:${m.text}`);
            const arr = [`SMS_summary:${JSON.stringify(existSum)}`, ...rem];
            c = c.trimEnd() + `\n\n[SMS_HISTORY_START]\n${JSON.stringify(arr)}\n[SMS_HISTORY_END]`;
            en.content = c.trim(); await saveWorldInfo(bookName, worldData);
        }
        reply('COMPRESS_SMS_RESULT', requestId, { summary: sum, newSummarizedCount: newSc });
    } catch (e) { replyErr('COMPRESS_SMS_RESULT', requestId, `压缩失败: ${e.message}`); }
}

async function handleCheckStrangerWb({ requestId, strangerName }) {
    const r = await searchEntry(strangerName);
    postFrame({ type: 'CHECK_STRANGER_WORLDBOOK_RESULT', requestId, found: !!r, ...(r && { worldbookUid: r.uid, worldbook: r.bookName, entryName: r.entry.comment || r.entry.key?.[0] || strangerName }) });
}

async function handleGenNpc({ requestId, strangerName, strangerInfo }) {
    try {
        const comm = getCommSettings();
        const ctx = getContext(), char = ctx.characters?.[ctx.characterId];
        if (!char) return replyErr('GENERATE_NPC_RESULT', requestId, '未找到当前角色卡');
        const primary = char.data?.extensions?.world;
        if (!primary || !world_names?.includes(primary)) return replyErr('GENERATE_NPC_RESULT', requestId, '角色卡未绑定世界书，请先绑定世界书');
        const msgs = buildNpcGenerationMessages(getCommonPromptVars({ strangerName, strangerInfo: strangerInfo || '(无描述)' }));
        const npc = await callLLMJson({ messages: msgs, validate: V.npc });
        if (!npc?.name) return replyErr('GENERATE_NPC_RESULT', requestId, 'NPC 生成失败：无法解析 JSON 数据');
        const wd = await loadWorldInfo(primary); if (!wd) return replyErr('GENERATE_NPC_RESULT', requestId, `无法加载世界书: ${primary}`);
        const { createWorldInfoEntry } = await import("../../../../../world-info.js");
        const newE = createWorldInfoEntry(primary, wd); if (!newE) return replyErr('GENERATE_NPC_RESULT', requestId, '创建世界书条目失败');
        Object.assign(newE, { key: npc.aliases || [npc.name], comment: npc.name, content: formatNpcToWorldbookContent(npc), constant: false, selective: true, disable: false, position: typeof comm.npcPosition === 'number' ? comm.npcPosition : 0, order: typeof comm.npcOrder === 'number' ? comm.npcOrder : 100 });
        await saveWorldInfo(primary, wd, true);
        reply('GENERATE_NPC_RESULT', requestId, { success: true, npcData: npc, worldbookUid: String(newE.uid), worldbook: primary });
    } catch (e) { replyErr('GENERATE_NPC_RESULT', requestId, `生成失败: ${e.message}`); }
}

async function handleExtractStrangers({ requestId, existingContacts, existingStrangers }) {
    try {
        const msgs = buildExtractStrangersMessages(getCommonPromptVars({ existingContacts: existingContacts || [], existingStrangers: existingStrangers || [] }));
        const data = await callLLMJson({ messages: msgs, isArray: true, validate: V.arr });
        if (!Array.isArray(data)) return replyErr('EXTRACT_STRANGERS_RESULT', requestId, '提取失败：无法解析 JSON 数据');
        const strangers = data.filter(s => s?.name).map(s => ({ name: s.name, avatar: s.name[0] || '?', color: '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'), location: s.location || '未知', info: s.info || '' }));
        reply('EXTRACT_STRANGERS_RESULT', requestId, { success: true, strangers });
    } catch (e) { replyErr('EXTRACT_STRANGERS_RESULT', requestId, `提取失败: ${e.message}`); }
}

async function handleSceneSwitch({ requestId, prevLocationName, prevLocationInfo, targetLocationName, targetLocationType, targetLocationInfo, playerAction }) {
    try {
        const store = getOutlineStore();
        const msgs = buildSceneSwitchMessages(getCommonPromptVars({ prevLocationName: prevLocationName || '未知地点', prevLocationInfo: prevLocationInfo || '', targetLocationName: targetLocationName || '未知地点', targetLocationType: targetLocationType || 'sub', targetLocationInfo: targetLocationInfo || '', playerAction: playerAction || '' }));
        const data = await callLLMJson({ messages: msgs, validate: V.scene });
        if (!data || !V.scene(data)) return replyErr('SCENE_SWITCH_RESULT', requestId, '场景生成失败：无法解析 JSON 数据');
        const delta = data.review?.deviation?.score_delta || 0, old = store?.deviationScore || 0, newS = Math.min(100, Math.max(0, old + delta));
        if (store) { store.deviationScore = newS; tickSimCountdown(store); }
        const lm = data.local_map || data.scene_setup?.local_map || null;
        reply('SCENE_SWITCH_RESULT', requestId, { success: true, sceneData: { review: data.review, localMap: lm, strangers: [], scoreDelta: delta, newScore: newS } });
    } catch (e) { replyErr('SCENE_SWITCH_RESULT', requestId, `场景切换失败: ${e.message}`); }
}

async function handleExecSlash({ command }) {
    try {
        if (typeof command !== 'string') return;
        for (const line of command.split(/\r?\n/).map(l => l.trim()).filter(Boolean)) {
            if (/^\/(send|sendas|as)\b/i.test(line)) await processCommands(line);
        }
    } catch (e) { console.warn('[Story Outline] Slash command failed:', e); }
}

async function handleSendInvite({ requestId, contactName, contactUid, targetLocation, smsHistory }) {
    try {
        let charC = '';
        if (contactUid) { const es = Object.values(world_info?.entries || world_info || {}); charC = es.find(e => e.uid?.toString() === contactUid.toString())?.content || ''; }
        const msgs = buildInviteMessages(getCommonPromptVars({ contactName, userName: name1 || '{{user}}', targetLocation, smsHistoryContent: buildSmsHistoryContent(smsHistory || ''), characterContent: charC }));
        const data = await callLLMJson({ messages: msgs, validate: V.inv });
        if (typeof data?.invite !== 'boolean') return replyErr('SEND_INVITE_RESULT', requestId, '邀请处理失败：无法解析 JSON 数据');
        reply('SEND_INVITE_RESULT', requestId, { success: true, inviteData: { accepted: data.invite, reply: data.reply, targetLocation } });
    } catch (e) { replyErr('SEND_INVITE_RESULT', requestId, `邀请处理失败: ${e.message}`); }
}

async function handleGenLocalMap({ requestId, outdoorDescription }) {
    try {
        const msgs = buildLocalMapGenMessages(getCommonPromptVars({ outdoorDescription: outdoorDescription || '' }));
        const data = await callLLMJson({ messages: msgs, validate: V.lm });
        if (!data?.inside) return replyErr('GENERATE_LOCAL_MAP_RESULT', requestId, '局部地图生成失败：无法解析 JSON 数据');
        tickSimCountdown(getOutlineStore());
        reply('GENERATE_LOCAL_MAP_RESULT', requestId, { success: true, localMapData: data.inside });
    } catch (e) { replyErr('GENERATE_LOCAL_MAP_RESULT', requestId, `局部地图生成失败: ${e.message}`); }
}

async function handleRefreshLocalMap({ requestId, locationName, currentLocalMap, outdoorDescription }) {
    try {
        const store = getOutlineStore();
        const msgs = buildLocalMapRefreshMessages(getCommonPromptVars({ locationName: locationName || store?.playerLocation || '未知地点', locationInfo: currentLocalMap?.description || '', currentLocalMap: currentLocalMap || null, outdoorDescription: outdoorDescription || '' }));
        const data = await callLLMJson({ messages: msgs, validate: V.lm });
        if (!data?.inside) return replyErr('REFRESH_LOCAL_MAP_RESULT', requestId, '局部地图刷新失败：无法解析 JSON 数据');
        tickSimCountdown(store);
        reply('REFRESH_LOCAL_MAP_RESULT', requestId, { success: true, localMapData: data.inside });
    } catch (e) { replyErr('REFRESH_LOCAL_MAP_RESULT', requestId, `局部地图刷新失败: ${e.message}`); }
}

async function handleGenLocalScene({ requestId, locationName, locationInfo }) {
    try {
        const store = getOutlineStore();
        const msgs = buildLocalSceneGenMessages(getCommonPromptVars({ locationName: locationName || store?.playerLocation || '未知地点', locationInfo: locationInfo || '' }));
        const data = await callLLMJson({ messages: msgs, validate: V.lscene });
        if (!data || !V.lscene(data)) return replyErr('GENERATE_LOCAL_SCENE_RESULT', requestId, '局部剧情生成失败：无法解析 JSON 数据');
        tickSimCountdown(store);
        const ssf = data.side_story || null;
        const intro = (ssf?.Incident || '').trim();
        const ss = ssf ? { Facade: ssf.Facade || '', Undercurrent: ssf.Undercurrent || '' } : null;
        reply('GENERATE_LOCAL_SCENE_RESULT', requestId, { success: true, sceneSetup: { sideStory: ss, review: data.review || null }, introduce: intro, loc: locationName });
    } catch (e) { replyErr('GENERATE_LOCAL_SCENE_RESULT', requestId, `局部剧情生成失败: ${e.message}`); }
}

async function handleGenWorld({ requestId, playerRequests }) {
    try {
        const mode = getGlobalSettings().mode || 'story', store = getOutlineStore();

        // 递归查找函数 - 在任意层级找到目标键
        const deepFind = (obj, key) => {
            if (!obj || typeof obj !== 'object') return null;
            if (obj[key] !== undefined) return obj[key];
            for (const v of Object.values(obj)) {
                const found = deepFind(v, key);
                if (found !== null) return found;
            }
            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(getCommonPromptVars({ playerRequests, mode: 'assist' }));
            let wd = await callLLMJson({ messages: msgs, validate: V.wga });
            wd = normalizeStep2Maps(wd);
            if (!wd?.maps?.outdoor || !Array.isArray(wd.maps.outdoor.nodes)) return replyErr('GENERATE_WORLD_RESULT', requestId, '生成失败：返回数据缺少地图节点');
            if (store) { Object.assign(store, { stage: 0, deviationScore: 0, simulationTarget: randRange(3, 7) }); store.outlineData = { ...wd }; saveMetadataDebounced?.(); sendSimStateOnly(); }
            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(getCommonPromptVars({ 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(getCommonPromptVars({ playerRequests, step1Data: s1d }));
        let s2d = await callLLMJson({ messages: s2m, validate: V.wg2 });
        s2d = normalizeStep2Maps(s2d);
        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, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); sendSimStateOnly(); }
        reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: final });
    } catch (e) { replyErr('GENERATE_WORLD_RESULT', requestId, `生成失败: ${e.message}`); }
}

async function handleRetryStep2({ requestId }) {
    try {
        if (!step1Cache?.step1Data?.meta) return replyErr('GENERATE_WORLD_RESULT', requestId, 'Step 2 重试失败：缺少 Step 1 数据，请重新开始生成');
        const 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(getCommonPromptVars({ playerRequests: pr, step1Data: s1d }));
        let s2d = await callLLMJson({ messages: s2m, validate: V.wg2 });
        s2d = normalizeStep2Maps(s2d);
        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, simulationTarget: randRange(3, 7) }); store.outlineData = final; saveMetadataDebounced?.(); sendSimStateOnly(); }
        reply('GENERATE_WORLD_RESULT', requestId, { success: true, worldData: final });
    } catch (e) { replyErr('GENERATE_WORLD_RESULT', requestId, `Step 2 重试失败: ${e.message}`); }
}

async function handleSimWorld({ requestId, currentData, isAuto }) {
    try {
        const store = getOutlineStore();
        const mode = getGlobalSettings().mode || 'story';
        const msgs = buildWorldSimMessages(getCommonPromptVars({ currentWorldData: currentData || '{}' }));
        const data = await callLLMJson({ messages: msgs, validate: V.w });
        if (!data || !V.w(data)) return replyErr('SIMULATE_WORLD_RESULT', requestId, mode === 'assist' ? '世界推演失败：无法解析 JSON 数据（需包含 world 或 maps 字段）' : '世界推演失败：无法解析 JSON 数据');
        const orig = safe(() => JSON.parse(currentData)) || {}, merged = mergeSimData(orig, data);
        if (store) { store.stage = (store.stage || 0) + 1; store.simulationTarget = randRange(3, 7); saveMetadataDebounced?.(); sendSimStateOnly(); }
        reply('SIMULATE_WORLD_RESULT', requestId, { success: true, simData: merged, isAuto: !!isAuto });
    } catch (e) { replyErr('SIMULATE_WORLD_RESULT', requestId, `推演失败: ${e.message}`); }
}

function handleSaveSettings(d) {
    if (d.globalSettings) saveGlobalSettings(d.globalSettings);
    if (d.commSettings) saveCommSettings(d.commSettings);
    const store = getOutlineStore();
    if (store) {
        ['stage', 'deviationScore', 'simulationTarget', 'playerLocation'].forEach(k => { if (d[k] !== undefined) store[k] = d[k]; });
        if (d.dataChecked) store.dataChecked = d.dataChecked;
        if (d.allData) store.outlineData = d.allData;
        store.updatedAt = Date.now();
        saveMetadataDebounced?.();
    }
    injectOutline();
    try {
        StoryOutlineStorage?.set?.('settings', {
            globalSettings: getGlobalSettings(),
            commSettings: getCommSettings(),
        });
    } catch { }
}

async function handleSavePrompts(d) {
    // Back-compat: full payload (old iframe)
    if (d?.promptConfig) {
        const payload = setPromptConfig?.(d.promptConfig, false) || d.promptConfig;
        try { await StoryOutlineStorage?.set?.('promptConfig', payload); } catch { }
        postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
        return;
    }

    // New: incremental update by key
    const key = d?.key;
    if (!key) return;

    let current = null;
    try { current = await StoryOutlineStorage?.get?.('promptConfig', null); } catch { }
    const next = (current && typeof current === 'object') ? {
        jsonTemplates: { ...(current.jsonTemplates || {}) },
        promptSources: { ...(current.promptSources || {}) },
    } : { jsonTemplates: {}, promptSources: {} };

    if (d?.reset) {
        delete next.promptSources[key];
        delete next.jsonTemplates[key];
    } else {
        if (d?.prompt && typeof d.prompt === 'object') next.promptSources[key] = d.prompt;
        if ('jsonTemplate' in (d || {})) {
            if (d.jsonTemplate == null) delete next.jsonTemplates[key];
            else next.jsonTemplates[key] = String(d.jsonTemplate ?? '');
        }
    }

    const payload = setPromptConfig?.(next, false) || next;
    try { await StoryOutlineStorage?.set?.('promptConfig', payload); } catch { }
    postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
}

function handleSaveContacts(d) {
    const store = getOutlineStore(); if (!store) return;
    store.outlineData ||= {};
    if (d.contacts) store.outlineData.contacts = d.contacts;
    if (d.strangers) store.outlineData.strangers = d.strangers;
    store.updatedAt = Date.now();
    saveMetadataDebounced?.();
    injectOutline();
}

function handleSaveAllData(d) {
    const store = getOutlineStore();
    if (store && d.allData) {
        store.outlineData = d.allData;
        if (d.playerLocation !== undefined) store.playerLocation = d.playerLocation;
        store.updatedAt = Date.now();
        saveMetadataDebounced?.();
        injectOutline();
    }
}

function handleSaveCharSmsHistory(d) {
    const h = getCharSmsHistory();
    if (!h) return;
    const sums = d?.summaries ?? d?.history?.summaries;
    if (!sums || typeof sums !== 'object' || Array.isArray(sums)) return;
    h.summaries = sums;
    saveMetadataDebounced?.();
    injectOutline();
}

// 处理器映射
const handlers = {
    FRAME_READY: () => { frameReady = true; flushPending(); loadAndSend(); },
    CLOSE_PANEL: hideOverlay,
    SAVE_MAP_DATA: d => { const s = getOutlineStore(); if (s && d.mapData) { s.mapData = d.mapData; s.updatedAt = Date.now(); saveMetadataDebounced?.(); } },
    GET_SETTINGS: sendSettings,
    SAVE_SETTINGS: handleSaveSettings,
    SAVE_PROMPTS: handleSavePrompts,
    SAVE_CONTACTS: handleSaveContacts,
    SAVE_ALL_DATA: handleSaveAllData,
    FETCH_MODELS: handleFetchModels,
    TEST_CONNECTION: handleTestConn,
    CHECK_WORLDBOOK_UID: handleCheckUid,
    SEND_SMS: handleSendSms,
    LOAD_SMS_HISTORY: handleLoadSmsHistory,
    SAVE_SMS_HISTORY: handleSaveSmsHistory,
    SAVE_CHAR_SMS_HISTORY: handleSaveCharSmsHistory,
    COMPRESS_SMS: handleCompressSms,
    CHECK_STRANGER_WORLDBOOK: handleCheckStrangerWb,
    GENERATE_NPC: handleGenNpc,
    EXTRACT_STRANGERS: handleExtractStrangers,
    SCENE_SWITCH: handleSceneSwitch,
    EXECUTE_SLASH_COMMAND: handleExecSlash,
    SEND_INVITE: handleSendInvite,
    GENERATE_WORLD: handleGenWorld,
    RETRY_WORLD_GEN_STEP2: handleRetryStep2,
    SIMULATE_WORLD: handleSimWorld,
    GENERATE_LOCAL_MAP: handleGenLocalMap,
    REFRESH_LOCAL_MAP: handleRefreshLocalMap,
    GENERATE_LOCAL_SCENE: handleGenLocalScene
};

const handleMsg = (event) => {
    const iframe = document.getElementById("xiaobaix-story-outline-iframe");
    if (!isTrustedMessage(event, iframe, "LittleWhiteBox-OutlineFrame")) return;
    const { data } = event;
    handlers[data.type]?.(data);
};

// ==================== 10. UI管理 ====================

/** 指针拖拽 */
function setupDrag(el, { onStart, onMove, onEnd, shouldHandle }) {
    if (!el) return;
    let state = null;
    el.addEventListener('pointerdown', e => { if (shouldHandle && !shouldHandle()) return; e.preventDefault(); e.stopPropagation(); state = onStart(e); state.pointerId = e.pointerId; el.setPointerCapture(e.pointerId); });
    el.addEventListener('pointermove', e => state && onMove(e, state));
    const end = () => { if (!state) return; onEnd?.(state); try { el.releasePointerCapture(state.pointerId); } catch { } state = null; };
    ['pointerup', 'pointercancel', 'lostpointercapture'].forEach(ev => el.addEventListener(ev, end));
}

/** 创建Overlay */
function createOverlay() {
    if (overlayCreated) return;
    overlayCreated = true;
    document.body.appendChild($(buildOverlayHtml(IFRAME_PATH))[0]);
    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) }; },
        onMove(e, s) { wrap.style.left = Math.max(0, Math.min(overlay.clientWidth - wrap.offsetWidth, s.sl + e.clientX - s.sx)) + 'px'; wrap.style.top = Math.max(0, Math.min(overlay.clientHeight - wrap.offsetHeight, s.st + e.clientY - s.sy)) + 'px'; },
        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('')
    });

    // 移动端
    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('')
    });

    // Guarded by isTrustedMessage (origin + source).
    // eslint-disable-next-line no-restricted-syntax
    window.addEventListener("message", handleMsg);
}

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 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(); 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;

function addBtnToMsg(mesId) {
    if (!getSettings().storyOutline?.enabled) return;
    const msg = document.querySelector(`#chat .mes[mesid="${mesId}"]`);
    if (!msg || msg.querySelector('.xiaobaix-story-outline-btn')) return;
    const btn = document.createElement('div');
    btn.className = 'mes_btn xiaobaix-story-outline-btn';
    btn.title = '小白板';
    btn.dataset.mesid = mesId;
    btn.innerHTML = '<i class="fa-regular fa-map"></i>';
    btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); if (!getSettings().storyOutline?.enabled) return; showOverlay(); loadAndSend(); });
    if (window.registerButtonToSubContainer?.(mesId, btn)) return;
    msg.querySelector('.flex-container.flex1.alignitemscenter')?.appendChild(btn);
}

function initBtns() {
    if (!getSettings().storyOutline?.enabled) return;
    $("#chat .mes").each((_, el) => { const id = el.getAttribute("mesid"); if (id != null) addBtnToMsg(id); });
}

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,
        event_types.MESSAGE_RECEIVED,
        event_types.MESSAGE_UPDATED,
        event_types.MESSAGE_SWIPED,
        event_types.MESSAGE_EDITED
    ], handler);

    setupSTEvents();
}

function cleanup() {
    events.cleanup();
    eventsRegistered = false;
    $(".xiaobaix-story-outline-btn").remove();
    hideOverlay();
    overlayCreated = false; frameReady = false; pendingMsgs = [];
    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();
        initBtns();
        injectOutline();
    } else {
        cleanup();
    }
});

document.addEventListener('xiaobaixEnabledChanged', e => {
    if (!e?.detail?.enabled) {
        cleanup();
    } else if (getSettings().storyOutline?.enabled) {
        registerEvents();
        initBtns();
        injectOutline();
    }
});

// ==================== 初始化 ====================

async function initPromptConfigFromServer() {
    try {
        const cfg = await StoryOutlineStorage?.get?.('promptConfig', null);
        if (!cfg) return;
        setPromptConfig?.(cfg, false);
        postFrame({ type: "PROMPT_CONFIG_UPDATED", promptConfig: getPromptConfigPayload?.() });
    } catch { }
}

async function initSettingsFromServer() {
    try {
        const s = await StoryOutlineStorage?.get?.('settings', null);
        if (!s || typeof s !== 'object') return;
        if (s.globalSettings) saveGlobalSettings(s.globalSettings);
        if (s.commSettings) saveCommSettings(s.commSettings);
    } catch { }
}

jQuery(() => {
    if (!getSettings().storyOutline?.enabled) return;
    initSettingsFromServer();
    initPromptConfigFromServer();
    registerEvents();
    setTimeout(injectOutline, 200);
    window.registerModuleCleanup?.('storyOutline', cleanup);
});

export { cleanup };
