Files
rpg-companion-sillytavern/src/systems/generation/lockManager.js
T
dd178 96d589adc0 ```
feat(encounter): 添加战斗遭遇界面国际化支持和优化错误处理

- 添加新的中文翻译项包括战斗结果状态、错误消息、界面标签等
- 将硬编码的文本替换为国际化翻译调用
- 添加战斗遭遇初始化和处理过程中的错误处理消息
- 增加确认对话框的本地化文本

fix(regex): 更新正则表达式以支持Unicode字符

- 将多个文件中的ASCII限定正则表达式 /[^a-z0-9]+/g 替换为Unicode感知的
  /[^\p{L}\p{N}]+/gu 以正确处理非ASCII字符
- 修复jsonMigration.js中的字符过滤逻辑

feat(weather): 为中文添加天气模式识别规则

- 在WEATHER_PATTERNS_BY_LANGUAGE中为zh-cn语言添加完整的天气关键词模式
- 支持中文天气条件的自动识别和效果应用

style(fab): 添加nowrap样式防止文本换行

- 在FAB组件中添加white-space: nowrap样式属性
```
2026-03-23 03:27:12 +08:00

467 lines
18 KiB
JavaScript

/**
* Lock Manager
* Handles applying and removing locks for tracker items
* Locks prevent AI from modifying specific values
*/
import { extensionSettings } from '../../core/state.js';
import { repairJSON } from '../../utils/jsonRepair.js';
/**
* Apply locks to tracker data before sending to AI.
* Adds "locked": true to locked items in JSON format.
*
* @param {string} trackerData - JSON string of tracker data
* @param {string} trackerType - Type of tracker ('userStats', 'infoBox', 'characters')
* @returns {string} Tracker data with locks applied
*/
export function applyLocks(trackerData, trackerType) {
if (!trackerData) return trackerData;
// Try to parse as JSON
const parsed = repairJSON(trackerData);
if (!parsed) {
// Not JSON format, return as-is (text format doesn't support locks)
return trackerData;
}
// Get locked items for this tracker type
const lockedItems = extensionSettings.lockedItems?.[trackerType] || {};
// Apply locks based on tracker type
switch (trackerType) {
case 'userStats':
return applyUserStatsLocks(parsed, lockedItems);
case 'infoBox':
return applyInfoBoxLocks(parsed, lockedItems);
case 'characters':
return applyCharactersLocks(parsed, lockedItems);
default:
return trackerData;
}
}
/**
* Apply locks to User Stats tracker
* @param {Object} data - Parsed user stats data
* @param {Object} lockedItems - Locked items configuration
* @returns {string} JSON string with locks applied
*/
function applyUserStatsLocks(data, lockedItems) {
// Lock individual stats within stats object
if (data.stats && lockedItems.stats) {
// Handle both section lock and individual stat locks
const isStatsLocked = lockedItems.stats === true;
if (isStatsLocked) {
// Lock entire stats section
for (const statName in data.stats) {
data.stats[statName] = {
value: data.stats[statName].value || data.stats[statName],
locked: true
};
}
} else {
// Lock individual stats
for (const statName in lockedItems.stats) {
if (lockedItems.stats[statName] && data.stats[statName] !== undefined) {
data.stats[statName] = {
value: data.stats[statName].value || data.stats[statName],
locked: true
};
}
}
}
}
// Lock status field
if (data.status && lockedItems.status) {
data.status = {
...data.status,
locked: true
};
}
// Lock individual skills
if (data.skills && lockedItems.skills) {
if (Array.isArray(data.skills)) {
data.skills = data.skills.map(skill => {
if (typeof skill === 'string') {
if (lockedItems.skills[skill]) {
return { name: skill, locked: true };
}
return skill;
} else if (skill.name && lockedItems.skills[skill.name]) {
return { ...skill, locked: true };
}
return skill;
});
}
}
// Lock inventory items - match by item name instead of index
if (data.inventory && lockedItems.inventory) {
// Helper function to apply locks based on item name
const applyInventoryLocks = (items, category) => {
if (!Array.isArray(items)) return items;
if (!lockedItems.inventory[category]) return items;
return items.map((item) => {
// Get item name (handle both string and object formats)
const itemName = typeof item === 'string' ? item : (item.item || item.name || '');
// Check if this specific item name is locked
if (lockedItems.inventory[category][itemName]) {
return typeof item === 'string'
? { item, locked: true }
: { ...item, locked: true };
}
return item;
});
};
// Apply locks to onPerson items
if (data.inventory.onPerson) {
data.inventory.onPerson = applyInventoryLocks(data.inventory.onPerson, 'onPerson');
}
// Apply locks to clothing items
if (data.inventory.clothing) {
data.inventory.clothing = applyInventoryLocks(data.inventory.clothing, 'clothing');
}
// Apply locks to assets
if (data.inventory.assets) {
data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets');
}
// Apply locks to stored items - match by item name
if (data.inventory.stored && lockedItems.inventory.stored) {
for (const location in data.inventory.stored) {
if (Array.isArray(data.inventory.stored[location]) && lockedItems.inventory.stored[location]) {
data.inventory.stored[location] = data.inventory.stored[location].map((item) => {
const itemName = typeof item === 'string' ? item : (item.item || item.name || '');
if (lockedItems.inventory.stored[location][itemName]) {
return typeof item === 'string'
? { item, locked: true }
: { ...item, locked: true };
}
return item;
});
}
}
}
}
// Lock individual quests - handle paths like "quests.main" and "quests.optional[0]"
if (data.quests && lockedItems.quests) {
// Check if main quest is locked (entire section)
if (data.quests.main && lockedItems.quests.main === true) {
data.quests.main = { value: data.quests.main, locked: true };
}
// Check individual optional quests
if (data.quests.optional && Array.isArray(data.quests.optional)) {
data.quests.optional = data.quests.optional.map((quest, index) => {
const bracketPath = `optional[${index}]`;
if (lockedItems.quests[bracketPath]) {
return typeof quest === 'string'
? { title: quest, locked: true }
: { ...quest, locked: true };
}
return quest;
});
}
}
return JSON.stringify(data, null, 2);
}
/**
* Apply locks to Info Box tracker
* @param {Object} data - Parsed info box data
* @param {Object} lockedItems - Locked items configuration
* @returns {string} JSON string with locks applied
*/
function applyInfoBoxLocks(data, lockedItems) {
if (lockedItems.date && data.date) {
data.date = { ...data.date, locked: true };
}
if (lockedItems.weather && data.weather) {
data.weather = { ...data.weather, locked: true };
}
if (lockedItems.temperature && data.temperature) {
data.temperature = { ...data.temperature, locked: true };
}
if (lockedItems.time && data.time) {
data.time = { ...data.time, locked: true };
}
if (lockedItems.location && data.location) {
data.location = { ...data.location, locked: true };
}
if (lockedItems.recentEvents && data.recentEvents) {
data.recentEvents = { ...data.recentEvents, locked: true };
}
return JSON.stringify(data, null, 2);
}
/**
* Apply locks to Characters tracker
* @param {Object} data - Parsed characters data
* @param {Object} lockedItems - Locked items configuration
* @returns {string} JSON string with locks applied
*/
function applyCharactersLocks(data, lockedItems) {
// console.log('[Lock Manager] applyCharactersLocks called');
// console.log('[Lock Manager] Locked items:', JSON.stringify(lockedItems, null, 2));
// console.log('[Lock Manager] Input data:', JSON.stringify(data, null, 2));
// Handle both array format and object format
let characters = Array.isArray(data) ? data : (data.characters || []);
characters = characters.map((char, index) => {
const charName = char.name || char.characterName;
// Check if entire character is locked (index-based)
if (lockedItems[index] === true) {
// console.log('[Lock Manager] Locking entire character by index:', index);
return { ...char, locked: true };
}
// Check if character name exists in locked items (could be nested object for field locks or boolean for full lock)
const charLocks = lockedItems[charName];
if (charLocks === true) {
// Entire character is locked
// console.log('[Lock Manager] Locking entire character:', charName);
return { ...char, locked: true };
} else if (charLocks && typeof charLocks === 'object') {
// Character has field-level locks
const modifiedChar = { ...char };
for (const fieldName in charLocks) {
if (charLocks[fieldName] === true) {
// Check both the original field name and snake_case version
// (AI returns snake_case, but locks are stored with original configured names)
// Use the same conversion as toSnakeCase in thoughts.js
const snakeCaseFieldName = fieldName
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '_')
.replace(/^_+|_+$/g, '');
let locked = false;
// Check at root level first (backward compatibility)
if (modifiedChar[fieldName] !== undefined) {
// console.log('[Lock Manager] Applying lock to field:', `${charName}.${fieldName}`);
modifiedChar[fieldName] = {
value: modifiedChar[fieldName],
locked: true
};
locked = true;
} else if (modifiedChar[snakeCaseFieldName] !== undefined) {
// console.log('[Lock Manager] Applying lock to snake_case field:', `${charName}.${snakeCaseFieldName} (from ${fieldName})`);
modifiedChar[snakeCaseFieldName] = {
value: modifiedChar[snakeCaseFieldName],
locked: true
};
locked = true;
}
// Check in nested objects (details, relationship, thoughts)
if (!locked && modifiedChar.details) {
if (modifiedChar.details[fieldName] !== undefined) {
// console.log('[Lock Manager] Applying lock to details field:', `${charName}.details.${fieldName}`);
if (!modifiedChar.details || typeof modifiedChar.details !== 'object') {
modifiedChar.details = {};
} else {
modifiedChar.details = { ...modifiedChar.details };
}
modifiedChar.details[fieldName] = {
value: modifiedChar.details[fieldName],
locked: true
};
locked = true;
} else if (modifiedChar.details[snakeCaseFieldName] !== undefined) {
// console.log('[Lock Manager] Applying lock to details snake_case field:', `${charName}.details.${snakeCaseFieldName} (from ${fieldName})`);
if (!modifiedChar.details || typeof modifiedChar.details !== 'object') {
modifiedChar.details = {};
} else {
modifiedChar.details = { ...modifiedChar.details };
}
modifiedChar.details[snakeCaseFieldName] = {
value: modifiedChar.details[snakeCaseFieldName],
locked: true
};
locked = true;
}
}
// Check in relationship object
if (!locked && modifiedChar.relationship) {
if (modifiedChar.relationship[fieldName] !== undefined) {
// console.log('[Lock Manager] Applying lock to relationship field:', `${charName}.relationship.${fieldName}`);
modifiedChar.relationship = { ...modifiedChar.relationship };
modifiedChar.relationship[fieldName] = {
value: modifiedChar.relationship[fieldName],
locked: true
};
locked = true;
} else if (modifiedChar.relationship[snakeCaseFieldName] !== undefined) {
// console.log('[Lock Manager] Applying lock to relationship snake_case field:', `${charName}.relationship.${snakeCaseFieldName} (from ${fieldName})`);
modifiedChar.relationship = { ...modifiedChar.relationship };
modifiedChar.relationship[snakeCaseFieldName] = {
value: modifiedChar.relationship[snakeCaseFieldName],
locked: true
};
locked = true;
}
}
// Check in thoughts object
if (!locked && modifiedChar.thoughts) {
if (modifiedChar.thoughts[fieldName] !== undefined) {
// console.log('[Lock Manager] Applying lock to thoughts field:', `${charName}.thoughts.${fieldName}`);
modifiedChar.thoughts = { ...modifiedChar.thoughts };
modifiedChar.thoughts[fieldName] = {
value: modifiedChar.thoughts[fieldName],
locked: true
};
locked = true;
} else if (modifiedChar.thoughts[snakeCaseFieldName] !== undefined) {
// console.log('[Lock Manager] Applying lock to thoughts snake_case field:', `${charName}.thoughts.${snakeCaseFieldName} (from ${fieldName})`);
modifiedChar.thoughts = { ...modifiedChar.thoughts };
modifiedChar.thoughts[snakeCaseFieldName] = {
value: modifiedChar.thoughts[snakeCaseFieldName],
locked: true
};
locked = true;
}
}
}
}
return modifiedChar;
}
// No locks for this character
return char;
});
const result = Array.isArray(data)
? JSON.stringify(characters, null, 2)
: JSON.stringify({ ...data, characters }, null, 2);
// console.log('[Lock Manager] Output data:', result);
return result;
}
/**
* Remove locks from tracker data received from AI.
* Strips "locked": true from all items to clean up the data.
*
* @param {string} trackerData - JSON string of tracker data
* @returns {string} Tracker data with locks removed
*/
export function removeLocks(trackerData) {
if (!trackerData) return trackerData;
// Try to parse as JSON
const parsed = repairJSON(trackerData);
if (!parsed) {
// Not JSON format, return as-is
return trackerData;
}
// Recursively remove all "locked" properties
const cleaned = removeLockedProperties(parsed);
return JSON.stringify(cleaned, null, 2);
}
/**
* Recursively remove "locked" properties from an object
* @param {*} obj - Object to clean
* @returns {*} Object with locked properties removed
*/
function removeLockedProperties(obj) {
if (Array.isArray(obj)) {
return obj.map(item => removeLockedProperties(item));
} else if (obj !== null && typeof obj === 'object') {
const cleaned = {};
for (const key in obj) {
if (key !== 'locked') {
cleaned[key] = removeLockedProperties(obj[key]);
}
}
return cleaned;
}
return obj;
}
/**
* Check if a specific item is locked
* @param {string} trackerType - Type of tracker
* @param {string} itemPath - Path to the item (e.g., 'stats.Health', 'quests.main.0')
* @returns {boolean} Whether the item is locked
*/
export function isItemLocked(trackerType, itemPath) {
const lockedItems = extensionSettings.lockedItems?.[trackerType];
if (!lockedItems) return false;
const parts = itemPath.split('.');
let current = lockedItems;
for (const part of parts) {
if (current[part] === undefined) return false;
current = current[part];
}
return !!current;
}
/**
* Toggle lock state for a specific item
* @param {string} trackerType - Type of tracker
* @param {string} itemPath - Path to the item
* @param {boolean} locked - New lock state
*/
export function setItemLock(trackerType, itemPath, locked) {
// console.log('[Lock Manager] setItemLock called:', { trackerType, itemPath, locked });
if (!extensionSettings.lockedItems) {
extensionSettings.lockedItems = {};
}
if (!extensionSettings.lockedItems[trackerType]) {
extensionSettings.lockedItems[trackerType] = {};
}
const parts = itemPath.split('.');
let current = extensionSettings.lockedItems[trackerType];
// Navigate to parent of target
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
// Set or remove lock
const finalKey = parts[parts.length - 1];
if (locked) {
current[finalKey] = true;
} else {
delete current[finalKey];
}
// console.log('[Lock Manager] Locked items after set:', JSON.stringify(extensionSettings.lockedItems, null, 2));
}