dc603b8b49
Enhances validation to clean corrupted items at load time while preserving valid ones, rather than discarding entire sections. Also auto-capitalizes first letter of items for consistency. New capability - Granular Item Cleaning: 1. **cleanItemString()** (src/utils/security.js): - Parses item string, removes bad items, re-serializes clean ones - Applies ALL parsing rules: markdown, sanitization, length limits - Used at load time to clean persisted data immediately - Returns "None" if no valid items remain 2. **Enhanced validateStoredInventory()**: - Now cleans items within each location - Only removes locations if ALL items are invalid - Example: "Home": "Sword, __proto__, Shield" → "Home": "Sword, Shield" - Example: "Bad": "__proto__, constructor" → location removed 3. **Enhanced validateInventoryStructure()** (src/core/persistence.js): - Cleans onPerson, stored, and assets at load time - Logs exactly what was cleaned for debugging - Auto-saves cleaned data back to storage Auto-Capitalization: - Added to cleanSingleItem() in itemParser.js - Capitalizes first letter of each item after all cleaning - Preserves rest of case: "iPhone" → "iPhone" (not "Iphone") - Examples: "sword" → "Sword", "3x potions" → "3x potions" Behavior examples: Before (threw away entire array): - "Home": "Sword, " + "A".repeat(600) + ", Shield" → Entire location lost After (granular cleaning): - "Home": "Sword, " + "A".repeat(600) + ", Shield" → "Home": "Sword, AAA...(500 chars), Shield" Before (kept corrupted data): - onPerson: "sword, __proto__, shield" → Stored as-is, filtered only at render After (cleaned at load): - onPerson: "Sword, Shield" → Cleaned and saved immediately, capitalized Benefits: - ✓ Preserves valid items when some are corrupted - ✓ Cleans data at source, not just at render - ✓ Detailed logging of what was cleaned - ✓ Consistent capitalization across all items - ✓ Single source of truth for "valid item"
264 lines
8.9 KiB
JavaScript
264 lines
8.9 KiB
JavaScript
/**
|
|
* Core Persistence Module
|
|
* Handles saving/loading extension settings and chat data
|
|
*/
|
|
|
|
import { saveSettingsDebounced, chat_metadata, saveChatDebounced } from '../../../../../../script.js';
|
|
import { power_user } from '../../../../../power-user.js';
|
|
import { getContext } from '../../../../../extensions.js';
|
|
import {
|
|
extensionSettings,
|
|
lastGeneratedData,
|
|
setExtensionSettings,
|
|
updateExtensionSettings,
|
|
setLastGeneratedData,
|
|
FEATURE_FLAGS
|
|
} from './state.js';
|
|
import { migrateInventory } from '../utils/migration.js';
|
|
import { validateStoredInventory, cleanItemString } from '../utils/security.js';
|
|
|
|
const extensionName = 'third-party/rpg-companion-sillytavern';
|
|
|
|
/**
|
|
* Loads the extension settings from the global settings object.
|
|
* Automatically migrates v1 inventory to v2 format if needed.
|
|
*/
|
|
export function loadSettings() {
|
|
if (power_user.extensions && power_user.extensions[extensionName]) {
|
|
updateExtensionSettings(power_user.extensions[extensionName]);
|
|
// console.log('[RPG Companion] Settings loaded:', extensionSettings);
|
|
} else {
|
|
// console.log('[RPG Companion] No saved settings found, using defaults');
|
|
}
|
|
|
|
// Migrate inventory if feature flag enabled
|
|
if (FEATURE_FLAGS.useNewInventory) {
|
|
const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
|
|
if (migrationResult.migrated) {
|
|
console.log(`[RPG Companion] Inventory migrated from ${migrationResult.source} to v2 format`);
|
|
extensionSettings.userStats.inventory = migrationResult.inventory;
|
|
saveSettings(); // Persist migrated inventory
|
|
}
|
|
}
|
|
|
|
// Validate inventory structure (Bug #3 fix)
|
|
validateInventoryStructure(extensionSettings.userStats.inventory, 'settings');
|
|
}
|
|
|
|
/**
|
|
* Saves the extension settings to the global settings object.
|
|
*/
|
|
export function saveSettings() {
|
|
if (!power_user.extensions) {
|
|
power_user.extensions = {};
|
|
}
|
|
power_user.extensions[extensionName] = extensionSettings;
|
|
saveSettingsDebounced();
|
|
}
|
|
|
|
/**
|
|
* Saves RPG data to the current chat's metadata.
|
|
*/
|
|
export function saveChatData() {
|
|
if (!chat_metadata) {
|
|
return;
|
|
}
|
|
|
|
chat_metadata.rpg_companion = {
|
|
userStats: extensionSettings.userStats,
|
|
classicStats: extensionSettings.classicStats,
|
|
lastGeneratedData: lastGeneratedData,
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
saveChatDebounced();
|
|
}
|
|
|
|
/**
|
|
* Updates the last assistant message's swipe data with current tracker data.
|
|
* This ensures user edits are preserved across swipes and included in generation context.
|
|
*/
|
|
export function updateMessageSwipeData() {
|
|
const chat = getContext().chat;
|
|
if (!chat || chat.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Find the last assistant message
|
|
for (let i = chat.length - 1; i >= 0; i--) {
|
|
const message = chat[i];
|
|
if (!message.is_user) {
|
|
// Found last assistant message - update its swipe data
|
|
if (!message.extra) {
|
|
message.extra = {};
|
|
}
|
|
if (!message.extra.rpg_companion_swipes) {
|
|
message.extra.rpg_companion_swipes = {};
|
|
}
|
|
|
|
const swipeId = message.swipe_id || 0;
|
|
message.extra.rpg_companion_swipes[swipeId] = {
|
|
userStats: lastGeneratedData.userStats,
|
|
infoBox: lastGeneratedData.infoBox,
|
|
characterThoughts: lastGeneratedData.characterThoughts
|
|
};
|
|
|
|
// console.log('[RPG Companion] Updated message swipe data after user edit');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads RPG data from the current chat's metadata.
|
|
* Automatically migrates v1 inventory to v2 format if needed.
|
|
*/
|
|
export function loadChatData() {
|
|
if (!chat_metadata || !chat_metadata.rpg_companion) {
|
|
// Reset to defaults if no data exists
|
|
updateExtensionSettings({
|
|
userStats: {
|
|
health: 100,
|
|
satiety: 100,
|
|
energy: 100,
|
|
hygiene: 100,
|
|
arousal: 0,
|
|
mood: '😐',
|
|
conditions: 'None',
|
|
// Use v2 inventory format for defaults
|
|
inventory: {
|
|
version: 2,
|
|
onPerson: "None",
|
|
stored: {},
|
|
assets: "None"
|
|
}
|
|
}
|
|
});
|
|
setLastGeneratedData({
|
|
userStats: null,
|
|
infoBox: null,
|
|
characterThoughts: null,
|
|
html: null
|
|
});
|
|
return;
|
|
}
|
|
|
|
const savedData = chat_metadata.rpg_companion;
|
|
|
|
// Restore stats
|
|
if (savedData.userStats) {
|
|
extensionSettings.userStats = { ...savedData.userStats };
|
|
}
|
|
|
|
// Restore classic stats
|
|
if (savedData.classicStats) {
|
|
extensionSettings.classicStats = { ...savedData.classicStats };
|
|
}
|
|
|
|
// Restore last generated data
|
|
if (savedData.lastGeneratedData) {
|
|
setLastGeneratedData({ ...savedData.lastGeneratedData });
|
|
}
|
|
|
|
// Migrate inventory in chat data if feature flag enabled
|
|
if (FEATURE_FLAGS.useNewInventory && extensionSettings.userStats.inventory) {
|
|
const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
|
|
if (migrationResult.migrated) {
|
|
console.log(`[RPG Companion] Chat inventory migrated from ${migrationResult.source} to v2 format`);
|
|
extensionSettings.userStats.inventory = migrationResult.inventory;
|
|
saveChatData(); // Persist migrated inventory to chat metadata
|
|
}
|
|
}
|
|
|
|
// Validate inventory structure (Bug #3 fix)
|
|
validateInventoryStructure(extensionSettings.userStats.inventory, 'chat');
|
|
|
|
// console.log('[RPG Companion] Loaded chat data:', savedData);
|
|
}
|
|
|
|
/**
|
|
* Validates and repairs inventory structure to prevent corruption.
|
|
* Ensures all v2 fields exist and are the correct type.
|
|
* Fixes Bug #3: Location disappears when switching tabs
|
|
*
|
|
* @param {Object} inventory - Inventory object to validate
|
|
* @param {string} source - Source of load ('settings' or 'chat') for logging
|
|
* @private
|
|
*/
|
|
function validateInventoryStructure(inventory, source) {
|
|
if (!inventory || typeof inventory !== 'object') {
|
|
console.error(`[RPG Companion] Invalid inventory from ${source}, resetting to defaults`);
|
|
extensionSettings.userStats.inventory = {
|
|
version: 2,
|
|
onPerson: "None",
|
|
stored: {},
|
|
assets: "None"
|
|
};
|
|
saveSettings();
|
|
return;
|
|
}
|
|
|
|
let needsSave = false;
|
|
|
|
// Ensure v2 structure
|
|
if (inventory.version !== 2) {
|
|
console.warn(`[RPG Companion] Inventory from ${source} missing version, setting to 2`);
|
|
inventory.version = 2;
|
|
needsSave = true;
|
|
}
|
|
|
|
// Validate onPerson field
|
|
if (typeof inventory.onPerson !== 'string') {
|
|
console.warn(`[RPG Companion] Invalid onPerson from ${source}, resetting to "None"`);
|
|
inventory.onPerson = "None";
|
|
needsSave = true;
|
|
} else {
|
|
// Clean items in onPerson (removes corrupted/dangerous items)
|
|
const cleanedOnPerson = cleanItemString(inventory.onPerson);
|
|
if (cleanedOnPerson !== inventory.onPerson) {
|
|
console.warn(`[RPG Companion] Cleaned corrupted items from onPerson inventory (${source})`);
|
|
inventory.onPerson = cleanedOnPerson;
|
|
needsSave = true;
|
|
}
|
|
}
|
|
|
|
// Validate stored field (CRITICAL for Bug #3)
|
|
if (!inventory.stored || typeof inventory.stored !== 'object' || Array.isArray(inventory.stored)) {
|
|
console.error(`[RPG Companion] Corrupted stored inventory from ${source}, resetting to empty object`);
|
|
inventory.stored = {};
|
|
needsSave = true;
|
|
} else {
|
|
// Validate stored object keys/values
|
|
const cleanedStored = validateStoredInventory(inventory.stored);
|
|
if (JSON.stringify(cleanedStored) !== JSON.stringify(inventory.stored)) {
|
|
console.warn(`[RPG Companion] Cleaned dangerous/invalid stored locations from ${source}`);
|
|
inventory.stored = cleanedStored;
|
|
needsSave = true;
|
|
}
|
|
}
|
|
|
|
// Validate assets field
|
|
if (typeof inventory.assets !== 'string') {
|
|
console.warn(`[RPG Companion] Invalid assets from ${source}, resetting to "None"`);
|
|
inventory.assets = "None";
|
|
needsSave = true;
|
|
} else {
|
|
// Clean items in assets (removes corrupted/dangerous items)
|
|
const cleanedAssets = cleanItemString(inventory.assets);
|
|
if (cleanedAssets !== inventory.assets) {
|
|
console.warn(`[RPG Companion] Cleaned corrupted items from assets inventory (${source})`);
|
|
inventory.assets = cleanedAssets;
|
|
needsSave = true;
|
|
}
|
|
}
|
|
|
|
// Persist repairs if needed
|
|
if (needsSave) {
|
|
console.log(`[RPG Companion] Repaired inventory structure from ${source}, saving...`);
|
|
saveSettings();
|
|
if (source === 'chat') {
|
|
saveChatData();
|
|
}
|
|
}
|
|
}
|