Files
ST-Amily2-Chat-Optimisation/CharacterWorldBook/src/cwb_core.js

865 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getContext } from '/scripts/extensions.js';
import { state, SCRIPT_ID_PREFIX } from './cwb_state.js';
import { logDebug, logError, showToastr, escapeHtml, cleanChatName, parseCustomFormat, buildCustomFormat, isCwbEnabled } from './cwb_utils.js';
import { callCustomOpenAI } from './cwb_apiService.js';
import { saveDescriptionToLorebook, updateCharacterRosterLorebookEntry, manageAutoCardUpdateLorebookEntry, getTargetWorldBook } from './cwb_lorebookManager.js';
import { extractBlocksByTags, applyExclusionRules } from '../../core/utils/rag-tag-extractor.js';
import { getExtensionSettings } from '../../utils/settings.js';
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
import { generateRandomSeed } from '../../core/api.js';
import { getChatIdentifier } from '../../core/lore.js';
import { safeLorebookEntries } from '../../core/tavernhelper-compatibility.js';
import { amilyHelper } from '../../core/tavern-helper/main.js';
const { SillyTavern, jQuery, characters } = window;
let isUpdatingCard = false;
let isBatchUpdating = false;
let manualBatchStopRequested = false;
let currentBatchNum = 0;
let totalBatchesNum = 0;
const MAX_BATCH_RETRIES = 2;
export async function updateCardUpdateStatusDisplay($panel) {
if (!$panel || !$panel.length) return;
const $statusDisplay = $panel.find(`#${SCRIPT_ID_PREFIX}-card-update-status-display`);
const $totalMessagesDisplay = $panel.find(`#${SCRIPT_ID_PREFIX}-total-messages-display`);
$totalMessagesDisplay.text(`上下文总层数: ${state.allChatMessages.length}`);
if (!state.currentChatFileIdentifier || state.currentChatFileIdentifier.startsWith('unknown_chat')) {
$statusDisplay.text('当前聊天未知,无法获取更新状态。');
return;
}
try {
const context = SillyTavern.getContext();
if (!context || !context.characterId) {
$statusDisplay.text('没有选择角色。');
return;
}
const bookName = await getTargetWorldBook();
if (!bookName) {
$statusDisplay.text('当前角色未设置主世界书或自定义世界书。');
return;
}
const entries = await safeLorebookEntries(bookName);
const entryPrefixForCurrentChat = `角色卡更新-${state.currentChatFileIdentifier}-`;
let latestEntryToShow = null;
let maxEndFloorOverall = -1;
for (const entry of entries) {
if (entry.comment && entry.comment.startsWith(entryPrefixForCurrentChat)) {
const match = entry.comment.match(/-(\d+)-(\d+)$/);
if (match && match[2]) {
const endFloor = parseInt(match[2], 10);
if (endFloor > maxEndFloorOverall) {
maxEndFloorOverall = endFloor;
latestEntryToShow = entry;
}
}
}
}
if (latestEntryToShow) {
const commentParts = latestEntryToShow.comment.split('-');
const charNameInComment = commentParts.slice(2, -2).join('-');
const startFloorStr = commentParts[commentParts.length - 2];
const endFloorStr = commentParts[commentParts.length - 1];
$statusDisplay.html(
`最新更新: 角色 <b>${escapeHtml(charNameInComment)}</b> (基于楼层 <b>${startFloorStr}-${endFloorStr}</b>)`
);
} else {
$statusDisplay.text('当前聊天信息尚未在世界书中更新。');
}
} catch (e) {
logError('加载/解析世界书条目以更新UI状态时失败:', e);
$statusDisplay.text('获取世界书更新状态时出错。');
}
}
async function loadAllChatMessages($panel) {
logDebug('尝试使用 getContext() 加载所有聊天消息...');
if (!SillyTavern) {
logError('SillyTavern API 不可用。');
state.allChatMessages = [];
return;
}
try {
const context = SillyTavern.getContext();
const chat = context?.chat || [];
if (chat.length === 0) {
logDebug('聊天为空,无需加载消息。');
state.allChatMessages = [];
} else {
state.allChatMessages = chat.map((msg, idx) => ({
...msg,
message: msg.mes,
id: idx
}));
}
logDebug(`成功为 ${state.currentChatFileIdentifier} 加载了 ${state.allChatMessages.length} 条消息。`);
await updateCardUpdateStatusDisplay($panel);
} catch (error) {
logError('使用 getContext() 获取聊天消息时发生严重错误:', error);
showToastr('error', '获取聊天记录时发生内部错误。');
state.allChatMessages = [];
}
}
function processChatMessages(messages) {
if (!messages || !Array.isArray(messages) || messages.length === 0) {
logDebug('[CWB] processChatMessages: 没有可处理的消息。');
return '';
}
logDebug(`[CWB] processChatMessages: 开始处理 ${messages.length} 条消息。`);
try {
const mainSettings = getExtensionSettings();
if (!mainSettings) {
logError('[CWB] 无法访问主扩展设置。将使用原始消息。');
return messages.map(msg => `${msg.is_user ? SillyTavern?.name1 || '用户' : msg.name || '角色'}: ${msg.message}`).join('\n\n');
}
const useTagExtraction = mainSettings.historiographyTagExtractionEnabled ?? false;
const tagsToExtract = useTagExtraction ? (mainSettings.historiographyTags || '').split(',').map(t => t.trim()).filter(Boolean) : [];
const exclusionRules = mainSettings.historiographyExclusionRules || [];
logDebug(`[CWB] 标签提取: ${useTagExtraction}, 标签: ${tagsToExtract.join(', ')}, 排除规则: ${exclusionRules.length}`);
if (!useTagExtraction && exclusionRules.length === 0) {
logDebug('[CWB] 未激活任何处理规则。返回合并后的原始消息。');
return messages.map(msg => `${msg.is_user ? SillyTavern?.name1 || '用户' : msg.name || '角色'}: ${msg.message}`).join('\n\n');
}
const processedMessages = messages.map((msg) => {
let content = msg.message;
if (useTagExtraction && tagsToExtract.length > 0) {
const blocks = extractBlocksByTags(content, tagsToExtract);
if (blocks.length > 0) {
content = blocks.join('\n\n');
}
}
content = applyExclusionRules(content, exclusionRules);
if (!content.trim()) return null;
return `${msg.is_user ? SillyTavern?.name1 || '用户' : msg.name || '角色'}】:\n${content.trim()}`;
}).filter(Boolean);
logDebug(`[CWB] processChatMessages: 处理完成。${messages.length} -> ${processedMessages.length} 条有效消息。`);
return processedMessages.join('\n\n');
} catch (error) {
logError('[CWB] processChatMessages 中发生错误:', error);
return messages.map(msg => `${msg.is_user ? SillyTavern?.name1 || '用户' : msg.name || '角色'}: ${msg.message}`).join('\n\n');
}
}
async function proceedWithCardUpdate($panel, messagesToUse) {
const statusUpdater = text => {
if ($panel && $panel.length) {
$panel.find(`#${SCRIPT_ID_PREFIX}-status-message`).text(text);
}
};
statusUpdater('正在生成角色卡描述...');
try {
const mode = state.isIncrementalUpdateEnabled ? 'cwb_summarizer_incremental' : 'cwb_summarizer';
const presetPrompts = await getPresetPrompts(mode);
const order = getMixedOrder(mode) || [];
const messages = [
{ role: 'system', content: generateRandomSeed() }
];
let promptCounter = 0;
let existingData = {};
if (state.isIncrementalUpdateEnabled) {
statusUpdater('增量更新模式:正在获取现有角色数据...');
try {
const bookName = await getTargetWorldBook();
if (bookName) {
const entries = (await safeLorebookEntries(bookName)) || [];
let chatIdentifier = state.currentChatFileIdentifier.replace(/ imported/g, '');
const messagesText = messagesToUse.map(m => {
const name = m.name || '';
const content = m.message || '';
return `${name}\n${content}`;
}).join('\n').toLowerCase();
const characterEntries = entries.filter(e =>
e.enabled &&
Array.isArray(e.keys) &&
e.keys.includes(chatIdentifier) &&
!e.keys.includes('Amily2角色总集')
);
for (const entry of characterEntries) {
try {
const keysToCheck = entry.keys.filter(k => k !== chatIdentifier);
if (entry.secondary_keys && Array.isArray(entry.secondary_keys)) {
keysToCheck.push(...entry.secondary_keys);
}
let isTriggered = false;
if (keysToCheck.length > 0) {
isTriggered = keysToCheck.some(key => messagesText.includes(key.toLowerCase()));
}
if (isTriggered) {
const parsedData = parseCustomFormat(entry.content);
const entryCharName = parsedData?.name?.trim() || parsedData?.CI?.name?.trim() || parsedData?.core_identity?.name?.trim();
if (entryCharName) {
existingData[entryCharName] = entry.content;
}
}
} catch (parseError) {
logError(`解析现有角色条目时出错 (UID: ${entry.uid}):`, parseError);
}
}
logDebug(`为 '${chatIdentifier}' 找到了 ${Object.keys(existingData).length} 个被触发的现有角色条目。`);
}
} catch (e) {
logError('在增量更新中获取现有角色数据时出错:', e);
showToastr('error', '获取旧档案失败,请检查控制台。');
}
}
for (const item of order) {
if (item.type === 'prompt') {
if (presetPrompts && presetPrompts[promptCounter]) {
messages.push(presetPrompts[promptCounter]);
promptCounter++;
}
} else if (item.type === 'conditional') {
switch (item.id) {
case 'cwb_break_armor_prompt':
if (state.currentBreakArmorPrompt) {
messages.push({ role: "system", content: state.currentBreakArmorPrompt });
}
break;
case 'cwb_char_card_prompt':
if (state.currentCharCardPrompt) {
messages.push({ role: "system", content: state.currentCharCardPrompt });
}
break;
case 'oldFiles':
if (state.isIncrementalUpdateEnabled) {
let oldFilesContent = "【旧档案】\n";
if (Object.keys(existingData).length > 0) {
for (const charName in existingData) {
oldFilesContent += `${existingData[charName]}\n`;
}
} else {
oldFilesContent += "无\n";
}
messages.push({ role: 'user', content: oldFilesContent });
}
break;
case 'newContext':
const processedText = processChatMessages(messagesToUse);
let newContextContent = "";
if (state.isIncrementalUpdateEnabled) {
newContextContent = "【新对话】\n";
} else {
newContextContent = "最近的聊天记录摘要:\n";
}
if (processedText) {
newContextContent += processedText;
} else {
newContextContent += "(无有效对话内容)";
}
if (!state.isIncrementalUpdateEnabled) {
newContextContent += "\n\n请根据以上聊天记录更新角色描述";
}
messages.push({ role: 'user', content: newContextContent });
break;
}
}
}
statusUpdater('正在调用AI生成角色卡...');
const aiResponse = await callCustomOpenAI(messages);
if (!aiResponse) throw new Error('AI未能生成有效描述。');
const endFloor_0idx = state.allChatMessages.length - 1;
const startFloor_0idx = Math.max(0, state.allChatMessages.length - messagesToUse.length);
const characterBlocks = aiResponse.split(/(?=\[--Amily2::CHAR_START--\])/).filter(block => block.trim());
if (characterBlocks.length === 0) throw new Error('AI未能生成任何角色描述块。');
let allSucceeded = true;
let processedNames = [];
for (const block of characterBlocks) {
const trimmedBlock = block.trim();
if (!trimmedBlock) continue;
const parsedData = parseCustomFormat(trimmedBlock);
const charName = (parsedData?.name?.trim() || parsedData?.CI?.name?.trim() || parsedData?.core_identity?.name?.trim()) || 'UnknownCharacter';
if (charName === 'UnknownCharacter') {
logError('无法在块中找到角色名:', trimmedBlock);
continue;
}
const success = await saveDescriptionToLorebook(charName, trimmedBlock, startFloor_0idx, endFloor_0idx);
if (success) {
processedNames.push(charName);
} else {
allSucceeded = false;
}
}
if (processedNames.length > 0) {
await updateCharacterRosterLorebookEntry([...new Set(processedNames)], startFloor_0idx, endFloor_0idx);
statusUpdater(`已为 ${processedNames.length} 个角色更新描述!`);
} else {
throw new Error('AI生成了内容但未能成功提取任何有效的角色卡。');
}
updateCardUpdateStatusDisplay($panel);
return allSucceeded;
} catch (error) {
logError('角色卡更新过程出错:', error);
showToastr('error', `更新失败: ${error.message}`);
statusUpdater('错误:更新失败。');
return false;
}
}
async function triggerAutomaticUpdate($panel) {
logDebug(`检查是否需要更新。总消息数: ${state.allChatMessages.length}, 自动更新启用: ${state.autoUpdateEnabled}`);
if (!isCwbEnabled()) {
logDebug('更新检查已跳过 - CharacterWorldBook总开关已关闭。');
return;
}
if (!state.autoUpdateEnabled || isUpdatingCard || !state.customApiConfig.url || !state.customApiConfig.model || state.allChatMessages.length === 0) {
logDebug('更新检查已跳过(未启用、正在更新、未配置或无消息)。');
return;
}
let maxEndFloorInLorebook = 0;
try {
const context = SillyTavern.getContext();
if (!context || !context.characterId) {
logDebug('角色上下文未准备好,跳过自动更新的世界书检查。');
return;
}
const bookName = await getTargetWorldBook();
if (bookName) {
const entries = (await safeLorebookEntries(bookName)) || [];
const cleanChatId = state.currentChatFileIdentifier.replace(/ imported/g, '');
const rosterEntry = entries.find(e =>
Array.isArray(e.keys) &&
e.keys.includes('Amily2角色总集') &&
e.keys.includes(cleanChatId)
);
if (rosterEntry && rosterEntry.content) {
const floorMatch = rosterEntry.content.match(/【前(\d+)楼角色世界书已更新完成】/);
if (floorMatch && floorMatch[1]) {
maxEndFloorInLorebook = parseInt(floorMatch[1], 10);
} else {
// Fallback for older entries
const floorRangeKey = rosterEntry.keys.find(k => /^\d+-\d+$/.test(k));
if (floorRangeKey) {
maxEndFloorInLorebook = parseInt(floorRangeKey.split('-')[1], 10);
}
}
}
}
} catch (e) {
logError('从世界书获取最大结束楼层时出错:', e);
}
const unupdatedCount = state.allChatMessages.length - maxEndFloorInLorebook;
logDebug(`未更新消息数: ${unupdatedCount} (阈值: ${state.autoUpdateThreshold}). 上次更新楼层: ${maxEndFloorInLorebook}.`);
if (unupdatedCount >= state.autoUpdateThreshold) {
showToastr('info', `检测到 ${unupdatedCount} 条新消息,将自动更新角色卡。`);
const messagesToUse = state.allChatMessages.slice(maxEndFloorInLorebook);
isUpdatingCard = true;
await proceedWithCardUpdate($panel, messagesToUse);
isUpdatingCard = false;
}
}
export async function getLatestChatName() {
let attempts = 0;
const maxAttempts = 50;
const interval = 100;
while (attempts < maxAttempts) {
const context = getContext();
if (context && context.chatId) {
return context.chatId;
}
await new Promise((resolve) => setTimeout(resolve, interval));
attempts++;
}
logError("[CWB] 长时间等待后仍无法确定聊天ID。");
return "unknown_chat_timeout";
}
export async function handleMessageReceived($panel) {
if (!isCwbEnabled('消息接收处理')) {
return;
}
const context = SillyTavern.getContext();
if (!context || !context.chat || !context.chat.length === 0) return;
const latestMessage = context.chat[context.chat.length - 1];
if (!latestMessage || latestMessage.is_user) {
return;
}
await loadAllChatMessages($panel);
await triggerAutomaticUpdate($panel);
}
export async function resetScriptStateForNewChat($panel, newChatName) {
logDebug(`为新聊天重置脚本状态: "${newChatName}"`);
state.allChatMessages = [];
state.currentChatFileIdentifier = newChatName || 'unknown_chat_fallback';
await loadAllChatMessages($panel);
logDebug('状态重置完成。');
}
function updateBatchButtonState($panel, state, batchNum = 0, attemptNum = 0) {
if (!$panel || !$panel.length) return;
const $button = $panel.find('#cwb-batch-update-card');
const $progress = $panel.find('#cwb-batch-progress');
if (!$button.length) return;
switch (state) {
case 'processing':
let attemptText = attemptNum > 0 ? ` (尝试 ${attemptNum + 1})` : '';
$button.text(`点击停止 (${batchNum}/${totalBatchesNum})${attemptText}`);
$button.prop('disabled', false);
$progress.show().text(`正在处理批次 ${batchNum}/${totalBatchesNum}...`);
isBatchUpdating = true;
break;
case 'stopping':
$button.text('正在停止...');
$button.prop('disabled', true);
$progress.text('正在停止批量更新...');
break;
case 'paused':
$button.text('继续批量更新');
$button.prop('disabled', false);
$progress.text('批量更新已暂停,点击继续...');
isBatchUpdating = true;
break;
case 'error':
$button.text('继续批量更新 (出错)');
$button.prop('disabled', false);
$progress.text('批量更新出错,请检查后继续...');
isBatchUpdating = true;
break;
case 'idle':
default:
$button.text('立即批量更新');
$button.prop('disabled', false);
$progress.hide();
isBatchUpdating = false;
currentBatchNum = 0;
manualBatchStopRequested = false;
break;
}
}
function getMessagesForFloorRange(startFloor, endFloor) {
if (!state.allChatMessages || state.allChatMessages.length === 0) {
return [];
}
// 转换为0-based索引
const startIndex = Math.max(0, startFloor - 1);
const endIndex = Math.min(state.allChatMessages.length, endFloor);
if (startIndex >= endIndex) {
return [];
}
return state.allChatMessages.slice(startIndex, endIndex);
}
async function runBatchUpdateAttempt($panel, batchNum, attemptNum) {
try {
if (manualBatchStopRequested) {
logDebug(`批次 ${batchNum} 在开始前被手动停止。`);
updateBatchButtonState($panel, 'paused');
return;
}
updateBatchButtonState($panel, 'processing', batchNum, attemptNum);
const startFloor = (batchNum - 1) * state.autoUpdateThreshold + 1;
const endFloor = Math.min(startFloor + state.autoUpdateThreshold - 1, state.allChatMessages.length);
logDebug(`正在处理批次 ${batchNum}/${totalBatchesNum} (楼层 ${startFloor}-${endFloor}, 尝试 ${attemptNum + 1}/${MAX_BATCH_RETRIES + 1})`);
const messagesToProcess = getMessagesForFloorRange(startFloor, endFloor);
if (!messagesToProcess || messagesToProcess.length === 0) {
throw new Error('指定范围内无有效消息可处理。');
}
const success = await proceedWithCardUpdate($panel, messagesToProcess);
if (!success) {
throw new Error('角色卡更新失败。');
}
logDebug(`批次 ${batchNum} 处理成功。`);
currentBatchNum = batchNum;
setTimeout(() => processNextBatch($panel), 1000);
} catch (error) {
logError(`批次 ${batchNum} 尝试 ${attemptNum + 1} 失败: ${error.message}`);
if (attemptNum >= MAX_BATCH_RETRIES) {
logError(`批次 ${batchNum} 已达到最大重试次数,任务暂停。`);
showToastr('error', `批次 ${batchNum} 多次失败请检查网络或API设置后手动继续。`);
currentBatchNum = batchNum - 1;
updateBatchButtonState($panel, 'error');
} else {
logDebug(`将在3秒后自动重试批次 ${batchNum}...`);
setTimeout(() => runBatchUpdateAttempt($panel, batchNum, attemptNum + 1), 3000);
}
}
}
async function processNextBatch($panel) {
if (manualBatchStopRequested) {
logDebug(`批次 ${currentBatchNum + 1} 在开始前被手动停止。`);
updateBatchButtonState($panel, 'paused');
return;
}
if (currentBatchNum >= totalBatchesNum) {
logDebug('所有批次处理完毕!');
showToastr('success', '批量更新完成!');
updateBatchButtonState($panel, 'idle');
return;
}
await runBatchUpdateAttempt($panel, currentBatchNum + 1, 0);
}
export async function startBatchUpdate($panel) {
if (!isCwbEnabled()) {
showToastr('warning', 'CharacterWorldBook总开关已关闭无法执行批量更新。');
return;
}
await loadAllChatMessages($panel);
if (!state.customApiConfig.url || !state.customApiConfig.model) {
showToastr('warning', '请先配置API信息。');
return;
}
if (isBatchUpdating) {
const $button = $panel.find('#cwb-batch-update-card');
if ($button.text().startsWith('点击停止')) {
manualBatchStopRequested = true;
updateBatchButtonState($panel, 'stopping');
logDebug('批量更新停止请求已发出!将在当前批次完成后暂停。');
} else if ($button.text().startsWith('继续批量更新')) {
manualBatchStopRequested = false;
logDebug('从上次暂停处继续批量更新...');
await processNextBatch($panel);
}
return;
}
manualBatchStopRequested = false;
if (state.allChatMessages.length === 0) {
showToastr('info', '当前没有聊天记录,无需更新。');
return;
}
totalBatchesNum = Math.ceil(state.allChatMessages.length / state.autoUpdateThreshold);
currentBatchNum = 0;
logDebug(`准备开始批量更新任务,共 ${totalBatchesNum} 个批次。`);
showToastr('info', `开始批量更新,共 ${totalBatchesNum} 个批次...`);
await processNextBatch($panel);
}
export async function handleFloorRangeUpdate($panel) {
if (!isCwbEnabled()) {
showToastr('warning', 'CharacterWorldBook总开关已关闭无法执行楼层范围更新。');
return;
}
await loadAllChatMessages($panel);
if (isUpdatingCard || isBatchUpdating) {
showToastr('info', '已有更新任务在进行中。');
return;
}
if (!state.customApiConfig.url || !state.customApiConfig.model) {
showToastr('warning', '请先配置API信息。');
return;
}
const startFloor = parseInt($panel.find('#cwb-start-floor').val(), 10);
const endFloor = parseInt($panel.find('#cwb-end-floor').val(), 10);
if (!startFloor || !endFloor || startFloor <= 0 || endFloor <= 0) {
showToastr('warning', '请输入有效的楼层范围。');
return;
}
if (startFloor > endFloor) {
showToastr('warning', '起始楼层不能大于结束楼层。');
return;
}
if (state.allChatMessages.length === 0) {
showToastr('info', '当前没有聊天记录,无需更新。');
return;
}
if (endFloor > state.allChatMessages.length) {
showToastr('warning', `结束楼层 ${endFloor} 超出了当前聊天记录长度 ${state.allChatMessages.length}`);
return;
}
const messagesToProcess = getMessagesForFloorRange(startFloor, endFloor);
if (!messagesToProcess || messagesToProcess.length === 0) {
showToastr('warning', '指定楼层范围内没有有效内容可处理。');
return;
}
isUpdatingCard = true;
const $button = $panel.find('#cwb-floor-range-update');
$button.prop('disabled', true).text('更新中...');
try {
logDebug(`开始处理楼层 ${startFloor}-${endFloor} 的内容...`);
const success = await proceedWithCardUpdate($panel, messagesToProcess);
if (success) {
showToastr('success', `楼层 ${startFloor}-${endFloor} 更新完成!`);
}
} finally {
isUpdatingCard = false;
$button.prop('disabled', false).text('楼层范围更新');
}
}
export async function manualUpdateLogic($panel = null) {
if (!isCwbEnabled()) {
logDebug('手动更新已跳过 - CharacterWorldBook总开关已关闭。');
return;
}
if (isUpdatingCard) {
showToastr('info', '已有更新任务在进行中。');
return;
}
if (!state.customApiConfig.url || !state.customApiConfig.model) {
showToastr('warning', '请先配置API信息。');
return;
}
isUpdatingCard = true;
await loadAllChatMessages($panel);
const depth = state.scanDepth || state.autoUpdateThreshold || 6;
const messagesToProcess = state.allChatMessages.slice(-depth);
await proceedWithCardUpdate($panel, messagesToProcess);
isUpdatingCard = false;
logDebug('手动更新完成。');
}
export async function handleManualUpdateCard($panel) {
const $button = $panel.find(`#${SCRIPT_ID_PREFIX}-manual-update-card`);
$button.prop('disabled', true).text('更新中...');
await manualUpdateLogic($panel);
$button.prop('disabled', false).text('立即更新角色描述');
}
export async function handleLegacyFormatConversion($panel) {
if (!isCwbEnabled()) {
showToastr('warning', 'CharacterWorldBook总开关已关闭。');
return;
}
const $button = $panel.find('#cwb-legacy-auto-update');
$button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 转换中...');
try {
const bookName = await getTargetWorldBook();
if (!bookName) {
showToastr('warning', '未找到目标世界书。');
return;
}
const entries = await safeLorebookEntries(bookName);
let updatedCount = 0;
const entriesToUpdate = [];
for (const entry of entries) {
if (!entry.content || !entry.content.includes('[--Amily2::CHAR_START--]')) continue;
try {
const parsed = parseCustomFormat(entry.content);
if (!parsed || Object.keys(parsed).length === 0) continue;
let hasChanges = false;
const newData = {};
// Helper to rename keys
const renameKey = (obj, oldKey, newKey) => {
if (obj[oldKey] !== undefined) {
obj[newKey] = obj[oldKey];
delete obj[oldKey];
return true;
}
return false;
};
// Helper to rename sub-keys
const renameSubKeys = (parentObj, parentKey, mapping) => {
if (parentObj[parentKey]) {
let subChanged = false;
for (const [oldSub, newSub] of Object.entries(mapping)) {
if (renameKey(parentObj[parentKey], oldSub, newSub)) {
subChanged = true;
}
}
return subChanged;
}
return false;
};
// Copy parsed data to newData to avoid mutating original if needed (though parseCustomFormat returns new obj)
Object.assign(newData, JSON.parse(JSON.stringify(parsed)));
// 1. Rename Top Level Modules
if (renameKey(newData, 'core_identity', 'CI')) hasChanges = true;
if (renameKey(newData, 'physical_imprint', 'PI')) hasChanges = true;
if (renameKey(newData, 'psyche_profile', 'PP')) hasChanges = true;
if (renameKey(newData, 'social_matrix', 'SM')) hasChanges = true;
if (renameKey(newData, 'narrative_essence', 'NE')) hasChanges = true;
// 2. Rename Sub-keys
// CI
if (renameSubKeys(newData, 'CI', {
'archetype': 'arch',
'gender': 'gen',
'current_status': 'status'
})) hasChanges = true;
// PI
if (renameSubKeys(newData, 'PI', {
'first_impression': 'first',
'key_features': 'feat',
'mannerisms': 'manner'
})) hasChanges = true;
// PP
if (renameSubKeys(newData, 'PP', {
'description': 'desc',
'motivation': 'mot',
'values': 'val',
'inner_conflict': 'conf'
})) hasChanges = true;
// SM
if (renameSubKeys(newData, 'SM', {
'interaction_style': 'style',
'skills': 'skill',
'reputation': 'rep'
})) hasChanges = true;
// NE
if (newData.NE) {
// core_traits -> trait
if (newData.NE.core_traits) {
newData.NE.trait = newData.NE.core_traits.map(t => {
const newT = { ...t };
renameKey(newT, 'definition', 'def');
renameKey(newT, 'evidence', 'evid');
return newT;
});
delete newData.NE.core_traits;
hasChanges = true;
}
// verbal_patterns -> verb
if (newData.NE.verbal_patterns) {
newData.NE.verb = { ...newData.NE.verbal_patterns };
delete newData.NE.verbal_patterns;
renameKey(newData.NE.verb, 'style_summary', 'style');
renameKey(newData.NE.verb, 'quotes', 'quote');
hasChanges = true;
}
// key_relationships -> rel
if (newData.NE.key_relationships) {
newData.NE.rel = newData.NE.key_relationships.map(r => {
const newR = { ...r };
renameKey(newR, 'summary', 'sum');
return newR;
});
delete newData.NE.key_relationships;
hasChanges = true;
}
}
if (hasChanges) {
const newContent = buildCustomFormat(newData);
entriesToUpdate.push({
uid: entry.uid,
content: newContent
});
updatedCount++;
}
} catch (e) {
logError(`转换条目失败 (UID: ${entry.uid}):`, e);
}
}
if (updatedCount > 0) {
await amilyHelper.setLorebookEntries(bookName, entriesToUpdate);
showToastr('success', `成功转换了 ${updatedCount} 个旧版格式条目!`);
} else {
showToastr('info', '没有发现需要转换的旧版格式条目。');
}
} catch (error) {
logError('旧版格式转换失败:', error);
showToastr('error', `转换失败: ${error.message}`);
} finally {
$button.prop('disabled', false).html('<i class="fa-solid fa-history"></i> 旧版格式转换');
}
}
export async function initializeCore($panel) {
const initialChatName = await getLatestChatName();
await resetScriptStateForNewChat($panel, initialChatName);
logDebug('CWB 核心已初始化。基于事件的检查已激活。');
}