feat: variables 2.0 state + L0 summary integration
This commit is contained in:
12
index.js
12
index.js
@@ -40,6 +40,7 @@ extension_settings[EXT_ID] = extension_settings[EXT_ID] || {
|
|||||||
audio: { enabled: true },
|
audio: { enabled: true },
|
||||||
variablesPanel: { enabled: false },
|
variablesPanel: { enabled: false },
|
||||||
variablesCore: { enabled: true },
|
variablesCore: { enabled: true },
|
||||||
|
variablesMode: '1.0',
|
||||||
storySummary: { enabled: true },
|
storySummary: { enabled: true },
|
||||||
storyOutline: { enabled: false },
|
storyOutline: { enabled: false },
|
||||||
novelDraw: { enabled: false },
|
novelDraw: { enabled: false },
|
||||||
@@ -273,7 +274,7 @@ function toggleSettingsControls(enabled) {
|
|||||||
'scheduled_tasks_enabled', 'xiaobaix_template_enabled',
|
'scheduled_tasks_enabled', 'xiaobaix_template_enabled',
|
||||||
'xiaobaix_immersive_enabled', 'xiaobaix_fourth_wall_enabled',
|
'xiaobaix_immersive_enabled', 'xiaobaix_fourth_wall_enabled',
|
||||||
'xiaobaix_audio_enabled', 'xiaobaix_variables_panel_enabled',
|
'xiaobaix_audio_enabled', 'xiaobaix_variables_panel_enabled',
|
||||||
'xiaobaix_use_blob', 'xiaobaix_variables_core_enabled', 'Wrapperiframe', 'xiaobaix_render_enabled',
|
'xiaobaix_use_blob', 'xiaobaix_variables_core_enabled', 'xiaobaix_variables_mode', 'Wrapperiframe', 'xiaobaix_render_enabled',
|
||||||
'xiaobaix_max_rendered', 'xiaobaix_story_outline_enabled', 'xiaobaix_story_summary_enabled',
|
'xiaobaix_max_rendered', 'xiaobaix_story_outline_enabled', 'xiaobaix_story_summary_enabled',
|
||||||
'xiaobaix_novel_draw_enabled', 'xiaobaix_novel_draw_open_settings',
|
'xiaobaix_novel_draw_enabled', 'xiaobaix_novel_draw_open_settings',
|
||||||
'xiaobaix_tts_enabled', 'xiaobaix_tts_open_settings'
|
'xiaobaix_tts_enabled', 'xiaobaix_tts_open_settings'
|
||||||
@@ -430,6 +431,15 @@ async function setupSettings() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// variables mode selector
|
||||||
|
$("#xiaobaix_variables_mode")
|
||||||
|
.val(settings.variablesMode || "1.0")
|
||||||
|
.on("change", function () {
|
||||||
|
settings.variablesMode = String($(this).val() || "1.0");
|
||||||
|
saveSettingsDebounced();
|
||||||
|
toastr.info(`变量系统已切换为 ${settings.variablesMode}`);
|
||||||
|
});
|
||||||
|
|
||||||
$("#xiaobaix_novel_draw_open_settings").on("click", function () {
|
$("#xiaobaix_novel_draw_open_settings").on("click", function () {
|
||||||
if (!isXiaobaixEnabled) return;
|
if (!isXiaobaixEnabled) return;
|
||||||
if (settings.novelDraw?.enabled && window.xiaobaixNovelDraw?.openSettings) {
|
if (settings.novelDraw?.enabled && window.xiaobaixNovelDraw?.openSettings) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-new-func */
|
||||||
// Story Outline 提示词模板配置
|
// Story Outline 提示词模板配置
|
||||||
// 统一 UAUA (User-Assistant-User-Assistant) 结构
|
// 统一 UAUA (User-Assistant-User-Assistant) 结构
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
/**
|
/**
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
* Story Outline 模块 - 小白板
|
* Story Outline 模块 - 小白板
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Dexie from '../../../libs/dexie.mjs';
|
import Dexie from '../../../libs/dexie.mjs';
|
||||||
|
|
||||||
const DB_NAME = 'LittleWhiteBox_Memory';
|
const DB_NAME = 'LittleWhiteBox_Memory';
|
||||||
const DB_VERSION = 2;
|
const DB_VERSION = 3; // 升级版本
|
||||||
|
|
||||||
// Chunk parameters
|
// Chunk parameters
|
||||||
export const CHUNK_MAX_TOKENS = 200;
|
export const CHUNK_MAX_TOKENS = 200;
|
||||||
@@ -15,6 +15,7 @@ db.version(DB_VERSION).stores({
|
|||||||
chunks: '[chatId+chunkId], chatId, [chatId+floor]',
|
chunks: '[chatId+chunkId], chatId, [chatId+floor]',
|
||||||
chunkVectors: '[chatId+chunkId], chatId',
|
chunkVectors: '[chatId+chunkId], chatId',
|
||||||
eventVectors: '[chatId+eventId], chatId',
|
eventVectors: '[chatId+eventId], chatId',
|
||||||
|
stateVectors: '[chatId+atomId], chatId, [chatId+floor]', // L0 向量表
|
||||||
});
|
});
|
||||||
|
|
||||||
export { db };
|
export { db };
|
||||||
@@ -22,3 +23,4 @@ export const metaTable = db.meta;
|
|||||||
export const chunksTable = db.chunks;
|
export const chunksTable = db.chunks;
|
||||||
export const chunkVectorsTable = db.chunkVectors;
|
export const chunkVectorsTable = db.chunkVectors;
|
||||||
export const eventVectorsTable = db.eventVectors;
|
export const eventVectorsTable = db.eventVectors;
|
||||||
|
export const stateVectorsTable = db.stateVectors;
|
||||||
|
|||||||
@@ -128,9 +128,16 @@ function formatArcLine(a) {
|
|||||||
return `- ${a.name}:${a.trajectory}`;
|
return `- ${a.name}:${a.trajectory}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 完整 chunk 输出(不截断)
|
// 完整 chunk 输出(支持 L0 虚拟 chunk)
|
||||||
function formatChunkFullLine(c) {
|
function formatChunkFullLine(c) {
|
||||||
const { name1, name2 } = getContext();
|
const { name1, name2 } = getContext();
|
||||||
|
|
||||||
|
// L0 虚拟 chunk
|
||||||
|
if (c.isL0) {
|
||||||
|
return `› #${c.floor + 1} [📌] ${String(c.text || "").trim()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// L1 真实 chunk
|
||||||
const speaker = c.isUser ? (name1 || "用户") : (name2 || "角色");
|
const speaker = c.isUser ? (name1 || "用户") : (name2 || "角色");
|
||||||
return `› #${c.floor + 1} [${speaker}] ${String(c.text || "").trim()}`;
|
return `› #${c.floor + 1} [${speaker}] ${String(c.text || "").trim()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ import {
|
|||||||
syncOnMessageSwiped,
|
syncOnMessageSwiped,
|
||||||
syncOnMessageReceived,
|
syncOnMessageReceived,
|
||||||
} from "./vector/chunk-builder.js";
|
} from "./vector/chunk-builder.js";
|
||||||
|
import { initStateIntegration, rebuildStateVectors } from "./vector/state-integration.js";
|
||||||
|
import { clearStateVectors, getStateAtomsCount, getStateVectorsCount } from "./vector/state-store.js";
|
||||||
|
|
||||||
// vector io
|
// vector io
|
||||||
import { exportVectors, importVectors } from "./vector/vector-io.js";
|
import { exportVectors, importVectors } from "./vector/vector-io.js";
|
||||||
@@ -210,6 +212,8 @@ async function sendVectorStatsToFrame() {
|
|||||||
const stats = await getStorageStats(chatId);
|
const stats = await getStorageStats(chatId);
|
||||||
const chunkStatus = await getChunkBuildStatus();
|
const chunkStatus = await getChunkBuildStatus();
|
||||||
const totalMessages = chat?.length || 0;
|
const totalMessages = chat?.length || 0;
|
||||||
|
const stateAtomsCount = getStateAtomsCount();
|
||||||
|
const stateVectorsCount = await getStateVectorsCount(chatId);
|
||||||
|
|
||||||
const cfg = getVectorConfig();
|
const cfg = getVectorConfig();
|
||||||
let mismatch = false;
|
let mismatch = false;
|
||||||
@@ -228,6 +232,8 @@ async function sendVectorStatsToFrame() {
|
|||||||
builtFloors: chunkStatus.builtFloors,
|
builtFloors: chunkStatus.builtFloors,
|
||||||
totalFloors: chunkStatus.totalFloors,
|
totalFloors: chunkStatus.totalFloors,
|
||||||
totalMessages,
|
totalMessages,
|
||||||
|
stateAtoms: stateAtomsCount,
|
||||||
|
stateVectors: stateVectorsCount,
|
||||||
},
|
},
|
||||||
mismatch,
|
mismatch,
|
||||||
});
|
});
|
||||||
@@ -350,6 +356,14 @@ async function handleGenerateVectors(vectorCfg) {
|
|||||||
const batchSize = isLocal ? 5 : 25;
|
const batchSize = isLocal ? 5 : 25;
|
||||||
const concurrency = isLocal ? 1 : 2;
|
const concurrency = isLocal ? 1 : 2;
|
||||||
|
|
||||||
|
// L0 向量重建
|
||||||
|
try {
|
||||||
|
await rebuildStateVectors(chatId, vectorCfg);
|
||||||
|
} catch (e) {
|
||||||
|
xbLog.error(MODULE_ID, "L0 向量重建失败", e);
|
||||||
|
// 不阻塞,继续 L1/L2
|
||||||
|
}
|
||||||
|
|
||||||
await clearAllChunks(chatId);
|
await clearAllChunks(chatId);
|
||||||
await updateMeta(chatId, { lastChunkFloor: -1, fingerprint });
|
await updateMeta(chatId, { lastChunkFloor: -1, fingerprint });
|
||||||
|
|
||||||
@@ -649,6 +663,7 @@ async function handleClearVectors() {
|
|||||||
|
|
||||||
await clearEventVectors(chatId);
|
await clearEventVectors(chatId);
|
||||||
await clearAllChunks(chatId);
|
await clearAllChunks(chatId);
|
||||||
|
await clearStateVectors(chatId);
|
||||||
await updateMeta(chatId, { lastChunkFloor: -1 });
|
await updateMeta(chatId, { lastChunkFloor: -1 });
|
||||||
await sendVectorStatsToFrame();
|
await sendVectorStatsToFrame();
|
||||||
await executeSlashCommand('/echo severity=info 向量数据已清除。如需恢复召回功能,请重新点击"生成向量"。');
|
await executeSlashCommand('/echo severity=info 向量数据已清除。如需恢复召回功能,请重新点击"生成向量"。');
|
||||||
@@ -1400,7 +1415,7 @@ async function handleGenerationStarted(type, _params, isDryRun) {
|
|||||||
|
|
||||||
// 2) depth:倒序插入,从末尾往前数
|
// 2) depth:倒序插入,从末尾往前数
|
||||||
// 最小为 1,避免插入到最底部导致 AI 看到的最后是总结
|
// 最小为 1,避免插入到最底部导致 AI 看到的最后是总结
|
||||||
const depth = Math.max(1, chatLen - boundary - 1);
|
const depth = Math.max(2, chatLen - boundary - 1);
|
||||||
if (depth < 0) return;
|
if (depth < 0) return;
|
||||||
|
|
||||||
// 3) 构建注入文本(保持原逻辑)
|
// 3) 构建注入文本(保持原逻辑)
|
||||||
@@ -1504,4 +1519,5 @@ $(document).on("xiaobaix:storySummary:toggle", (_e, enabled) => {
|
|||||||
jQuery(() => {
|
jQuery(() => {
|
||||||
if (!getSettings().storySummary?.enabled) return;
|
if (!getSettings().storySummary?.enabled) return;
|
||||||
registerEvents();
|
registerEvents();
|
||||||
|
initStateIntegration();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ export function chunkMessage(floor, message, maxTokens = CHUNK_MAX_TOKENS) {
|
|||||||
|
|
||||||
// 1. 应用用户自定义过滤规则
|
// 1. 应用用户自定义过滤规则
|
||||||
// 2. 移除 TTS 标记(硬编码)
|
// 2. 移除 TTS 标记(硬编码)
|
||||||
|
// 3. 移除 <state> 标签(硬编码,L0 已单独存储)
|
||||||
const cleanText = filterText(text)
|
const cleanText = filterText(text)
|
||||||
.replace(/\[tts:[^\]]*\]/gi, '')
|
.replace(/\[tts:[^\]]*\]/gi, '')
|
||||||
|
.replace(/<state>[\s\S]*?<\/state>/gi, '')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
if (!cleanText) return [];
|
if (!cleanText) return [];
|
||||||
|
|||||||
@@ -471,8 +471,6 @@ async function embedOnline(texts, provider, config, options = {}) {
|
|||||||
const providerConfig = ONLINE_PROVIDERS[provider];
|
const providerConfig = ONLINE_PROVIDERS[provider];
|
||||||
const baseUrl = (providerConfig?.baseUrl || url || '').replace(/\/+$/, '');
|
const baseUrl = (providerConfig?.baseUrl || url || '').replace(/\/+$/, '');
|
||||||
|
|
||||||
const reqId = Math.random().toString(36).slice(2, 6);
|
|
||||||
|
|
||||||
// 永远重试:指数退避 + 上限 + 抖动
|
// 永远重试:指数退避 + 上限 + 抖动
|
||||||
const BASE_WAIT_MS = 1200;
|
const BASE_WAIT_MS = 1200;
|
||||||
const MAX_WAIT_MS = 15000;
|
const MAX_WAIT_MS = 15000;
|
||||||
@@ -491,9 +489,6 @@ async function embedOnline(texts, provider, config, options = {}) {
|
|||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
attempt++;
|
attempt++;
|
||||||
const startTime = Date.now();
|
|
||||||
console.log(`[embed ${reqId}] send ${texts.length} items (attempt ${attempt})`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
@@ -526,8 +521,6 @@ async function embedOnline(texts, provider, config, options = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[embed ${reqId}] status=${response.status} time=${Date.now() - startTime}ms`);
|
|
||||||
|
|
||||||
// 需要“永远重试”的典型状态:
|
// 需要“永远重试”的典型状态:
|
||||||
// - 429:限流
|
// - 429:限流
|
||||||
// - 403:配额/风控/未实名等(你提到的硅基未认证)
|
// - 403:配额/风控/未实名等(你提到的硅基未认证)
|
||||||
@@ -541,7 +534,6 @@ async function embedOnline(texts, provider, config, options = {}) {
|
|||||||
const exp = Math.min(MAX_WAIT_MS, BASE_WAIT_MS * Math.pow(2, Math.min(attempt, 6) - 1));
|
const exp = Math.min(MAX_WAIT_MS, BASE_WAIT_MS * Math.pow(2, Math.min(attempt, 6) - 1));
|
||||||
const jitter = Math.floor(Math.random() * 350);
|
const jitter = Math.floor(Math.random() * 350);
|
||||||
const waitMs = exp + jitter;
|
const waitMs = exp + jitter;
|
||||||
console.warn(`[embed ${reqId}] retryable error ${response.status}, wait ${waitMs}ms`);
|
|
||||||
await sleepAbortable(waitMs);
|
await sleepAbortable(waitMs);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -569,7 +561,6 @@ async function embedOnline(texts, provider, config, options = {}) {
|
|||||||
const exp = Math.min(MAX_WAIT_MS, BASE_WAIT_MS * Math.pow(2, Math.min(attempt, 6) - 1));
|
const exp = Math.min(MAX_WAIT_MS, BASE_WAIT_MS * Math.pow(2, Math.min(attempt, 6) - 1));
|
||||||
const jitter = Math.floor(Math.random() * 350);
|
const jitter = Math.floor(Math.random() * 350);
|
||||||
const waitMs = exp + jitter;
|
const waitMs = exp + jitter;
|
||||||
console.warn(`[embed ${reqId}] network/error, wait ${waitMs}ms then retry: ${e?.message || e}`);
|
|
||||||
await sleepAbortable(waitMs);
|
await sleepAbortable(waitMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ import { xbLog } from '../../../core/debug-core.js';
|
|||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { getSummaryStore } from '../data/store.js';
|
import { getSummaryStore } from '../data/store.js';
|
||||||
import { filterText } from './text-filter.js';
|
import { filterText } from './text-filter.js';
|
||||||
|
import {
|
||||||
|
searchStateAtoms,
|
||||||
|
buildL0FloorBonus,
|
||||||
|
stateToVirtualChunks,
|
||||||
|
mergeAndSparsify,
|
||||||
|
} from './state-recall.js';
|
||||||
|
|
||||||
const MODULE_ID = 'recall';
|
const MODULE_ID = 'recall';
|
||||||
|
|
||||||
@@ -39,6 +45,10 @@ const CONFIG = {
|
|||||||
BONUS_TEXT_HIT: 0.05,
|
BONUS_TEXT_HIT: 0.05,
|
||||||
BONUS_WORLD_TOPIC_HIT: 0.06,
|
BONUS_WORLD_TOPIC_HIT: 0.06,
|
||||||
|
|
||||||
|
// L0 配置
|
||||||
|
L0_FLOOR_BONUS_FACTOR: 0.10,
|
||||||
|
FLOOR_MAX_CHUNKS: 2,
|
||||||
|
|
||||||
FLOOR_LIMIT: 1,
|
FLOOR_LIMIT: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,6 +150,16 @@ function normalize(s) {
|
|||||||
return String(s || '').normalize('NFKC').replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
|
return String(s || '').normalize('NFKC').replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 summary 解析楼层范围:(#321-322) 或 (#321)
|
||||||
|
function parseFloorRange(summary) {
|
||||||
|
if (!summary) return null;
|
||||||
|
const match = String(summary).match(/\(#(\d+)(?:-(\d+))?\)/);
|
||||||
|
if (!match) return null;
|
||||||
|
const start = Math.max(0, parseInt(match[1], 10) - 1);
|
||||||
|
const end = Math.max(0, (match[2] ? parseInt(match[2], 10) : parseInt(match[1], 10)) - 1);
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
function cleanForRecall(text) {
|
function cleanForRecall(text) {
|
||||||
// 1. 应用用户自定义过滤规则
|
// 1. 应用用户自定义过滤规则
|
||||||
// 2. 移除 TTS 标记(硬编码)
|
// 2. 移除 TTS 标记(硬编码)
|
||||||
@@ -308,7 +328,7 @@ function mmrSelect(candidates, k, lambda, getVector, getScore) {
|
|||||||
// L1 Chunks 检索
|
// L1 Chunks 检索
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
async function searchChunks(queryVector, vectorConfig) {
|
async function searchChunks(queryVector, vectorConfig, l0FloorBonus = new Map()) {
|
||||||
const { chatId } = getContext();
|
const { chatId } = getContext();
|
||||||
if (!chatId || !queryVector?.length) return [];
|
if (!chatId || !queryVector?.length) return [];
|
||||||
|
|
||||||
@@ -321,12 +341,18 @@ async function searchChunks(queryVector, vectorConfig) {
|
|||||||
|
|
||||||
const scored = chunkVectors.map(cv => {
|
const scored = chunkVectors.map(cv => {
|
||||||
const match = String(cv.chunkId).match(/c-(\d+)-(\d+)/);
|
const match = String(cv.chunkId).match(/c-(\d+)-(\d+)/);
|
||||||
|
const floor = match ? parseInt(match[1], 10) : 0;
|
||||||
|
const baseSim = cosineSimilarity(queryVector, cv.vector);
|
||||||
|
const l0Bonus = l0FloorBonus.get(floor) || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: cv.chunkId,
|
_id: cv.chunkId,
|
||||||
chunkId: cv.chunkId,
|
chunkId: cv.chunkId,
|
||||||
floor: match ? parseInt(match[1], 10) : 0,
|
floor,
|
||||||
chunkIdx: match ? parseInt(match[2], 10) : 0,
|
chunkIdx: match ? parseInt(match[2], 10) : 0,
|
||||||
similarity: cosineSimilarity(queryVector, cv.vector),
|
similarity: baseSim + l0Bonus,
|
||||||
|
_baseSimilarity: baseSim,
|
||||||
|
_l0Bonus: l0Bonus,
|
||||||
vector: cv.vector,
|
vector: cv.vector,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -403,29 +429,18 @@ async function searchChunks(queryVector, vectorConfig) {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// L2 Events 检索
|
// L2 Events 检索
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEntities, l0FloorBonus = new Map()) {
|
async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEntities, l0FloorBonus = new Map()) {
|
||||||
const { chatId, name1 } = getContext();
|
const { chatId, name1 } = getContext();
|
||||||
if (!chatId || !queryVector?.length) {
|
|
||||||
if (!chatId || !queryVector?.length) {
|
if (!chatId || !queryVector?.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = await getMeta(chatId);
|
const meta = await getMeta(chatId);
|
||||||
const fp = getEngineFingerprint(vectorConfig);
|
|
||||||
console.log('[searchEvents] fingerprint检查:', {
|
|
||||||
metaFp: meta.fingerprint,
|
|
||||||
currentFp: fp,
|
|
||||||
match: meta.fingerprint === fp || !meta.fingerprint,
|
|
||||||
const fp = getEngineFingerprint(vectorConfig);
|
const fp = getEngineFingerprint(vectorConfig);
|
||||||
if (meta.fingerprint && meta.fingerprint !== fp) return [];
|
if (meta.fingerprint && meta.fingerprint !== fp) return [];
|
||||||
|
|
||||||
const eventVectors = await getAllEventVectors(chatId);
|
const eventVectors = await getAllEventVectors(chatId);
|
||||||
const vectorMap = new Map(eventVectors.map(v => [v.eventId, v.vector]));
|
|
||||||
console.log('[searchEvents] 向量数据:', {
|
|
||||||
eventVectorsCount: eventVectors.length,
|
|
||||||
vectorMapSize: vectorMap.size,
|
|
||||||
allEventsCount: allEvents?.length,
|
|
||||||
const vectorMap = new Map(eventVectors.map(v => [v.eventId, v.vector]));
|
const vectorMap = new Map(eventVectors.map(v => [v.eventId, v.vector]));
|
||||||
if (!vectorMap.size) return [];
|
if (!vectorMap.size) return [];
|
||||||
|
|
||||||
@@ -464,6 +479,18 @@ async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEn
|
|||||||
bonus += CONFIG.BONUS_WORLD_TOPIC_HIT;
|
bonus += CONFIG.BONUS_WORLD_TOPIC_HIT;
|
||||||
reasons.push('world');
|
reasons.push('world');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// L0 加权:事件覆盖楼层范围命中
|
||||||
|
const range = parseFloorRange(event.summary);
|
||||||
|
if (range) {
|
||||||
|
for (let f = range.start; f <= range.end; f++) {
|
||||||
|
if (l0FloorBonus.has(f)) {
|
||||||
|
bonus += l0FloorBonus.get(f);
|
||||||
|
reasons.push('L0');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_id: event.id,
|
_id: event.id,
|
||||||
@@ -477,15 +504,6 @@ async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEn
|
|||||||
vector: v,
|
vector: v,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 相似度分布日志
|
|
||||||
const simValues = scored.map(s => s.similarity).sort((a, b) => b - a);
|
|
||||||
console.log('[searchEvents] 相似度分布(前20):', simValues.slice(0, 20));
|
|
||||||
console.log('[searchEvents] 相似度分布(后20):', simValues.slice(-20));
|
|
||||||
console.log('[searchEvents] 有向量的事件数:', scored.filter(s => s.similarity > 0).length);
|
|
||||||
console.log('[searchEvents] sim >= 0.6:', scored.filter(s => s.similarity >= 0.6).length);
|
|
||||||
console.log('[searchEvents] sim >= 0.5:', scored.filter(s => s.similarity >= 0.5).length);
|
|
||||||
console.log('[searchEvents] sim >= 0.3:', scored.filter(s => s.similarity >= 0.3).length);
|
|
||||||
|
|
||||||
// ★ 记录过滤前的分布(用 finalScore,与显示一致)
|
// ★ 记录过滤前的分布(用 finalScore,与显示一致)
|
||||||
const preFilterDistribution = {
|
const preFilterDistribution = {
|
||||||
@@ -503,7 +521,6 @@ async function searchEvents(queryVector, allEvents, vectorConfig, store, queryEn
|
|||||||
const candidates = scored
|
const candidates = scored
|
||||||
.filter(s => s.finalScore >= CONFIG.MIN_SIMILARITY_EVENT)
|
.filter(s => s.finalScore >= CONFIG.MIN_SIMILARITY_EVENT)
|
||||||
.sort((a, b) => b.finalScore - a.finalScore)
|
.sort((a, b) => b.finalScore - a.finalScore)
|
||||||
.slice(0, CONFIG.CANDIDATE_EVENTS);
|
|
||||||
.slice(0, CONFIG.CANDIDATE_EVENTS);
|
.slice(0, CONFIG.CANDIDATE_EVENTS);
|
||||||
|
|
||||||
// 动态 K:质量不够就少拿
|
// 动态 K:质量不够就少拿
|
||||||
@@ -575,7 +592,7 @@ function formatCausalTree(causalEvents, recalledEvents) {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 日志:主报告
|
// 日志:主报告
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
function formatRecallLog({ elapsed, segments, weights, chunkResults, eventResults, allEvents, queryEntities, causalEvents = [], chunkPreFilterStats = null, l0Results = [] }) {
|
function formatRecallLog({ elapsed, segments, weights, chunkResults, eventResults, allEvents, queryEntities, causalEvents = [], chunkPreFilterStats = null, l0Results = [] }) {
|
||||||
const lines = [
|
const lines = [
|
||||||
'╔══════════════════════════════════════════════════════════════╗',
|
'╔══════════════════════════════════════════════════════════════╗',
|
||||||
@@ -609,6 +626,29 @@ function formatRecallLog({ elapsed, segments, weights, chunkResults, eventResult
|
|||||||
lines.push('│ 【提取实体】用于判断"亲身经历"(DIRECT) │');
|
lines.push('│ 【提取实体】用于判断"亲身经历"(DIRECT) │');
|
||||||
lines.push('└─────────────────────────────────────────────────────────────┘');
|
lines.push('└─────────────────────────────────────────────────────────────┘');
|
||||||
lines.push(` ${queryEntities?.length ? queryEntities.join('、') : '(无)'}`);
|
lines.push(` ${queryEntities?.length ? queryEntities.join('、') : '(无)'}`);
|
||||||
|
|
||||||
|
lines.push('');
|
||||||
|
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
||||||
|
lines.push('│ 【L0 语义锚点】状态变更加权信号 │');
|
||||||
|
lines.push('└─────────────────────────────────────────────────────────────┘');
|
||||||
|
|
||||||
|
if (l0Results.length) {
|
||||||
|
const l0Floors = [...new Set(l0Results.map(r => r.floor))].sort((a, b) => a - b);
|
||||||
|
lines.push(` 召回: ${l0Results.length} 条`);
|
||||||
|
lines.push(` 影响楼层: ${l0Floors.join(', ')}(L1/L2 候选在这些楼层获得 +${CONFIG.L0_FLOOR_BONUS_FACTOR} 加分)`);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
l0Results.slice(0, 10).forEach((r, i) => {
|
||||||
|
lines.push(` ${String(i + 1).padStart(2)}. #${r.floor} ${r.atom.semantic.slice(0, 50)}${r.atom.semantic.length > 50 ? '...' : ''}`);
|
||||||
|
lines.push(` 相似度: ${r.similarity.toFixed(3)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (l0Results.length > 10) {
|
||||||
|
lines.push(` ... 还有 ${l0Results.length - 10} 条`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(' 召回: 0 条(无 L0 数据或未启用)');
|
||||||
|
}
|
||||||
|
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
lines.push('┌─────────────────────────────────────────────────────────────┐');
|
||||||
@@ -709,13 +749,33 @@ export async function recallMemory(queryText, allEvents, vectorConfig, options =
|
|||||||
|
|
||||||
const lexicon = buildEntityLexicon(store, allEvents);
|
const lexicon = buildEntityLexicon(store, allEvents);
|
||||||
const queryEntities = extractEntities([queryText, ...segments].join('\n'), lexicon);
|
const queryEntities = extractEntities([queryText, ...segments].join('\n'), lexicon);
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// L0 召回
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
let l0Results = [];
|
||||||
|
let l0FloorBonus = new Map();
|
||||||
|
let l0VirtualChunks = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
l0Results = await searchStateAtoms(queryVector, vectorConfig);
|
||||||
|
l0FloorBonus = buildL0FloorBonus(l0Results, CONFIG.L0_FLOOR_BONUS_FACTOR);
|
||||||
|
l0VirtualChunks = stateToVirtualChunks(l0Results);
|
||||||
|
} catch (e) {
|
||||||
|
xbLog.warn(MODULE_ID, 'L0 召回失败,降级处理', e);
|
||||||
|
}
|
||||||
|
|
||||||
const [chunkResults, eventResults] = await Promise.all([
|
const [chunkResults, eventResults] = await Promise.all([
|
||||||
searchChunks(queryVector, vectorConfig),
|
searchChunks(queryVector, vectorConfig, l0FloorBonus),
|
||||||
searchEvents(queryVector, allEvents, vectorConfig, store, queryEntities, l0FloorBonus),
|
searchEvents(queryVector, allEvents, vectorConfig, store, queryEntities, l0FloorBonus),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const chunkPreFilterStats = chunkResults._preFilterStats || null;
|
const chunkPreFilterStats = chunkResults._preFilterStats || null;
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
// 合并 L0 虚拟 chunks 到 L1
|
||||||
|
// ════════════════════════════════════════════════════════════════════════
|
||||||
|
const mergedChunks = mergeAndSparsify(l0VirtualChunks, chunkResults, CONFIG.FLOOR_MAX_CHUNKS);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
// 因果链追溯:从 eventResults 出发找祖先事件
|
// 因果链追溯:从 eventResults 出发找祖先事件
|
||||||
@@ -747,20 +807,21 @@ export async function recallMemory(queryText, allEvents, vectorConfig, options =
|
|||||||
elapsed,
|
elapsed,
|
||||||
queryText,
|
queryText,
|
||||||
segments,
|
segments,
|
||||||
weights,
|
weights,
|
||||||
chunkResults: mergedChunks,
|
chunkResults: mergedChunks,
|
||||||
eventResults,
|
eventResults,
|
||||||
allEvents,
|
allEvents,
|
||||||
queryEntities,
|
queryEntities,
|
||||||
causalEvents: causalEventsTruncated,
|
causalEvents: causalEventsTruncated,
|
||||||
|
chunkPreFilterStats,
|
||||||
l0Results,
|
l0Results,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.group('%c[Recall]', 'color: #7c3aed; font-weight: bold');
|
console.group('%c[Recall]', 'color: #7c3aed; font-weight: bold');
|
||||||
console.log(`Elapsed: ${elapsed}ms | Entities: ${queryEntities.join(', ') || '(none)'}`);
|
console.log(`Elapsed: ${elapsed}ms | L0: ${l0Results.length} | Entities: ${queryEntities.join(', ') || '(none)'}`);
|
||||||
console.log(`L1: ${mergedChunks.length} | L2: ${eventResults.length}/${allEvents.length} | Causal: ${causalEventsTruncated.length}`);
|
console.log(`L1: ${mergedChunks.length} | L2: ${eventResults.length}/${allEvents.length} | Causal: ${causalEventsTruncated.length}`);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
return { events: eventResults, causalEvents: causalEventsTruncated, chunks: mergedChunks, elapsed, logText, queryEntities, l0Results };
|
return { events: eventResults, causalEvents: causalEventsTruncated, chunks: mergedChunks, elapsed, logText, queryEntities, l0Results };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
153
modules/story-summary/vector/state-integration.js
Normal file
153
modules/story-summary/vector/state-integration.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Story Summary - State Integration (L0)
|
||||||
|
// 事件监听 + 回滚钩子注册
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import { xbLog } from '../../../core/debug-core.js';
|
||||||
|
import {
|
||||||
|
saveStateAtoms,
|
||||||
|
saveStateVectors,
|
||||||
|
deleteStateAtomsFromFloor,
|
||||||
|
deleteStateVectorsFromFloor,
|
||||||
|
getStateAtoms,
|
||||||
|
clearStateVectors,
|
||||||
|
} from './state-store.js';
|
||||||
|
import { embed, getEngineFingerprint } from './embedder.js';
|
||||||
|
import { getVectorConfig } from '../data/config.js';
|
||||||
|
|
||||||
|
const MODULE_ID = 'state-integration';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 初始化
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export function initStateIntegration() {
|
||||||
|
if (initialized) return;
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
// 监听变量团队的事件
|
||||||
|
$(document).on('xiaobaix:variables:stateAtomsGenerated', handleStateAtomsGenerated);
|
||||||
|
|
||||||
|
// 注册回滚钩子
|
||||||
|
globalThis.LWB_StateRollbackHook = handleStateRollback;
|
||||||
|
|
||||||
|
xbLog.info(MODULE_ID, 'L0 状态层集成已初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 事件处理
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async function handleStateAtomsGenerated(e, data) {
|
||||||
|
const { atoms } = data || {};
|
||||||
|
if (!atoms?.length) return;
|
||||||
|
|
||||||
|
const { chatId } = getContext();
|
||||||
|
if (!chatId) return;
|
||||||
|
|
||||||
|
const validAtoms = atoms.filter(a => a?.chatId === chatId);
|
||||||
|
if (!validAtoms.length) {
|
||||||
|
xbLog.warn(MODULE_ID, `atoms.chatId 不匹配,期望 ${chatId},跳过`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xbLog.info(MODULE_ID, `收到 ${validAtoms.length} 个 StateAtom`);
|
||||||
|
|
||||||
|
// 1. 存入 chat_metadata(持久化)
|
||||||
|
saveStateAtoms(validAtoms);
|
||||||
|
|
||||||
|
// 2. 向量化并存入 IndexedDB
|
||||||
|
const vectorCfg = getVectorConfig();
|
||||||
|
if (!vectorCfg?.enabled) {
|
||||||
|
xbLog.info(MODULE_ID, '向量未启用,跳过 L0 向量化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await vectorizeAtoms(chatId, validAtoms, vectorCfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function vectorizeAtoms(chatId, atoms, vectorCfg) {
|
||||||
|
const texts = atoms.map(a => a.semantic);
|
||||||
|
const fingerprint = getEngineFingerprint(vectorCfg);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const vectors = await embed(texts, vectorCfg);
|
||||||
|
|
||||||
|
const items = atoms.map((a, i) => ({
|
||||||
|
atomId: a.atomId,
|
||||||
|
floor: a.floor,
|
||||||
|
vector: vectors[i],
|
||||||
|
}));
|
||||||
|
|
||||||
|
await saveStateVectors(chatId, items, fingerprint);
|
||||||
|
xbLog.info(MODULE_ID, `L0 向量化完成: ${items.length} 个`);
|
||||||
|
} catch (e) {
|
||||||
|
xbLog.error(MODULE_ID, 'L0 向量化失败', e);
|
||||||
|
// 不阻塞,向量可后续通过"生成向量"重建
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 回滚钩子
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async function handleStateRollback(floor) {
|
||||||
|
xbLog.info(MODULE_ID, `收到回滚请求: floor >= ${floor}`);
|
||||||
|
|
||||||
|
const { chatId } = getContext();
|
||||||
|
|
||||||
|
// 1. 删除 chat_metadata 中的 atoms
|
||||||
|
deleteStateAtomsFromFloor(floor);
|
||||||
|
|
||||||
|
// 2. 删除 IndexedDB 中的 vectors
|
||||||
|
if (chatId) {
|
||||||
|
await deleteStateVectorsFromFloor(chatId, floor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 重建向量(供"生成向量"按钮调用)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export async function rebuildStateVectors(chatId, vectorCfg) {
|
||||||
|
if (!chatId || !vectorCfg?.enabled) return { built: 0 };
|
||||||
|
|
||||||
|
const atoms = getStateAtoms();
|
||||||
|
if (!atoms.length) return { built: 0 };
|
||||||
|
|
||||||
|
xbLog.info(MODULE_ID, `开始重建 L0 向量: ${atoms.length} 个 atom`);
|
||||||
|
|
||||||
|
// 清空旧向量
|
||||||
|
await clearStateVectors(chatId);
|
||||||
|
|
||||||
|
// 重新向量化
|
||||||
|
const fingerprint = getEngineFingerprint(vectorCfg);
|
||||||
|
const batchSize = vectorCfg.engine === 'local' ? 5 : 25;
|
||||||
|
let built = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < atoms.length; i += batchSize) {
|
||||||
|
const batch = atoms.slice(i, i + batchSize);
|
||||||
|
const texts = batch.map(a => a.semantic);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const vectors = await embed(texts, vectorCfg);
|
||||||
|
|
||||||
|
const items = batch.map((a, j) => ({
|
||||||
|
atomId: a.atomId,
|
||||||
|
floor: a.floor,
|
||||||
|
vector: vectors[j],
|
||||||
|
}));
|
||||||
|
|
||||||
|
await saveStateVectors(chatId, items, fingerprint);
|
||||||
|
built += items.length;
|
||||||
|
} catch (e) {
|
||||||
|
xbLog.error(MODULE_ID, `L0 向量化批次失败: ${i}-${i + batchSize}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xbLog.info(MODULE_ID, `L0 向量重建完成: ${built}/${atoms.length}`);
|
||||||
|
return { built };
|
||||||
|
}
|
||||||
160
modules/story-summary/vector/state-recall.js
Normal file
160
modules/story-summary/vector/state-recall.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Story Summary - State Recall (L0)
|
||||||
|
// L0 语义锚点召回 + floor bonus + 虚拟 chunk 转换
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import { getAllStateVectors, getStateAtoms } from './state-store.js';
|
||||||
|
import { getMeta } from './chunk-store.js';
|
||||||
|
import { getEngineFingerprint } from './embedder.js';
|
||||||
|
import { xbLog } from '../../../core/debug-core.js';
|
||||||
|
|
||||||
|
const MODULE_ID = 'state-recall';
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
MAX_RESULTS: 20,
|
||||||
|
MIN_SIMILARITY: 0.55,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 工具函数
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function cosineSimilarity(a, b) {
|
||||||
|
if (!a?.length || !b?.length || a.length !== b.length) return 0;
|
||||||
|
let dot = 0, nA = 0, nB = 0;
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
dot += a[i] * b[i];
|
||||||
|
nA += a[i] * a[i];
|
||||||
|
nB += b[i] * b[i];
|
||||||
|
}
|
||||||
|
return nA && nB ? dot / (Math.sqrt(nA) * Math.sqrt(nB)) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// L0 向量检索
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检索与 query 相似的 StateAtoms
|
||||||
|
* @returns {Array<{atom, similarity}>}
|
||||||
|
*/
|
||||||
|
export async function searchStateAtoms(queryVector, vectorConfig) {
|
||||||
|
const { chatId } = getContext();
|
||||||
|
if (!chatId || !queryVector?.length) return [];
|
||||||
|
|
||||||
|
// 检查 fingerprint
|
||||||
|
const meta = await getMeta(chatId);
|
||||||
|
const fp = getEngineFingerprint(vectorConfig);
|
||||||
|
if (meta.fingerprint && meta.fingerprint !== fp) {
|
||||||
|
xbLog.warn(MODULE_ID, 'fingerprint 不匹配,跳过 L0 召回');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取向量
|
||||||
|
const stateVectors = await getAllStateVectors(chatId);
|
||||||
|
if (!stateVectors.length) return [];
|
||||||
|
|
||||||
|
// 获取 atoms(用于关联 semantic 等字段)
|
||||||
|
const atoms = getStateAtoms();
|
||||||
|
const atomMap = new Map(atoms.map(a => [a.atomId, a]));
|
||||||
|
|
||||||
|
// 计算相似度
|
||||||
|
const scored = stateVectors
|
||||||
|
.map(sv => {
|
||||||
|
const atom = atomMap.get(sv.atomId);
|
||||||
|
if (!atom) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
atomId: sv.atomId,
|
||||||
|
floor: sv.floor,
|
||||||
|
similarity: cosineSimilarity(queryVector, sv.vector),
|
||||||
|
atom,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(s => s.similarity >= CONFIG.MIN_SIMILARITY)
|
||||||
|
.sort((a, b) => b.similarity - a.similarity)
|
||||||
|
.slice(0, CONFIG.MAX_RESULTS);
|
||||||
|
|
||||||
|
return scored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Floor Bonus 构建
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 L0 相关楼层的加权映射
|
||||||
|
* @returns {Map<number, number>}
|
||||||
|
*/
|
||||||
|
export function buildL0FloorBonus(l0Results, bonusFactor = 0.10) {
|
||||||
|
const floorBonus = new Map();
|
||||||
|
|
||||||
|
for (const r of l0Results || []) {
|
||||||
|
// 每个楼层只加一次,取最高相似度对应的 bonus
|
||||||
|
// 简化处理:统一加 bonusFactor,不区分相似度高低
|
||||||
|
if (!floorBonus.has(r.floor)) {
|
||||||
|
floorBonus.set(r.floor, bonusFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return floorBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 虚拟 Chunk 转换
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 L0 结果转换为虚拟 chunk 格式
|
||||||
|
* 用于和 L1 chunks 统一处理
|
||||||
|
*/
|
||||||
|
export function stateToVirtualChunks(l0Results) {
|
||||||
|
return (l0Results || []).map(r => ({
|
||||||
|
chunkId: `state-${r.atomId}`,
|
||||||
|
floor: r.floor,
|
||||||
|
chunkIdx: -1, // 负值,排序时排在 L1 前面
|
||||||
|
speaker: '📌', // 固定标记
|
||||||
|
isUser: false,
|
||||||
|
text: r.atom.semantic,
|
||||||
|
textHash: null,
|
||||||
|
similarity: r.similarity,
|
||||||
|
isL0: true, // 标记字段
|
||||||
|
// 保留原始 atom 信息
|
||||||
|
_atom: r.atom,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 每楼层稀疏去重
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并 L0 和 L1 chunks,每楼层最多保留 limit 条
|
||||||
|
* @param {Array} l0Chunks - 虚拟 chunks(已按相似度排序)
|
||||||
|
* @param {Array} l1Chunks - 真实 chunks(已按相似度排序)
|
||||||
|
* @param {number} limit - 每楼层上限
|
||||||
|
* @returns {Array} 合并后的 chunks
|
||||||
|
*/
|
||||||
|
export function mergeAndSparsify(l0Chunks, l1Chunks, limit = 2) {
|
||||||
|
// 合并并按相似度排序
|
||||||
|
const all = [...(l0Chunks || []), ...(l1Chunks || [])]
|
||||||
|
.sort((a, b) => b.similarity - a.similarity);
|
||||||
|
|
||||||
|
// 每楼层稀疏去重
|
||||||
|
const byFloor = new Map();
|
||||||
|
|
||||||
|
for (const c of all) {
|
||||||
|
const arr = byFloor.get(c.floor) || [];
|
||||||
|
if (arr.length < limit) {
|
||||||
|
arr.push(c);
|
||||||
|
byFloor.set(c.floor, arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扁平化并保持相似度排序
|
||||||
|
return Array.from(byFloor.values())
|
||||||
|
.flat()
|
||||||
|
.sort((a, b) => b.similarity - a.similarity);
|
||||||
|
}
|
||||||
187
modules/story-summary/vector/state-store.js
Normal file
187
modules/story-summary/vector/state-store.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Story Summary - State Store (L0)
|
||||||
|
// StateAtom 存 chat_metadata(持久化)
|
||||||
|
// StateVector 存 IndexedDB(可重建)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
import { saveMetadataDebounced } from '../../../../../../extensions.js';
|
||||||
|
import { chat_metadata } from '../../../../../../../script.js';
|
||||||
|
import { stateVectorsTable } from '../data/db.js';
|
||||||
|
import { EXT_ID } from '../../../core/constants.js';
|
||||||
|
import { xbLog } from '../../../core/debug-core.js';
|
||||||
|
|
||||||
|
const MODULE_ID = 'state-store';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 工具函数
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export function float32ToBuffer(arr) {
|
||||||
|
return arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bufferToFloat32(buffer) {
|
||||||
|
return new Float32Array(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// StateAtom 操作(chat_metadata)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function ensureStateAtomsArray() {
|
||||||
|
chat_metadata.extensions ||= {};
|
||||||
|
chat_metadata.extensions[EXT_ID] ||= {};
|
||||||
|
chat_metadata.extensions[EXT_ID].stateAtoms ||= [];
|
||||||
|
return chat_metadata.extensions[EXT_ID].stateAtoms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前聊天的所有 StateAtoms
|
||||||
|
*/
|
||||||
|
export function getStateAtoms() {
|
||||||
|
return ensureStateAtomsArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存新的 StateAtoms(追加,去重)
|
||||||
|
*/
|
||||||
|
export function saveStateAtoms(atoms) {
|
||||||
|
if (!atoms?.length) return;
|
||||||
|
|
||||||
|
const arr = ensureStateAtomsArray();
|
||||||
|
const existing = new Set(arr.map(a => a.atomId));
|
||||||
|
|
||||||
|
let added = 0;
|
||||||
|
for (const atom of atoms) {
|
||||||
|
// 有效性检查
|
||||||
|
if (!atom?.atomId || typeof atom.floor !== 'number' || atom.floor < 0 || !atom.semantic) {
|
||||||
|
xbLog.warn(MODULE_ID, `跳过无效 atom: ${atom?.atomId}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.has(atom.atomId)) {
|
||||||
|
arr.push(atom);
|
||||||
|
existing.add(atom.atomId);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added > 0) {
|
||||||
|
saveMetadataDebounced();
|
||||||
|
xbLog.info(MODULE_ID, `存储 ${added} 个 StateAtom`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定楼层及之后的 StateAtoms
|
||||||
|
*/
|
||||||
|
export function deleteStateAtomsFromFloor(floor) {
|
||||||
|
const arr = ensureStateAtomsArray();
|
||||||
|
const before = arr.length;
|
||||||
|
|
||||||
|
const filtered = arr.filter(a => a.floor < floor);
|
||||||
|
chat_metadata.extensions[EXT_ID].stateAtoms = filtered;
|
||||||
|
|
||||||
|
const deleted = before - filtered.length;
|
||||||
|
if (deleted > 0) {
|
||||||
|
saveMetadataDebounced();
|
||||||
|
xbLog.info(MODULE_ID, `删除 ${deleted} 个 StateAtom (floor >= ${floor})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有 StateAtoms
|
||||||
|
*/
|
||||||
|
export function clearStateAtoms() {
|
||||||
|
const arr = ensureStateAtomsArray();
|
||||||
|
const count = arr.length;
|
||||||
|
|
||||||
|
chat_metadata.extensions[EXT_ID].stateAtoms = [];
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
saveMetadataDebounced();
|
||||||
|
xbLog.info(MODULE_ID, `清空 ${count} 个 StateAtom`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 StateAtoms 数量
|
||||||
|
*/
|
||||||
|
export function getStateAtomsCount() {
|
||||||
|
return ensureStateAtomsArray().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// StateVector 操作(IndexedDB)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 StateVectors
|
||||||
|
*/
|
||||||
|
export async function saveStateVectors(chatId, items, fingerprint) {
|
||||||
|
if (!chatId || !items?.length) return;
|
||||||
|
|
||||||
|
const records = items.map(item => ({
|
||||||
|
chatId,
|
||||||
|
atomId: item.atomId,
|
||||||
|
floor: item.floor,
|
||||||
|
vector: float32ToBuffer(new Float32Array(item.vector)),
|
||||||
|
dims: item.vector.length,
|
||||||
|
fingerprint,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await stateVectorsTable.bulkPut(records);
|
||||||
|
xbLog.info(MODULE_ID, `存储 ${records.length} 个 StateVector`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有 StateVectors
|
||||||
|
*/
|
||||||
|
export async function getAllStateVectors(chatId) {
|
||||||
|
if (!chatId) return [];
|
||||||
|
|
||||||
|
const records = await stateVectorsTable.where('chatId').equals(chatId).toArray();
|
||||||
|
return records.map(r => ({
|
||||||
|
...r,
|
||||||
|
vector: bufferToFloat32(r.vector),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定楼层及之后的 StateVectors
|
||||||
|
*/
|
||||||
|
export async function deleteStateVectorsFromFloor(chatId, floor) {
|
||||||
|
if (!chatId) return;
|
||||||
|
|
||||||
|
const deleted = await stateVectorsTable
|
||||||
|
.where('chatId')
|
||||||
|
.equals(chatId)
|
||||||
|
.filter(v => v.floor >= floor)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
if (deleted > 0) {
|
||||||
|
xbLog.info(MODULE_ID, `删除 ${deleted} 个 StateVector (floor >= ${floor})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有 StateVectors
|
||||||
|
*/
|
||||||
|
export async function clearStateVectors(chatId) {
|
||||||
|
if (!chatId) return;
|
||||||
|
|
||||||
|
const deleted = await stateVectorsTable.where('chatId').equals(chatId).delete();
|
||||||
|
if (deleted > 0) {
|
||||||
|
xbLog.info(MODULE_ID, `清空 ${deleted} 个 StateVector`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 StateVectors 数量
|
||||||
|
*/
|
||||||
|
export async function getStateVectorsCount(chatId) {
|
||||||
|
if (!chatId) return 0;
|
||||||
|
return await stateVectorsTable.where('chatId').equals(chatId).count();
|
||||||
|
}
|
||||||
@@ -18,6 +18,14 @@ import {
|
|||||||
clearEventVectors,
|
clearEventVectors,
|
||||||
saveEventVectors,
|
saveEventVectors,
|
||||||
} from './chunk-store.js';
|
} from './chunk-store.js';
|
||||||
|
import {
|
||||||
|
getStateAtoms,
|
||||||
|
saveStateAtoms,
|
||||||
|
clearStateAtoms,
|
||||||
|
getAllStateVectors,
|
||||||
|
saveStateVectors,
|
||||||
|
clearStateVectors,
|
||||||
|
} from './state-store.js';
|
||||||
import { getEngineFingerprint } from './embedder.js';
|
import { getEngineFingerprint } from './embedder.js';
|
||||||
import { getVectorConfig } from '../data/config.js';
|
import { getVectorConfig } from '../data/config.js';
|
||||||
|
|
||||||
@@ -81,13 +89,18 @@ export async function exportVectors(onProgress) {
|
|||||||
const chunks = await getAllChunks(chatId);
|
const chunks = await getAllChunks(chatId);
|
||||||
const chunkVectors = await getAllChunkVectors(chatId);
|
const chunkVectors = await getAllChunkVectors(chatId);
|
||||||
const eventVectors = await getAllEventVectors(chatId);
|
const eventVectors = await getAllEventVectors(chatId);
|
||||||
|
const stateAtoms = getStateAtoms();
|
||||||
|
const stateVectors = await getAllStateVectors(chatId);
|
||||||
|
|
||||||
if (chunks.length === 0 && eventVectors.length === 0) {
|
if (chunkVectors.length === 0 && eventVectors.length === 0 && stateVectors.length === 0) {
|
||||||
throw new Error('没有可导出的向量数据');
|
throw new Error('没有可导出的向量数据');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定维度
|
// 确定维度
|
||||||
const dims = chunkVectors[0]?.vector?.length || eventVectors[0]?.vector?.length || 0;
|
const dims = chunkVectors[0]?.vector?.length
|
||||||
|
|| eventVectors[0]?.vector?.length
|
||||||
|
|| stateVectors[0]?.vector?.length
|
||||||
|
|| 0;
|
||||||
if (dims === 0) {
|
if (dims === 0) {
|
||||||
throw new Error('无法确定向量维度');
|
throw new Error('无法确定向量维度');
|
||||||
}
|
}
|
||||||
@@ -123,6 +136,14 @@ export async function exportVectors(onProgress) {
|
|||||||
// event_vectors.bin
|
// event_vectors.bin
|
||||||
const eventVectorsOrdered = sortedEventVectors.map(ev => ev.vector);
|
const eventVectorsOrdered = sortedEventVectors.map(ev => ev.vector);
|
||||||
|
|
||||||
|
// state vectors
|
||||||
|
const sortedStateVectors = [...stateVectors].sort((a, b) => String(a.atomId).localeCompare(String(b.atomId)));
|
||||||
|
const stateVectorsOrdered = sortedStateVectors.map(v => v.vector);
|
||||||
|
const stateVectorsJsonl = sortedStateVectors.map(v => JSON.stringify({
|
||||||
|
atomId: v.atomId,
|
||||||
|
floor: v.floor,
|
||||||
|
})).join('\n');
|
||||||
|
|
||||||
// manifest
|
// manifest
|
||||||
const manifest = {
|
const manifest = {
|
||||||
version: EXPORT_VERSION,
|
version: EXPORT_VERSION,
|
||||||
@@ -133,6 +154,8 @@ export async function exportVectors(onProgress) {
|
|||||||
chunkCount: sortedChunks.length,
|
chunkCount: sortedChunks.length,
|
||||||
chunkVectorCount: chunkVectors.length,
|
chunkVectorCount: chunkVectors.length,
|
||||||
eventCount: sortedEventVectors.length,
|
eventCount: sortedEventVectors.length,
|
||||||
|
stateAtomCount: stateAtoms.length,
|
||||||
|
stateVectorCount: stateVectors.length,
|
||||||
lastChunkFloor: meta.lastChunkFloor ?? -1,
|
lastChunkFloor: meta.lastChunkFloor ?? -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,6 +168,11 @@ export async function exportVectors(onProgress) {
|
|||||||
'chunk_vectors.bin': float32ToBytes(chunkVectorsOrdered, dims),
|
'chunk_vectors.bin': float32ToBytes(chunkVectorsOrdered, dims),
|
||||||
'events.jsonl': strToU8(eventsJsonl),
|
'events.jsonl': strToU8(eventsJsonl),
|
||||||
'event_vectors.bin': float32ToBytes(eventVectorsOrdered, dims),
|
'event_vectors.bin': float32ToBytes(eventVectorsOrdered, dims),
|
||||||
|
'state_atoms.json': strToU8(JSON.stringify(stateAtoms)),
|
||||||
|
'state_vectors.jsonl': strToU8(stateVectorsJsonl),
|
||||||
|
'state_vectors.bin': stateVectorsOrdered.length
|
||||||
|
? float32ToBytes(stateVectorsOrdered, dims)
|
||||||
|
: new Uint8Array(0),
|
||||||
}, { level: 1 }); // 降低压缩级别,速度优先
|
}, { level: 1 }); // 降低压缩级别,速度优先
|
||||||
|
|
||||||
onProgress?.('下载文件...');
|
onProgress?.('下载文件...');
|
||||||
@@ -238,6 +266,21 @@ export async function importVectors(file, onProgress) {
|
|||||||
const eventVectorsBytes = unzipped['event_vectors.bin'];
|
const eventVectorsBytes = unzipped['event_vectors.bin'];
|
||||||
const eventVectors = eventVectorsBytes ? bytesToFloat32(eventVectorsBytes, manifest.dims) : [];
|
const eventVectors = eventVectorsBytes ? bytesToFloat32(eventVectorsBytes, manifest.dims) : [];
|
||||||
|
|
||||||
|
// 解析 L0 state atoms
|
||||||
|
const stateAtoms = unzipped['state_atoms.json']
|
||||||
|
? JSON.parse(strFromU8(unzipped['state_atoms.json']))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 解析 L0 state vectors metas
|
||||||
|
const stateVectorsJsonl = unzipped['state_vectors.jsonl'] ? strFromU8(unzipped['state_vectors.jsonl']) : '';
|
||||||
|
const stateVectorMetas = stateVectorsJsonl.split('\n').filter(Boolean).map(line => JSON.parse(line));
|
||||||
|
|
||||||
|
// 解析 L0 state vectors
|
||||||
|
const stateVectorsBytes = unzipped['state_vectors.bin'];
|
||||||
|
const stateVectors = (stateVectorsBytes && stateVectorMetas.length)
|
||||||
|
? bytesToFloat32(stateVectorsBytes, manifest.dims)
|
||||||
|
: [];
|
||||||
|
|
||||||
// 校验数量
|
// 校验数量
|
||||||
if (chunkMetas.length !== chunkVectors.length) {
|
if (chunkMetas.length !== chunkVectors.length) {
|
||||||
throw new Error(`chunk 数量不匹配: 元数据 ${chunkMetas.length}, 向量 ${chunkVectors.length}`);
|
throw new Error(`chunk 数量不匹配: 元数据 ${chunkMetas.length}, 向量 ${chunkVectors.length}`);
|
||||||
@@ -245,12 +288,17 @@ export async function importVectors(file, onProgress) {
|
|||||||
if (eventMetas.length !== eventVectors.length) {
|
if (eventMetas.length !== eventVectors.length) {
|
||||||
throw new Error(`event 数量不匹配: 元数据 ${eventMetas.length}, 向量 ${eventVectors.length}`);
|
throw new Error(`event 数量不匹配: 元数据 ${eventMetas.length}, 向量 ${eventVectors.length}`);
|
||||||
}
|
}
|
||||||
|
if (stateVectorMetas.length !== stateVectors.length) {
|
||||||
|
throw new Error(`state 向量数量不匹配: 元数据 ${stateVectorMetas.length}, 向量 ${stateVectors.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
onProgress?.('清空旧数据...');
|
onProgress?.('清空旧数据...');
|
||||||
|
|
||||||
// 清空当前数据
|
// 清空当前数据
|
||||||
await clearAllChunks(chatId);
|
await clearAllChunks(chatId);
|
||||||
await clearEventVectors(chatId);
|
await clearEventVectors(chatId);
|
||||||
|
await clearStateVectors(chatId);
|
||||||
|
clearStateAtoms();
|
||||||
|
|
||||||
onProgress?.('写入数据...');
|
onProgress?.('写入数据...');
|
||||||
|
|
||||||
@@ -284,13 +332,28 @@ export async function importVectors(file, onProgress) {
|
|||||||
await saveEventVectors(chatId, eventVectorItems, manifest.fingerprint);
|
await saveEventVectors(chatId, eventVectorItems, manifest.fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入 state atoms
|
||||||
|
if (stateAtoms.length > 0) {
|
||||||
|
saveStateAtoms(stateAtoms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入 state vectors
|
||||||
|
if (stateVectorMetas.length > 0) {
|
||||||
|
const stateVectorItems = stateVectorMetas.map((meta, idx) => ({
|
||||||
|
atomId: meta.atomId,
|
||||||
|
floor: meta.floor,
|
||||||
|
vector: stateVectors[idx],
|
||||||
|
}));
|
||||||
|
await saveStateVectors(chatId, stateVectorItems, manifest.fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
// 更新 meta
|
// 更新 meta
|
||||||
await updateMeta(chatId, {
|
await updateMeta(chatId, {
|
||||||
fingerprint: manifest.fingerprint,
|
fingerprint: manifest.fingerprint,
|
||||||
lastChunkFloor: manifest.lastChunkFloor,
|
lastChunkFloor: manifest.lastChunkFloor,
|
||||||
});
|
});
|
||||||
|
|
||||||
xbLog.info(MODULE_ID, `导入完成: ${chunkMetas.length} chunks, ${eventMetas.length} events`);
|
xbLog.info(MODULE_ID, `导入完成: ${chunkMetas.length} chunks, ${eventMetas.length} events, ${stateAtoms.length} state atoms`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chunkCount: chunkMetas.length,
|
chunkCount: chunkMetas.length,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// 删掉:getRequestHeaders, extractMessageFromData, getStreamingReply, tryParseStreamingError, getEventSourceStream
|
// 删掉:getRequestHeaders, extractMessageFromData, getStreamingReply, tryParseStreamingError, getEventSourceStream
|
||||||
|
|
||||||
import { eventSource, event_types, chat, name1, activateSendButtons, deactivateSendButtons } from "../../../../../script.js";
|
import { eventSource, event_types, chat, name1, activateSendButtons, deactivateSendButtons, substituteParams } from "../../../../../script.js";
|
||||||
import { chat_completion_sources, oai_settings, promptManager, getChatCompletionModel } from "../../../../openai.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 { getContext } from "../../../../st-context.js";
|
import { getContext } from "../../../../st-context.js";
|
||||||
@@ -12,6 +12,7 @@ import { power_user } from "../../../../power-user.js";
|
|||||||
import { world_info } from "../../../../world-info.js";
|
import { world_info } from "../../../../world-info.js";
|
||||||
import { xbLog, CacheRegistry } from "../core/debug-core.js";
|
import { xbLog, CacheRegistry } from "../core/debug-core.js";
|
||||||
import { getTrustedOrigin } from "../core/iframe-messaging.js";
|
import { getTrustedOrigin } from "../core/iframe-messaging.js";
|
||||||
|
import { replaceXbGetVarInString, replaceXbGetVarYamlInString } from "./variables/var-commands.js";
|
||||||
|
|
||||||
const EVT_DONE = 'xiaobaix_streaming_completed';
|
const EVT_DONE = 'xiaobaix_streaming_completed';
|
||||||
|
|
||||||
@@ -366,11 +367,28 @@ class StreamingGeneration {
|
|||||||
async _emitPromptReady(chatArray) {
|
async _emitPromptReady(chatArray) {
|
||||||
try {
|
try {
|
||||||
if (Array.isArray(chatArray)) {
|
if (Array.isArray(chatArray)) {
|
||||||
await eventSource?.emit?.(event_types.CHAT_COMPLETION_PROMPT_READY, { chat: chatArray, dryRun: false });
|
const snapshot = this._cloneChat(chatArray);
|
||||||
|
await eventSource?.emit?.(event_types.CHAT_COMPLETION_PROMPT_READY, { chat: snapshot, dryRun: false });
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cloneChat(chatArray) {
|
||||||
|
try {
|
||||||
|
if (typeof structuredClone === 'function') return structuredClone(chatArray);
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(chatArray));
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
return Array.isArray(chatArray)
|
||||||
|
? chatArray.map(m => (m && typeof m === 'object' ? { ...m } : m))
|
||||||
|
: chatArray;
|
||||||
|
} catch {
|
||||||
|
return chatArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async processGeneration(generateData, prompt, sessionId, stream = true) {
|
async processGeneration(generateData, prompt, sessionId, stream = true) {
|
||||||
const session = this._ensureSession(sessionId, prompt);
|
const session = this._ensureSession(sessionId, prompt);
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -788,6 +806,10 @@ class StreamingGeneration {
|
|||||||
.replace(/<\s*user\s*>/gi, String(ctx?.name1 || 'User'))
|
.replace(/<\s*user\s*>/gi, String(ctx?.name1 || 'User'))
|
||||||
.replace(/<\s*(char|character)\s*>/gi, String(ctx?.name2 || 'Assistant'))
|
.replace(/<\s*(char|character)\s*>/gi, String(ctx?.name2 || 'Assistant'))
|
||||||
.replace(/<\s*persona\s*>/gi, String(f.persona || ''));
|
.replace(/<\s*persona\s*>/gi, String(f.persona || ''));
|
||||||
|
try {
|
||||||
|
out = replaceXbGetVarInString(out);
|
||||||
|
out = replaceXbGetVarYamlInString(out);
|
||||||
|
} catch {}
|
||||||
const snap = this._getLastMessagesSnapshot();
|
const snap = this._getLastMessagesSnapshot();
|
||||||
const lastDict = {
|
const lastDict = {
|
||||||
'{{lastmessage}}': snap.lastMessage,
|
'{{lastmessage}}': snap.lastMessage,
|
||||||
@@ -855,13 +877,14 @@ class StreamingGeneration {
|
|||||||
/\{\{getvar::([\s\S]*?)\}\}/gi,
|
/\{\{getvar::([\s\S]*?)\}\}/gi,
|
||||||
(root) => `/getvar key=${escapeForCmd(root)}`
|
(root) => `/getvar key=${escapeForCmd(root)}`
|
||||||
);
|
);
|
||||||
await apply(
|
await apply(
|
||||||
/\{\{getglobalvar::([\s\S]*?)\}\}/gi,
|
/\{\{getglobalvar::([\s\S]*?)\}\}/gi,
|
||||||
(root) => `/getglobalvar ${escapeForCmd(root)}`
|
(root) => `/getglobalvar ${escapeForCmd(root)}`
|
||||||
);
|
);
|
||||||
return txt;
|
return txt;
|
||||||
};
|
};
|
||||||
out = await expandVarMacros(out);
|
out = await expandVarMacros(out);
|
||||||
|
try { out = substituteParams(out); } catch {}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -964,16 +987,12 @@ class StreamingGeneration {
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
let topMsgs = await mapHistoryPlaceholders(
|
let topMsgs = []
|
||||||
[]
|
.concat(topComposite ? this._parseCompositeParam(topComposite) : [])
|
||||||
.concat(topComposite ? this._parseCompositeParam(topComposite) : [])
|
.concat(createMsgs('top'));
|
||||||
.concat(createMsgs('top'))
|
let bottomMsgs = []
|
||||||
);
|
.concat(bottomComposite ? this._parseCompositeParam(bottomComposite) : [])
|
||||||
let bottomMsgs = await mapHistoryPlaceholders(
|
.concat(createMsgs('bottom'));
|
||||||
[]
|
|
||||||
.concat(bottomComposite ? this._parseCompositeParam(bottomComposite) : [])
|
|
||||||
.concat(createMsgs('bottom'))
|
|
||||||
);
|
|
||||||
const expandSegmentInline = async (arr) => {
|
const expandSegmentInline = async (arr) => {
|
||||||
for (const m of arr) {
|
for (const m of arr) {
|
||||||
if (m && typeof m.content === 'string') {
|
if (m && typeof m.content === 'string') {
|
||||||
@@ -988,10 +1007,13 @@ class StreamingGeneration {
|
|||||||
|
|
||||||
await expandSegmentInline(bottomMsgs);
|
await expandSegmentInline(bottomMsgs);
|
||||||
|
|
||||||
|
topMsgs = await mapHistoryPlaceholders(topMsgs);
|
||||||
|
bottomMsgs = await mapHistoryPlaceholders(bottomMsgs);
|
||||||
|
|
||||||
if (typeof prompt === 'string' && prompt.trim()) {
|
if (typeof prompt === 'string' && prompt.trim()) {
|
||||||
const beforeP = await resolveHistoryPlaceholder(prompt);
|
const afterP = await this.expandInline(prompt);
|
||||||
const afterP = await this.expandInline(beforeP);
|
const beforeP = await resolveHistoryPlaceholder(afterP);
|
||||||
prompt = afterP && afterP.length ? afterP : beforeP;
|
prompt = beforeP && beforeP.length ? beforeP : afterP;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const needsWI = [...topMsgs, ...bottomMsgs].some(m => m && typeof m.content === 'string' && m.content.includes('{$worldInfo}')) || (typeof prompt === 'string' && prompt.includes('{$worldInfo}'));
|
const needsWI = [...topMsgs, ...bottomMsgs].some(m => m && typeof m.content === 'string' && m.content.includes('{$worldInfo}')) || (typeof prompt === 'string' && prompt.includes('{$worldInfo}'));
|
||||||
|
|||||||
199
modules/variables/state2/executor.js
Normal file
199
modules/variables/state2/executor.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import {
|
||||||
|
lwbResolveVarPath,
|
||||||
|
lwbAssignVarPath,
|
||||||
|
lwbAddVarPath,
|
||||||
|
lwbPushVarPath,
|
||||||
|
lwbDeleteVarPath,
|
||||||
|
lwbRemoveArrayItemByValue,
|
||||||
|
} from '../var-commands.js';
|
||||||
|
import { lwbSplitPathWithBrackets } from '../../../core/variable-path.js';
|
||||||
|
|
||||||
|
import { extractStateBlocks, computeStateSignature, parseStateBlock } from './parser.js';
|
||||||
|
import { generateSemantic } from './semantic.js';
|
||||||
|
|
||||||
|
const LWB_STATE_APPLIED_KEY = 'LWB_STATE_APPLIED_KEY';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chatMetadata 内记录每楼层 signature,防止重复执行
|
||||||
|
*/
|
||||||
|
function getAppliedMap() {
|
||||||
|
const meta = getContext()?.chatMetadata || {};
|
||||||
|
meta[LWB_STATE_APPLIED_KEY] ||= {};
|
||||||
|
return meta[LWB_STATE_APPLIED_KEY];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStateAppliedFor(floor) {
|
||||||
|
try {
|
||||||
|
const map = getAppliedMap();
|
||||||
|
delete map[floor];
|
||||||
|
getContext()?.saveMetadataDebounced?.();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStateAppliedFrom(floorInclusive) {
|
||||||
|
try {
|
||||||
|
const map = getAppliedMap();
|
||||||
|
for (const k of Object.keys(map)) {
|
||||||
|
const id = Number(k);
|
||||||
|
if (!Number.isNaN(id) && id >= floorInclusive) delete map[k];
|
||||||
|
}
|
||||||
|
getContext()?.saveMetadataDebounced?.();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeParseAny(str) {
|
||||||
|
if (str == null || str === '') return undefined;
|
||||||
|
if (typeof str !== 'string') return str;
|
||||||
|
const t = str.trim();
|
||||||
|
if (!t) return undefined;
|
||||||
|
if (t[0] === '{' || t[0] === '[') {
|
||||||
|
try { return JSON.parse(t); } catch { return str; }
|
||||||
|
}
|
||||||
|
if (/^-?\d+(?:\.\d+)?$/.test(t)) return Number(t);
|
||||||
|
if (t === 'true') return true;
|
||||||
|
if (t === 'false') return false;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIndexDeleteOp(opItem) {
|
||||||
|
if (!opItem || opItem.op !== 'del') return false;
|
||||||
|
const segs = lwbSplitPathWithBrackets(opItem.path);
|
||||||
|
if (!segs.length) return false;
|
||||||
|
const last = segs[segs.length - 1];
|
||||||
|
return typeof last === 'number' && Number.isFinite(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParentPathFromSegs(segs) {
|
||||||
|
return segs.reduce((acc, s) => {
|
||||||
|
if (typeof s === 'number') return `${acc}[${s}]`;
|
||||||
|
return acc ? `${acc}.${s}` : String(s);
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExecOpsWithIndexDeleteReorder(ops) {
|
||||||
|
const groups = new Map(); // parentPath -> [{ op, idx }]
|
||||||
|
const groupOrder = new Map();
|
||||||
|
let orderCounter = 0;
|
||||||
|
|
||||||
|
const normalOps = [];
|
||||||
|
|
||||||
|
for (const op of ops) {
|
||||||
|
if (isIndexDeleteOp(op)) {
|
||||||
|
const segs = lwbSplitPathWithBrackets(op.path);
|
||||||
|
const idx = segs[segs.length - 1];
|
||||||
|
const parentPath = buildParentPathFromSegs(segs.slice(0, -1));
|
||||||
|
|
||||||
|
if (!groups.has(parentPath)) {
|
||||||
|
groups.set(parentPath, []);
|
||||||
|
groupOrder.set(parentPath, orderCounter++);
|
||||||
|
}
|
||||||
|
groups.get(parentPath).push({ op, idx });
|
||||||
|
} else {
|
||||||
|
normalOps.push(op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedParents = Array.from(groups.keys()).sort(
|
||||||
|
(a, b) => (groupOrder.get(a) ?? 0) - (groupOrder.get(b) ?? 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const reorderedIndexDeletes = [];
|
||||||
|
for (const parent of orderedParents) {
|
||||||
|
const items = groups.get(parent) || [];
|
||||||
|
items.sort((a, b) => b.idx - a.idx);
|
||||||
|
for (const it of items) reorderedIndexDeletes.push(it.op);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...reorderedIndexDeletes, ...normalOps];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量 2.0:执行单条消息里的 <state>,返回 atoms
|
||||||
|
*/
|
||||||
|
export function applyStateForMessage(messageId, messageContent) {
|
||||||
|
const ctx = getContext();
|
||||||
|
const chatId = ctx?.chatId || '';
|
||||||
|
|
||||||
|
const text = String(messageContent ?? '');
|
||||||
|
const signature = computeStateSignature(text);
|
||||||
|
|
||||||
|
// 没有 state:清理旧 signature(避免“删掉 state 后仍然认为执行过”)
|
||||||
|
if (!signature) {
|
||||||
|
clearStateAppliedFor(messageId);
|
||||||
|
return { atoms: [], errors: [], skipped: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 幂等:signature 没变就跳过
|
||||||
|
const appliedMap = getAppliedMap();
|
||||||
|
if (appliedMap[messageId] === signature) {
|
||||||
|
return { atoms: [], errors: [], skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = extractStateBlocks(text);
|
||||||
|
const atoms = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const ops = parseStateBlock(block);
|
||||||
|
const execOps = buildExecOpsWithIndexDeleteReorder(ops);
|
||||||
|
for (const opItem of execOps) {
|
||||||
|
const { path, op, value, delta, warning } = opItem;
|
||||||
|
if (!path) continue;
|
||||||
|
if (warning) errors.push(`[${path}] ${warning}`);
|
||||||
|
|
||||||
|
const oldValue = safeParseAny(lwbResolveVarPath(path));
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (op) {
|
||||||
|
case 'set':
|
||||||
|
lwbAssignVarPath(path, value);
|
||||||
|
break;
|
||||||
|
case 'inc':
|
||||||
|
lwbAddVarPath(path, delta);
|
||||||
|
break;
|
||||||
|
case 'push':
|
||||||
|
lwbPushVarPath(path, value);
|
||||||
|
break;
|
||||||
|
case 'pop':
|
||||||
|
lwbRemoveArrayItemByValue(path, value);
|
||||||
|
break;
|
||||||
|
case 'del':
|
||||||
|
lwbDeleteVarPath(path);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errors.push(`[${path}] 未知 op=${op}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(`[${path}] 执行失败: ${e?.message || e}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = safeParseAny(lwbResolveVarPath(path));
|
||||||
|
|
||||||
|
atoms.push({
|
||||||
|
atomId: `sa-${messageId}-${idx}`,
|
||||||
|
chatId,
|
||||||
|
floor: messageId,
|
||||||
|
idx,
|
||||||
|
path,
|
||||||
|
op,
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
delta: op === 'inc' ? delta : undefined,
|
||||||
|
semantic: generateSemantic(path, op, oldValue, newValue, delta, value),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appliedMap[messageId] = signature;
|
||||||
|
getContext()?.saveMetadataDebounced?.();
|
||||||
|
|
||||||
|
return { atoms, errors, skipped: false };
|
||||||
|
}
|
||||||
3
modules/variables/state2/index.js
Normal file
3
modules/variables/state2/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { applyStateForMessage, clearStateAppliedFor, clearStateAppliedFrom } from './executor.js';
|
||||||
|
export { parseStateBlock, extractStateBlocks, computeStateSignature, parseInlineValue } from './parser.js';
|
||||||
|
export { generateSemantic } from './semantic.js';
|
||||||
196
modules/variables/state2/parser.js
Normal file
196
modules/variables/state2/parser.js
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import jsyaml from '../../../libs/js-yaml.mjs';
|
||||||
|
|
||||||
|
const STATE_TAG_RE = /<\s*state\b[^>]*>([\s\S]*?)<\s*\/\s*state\s*>/gi;
|
||||||
|
|
||||||
|
export function extractStateBlocks(text) {
|
||||||
|
const s = String(text ?? '');
|
||||||
|
if (!s || s.toLowerCase().indexOf('<state') === -1) return [];
|
||||||
|
const out = [];
|
||||||
|
STATE_TAG_RE.lastIndex = 0;
|
||||||
|
let m;
|
||||||
|
while ((m = STATE_TAG_RE.exec(s)) !== null) {
|
||||||
|
const inner = String(m[1] ?? '');
|
||||||
|
if (inner.trim()) out.push(inner);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeStateSignature(text) {
|
||||||
|
const s = String(text ?? '');
|
||||||
|
if (!s || s.toLowerCase().indexOf('<state') === -1) return '';
|
||||||
|
const chunks = [];
|
||||||
|
STATE_TAG_RE.lastIndex = 0;
|
||||||
|
let m;
|
||||||
|
while ((m = STATE_TAG_RE.exec(s)) !== null) chunks.push(String(m[0] ?? '').trim());
|
||||||
|
return chunks.length ? chunks.join('\n---\n') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 <state> 块内容 -> ops[]
|
||||||
|
* 单行支持运算符,多行只支持覆盖 set(YAML)
|
||||||
|
*/
|
||||||
|
export function parseStateBlock(content) {
|
||||||
|
const results = [];
|
||||||
|
const lines = String(content ?? '').split(/\r?\n/);
|
||||||
|
|
||||||
|
let pendingPath = null;
|
||||||
|
let pendingLines = [];
|
||||||
|
|
||||||
|
const flushPending = () => {
|
||||||
|
if (!pendingPath) return;
|
||||||
|
// 没有任何缩进行:视为 set 空字符串
|
||||||
|
if (!pendingLines.length) {
|
||||||
|
results.push({ path: pendingPath, op: 'set', value: '' });
|
||||||
|
pendingPath = null;
|
||||||
|
pendingLines = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 去除公共缩进
|
||||||
|
const nonEmpty = pendingLines.filter(l => l.trim());
|
||||||
|
const minIndent = nonEmpty.length
|
||||||
|
? Math.min(...nonEmpty.map(l => l.search(/\S/)))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const yamlText = pendingLines
|
||||||
|
.map(l => (l.trim() ? l.slice(minIndent) : ''))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const obj = jsyaml.load(yamlText);
|
||||||
|
results.push({ path: pendingPath, op: 'set', value: obj });
|
||||||
|
} catch (e) {
|
||||||
|
results.push({ path: pendingPath, op: 'set', value: null, warning: `YAML 解析失败: ${e.message}` });
|
||||||
|
} finally {
|
||||||
|
pendingPath = null;
|
||||||
|
pendingLines = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const raw of lines) {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
|
|
||||||
|
const indent = raw.search(/\S/);
|
||||||
|
|
||||||
|
if (indent === 0) {
|
||||||
|
flushPending();
|
||||||
|
const colonIdx = findTopLevelColon(trimmed);
|
||||||
|
if (colonIdx === -1) continue;
|
||||||
|
|
||||||
|
const path = trimmed.slice(0, colonIdx).trim();
|
||||||
|
const rhs = trimmed.slice(colonIdx + 1).trim();
|
||||||
|
if (!path) continue;
|
||||||
|
|
||||||
|
if (!rhs) {
|
||||||
|
pendingPath = path;
|
||||||
|
pendingLines = [];
|
||||||
|
} else {
|
||||||
|
results.push({ path, ...parseInlineValue(rhs) });
|
||||||
|
}
|
||||||
|
} else if (pendingPath) {
|
||||||
|
pendingLines.push(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushPending();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTopLevelColon(line) {
|
||||||
|
let inQuote = false;
|
||||||
|
let q = '';
|
||||||
|
let esc = false;
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const ch = line[i];
|
||||||
|
if (esc) { esc = false; continue; }
|
||||||
|
if (ch === '\\') { esc = true; continue; }
|
||||||
|
if (!inQuote && (ch === '"' || ch === "'")) { inQuote = true; q = ch; continue; }
|
||||||
|
if (inQuote && ch === q) { inQuote = false; q = ''; continue; }
|
||||||
|
if (!inQuote && ch === ':') return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unescapeString(s) {
|
||||||
|
return String(s ?? '')
|
||||||
|
.replace(/\\n/g, '\n')
|
||||||
|
.replace(/\\t/g, '\t')
|
||||||
|
.replace(/\\r/g, '\r')
|
||||||
|
.replace(/\\"/g, '"')
|
||||||
|
.replace(/\\'/g, "'")
|
||||||
|
.replace(/\\\\/g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单行内联值解析
|
||||||
|
*/
|
||||||
|
export function parseInlineValue(raw) {
|
||||||
|
const t = String(raw ?? '').trim();
|
||||||
|
|
||||||
|
if (t === 'null') return { op: 'del' };
|
||||||
|
|
||||||
|
// (负数) 用于强制 set -5,而不是 inc -5
|
||||||
|
const parenNum = t.match(/^\((-?\d+(?:\.\d+)?)\)$/);
|
||||||
|
if (parenNum) return { op: 'set', value: Number(parenNum[1]) };
|
||||||
|
|
||||||
|
// +10 / -20
|
||||||
|
if (/^\+\d/.test(t) || /^-\d/.test(t)) {
|
||||||
|
const n = Number(t);
|
||||||
|
if (Number.isFinite(n)) return { op: 'inc', delta: n };
|
||||||
|
}
|
||||||
|
|
||||||
|
// +"str" / +'str'
|
||||||
|
const pushD = t.match(/^\+"((?:[^"\\]|\\.)*)"\s*$/);
|
||||||
|
if (pushD) return { op: 'push', value: unescapeString(pushD[1]) };
|
||||||
|
const pushS = t.match(/^\+'((?:[^'\\]|\\.)*)'\s*$/);
|
||||||
|
if (pushS) return { op: 'push', value: unescapeString(pushS[1]) };
|
||||||
|
|
||||||
|
// +[...]
|
||||||
|
if (t.startsWith('+[')) {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(t.slice(1));
|
||||||
|
if (Array.isArray(arr)) return { op: 'push', value: arr };
|
||||||
|
return { op: 'set', value: t, warning: '+[] 不是数组,作为字符串' };
|
||||||
|
} catch {
|
||||||
|
return { op: 'set', value: t, warning: '+[] JSON 解析失败,作为字符串' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -"str" / -'str'
|
||||||
|
const popD = t.match(/^-"((?:[^"\\]|\\.)*)"\s*$/);
|
||||||
|
if (popD) return { op: 'pop', value: unescapeString(popD[1]) };
|
||||||
|
const popS = t.match(/^-'((?:[^'\\]|\\.)*)'\s*$/);
|
||||||
|
if (popS) return { op: 'pop', value: unescapeString(popS[1]) };
|
||||||
|
|
||||||
|
// -[...]
|
||||||
|
if (t.startsWith('-[')) {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(t.slice(1));
|
||||||
|
if (Array.isArray(arr)) return { op: 'pop', value: arr };
|
||||||
|
return { op: 'set', value: t, warning: '-[] 不是数组,作为字符串' };
|
||||||
|
} catch {
|
||||||
|
return { op: 'set', value: t, warning: '-[] JSON 解析失败,作为字符串' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 裸数字 set
|
||||||
|
if (/^-?\d+(?:\.\d+)?$/.test(t)) return { op: 'set', value: Number(t) };
|
||||||
|
|
||||||
|
// "str" / 'str'
|
||||||
|
const strD = t.match(/^"((?:[^"\\]|\\.)*)"\s*$/);
|
||||||
|
if (strD) return { op: 'set', value: unescapeString(strD[1]) };
|
||||||
|
const strS = t.match(/^'((?:[^'\\]|\\.)*)'\s*$/);
|
||||||
|
if (strS) return { op: 'set', value: unescapeString(strS[1]) };
|
||||||
|
|
||||||
|
if (t === 'true') return { op: 'set', value: true };
|
||||||
|
if (t === 'false') return { op: 'set', value: false };
|
||||||
|
|
||||||
|
// JSON set
|
||||||
|
if (t.startsWith('{') || t.startsWith('[')) {
|
||||||
|
try { return { op: 'set', value: JSON.parse(t) }; }
|
||||||
|
catch { return { op: 'set', value: t, warning: 'JSON 解析失败,作为字符串' }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兜底 set 原文本
|
||||||
|
return { op: 'set', value: t };
|
||||||
|
}
|
||||||
42
modules/variables/state2/semantic.js
Normal file
42
modules/variables/state2/semantic.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export function generateSemantic(path, op, oldValue, newValue, delta, operandValue) {
|
||||||
|
const p = String(path ?? '').replace(/\./g, ' > ');
|
||||||
|
|
||||||
|
const fmt = (v) => {
|
||||||
|
if (v === undefined) return '空';
|
||||||
|
if (v === null) return 'null';
|
||||||
|
try {
|
||||||
|
if (typeof v === 'string') return JSON.stringify(v);
|
||||||
|
return JSON.stringify(v);
|
||||||
|
} catch {
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case 'set':
|
||||||
|
return oldValue === undefined
|
||||||
|
? `${p} 设为 ${fmt(newValue)}`
|
||||||
|
: `${p} 从 ${fmt(oldValue)} 变为 ${fmt(newValue)}`;
|
||||||
|
|
||||||
|
case 'inc': {
|
||||||
|
const sign = (delta ?? 0) >= 0 ? '+' : '';
|
||||||
|
return `${p} ${sign}${delta}(${fmt(oldValue)} → ${fmt(newValue)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'push': {
|
||||||
|
const items = Array.isArray(operandValue) ? operandValue : [operandValue];
|
||||||
|
return `${p} 加入 ${items.map(fmt).join('、')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'pop': {
|
||||||
|
const items = Array.isArray(operandValue) ? operandValue : [operandValue];
|
||||||
|
return `${p} 移除 ${items.map(fmt).join('、')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'del':
|
||||||
|
return `${p} 被删除(原值 ${fmt(oldValue)})`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return `${p} 操作 ${op}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -667,6 +667,62 @@ export function lwbPushVarPath(path, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lwbRemoveArrayItemByValue(path, valuesToRemove) {
|
||||||
|
try {
|
||||||
|
const segs = lwbSplitPathWithBrackets(path);
|
||||||
|
if (!segs.length) return '';
|
||||||
|
|
||||||
|
const rootName = String(segs[0]);
|
||||||
|
const rootRaw = getLocalVariable(rootName);
|
||||||
|
const rootObj = maybeParseObject(rootRaw);
|
||||||
|
if (!rootObj) return '';
|
||||||
|
|
||||||
|
// 定位到目标数组
|
||||||
|
let cur = rootObj;
|
||||||
|
for (let i = 1; i < segs.length; i++) {
|
||||||
|
cur = cur?.[segs[i]];
|
||||||
|
if (cur == null) return '';
|
||||||
|
}
|
||||||
|
if (!Array.isArray(cur)) return '';
|
||||||
|
|
||||||
|
const toRemove = Array.isArray(valuesToRemove) ? valuesToRemove : [valuesToRemove];
|
||||||
|
if (!toRemove.length) return '';
|
||||||
|
|
||||||
|
// 找到索引(每个值只删除一个匹配项)
|
||||||
|
const indices = [];
|
||||||
|
for (const v of toRemove) {
|
||||||
|
const vStr = safeJSONStringify(v);
|
||||||
|
if (!vStr) continue;
|
||||||
|
const idx = cur.findIndex(x => safeJSONStringify(x) === vStr);
|
||||||
|
if (idx !== -1) indices.push(idx);
|
||||||
|
}
|
||||||
|
if (!indices.length) return '';
|
||||||
|
|
||||||
|
// 倒序删除,且逐个走 guardian 的 delNode 校验(用 index path)
|
||||||
|
indices.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
for (const idx of indices) {
|
||||||
|
const absIndexPath = normalizePath(`${path}[${idx}]`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (globalThis.LWB_Guard?.validate) {
|
||||||
|
const g = globalThis.LWB_Guard.validate('delNode', absIndexPath);
|
||||||
|
if (!g?.allow) continue;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (idx >= 0 && idx < cur.length) {
|
||||||
|
cur.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalVariable(rootName, safeJSONStringify(rootObj));
|
||||||
|
return '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function registerXbGetVarSlashCommand() {
|
function registerXbGetVarSlashCommand() {
|
||||||
try {
|
try {
|
||||||
const ctx = getContext();
|
const ctx = getContext();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* @file modules/variables/variables-core.js
|
* @file modules/variables/variables-core.js
|
||||||
* @description 变量管理核心(受开关控制)
|
* @description Variables core (feature-flag controlled)
|
||||||
* @description 包含 plot-log 解析、快照回滚、变量守护
|
* @description Includes plot-log parsing, snapshot rollback, and variable guard
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from "../../../../../extensions.js";
|
import { extension_settings, getContext } from "../../../../../extensions.js";
|
||||||
import { updateMessageBlock } from "../../../../../../script.js";
|
import { updateMessageBlock } from "../../../../../../script.js";
|
||||||
import { getLocalVariable, setLocalVariable } from "../../../../../variables.js";
|
import { getLocalVariable, setLocalVariable } from "../../../../../variables.js";
|
||||||
import { createModuleEvents, event_types } from "../../core/event-manager.js";
|
import { createModuleEvents, event_types } from "../../core/event-manager.js";
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
applyXbGetVarForMessage,
|
applyXbGetVarForMessage,
|
||||||
parseValueForSet,
|
parseValueForSet,
|
||||||
} from "./var-commands.js";
|
} from "./var-commands.js";
|
||||||
|
import { applyStateForMessage, clearStateAppliedFrom } from "./state2/index.js";
|
||||||
import {
|
import {
|
||||||
preprocessBumpAliases,
|
preprocessBumpAliases,
|
||||||
executeQueuedVareventJsAfterTurn,
|
executeQueuedVareventJsAfterTurn,
|
||||||
@@ -36,17 +37,18 @@ import {
|
|||||||
TOP_OP_RE,
|
TOP_OP_RE,
|
||||||
} from "./varevent-editor.js";
|
} from "./varevent-editor.js";
|
||||||
|
|
||||||
/* ============= 模块常量 ============= */
|
/* ============ Module Constants ============= */
|
||||||
|
|
||||||
const MODULE_ID = 'variablesCore';
|
const MODULE_ID = 'variablesCore';
|
||||||
|
const EXT_ID = 'LittleWhiteBox';
|
||||||
const LWB_RULES_KEY = 'LWB_RULES';
|
const LWB_RULES_KEY = 'LWB_RULES';
|
||||||
const LWB_SNAP_KEY = 'LWB_SNAP';
|
const LWB_SNAP_KEY = 'LWB_SNAP';
|
||||||
const LWB_PLOT_APPLIED_KEY = 'LWB_PLOT_APPLIED_KEY';
|
const LWB_PLOT_APPLIED_KEY = 'LWB_PLOT_APPLIED_KEY';
|
||||||
|
|
||||||
// plot-log 标签正则
|
// plot-log tag regex
|
||||||
const TAG_RE_PLOTLOG = /<\s*plot-log[^>]*>([\s\S]*?)<\s*\/\s*plot-log\s*>/gi;
|
const TAG_RE_PLOTLOG = /<\s*plot-log[^>]*>([\s\S]*?)<\s*\/\s*plot-log\s*>/gi;
|
||||||
|
|
||||||
// 守护状态
|
// guardian state
|
||||||
const guardianState = {
|
const guardianState = {
|
||||||
table: {},
|
table: {},
|
||||||
regexCache: {},
|
regexCache: {},
|
||||||
@@ -55,7 +57,8 @@ const guardianState = {
|
|||||||
lastMetaSyncAt: 0
|
lastMetaSyncAt: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// 事件管理器
|
// note
|
||||||
|
|
||||||
let events = null;
|
let events = null;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let pendingSwipeApply = new Map();
|
let pendingSwipeApply = new Map();
|
||||||
@@ -76,7 +79,7 @@ CacheRegistry.register(MODULE_ID, {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 新增:估算字节大小(用于 debug-panel 缓存统计)
|
// estimate bytes for debug panel
|
||||||
getBytes: () => {
|
getBytes: () => {
|
||||||
try {
|
try {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
@@ -137,7 +140,7 @@ CacheRegistry.register(MODULE_ID, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ============= 内部辅助函数 ============= */
|
/* ============ Internal Helpers ============= */
|
||||||
|
|
||||||
function getMsgKey(msg) {
|
function getMsgKey(msg) {
|
||||||
return (typeof msg?.mes === 'string') ? 'mes'
|
return (typeof msg?.mes === 'string') ? 'mes'
|
||||||
@@ -160,7 +163,7 @@ function normalizeOpName(k) {
|
|||||||
return OP_MAP[String(k).toLowerCase().trim()] || null;
|
return OP_MAP[String(k).toLowerCase().trim()] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= 应用签名追踪 ============= */
|
/* ============ Applied Signature Tracking ============= */
|
||||||
|
|
||||||
function getAppliedMap() {
|
function getAppliedMap() {
|
||||||
const meta = getContext()?.chatMetadata || {};
|
const meta = getContext()?.chatMetadata || {};
|
||||||
@@ -206,10 +209,10 @@ function computePlotSignatureFromText(text) {
|
|||||||
return chunks.join('\n---\n');
|
return chunks.join('\n---\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= Plot-Log 解析 ============= */
|
/* ============ Plot-Log Parsing ============= */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取 plot-log 块
|
* Extract plot-log blocks
|
||||||
*/
|
*/
|
||||||
function extractPlotLogBlocks(text) {
|
function extractPlotLogBlocks(text) {
|
||||||
if (!text || typeof text !== 'string') return [];
|
if (!text || typeof text !== 'string') return [];
|
||||||
@@ -224,10 +227,10 @@ function extractPlotLogBlocks(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 plot-log 块内容
|
* Parse plot-log block content
|
||||||
*/
|
*/
|
||||||
function parseBlock(innerText) {
|
function parseBlock(innerText) {
|
||||||
// 预处理 bump 别名
|
// preprocess bump aliases
|
||||||
innerText = preprocessBumpAliases(innerText);
|
innerText = preprocessBumpAliases(innerText);
|
||||||
const textForJsonToml = stripLeadingHtmlComments(innerText);
|
const textForJsonToml = stripLeadingHtmlComments(innerText);
|
||||||
|
|
||||||
@@ -243,7 +246,7 @@ function parseBlock(innerText) {
|
|||||||
};
|
};
|
||||||
const norm = (p) => String(p || '').replace(/\[(\d+)\]/g, '.$1');
|
const norm = (p) => String(p || '').replace(/\[(\d+)\]/g, '.$1');
|
||||||
|
|
||||||
// 守护指令记录
|
// guard directive tracking
|
||||||
const guardMap = new Map();
|
const guardMap = new Map();
|
||||||
|
|
||||||
const recordGuardDirective = (path, directives) => {
|
const recordGuardDirective = (path, directives) => {
|
||||||
@@ -292,7 +295,7 @@ function parseBlock(innerText) {
|
|||||||
return { directives, curPathRaw, guardTargetRaw, segment: segTrim };
|
return { directives, curPathRaw, guardTargetRaw, segment: segTrim };
|
||||||
};
|
};
|
||||||
|
|
||||||
// 操作记录函数
|
// operation record helpers
|
||||||
const putSet = (top, path, value) => {
|
const putSet = (top, path, value) => {
|
||||||
ops.set[top] ||= {};
|
ops.set[top] ||= {};
|
||||||
ops.set[top][path] = value;
|
ops.set[top][path] = value;
|
||||||
@@ -348,7 +351,7 @@ function parseBlock(innerText) {
|
|||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解码键
|
// decode key
|
||||||
const decodeKey = (rawKey) => {
|
const decodeKey = (rawKey) => {
|
||||||
const { directives, remainder, original } = extractDirectiveInfo(rawKey);
|
const { directives, remainder, original } = extractDirectiveInfo(rawKey);
|
||||||
const path = (remainder || original || String(rawKey)).trim();
|
const path = (remainder || original || String(rawKey)).trim();
|
||||||
@@ -356,7 +359,7 @@ function parseBlock(innerText) {
|
|||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 遍历节点
|
// walk nodes
|
||||||
const walkNode = (op, top, node, basePath = '') => {
|
const walkNode = (op, top, node, basePath = '') => {
|
||||||
if (op === 'set') {
|
if (op === 'set') {
|
||||||
if (node === null || node === undefined) return;
|
if (node === null || node === undefined) return;
|
||||||
@@ -441,7 +444,7 @@ function parseBlock(innerText) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理结构化数据(JSON/TOML)
|
// process structured data (json/toml)
|
||||||
const processStructuredData = (data) => {
|
const processStructuredData = (data) => {
|
||||||
const process = (d) => {
|
const process = (d) => {
|
||||||
if (!d || typeof d !== 'object') return;
|
if (!d || typeof d !== 'object') return;
|
||||||
@@ -507,7 +510,7 @@ function parseBlock(innerText) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 尝试 JSON 解析
|
// try JSON parsing
|
||||||
const tryParseJson = (text) => {
|
const tryParseJson = (text) => {
|
||||||
const s = String(text || '').trim();
|
const s = String(text || '').trim();
|
||||||
if (!s || (s[0] !== '{' && s[0] !== '[')) return false;
|
if (!s || (s[0] !== '{' && s[0] !== '[')) return false;
|
||||||
@@ -563,7 +566,7 @@ function parseBlock(innerText) {
|
|||||||
return relaxed !== s && attempt(relaxed);
|
return relaxed !== s && attempt(relaxed);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 尝试 TOML 解析
|
// try TOML parsing
|
||||||
const tryParseToml = (text) => {
|
const tryParseToml = (text) => {
|
||||||
const src = String(text || '').trim();
|
const src = String(text || '').trim();
|
||||||
if (!src || !src.includes('[') || !src.includes('=')) return false;
|
if (!src || !src.includes('[') || !src.includes('=')) return false;
|
||||||
@@ -638,11 +641,11 @@ function parseBlock(innerText) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 尝试 JSON/TOML
|
// try JSON/TOML
|
||||||
if (tryParseJson(textForJsonToml)) return finalizeResults();
|
if (tryParseJson(textForJsonToml)) return finalizeResults();
|
||||||
if (tryParseToml(textForJsonToml)) return finalizeResults();
|
if (tryParseToml(textForJsonToml)) return finalizeResults();
|
||||||
|
|
||||||
// YAML 解析
|
// YAML parsing
|
||||||
let curOp = '';
|
let curOp = '';
|
||||||
const stack = [];
|
const stack = [];
|
||||||
|
|
||||||
@@ -729,7 +732,8 @@ function parseBlock(innerText) {
|
|||||||
const curPath = norm(curPathRaw);
|
const curPath = norm(curPathRaw);
|
||||||
if (!curPath) continue;
|
if (!curPath) continue;
|
||||||
|
|
||||||
// 块标量
|
// note
|
||||||
|
|
||||||
if (rhs && (rhs[0] === '|' || rhs[0] === '>')) {
|
if (rhs && (rhs[0] === '|' || rhs[0] === '>')) {
|
||||||
const { text, next } = readBlockScalar(i + 1, ind, rhs[0]);
|
const { text, next } = readBlockScalar(i + 1, ind, rhs[0]);
|
||||||
i = next;
|
i = next;
|
||||||
@@ -741,7 +745,7 @@ function parseBlock(innerText) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 空值(嵌套对象或列表)
|
// empty value (nested object or list)
|
||||||
if (rhs === '') {
|
if (rhs === '') {
|
||||||
stack.push({
|
stack.push({
|
||||||
indent: ind,
|
indent: ind,
|
||||||
@@ -791,7 +795,8 @@ function parseBlock(innerText) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通值
|
// note
|
||||||
|
|
||||||
const [top, ...rest] = curPath.split('.');
|
const [top, ...rest] = curPath.split('.');
|
||||||
const rel = rest.join('.');
|
const rel = rest.join('.');
|
||||||
if (curOp === 'set') {
|
if (curOp === 'set') {
|
||||||
@@ -817,7 +822,8 @@ function parseBlock(innerText) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 顶层列表项(del 操作)
|
// note
|
||||||
|
|
||||||
const mArr = t.match(/^-+\s*(.+)$/);
|
const mArr = t.match(/^-+\s*(.+)$/);
|
||||||
if (mArr && stack.length === 0 && curOp === 'del') {
|
if (mArr && stack.length === 0 && curOp === 'del') {
|
||||||
const rawItem = stripQ(stripYamlInlineComment(mArr[1]));
|
const rawItem = stripQ(stripYamlInlineComment(mArr[1]));
|
||||||
@@ -830,7 +836,8 @@ function parseBlock(innerText) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 嵌套列表项
|
// note
|
||||||
|
|
||||||
if (mArr && stack.length) {
|
if (mArr && stack.length) {
|
||||||
const curPath = stack[stack.length - 1].path;
|
const curPath = stack[stack.length - 1].path;
|
||||||
const [top, ...rest] = curPath.split('.');
|
const [top, ...rest] = curPath.split('.');
|
||||||
@@ -856,7 +863,7 @@ function parseBlock(innerText) {
|
|||||||
return finalizeResults();
|
return finalizeResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= 变量守护与规则集 ============= */
|
/* ============ Variable Guard & Rules ============= */
|
||||||
|
|
||||||
function rulesGetTable() {
|
function rulesGetTable() {
|
||||||
return guardianState.table || {};
|
return guardianState.table || {};
|
||||||
@@ -877,7 +884,7 @@ function rulesLoadFromMeta() {
|
|||||||
const raw = meta[LWB_RULES_KEY];
|
const raw = meta[LWB_RULES_KEY];
|
||||||
if (raw && typeof raw === 'object') {
|
if (raw && typeof raw === 'object') {
|
||||||
rulesSetTable(deepClone(raw));
|
rulesSetTable(deepClone(raw));
|
||||||
// 重建正则缓存
|
// rebuild regex cache
|
||||||
for (const [p, node] of Object.entries(guardianState.table)) {
|
for (const [p, node] of Object.entries(guardianState.table)) {
|
||||||
if (node?.constraints?.regex?.source) {
|
if (node?.constraints?.regex?.source) {
|
||||||
const src = node.constraints.regex.source;
|
const src = node.constraints.regex.source;
|
||||||
@@ -1043,7 +1050,7 @@ function getEffectiveParentNode(p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 守护验证
|
* guard validation
|
||||||
*/
|
*/
|
||||||
export function guardValidate(op, absPath, payload) {
|
export function guardValidate(op, absPath, payload) {
|
||||||
if (guardianState.bypass) return { allow: true, value: payload };
|
if (guardianState.bypass) return { allow: true, value: payload };
|
||||||
@@ -1057,14 +1064,15 @@ export function guardValidate(op, absPath, payload) {
|
|||||||
constraints: {}
|
constraints: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 只读检查
|
// note
|
||||||
|
|
||||||
if (node.ro) return { allow: false, reason: 'ro' };
|
if (node.ro) return { allow: false, reason: 'ro' };
|
||||||
|
|
||||||
const parentPath = getParentPath(p);
|
const parentPath = getParentPath(p);
|
||||||
const parentNode = parentPath ? (getEffectiveParentNode(p) || { objectPolicy: 'none', arrayPolicy: 'lock' }) : null;
|
const parentNode = parentPath ? (getEffectiveParentNode(p) || { objectPolicy: 'none', arrayPolicy: 'lock' }) : null;
|
||||||
const currentValue = getValueAtPath(p);
|
const currentValue = getValueAtPath(p);
|
||||||
|
|
||||||
// 删除操作
|
// delete op
|
||||||
if (op === 'delNode') {
|
if (op === 'delNode') {
|
||||||
if (!parentPath) return { allow: false, reason: 'no-parent' };
|
if (!parentPath) return { allow: false, reason: 'no-parent' };
|
||||||
|
|
||||||
@@ -1087,7 +1095,7 @@ export function guardValidate(op, absPath, payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 推入操作
|
// push op
|
||||||
if (op === 'push') {
|
if (op === 'push') {
|
||||||
const arr = getValueAtPath(p);
|
const arr = getValueAtPath(p);
|
||||||
if (arr === undefined) {
|
if (arr === undefined) {
|
||||||
@@ -1124,7 +1132,7 @@ export function guardValidate(op, absPath, payload) {
|
|||||||
return { allow: true, value: payload };
|
return { allow: true, value: payload };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增量操作
|
// bump op
|
||||||
if (op === 'bump') {
|
if (op === 'bump') {
|
||||||
let d = Number(payload);
|
let d = Number(payload);
|
||||||
if (!Number.isFinite(d)) return { allow: false, reason: 'delta-nan' };
|
if (!Number.isFinite(d)) return { allow: false, reason: 'delta-nan' };
|
||||||
@@ -1167,7 +1175,7 @@ export function guardValidate(op, absPath, payload) {
|
|||||||
return { allow: true, value: clamped.value };
|
return { allow: true, value: clamped.value };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置操作
|
// set op
|
||||||
if (op === 'set') {
|
if (op === 'set') {
|
||||||
const exists = currentValue !== undefined;
|
const exists = currentValue !== undefined;
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -1229,7 +1237,7 @@ export function guardValidate(op, absPath, payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用规则增量
|
* apply rules delta
|
||||||
*/
|
*/
|
||||||
export function applyRuleDelta(path, delta) {
|
export function applyRuleDelta(path, delta) {
|
||||||
const p = normalizePath(path);
|
const p = normalizePath(path);
|
||||||
@@ -1284,7 +1292,7 @@ export function applyRuleDelta(path, delta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从树加载规则
|
* load rules from tree
|
||||||
*/
|
*/
|
||||||
export function rulesLoadFromTree(valueTree, basePath) {
|
export function rulesLoadFromTree(valueTree, basePath) {
|
||||||
const isObj = v => v && typeof v === 'object' && !Array.isArray(v);
|
const isObj = v => v && typeof v === 'object' && !Array.isArray(v);
|
||||||
@@ -1351,7 +1359,7 @@ export function rulesLoadFromTree(valueTree, basePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用规则增量表
|
* apply rules delta table
|
||||||
*/
|
*/
|
||||||
export function applyRulesDeltaToTable(delta) {
|
export function applyRulesDeltaToTable(delta) {
|
||||||
if (!delta || typeof delta !== 'object') return;
|
if (!delta || typeof delta !== 'object') return;
|
||||||
@@ -1362,7 +1370,7 @@ export function applyRulesDeltaToTable(delta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安装变量 API 补丁
|
* install variable API patch
|
||||||
*/
|
*/
|
||||||
function installVariableApiPatch() {
|
function installVariableApiPatch() {
|
||||||
try {
|
try {
|
||||||
@@ -1449,7 +1457,7 @@ function installVariableApiPatch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 卸载变量 API 补丁
|
* uninstall variable API patch
|
||||||
*/
|
*/
|
||||||
function uninstallVariableApiPatch() {
|
function uninstallVariableApiPatch() {
|
||||||
try {
|
try {
|
||||||
@@ -1467,7 +1475,7 @@ function uninstallVariableApiPatch() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= 快照/回滚 ============= */
|
/* ============ Snapshots / Rollback ============= */
|
||||||
|
|
||||||
function getSnapMap() {
|
function getSnapMap() {
|
||||||
const meta = getContext()?.chatMetadata || {};
|
const meta = getContext()?.chatMetadata || {};
|
||||||
@@ -1488,7 +1496,7 @@ function setVarDict(dict) {
|
|||||||
const current = meta.variables || {};
|
const current = meta.variables || {};
|
||||||
const next = dict || {};
|
const next = dict || {};
|
||||||
|
|
||||||
// 清除不存在的变量
|
// remove missing variables
|
||||||
for (const k of Object.keys(current)) {
|
for (const k of Object.keys(current)) {
|
||||||
if (!(k in next)) {
|
if (!(k in next)) {
|
||||||
try { delete current[k]; } catch {}
|
try { delete current[k]; } catch {}
|
||||||
@@ -1496,7 +1504,8 @@ function setVarDict(dict) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置新值
|
// note
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(next)) {
|
for (const [k, v] of Object.entries(next)) {
|
||||||
let toStore = v;
|
let toStore = v;
|
||||||
if (v && typeof v === 'object') {
|
if (v && typeof v === 'object') {
|
||||||
@@ -1615,6 +1624,13 @@ function rollbackToPreviousOf(messageId) {
|
|||||||
const id = Number(messageId);
|
const id = Number(messageId);
|
||||||
if (Number.isNaN(id)) return;
|
if (Number.isNaN(id)) return;
|
||||||
|
|
||||||
|
clearStateAppliedFrom(id);
|
||||||
|
|
||||||
|
if (typeof globalThis.LWB_StateRollbackHook === 'function') {
|
||||||
|
Promise.resolve(globalThis.LWB_StateRollbackHook(id)).catch((e) => {
|
||||||
|
console.error('[variablesCore] LWB_StateRollbackHook failed:', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
const prevId = id - 1;
|
const prevId = id - 1;
|
||||||
if (prevId < 0) return;
|
if (prevId < 0) return;
|
||||||
|
|
||||||
@@ -1641,10 +1657,10 @@ function rebuildVariablesFromScratch() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= 应用变量到消息 ============= */
|
/* ============ Apply Variables To Message ============= */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将对象模式转换
|
* switch to object mode
|
||||||
*/
|
*/
|
||||||
function asObject(rec) {
|
function asObject(rec) {
|
||||||
if (rec.mode !== 'object') {
|
if (rec.mode !== 'object') {
|
||||||
@@ -1658,7 +1674,7 @@ function asObject(rec) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增量操作辅助
|
* bump helper
|
||||||
*/
|
*/
|
||||||
function bumpAtPath(rec, path, delta) {
|
function bumpAtPath(rec, path, delta) {
|
||||||
const numDelta = Number(delta);
|
const numDelta = Number(delta);
|
||||||
@@ -1715,7 +1731,7 @@ function bumpAtPath(rec, path, delta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析标量数组
|
* parse scalar array
|
||||||
*/
|
*/
|
||||||
function parseScalarArrayMaybe(str) {
|
function parseScalarArrayMaybe(str) {
|
||||||
try {
|
try {
|
||||||
@@ -1727,8 +1743,55 @@ function parseScalarArrayMaybe(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用变量到消息
|
* apply variables for message
|
||||||
*/
|
*/
|
||||||
|
function readMessageText(msg) {
|
||||||
|
if (!msg) return '';
|
||||||
|
if (typeof msg.mes === 'string') return msg.mes;
|
||||||
|
if (typeof msg.content === 'string') return msg.content;
|
||||||
|
if (Array.isArray(msg.content)) {
|
||||||
|
return msg.content
|
||||||
|
.filter(p => p?.type === 'text' && typeof p.text === 'string')
|
||||||
|
.map(p => p.text)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariablesMode() {
|
||||||
|
try {
|
||||||
|
return extension_settings?.[EXT_ID]?.variablesMode || '1.0';
|
||||||
|
} catch {
|
||||||
|
return '1.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyVarsForMessage(messageId) {
|
||||||
|
const ctx = getContext();
|
||||||
|
const msg = ctx?.chat?.[messageId];
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
const text = readMessageText(msg);
|
||||||
|
const mode = getVariablesMode();
|
||||||
|
|
||||||
|
if (mode === '2.0') {
|
||||||
|
const result = applyStateForMessage(messageId, text);
|
||||||
|
|
||||||
|
if (result.errors?.length) {
|
||||||
|
console.warn('[variablesCore][2.0] warnings:', result.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.atoms?.length) {
|
||||||
|
$(document).trigger('xiaobaix:variables:stateAtomsGenerated', {
|
||||||
|
messageId,
|
||||||
|
atoms: result.atoms
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyVariablesForMessage(messageId);
|
||||||
|
}
|
||||||
async function applyVariablesForMessage(messageId) {
|
async function applyVariablesForMessage(messageId) {
|
||||||
try {
|
try {
|
||||||
const ctx = getContext();
|
const ctx = getContext();
|
||||||
@@ -1739,7 +1802,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
const preview = (text, max = 220) => {
|
const preview = (text, max = 220) => {
|
||||||
try {
|
try {
|
||||||
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
|
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
|
||||||
return s.length > max ? s.slice(0, max) + '…' : s;
|
return s.length > max ? s.slice(0, max) + '...' : s;
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -1779,7 +1842,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
parseErrors++;
|
parseErrors++;
|
||||||
if (debugOn) {
|
if (debugOn) {
|
||||||
try { xbLog.error(MODULE_ID, `plot-log 解析失败:楼层=${messageId} 块#${idx + 1} 预览=${preview(b)}`, e); } catch {}
|
try { xbLog.error(MODULE_ID, `plot-log è§£æž<EFBFBD>失败:楼å±?${messageId} å<EFBFBD>?${idx + 1} 预览=${preview(b)}`, e); } catch {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1810,7 +1873,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
try {
|
try {
|
||||||
xbLog.warn(
|
xbLog.warn(
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
`plot-log 未产生可执行指令:楼层=${messageId} 块数=${blocks.length} 解析条目=${parsedPartsTotal} 解析失败=${parseErrors} 预览=${preview(blocks[0])}`
|
`plot-log 未产生å<EFBFBD>¯æ‰§è¡ŒæŒ‡ä»¤ï¼šæ¥¼å±?${messageId} å<EFBFBD>—æ•°=${blocks.length} è§£æž<EFBFBD>æ<EFBFBD>¡ç›®=${parsedPartsTotal} è§£æž<EFBFBD>失败=${parseErrors} 预览=${preview(blocks[0])}`
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -1818,7 +1881,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建变量记录
|
// build variable records
|
||||||
const byName = new Map();
|
const byName = new Map();
|
||||||
|
|
||||||
for (const { name } of ops) {
|
for (const { name } of ops) {
|
||||||
@@ -1838,9 +1901,9 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
|
|
||||||
const norm = (p) => String(p || '').replace(/\[(\d+)\]/g, '.$1');
|
const norm = (p) => String(p || '').replace(/\[(\d+)\]/g, '.$1');
|
||||||
|
|
||||||
// 执行操作
|
// execute operations
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
// 守护指令
|
// guard directives
|
||||||
if (op.operation === 'guard') {
|
if (op.operation === 'guard') {
|
||||||
for (const entry of op.data) {
|
for (const entry of op.data) {
|
||||||
const path = typeof entry?.path === 'string' ? entry.path.trim() : '';
|
const path = typeof entry?.path === 'string' ? entry.path.trim() : '';
|
||||||
@@ -1865,7 +1928,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
const rec = byName.get(root);
|
const rec = byName.get(root);
|
||||||
if (!rec) continue;
|
if (!rec) continue;
|
||||||
|
|
||||||
// SET 操作
|
// set op
|
||||||
if (op.operation === 'setObject') {
|
if (op.operation === 'setObject') {
|
||||||
for (const [k, v] of Object.entries(op.data)) {
|
for (const [k, v] of Object.entries(op.data)) {
|
||||||
const localPath = joinPath(subPath, k);
|
const localPath = joinPath(subPath, k);
|
||||||
@@ -1903,7 +1966,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEL 操作
|
// delete op
|
||||||
else if (op.operation === 'del') {
|
else if (op.operation === 'del') {
|
||||||
const obj = asObject(rec);
|
const obj = asObject(rec);
|
||||||
const pending = [];
|
const pending = [];
|
||||||
@@ -1951,7 +2014,8 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按索引分组(倒序删除)
|
// note
|
||||||
|
|
||||||
const arrGroups = new Map();
|
const arrGroups = new Map();
|
||||||
const objDeletes = [];
|
const objDeletes = [];
|
||||||
|
|
||||||
@@ -1977,7 +2041,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUSH 操作
|
// push op
|
||||||
else if (op.operation === 'push') {
|
else if (op.operation === 'push') {
|
||||||
for (const [k, vals] of Object.entries(op.data)) {
|
for (const [k, vals] of Object.entries(op.data)) {
|
||||||
const localPath = joinPath(subPath, k);
|
const localPath = joinPath(subPath, k);
|
||||||
@@ -2033,7 +2097,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUMP 操作
|
// bump op
|
||||||
else if (op.operation === 'bump') {
|
else if (op.operation === 'bump') {
|
||||||
for (const [k, delta] of Object.entries(op.data)) {
|
for (const [k, delta] of Object.entries(op.data)) {
|
||||||
const num = Number(delta);
|
const num = Number(delta);
|
||||||
@@ -2077,7 +2141,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有变化
|
// check for changes
|
||||||
const hasChanges = Array.from(byName.values()).some(rec => rec?.changed === true);
|
const hasChanges = Array.from(byName.values()).some(rec => rec?.changed === true);
|
||||||
if (!hasChanges && delVarNames.size === 0) {
|
if (!hasChanges && delVarNames.size === 0) {
|
||||||
if (debugOn) {
|
if (debugOn) {
|
||||||
@@ -2085,7 +2149,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
const denied = guardDenied ? `,被规则拦截=${guardDenied}` : '';
|
const denied = guardDenied ? `,被规则拦截=${guardDenied}` : '';
|
||||||
xbLog.warn(
|
xbLog.warn(
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
`plot-log 指令执行后无变化:楼层=${messageId} 指令数=${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
|
`plot-log 指令执行å<EFBFBD>Žæ— å<EFBFBD>˜åŒ–:楼å±?${messageId} 指令æ•?${ops.length}${denied} 示例=${preview(JSON.stringify(guardDeniedSamples))}`
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -2093,7 +2157,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存变量
|
// save variables
|
||||||
for (const [name, rec] of byName.entries()) {
|
for (const [name, rec] of byName.entries()) {
|
||||||
if (!rec.changed) continue;
|
if (!rec.changed) continue;
|
||||||
try {
|
try {
|
||||||
@@ -2105,7 +2169,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除变量
|
// delete variables
|
||||||
if (delVarNames.size > 0) {
|
if (delVarNames.size > 0) {
|
||||||
try {
|
try {
|
||||||
for (const v of delVarNames) {
|
for (const v of delVarNames) {
|
||||||
@@ -2124,7 +2188,7 @@ async function applyVariablesForMessage(messageId) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= 事件处理 ============= */
|
/* ============ Event Handling ============= */
|
||||||
|
|
||||||
function getMsgIdLoose(payload) {
|
function getMsgIdLoose(payload) {
|
||||||
if (payload && typeof payload === 'object') {
|
if (payload && typeof payload === 'object') {
|
||||||
@@ -2150,56 +2214,57 @@ function bindEvents() {
|
|||||||
let lastSwipedId;
|
let lastSwipedId;
|
||||||
suppressUpdatedOnce = new Set();
|
suppressUpdatedOnce = new Set();
|
||||||
|
|
||||||
// 消息发送
|
// note
|
||||||
|
|
||||||
events?.on(event_types.MESSAGE_SENT, async () => {
|
events?.on(event_types.MESSAGE_SENT, async () => {
|
||||||
try {
|
try {
|
||||||
snapshotCurrentLastFloor();
|
snapshotCurrentLastFloor();
|
||||||
const chat = getContext()?.chat || [];
|
const chat = getContext()?.chat || [];
|
||||||
const id = chat.length ? chat.length - 1 : undefined;
|
const id = chat.length ? chat.length - 1 : undefined;
|
||||||
if (typeof id === 'number') {
|
if (typeof id === 'number') {
|
||||||
await applyVariablesForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
applyXbGetVarForMessage(id, true);
|
applyXbGetVarForMessage(id, true);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 消息接收
|
// message received
|
||||||
events?.on(event_types.MESSAGE_RECEIVED, async (data) => {
|
events?.on(event_types.MESSAGE_RECEIVED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdLoose(data);
|
const id = getMsgIdLoose(data);
|
||||||
if (typeof id === 'number') {
|
if (typeof id === 'number') {
|
||||||
await applyVariablesForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
applyXbGetVarForMessage(id, true);
|
applyXbGetVarForMessage(id, true);
|
||||||
await executeQueuedVareventJsAfterTurn();
|
await executeQueuedVareventJsAfterTurn();
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 用户消息渲染
|
// user message rendered
|
||||||
events?.on(event_types.USER_MESSAGE_RENDERED, async (data) => {
|
events?.on(event_types.USER_MESSAGE_RENDERED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdLoose(data);
|
const id = getMsgIdLoose(data);
|
||||||
if (typeof id === 'number') {
|
if (typeof id === 'number') {
|
||||||
await applyVariablesForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
applyXbGetVarForMessage(id, true);
|
applyXbGetVarForMessage(id, true);
|
||||||
snapshotForMessageId(id);
|
snapshotForMessageId(id);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 角色消息渲染
|
// character message rendered
|
||||||
events?.on(event_types.CHARACTER_MESSAGE_RENDERED, async (data) => {
|
events?.on(event_types.CHARACTER_MESSAGE_RENDERED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdLoose(data);
|
const id = getMsgIdLoose(data);
|
||||||
if (typeof id === 'number') {
|
if (typeof id === 'number') {
|
||||||
await applyVariablesForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
applyXbGetVarForMessage(id, true);
|
applyXbGetVarForMessage(id, true);
|
||||||
snapshotForMessageId(id);
|
snapshotForMessageId(id);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 消息更新
|
// message updated
|
||||||
events?.on(event_types.MESSAGE_UPDATED, async (data) => {
|
events?.on(event_types.MESSAGE_UPDATED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdLoose(data);
|
const id = getMsgIdLoose(data);
|
||||||
@@ -2208,13 +2273,13 @@ function bindEvents() {
|
|||||||
suppressUpdatedOnce.delete(id);
|
suppressUpdatedOnce.delete(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await applyVariablesForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
applyXbGetVarForMessage(id, true);
|
applyXbGetVarForMessage(id, true);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 消息编辑
|
// message edited
|
||||||
events?.on(event_types.MESSAGE_EDITED, async (data) => {
|
events?.on(event_types.MESSAGE_EDITED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdLoose(data);
|
const id = getMsgIdLoose(data);
|
||||||
@@ -2223,7 +2288,7 @@ function bindEvents() {
|
|||||||
rollbackToPreviousOf(id);
|
rollbackToPreviousOf(id);
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await applyVariablesForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
applyXbGetVarForMessage(id, true);
|
applyXbGetVarForMessage(id, true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -2248,7 +2313,7 @@ function bindEvents() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 消息滑动
|
// message swiped
|
||||||
events?.on(event_types.MESSAGE_SWIPED, async (data) => {
|
events?.on(event_types.MESSAGE_SWIPED, async (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdLoose(data);
|
const id = getMsgIdLoose(data);
|
||||||
@@ -2259,7 +2324,7 @@ function bindEvents() {
|
|||||||
|
|
||||||
const tId = setTimeout(async () => {
|
const tId = setTimeout(async () => {
|
||||||
pendingSwipeApply.delete(id);
|
pendingSwipeApply.delete(id);
|
||||||
await applyVariablesForMessage(id);
|
await applyVarsForMessage(id);
|
||||||
await executeQueuedVareventJsAfterTurn();
|
await executeQueuedVareventJsAfterTurn();
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
@@ -2268,7 +2333,7 @@ function bindEvents() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 消息删除
|
// message deleted
|
||||||
events?.on(event_types.MESSAGE_DELETED, (data) => {
|
events?.on(event_types.MESSAGE_DELETED, (data) => {
|
||||||
try {
|
try {
|
||||||
const id = getMsgIdStrict(data);
|
const id = getMsgIdStrict(data);
|
||||||
@@ -2280,12 +2345,13 @@ function bindEvents() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成开始
|
// note
|
||||||
|
|
||||||
events?.on(event_types.GENERATION_STARTED, (data) => {
|
events?.on(event_types.GENERATION_STARTED, (data) => {
|
||||||
try {
|
try {
|
||||||
snapshotPreviousFloor();
|
snapshotPreviousFloor();
|
||||||
|
|
||||||
// 取消滑动延迟
|
// cancel swipe delay
|
||||||
const t = (typeof data === 'string' ? data : (data?.type || '')).toLowerCase();
|
const t = (typeof data === 'string' ? data : (data?.type || '')).toLowerCase();
|
||||||
if (t === 'swipe' && lastSwipedId != null) {
|
if (t === 'swipe' && lastSwipedId != null) {
|
||||||
const tId = pendingSwipeApply.get(lastSwipedId);
|
const tId = pendingSwipeApply.get(lastSwipedId);
|
||||||
@@ -2297,7 +2363,7 @@ function bindEvents() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 聊天切换
|
// chat changed
|
||||||
events?.on(event_types.CHAT_CHANGED, () => {
|
events?.on(event_types.CHAT_CHANGED, () => {
|
||||||
try {
|
try {
|
||||||
rulesClearCache();
|
rulesClearCache();
|
||||||
@@ -2310,29 +2376,31 @@ function bindEvents() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= 初始化与清理 ============= */
|
/* ============ Init & Cleanup ============= */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化模块
|
* init module
|
||||||
*/
|
*/
|
||||||
export function initVariablesCore() {
|
export function initVariablesCore() {
|
||||||
try { xbLog.info('variablesCore', 'å<>˜é‡<C3A9>系统å<C5B8>¯åЍ'); } catch {}
|
try { xbLog.info('variablesCore', 'å<>˜é‡<C3A9>系统å<C5B8>¯åЍ'); } catch {}
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
// 创建事件管理器
|
// init events
|
||||||
|
|
||||||
events = createModuleEvents(MODULE_ID);
|
events = createModuleEvents(MODULE_ID);
|
||||||
|
|
||||||
// 加载规则
|
// load rules
|
||||||
rulesLoadFromMeta();
|
rulesLoadFromMeta();
|
||||||
|
|
||||||
// 安装 API 补丁
|
// install API patch
|
||||||
installVariableApiPatch();
|
installVariableApiPatch();
|
||||||
|
|
||||||
// 绑定事件
|
// bind events
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
// 挂载全局函数(供 var-commands.js 使用)
|
// note
|
||||||
|
|
||||||
globalThis.LWB_Guard = {
|
globalThis.LWB_Guard = {
|
||||||
validate: guardValidate,
|
validate: guardValidate,
|
||||||
loadRules: rulesLoadFromTree,
|
loadRules: rulesLoadFromTree,
|
||||||
@@ -2343,45 +2411,45 @@ export function initVariablesCore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理模块
|
* cleanup module
|
||||||
*/
|
*/
|
||||||
export function cleanupVariablesCore() {
|
export function cleanupVariablesCore() {
|
||||||
try { xbLog.info('variablesCore', 'å<>˜é‡<C3A9>系统清ç<E280A6>†'); } catch {}
|
try { xbLog.info('variablesCore', 'å<>˜é‡<C3A9>系统清ç<E280A6>†'); } catch {}
|
||||||
if (!initialized) return;
|
if (!initialized) return;
|
||||||
|
|
||||||
// 清理事件
|
// cleanup events
|
||||||
events?.cleanup();
|
events?.cleanup();
|
||||||
events = null;
|
events = null;
|
||||||
|
|
||||||
// 卸载 API 补丁
|
// uninstall API patch
|
||||||
uninstallVariableApiPatch();
|
uninstallVariableApiPatch();
|
||||||
|
|
||||||
// 清理规则
|
// clear rules
|
||||||
rulesClearCache();
|
rulesClearCache();
|
||||||
|
|
||||||
// 清理全局函数
|
// clear global hooks
|
||||||
delete globalThis.LWB_Guard;
|
delete globalThis.LWB_Guard;
|
||||||
|
|
||||||
// 清理守护状态
|
// clear guard state
|
||||||
guardBypass(false);
|
guardBypass(false);
|
||||||
|
|
||||||
initialized = false;
|
initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============= 导出 ============= */
|
/* ============ Exports ============= */
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MODULE_ID,
|
MODULE_ID,
|
||||||
// 解析
|
// parsing
|
||||||
parseBlock,
|
parseBlock,
|
||||||
applyVariablesForMessage,
|
applyVariablesForMessage,
|
||||||
extractPlotLogBlocks,
|
extractPlotLogBlocks,
|
||||||
// 快照
|
// snapshots
|
||||||
snapshotCurrentLastFloor,
|
snapshotCurrentLastFloor,
|
||||||
snapshotForMessageId,
|
snapshotForMessageId,
|
||||||
rollbackToPreviousOf,
|
rollbackToPreviousOf,
|
||||||
rebuildVariablesFromScratch,
|
rebuildVariablesFromScratch,
|
||||||
// 规则
|
// rules
|
||||||
rulesGetTable,
|
rulesGetTable,
|
||||||
rulesSetTable,
|
rulesSetTable,
|
||||||
rulesLoadFromMeta,
|
rulesLoadFromMeta,
|
||||||
|
|||||||
@@ -213,10 +213,16 @@
|
|||||||
<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" style="gap:8px;flex-wrap:wrap;align-items:center;">
|
||||||
<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>
|
||||||
|
|
||||||
|
<select id="xiaobaix_variables_mode" class="text_pole" style="width:auto;margin-left:8px;padding:2px 6px;">
|
||||||
|
<option value="1.0">1.0 (plot-log)</option>
|
||||||
|
<option value="2.0">2.0 (state)</option>
|
||||||
|
</select>
|
||||||
</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" />
|
||||||
<label for="xiaobaix_variables_panel_enabled">变量面板</label>
|
<label for="xiaobaix_variables_panel_enabled">变量面板</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user