Add files via upload
This commit is contained in:
10
README.md
10
README.md
@@ -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
138
core/server-storage.js
Normal 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');
|
||||||
123
index.js
123
index.js
@@ -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,13 +678,13 @@ window.registerModuleCleanup = registerModuleCleanup;
|
|||||||
window.updateLittleWhiteBoxExtension = updateLittleWhiteBoxExtension;
|
window.updateLittleWhiteBoxExtension = updateLittleWhiteBoxExtension;
|
||||||
window.removeAllUpdateNotices = removeAllUpdateNotices;
|
window.removeAllUpdateNotices = removeAllUpdateNotices;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ===========================================================================
|
||||||
// 入口初始化
|
// Entry Point
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ===========================================================================
|
||||||
|
|
||||||
jQuery(async () => {
|
jQuery(async () => {
|
||||||
try {
|
try {
|
||||||
cleanupDeprecatedData();
|
cleanupDeprecatedData();
|
||||||
isXiaobaixEnabled = settings.enabled;
|
isXiaobaixEnabled = settings.enabled;
|
||||||
window.isXiaobaixEnabled = isXiaobaixEnabled;
|
window.isXiaobaixEnabled = isXiaobaixEnabled;
|
||||||
|
|
||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 request = indexedDB.open(this.dbName, 1);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
request.onsuccess = () => { this._db = request.result; resolve(this._db); };
|
|
||||||
request.onupgradeneeded = (e) => {
|
|
||||||
const db = e.target.result;
|
|
||||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
||||||
db.createObjectStore(this.storeName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async get(taskId) {
|
const settings = getSettings();
|
||||||
if (!taskId) return '';
|
for (const task of (settings.globalTasks || [])) {
|
||||||
if (this._cache.has(taskId)) return this._cache.get(taskId);
|
if (!task) continue;
|
||||||
try {
|
if (!task.id) task.id = uuidv4();
|
||||||
const db = await this.open();
|
if (task.commands) {
|
||||||
return new Promise((resolve) => {
|
await TasksStorage.set(task.id, task.commands);
|
||||||
const tx = db.transaction(this.storeName, 'readonly');
|
delete task.commands;
|
||||||
const request = tx.objectStore(this.storeName).get(taskId);
|
count++;
|
||||||
request.onerror = () => resolve('');
|
}
|
||||||
request.onsuccess = () => {
|
|
||||||
const val = request.result || '';
|
|
||||||
this._cache.set(taskId, val);
|
|
||||||
resolve(val);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch { return ''; }
|
|
||||||
},
|
|
||||||
|
|
||||||
async set(taskId, commands) {
|
|
||||||
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 {}
|
|
||||||
},
|
|
||||||
|
|
||||||
async delete(taskId) {
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
};
|
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;
|
||||||
|
if (!db.objectStoreNames.contains('scripts')) {
|
||||||
|
db.close();
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const tx = db.transaction('scripts', 'readonly');
|
||||||
|
const store = tx.objectStore('scripts');
|
||||||
|
const keys = await new Promise(r => {
|
||||||
|
const req = store.getAllKeys();
|
||||||
|
req.onsuccess = () => r(req.result || []);
|
||||||
|
req.onerror = () => r([]);
|
||||||
|
});
|
||||||
|
const vals = await new Promise(r => {
|
||||||
|
const req = store.getAll();
|
||||||
|
req.onsuccess = () => r(req.result || []);
|
||||||
|
req.onerror = () => r([]);
|
||||||
|
});
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
if (keys[i] && vals[i]) {
|
||||||
|
await TasksStorage.set(keys[i], vals[i]);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
await executeQueuedVareventJsAfterTurn();
|
const ctx = getContext();
|
||||||
|
const chat = ctx?.chat || [];
|
||||||
|
const lastMsg = chat[chat.length - 1];
|
||||||
|
if (lastMsg && !lastMsg.is_user) {
|
||||||
|
await executeQueuedVareventJsAfterTurn();
|
||||||
|
} else {
|
||||||
|
|
||||||
|
drainPendingVareventBlocks();
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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] = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user