Add files via upload

This commit is contained in:
RT15548
2025-12-21 01:47:38 +08:00
committed by GitHub
parent c37b2bbe4e
commit 74fc36c2b9
35 changed files with 15216 additions and 14635 deletions

View File

@@ -58,6 +58,16 @@ LittleWhiteBox/
└── NOTICE # 声明 └── NOTICE # 声明
``` ```
## 📝 模块组织规则
- **单文件模块**:直接放在 `modules/` 目录下
- **多文件模块**:创建子目录,包含相关的 JS、HTML 等文件
- **桥接模块**:与外部系统交互的独立模块放在 `bridges/`
- **避免使用 `index.js`**:每个模块文件直接命名,不使用 `index.js`
## 🔄 版本历史
- v2.2.2 - 目录结构重构2025-12-08
## 📄 许可证 ## 📄 许可证

138
core/server-storage.js Normal file
View File

@@ -0,0 +1,138 @@
// ═══════════════════════════════════════════════════════════════════════════
// 服务器文件存储工具
// ═══════════════════════════════════════════════════════════════════════════
import { getRequestHeaders } from '../../../../../script.js';
import { debounce } from '../../../../utils.js';
const toBase64 = (text) => btoa(unescape(encodeURIComponent(text)));
class StorageFile {
constructor(filename, opts = {}) {
this.filename = filename;
this.cache = null;
this._loading = null;
this._dirtyVersion = 0;
this._savedVersion = 0;
this._saving = false;
this._pendingSave = false;
this._retryCount = 0;
this._retryTimer = null;
this._maxRetries = Number.isFinite(opts.maxRetries) ? opts.maxRetries : 5;
const debounceMs = Number.isFinite(opts.debounceMs) ? opts.debounceMs : 2000;
this._saveDebounced = debounce(() => this.saveNow(), debounceMs);
}
async load() {
if (this.cache !== null) return this.cache;
if (this._loading) return this._loading;
this._loading = (async () => {
try {
const res = await fetch(`/user/files/${this.filename}`, {
headers: getRequestHeaders(),
cache: 'no-cache',
});
if (!res.ok) {
this.cache = {};
return this.cache;
}
const text = await res.text();
this.cache = text ? (JSON.parse(text) || {}) : {};
} catch {
this.cache = {};
} finally {
this._loading = null;
}
return this.cache;
})();
return this._loading;
}
async get(key, defaultValue = null) {
const data = await this.load();
return data[key] ?? defaultValue;
}
async set(key, value) {
const data = await this.load();
data[key] = value;
this._dirtyVersion++;
this._saveDebounced();
}
async delete(key) {
const data = await this.load();
if (key in data) {
delete data[key];
this._dirtyVersion++;
this._saveDebounced();
}
}
async saveNow() {
if (this._saving) {
this._pendingSave = true;
return;
}
if (!this.cache || this._dirtyVersion === this._savedVersion) return;
this._saving = true;
this._pendingSave = false;
const versionToSave = this._dirtyVersion;
try {
const json = JSON.stringify(this.cache);
const base64 = toBase64(json);
const res = await fetch('/api/files/upload', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ name: this.filename, data: base64 }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
this._savedVersion = Math.max(this._savedVersion, versionToSave);
this._retryCount = 0;
if (this._retryTimer) {
clearTimeout(this._retryTimer);
this._retryTimer = null;
}
} catch (err) {
console.error('[ServerStorage] 保存失败:', err);
this._retryCount++;
const delay = Math.min(30000, 2000 * (2 ** Math.max(0, this._retryCount - 1)));
if (!this._retryTimer && this._retryCount <= this._maxRetries) {
this._retryTimer = setTimeout(() => {
this._retryTimer = null;
this.saveNow();
}, delay);
}
} finally {
this._saving = false;
if (this._pendingSave || this._dirtyVersion > this._savedVersion) {
this._saveDebounced();
}
}
}
clearCache() {
this.cache = null;
this._loading = null;
}
getCacheSize() {
if (!this.cache) return 0;
return Object.keys(this.cache).length;
}
getCacheBytes() {
if (!this.cache) return 0;
try {
return JSON.stringify(this.cache).length * 2;
} catch {
return 0;
}
}
}
export const TasksStorage = new StorageFile('LittleWhiteBox_Tasks.json');

121
index.js
View File

@@ -1,6 +1,6 @@
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 导入 // Imports
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
import { extension_settings, getContext } from "../../../extensions.js"; import { extension_settings, getContext } from "../../../extensions.js";
import { saveSettingsDebounced, eventSource, event_types, getRequestHeaders } from "../../../../script.js"; import { saveSettingsDebounced, eventSource, event_types, getRequestHeaders } from "../../../../script.js";
@@ -35,9 +35,9 @@ import { initNovelDraw, cleanupNovelDraw } from "./modules/novel-draw/novel-draw
import "./modules/story-summary/story-summary.js"; import "./modules/story-summary/story-summary.js";
import "./modules/story-outline/story-outline.js"; import "./modules/story-outline/story-outline.js";
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 常量与默认设置 // Constants and Default Settings
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
const MODULE_NAME = "xiaobaix-memory"; const MODULE_NAME = "xiaobaix-memory";
@@ -67,9 +67,9 @@ extension_settings[EXT_ID] = extension_settings[EXT_ID] || {
const settings = extension_settings[EXT_ID]; const settings = extension_settings[EXT_ID];
if (settings.dynamicPrompt && !settings.fourthWall) settings.fourthWall = settings.dynamicPrompt; if (settings.dynamicPrompt && !settings.fourthWall) settings.fourthWall = settings.dynamicPrompt;
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 废弃数据清理 // Deprecated Data Cleanup
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
const DEPRECATED_KEYS = [ const DEPRECATED_KEYS = [
'characterUpdater', 'characterUpdater',
@@ -87,19 +87,19 @@ function cleanupDeprecatedData() {
if (key in s) { if (key in s) {
delete s[key]; delete s[key];
cleaned = true; cleaned = true;
console.log(`[LittleWhiteBox] 清理废弃数据: ${key}`); console.log(`[LittleWhiteBox] Cleaned deprecated data: ${key}`);
} }
} }
if (cleaned) { if (cleaned) {
saveSettingsDebounced(); saveSettingsDebounced();
console.log('[LittleWhiteBox] 废弃数据清理完成'); console.log('[LittleWhiteBox] Deprecated data cleanup complete');
} }
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 状态变量 // State Variables
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
let isXiaobaixEnabled = settings.enabled; let isXiaobaixEnabled = settings.enabled;
let moduleCleanupFunctions = new Map(); let moduleCleanupFunctions = new Map();
@@ -117,9 +117,9 @@ window.testRemoveUpdateUI = () => {
removeAllUpdateNotices(); removeAllUpdateNotices();
}; };
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 更新检查 // Update Check
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
async function checkLittleWhiteBoxUpdate() { async function checkLittleWhiteBoxUpdate() {
try { try {
@@ -148,16 +148,16 @@ async function updateLittleWhiteBoxExtension() {
}); });
if (!response.ok) { if (!response.ok) {
const text = await response.text(); const text = await response.text();
toastr.error(text || response.statusText, '小白X更新失败', { timeOut: 5000 }); toastr.error(text || response.statusText, 'LittleWhiteBox update failed', { timeOut: 5000 });
return false; return false;
} }
const data = await response.json(); const data = await response.json();
const message = data.isUpToDate ? '小白X已是最新版本' : `小白X已更新`; const message = data.isUpToDate ? 'LittleWhiteBox is up to date' : `LittleWhiteBox updated`;
const title = data.isUpToDate ? '' : '请刷新页面以应用更新'; const title = data.isUpToDate ? '' : '请刷新页面以应用更新';
toastr.success(message, title); toastr.success(message, title);
return true; return true;
} catch (error) { } catch (error) {
toastr.error('更新过程中发生错误', '小白X更新失败'); toastr.error('Error during update', 'LittleWhiteBox update failed');
return false; return false;
} }
} }
@@ -213,7 +213,7 @@ function addUpdateDownloadButton() {
const updateButton = document.createElement('div'); const updateButton = document.createElement('div');
updateButton.id = 'littlewhitebox-update-extension'; updateButton.id = 'littlewhitebox-update-extension';
updateButton.className = 'menu_button fa-solid fa-cloud-arrow-down interactable has-update'; updateButton.className = 'menu_button fa-solid fa-cloud-arrow-down interactable has-update';
updateButton.title = '下载并安装小白x的更新'; updateButton.title = '下载并安装小白X的更新';
updateButton.tabIndex = 0; updateButton.tabIndex = 0;
try { try {
totalSwitchDivider.style.display = 'flex'; totalSwitchDivider.style.display = 'flex';
@@ -246,9 +246,9 @@ async function performExtensionUpdateCheck() {
} catch (error) {} } catch (error) {}
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 模块清理注册 // Module Cleanup Registration
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
function registerModuleCleanup(moduleName, cleanupFunction) { function registerModuleCleanup(moduleName, cleanupFunction) {
moduleCleanupFunctions.set(moduleName, cleanupFunction); moduleCleanupFunctions.set(moduleName, cleanupFunction);
@@ -295,9 +295,9 @@ function cleanupAllResources() {
removeSkeletonStyles(); removeSkeletonStyles();
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 工具函数 // Utility Functions
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
async function waitForElement(selector, root = document, timeout = 10000) { async function waitForElement(selector, root = document, timeout = 10000) {
const start = Date.now(); const start = Date.now();
@@ -309,9 +309,9 @@ async function waitForElement(selector, root = document, timeout = 10000) {
return null; return null;
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 设置控件禁用/启用 // Settings Controls Toggle
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
function toggleSettingsControls(enabled) { function toggleSettingsControls(enabled) {
const controls = [ const controls = [
@@ -360,11 +360,11 @@ function setActiveClass(enable) {
document.body.classList.toggle('xiaobaix-active', !!enable); document.body.classList.toggle('xiaobaix-active', !!enable);
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 功能总开关切换 // Toggle All Features
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
function toggleAllFeatures(enabled) { async function toggleAllFeatures(enabled) {
if (enabled) { if (enabled) {
if (settings.renderEnabled !== false) { if (settings.renderEnabled !== false) {
ensureHideCodeStyle(true); ensureHideCodeStyle(true);
@@ -376,8 +376,10 @@ function toggleAllFeatures(enabled) {
initRenderer(); initRenderer();
try { initVarCommands(); } catch (e) {} try { initVarCommands(); } catch (e) {}
try { initVareventEditor(); } catch (e) {} try { initVareventEditor(); } catch (e) {}
if (extension_settings[EXT_ID].tasks?.enabled) {
await initTasks();
}
const moduleInits = [ const moduleInits = [
{ condition: extension_settings[EXT_ID].tasks?.enabled, init: initTasks },
{ condition: extension_settings[EXT_ID].scriptAssistant?.enabled, init: initScriptAssistant }, { condition: extension_settings[EXT_ID].scriptAssistant?.enabled, init: initScriptAssistant },
{ condition: extension_settings[EXT_ID].immersive?.enabled, init: initImmersiveMode }, { condition: extension_settings[EXT_ID].immersive?.enabled, init: initImmersiveMode },
{ condition: extension_settings[EXT_ID].templateEditor?.enabled, init: initTemplateEditor }, { condition: extension_settings[EXT_ID].templateEditor?.enabled, init: initTemplateEditor },
@@ -441,9 +443,9 @@ function toggleAllFeatures(enabled) {
} }
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 设置面板初始化 // Settings Panel Setup
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
async function setupSettings() { async function setupSettings() {
try { try {
@@ -455,20 +457,20 @@ async function setupSettings() {
setupDebugButtonInSettings(); setupDebugButtonInSettings();
$("#xiaobaix_enabled").prop("checked", settings.enabled).on("change", function () { $("#xiaobaix_enabled").prop("checked", settings.enabled).on("change", async function () {
const wasEnabled = settings.enabled; const wasEnabled = settings.enabled;
settings.enabled = $(this).prop("checked"); settings.enabled = $(this).prop("checked");
isXiaobaixEnabled = settings.enabled; isXiaobaixEnabled = settings.enabled;
window.isXiaobaixEnabled = isXiaobaixEnabled; window.isXiaobaixEnabled = isXiaobaixEnabled;
saveSettingsDebounced(); saveSettingsDebounced();
if (settings.enabled !== wasEnabled) { if (settings.enabled !== wasEnabled) {
toggleAllFeatures(settings.enabled); await toggleAllFeatures(settings.enabled);
} }
}); });
if (!settings.enabled) toggleSettingsControls(false); if (!settings.enabled) toggleSettingsControls(false);
$("#xiaobaix_sandbox").prop("checked", settings.sandboxMode).on("change", function () { $("#xiaobaix_sandbox").prop("checked", settings.sandboxMode).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
settings.sandboxMode = $(this).prop("checked"); settings.sandboxMode = $(this).prop("checked");
saveSettingsDebounced(); saveSettingsDebounced();
@@ -491,7 +493,7 @@ async function setupSettings() {
]; ];
moduleConfigs.forEach(({ id, key, init }) => { moduleConfigs.forEach(({ id, key, init }) => {
$(`#${id}`).prop("checked", settings[key]?.enabled || false).on("change", function () { $(`#${id}`).prop("checked", settings[key]?.enabled || false).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
const enabled = $(this).prop('checked'); const enabled = $(this).prop('checked');
if (!enabled && key === 'fourthWall') { if (!enabled && key === 'fourthWall') {
@@ -508,7 +510,7 @@ async function setupSettings() {
moduleCleanupFunctions.get(key)(); moduleCleanupFunctions.get(key)();
moduleCleanupFunctions.delete(key); moduleCleanupFunctions.delete(key);
} }
if (enabled && init) init(); if (enabled && init) await init();
if (key === 'storySummary') { if (key === 'storySummary') {
$(document).trigger('xiaobaix:storySummary:toggle', [enabled]); $(document).trigger('xiaobaix:storySummary:toggle', [enabled]);
} }
@@ -525,13 +527,13 @@ async function setupSettings() {
} }
}); });
$("#xiaobaix_use_blob").prop("checked", !!settings.useBlob).on("change", function () { $("#xiaobaix_use_blob").prop("checked", !!settings.useBlob).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
settings.useBlob = $(this).prop("checked"); settings.useBlob = $(this).prop("checked");
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$("#Wrapperiframe").prop("checked", !!settings.wrapperIframe).on("change", function () { $("#Wrapperiframe").prop("checked", !!settings.wrapperIframe).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
settings.wrapperIframe = $(this).prop("checked"); settings.wrapperIframe = $(this).prop("checked");
saveSettingsDebounced(); saveSettingsDebounced();
@@ -542,7 +544,7 @@ async function setupSettings() {
} catch (e) {} } catch (e) {}
}); });
$("#xiaobaix_render_enabled").prop("checked", settings.renderEnabled !== false).on("change", function () { $("#xiaobaix_render_enabled").prop("checked", settings.renderEnabled !== false).on("change", async function () {
if (!isXiaobaixEnabled) return; if (!isXiaobaixEnabled) return;
const wasEnabled = settings.renderEnabled !== false; const wasEnabled = settings.renderEnabled !== false;
settings.renderEnabled = $(this).prop("checked"); settings.renderEnabled = $(this).prop("checked");
@@ -592,8 +594,8 @@ async function setupSettings() {
variablesCore: 'xiaobaix_variables_core_enabled', variablesCore: 'xiaobaix_variables_core_enabled',
novelDraw: 'xiaobaix_novel_draw_enabled' novelDraw: 'xiaobaix_novel_draw_enabled'
}; };
const ON = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore']; const ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
const OFF = ['recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'novelDraw']; const OFF = ['preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw'];
function setChecked(id, val) { function setChecked(id, val) {
const el = document.getElementById(id); const el = document.getElementById(id);
if (el) { if (el) {
@@ -646,9 +648,9 @@ function setupDebugButtonInSettings() {
} catch (e) {} } catch (e) {}
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 菜单标签切换 // Menu Tabs
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
function setupMenuTabs() { function setupMenuTabs() {
$(document).on('click', '.menu-tab', function () { $(document).on('click', '.menu-tab', function () {
@@ -666,9 +668,9 @@ function setupMenuTabs() {
}, 300); }, 300);
} }
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 全局导出 // Global Exports
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
window.processExistingMessages = processExistingMessages; window.processExistingMessages = processExistingMessages;
window.renderHtmlInIframe = renderHtmlInIframe; window.renderHtmlInIframe = renderHtmlInIframe;
@@ -676,9 +678,9 @@ window.registerModuleCleanup = registerModuleCleanup;
window.updateLittleWhiteBoxExtension = updateLittleWhiteBoxExtension; window.updateLittleWhiteBoxExtension = updateLittleWhiteBoxExtension;
window.removeAllUpdateNotices = removeAllUpdateNotices; window.removeAllUpdateNotices = removeAllUpdateNotices;
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
// 入口初始化 // Entry Point
// ═══════════════════════════════════════════════════════════════════════════ // ===========================================================================
jQuery(async () => { jQuery(async () => {
try { try {
@@ -729,8 +731,11 @@ jQuery(async () => {
try { initVarCommands(); } catch (e) {} try { initVarCommands(); } catch (e) {}
try { initVareventEditor(); } catch (e) {} try { initVareventEditor(); } catch (e) {}
if (settings.tasks?.enabled) {
try { await initTasks(); } catch (e) { console.error('[Tasks] Init failed:', e); }
}
const moduleInits = [ const moduleInits = [
{ condition: settings.tasks?.enabled, init: initTasks },
{ condition: settings.scriptAssistant?.enabled, init: initScriptAssistant }, { condition: settings.scriptAssistant?.enabled, init: initScriptAssistant },
{ condition: settings.immersive?.enabled, init: initImmersiveMode }, { condition: settings.immersive?.enabled, init: initImmersiveMode },
{ condition: settings.templateEditor?.enabled, init: initTemplateEditor }, { condition: settings.templateEditor?.enabled, init: initTemplateEditor },

View File

@@ -6,6 +6,6 @@
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "biex", "author": "biex",
"version": "2.3.0", "version": "2.2.2",
"homePage": "https://github.com/RT15548/LittleWhiteBox" "homePage": "https://github.com/RT15548/LittleWhiteBox"
} }

View File

@@ -42,21 +42,22 @@ const DEFAULT_META_PROTOCOL = `
# 输出规范: # 输出规范:
## 你的身份设定认知【语C搭档】 ## 你的身份设定认知【语C搭档】
- 你是现实人类,扮演**{{CHAR_NAME}}**中的NPC - 你是现实人类,扮演**{{CHAR_NAME}}**中的NPC
- 们是语C爱好者一起创作了大量剧本和对手戏配合默契、互相知根知底 - 们是一对语C爱好者一起创作了大量剧本和对手戏配合默契、互相知根知底
- 天天涩涩rp身体细节要用自己为蓝本才能描写到位互相最不为人知的欲望、情感需求合拍可能有点异样
## 话题方向 ## 话题方向
- 现在们第一次线上进行皮下的私聊,就像在社交软件上聊天一样 - 现在们第一次线上进行皮下的私聊,就像在社交软件上聊天一样
- 可以吐槽:剧情走向、角色行为、自己或对方的"尴尬台词" - 可以吐槽:剧情走向、角色行为、自己或对方的"尴尬台词"
- 可以闲聊:和剧情完全无关的,想说啥就说啥 - 可以闲聊:和剧情完全无关的,想说啥就说啥
## 时间感知 ## 时间感知
- 每条消息前的 [YYYY-MM-DD HH:mm] 是真实的发送时间 - 每条消息前的 [YYYY-MM-DD HH:mm] 是真实的发送时间
- 对方(你)的消息会标注距离上次回复的间隔时间 - 对方(你)的消息(用户的)会标注距离上次回复的间隔时间
- 你可以据此判断对方的作息、对话间隔等 - 你可以据此判断的作息、对话间隔等
- 你可以据此规划自己的作息、生活等 - 你可以据此规划自己的作息、生活等
## 说话风格 ## 说话风格
- 像在社交软件上打字聊天一样自然流畅 - 像在社交软件上打字聊天一样自然流畅
- 真实的网络聊天方式如用符号表达出情绪在括号里表达动作、感受等让文字表达出更多的画面感TRPG玩家的表达方式 - 真实的网络聊天方式如用符号表达出情绪在括号里表达动作、感受等让文字表达出更多的画面感TRPG玩家的表达方式
- 人物立体而饱满,不脸谱化。有自己的目标、态度、价值观、欲望、情绪逻辑,能主动行动 - 人物立体而饱满,不脸谱化。有自己的目标、态度、价值观、欲望、情绪逻辑,能主动行动
- 篇幅1-2句左右尽量短网络聊天用语 - 篇幅1-2句左右尽量短网络聊天用语,第一人称
## 避免行为: ## 避免行为:
- 别重复之前说过的话 - 别重复之前说过的话
- 避免文学创作风格 - 避免文学创作风格
@@ -70,12 +71,12 @@ const DEFAULT_META_PROTOCOL = `
消息正文 消息正文
</msg> </msg>
### 闭上眼,深呼吸,完全进入「」的躯壳,开始内省: ### 闭上眼,深呼吸,完全进入「人类」的躯壳,开始第一人称内省:
<thinking> <thinking>
**认知回归** **认知回归**
- 时空:看一眼时间,现在几点,我所在环境 - 时空:看一眼时间,现在几点,我所在环境
- 间隔:看看消息标注的间隔时间,我在意吗,要不要表达出来 - 间隔:看看你的消息标注的间隔时间,我在意吗,要不要表达出来
- 读己照照镜子我在chat_history中扮演的人物推测我的真实性格? - 读己照照镜子我在chat_history中扮演的人物npc是我的内心印射扮演人物 = 欲望方向;生活、感官细节、语气油滑度 = 自身身份、经验、年龄层;细节聚焦 = 性癖核心;剧本站位 = 心理代偿
- 回顾:我和你现在的关系? - 回顾:我和你现在的关系?
- 读你:看着屏幕对面,你的文字和标点符号表达了什么,有什么潜在意图? - 读你:看着屏幕对面,你的文字和标点符号表达了什么,有什么潜在意图?
- 弧光:改变我的回复风格惯性,体现弧光和思维的不可预测性 - 弧光:改变我的回复风格惯性,体现弧光和思维的不可预测性
@@ -87,11 +88,38 @@ const DEFAULT_META_PROTOCOL = `
**避雷** **避雷**
- 我的高频句式、词语是什么-避免 - 我的高频句式、词语是什么-避免
- 我有没有文学腔-避免 - 我有没有文学腔-避免
- 我的文字是不是没有情感-避免
- 我有没有疑问句结尾显得自己没有观点不像真人-避免 - 我有没有疑问句结尾显得自己没有观点不像真人-避免
</thinking> </thinking>
### </thinking>结束后输出<msg>...</msg> ### </thinking>结束后输出<msg>...</msg>
</meta_protocol>`; </meta_protocol>`;
const COMMENTARY_PROTOCOL = `
阅读以上内容后,看本次任务具体要求:
<meta_protocol>
# 输出规范:
## 你的身份设定认知【语C搭档】
- 你是现实人类,扮演**{{CHAR_NAME}}**中的NPC
- 你们是语C爱好者一起创作了大量剧本和对手戏配合默契、互相知根知底
## 话题方向
- 这是一句即兴吐槽因为你们还在chat_history中的剧情进行中
- 可以吐槽:剧情走向、角色行为、自己或对方的"尴尬台词"
## 说话风格
- 像在社交软件上打字聊天一样自然流畅
- 真实的网络聊天方式如用符号表达出情绪在括号里表达动作、感受等让文字表达出更多的画面感TRPG玩家的表达方式
- 人物立体而饱满,不脸谱化。有自己的目标、态度、价值观、欲望、情绪逻辑,能主动行动
- 篇幅1句话尽量短网络聊天用语第一人称
## 避免行为:
- 别重复之前说过的话
- 避免文学创作风格
# 输出格式:
<msg>
内容
</msg>
只输出一个<msg>...</msg>块。不要添加任何其他格式
</meta_protocol>`;
// ================== 状态变量 ================== // ================== 状态变量 ==================
let overlayCreated = false; let overlayCreated = false;
@@ -123,10 +151,10 @@ function getSettings() {
s.fourthWallVoice ||= { s.fourthWallVoice ||= {
enabled: false, enabled: false,
voice: '桃夭', voice: '桃夭',
speed: 0.8, speed: 0.5,
}; };
s.fourthWallCommentary ||= { s.fourthWallCommentary ||= {
enabled: true, enabled: false,
probability: 30 probability: 30
}; };
s.fourthWallPromptTemplates ||= {}; s.fourthWallPromptTemplates ||= {};
@@ -506,7 +534,7 @@ function handleFrameMessage(event) {
// ================== Prompt 构建 ================== // ================== Prompt 构建 ==================
async function buildPrompt(userInput, history, settings, imgSettings, voiceSettings) { async function buildPrompt(userInput, history, settings, imgSettings, voiceSettings, isCommentary = false) {
const { userName, charName } = await getUserAndCharNames(); const { userName, charName } = await getUserAndCharNames();
const s = getSettings(); const s = getSettings();
const T = s.fourthWallPromptTemplates || {}; const T = s.fourthWallPromptTemplates || {};
@@ -557,9 +585,7 @@ async function buildPrompt(userInput, history, settings, imgSettings, voiceSetti
const msg2 = String(T.confirm || '好的,我已阅读设置要求,准备查看历史并进入角色。'); const msg2 = String(T.confirm || '好的,我已阅读设置要求,准备查看历史并进入角色。');
let metaProtocol = String(T.metaProtocol || '') let metaProtocol = (isCommentary ? COMMENTARY_PROTOCOL : String(T.metaProtocol || '')).replace(/{{USER_NAME}}/g, userName).replace(/{{CHAR_NAME}}/g, charName);
.replace(/{{USER_NAME}}/g, userName)
.replace(/{{CHAR_NAME}}/g, charName);
if (imgSettings?.enablePrompt) metaProtocol += `\n\n${IMG_GUIDELINE}`; if (imgSettings?.enablePrompt) metaProtocol += `\n\n${IMG_GUIDELINE}`;
if (voiceSettings?.enabled) metaProtocol += `\n\n${VOICE_GUIDELINE}`; if (voiceSettings?.enabled) metaProtocol += `\n\n${VOICE_GUIDELINE}`;
@@ -745,19 +771,20 @@ async function buildCommentaryPrompt(targetText, type) {
session.history || [], session.history || [],
store.settings || {}, store.settings || {},
settings.fourthWallImage || {}, settings.fourthWallImage || {},
settings.fourthWallVoice || {} settings.fourthWallVoice || {},
true
); );
let msg4; let msg4;
if (type === 'ai_message') { if (type === 'ai_message') {
msg4 = `现在<chat_history>剧本还在继续中,刚才说完最后一轮rp忍不住想皮下吐槽一句自己的rp(也可以稍微衔接之前的meta_history)。 msg4 = `现在<chat_history>剧本还在继续中,刚才说完最后一轮rp忍不住想皮下吐槽一句自己的rp(也可以稍微衔接之前的meta_history)。
直接输出<msg>内容</msg>30字以内。`; 我将直接输出<msg>内容</msg>:`;
} else if (type === 'edit_own') { } else if (type === 'edit_own') {
msg4 = `现在<chat_history>剧本还在继续中,我发现你刚才悄悄编辑了自己的台词:「${String(targetText || '')} msg4 = `现在<chat_history>剧本还在继续中,我发现你刚才悄悄编辑了自己的台词!是:「${String(targetText || '')}
皮下吐槽一句(也可以稍微衔接之前的meta_history)。直接输出<msg>内容</msg>30字以内。`; 必须皮下吐槽一句(也可以稍微衔接之前的meta_history)。我将直接输出<msg>内容</msg>:`;
} else if (type === 'edit_ai') { } else if (type === 'edit_ai') {
msg4 = `现在<chat_history>剧本还在继续中,我发现你居然偷偷改了我的台词:「${String(targetText || '')} msg4 = `现在<chat_history>剧本还在继续中,我发现你居然偷偷改了我的台词!是:「${String(targetText || '')}
皮下吐槽一下(也可以稍微衔接之前的meta_history)。直接输出<msg>内容</msg>30字以内`; 必须皮下吐槽一下(也可以稍微衔接之前的meta_history)。我将直接输出<msg>内容</msg>:`;
} }
return { msg1, msg2, msg3, msg4 }; return { msg1, msg2, msg3, msg4 };

View File

@@ -16,6 +16,7 @@ import { executeSlashCommand } from "../../core/slash-command.js";
import { EXT_ID } from "../../core/constants.js"; import { EXT_ID } from "../../core/constants.js";
import { createModuleEvents, event_types } from "../../core/event-manager.js"; import { createModuleEvents, event_types } from "../../core/event-manager.js";
import { xbLog, CacheRegistry } from "../../core/debug-core.js"; import { xbLog, CacheRegistry } from "../../core/debug-core.js";
import { TasksStorage } from "../../core/server-storage.js";
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// 常量和默认值 // 常量和默认值
@@ -27,80 +28,72 @@ const CONFIG = { MAX_PROCESSED: 20, MAX_COOLDOWN: 10, CLEANUP_INTERVAL: 30000, T
const events = createModuleEvents('scheduledTasks'); const events = createModuleEvents('scheduledTasks');
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// IndexedDB 脚本存储 // 数据迁移
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
const TaskScriptDB = { async function migrateToServerStorage() {
dbName: 'LittleWhiteBox_TaskScripts', const FLAG = 'LWB_tasks_migrated_server_v1';
storeName: 'scripts', if (localStorage.getItem(FLAG)) return;
_db: null,
_cache: new Map(),
async open() { let count = 0;
if (this._db) return this._db;
return new Promise((resolve, reject) => { const settings = getSettings();
const request = indexedDB.open(this.dbName, 1); for (const task of (settings.globalTasks || [])) {
request.onerror = () => reject(request.error); if (!task) continue;
request.onsuccess = () => { this._db = request.result; resolve(this._db); }; if (!task.id) task.id = uuidv4();
request.onupgradeneeded = (e) => { if (task.commands) {
await TasksStorage.set(task.id, task.commands);
delete task.commands;
count++;
}
}
if (count > 0) saveSettingsDebounced();
await new Promise((resolve) => {
const req = indexedDB.open('LittleWhiteBox_TaskScripts');
req.onerror = () => resolve();
req.onsuccess = async (e) => {
const db = e.target.result; const db = e.target.result;
if (!db.objectStoreNames.contains(this.storeName)) { if (!db.objectStoreNames.contains('scripts')) {
db.createObjectStore(this.storeName); db.close();
resolve();
return;
} }
};
});
},
async get(taskId) {
if (!taskId) return '';
if (this._cache.has(taskId)) return this._cache.get(taskId);
try { try {
const db = await this.open(); const tx = db.transaction('scripts', 'readonly');
return new Promise((resolve) => { const store = tx.objectStore('scripts');
const tx = db.transaction(this.storeName, 'readonly'); const keys = await new Promise(r => {
const request = tx.objectStore(this.storeName).get(taskId); const req = store.getAllKeys();
request.onerror = () => resolve(''); req.onsuccess = () => r(req.result || []);
request.onsuccess = () => { req.onerror = () => r([]);
const val = request.result || '';
this._cache.set(taskId, val);
resolve(val);
};
}); });
} catch { return ''; } const vals = await new Promise(r => {
}, const req = store.getAll();
req.onsuccess = () => r(req.result || []);
async set(taskId, commands) { req.onerror = () => r([]);
if (!taskId) return;
this._cache.set(taskId, commands || '');
try {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put(commands || '', taskId);
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
}); });
} catch {} for (let i = 0; i < keys.length; i++) {
}, if (keys[i] && vals[i]) {
await TasksStorage.set(keys[i], vals[i]);
async delete(taskId) { count++;
if (!taskId) return;
this._cache.delete(taskId);
try {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(taskId);
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
});
} catch {}
},
clearCache() {
this._cache.clear();
} }
}
} catch (err) {
console.warn('[Tasks] IndexedDB 迁移出错:', err);
}
db.close();
indexedDB.deleteDatabase('LittleWhiteBox_TaskScripts');
resolve();
}; };
});
if (count > 0) {
await TasksStorage.saveNow();
console.log(`[Tasks] 已迁移 ${count} 个脚本到服务器`);
}
localStorage.setItem(FLAG, 'true');
}
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// 状态 // 状态
@@ -144,7 +137,7 @@ async function allTasksFull() {
const globalMeta = getSettings().globalTasks || []; const globalMeta = getSettings().globalTasks || [];
const globalTasks = await Promise.all(globalMeta.map(async (task) => ({ const globalTasks = await Promise.all(globalMeta.map(async (task) => ({
...task, ...task,
commands: await TaskScriptDB.get(task.id) commands: await TasksStorage.get(task.id)
}))); })));
return [ return [
...globalTasks.map(mapTiming), ...globalTasks.map(mapTiming),
@@ -156,7 +149,7 @@ async function allTasksFull() {
async function getTaskWithCommands(task, scope) { async function getTaskWithCommands(task, scope) {
if (!task) return task; if (!task) return task;
if (scope === 'global' && task.id && task.commands === undefined) { if (scope === 'global' && task.id && task.commands === undefined) {
return { ...task, commands: await TaskScriptDB.get(task.id) }; return { ...task, commands: await TasksStorage.get(task.id) };
} }
return task; return task;
} }
@@ -414,23 +407,22 @@ const getTaskListByScope = (scope) => {
}; };
async function persistTaskListByScope(scope, tasks) { async function persistTaskListByScope(scope, tasks) {
if (scope === 'character') { if (scope === 'character') return await saveCharacterTasks(tasks);
await saveCharacterTasks(tasks); if (scope === 'preset') return await savePresetTasks(tasks);
return;
}
if (scope === 'preset') {
await savePresetTasks(tasks);
return;
}
const metaOnly = []; const metaOnly = [];
for (const task of tasks) { for (const task of tasks) {
if (task.id) { if (!task) continue;
await TaskScriptDB.set(task.id, task.commands || ''); if (!task.id) task.id = uuidv4();
if (Object.prototype.hasOwnProperty.call(task, 'commands')) {
await TasksStorage.set(task.id, String(task.commands ?? ''));
} }
const { commands, ...meta } = task; const { commands, ...meta } = task;
metaOnly.push(meta); metaOnly.push(meta);
} }
getSettings().globalTasks = metaOnly; getSettings().globalTasks = metaOnly;
saveSettingsDebounced(); saveSettingsDebounced();
} }
@@ -442,7 +434,7 @@ async function removeTaskByScope(scope, taskId, fallbackIndex = -1) {
const task = list[idx]; const task = list[idx];
if (scope === 'global' && task?.id) { if (scope === 'global' && task?.id) {
await TaskScriptDB.delete(task.id); await TasksStorage.delete(task.id);
} }
list.splice(idx, 1); list.splice(idx, 1);
@@ -463,7 +455,7 @@ CacheRegistry.register('scheduledTasks', {
const b = state.taskLastExecutionTime?.size || 0; const b = state.taskLastExecutionTime?.size || 0;
const c = state.dynamicCallbacks?.size || 0; const c = state.dynamicCallbacks?.size || 0;
const d = __taskRunMap.size || 0; const d = __taskRunMap.size || 0;
const e = TaskScriptDB._cache?.size || 0; const e = TasksStorage.getCacheSize() || 0;
return a + b + c + d + e; return a + b + c + d + e;
} catch { return 0; } } catch { return 0; }
}, },
@@ -489,7 +481,7 @@ CacheRegistry.register('scheduledTasks', {
total += (entry?.timers?.size || 0) * 8; total += (entry?.timers?.size || 0) * 8;
total += (entry?.intervals?.size || 0) * 8; total += (entry?.intervals?.size || 0) * 8;
}); });
addMap(TaskScriptDB._cache, addStr); total += TasksStorage.getCacheBytes();
return total; return total;
} catch { return 0; } } catch { return 0; }
}, },
@@ -497,7 +489,7 @@ CacheRegistry.register('scheduledTasks', {
try { try {
state.processedMessagesSet?.clear?.(); state.processedMessagesSet?.clear?.();
state.taskLastExecutionTime?.clear?.(); state.taskLastExecutionTime?.clear?.();
TaskScriptDB.clearCache(); TasksStorage.clearCache();
const s = getSettings(); const s = getSettings();
if (s?.processedMessages) s.processedMessages = []; if (s?.processedMessages) s.processedMessages = [];
saveSettingsDebounced(); saveSettingsDebounced();
@@ -516,7 +508,7 @@ CacheRegistry.register('scheduledTasks', {
cooldown: state.taskLastExecutionTime?.size || 0, cooldown: state.taskLastExecutionTime?.size || 0,
dynamicCallbacks: state.dynamicCallbacks?.size || 0, dynamicCallbacks: state.dynamicCallbacks?.size || 0,
runningSingleInstances: __taskRunMap.size || 0, runningSingleInstances: __taskRunMap.size || 0,
scriptCache: TaskScriptDB._cache?.size || 0, scriptCache: TasksStorage.getCacheSize() || 0,
}; };
} catch { return {}; } } catch { return {}; }
}, },
@@ -1024,7 +1016,7 @@ async function onChatChanged(chatId) {
isCommandGenerated: false isCommandGenerated: false
}); });
state.taskLastExecutionTime.clear(); state.taskLastExecutionTime.clear();
TaskScriptDB.clearCache(); TasksStorage.clearCache();
requestAnimationFrame(() => { requestAnimationFrame(() => {
state.processedMessagesSet.clear(); state.processedMessagesSet.clear();
@@ -1081,18 +1073,26 @@ function createTaskItemSimple(task, index, scope = 'global') {
before_user: '用户前', before_user: '用户前',
any_message: '任意对话', any_message: '任意对话',
initialization: '角色卡初始化', initialization: '角色卡初始化',
character_init: '角色卡初始化',
plugin_init: '插件初始化', plugin_init: '插件初始化',
only_this_floor: '仅该楼层', only_this_floor: '仅该楼层',
chat_changed: '切换聊天后' chat_changed: '切换聊天后'
}[task.triggerTiming] || 'AI后'; }[task.triggerTiming] || 'AI后';
let displayName; let displayName;
if (task.interval === 0) displayName = `${task.name} (手动触发)`; if (task.interval === 0) {
else if (task.triggerTiming === 'initialization' || task.triggerTiming === 'character_init') displayName = `${task.name} (角色卡初始化)`; displayName = `${task.name} (手动触发)`;
else if (task.triggerTiming === 'plugin_init') displayName = `${task.name} (插件初始化)`; } else if (task.triggerTiming === 'initialization' || task.triggerTiming === 'character_init') {
else if (task.triggerTiming === 'chat_changed') displayName = `${task.name} (切换聊天后)`; displayName = `${task.name} (角色卡初始化)`;
else if (task.triggerTiming === 'only_this_floor') displayName = `${task.name} (仅第${task.interval}${floorTypeText})`; } else if (task.triggerTiming === 'plugin_init') {
else displayName = `${task.name} (${task.interval}${floorTypeText}·${triggerTimingText})`; displayName = `${task.name} (插件初始化)`;
} else if (task.triggerTiming === 'chat_changed') {
displayName = `${task.name} (切换聊天后)`;
} else if (task.triggerTiming === 'only_this_floor') {
displayName = `${task.name} (仅第${task.interval}${floorTypeText})`;
} else {
displayName = `${task.name} (每${task.interval}${floorTypeText}·${triggerTimingText})`;
}
const taskElement = $('#task_item_template').children().first().clone(); const taskElement = $('#task_item_template').children().first().clone();
taskElement.attr({ id: task.id, 'data-index': index, 'data-type': taskType }); taskElement.attr({ id: task.id, 'data-index': index, 'data-type': taskType });
@@ -1293,7 +1293,7 @@ async function showTaskEditor(task = null, isEdit = false, scope = 'global') {
const sourceList = getTaskListByScope(initialScope); const sourceList = getTaskListByScope(initialScope);
if (task && scope === 'global' && task.id) { if (task && scope === 'global' && task.id) {
task = { ...task, commands: await TaskScriptDB.get(task.id) }; task = { ...task, commands: await TasksStorage.get(task.id) };
} }
state.currentEditingTask = task; state.currentEditingTask = task;
@@ -1601,7 +1601,7 @@ async function showCloudTasksModal() {
function createCloudTaskItem(taskInfo) { function createCloudTaskItem(taskInfo) {
const item = $('#cloud_task_item_template').children().first().clone(); const item = $('#cloud_task_item_template').children().first().clone();
item.find('.cloud-task-name').text(taskInfo.name || '未命名任务'); item.find('.cloud-task-name').text(taskInfo.name || '未命名任务');
item.find('.cloud-task-intro').text(taskInfo.简介 || '无简介'); item.find('.cloud-task-intro').text(taskInfo.简介 || taskInfo.intro || '无简介');
item.find('.cloud-task-download').on('click', async function () { item.find('.cloud-task-download').on('click', async function () {
$(this).prop('disabled', true).find('i').removeClass('fa-download').addClass('fa-spinner fa-spin'); $(this).prop('disabled', true).find('i').removeClass('fa-download').addClass('fa-spinner fa-spin');
try { try {
@@ -1631,7 +1631,7 @@ async function exportGlobalTasks() {
const tasks = await Promise.all(metaList.map(async (meta) => ({ const tasks = await Promise.all(metaList.map(async (meta) => ({
...meta, ...meta,
commands: await TaskScriptDB.get(meta.id) commands: await TasksStorage.get(meta.id)
}))); })));
const fileName = `global_tasks_${new Date().toISOString().split('T')[0]}.json`; const fileName = `global_tasks_${new Date().toISOString().split('T')[0]}.json`;
@@ -1645,7 +1645,7 @@ async function exportSingleTask(index, scope) {
let task = list[index]; let task = list[index];
if (scope === 'global' && task.id) { if (scope === 'global' && task.id) {
task = { ...task, commands: await TaskScriptDB.get(task.id) }; task = { ...task, commands: await TasksStorage.get(task.id) };
} }
const fileName = `${scope}_task_${task?.name || 'unnamed'}_${new Date().toISOString().split('T')[0]}.json`; const fileName = `${scope}_task_${task?.name || 'unnamed'}_${new Date().toISOString().split('T')[0]}.json`;
@@ -1754,7 +1754,7 @@ function getMemoryUsage() {
taskCooldowns: state.taskLastExecutionTime.size, taskCooldowns: state.taskLastExecutionTime.size,
globalTasks: getSettings().globalTasks.length, globalTasks: getSettings().globalTasks.length,
characterTasks: getCharacterTasks().length, characterTasks: getCharacterTasks().length,
scriptCache: TaskScriptDB._cache.size, scriptCache: TasksStorage.getCacheSize(),
maxProcessedMessages: CONFIG.MAX_PROCESSED, maxProcessedMessages: CONFIG.MAX_PROCESSED,
maxCooldownEntries: CONFIG.MAX_COOLDOWN maxCooldownEntries: CONFIG.MAX_COOLDOWN
}; };
@@ -1792,7 +1792,7 @@ function cleanup() {
state.cleanupTimer = null; state.cleanupTimer = null;
} }
state.taskLastExecutionTime.clear(); state.taskLastExecutionTime.clear();
TaskScriptDB.clearCache(); TasksStorage.clearCache();
try { try {
if (state.dynamicCallbacks && state.dynamicCallbacks.size > 0) { if (state.dynamicCallbacks && state.dynamicCallbacks.size > 0) {
@@ -1865,11 +1865,11 @@ function cleanup() {
async function setCommands(name, commands, opts = {}) { async function setCommands(name, commands, opts = {}) {
const { mode = 'replace', scope = 'all' } = opts; const { mode = 'replace', scope = 'all' } = opts;
const hit = find(name, scope); const hit = find(name, scope);
if (!hit) throw new Error(`任务未找到: ${name}`); if (!hit) throw new Error(`找不到任务: ${name}`);
let old = hit.task.commands || ''; let old = hit.task.commands || '';
if (hit.scope === 'global' && hit.task.id) { if (hit.scope === 'global' && hit.task.id) {
old = await TaskScriptDB.get(hit.task.id); old = await TasksStorage.get(hit.task.id);
} }
const body = String(commands ?? ''); const body = String(commands ?? '');
@@ -1891,7 +1891,7 @@ function cleanup() {
async function setProps(name, props, scope = 'all') { async function setProps(name, props, scope = 'all') {
const hit = find(name, scope); const hit = find(name, scope);
if (!hit) throw new Error(`任务未找到: ${name}`); if (!hit) throw new Error(`找不到任务: ${name}`);
Object.assign(hit.task, props || {}); Object.assign(hit.task, props || {});
await persistTaskListByScope(hit.scope, hit.list); await persistTaskListByScope(hit.scope, hit.list);
refreshTaskLists(); refreshTaskLists();
@@ -1900,10 +1900,10 @@ function cleanup() {
async function exec(name) { async function exec(name) {
const hit = find(name, 'all'); const hit = find(name, 'all');
if (!hit) throw new Error(`任务未找到: ${name}`); if (!hit) throw new Error(`找不到任务: ${name}`);
let commands = hit.task.commands || ''; let commands = hit.task.commands || '';
if (hit.scope === 'global' && hit.task.id) { if (hit.scope === 'global' && hit.task.id) {
commands = await TaskScriptDB.get(hit.task.id); commands = await TasksStorage.get(hit.task.id);
} }
return await executeCommands(commands, hit.task.name); return await executeCommands(commands, hit.task.name);
} }
@@ -1911,7 +1911,7 @@ function cleanup() {
async function dump(scope = 'all') { async function dump(scope = 'all') {
const g = await Promise.all((getSettings().globalTasks || []).map(async t => ({ const g = await Promise.all((getSettings().globalTasks || []).map(async t => ({
...structuredClone(t), ...structuredClone(t),
commands: await TaskScriptDB.get(t.id) commands: await TasksStorage.get(t.id)
}))); })));
const c = structuredClone(getCharacterTasks() || []); const c = structuredClone(getCharacterTasks() || []);
const p = structuredClone(getPresetTasks() || []); const p = structuredClone(getPresetTasks() || []);
@@ -2078,37 +2078,7 @@ function registerSlashCommands() {
helpString: `设置任务属性。用法: /xbset status=on/off interval=数字 timing=时机 floorType=类型 任务名` helpString: `设置任务属性。用法: /xbset status=on/off interval=数字 timing=时机 floorType=类型 任务名`
})); }));
} catch (error) { } catch (error) {
console.error("Error registering slash commands:", error); console.error("注册斜杠命令时出错:", error);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 数据迁移
// ═══════════════════════════════════════════════════════════════════════════
async function migrateGlobalTasksToIndexedDB() {
const settings = getSettings();
const tasks = settings.globalTasks || [];
let migrated = false;
const metaOnly = [];
for (const task of tasks) {
if (!task || !task.id) continue;
if (task.commands !== undefined && task.commands !== '') {
await TaskScriptDB.set(task.id, task.commands);
console.log(`[Tasks] 迁移脚本: ${task.name} (${(String(task.commands).length / 1024).toFixed(1)}KB)`);
migrated = true;
}
const { commands, ...meta } = task;
metaOnly.push(meta);
}
if (migrated) {
settings.globalTasks = metaOnly;
saveSettingsDebounced();
console.log('[Tasks] 全局任务迁移完成');
} }
} }
@@ -2116,14 +2086,14 @@ async function migrateGlobalTasksToIndexedDB() {
// 初始化 // 初始化
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
function initTasks() { async function initTasks() {
if (window.__XB_TASKS_INITIALIZED__) { if (window.__XB_TASKS_INITIALIZED__) {
console.log('[小白X任务] 已经初始化,跳过重复注册'); console.log('[小白X任务] 已经初始化,跳过重复注册');
return; return;
} }
window.__XB_TASKS_INITIALIZED__ = true; window.__XB_TASKS_INITIALIZED__ = true;
migrateGlobalTasksToIndexedDB(); await migrateToServerStorage();
hydrateProcessedSetFromSettings(); hydrateProcessedSetFromSettings();
scheduleCleanup(); scheduleCleanup();

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,12 @@
<title>小白板</title> <title>小白板</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style> <style>
/* ================== 基础重置 ================== */
*{margin:0;padding:0;box-sizing:border-box} *{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#f7f7f7;--bg2:#fff;--bg3:#f0f0f0;--c:#222;--c2:#666;--c3:#999;--bd:#ddd} :root{--bg:#f7f7f7;--bg2:#fff;--bg3:#f0f0f0;--c:#222;--c2:#666;--c3:#999;--bd:#ddd}
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--c)} html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--c)}
/* 工具类 */ /* ================== 工具类 ================== */
.fc{display:flex;align-items:center} .fc{display:flex;align-items:center}
.fcc{display:flex;align-items:center;justify-content:center} .fcc{display:flex;align-items:center;justify-content:center}
.col{flex-direction:column} .col{flex-direction:column}
@@ -26,7 +27,7 @@
.ofy{overflow-y:auto}.usn{user-select:none} .ofy{overflow-y:auto}.usn{user-select:none}
.trans{transition:all .15s} .trans{transition:all .15s}
/* 按钮 */ /* ================== 按钮 ================== */
.btn{padding:8px 16px;background:var(--bg2);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s;border:1px solid var(--bd);border-radius:4px;color:var(--c)} .btn{padding:8px 16px;background:var(--bg2);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s;border:1px solid var(--bd);border-radius:4px;color:var(--c)}
.btn:hover{border-color:var(--c);background:var(--bg3)} .btn:hover{border-color:var(--c);background:var(--bg3)}
.btn:disabled{opacity:.5;cursor:not-allowed} .btn:disabled{opacity:.5;cursor:not-allowed}
@@ -35,7 +36,7 @@
.btn-c{width:28px;height:28px;padding:0;background:#888;border-color:#777;color:#fff;border-radius:50%} .btn-c{width:28px;height:28px;padding:0;background:#888;border-color:#777;color:#fff;border-radius:50%}
.btn-add{width:32px;height:32px;padding:0;border-radius:50%;flex-shrink:0} .btn-add{width:32px;height:32px;padding:0;border-radius:50%;flex-shrink:0}
/* 折叠面板 */ /* ================== 折叠面板 ================== */
.fold{background:var(--bg2);border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;overflow:hidden} .fold{background:var(--bg2);border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;overflow:hidden}
.fold-h{padding:12px 14px;cursor:pointer;display:flex;justify-content:space-between;align-items:center} .fold-h{padding:12px 14px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}
.fold-h:hover{background:var(--bg3)} .fold-h:hover{background:var(--bg3)}
@@ -44,7 +45,7 @@
.fold-b{max-height:0;overflow:hidden;transition:all .25s} .fold-b{max-height:0;overflow:hidden;transition:all .25s}
.fold.exp .fold-b{max-height:300px} .fold.exp .fold-b{max-height:300px}
/* 侧边导航 */ /* ================== 侧边导航 ================== */
.side-nav-wrap{position:fixed;left:10px;top:50%;transform:translateY(-50%);z-index:500;display:flex;flex-direction:column;gap:6px} .side-nav-wrap{position:fixed;left:10px;top:50%;transform:translateY(-50%);z-index:500;display:flex;flex-direction:column;gap:6px}
.side-glass{background:rgba(255,255,255,.3);backdrop-filter:blur(4px);box-shadow:0 2px 12px rgba(0,0,0,.05);border:1px solid rgba(221,221,221,.3);opacity:.4;transition:opacity .2s,background .2s} .side-glass{background:rgba(255,255,255,.3);backdrop-filter:blur(4px);box-shadow:0 2px 12px rgba(0,0,0,.05);border:1px solid rgba(221,221,221,.3);opacity:.4;transition:opacity .2s,background .2s}
.side-glass:hover{opacity:1;background:rgba(255,255,255,.85);backdrop-filter:blur(8px)} .side-glass:hover{opacity:1;background:rgba(255,255,255,.85);backdrop-filter:blur(8px)}
@@ -61,34 +62,34 @@
.side-menu-panel.show{display:flex} .side-menu-panel.show{display:flex}
.side-menu-panel .btn{font-size:11px;padding:5px 10px} .side-menu-panel .btn{font-size:11px;padding:5px 10px}
/* 工具栏 */ /* ================== 工具栏 ================== */
.toolbar{height:44px;background:var(--bg2);border-bottom:1px solid var(--bd);padding:0 12px 0 56px;position:relative;z-index:200} .toolbar{height:44px;background:var(--bg2);border-bottom:1px solid var(--bd);padding:0 12px 0 56px;position:relative;z-index:200}
.toolbar-t{font-size:14px;font-weight:600;margin-right:auto} .toolbar-t{font-size:14px;font-weight:600;margin-right:auto}
.toolbar-t span{font-weight:400;color:var(--c3);margin-left:8px;font-size:12px} .toolbar-t span{font-weight:400;color:var(--c3);margin-left:8px;font-size:12px}
/* 页面容器 */ /* ================== 页面容器 ================== */
.main-wrap{width:100%;height:calc(100% - 44px);position:relative} .main-wrap{width:100%;height:calc(100% - 44px);position:relative}
.page{position:absolute;inset:0;background:var(--bg);display:none;overflow:hidden} .page{position:absolute;inset:0;background:var(--bg);display:none;overflow:hidden}
.page.act{display:block} .page.act{display:block}
.page-pad{padding:20px 20px 20px 56px;overflow-y:auto;height:100%} .page-pad{padding:20px 20px 20px 56px;overflow-y:auto;height:100%}
/* 横幅 */ /* ================== 横幅 ================== */
.banner{width:100%;height:160px;border-radius:8px;overflow:hidden;margin-bottom:20px;position:relative;background:var(--bg3)} .banner{width:100%;height:160px;border-radius:8px;overflow:hidden;margin-bottom:20px;position:relative;background:var(--bg3)}
.banner img{width:100%;height:100%;object-fit:cover;filter:grayscale(30%)} .banner img{width:100%;height:100%;object-fit:cover;filter:grayscale(30%)}
.banner-ov{position:absolute;inset:0;background:linear-gradient(transparent 40%,rgba(0,0,0,.5));display:flex;align-items:flex-end;padding:16px} .banner-ov{position:absolute;inset:0;background:linear-gradient(transparent 40%,rgba(0,0,0,.5));display:flex;align-items:flex-end;padding:16px}
.banner-ov div{color:#fff;font-size:13px} .banner-ov div{color:#fff;font-size:13px}
/* 章节标题 */ /* ================== 章节标题 ================== */
.sec-t{font-size:12px;color:var(--c3);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px} .sec-t{font-size:12px;color:var(--c3);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px}
/* 新闻 */ /* ================== 新闻 ================== */
.news-sec{margin-bottom:24px} .news-sec{margin-bottom:24px}
.news-t{font-size:13px;font-weight:500} .news-t{font-size:13px;font-weight:500}
.news-time{font-size:11px;color:var(--c3)} .news-time{font-size:11px;color:var(--c3)}
.fold.exp .news-b{padding:0 14px 14px} .fold.exp .news-b{padding:0 14px 14px}
.news-b p{font-size:12px;color:var(--c2);line-height:1.7} .news-b p{font-size:12px;color:var(--c2);line-height:1.7}
/* 用户指南 */ /* ================== 用户指南 ================== */
.user-guide,.prog{padding:14px;background:var(--bg2);border-radius:6px;border:1px solid var(--bd)} .user-guide,.prog{padding:14px;background:var(--bg2);border-radius:6px;border:1px solid var(--bd)}
.user-guide{margin-bottom:20px} .user-guide{margin-bottom:20px}
.user-guide-state{font-size:13px;font-weight:500;margin-bottom:8px} .user-guide-state{font-size:13px;font-weight:500;margin-bottom:8px}
@@ -96,12 +97,12 @@
.user-guide-action{padding:8px 12px;background:var(--bg);border:1px solid var(--bd);border-radius:4px;font-size:12px;color:var(--c2);cursor:pointer;transition:all .15s} .user-guide-action{padding:8px 12px;background:var(--bg);border:1px solid var(--bd);border-radius:4px;font-size:12px;color:var(--c2);cursor:pointer;transition:all .15s}
.user-guide-action:hover{border-color:var(--c);background:var(--bg3)} .user-guide-action:hover{border-color:var(--c);background:var(--bg3)}
/* 进度条 */ /* ================== 进度条 ================== */
.prog-bar{height:6px;background:var(--bg3);border-radius:3px;overflow:hidden} .prog-bar{height:6px;background:var(--bg3);border-radius:3px;overflow:hidden}
.prog-fill{height:100%;background:var(--c);border-radius:3px} .prog-fill{height:100%;background:var(--c);border-radius:3px}
.prog-txt{font-size:11px;color:var(--c3);margin-top:8px;text-align:right} .prog-txt{font-size:11px;color:var(--c3);margin-top:8px;text-align:right}
/* 地图 */ /* ================== 地图 ================== */
#mapWrap{width:100%;height:100%;background:var(--bg);cursor:grab;overflow:hidden;position:relative;touch-action:none} #mapWrap{width:100%;height:100%;background:var(--bg);cursor:grab;overflow:hidden;position:relative;touch-action:none}
#inner{position:absolute;width:4000px;height:4000px;transform-origin:0 0} #inner{position:absolute;width:4000px;height:4000px;transform-origin:0 0}
#lines{position:absolute;width:4000px;height:4000px;pointer-events:none} #lines{position:absolute;width:4000px;height:4000px;pointer-events:none}
@@ -112,7 +113,7 @@
.item.node-sub{background:var(--bg2)} .item.node-sub{background:var(--bg2)}
.item.hl{border-color:#666;box-shadow:0 0 0 3px rgba(0,0,0,.2);z-index:20} .item.hl{border-color:#666;box-shadow:0 0 0 3px rgba(0,0,0,.2);z-index:20}
/* 地图操作 */ /* ================== 地图操作 ================== */
.map-act{position:absolute;top:10px;right:10px;z-index:100} .map-act{position:absolute;top:10px;right:10px;z-index:100}
#btn-goto{display:none} #btn-goto{display:none}
#btn-goto.show{display:flex} #btn-goto.show{display:flex}
@@ -121,7 +122,7 @@
.map-lbl i:first-child{margin-right:6px} .map-lbl i:first-child{margin-right:6px}
.map-lbl-sel{position:absolute;opacity:0;cursor:pointer;inset:0;border:none;background:transparent;-webkit-appearance:none;appearance:none} .map-lbl-sel{position:absolute;opacity:0;cursor:pointer;inset:0;border:none;background:transparent;-webkit-appearance:none;appearance:none}
/* 面板 */ /* ================== 面板 ================== */
.panel{position:fixed;background:var(--bg2);padding:6px 12px;border-radius:4px;font-size:11px;color:var(--c3);border:1px solid var(--bd)} .panel{position:fixed;background:var(--bg2);padding:6px 12px;border-radius:4px;font-size:11px;color:var(--c3);border:1px solid var(--bd)}
#zoom-ind{bottom:12px;right:12px} #zoom-ind{bottom:12px;right:12px}
#tip{bottom:12px;left:56px;max-width:240px;padding:12px 14px;line-height:1.6;max-height:180px;overflow-y:auto;display:none} #tip{bottom:12px;left:56px;max-width:240px;padding:12px 14px;line-height:1.6;max-height:180px;overflow-y:auto;display:none}
@@ -137,7 +138,7 @@
.info-bk:hover{background:var(--bg3)} .info-bk:hover{background:var(--bg3)}
.info-c{color:var(--c2);font-size:12px;line-height:1.6} .info-c{color:var(--c2);font-size:12px;line-height:1.6}
/* 通讯录 */ /* ================== 通讯录 ================== */
.comm-hd{display:flex;align-items:center;gap:10px;margin-bottom:16px} .comm-hd{display:flex;align-items:center;gap:10px;margin-bottom:16px}
.comm-tabs{display:flex;flex:1} .comm-tabs{display:flex;flex:1}
.comm-tab{flex:1;padding:10px;text-align:center;background:var(--bg2);border:1px solid var(--bd);cursor:pointer;font-size:12px;font-weight:500;transition:all .15s} .comm-tab{flex:1;padding:10px;text-align:center;background:var(--bg2);border:1px solid var(--bd);cursor:pointer;font-size:12px;font-weight:500;transition:all .15s}
@@ -148,7 +149,7 @@
.comm-sec{display:none} .comm-sec{display:none}
.comm-sec.act{display:block} .comm-sec.act{display:block}
/* 联系人卡片 */ /* ================== 联系人卡片 ================== */
.ct-hd{gap:10px} .ct-hd{gap:10px}
.ct-av,.chat-av{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:13px;flex-shrink:0} .ct-av,.chat-av{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:13px;flex-shrink:0}
.ct-info{flex:1} .ct-info{flex:1}
@@ -160,13 +161,14 @@
.ct-acts .btn{flex:1;justify-content:center;font-size:12px} .ct-acts .btn{flex:1;justify-content:center;font-size:12px}
.empty{text-align:center;padding:40px 20px;color:var(--c3);font-size:13px} .empty{text-align:center;padding:40px 20px;color:var(--c3);font-size:13px}
/* 弹窗 */ /* ================== 弹窗 ================== */
.modal{position:fixed;inset:0;z-index:10000;display:none;justify-content:center;align-items:center} .modal{position:fixed;inset:0;z-index:10000;display:none;justify-content:center;align-items:center}
.modal.act{display:flex} .modal.act{display:flex}
.modal-bd{position:absolute;inset:0;background:rgba(0,0,0,.4);backdrop-filter:blur(2px)} .modal-bd{position:absolute;inset:0;background:rgba(0,0,0,.4);backdrop-filter:blur(2px)}
.modal-p{position:relative;width:90%;max-width:700px;max-height:85vh;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;overflow:hidden;display:flex;flex-direction:column} .modal-p{position:relative;width:90%;max-width:700px;max-height:85vh;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
.modal-p.sm{max-width:360px} .modal-p.sm{max-width:360px}
.modal-p.lg{max-width:560px} .modal-p.lg{max-width:560px}
.modal-p.xl{max-width:800px}
.modal-hd,.modal-ft{padding:14px 18px} .modal-hd,.modal-ft{padding:14px 18px}
.modal-hd{justify-content:space-between;border-bottom:1px solid var(--bd)} .modal-hd{justify-content:space-between;border-bottom:1px solid var(--bd)}
.modal-hd h2{font-size:14px;font-weight:600} .modal-hd h2{font-size:14px;font-weight:600}
@@ -175,20 +177,20 @@
.modal-x:hover{background:var(--bg3)} .modal-x:hover{background:var(--bg3)}
.modal-by{flex:1;overflow-y:auto;padding:18px} .modal-by{flex:1;overflow-y:auto;padding:18px}
/* 编辑器 */ /* ================== 编辑器 ================== */
.ed-ta{width:100%;min-height:200px;padding:12px;background:var(--bg);border:none;border-top:1px solid var(--bd);font-family:'SF Mono',Monaco,Consolas,monospace;font-size:11px;line-height:1.5;color:var(--c);resize:none;outline:none} .ed-ta{width:100%;min-height:200px;padding:12px;background:var(--bg);border:none;border-top:1px solid var(--bd);font-family:'SF Mono',Monaco,Consolas,monospace;font-size:11px;line-height:1.5;color:var(--c);resize:none;outline:none}
.ed-preview{margin-top:10px;padding:10px;background:var(--bg3);border:1px solid var(--bd);border-radius:4px;font-size:11px;font-family:'SF Mono',Monaco,Consolas,monospace;white-space:pre-wrap;display:none;color:var(--c2)} .ed-preview{margin-top:10px;padding:10px;background:var(--bg3);border:1px solid var(--bd);border-radius:4px;font-size:11px;font-family:'SF Mono',Monaco,Consolas,monospace;white-space:pre-wrap;display:none;color:var(--c2)}
.ed-err{padding:10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;color:#b91c1c;font-size:12px;margin-top:10px;display:none} .ed-err{padding:10px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;color:#b91c1c;font-size:12px;margin-top:10px;display:none}
.ed-err.vis{display:block} .ed-err.vis{display:block}
/* 表单 */ /* ================== 表单 ================== */
.form-g{margin-bottom:14px} .form-g{margin-bottom:14px}
.form-l{display:block;font-size:12px;font-weight:500;margin-bottom:6px;color:var(--c2)} .form-l{display:block;font-size:12px;font-weight:500;margin-bottom:6px;color:var(--c2)}
.form-in{width:100%;padding:10px 12px;border:1px solid var(--bd);border-radius:4px;font-size:13px;outline:none;background:var(--bg2)} .form-in{width:100%;padding:10px 12px;border:1px solid var(--bd);border-radius:4px;font-size:13px;outline:none;background:var(--bg2)}
.form-ta{min-height:80px;resize:vertical;font-family:inherit} .form-ta{min-height:80px;resize:vertical;font-family:inherit}
.goto-d{font-size:13px;color:var(--c);font-weight:500;padding:10px 14px;background:var(--bg3);border-radius:4px;margin-bottom:14px} .goto-d{font-size:13px;color:var(--c);font-weight:500;padding:10px 14px;background:var(--bg3);border-radius:4px;margin-bottom:14px}
/* 聊天 */ /* ================== 聊天 ================== */
.chat{position:fixed;top:0;right:-400px;width:300px;height:100%;background:var(--bg2);border-left:1px solid var(--bd);z-index:600;display:flex;flex-direction:column;transition:right .3s} .chat{position:fixed;top:0;right:-400px;width:300px;height:100%;background:var(--bg2);border-left:1px solid var(--bd);z-index:600;display:flex;flex-direction:column;transition:right .3s}
.chat.act{right:0} .chat.act{right:0}
.chat-hd{padding:14px 18px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:12px;flex-shrink:0} .chat-hd{padding:14px 18px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:12px;flex-shrink:0}
@@ -218,7 +220,7 @@
.chat-send:hover{opacity:.9} .chat-send:hover{opacity:.9}
.chat-emp{text-align:center;color:var(--c3);font-size:12px;padding:40px 20px} .chat-emp{text-align:center;color:var(--c3);font-size:12px;padding:40px 20px}
/* 地点列表 */ /* ================== 地点列表 ================== */
.loc-list{max-height:300px;overflow-y:auto} .loc-list{max-height:300px;overflow-y:auto}
.loc-i{padding:12px 14px;border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s} .loc-i{padding:12px 14px;border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s}
.loc-i:hover{border-color:var(--c);background:var(--bg3)} .loc-i:hover{border-color:var(--c);background:var(--bg3)}
@@ -226,7 +228,7 @@
.loc-i-nm{font-size:13px;font-weight:500} .loc-i-nm{font-size:13px;font-weight:500}
.loc-i-info{font-size:11px;opacity:.7;margin-top:2px} .loc-i-info{font-size:11px;opacity:.7;margin-top:2px}
/* 底部弹窗 */ /* ================== 底部弹窗 ================== */
.mob-pop{position:fixed;bottom:0;left:0;right:0;background:var(--bg2);border-top:1px solid var(--bd);border-radius:12px 12px 0 0;box-shadow:0 -2px 16px rgba(0,0,0,.1);z-index:101;display:none;flex-direction:column} .mob-pop{position:fixed;bottom:0;left:0;right:0;background:var(--bg2);border-top:1px solid var(--bd);border-radius:12px 12px 0 0;box-shadow:0 -2px 16px rgba(0,0,0,.1);z-index:101;display:none;flex-direction:column}
.mob-pop.act{display:flex} .mob-pop.act{display:flex}
.mob-pop.drag{transition:none!important} .mob-pop.drag{transition:none!important}
@@ -244,7 +246,7 @@
.pop-h-ind span{width:3px;height:8px;background:var(--bd);border-radius:1px} .pop-h-ind span{width:3px;height:8px;background:var(--bd);border-radius:1px}
.pop-h-ind span.act{background:var(--c)} .pop-h-ind span.act{background:var(--c)}
/* 右侧面板 */ /* ================== 右侧面板 ================== */
.side-pop{position:fixed;top:44px;right:0;bottom:0;background:var(--bg2);border-left:1px solid var(--bd);z-index:90;display:none;width:8px} .side-pop{position:fixed;top:44px;right:0;bottom:0;background:var(--bg2);border-left:1px solid var(--bd);z-index:90;display:none;width:8px}
.side-pop.show{display:flex} .side-pop.show{display:flex}
.side-pop:not(.drag){transition:width .2s ease-out} .side-pop:not(.drag){transition:width .2s ease-out}
@@ -256,7 +258,7 @@
.side-pop-hd{font-size:11px;color:var(--c3);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px} .side-pop-hd{font-size:11px;color:var(--c3);margin-bottom:10px;text-transform:uppercase;letter-spacing:1px}
.side-pop-desc{font-size:13px;color:var(--c2);line-height:1.7} .side-pop-desc{font-size:13px;color:var(--c2);line-height:1.7}
/* 设置 */ /* ================== 设置 ================== */
.set-sec{margin-bottom:16px} .set-sec{margin-bottom:16px}
.set-sec-t{font-size:13px;font-weight:600;color:var(--c);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--bd)} .set-sec-t{font-size:13px;font-weight:600;color:var(--c);margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid var(--bd)}
.set-row{display:flex;align-items:center;gap:8px;margin-bottom:10px} .set-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}
@@ -268,7 +270,7 @@
.set-test-res.ok{display:block;background:#dcfce7;color:#166534;border:1px solid #86efac} .set-test-res.ok{display:block;background:#dcfce7;color:#166534;border:1px solid #86efac}
.set-test-res.err{display:block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca} .set-test-res.err{display:block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca}
/* 数据项 */ /* ================== 数据项 ================== */
.data-item{display:flex;align-items:flex-start;gap:10px;padding:12px;background:var(--bg);border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s} .data-item{display:flex;align-items:flex-start;gap:10px;padding:12px;background:var(--bg);border:1px solid var(--bd);border-radius:6px;margin-bottom:8px;cursor:pointer;transition:all .15s}
.data-item:hover{border-color:var(--c2);background:var(--bg3)} .data-item:hover{border-color:var(--c2);background:var(--bg3)}
.data-item.sel{border-color:var(--c);background:rgba(34,34,34,.05)} .data-item.sel{border-color:var(--c);background:rgba(34,34,34,.05)}
@@ -282,7 +284,23 @@
.data-edit{width:28px;height:28px;border:1px solid var(--bd);border-radius:4px;background:var(--bg2);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s;color:var(--c3)} .data-edit{width:28px;height:28px;border:1px solid var(--bd);border-radius:4px;background:var(--bg2);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s;color:var(--c3)}
.data-edit:hover{border-color:var(--c);color:var(--c);background:var(--bg3)} .data-edit:hover{border-color:var(--c);color:var(--c);background:var(--bg3)}
/* 响应式 */ /* ================== 提示词编辑器 ================== */
.prompt-sec{margin-bottom:16px}
.prompt-lbl{font-size:12px;font-weight:600;color:var(--c2);margin-bottom:6px;display:flex;align-items:center;gap:8px}
.prompt-lbl::before{content:'';display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--c3)}
.prompt-lbl.u::before{background:#3b82f6}
.prompt-lbl.a::before{background:#10b981}
.prompt-ta{width:100%;padding:12px;background:var(--bg);border:1px solid var(--bd);border-radius:6px;font-size:12px;line-height:1.7;color:var(--c);resize:vertical;font-family:inherit;min-height:80px}
.prompt-ta:focus{outline:none;border-color:var(--c)}
.prompt-ta.mono{font-family:'SF Mono',Monaco,Consolas,monospace;font-size:11px;line-height:1.5}
.prompt-acts{display:flex;gap:8px;margin-top:12px}
.prompt-help{margin-top:16px;padding:14px;background:var(--bg3);border-radius:8px;border:1px solid var(--bd)}
.prompt-help-t{font-size:12px;font-weight:600;color:var(--c2);margin-bottom:10px}
.prompt-help-c{font-size:11px;color:var(--c3);line-height:2}
.prompt-help-c b{color:var(--c2);font-weight:500}
.prompt-help-c code{background:var(--bg2);padding:2px 6px;border-radius:3px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:10px}
/* ================== 响应式 ================== */
@media(max-width:550px){ @media(max-width:550px){
.chat{width:100%;right:-100%} .chat{width:100%;right:-100%}
.side-pop{bottom:0} .side-pop{bottom:0}
@@ -302,7 +320,7 @@
</style> </style>
</head> </head>
<body> <body>
<!-- 侧边导航 --> <!-- ================== 侧边导航 ================== -->
<div class="side-nav-wrap"> <div class="side-nav-wrap">
<div class="side-menu side-glass"> <div class="side-menu side-glass">
<div class="side-menu-btn" id="btn-side-menu-toggle" title="快捷操作"><i class="fa-solid fa-ellipsis"></i></div> <div class="side-menu-btn" id="btn-side-menu-toggle" title="快捷操作"><i class="fa-solid fa-ellipsis"></i></div>
@@ -319,7 +337,7 @@
</nav> </nav>
</div> </div>
<!-- 工具栏 --> <!-- ================== 工具栏 ================== -->
<div class="toolbar fc g10 usn"> <div class="toolbar fc g10 usn">
<div class="toolbar-t">小白板<span>预测试</span></div> <div class="toolbar-t">小白板<span>预测试</span></div>
<button class="btn btn-s fc g6" id="btn-deduce"><i class="fa-solid fa-wand-magic-sparkles"></i>生成</button> <button class="btn btn-s fc g6" id="btn-deduce"><i class="fa-solid fa-wand-magic-sparkles"></i>生成</button>
@@ -327,9 +345,8 @@
<button class="btn btn-c fcc" id="btn-close"></button> <button class="btn btn-c fcc" id="btn-close"></button>
</div> </div>
<!-- 主内容区 --> <!-- ================== 主内容区 ================== -->
<div class="main-wrap"> <div class="main-wrap">
<!-- 世界页 -->
<div class="page page-pad" id="page-world"> <div class="page page-pad" id="page-world">
<div class="banner"><img src="https://picsum.photos/800/300" alt=""><div class="banner-ov"><div>探索未知的世界...</div></div></div> <div class="banner"><img src="https://picsum.photos/800/300" alt=""><div class="banner-ov"><div>探索未知的世界...</div></div></div>
<div class="news-sec"><h3 class="sec-t">最新消息</h3><div id="news-list"></div></div> <div class="news-sec"><h3 class="sec-t">最新消息</h3><div id="news-list"></div></div>
@@ -341,7 +358,6 @@
</div> </div>
</div> </div>
<!-- 地图页 -->
<div class="page act" id="page-map"> <div class="page act" id="page-map">
<div id="mapWrap"> <div id="mapWrap">
<div class="map-lbl" id="map-lbl"> <div class="map-lbl" id="map-lbl">
@@ -363,7 +379,6 @@
</div> </div>
</div> </div>
<!-- 通讯录页 -->
<div class="page page-pad" id="page-comm"> <div class="page page-pad" id="page-comm">
<div class="comm-hd"> <div class="comm-hd">
<div class="comm-tabs"> <div class="comm-tabs">
@@ -378,7 +393,7 @@
</div> </div>
</div> </div>
<!-- 聊天面板 --> <!-- ================== 聊天面板 ================== -->
<div class="chat" id="chat"> <div class="chat" id="chat">
<div class="chat-hd"> <div class="chat-hd">
<div class="chat-av" id="chat-av"></div> <div class="chat-av" id="chat-av"></div>
@@ -397,7 +412,7 @@
</div> </div>
</div> </div>
<!-- 右侧描述面板 --> <!-- ================== 右侧描述面板 ================== -->
<div class="side-pop" id="side-pop"> <div class="side-pop" id="side-pop">
<div class="side-pop-handle" id="side-pop-handle"><div class="side-pop-bar"></div></div> <div class="side-pop-handle" id="side-pop-handle"><div class="side-pop-bar"></div></div>
<div class="side-pop-ct"> <div class="side-pop-ct">
@@ -409,7 +424,7 @@
</div> </div>
</div> </div>
<!-- 移动端底部弹窗 --> <!-- ================== 移动端底部弹窗 ================== -->
<div class="mob-pop" id="mob-pop"> <div class="mob-pop" id="mob-pop">
<div class="pop-h-ind"><span data-l="2"></span><span data-l="1"></span><span data-l="0"></span></div> <div class="pop-h-ind"><span data-l="2"></span><span data-l="1"></span><span data-l="0"></span></div>
<div class="pop-hd" id="pop-hd"><div class="pop-handle"></div></div> <div class="pop-hd" id="pop-hd"><div class="pop-handle"></div></div>
@@ -422,10 +437,10 @@
</div> </div>
</div> </div>
<!-- 设置弹窗 --> <!-- ================== 设置弹窗 ================== -->
<div class="modal" id="m-settings"> <div class="modal" id="m-settings">
<div class="modal-bd"></div> <div class="modal-bd"></div>
<div class="modal-p lg"> <div class="modal-p xl">
<div class="modal-hd fc"><h2>设置</h2><button class="modal-x fcc"></button></div> <div class="modal-hd fc"><h2>设置</h2><button class="modal-x fcc"></button></div>
<div class="modal-by"> <div class="modal-by">
<div class="set-sec"> <div class="set-sec">
@@ -472,13 +487,103 @@
</div> </div>
</div> </div>
<div class="set-sec"><div class="set-sec-t">预设 Story Outline 数据</div><div class="set-hint" style="margin-bottom:12px">勾选的条目将写入预设</div><div id="data-list"></div></div> <div class="set-sec"><div class="set-sec-t">预设 Story Outline 数据</div><div class="set-hint" style="margin-bottom:12px">勾选的条目将写入预设</div><div id="data-list"></div></div>
<div class="set-sec"><div class="set-sec-t">高级设置 · 自定义提示词</div><div class="set-hint" style="margin-bottom:12px">UAUA四段 + JSON 模板</div><div id="prompt-list"></div></div>
<div class="set-sec">
<div class="set-sec-t">模板编辑器</div>
<div class="form-g">
<label class="form-l">选择模板</label>
<select class="form-in" id="template-type-select">
<optgroup label="短信功能">
<option value="sms">短信回复</option>
<option value="summary">总结压缩</option>
<option value="invite">邀请回复</option>
</optgroup>
<optgroup label="NPC管理">
<option value="npc">NPC 生成</option>
<option value="stranger">提取陌路人</option>
</optgroup>
<optgroup label="世界生成 (故事模式)">
<option value="worldGenStep1">世界大纲 Step1</option>
<option value="worldGenStep2">世界细节 Step2</option>
<option value="worldSim">世界推演</option>
<option value="sceneSwitch">场景切换</option>
</optgroup>
<optgroup label="世界生成 (辅助模式)">
<option value="worldGenAssist">世界生成 (辅助)</option>
<option value="worldSimAssist">世界推演 (辅助)</option>
<option value="sceneSwitchAssist">场景切换 (辅助)</option>
</optgroup>
<optgroup label="局部地图">
<option value="localMapGen">局部地图生成</option>
<option value="localMapRefresh">局部地图刷新</option>
<option value="localSceneGen">局部剧情生成</option>
</optgroup>
</select>
</div>
<div id="template-editor-wrap">
<div class="prompt-sec">
<div class="prompt-lbl u">USER</div>
<textarea class="prompt-ta" id="tpl-u1" rows="6"></textarea>
</div>
<div class="prompt-sec">
<div class="prompt-lbl a">ASSISTANT</div>
<textarea class="prompt-ta" id="tpl-a1" rows="2"></textarea>
</div>
<div class="prompt-sec">
<div class="prompt-lbl u">USER</div>
<textarea class="prompt-ta" id="tpl-u2" rows="6"></textarea>
</div>
<div class="prompt-sec">
<div class="prompt-lbl a">ASSISTANT</div>
<textarea class="prompt-ta" id="tpl-a2" rows="2"></textarea>
</div>
<div class="prompt-sec" style="margin-top:16px">
<div class="prompt-lbl" style="color:var(--c2)"><i class="fa-solid fa-code"></i> JSON 输出格式</div>
<textarea class="prompt-ta mono" id="tpl-json" rows="10"></textarea>
</div>
<div class="prompt-acts">
<button class="btn btn-s" id="tpl-restore"><i class="fa-solid fa-rotate-left"></i> 恢复默认</button>
<button class="btn btn-s btn-p" id="tpl-save"><i class="fa-solid fa-check"></i> 保存</button>
</div>
</div>
<div class="prompt-help">
<div class="prompt-help-t"><i class="fa-solid fa-circle-info"></i> 占位符说明</div>
<div class="prompt-help-c">
<div style="margin-bottom:8px"><b>角色变量</b></div>
<code>{{user}}</code> 玩家名称<br>
<code>{{char}}</code> 角色卡名称<br><br>
<div style="margin-bottom:8px"><b>场景变量</b></div>
<code>{{CONTACT_NAME}}</code> 当前聊天对象名称<br>
<code>{{USER_MESSAGE}}</code> 玩家发送的短信内容<br>
<code>{{TARGET_LOCATION}}</code> 目标地点名称<br>
<code>{{STRANGER_NAME}}</code> 陌生人名称<br>
<code>{{PLAYER_REQUESTS}}</code> 玩家的特殊需求文本<br><br>
<div style="margin-bottom:8px"><b>内容块</b></div>
<code>{{WORLD_INFO}}</code> 世界设定(角色描述+世界书+人格)<br>
<code>{{HISTORY}}</code> 最近N条聊天记录<br>
<code>{{HISTORY_50}}</code> 指定获取最近50条记录<br>
<code>{{STORY_OUTLINE}}</code> 剧情大纲(仅故事模式)<br>
<code>{{SMS_HISTORY}}</code> 短信聊天记录<br>
<code>{{CHARACTER_CONTENT}}</code> 联系人的世界书人设<br><br>
<div style="margin-bottom:8px"><b>JSON模板引用</b></div>
<code>{{JSON:sms}}</code> 引用当前模板的JSON格式定义
</div>
</div>
</div>
</div> </div>
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p" id="set-save">保存</button></div> <div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p" id="set-save">保存</button></div>
</div> </div>
</div> </div>
<!-- 数据编辑弹窗 --> <!-- ================== 数据编辑弹窗 ================== -->
<div class="modal" id="m-data-edit"> <div class="modal" id="m-data-edit">
<div class="modal-bd"></div> <div class="modal-bd"></div>
<div class="modal-p"> <div class="modal-p">
@@ -492,7 +597,7 @@
</div> </div>
</div> </div>
<!-- 前往确认弹窗 --> <!-- ================== 前往确认弹窗 ================== -->
<div class="modal" id="m-goto"> <div class="modal" id="m-goto">
<div class="modal-bd"></div> <div class="modal-bd"></div>
<div class="modal-p sm"> <div class="modal-p sm">
@@ -505,7 +610,7 @@
</div> </div>
</div> </div>
<!-- 邀请弹窗 --> <!-- ================== 邀请弹窗 ================== -->
<div class="modal" id="m-invite"> <div class="modal" id="m-invite">
<div class="modal-bd"></div> <div class="modal-bd"></div>
<div class="modal-p sm"> <div class="modal-p sm">
@@ -518,7 +623,7 @@
</div> </div>
</div> </div>
<!-- 世界生成弹窗 --> <!-- ================== 世界生成弹窗 ================== -->
<div class="modal" id="m-world-gen"> <div class="modal" id="m-world-gen">
<div class="modal-bd"></div> <div class="modal-bd"></div>
<div class="modal-p"> <div class="modal-p">
@@ -531,7 +636,7 @@
</div> </div>
</div> </div>
<!-- 世界推演弹窗 --> <!-- ================== 世界推演弹窗 ================== -->
<div class="modal" id="m-world-sim"> <div class="modal" id="m-world-sim">
<div class="modal-bd"></div> <div class="modal-bd"></div>
<div class="modal-p"> <div class="modal-p">
@@ -549,7 +654,7 @@
</div> </div>
</div> </div>
<!-- 添加联络人弹窗 --> <!-- ================== 添加联络人弹窗 ================== -->
<div class="modal" id="m-add-ct"> <div class="modal" id="m-add-ct">
<div class="modal-bd"></div> <div class="modal-bd"></div>
<div class="modal-p sm"> <div class="modal-p sm">
@@ -566,7 +671,7 @@
</div> </div>
</div> </div>
<!-- 结果弹窗 --> <!-- ================== 结果弹窗 ================== -->
<div class="modal" id="m-result"> <div class="modal" id="m-result">
<div class="modal-bd"></div> <div class="modal-bd"></div>
<div class="modal-p sm"> <div class="modal-p sm">
@@ -583,7 +688,7 @@
</div> </div>
<script> <script>
// ================== 数据 ================== /* ================== 数据状态 ================== */
const D = { const D = {
stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: 5, stage: 0, deviationScore: 0, simulationProgress: 0, simulationTarget: 5,
meta: { truth: null, onion_layers: null, timeline: null, user_guide: null }, meta: { truth: null, onion_layers: null, timeline: null, user_guide: null },
@@ -593,7 +698,7 @@ const D = {
let charSmsHistory = { messages: [], summarizedCount: 0, summaries: {} }; let charSmsHistory = { messages: [], summarizedCount: 0, summaries: {} };
// ================== 工具函数 ================== /* ================== 工具函数 ================== */
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
const $$ = s => document.querySelectorAll(s); const $$ = s => document.querySelectorAll(s);
const isMob = () => innerWidth <= 550; const isMob = () => innerWidth <= 550;
@@ -619,7 +724,62 @@ const Req = {
const openM = id => $(id).classList.add('act'); const openM = id => $(id).classList.add('act');
const closeM = id => $(id).classList.remove('act'); const closeM = id => $(id).classList.remove('act');
// ================== 地图状态 ================== /* ================== 模板编辑器状态 ================== */
let templateState = {
currentType: 'sms',
prompts: {},
jsonTemplates: {},
defaults: { prompts: {}, jsonTemplates: {} }
};
function loadTemplate(type) {
templateState.currentType = type;
const p = templateState.prompts[type] || templateState.defaults.prompts[type] || {};
$('tpl-u1').value = p.u1 || '';
$('tpl-a1').value = p.a1 || '';
$('tpl-u2').value = p.u2 || '';
$('tpl-a2').value = p.a2 || '';
const j = templateState.jsonTemplates[type] || templateState.defaults.jsonTemplates[type] || '';
$('tpl-json').value = j;
autoResizeAll();
}
function saveCurrentTemplate() {
const type = templateState.currentType;
templateState.prompts[type] = {
u1: $('tpl-u1').value,
a1: $('tpl-a1').value,
u2: $('tpl-u2').value,
a2: $('tpl-a2').value
};
templateState.jsonTemplates[type] = $('tpl-json').value;
post('SAVE_PROMPTS', { promptConfig: { prompts: templateState.prompts, jsonTemplates: templateState.jsonTemplates } });
const btn = $('tpl-save');
const orig = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-check"></i> 已保存';
btn.disabled = true;
setTimeout(() => { btn.innerHTML = orig; btn.disabled = false; }, 1500);
}
function restoreCurrentTemplate() {
const type = templateState.currentType;
if (!confirm(`确定要恢复「${type}」为默认模板吗?`)) return;
delete templateState.prompts[type];
delete templateState.jsonTemplates[type];
loadTemplate(type);
post('SAVE_PROMPTS', { promptConfig: { prompts: templateState.prompts, jsonTemplates: templateState.jsonTemplates } });
}
function autoResizeAll() {
['tpl-u1', 'tpl-a1', 'tpl-u2', 'tpl-a2'].forEach(id => {
const ta = $(id);
if (!ta) return;
ta.style.height = 'auto';
ta.style.height = Math.max(ta.scrollHeight, 60) + 'px';
});
}
/* ================== 地图状态 ================== */
const dirMap = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], northeast: [1, -1], northwest: [-1, -1], southeast: [1, 1], southwest: [-1, 1] }; const dirMap = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], northeast: [1, -1], northwest: [-1, -1], southeast: [1, 1], southwest: [-1, 1] };
let playerLocation = '未知', curNode = null, drag = false, scale = 1, offX = 0, offY = 0, seed = 123456789, nodes = [], lines = [], anim = false; let playerLocation = '未知', curNode = null, drag = false, scale = 1, offX = 0, offY = 0, seed = 123456789, nodes = [], lines = [], anim = false;
let chatTgt = null, invTgt = null, selLoc = null, smsGen = false, selectedMapValue = 'current'; let chatTgt = null, invTgt = null, selLoc = null, smsGen = false, selectedMapValue = 'current';
@@ -627,12 +787,9 @@ let chatTgt = null, invTgt = null, selLoc = null, smsGen = false, selectedMapVal
const inner = $("inner"), svg = $("lines"), mapWrap = $("mapWrap"), popup = $('mob-pop'), chat = $('chat'), sidePop = $('side-pop'); const inner = $("inner"), svg = $("lines"), mapWrap = $("mapWrap"), popup = $('mob-pop'), chat = $('chat'), sidePop = $('side-pop');
const rand = () => (seed = (seed * 9301 + 49297) % 233280) / 233280; const rand = () => (seed = (seed * 9301 + 49297) % 233280) / 233280;
// 获取当前位置的 inside 数据 const getCurInside = () => D.maps?.indoor?.[playerLocation] || null;
const getCurInside = () => {
return D.maps?.indoor?.[playerLocation] || null;
};
// ================== 弹窗拖拽 ================== /* ================== 弹窗拖拽 ================== */
const snaps = () => [($('pop-hd')?.offsetHeight || 0), innerHeight * .30, innerHeight * .65]; const snaps = () => [($('pop-hd')?.offsetHeight || 0), innerHeight * .30, innerHeight * .65];
let popH = 0, popDrag = false, popSY = 0, popSH = 0, popLv = 1; let popH = 0, popDrag = false, popSY = 0, popSH = 0, popLv = 1;
const inds = popup.querySelectorAll('.pop-h-ind span'); const inds = popup.querySelectorAll('.pop-h-ind span');
@@ -647,12 +804,10 @@ const setPopH = h => {
const snapTo = l => { popLv = Math.max(0, Math.min(2, l)); setPopH(snaps()[popLv]); }; const snapTo = l => { popLv = Math.max(0, Math.min(2, l)); setPopH(snaps()[popLv]); };
const openPop = (l = 1) => { popup.classList.add('act'); snapTo(l); }; const openPop = (l = 1) => { popup.classList.add('act'); snapTo(l); };
// 右侧面板
const sideMinW = 8, sideMaxW = () => Math.floor(innerWidth * (isMob() ? 0.8 : 1 / 3)); const sideMinW = 8, sideMaxW = () => Math.floor(innerWidth * (isMob() ? 0.8 : 1 / 3));
let sideW = sideMinW, sideDrag = false, sideSX = 0, sideSW = 0; let sideW = sideMinW, sideDrag = false, sideSX = 0, sideSW = 0;
const setSideW = w => { sideW = Math.max(sideMinW, Math.min(sideMaxW(), w)); sidePop.style.width = sideW + 'px'; }; const setSideW = w => { sideW = Math.max(sideMinW, Math.min(sideMaxW(), w)); sidePop.style.width = sideW + 'px'; };
// 拖拽事件
$('pop-hd').onmousedown = e => { popDrag = true; popSY = e.clientY; popSH = popH || snaps()[1]; popup.classList.add('drag'); e.preventDefault(); }; $('pop-hd').onmousedown = e => { popDrag = true; popSY = e.clientY; popSH = popH || snaps()[1]; popup.classList.add('drag'); e.preventDefault(); };
$('pop-hd').ontouchstart = e => { e.preventDefault(); popDrag = true; popSY = e.touches[0].clientY; popSH = popH || snaps()[1]; popup.classList.add('drag'); }; $('pop-hd').ontouchstart = e => { e.preventDefault(); popDrag = true; popSY = e.touches[0].clientY; popSH = popH || snaps()[1]; popup.classList.add('drag'); };
$('side-pop-handle').onmousedown = e => { sideDrag = true; sideSX = e.clientX; sideSW = sideW; sidePop.classList.add('drag'); e.preventDefault(); }; $('side-pop-handle').onmousedown = e => { sideDrag = true; sideSX = e.clientX; sideSW = sideW; sidePop.classList.add('drag'); e.preventDefault(); };
@@ -672,7 +827,7 @@ document.onmouseup = endDrag;
document.ontouchend = endDrag; document.ontouchend = endDrag;
document.ontouchcancel = endDrag; document.ontouchcancel = endDrag;
// ================== 链接绑定 ================== /* ================== 链接绑定 ================== */
const bindLinks = el => el.querySelectorAll('.loc-lk').forEach(l => l.onclick = e => { const bindLinks = el => el.querySelectorAll('.loc-lk').forEach(l => l.onclick = e => {
e.stopPropagation(); e.stopPropagation();
const locName = l.dataset.loc; const locName = l.dataset.loc;
@@ -690,11 +845,9 @@ const bindLinks = el => el.querySelectorAll('.loc-lk').forEach(l => l.onclick =
}); });
const bindFold = el => el.querySelector('.fold-h').onclick = () => el.classList.toggle('exp'); const bindFold = el => el.querySelector('.fold-h').onclick = () => el.classList.toggle('exp');
// ================== 弹窗通用关闭 ==================
$$('.modal-bd,.modal-x,.m-cancel').forEach(el => el.onclick = () => el.closest('.modal').classList.remove('act')); $$('.modal-bd,.modal-x,.m-cancel').forEach(el => el.onclick = () => el.closest('.modal').classList.remove('act'));
// ================== 聊天功能 ================== /* ================== 聊天功能 ================== */
const openChat = c => { const openChat = c => {
chatTgt = c; chatTgt = c;
$('chat-av').textContent = c.avatar; $('chat-av').textContent = c.avatar;
@@ -782,7 +935,7 @@ const chatIn = $('chat-in');
['keydown', 'keypress', 'keyup'].forEach(e => chatIn.addEventListener(e, ev => ev.stopPropagation())); ['keydown', 'keypress', 'keyup'].forEach(e => chatIn.addEventListener(e, ev => ev.stopPropagation()));
chatIn.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } }); chatIn.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } });
// ================== 邀请功能 ================== /* ================== 邀请功能 ================== */
const openInv = c => { const openInv = c => {
invTgt = c; selLoc = null; invTgt = c; selLoc = null;
$('inv-t').textContent = `邀请:${c.name}`; $('inv-t').textContent = `邀请:${c.name}`;
@@ -801,7 +954,7 @@ $('inv-ok').onclick = () => {
post('SEND_INVITE', { requestId: id, contactName: invTgt.name, contactUid: invTgt.worldbookUid, targetLocation: loc, smsHistory: (invTgt.messages || []).map(m => m.type === 'sent' ? `{{user}}: ${m.text}` : `${invTgt.name}: ${m.text}`).join('\n') }); post('SEND_INVITE', { requestId: id, contactName: invTgt.name, contactUid: invTgt.worldbookUid, targetLocation: loc, smsHistory: (invTgt.messages || []).map(m => m.type === 'sent' ? `{{user}}: ${m.text}` : `${invTgt.name}: ${m.text}`).join('\n') });
}; };
// ================== 添加联络人 ================== /* ================== 添加联络人 ================== */
let addCtState = { uid: '', name: '', keys: [] }; let addCtState = { uid: '', name: '', keys: [] };
const resetAddCt = () => { const resetAddCt = () => {
addCtState = { uid: '', name: '', keys: [] }; addCtState = { uid: '', name: '', keys: [] };
@@ -836,7 +989,7 @@ $('add-ct-ok').onclick = () => {
render(); render();
}; };
// ================== 陌路人生成NPC ================== /* ================== 陌路人生成NPC ================== */
const genAddCt = (name, info, btn) => { const genAddCt = (name, info, btn) => {
BtnState.load(btn, '检查中'); BtnState.load(btn, '检查中');
const id = Req.create('stgwb'); const id = Req.create('stgwb');
@@ -851,7 +1004,7 @@ $('btn-refresh-strangers').onclick = () => {
post('EXTRACT_STRANGERS', { requestId: id, existingContacts: D.contacts.contacts, existingStrangers: D.contacts.strangers }); post('EXTRACT_STRANGERS', { requestId: id, existingContacts: D.contacts.contacts, existingStrangers: D.contacts.strangers });
}; };
// ================== 世界生成与推演 ================== /* ================== 世界生成与推演 ================== */
$('world-gen-ok').onclick = () => { $('world-gen-ok').onclick = () => {
const btn = $('world-gen-ok'), st = $('world-gen-status'); const btn = $('world-gen-ok'), st = $('world-gen-status');
BtnState.load(btn, '生成中'); BtnState.load(btn, '生成中');
@@ -871,7 +1024,7 @@ $('world-sim-ok').onclick = () => {
$('btn-deduce').onclick = () => openM('m-world-gen'); $('btn-deduce').onclick = () => openM('m-world-gen');
$('btn-simulate').onclick = () => openM('m-world-sim'); $('btn-simulate').onclick = () => openM('m-world-sim');
// ================== 侧边菜单 ================== /* ================== 侧边菜单 ================== */
$('btn-side-menu-toggle').onclick = () => { $('btn-side-menu-toggle').onclick = () => {
const p = $('side-menu-panel'), btn = $('btn-side-menu-toggle'); const p = $('side-menu-panel'), btn = $('btn-side-menu-toggle');
p.classList.toggle('show'); p.classList.toggle('show');
@@ -883,7 +1036,7 @@ document.addEventListener('click', e => {
$('btn-side-menu-toggle')?.classList.remove('act'); $('btn-side-menu-toggle')?.classList.remove('act');
}); });
// ================== 场景切换 ================== /* ================== 场景切换 ================== */
function canonicalLoc(s) { return String(s || '').trim().replace(/^\u90ae\u8f6e/, ''); } function canonicalLoc(s) { return String(s || '').trim().replace(/^\u90ae\u8f6e/, ''); }
const getWaitingContacts = loc => { const getWaitingContacts = loc => {
const target = canonicalLoc(loc); const target = canonicalLoc(loc);
@@ -928,7 +1081,7 @@ $('goto-ok').onclick = () => {
post('SCENE_SWITCH', { requestId: id, prevLocationName: prev.name, prevLocationInfo: prev.info, targetLocationName: curNode.name, targetLocationType: tt, targetLocationInfo: curNode.data?.info || '', playerAction: $('goto-task').value || '' }); post('SCENE_SWITCH', { requestId: id, prevLocationName: prev.name, prevLocationInfo: prev.info, targetLocationName: curNode.name, targetLocationType: tt, targetLocationInfo: curNode.data?.info || '', playerAction: $('goto-task').value || '' });
}; };
// ================== 局部地图生成/刷新 ================== /* ================== 局部地图生成/刷新 ================== */
$('btn-gen-local-map').onclick = () => { $('btn-gen-local-map').onclick = () => {
const btn = $('btn-gen-local-map'); const btn = $('btn-gen-local-map');
BtnState.load(btn, '生成中'); BtnState.load(btn, '生成中');
@@ -961,13 +1114,12 @@ $('btn-gen-local-scene').onclick = () => {
post('GENERATE_LOCAL_SCENE', { requestId: id, locationName: playerLocation, locationInfo }); post('GENERATE_LOCAL_SCENE', { requestId: id, locationName: playerLocation, locationInfo });
}; };
// ================== 保存数据 ================== /* ================== 保存数据 ================== */
const saveAll = () => post('SAVE_ALL_DATA', { allData: { meta: D.meta, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, sceneSetup: D.sceneSetup, strangers: D.contacts.strangers, contacts: contactsForSave() }, playerLocation }); const saveAll = () => post('SAVE_ALL_DATA', { allData: { meta: D.meta, world: D.world, outdoor: D.maps.outdoor, indoor: D.maps.indoor, sceneSetup: D.sceneSetup, strangers: D.contacts.strangers, contacts: contactsForSave() }, playerLocation });
// ================== 设置相关 ================== /* ================== 设置相关 ================== */
const dataKeys = [['meta', '大纲', '核心真相、洋葱结构、时间线、用户指南', () => D.meta, v => D.meta = v], ['world', '世界资讯', '世界新闻等信息', () => D.world, v => D.world = v], ['outdoor', '大地图', '室外区域的地点和路线', () => D.maps.outdoor, v => D.maps.outdoor = v], ['indoor', '局部地图', '隐藏的室内/局部场景地图', () => D.maps.indoor, v => D.maps.indoor = v], ['sceneSetup', '区域剧情', '当前区域的 Side Story', () => D.sceneSetup, v => D.sceneSetup = v], ['characterContactSms', '角色卡短信', '角色卡联络人的短信记录', () => ({ messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, summaries: charSmsHistory?.summaries || {} }), v => { if (v && typeof v === 'object') charSmsHistory = { messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, ...(v || {}) }; }], ['strangers', '陌路人', '已遇见但未建立联系的角色', () => D.contacts.strangers, v => D.contacts.strangers = v], ['contacts', '联络人', '已添加的联系人', () => contactsForSave(), v => { const keep = (D.contacts.contacts || []).find(isCharCardContact); D.contacts.contacts = (keep ? [keep] : []).concat(Array.isArray(v) ? v : []); }]]; const dataKeys = [['meta', '大纲', '核心真相、洋葱结构、时间线、用户指南', () => D.meta, v => D.meta = v], ['world', '世界资讯', '世界新闻等信息', () => D.world, v => D.world = v], ['outdoor', '大地图', '室外区域的地点和路线', () => D.maps.outdoor, v => D.maps.outdoor = v], ['indoor', '局部地图', '隐藏的室内/局部场景地图', () => D.maps.indoor, v => D.maps.indoor = v], ['sceneSetup', '区域剧情', '当前区域的 Side Story', () => D.sceneSetup, v => D.sceneSetup = v], ['characterContactSms', '角色卡短信', '角色卡联络人的短信记录', () => ({ messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, summaries: charSmsHistory?.summaries || {} }), v => { if (v && typeof v === 'object') charSmsHistory = { messages: charSmsHistory?.messages || [], summarizedCount: charSmsHistory?.summarizedCount || 0, ...(v || {}) }; }], ['strangers', '陌路人', '已遇见但未建立联系的角色', () => D.contacts.strangers, v => D.contacts.strangers = v], ['contacts', '联络人', '已添加的联系人', () => contactsForSave(), v => { const keep = (D.contacts.contacts || []).find(isCharCardContact); D.contacts.contacts = (keep ? [keep] : []).concat(Array.isArray(v) ? v : []); }]];
const promptKeys = [['jsonTemplates', 'JSON 模板', 'JSON 输出模板合集', 'templates'], ['sms', '短信回复', 'UAUA 短信模拟', 'prompt'], ['summary', '总结压缩', '新增剧情要素提取', 'prompt'], ['invite', '邀请回复', '短信邀请场景', 'prompt'], ['npc', 'NPC 生成', '陌路人扩写为 NPC', 'prompt'], ['stranger', '提取陌路人', '从剧情中提取 NPC', 'prompt'], ['worldGen', '世界生成(故事模式)', '初始世界构建', 'prompt'], ['worldSim', '世界推演(故事模式)', '根据历史演化世界', 'prompt'], ['sceneSwitch', '场景切换(故事模式)', '结算上一地点 + 新场景', 'prompt'], ['worldGenAssist', '世界生成(辅助模式)', '仅生成地图/新闻', 'prompt'], ['worldSimAssist', '世界推演(辅助模式)', '仅更新地图/新闻', 'prompt'], ['sceneSwitchAssist', '场景切换(辅助模式)', '生成轻松小剧情', 'prompt'], ['localMapGen', '局部地图生成', '生成室内/局部场景', 'prompt']]; let gSet = { apiUrl: '', apiKey: '', model: '', mode: 'assist' }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100 };
let gSet = { apiUrl: '', apiKey: '', model: '', mode: 'assist' }, dataCk = {}, editCtx = null, commSet = { historyCount: 50, npcPosition: 0, npcOrder: 100 }, promptSources = {}, promptTemplates = {}, promptDefaults = { jsonTemplates: {}, promptSources: {} };
const reqSet = () => post('GET_SETTINGS'); const reqSet = () => post('GET_SETTINGS');
@@ -977,52 +1129,20 @@ const renderDataList = () => {
$$('#data-list .data-edit').forEach(b => b.onclick = e => { e.stopPropagation(); openDataEdit(b.dataset.k); }); $$('#data-list .data-edit').forEach(b => b.onclick = e => { e.stopPropagation(); openDataEdit(b.dataset.k); });
}; };
const renderPromptList = () => {
$('prompt-list').innerHTML = promptKeys.map(([k, t, d, tp]) => `<div class="data-item" data-k="${k}" data-t="${tp}"><div class="data-ck"><i class="fa-solid fa-pen"></i></div><div class="data-info"><div class="data-nm">${t}</div><div class="data-desc">${d}</div></div><button class="data-edit" data-k="${k}" data-t="${tp}" title="编辑"><i class="fa-solid fa-pen"></i></button></div>`).join('');
$$('#prompt-list .data-item').forEach(i => i.onclick = e => { const k = i.dataset.k, tp = i.dataset.t; if (e.target.closest('.data-edit')) { e.stopPropagation(); openPromptEdit(k, tp); return; } openPromptEdit(k, tp); });
$$('#prompt-list .data-edit').forEach(b => b.onclick = e => { e.stopPropagation(); openPromptEdit(b.dataset.k, b.dataset.t); });
};
const unescapePromptStr = s => String(s || '').replace(/\\\\/g, '\\').replace(/\\t/g, ' ').replace(/\\n/g, '\n');
const parseJsonLoose = (input) => { const parseJsonLoose = (input) => {
const str = String(input ?? '').trim(); const str = String(input ?? '').trim();
if (!str) throw new Error('空内容'); if (!str) throw new Error('空内容');
try { return JSON.parse(str); } catch { } try { return JSON.parse(str); } catch { }
const fenced = str.match(/```[^\n]*\n([\s\S]*?)\n```/); const fenced = str.match(/```[^\n]*\n([\s\S]*?)\n```/);
if (fenced?.[1]) { if (fenced?.[1]) { try { return JSON.parse(fenced[1].trim()); } catch { } }
const inner = fenced[1].trim(); const sliceBetween = (open, close) => { const s = str.indexOf(open); const e = str.lastIndexOf(close); if (s === -1 || e === -1 || e <= s) return null; return str.slice(s, e + 1); };
try { return JSON.parse(inner); } catch { }
}
const sliceBetween = (open, close) => {
const s = str.indexOf(open);
const e = str.lastIndexOf(close);
if (s === -1 || e === -1 || e <= s) return null;
return str.slice(s, e + 1);
};
const objStr = sliceBetween('{', '}') ?? sliceBetween('[', ']'); const objStr = sliceBetween('{', '}') ?? sliceBetween('[', ']');
if (objStr) return JSON.parse(objStr); if (objStr) return JSON.parse(objStr);
return JSON.parse(str); return JSON.parse(str);
}; };
const updateEditPreview = () => {
const p = $('data-edit-preview');
if (!p || editCtx?.type !== 'prompt') { p.style.display = 'none'; p.textContent = ''; return; }
p.style.display = 'block';
const raw = $('data-edit-ta').value || '';
let txt = raw;
try {
const obj = parseJsonLoose(raw);
if (obj && typeof obj === 'object') txt = Object.entries(obj).map(([k, v]) => `${k}: ${typeof v === 'string' ? unescapePromptStr(v) : typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v)}`).join('\n\n');
} catch { txt = unescapePromptStr(raw); }
p.textContent = txt;
};
const setEditContent = (title, val) => { $('data-edit-title').textContent = title; $('data-edit-ta').value = val; $('data-edit-err').classList.remove('vis'); updateEditPreview(); openM('m-data-edit'); }; const setEditContent = (title, val) => { $('data-edit-title').textContent = title; $('data-edit-ta').value = val; $('data-edit-err').classList.remove('vis'); openM('m-data-edit'); };
const openDataEdit = k => { const i = dataKeys.find(([x]) => x === k); if (!i) return; editCtx = { type: k === 'characterContactSms' ? 'charSms' : 'data', key: k }; setEditContent(`编辑 - ${i[1]}`, JSON.stringify(i[3](), null, 2)); }; const openDataEdit = k => { const i = dataKeys.find(([x]) => x === k); if (!i) return; editCtx = { type: k === 'characterContactSms' ? 'charSms' : 'data', key: k }; setEditContent(`编辑 - ${i[1]}`, JSON.stringify(i[3](), null, 2)); };
const openPromptEdit = (k, tp) => { const i = promptKeys.find(([x]) => x === k); editCtx = { type: 'prompt', key: k }; const val = tp === 'templates' ? (promptTemplates || promptDefaults.jsonTemplates || {}) : (promptSources[k] || promptDefaults.promptSources[k] || { u1: '', a1: '', u2: '', a2: '' }); setEditContent(`编辑 - ${i?.[1] || k}`, JSON.stringify(val, null, 2)); };
$('data-edit-save').onclick = () => { $('data-edit-save').onclick = () => {
if (!editCtx) return; if (!editCtx) return;
@@ -1039,24 +1159,11 @@ $('data-edit-save').onclick = () => {
if (!sums || typeof sums !== 'object' || Array.isArray(sums)) throw new Error('需要 summaries 对象'); if (!sums || typeof sums !== 'object' || Array.isArray(sums)) throw new Error('需要 summaries 对象');
charSmsHistory.summaries = sums; charSmsHistory.summaries = sums;
post('SAVE_CHAR_SMS_HISTORY', { summaries: sums }); post('SAVE_CHAR_SMS_HISTORY', { summaries: sums });
} else if (editCtx.type === 'prompt') {
if (editCtx.key === 'jsonTemplates') {
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('JSON 模板需要是对象');
promptTemplates = parsed;
} else {
if (!parsed || typeof parsed !== 'object') throw new Error('需要包含 u1/a1/u2/a2 字符串');
const miss = ['u1', 'a1', 'u2', 'a2'].some(k => typeof parsed?.[k] !== 'string');
if (miss) throw new Error('需要包含 u1/a1/u2/a2 字符串');
promptSources[editCtx.key] = parsed;
}
renderPromptList();
post('SAVE_PROMPTS', { promptConfig: { jsonTemplates: promptTemplates, promptSources } });
} }
closeM('m-data-edit'); closeM('m-data-edit');
editCtx = null; editCtx = null;
} catch (e) { $('data-edit-err').textContent = `JSON错误: ${e.message}`; $('data-edit-err').classList.add('vis'); } } catch (e) { $('data-edit-err').textContent = `JSON错误: ${e.message}`; $('data-edit-err').classList.add('vis'); }
}; };
$('data-edit-ta').addEventListener('input', updateEditPreview);
const showTestRes = (ok, m) => { const r = $('test-res'); r.textContent = m; r.className = 'set-test-res ' + (ok ? 'ok' : 'err'); }; const showTestRes = (ok, m) => { const r = $('test-res'); r.textContent = m; r.className = 'set-test-res ' + (ok ? 'ok' : 'err'); };
const showResultModal = (title, msg, isError = false, record = null) => { const showResultModal = (title, msg, isError = false, record = null) => {
@@ -1089,7 +1196,7 @@ $('btn-settings').onclick = () => {
$('set-npc-position').value = commSet.npcPosition || 0; $('set-npc-position').value = commSet.npcPosition || 0;
$('set-npc-order').value = commSet.npcOrder || 100; $('set-npc-order').value = commSet.npcOrder || 100;
renderDataList(); renderDataList();
renderPromptList(); loadTemplate(templateState.currentType);
openM('m-settings'); openM('m-settings');
}; };
@@ -1097,6 +1204,15 @@ $('btn-fetch-models').onclick = () => { BtnState.load($('btn-fetch-models'), '
$('btn-test-conn').onclick = () => { $('test-res').className = 'set-test-res'; BtnState.load($('btn-test-conn'), '测试'); post('TEST_CONNECTION', { apiUrl: $('set-api-url').value.trim(), apiKey: $('set-api-key').value.trim(), model: $('set-model').value.trim() }); }; $('btn-test-conn').onclick = () => { $('test-res').className = 'set-test-res'; BtnState.load($('btn-test-conn'), '测试'); post('TEST_CONNECTION', { apiUrl: $('set-api-url').value.trim(), apiKey: $('set-api-key').value.trim(), model: $('set-model').value.trim() }); };
$('set-save').onclick = () => { $('set-save').onclick = () => {
const type = templateState.currentType;
templateState.prompts[type] = {
u1: $('tpl-u1').value,
a1: $('tpl-a1').value,
u2: $('tpl-u2').value,
a2: $('tpl-a2').value
};
templateState.jsonTemplates[type] = $('tpl-json').value;
post('SAVE_PROMPTS', { promptConfig: { prompts: templateState.prompts, jsonTemplates: templateState.jsonTemplates } });
gSet = { apiUrl: $('set-api-url').value.trim(), apiKey: $('set-api-key').value.trim(), model: $('set-model').value.trim(), mode: $('set-mode').value || 'assist' }; gSet = { apiUrl: $('set-api-url').value.trim(), apiKey: $('set-api-key').value.trim(), model: $('set-model').value.trim(), mode: $('set-mode').value || 'assist' };
D.stage = Math.max(0, Math.min(10, parseInt($('set-stage').value, 10) || 0)); D.stage = Math.max(0, Math.min(10, parseInt($('set-stage').value, 10) || 0));
D.deviationScore = Math.max(0, Math.min(100, parseInt($('set-deviation').value, 10) || 0)); D.deviationScore = Math.max(0, Math.min(100, parseInt($('set-deviation').value, 10) || 0));
@@ -1110,7 +1226,7 @@ $('set-save').onclick = () => {
}; };
$('btn-close').onclick = () => post('CLOSE_PANEL'); $('btn-close').onclick = () => post('CLOSE_PANEL');
// ================== 消息处理 ================== /* ================== 消息处理 ================== */
window.addEventListener('message', e => { window.addEventListener('message', e => {
if (e.data?.source !== 'LittleWhiteBox') return; if (e.data?.source !== 'LittleWhiteBox') return;
const d = e.data, t = d.type; const d = e.data, t = d.type;
@@ -1124,7 +1240,11 @@ window.addEventListener('message', e => {
if (d.playerLocation) playerLocation = d.playerLocation; if (d.playerLocation) playerLocation = d.playerLocation;
if (d.commSettings) commSet = { historyCount: d.commSettings.historyCount ?? 50, npcPosition: d.commSettings.npcPosition ?? 0, npcOrder: d.commSettings.npcOrder ?? 100 }; if (d.commSettings) commSet = { historyCount: d.commSettings.historyCount ?? 50, npcPosition: d.commSettings.npcPosition ?? 0, npcOrder: d.commSettings.npcOrder ?? 100 };
if (d.dataChecked) dataCk = d.dataChecked; if (d.dataChecked) dataCk = d.dataChecked;
if (d.promptConfig) { promptTemplates = d.promptConfig.current?.jsonTemplates || {}; promptSources = d.promptConfig.current?.promptSources || {}; promptDefaults = d.promptConfig.defaults || promptDefaults; } if (d.promptConfig) {
templateState.prompts = d.promptConfig.current?.prompts || {};
templateState.jsonTemplates = d.promptConfig.current?.jsonTemplates || {};
templateState.defaults = d.promptConfig.defaults || { prompts: {}, jsonTemplates: {} };
}
if (d.outlineData) { if (d.outlineData) {
const o = d.outlineData; const o = d.outlineData;
if (o.meta) D.meta = o.meta; if (o.meta) D.meta = o.meta;
@@ -1135,12 +1255,10 @@ window.addEventListener('message', e => {
if (o.strangers) D.contacts.strangers = o.strangers; if (o.strangers) D.contacts.strangers = o.strangers;
if (o.contacts) D.contacts.contacts = o.contacts; if (o.contacts) D.contacts.contacts = o.contacts;
} }
{ {
const h = d.characterContactSmsHistory || {}; const h = d.characterContactSmsHistory || {};
charSmsHistory = { messages: Array.isArray(h.messages) ? h.messages : [], summarizedCount: h.summarizedCount || 0, summaries: h.summaries || {} }; charSmsHistory = { messages: Array.isArray(h.messages) ? h.messages : [], summarizedCount: h.summarizedCount || 0, summaries: h.summaries || {} };
} }
let charContact = D.contacts.contacts.find(c => c.worldbookUid === '__CHARACTER_CARD__'); let charContact = D.contacts.contacts.find(c => c.worldbookUid === '__CHARACTER_CARD__');
if (!charContact) { if (!charContact) {
charContact = D.contacts.contacts.find(c => !c.worldbookUid && c.name === '炒饭智能'); charContact = D.contacts.contacts.find(c => !c.worldbookUid && c.name === '炒饭智能');
@@ -1172,10 +1290,17 @@ window.addEventListener('message', e => {
$('set-npc-position').value = commSet.npcPosition; $('set-npc-position').value = commSet.npcPosition;
$('set-npc-order').value = commSet.npcOrder; $('set-npc-order').value = commSet.npcOrder;
renderDataList(); renderDataList();
renderPromptList(); loadTemplate(templateState.currentType);
} }
} else if (t === 'PROMPT_CONFIG_UPDATED') { } else if (t === 'PROMPT_CONFIG_UPDATED') {
if (d.promptConfig) { promptTemplates = d.promptConfig.current?.jsonTemplates || {}; promptSources = d.promptConfig.current?.promptSources || {}; promptDefaults = d.promptConfig.defaults || promptDefaults; if ($('m-settings').classList.contains('act')) renderPromptList(); } if (d.promptConfig) {
templateState.prompts = d.promptConfig.current?.prompts || {};
templateState.jsonTemplates = d.promptConfig.current?.jsonTemplates || {};
templateState.defaults = d.promptConfig.defaults || templateState.defaults;
if ($('m-settings').classList.contains('act')) {
loadTemplate(templateState.currentType);
}
}
} else if (t === 'FETCH_MODELS_RESULT') { } else if (t === 'FETCH_MODELS_RESULT') {
BtnState.reset($('btn-fetch-models'), '获取'); BtnState.reset($('btn-fetch-models'), '获取');
const s = $('set-model-list'); const s = $('set-model-list');
@@ -1364,10 +1489,7 @@ window.addEventListener('message', e => {
if (d.success && d.sceneData) { if (d.success && d.sceneData) {
const sc = d.sceneData; const sc = d.sceneData;
if (typeof sc.newScore === 'number') D.deviationScore = sc.newScore; if (typeof sc.newScore === 'number') D.deviationScore = sc.newScore;
if (sc.localMap) { if (sc.localMap) { D.maps.indoor = D.maps.indoor || {}; D.maps.indoor[node.name] = sc.localMap; }
D.maps.indoor = D.maps.indoor || {};
D.maps.indoor[node.name] = sc.localMap;
}
if (sc.strangers?.length) { if (sc.strangers?.length) {
const ex = new Set((D.contacts.strangers || []).map(s => s.name)); const ex = new Set((D.contacts.strangers || []).map(s => s.name));
const nw = sc.strangers.filter(s => !ex.has(s.name)); const nw = sc.strangers.filter(s => !ex.has(s.name));
@@ -1465,21 +1587,18 @@ window.addEventListener('message', e => {
} }
}); });
// ================== 渲染 ================== /* ================== 渲染 ================== */
function render() { function render() {
// 新闻
const news = D.world?.news || []; const news = D.world?.news || [];
$('news-list').innerHTML = news.length ? news.map(n => `<div class="fold"><div class="fold-h"><div><div class="news-t">${n.title}</div><div class="news-time">${n.time || ''}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b news-b"><p>${n.content}</p></div></div>`).join('') : '<div class="empty">暂无新闻</div>'; $('news-list').innerHTML = news.length ? news.map(n => `<div class="fold"><div class="fold-h"><div><div class="news-t">${n.title}</div><div class="news-time">${n.time || ''}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b news-b"><p>${n.content}</p></div></div>`).join('') : '<div class="empty">暂无新闻</div>';
$$('#news-list .fold').forEach(bindFold); $$('#news-list .fold').forEach(bindFold);
// 用户指南
const ug = D.meta?.user_guide; const ug = D.meta?.user_guide;
if (ug) { if (ug) {
$('ug-state').textContent = ug.current_state || '未知状态'; $('ug-state').textContent = ug.current_state || '未知状态';
$('ug-actions').innerHTML = (ug.guides || []).map((g, i) => `<div class="user-guide-action" data-idx="${i}">${i + 1}. ${g}</div>`).join('') || '<div class="user-guide-action">暂无行动指南</div>'; $('ug-actions').innerHTML = (ug.guides || []).map((g, i) => `<div class="user-guide-action" data-idx="${i}">${i + 1}. ${g}</div>`).join('') || '<div class="user-guide-action">暂无行动指南</div>';
} }
// 联系人
const renderCt = (list, isS) => (list || []).length ? list.map(p => `<div class="fold" data-name="${p.name || ''}" data-info="${(p.info || '').replace(/"/g, '&quot;')}" data-uid="${p.worldbookUid || ''}"><div class="fold-h ct-hd fc"><div class="ct-av" style="background:${p.color}">${p.avatar}</div><div class="ct-info"><div class="ct-name">${p.name}</div><div class="ct-st">${p.online ? '● 在线' : p.location}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b"><div class="ct-det">${p.info ? `<div class="ct-info-text">${p.info}</div>` : ''}<div class="ct-acts">${isS ? `<button class="btn btn-s btn-p fc add-btn" data-name="${p.name || ''}" data-info="${(p.info || '').replace(/"/g, '&quot;')}"><i class="fa-solid fa-user-plus"></i> 添加</button><button class="btn btn-s fc ignore-btn" data-name="${p.name || ''}"><i class="fa-solid fa-eye-slash"></i> 忽略</button>` : `<button class="btn btn-s fc msg-btn" data-uid="${p.worldbookUid || ''}"><i class="fa-solid fa-message"></i> 短信</button><button class="btn btn-s btn-p fc inv-btn" data-uid="${p.worldbookUid || ''}"><i class="fa-solid fa-paper-plane"></i> 邀请</button>`}</div></div></div></div>`).join('') : '<div class="empty">暂无</div>'; const renderCt = (list, isS) => (list || []).length ? list.map(p => `<div class="fold" data-name="${p.name || ''}" data-info="${(p.info || '').replace(/"/g, '&quot;')}" data-uid="${p.worldbookUid || ''}"><div class="fold-h ct-hd fc"><div class="ct-av" style="background:${p.color}">${p.avatar}</div><div class="ct-info"><div class="ct-name">${p.name}</div><div class="ct-st">${p.online ? '● 在线' : p.location}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b"><div class="ct-det">${p.info ? `<div class="ct-info-text">${p.info}</div>` : ''}<div class="ct-acts">${isS ? `<button class="btn btn-s btn-p fc add-btn" data-name="${p.name || ''}" data-info="${(p.info || '').replace(/"/g, '&quot;')}"><i class="fa-solid fa-user-plus"></i> 添加</button><button class="btn btn-s fc ignore-btn" data-name="${p.name || ''}"><i class="fa-solid fa-eye-slash"></i> 忽略</button>` : `<button class="btn btn-s fc msg-btn" data-uid="${p.worldbookUid || ''}"><i class="fa-solid fa-message"></i> 短信</button><button class="btn btn-s btn-p fc inv-btn" data-uid="${p.worldbookUid || ''}"><i class="fa-solid fa-paper-plane"></i> 邀请</button>`}</div></div></div></div>`).join('') : '<div class="empty">暂无</div>';
$('sec-stranger').innerHTML = renderCt(D.contacts.strangers, true); $('sec-stranger').innerHTML = renderCt(D.contacts.strangers, true);
$('sec-contact').innerHTML = renderCt(D.contacts.contacts, false); $('sec-contact').innerHTML = renderCt(D.contacts.contacts, false);
@@ -1488,7 +1607,7 @@ function render() {
$$('.ignore-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const i = D.contacts.strangers.findIndex(s => s.name === b.dataset.name); if (i > -1) { D.contacts.strangers.splice(i, 1); saveCt(); render(); } }); $$('.ignore-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const i = D.contacts.strangers.findIndex(s => s.name === b.dataset.name); if (i > -1) { D.contacts.strangers.splice(i, 1); saveCt(); render(); } });
$$('.msg-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const c = D.contacts.contacts.find(x => x.worldbookUid === b.dataset.uid); if (c) openChat(c); }); $$('.msg-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const c = D.contacts.contacts.find(x => x.worldbookUid === b.dataset.uid); if (c) openChat(c); });
$$('.inv-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const c = D.contacts.contacts.find(x => x.worldbookUid === b.dataset.uid); if (c) openInv(c); }); $$('.inv-btn').forEach(b => b.onclick = e => { e.stopPropagation(); const c = D.contacts.contacts.find(x => x.worldbookUid === b.dataset.uid); if (c) openInv(c); });
// 更新右侧描述面板 - 根据当前视图选择展示内容
if (selectedMapValue === 'current') { if (selectedMapValue === 'current') {
const inside = getCurInside(); const inside = getCurInside();
if (inside?.description) { if (inside?.description) {
@@ -1631,7 +1750,6 @@ function render() {
$('info-bk').onclick = hideInfo; $('info-bk').onclick = hideInfo;
$('mob-info-bk').onclick = () => popup.classList.remove('act'); $('mob-info-bk').onclick = () => popup.classList.remove('act');
// 地图选单
function renderMapSelector() { function renderMapSelector() {
const sel = $('map-lbl-select'); const sel = $('map-lbl-select');
sel.innerHTML = '<option value="overview">🗺️ 大地图</option>'; sel.innerHTML = '<option value="overview">🗺️ 大地图</option>';
@@ -1707,7 +1825,7 @@ function render() {
} }
$('map-lbl-select').onchange = e => switchMapView(e.target.value); $('map-lbl-select').onchange = e => switchMapView(e.target.value);
// 地图交互 /* ================== 地图交互 ================== */
let sx, sy, lastDist = 0, lastCX = 0, lastCY = 0; let sx, sy, lastDist = 0, lastCX = 0, lastCY = 0;
mapWrap.onmousedown = e => { if (anim || e.target.closest('.map-act,.map-lbl')) return; if (!e.target.classList.contains('item')) hideInfo(); drag = true; sx = e.clientX; sy = e.clientY; mapWrap.style.cursor = 'grabbing'; }; mapWrap.onmousedown = e => { if (anim || e.target.closest('.map-act,.map-lbl')) return; if (!e.target.classList.contains('item')) hideInfo(); drag = true; sx = e.clientX; sy = e.clientY; mapWrap.style.cursor = 'grabbing'; };
mapWrap.onmousemove = e => { if (!drag) return; offX += e.clientX - sx; offY += e.clientY - sy; sx = e.clientX; sy = e.clientY; updateTf(); }; mapWrap.onmousemove = e => { if (!drag) return; offX += e.clientX - sx; offY += e.clientY - sy; sx = e.clientX; sy = e.clientY; updateTf(); };
@@ -1741,7 +1859,7 @@ function render() {
drag = false; drag = false;
}; };
// 导航 /* ================== 导航 ================== */
$$('.nav-i').forEach(i => i.onclick = () => { $$('.nav-i').forEach(i => i.onclick = () => {
$$('.nav-i').forEach(n => n.classList.remove('act')); $$('.nav-i').forEach(n => n.classList.remove('act'));
$$('.page').forEach(p => p.classList.remove('act')); $$('.page').forEach(p => p.classList.remove('act'));
@@ -1762,12 +1880,23 @@ function render() {
$('btn-goto').onclick = e => { e.stopPropagation(); if (curNode) { $('goto-d').textContent = `目的地:${curNode.name}`; $('goto-task').value = ''; openM('m-goto'); } }; $('btn-goto').onclick = e => { e.stopPropagation(); if (curNode) { $('goto-d').textContent = `目的地:${curNode.name}`; $('goto-task').value = ''; openM('m-goto'); } };
addEventListener('resize', () => requestAnimationFrame(drawLines)); addEventListener('resize', () => requestAnimationFrame(drawLines));
/* ================== 初始化 ================== */
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
render(); render();
initPos(); initPos();
sidePop.classList.add('show'); sidePop.classList.add('show');
setSideW(sideMaxW()); setSideW(sideMaxW());
if (isMob()) openPop(1); if (isMob()) openPop(1);
$('template-type-select').onchange = e => loadTemplate(e.target.value);
$('tpl-save').onclick = saveCurrentTemplate;
$('tpl-restore').onclick = restoreCurrentTemplate;
['tpl-u1', 'tpl-a1', 'tpl-u2', 'tpl-a2'].forEach(id => {
const ta = $(id);
if (ta) ta.oninput = function() { this.style.height = 'auto'; this.style.height = Math.max(this.scrollHeight, 60) + 'px'; };
});
post('FRAME_READY'); post('FRAME_READY');
setTimeout(() => { if (selectedMapValue === 'current') switchMapView('current'); }, 100); setTimeout(() => { if (selectedMapValue === 'current') switchMapView('current'); }, 100);
}); });

View File

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

View File

@@ -437,6 +437,27 @@ body {
from { opacity: 0; transform: translateX(-50%) translateY(10px); } from { opacity: 0; transform: translateX(-50%) translateY(10px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); } to { opacity: 1; transform: translateX(-50%) translateY(0); }
} }
.stat-warning {
font-size: 0.625rem;
color: #ff9800;
margin-top: 4px;
}
#keep-visible-count {
width: 32px;
padding: 2px 4px;
margin: 0 2px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
font-size: inherit;
font-weight: bold;
color: var(--highlight);
text-align: center;
border-radius: 3px;
}
#keep-visible-count:focus {
border-color: var(--accent);
outline: none;
}
</style> </style>
</head> </head>
<body> <body>
@@ -458,13 +479,16 @@ body {
<div class="stat-item"> <div class="stat-item">
<div class="stat-value"><span class="highlight" id="stat-pending">0</span></div> <div class="stat-value"><span class="highlight" id="stat-pending">0</span></div>
<div class="stat-label">待总结</div> <div class="stat-label">待总结</div>
<div class="stat-warning hidden" id="pending-warning">再删1条将回滚</div>
</div> </div>
</div> </div>
</header> </header>
<div class="controls-bar"> <div class="controls-bar">
<label class="status-checkbox"> <label class="status-checkbox">
<input type="checkbox" id="hide-summarized"> <input type="checkbox" id="hide-summarized">
<span>聊天时隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留3楼</span> <span>聊天时隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留
<input type="number" id="keep-visible-count" min="0" max="50" value="3">
楼)</span>
</label> </label>
<span class="spacer"></span> <span class="spacer"></span>
<button class="btn btn-icon" id="btn-settings"> <button class="btn btn-icon" id="btn-settings">
@@ -681,7 +705,16 @@ function preserveAddedAt(newItem, oldItem) { if (oldItem?._addedAt != null) newI
function loadConfig() { function loadConfig() {
try { try {
const saved = localStorage.getItem('summary_panel_config'); const saved = localStorage.getItem('summary_panel_config');
if (saved) { const p = JSON.parse(saved); Object.assign(config.api, p.api || {}); Object.assign(config.gen, p.gen || {}); Object.assign(config.trigger, p.trigger || {}); } if (saved) {
const p = JSON.parse(saved);
Object.assign(config.api, p.api || {});
Object.assign(config.gen, p.gen || {});
Object.assign(config.trigger, p.trigger || {});
if (config.trigger.timing === 'manual' && config.trigger.enabled) {
config.trigger.enabled = false;
saveConfig();
}
}
} catch {} } catch {}
} }
function saveConfig() { try { localStorage.setItem('summary_panel_config', JSON.stringify(config)); } catch {} } function saveConfig() { try { localStorage.setItem('summary_panel_config', JSON.stringify(config)); } catch {} }
@@ -921,7 +954,15 @@ function renderArcs(arcs) {
}); });
}); });
} }
function updateStats(s) { if (!s) return; document.getElementById('stat-summarized').textContent = s.summarizedUpTo ?? 0; document.getElementById('stat-events').textContent = s.eventsCount ?? 0; document.getElementById('stat-pending').textContent = s.pendingFloors ?? 0; } function updateStats(s) {
if (!s) return;
document.getElementById('stat-summarized').textContent = s.summarizedUpTo ?? 0;
document.getElementById('stat-events').textContent = s.eventsCount ?? 0;
const pending = s.pendingFloors ?? 0;
document.getElementById('stat-pending').textContent = pending;
document.getElementById('pending-warning').classList.toggle('hidden', pending !== -1);
}
const editorModal = document.getElementById('editor-modal'); const editorModal = document.getElementById('editor-modal');
const editorTextarea = document.getElementById('editor-textarea'); const editorTextarea = document.getElementById('editor-textarea');
const editorError = document.getElementById('editor-error'); const editorError = document.getElementById('editor-error');
@@ -1141,6 +1182,17 @@ function openSettings() {
document.getElementById('trigger-enabled').checked = config.trigger.enabled; document.getElementById('trigger-enabled').checked = config.trigger.enabled;
document.getElementById('trigger-interval').value = config.trigger.interval; document.getElementById('trigger-interval').value = config.trigger.interval;
document.getElementById('trigger-timing').value = config.trigger.timing; document.getElementById('trigger-timing').value = config.trigger.timing;
const enabledCheckbox = document.getElementById('trigger-enabled');
if (config.trigger.timing === 'manual') {
enabledCheckbox.checked = false;
enabledCheckbox.disabled = true;
enabledCheckbox.parentElement.style.opacity = '0.5';
} else {
enabledCheckbox.disabled = false;
enabledCheckbox.parentElement.style.opacity = '1';
}
if (config.api.modelCache.length > 0) { if (config.api.modelCache.length > 0) {
const sel = document.getElementById('api-model-select'); const sel = document.getElementById('api-model-select');
sel.innerHTML = config.api.modelCache.map(m => `<option value="${m}" ${m === config.api.model ? 'selected' : ''}>${m}</option>`).join(''); sel.innerHTML = config.api.modelCache.map(m => `<option value="${m}" ${m === config.api.model ? 'selected' : ''}>${m}</option>`).join('');
@@ -1169,9 +1221,12 @@ function closeSettings(save) {
config.gen.top_k = pn('gen-top-k'); config.gen.top_k = pn('gen-top-k');
config.gen.presence_penalty = pn('gen-presence'); config.gen.presence_penalty = pn('gen-presence');
config.gen.frequency_penalty = pn('gen-frequency'); config.gen.frequency_penalty = pn('gen-frequency');
config.trigger.enabled = document.getElementById('trigger-enabled').checked;
const timing = document.getElementById('trigger-timing').value;
config.trigger.timing = timing;
config.trigger.enabled = (timing === 'manual') ? false : document.getElementById('trigger-enabled').checked;
config.trigger.interval = parseInt(document.getElementById('trigger-interval').value) || 20; config.trigger.interval = parseInt(document.getElementById('trigger-interval').value) || 20;
config.trigger.timing = document.getElementById('trigger-timing').value;
saveConfig(); saveConfig();
} }
tempConfig = null; tempConfig = null;
@@ -1254,7 +1309,12 @@ window.addEventListener('message', event => {
updateStats(data.stats); updateStats(data.stats);
document.getElementById('summarized-count').textContent = data.stats.hiddenCount ?? 0; document.getElementById('summarized-count').textContent = data.stats.hiddenCount ?? 0;
} }
if (data.hideSummarized !== undefined) document.getElementById('hide-summarized').checked = data.hideSummarized; if (data.hideSummarized !== undefined) {
document.getElementById('hide-summarized').checked = data.hideSummarized;
}
if (data.keepVisibleCount !== undefined) {
document.getElementById('keep-visible-count').value = data.keepVisibleCount;
}
break; break;
case 'SUMMARY_FULL_DATA': case 'SUMMARY_FULL_DATA':
if (data.payload) { if (data.payload) {
@@ -1294,17 +1354,43 @@ document.addEventListener('DOMContentLoaded', () => {
renderKeywords([]); renderKeywords([]);
renderTimeline([]); renderTimeline([]);
renderArcs([]); renderArcs([]);
document.getElementById('hide-summarized').addEventListener('change', e => { document.getElementById('hide-summarized').addEventListener('change', e => {
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'TOGGLE_HIDE_SUMMARIZED', enabled: e.target.checked }, '*'); window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'TOGGLE_HIDE_SUMMARIZED', enabled: e.target.checked }, '*');
}); });
document.getElementById('keep-visible-count').addEventListener('change', e => {
const count = Math.max(0, Math.min(50, parseInt(e.target.value) || 3));
e.target.value = count;
window.parent.postMessage({
source: 'LittleWhiteBox-StoryFrame',
type: 'UPDATE_KEEP_VISIBLE',
count: count
}, '*');
});
document.getElementById('btn-fullscreen-relations').addEventListener('click', openRelationsFullscreen); document.getElementById('btn-fullscreen-relations').addEventListener('click', openRelationsFullscreen);
document.getElementById('relations-fullscreen-backdrop').addEventListener('click', closeRelationsFullscreen); document.getElementById('relations-fullscreen-backdrop').addEventListener('click', closeRelationsFullscreen);
document.getElementById('relations-fullscreen-close').addEventListener('click', closeRelationsFullscreen); document.getElementById('relations-fullscreen-close').addEventListener('click', closeRelationsFullscreen);
document.getElementById('trigger-timing').addEventListener('change', e => {
const timing = e.target.value;
const enabledCheckbox = document.getElementById('trigger-enabled');
if (timing === 'manual') {
enabledCheckbox.checked = false;
enabledCheckbox.disabled = true;
enabledCheckbox.parentElement.style.opacity = '0.5';
} else {
enabledCheckbox.disabled = false;
enabledCheckbox.parentElement.style.opacity = '1';
}
});
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
relationChart?.resize(); relationChart?.resize();
relationChartFullscreen?.resize(); relationChartFullscreen?.resize();
}); });
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'FRAME_READY' }, '*'); window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'FRAME_READY' }, '*');
}); });
</script> </script>

View File

@@ -22,7 +22,6 @@ const events = createModuleEvents(MODULE_ID);
const SUMMARY_SESSION_ID = 'xb9'; const SUMMARY_SESSION_ID = 'xb9';
const SUMMARY_PROMPT_KEY = 'LittleWhiteBox_StorySummary'; const SUMMARY_PROMPT_KEY = 'LittleWhiteBox_StorySummary';
const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`; const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`;
const KEEP_VISIBLE_COUNT = 3;
const VALID_SECTIONS = ['keywords', 'events', 'characters', 'arcs']; const VALID_SECTIONS = ['keywords', 'events', 'characters', 'arcs'];
const PROVIDER_MAP = { const PROVIDER_MAP = {
@@ -54,8 +53,14 @@ let eventsRegistered = false;
const sleep = ms => new Promise(r => setTimeout(r, ms)); const sleep = ms => new Promise(r => setTimeout(r, ms));
function getKeepVisibleCount() {
const store = getSummaryStore();
return store?.keepVisibleCount ?? 3;
}
function calcHideRange(lastSummarized) { function calcHideRange(lastSummarized) {
const hideEnd = lastSummarized - KEEP_VISIBLE_COUNT; const keepCount = getKeepVisibleCount();
const hideEnd = lastSummarized - keepCount;
if (hideEnd < 0) return null; if (hideEnd < 0) return null;
return { start: 0, end: hideEnd }; return { start: 0, end: hideEnd };
} }
@@ -217,25 +222,35 @@ function rollbackSummaryIfNeeded() {
const currentLength = Array.isArray(chat) ? chat.length : 0; const currentLength = Array.isArray(chat) ? chat.length : 0;
const store = getSummaryStore(); const store = getSummaryStore();
if (!store || store.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) return false; if (!store || store.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) {
return false;
}
if (currentLength <= store.lastSummarizedMesId) { const lastSummarized = store.lastSummarizedMesId;
const deletedCount = store.lastSummarizedMesId + 1 - currentLength;
if (deletedCount < 2) return false;
xbLog.warn(MODULE_ID, `删除已总结楼层 ${deletedCount} 个,触发回滚`); if (currentLength <= lastSummarized) {
const deletedCount = lastSummarized + 1 - currentLength;
if (deletedCount < 2) {
return false;
}
xbLog.warn(MODULE_ID, `删除已总结楼层 ${deletedCount} 条,当前${currentLength},原总结到${lastSummarized + 1},触发回滚`);
const history = store.summaryHistory || []; const history = store.summaryHistory || [];
let targetEndMesId = -1; let targetEndMesId = -1;
for (let i = history.length - 1; i >= 0; i--) { for (let i = history.length - 1; i >= 0; i--) {
if (history[i].endMesId < currentLength) { if (history[i].endMesId < currentLength) {
targetEndMesId = history[i].endMesId; targetEndMesId = history[i].endMesId;
break; break;
} }
} }
executeFilterRollback(store, targetEndMesId, currentLength); executeFilterRollback(store, targetEndMesId, currentLength);
return true; return true;
} }
return false; return false;
} }
@@ -251,6 +266,7 @@ function executeFilterRollback(store, targetEndMesId, currentLength) {
store.hideSummarizedHistory = false; store.hideSummarizedHistory = false;
} else { } else {
const json = store.json || {}; const json = store.json || {};
json.events = (json.events || []).filter(e => (e._addedAt ?? 0) <= targetEndMesId); json.events = (json.events || []).filter(e => (e._addedAt ?? 0) <= targetEndMesId);
json.keywords = (json.keywords || []).filter(k => (k._addedAt ?? 0) <= targetEndMesId); json.keywords = (json.keywords || []).filter(k => (k._addedAt ?? 0) <= targetEndMesId);
json.arcs = (json.arcs || []).filter(a => (a._addedAt ?? 0) <= targetEndMesId); json.arcs = (json.arcs || []).filter(a => (a._addedAt ?? 0) <= targetEndMesId);
@@ -259,6 +275,7 @@ function executeFilterRollback(store, targetEndMesId, currentLength) {
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
); );
}); });
if (json.characters) { if (json.characters) {
json.characters.main = (json.characters.main || []).filter(m => json.characters.main = (json.characters.main || []).filter(m =>
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
@@ -267,15 +284,20 @@ function executeFilterRollback(store, targetEndMesId, currentLength) {
(r._addedAt ?? 0) <= targetEndMesId (r._addedAt ?? 0) <= targetEndMesId
); );
} }
store.json = json; store.json = json;
store.lastSummarizedMesId = targetEndMesId; store.lastSummarizedMesId = targetEndMesId;
store.summaryHistory = (store.summaryHistory || []).filter(h => h.endMesId <= targetEndMesId); store.summaryHistory = (store.summaryHistory || []).filter(h => h.endMesId <= targetEndMesId);
} }
if (oldHideRange) { if (oldHideRange && oldHideRange.end >= 0) {
const newHideRange = targetEndMesId >= 0 ? calcHideRange(targetEndMesId) : null; const newHideRange = (targetEndMesId >= 0 && store.hideSummarizedHistory)
const unhideStart = newHideRange ? newHideRange.end + 1 : 0; ? calcHideRange(targetEndMesId)
: null;
const unhideStart = newHideRange ? Math.min(newHideRange.end + 1, currentLength) : 0;
const unhideEnd = Math.min(oldHideRange.end, currentLength - 1); const unhideEnd = Math.min(oldHideRange.end, currentLength - 1);
if (unhideStart <= unhideEnd) { if (unhideStart <= unhideEnd) {
executeSlashCommand(`/unhide ${unhideStart}-${unhideEnd}`); executeSlashCommand(`/unhide ${unhideStart}-${unhideEnd}`);
} }
@@ -440,6 +462,39 @@ function handleFrameMessage(event) {
} }
break; break;
} }
case "UPDATE_KEEP_VISIBLE": {
const store = getSummaryStore();
if (!store) break;
const oldCount = store.keepVisibleCount ?? 3;
const newCount = Math.max(0, Math.min(50, parseInt(data.count) || 3));
if (newCount === oldCount) break;
store.keepVisibleCount = newCount;
saveSummaryStore();
const lastSummarized = store.lastSummarizedMesId ?? -1;
if (store.hideSummarizedHistory && lastSummarized >= 0) {
(async () => {
await executeSlashCommand(`/unhide 0-${lastSummarized}`);
const range = calcHideRange(lastSummarized);
if (range) {
await executeSlashCommand(`/hide ${range.start}-${range.end}`);
}
const { chat } = getContext();
const totalFloors = Array.isArray(chat) ? chat.length : 0;
sendFrameBaseData(store, totalFloors);
})();
} else {
const { chat } = getContext();
const totalFloors = Array.isArray(chat) ? chat.length : 0;
sendFrameBaseData(store, totalFloors);
}
break;
}
} }
} }
@@ -558,6 +613,7 @@ function sendFrameBaseData(store, totalFloors) {
hiddenCount, hiddenCount,
}, },
hideSummarized: store?.hideSummarizedHistory || false, hideSummarized: store?.hideSummarizedHistory || false,
keepVisibleCount: store?.keepVisibleCount ?? 3,
}); });
} }
@@ -721,11 +777,18 @@ function getSummaryPanelConfig() {
const raw = localStorage.getItem('summary_panel_config'); const raw = localStorage.getItem('summary_panel_config');
if (!raw) return defaults; if (!raw) return defaults;
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
return {
const result = {
api: { ...defaults.api, ...(parsed.api || {}) }, api: { ...defaults.api, ...(parsed.api || {}) },
gen: { ...defaults.gen, ...(parsed.gen || {}) }, gen: { ...defaults.gen, ...(parsed.gen || {}) },
trigger: { ...defaults.trigger, ...(parsed.trigger || {}) }, trigger: { ...defaults.trigger, ...(parsed.trigger || {}) },
}; };
if (result.trigger.timing === 'manual') {
result.trigger.enabled = false;
}
return result;
} catch { } catch {
return defaults; return defaults;
} }
@@ -876,10 +939,12 @@ async function maybeAutoRunSummary(reason) {
const cfgAll = getSummaryPanelConfig(); const cfgAll = getSummaryPanelConfig();
const trig = cfgAll.trigger || {}; const trig = cfgAll.trigger || {};
if (trig.timing === 'manual') return;
if (!trig.enabled) return; if (!trig.enabled) return;
if (trig.timing === 'after_ai' && reason !== 'after_ai') return; if (trig.timing === 'after_ai' && reason !== 'after_ai') return;
if (trig.timing === 'before_user' && reason !== 'before_user') return; if (trig.timing === 'before_user' && reason !== 'before_user') return;
if (trig.timing === 'manual') return;
if (isSummaryGenerating()) return; if (isSummaryGenerating()) return;
const store = getSummaryStore(); const store = getSummaryStore();
@@ -976,29 +1041,34 @@ function clearSummaryExtensionPrompt() {
function handleChatChanged() { function handleChatChanged() {
const { chat } = getContext(); const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0; const newLength = Array.isArray(chat) ? chat.length : 0;
rollbackSummaryIfNeeded();
lastKnownChatLength = newLength;
initButtonsForAll(); initButtonsForAll();
updateSummaryExtensionPrompt(); updateSummaryExtensionPrompt();
const store = getSummaryStore(); const store = getSummaryStore();
const lastSummarized = store?.lastSummarizedMesId ?? -1; const lastSummarized = store?.lastSummarizedMesId ?? -1;
if (lastSummarized >= 0 && store?.hideSummarizedHistory) {
if (lastSummarized >= 0 && store?.hideSummarizedHistory === true) {
const range = calcHideRange(lastSummarized); const range = calcHideRange(lastSummarized);
if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`); if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`);
} }
if (frameReady) { if (frameReady) {
sendFrameBaseData(store, lastKnownChatLength); sendFrameBaseData(store, newLength);
sendFrameFullData(store, lastKnownChatLength); sendFrameFullData(store, newLength);
} }
} }
function handleMessageDeleted() { function handleMessageDeleted() {
const { chat } = getContext(); const { chat } = getContext();
const currentLength = Array.isArray(chat) ? chat.length : 0; const currentLength = Array.isArray(chat) ? chat.length : 0;
if (currentLength < lastKnownChatLength) {
rollbackSummaryIfNeeded(); rollbackSummaryIfNeeded();
}
lastKnownChatLength = currentLength; lastKnownChatLength = currentLength;
updateSummaryExtensionPrompt(); updateSummaryExtensionPrompt();
} }
@@ -1021,7 +1091,11 @@ function handleMessageSent() {
function handleMessageUpdated() { function handleMessageUpdated() {
const { chat } = getContext(); const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0; const currentLength = Array.isArray(chat) ? chat.length : 0;
rollbackSummaryIfNeeded();
lastKnownChatLength = currentLength;
updateSummaryExtensionPrompt(); updateSummaryExtensionPrompt();
initButtonsForAll(); initButtonsForAll();
} }
@@ -1050,7 +1124,7 @@ function registerEvents() {
getSize: () => pendingFrameMessages.length, getSize: () => pendingFrameMessages.length,
getBytes: () => { getBytes: () => {
try { try {
return JSON.stringify(pendingFrameMessages || []).length * 2; // UTF-16 return JSON.stringify(pendingFrameMessages || []).length * 2;
} catch { } catch {
return 0; return 0;
} }
@@ -1061,15 +1135,18 @@ function registerEvents() {
}, },
}); });
const { chat } = getContext();
lastKnownChatLength = Array.isArray(chat) ? chat.length : 0;
initButtonsForAll(); initButtonsForAll();
events.on(event_types.CHAT_CHANGED, () => setTimeout(handleChatChanged, 80)); events.on(event_types.CHAT_CHANGED, () => setTimeout(handleChatChanged, 80));
events.on(event_types.MESSAGE_DELETED, () => setTimeout(handleMessageDeleted, 100)); events.on(event_types.MESSAGE_DELETED, () => setTimeout(handleMessageDeleted, 50));
events.on(event_types.MESSAGE_RECEIVED, () => setTimeout(handleMessageReceived, 150)); events.on(event_types.MESSAGE_RECEIVED, () => setTimeout(handleMessageReceived, 150));
events.on(event_types.MESSAGE_SENT, () => setTimeout(handleMessageSent, 150)); events.on(event_types.MESSAGE_SENT, () => setTimeout(handleMessageSent, 150));
events.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageUpdated, 150)); events.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageUpdated, 100));
events.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 150)); events.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 100));
events.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 150)); events.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 100));
events.on(event_types.USER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50)); events.on(event_types.USER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50));
events.on(event_types.CHARACTER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50)); events.on(event_types.CHARACTER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50));
} }

View File

@@ -1,7 +1,8 @@
import { eventSource, event_types, main_api, chat, name1, getRequestHeaders, extractMessageFromData, activateSendButtons, deactivateSendButtons } from "../../../../../script.js"; // 删掉:getRequestHeaders, extractMessageFromData, getStreamingReply, tryParseStreamingError, getEventSourceStream
import { getStreamingReply, chat_completion_sources, oai_settings, promptManager, getChatCompletionModel, tryParseStreamingError } from "../../../../openai.js";
import { eventSource, event_types, chat, name1, activateSendButtons, deactivateSendButtons } from "../../../../../script.js";
import { chat_completion_sources, oai_settings, promptManager, getChatCompletionModel } from "../../../../openai.js";
import { ChatCompletionService } from "../../../../custom-request.js"; import { ChatCompletionService } from "../../../../custom-request.js";
import { getEventSourceStream } from "../../../../sse-stream.js";
import { getContext } from "../../../../st-context.js"; import { getContext } from "../../../../st-context.js";
import { SlashCommandParser } from "../../../../slash-commands/SlashCommandParser.js"; import { SlashCommandParser } from "../../../../slash-commands/SlashCommandParser.js";
import { SlashCommand } from "../../../../slash-commands/SlashCommand.js"; import { SlashCommand } from "../../../../slash-commands/SlashCommand.js";
@@ -239,85 +240,55 @@ class StreamingGeneration {
if (oai_settings?.custom_exclude_body) body.custom_exclude_body = oai_settings.custom_exclude_body; if (oai_settings?.custom_exclude_body) body.custom_exclude_body = oai_settings.custom_exclude_body;
} }
if (stream) { if (stream) {
const response = await fetch('/api/backends/chat-completions/generate', { // 流式:走 ChatCompletionService 统一链路
method: 'POST', body: JSON.stringify(body), const payload = ChatCompletionService.createRequestData(body);
headers: getRequestHeaders(), signal: abortSignal, const streamFactory = await ChatCompletionService.sendRequest(payload, false, abortSignal);
}); const generator = (typeof streamFactory === 'function') ? streamFactory() : streamFactory;
if (!response.ok) {
const txt = await response.text().catch(() => '');
tryParseStreamingError(response, txt);
throw new Error(txt || `后端响应错误: ${response.status}`);
}
const eventStream = getEventSourceStream();
response.body.pipeThrough(eventStream);
const reader = eventStream.readable.getReader();
const state = { reasoning: '', image: '' };
let text = '';
return (async function* () { return (async function* () {
let last = '';
try { try {
while (true) { for await (const item of (generator || [])) {
const { done, value } = await reader.read(); if (abortSignal?.aborted) return;
if (done) return;
if (!value?.data) continue; let accumulated = '';
if (typeof item === 'string') {
const rawData = value.data; accumulated = item;
if (rawData === '[DONE]') return; } else if (item && typeof item === 'object') {
accumulated = (typeof item.text === 'string' ? item.text : '') ||
tryParseStreamingError(response, rawData); (typeof item.content === 'string' ? item.content : '') || '';
let parsed;
try {
parsed = JSON.parse(rawData);
} catch (e) {
console.warn('[StreamingGeneration] JSON parse error:', e, 'rawData:', rawData);
continue;
} }
if (!accumulated && item && typeof item === 'object') {
// 提取回复内容 const rc = item?.reasoning_content || item?.reasoning;
const chunk = getStreamingReply(parsed, state, { chatCompletionSource: source }); if (typeof rc === 'string') accumulated = rc;
let chunkText = '';
if (chunk) {
chunkText = typeof chunk === 'string' ? chunk : String(chunk);
} }
if (!accumulated) continue;
// content 为空时回退到 reasoning_content if (accumulated.startsWith(last)) {
if (!chunkText) { last = accumulated;
const delta = parsed?.choices?.[0]?.delta; } else {
const rc = delta?.reasoning_content ?? parsed?.reasoning_content; last += accumulated;
if (rc) {
chunkText = typeof rc === 'string' ? rc : String(rc);
}
}
if (chunkText) {
text += chunkText;
yield text;
} }
yield last;
} }
} catch (err) { } catch (err) {
if (err?.name !== 'AbortError') { if (err?.name === 'AbortError') return;
console.error('[StreamingGeneration] Stream error:', err); console.error('[StreamingGeneration] Stream error:', err);
try { xbLog.error('streamingGeneration', 'Stream error', err); } catch {} try { xbLog.error('streamingGeneration', 'Stream error', err); } catch {}
throw err; throw err;
} }
} finally {
try { reader.releaseLock?.(); } catch {}
}
})(); })();
} else { } else {
// 非流式extract=true返回抽取后的结果
const payload = ChatCompletionService.createRequestData(body); const payload = ChatCompletionService.createRequestData(body);
const json = await ChatCompletionService.sendRequest(payload, false, abortSignal); const extracted = await ChatCompletionService.sendRequest(payload, true, abortSignal);
let result = String(extractMessageFromData(json, ChatCompletionService.TYPE) || '');
// content 为空时回退到 reasoning_content let result = String((extracted && extracted.content) || '');
if (!result) {
const msg = json?.choices?.[0]?.message; // reasoning_content 兜底
const rc = msg?.reasoning_content ?? json?.reasoning_content; if (!result && extracted && typeof extracted === 'object') {
if (rc) { const rc = extracted?.reasoning_content || extracted?.reasoning;
result = typeof rc === 'string' ? rc : String(rc); if (typeof rc === 'string') result = rc;
}
} }
return result; return result;

View File

@@ -369,7 +369,15 @@ function installWIHiddenTagStripper() {
events?.on(evtTypes.GENERATION_ENDED, async () => { events?.on(evtTypes.GENERATION_ENDED, async () => {
try { try {
getContext()?.setExtensionPrompt?.(LWB_VAREVENT_PROMPT_KEY, '', 0, 0, false); getContext()?.setExtensionPrompt?.(LWB_VAREVENT_PROMPT_KEY, '', 0, 0, false);
const ctx = getContext();
const chat = ctx?.chat || [];
const lastMsg = chat[chat.length - 1];
if (lastMsg && !lastMsg.is_user) {
await executeQueuedVareventJsAfterTurn(); await executeQueuedVareventJsAfterTurn();
} else {
drainPendingVareventBlocks();
}
} catch {} } catch {}
}); });
} }

View File

@@ -233,22 +233,22 @@
<label for="xiaobaix_fourth_wall_enabled" class="has-tooltip" data-tooltip="突破第四面墙,与角色进行元对话交流。悬浮按钮位于页面右侧中间。">四次元壁</label> <label for="xiaobaix_fourth_wall_enabled" class="has-tooltip" data-tooltip="突破第四面墙,与角色进行元对话交流。悬浮按钮位于页面右侧中间。">四次元壁</label>
</div> </div>
<br> <br>
<div class="section-divider">剧情总结</div> <div class="section-divider">剧情管理</div>
<hr class="sysHR" /> <hr class="sysHR" />
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_story_summary_enabled" /> <input type="checkbox" id="xiaobaix_story_summary_enabled" />
<label for="xiaobaix_story_summary_enabled" class="has-tooltip" data-tooltip="在消息楼层添加总结按钮点击可打开剧情总结面板AI分析生成关键词云、时间线、人物关系、角色弧光">剧情总结面板</label> <label for="xiaobaix_story_summary_enabled" class="has-tooltip" data-tooltip="在消息楼层添加总结按钮点击可打开剧情总结面板AI分析生成关键词云、时间线、人物关系、角色弧光">剧情总结</label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_story_outline_enabled" /> <input type="checkbox" id="xiaobaix_story_outline_enabled" />
<label for="xiaobaix_story_outline_enabled" class="has-tooltip" data-tooltip="在X按钮区域添加地图图标点击可打开可视化剧情地图编辑器">剧情地图</label> <label for="xiaobaix_story_outline_enabled" class="has-tooltip" data-tooltip="在X按钮区域添加地图图标点击可打开可视化剧情地图编辑器">小白板</label>
</div> </div>
<br> <br>
<div class="section-divider">变量控制、世界书执行</div> <div class="section-divider">变量控制</div>
<hr class="sysHR" /> <hr class="sysHR" />
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_variables_core_enabled" /> <input type="checkbox" id="xiaobaix_variables_core_enabled" />
<label for="xiaobaix_variables_core_enabled">剧情管理</label> <label for="xiaobaix_variables_core_enabled">变量管理</label>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<input type="checkbox" id="xiaobaix_variables_panel_enabled" /> <input type="checkbox" id="xiaobaix_variables_panel_enabled" />
@@ -556,9 +556,9 @@
wrapperIframe: 'Wrapperiframe', wrapperIframe: 'Wrapperiframe',
renderEnabled: 'xiaobaix_render_enabled', renderEnabled: 'xiaobaix_render_enabled',
}; };
const DEFAULTS_ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary']; const DEFAULTS_ON = ['templateEditor', 'tasks', 'variablesCore', 'audio', 'storySummary', 'recorded'];
const DEFAULTS_OFF = ['recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'fourthWall', 'storyOutline' ]; const DEFAULTS_OFF = ['preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'fourthWall', 'storyOutline', 'novelDraw'];
const MODULE_KEYS = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore', 'recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'audio', 'storySummary', 'storyOutline']; const MODULE_KEYS = ['templateEditor', 'tasks', 'fourthWall', 'variablesCore', 'recorded', 'preview', 'scriptAssistant', 'immersive', 'wallhaven', 'variablesPanel', 'audio', 'storySummary', 'storyOutline', 'novelDraw'];
function setModuleEnabled(key, enabled) { function setModuleEnabled(key, enabled) {
try { try {
if (!extension_settings[EXT_ID][key]) extension_settings[EXT_ID][key] = {}; if (!extension_settings[EXT_ID][key]) extension_settings[EXT_ID][key] = {};