feat: variables 2.0 state + L0 summary integration

This commit is contained in:
2026-01-31 23:06:03 +08:00
parent 201c74dc71
commit 4b0541610b
22 changed files with 1949 additions and 2314 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-syntax */
/** /**
* ============================================================================ * ============================================================================
* Story Outline 模块 - 小白板 * Story Outline 模块 - 小白板

View File

@@ -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;

View File

@@ -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()}`;
} }

View File

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

View File

@@ -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 [];

View File

@@ -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);
} }
} }

View File

@@ -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 };
} }

View 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 };
}

View 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);
}

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

View File

@@ -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,

View File

@@ -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,
@@ -862,6 +884,7 @@ class StreamingGeneration {
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 = []
let bottomMsgs = await mapHistoryPlaceholders(
[]
.concat(bottomComposite ? this._parseCompositeParam(bottomComposite) : []) .concat(bottomComposite ? this._parseCompositeParam(bottomComposite) : [])
.concat(createMsgs('bottom')) .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}'));

View 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 };
}

View 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';

View 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[]
* 单行支持运算符,多行只支持覆盖 setYAML
*/
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 };
}

View 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}`;
}
}

View File

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

View File

@@ -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,

View File

@@ -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>