mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-07 23:45:50 +00:00
Compare commits
4 Commits
e9c992c6d7
...
8b1cbf8a07
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b1cbf8a07 | |||
| 539d54fda5 | |||
| 2cda471b17 | |||
| 3963eba47d |
@@ -243,6 +243,13 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="settings-group">
|
<fieldset class="settings-group">
|
||||||
<legend>世界书管理</legend>
|
<legend>世界书管理</legend>
|
||||||
|
<div class="control-block-with-switch">
|
||||||
|
<label for="amily2_opt_new_memory_logic_enabled">启用新记忆逻辑</label>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input id="amily2_opt_new_memory_logic_enabled" type="checkbox" />
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="control-block-with-switch">
|
<div class="control-block-with-switch">
|
||||||
<label>世界书来源</label>
|
<label>世界书来源</label>
|
||||||
<div class="radio-group">
|
<div class="radio-group">
|
||||||
|
|||||||
100
core/lore.js
100
core/lore.js
@@ -286,6 +286,7 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings, isC
|
|||||||
} else if (isPanelReady) {
|
} else if (isPanelReady) {
|
||||||
// This is a main call and the panel is ready, read from UI.
|
// This is a main call and the panel is ready, read from UI.
|
||||||
liveSettings.worldbookEnabled = panel.find('#amily2_opt_worldbook_enabled').is(':checked');
|
liveSettings.worldbookEnabled = panel.find('#amily2_opt_worldbook_enabled').is(':checked');
|
||||||
|
liveSettings.newMemoryLogicEnabled = panel.find('#amily2_opt_new_memory_logic_enabled').is(':checked');
|
||||||
liveSettings.worldbookSource = panel.find('input[name="amily2_opt_worldbook_source"]:checked').val() || 'character';
|
liveSettings.worldbookSource = panel.find('input[name="amily2_opt_worldbook_source"]:checked').val() || 'character';
|
||||||
|
|
||||||
liveSettings.selectedWorldbooks = [];
|
liveSettings.selectedWorldbooks = [];
|
||||||
@@ -301,6 +302,7 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings, isC
|
|||||||
});
|
});
|
||||||
|
|
||||||
liveSettings.worldbookCharLimit = parseInt(panel.find('#amily2_opt_worldbook_char_limit').val(), 10) || 60000;
|
liveSettings.worldbookCharLimit = parseInt(panel.find('#amily2_opt_worldbook_char_limit').val(), 10) || 60000;
|
||||||
|
liveSettings.contextLimit = parseInt(panel.find('#amily2_opt_context_limit').val(), 10) || 5;
|
||||||
|
|
||||||
let enabledEntries = {};
|
let enabledEntries = {};
|
||||||
panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]:checked').each(function() {
|
panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]:checked').each(function() {
|
||||||
@@ -322,10 +324,12 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings, isC
|
|||||||
|
|
||||||
liveSettings = {
|
liveSettings = {
|
||||||
worldbookEnabled: apiSettings.plotOpt_worldbookEnabled,
|
worldbookEnabled: apiSettings.plotOpt_worldbookEnabled,
|
||||||
|
newMemoryLogicEnabled: apiSettings.plotOpt_newMemoryLogicEnabled,
|
||||||
worldbookSource: apiSettings.plotOpt_worldbookSource || 'character',
|
worldbookSource: apiSettings.plotOpt_worldbookSource || 'character',
|
||||||
selectedWorldbooks: apiSettings.plotOpt_selectedWorldbooks,
|
selectedWorldbooks: apiSettings.plotOpt_selectedWorldbooks,
|
||||||
autoSelectWorldbooks: apiSettings.plotOpt_autoSelectWorldbooks || [],
|
autoSelectWorldbooks: apiSettings.plotOpt_autoSelectWorldbooks || [],
|
||||||
worldbookCharLimit: apiSettings.plotOpt_worldbookCharLimit,
|
worldbookCharLimit: apiSettings.plotOpt_worldbookCharLimit,
|
||||||
|
contextLimit: apiSettings.plotOpt_contextLimit || 5,
|
||||||
enabledWorldbookEntries: apiSettings.plotOpt_enabledWorldbookEntries,
|
enabledWorldbookEntries: apiSettings.plotOpt_enabledWorldbookEntries,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -370,6 +374,34 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings, isC
|
|||||||
const userEnabledEntries = allEntries.filter(entry => {
|
const userEnabledEntries = allEntries.filter(entry => {
|
||||||
if (!entry.enabled) return false;
|
if (!entry.enabled) return false;
|
||||||
|
|
||||||
|
// New Memory Logic
|
||||||
|
if (liveSettings.newMemoryLogicEnabled) {
|
||||||
|
const character = characters[context.characterId];
|
||||||
|
const charName = character ? (character.data?.name || character.name) : null;
|
||||||
|
|
||||||
|
if (charName && entry.bookName === `Amily2_Memory_${charName}`) {
|
||||||
|
const keywords = [...new Set([...(entry.key || []), ...(entry.keys || [])])];
|
||||||
|
if (keywords.some(k => k.includes('索引'))) {
|
||||||
|
entry.constant = true; // Blue Light (Constant)
|
||||||
|
entry.prevent_recursion = true; // Prevent Index from triggering other entries
|
||||||
|
} else {
|
||||||
|
// Ensure it's not constant unless it was already constant in ST (which we might want to respect, or override?)
|
||||||
|
// The requirement says: "其余的绿灯条目,则依照SittlyTavern原本的绿灯关键词的触发逻辑"
|
||||||
|
// This implies we should treat them as potential Green Lights.
|
||||||
|
// In my logic, if entry.constant is false, it becomes a Green Light candidate.
|
||||||
|
// However, ST entries have a `constant` property. `safeLorebookEntries` returns it.
|
||||||
|
// If the entry was originally constant in ST, should we keep it constant?
|
||||||
|
// The requirement says "原逻辑转换...".
|
||||||
|
// "只要关键词里面包含索引,则将所有索引条目发送给我们的模型。"
|
||||||
|
// "而其余的绿灯条目..."
|
||||||
|
// This implies we are redefining what is constant/triggered based on this logic.
|
||||||
|
// So I will force constant=false if it doesn't have "索引".
|
||||||
|
entry.constant = false;
|
||||||
|
}
|
||||||
|
return true; // Always include as candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For concurrent calls where enabledWorldbookEntries is null, or for books marked as "auto-select",
|
// For concurrent calls where enabledWorldbookEntries is null, or for books marked as "auto-select",
|
||||||
// we consider all enabled entries within that book as selected.
|
// we consider all enabled entries within that book as selected.
|
||||||
const isAuto = autoSelectedBooks.includes(entry.bookName);
|
const isAuto = autoSelectedBooks.includes(entry.bookName);
|
||||||
@@ -396,46 +428,58 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings, isC
|
|||||||
|
|
||||||
if (userEnabledEntries.length === 0) return '';
|
if (userEnabledEntries.length === 0) return '';
|
||||||
|
|
||||||
const chatHistory = context.chat.map(message => message.mes).join('\n').toLowerCase();
|
let messagesToScan = context.chat;
|
||||||
const getEntryKeywords = (entry) => [...new Set([...(entry.key || []), ...(entry.keys || [])])].map(k => k.toLowerCase());
|
if (liveSettings.contextLimit > 0) {
|
||||||
|
messagesToScan = context.chat.slice(-liveSettings.contextLimit);
|
||||||
|
}
|
||||||
|
const chatHistory = messagesToScan.map(message => message.mes).join('\n').toLowerCase();
|
||||||
|
const getEntryKeywords = (entry) => [...new Set([...(entry.key || []), ...(entry.keys || [])])]
|
||||||
|
.filter(k => k && k.trim().length > 0)
|
||||||
|
.map(k => k.toLowerCase());
|
||||||
|
|
||||||
const blueLightEntries = userEnabledEntries.filter(entry => entry.constant);
|
const blueLightEntries = userEnabledEntries.filter(entry => entry.constant);
|
||||||
let pendingGreenLights = userEnabledEntries.filter(entry => !entry.constant);
|
let pendingGreenLights = userEnabledEntries.filter(entry => !entry.constant);
|
||||||
|
|
||||||
const triggeredEntries = new Set([...blueLightEntries]);
|
const triggeredEntries = new Set([...blueLightEntries]);
|
||||||
|
|
||||||
while (true) {
|
// 禁用递归扫描,防止总结/索引条目触发所有内容。
|
||||||
let hasChangedInThisPass = false;
|
// 仅扫描聊天记录。
|
||||||
|
for (const entry of pendingGreenLights) {
|
||||||
|
const keywords = getEntryKeywords(entry);
|
||||||
|
const secondaryKeys = (entry.secondary_keys || []).filter(k => k && k.trim().length > 0).map(k => k.toLowerCase());
|
||||||
|
const selectiveKeys = (entry.selective || []).filter(k => k && k.trim().length > 0).map(k => k.toLowerCase());
|
||||||
|
|
||||||
const recursionSourceContent = Array.from(triggeredEntries)
|
// 仅检查聊天记录,忽略其他条目的内容(防止递归触发)
|
||||||
.filter(e => !e.prevent_recursion)
|
const checkText = chatHistory;
|
||||||
.map(e => e.content)
|
|
||||||
.join('\n')
|
|
||||||
.toLowerCase();
|
|
||||||
const fullSearchText = `${chatHistory}\n${recursionSourceContent}`;
|
|
||||||
|
|
||||||
const nextPendingGreenLights = [];
|
|
||||||
|
|
||||||
for (const entry of pendingGreenLights) {
|
const hasPrimary = keywords.length > 0 && keywords.some(k => checkText.includes(k));
|
||||||
const keywords = getEntryKeywords(entry);
|
const hasSecondary = secondaryKeys.length === 0 || secondaryKeys.some(k => checkText.includes(k));
|
||||||
let isTriggered = keywords.length > 0 && keywords.some(keyword =>
|
const hasSelective = selectiveKeys.length > 0 && selectiveKeys.some(k => checkText.includes(k));
|
||||||
entry.exclude_recursion ? chatHistory.includes(keyword) : fullSearchText.includes(keyword)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isTriggered) {
|
let isTriggered = hasPrimary && hasSecondary && !hasSelective;
|
||||||
triggeredEntries.add(entry);
|
|
||||||
hasChangedInThisPass = true;
|
if (isTriggered) {
|
||||||
} else {
|
triggeredEntries.add(entry);
|
||||||
nextPendingGreenLights.push(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasChangedInThisPass) break;
|
|
||||||
|
|
||||||
pendingGreenLights = nextPendingGreenLights;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalContent = Array.from(triggeredEntries).map(entry => {
|
const finalEntries = Array.from(triggeredEntries);
|
||||||
|
|
||||||
|
// 排序:索引内容(常驻且防递归) > 触发的条目 > 其他常驻
|
||||||
|
finalEntries.sort((a, b) => {
|
||||||
|
const isIndex = (e) => e.constant && e.prevent_recursion;
|
||||||
|
const isTriggered = (e) => !e.constant;
|
||||||
|
|
||||||
|
const getPriority = (e) => {
|
||||||
|
if (isIndex(e)) return 1;
|
||||||
|
if (isTriggered(e)) return 2;
|
||||||
|
return 3; // 其他常驻
|
||||||
|
};
|
||||||
|
|
||||||
|
return getPriority(a) - getPriority(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalContent = finalEntries.map(entry => {
|
||||||
const keys = [...new Set([...(entry.key || []), ...(entry.keys || [])])].filter(Boolean).join('、');
|
const keys = [...new Set([...(entry.key || []), ...(entry.keys || [])])].filter(Boolean).join('、');
|
||||||
const displayName = entry.comment || `Entry ${entry.uid}`;
|
const displayName = entry.comment || `Entry ${entry.uid}`;
|
||||||
return `【世界书条目:${displayName}。绿灯触发关键词:${keys}】\n内容:${entry.content}`;
|
return `【世界书条目:${displayName}。绿灯触发关键词:${keys}】\n内容:${entry.content}`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Amily2号聊天优化助手",
|
"name": "Amily2号聊天优化助手",
|
||||||
"display_name": "Amily2号助手",
|
"display_name": "Amily2号助手",
|
||||||
"version": "1.8.2",
|
"version": "1.8.3",
|
||||||
"author": "Wx-2025",
|
"author": "Wx-2025",
|
||||||
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
||||||
"minSillyTavernVersion": "1.10.0",
|
"minSillyTavernVersion": "1.10.0",
|
||||||
@@ -53,5 +53,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1771,6 +1771,7 @@ function opt_loadSettings(panel) {
|
|||||||
panel.find('#amily2_opt_tavern_api_profile_select').val(settings.plotOpt_tavernProfile);
|
panel.find('#amily2_opt_tavern_api_profile_select').val(settings.plotOpt_tavernProfile);
|
||||||
panel.find(`input[name="amily2_opt_worldbook_source"][value="${settings.plotOpt_worldbookSource || 'character'}"]`).prop('checked', true);
|
panel.find(`input[name="amily2_opt_worldbook_source"][value="${settings.plotOpt_worldbookSource || 'character'}"]`).prop('checked', true);
|
||||||
panel.find('#amily2_opt_worldbook_enabled').prop('checked', settings.plotOpt_worldbookEnabled);
|
panel.find('#amily2_opt_worldbook_enabled').prop('checked', settings.plotOpt_worldbookEnabled);
|
||||||
|
panel.find('#amily2_opt_new_memory_logic_enabled').prop('checked', settings.plotOpt_newMemoryLogicEnabled);
|
||||||
panel.find('#amily2_opt_api_url').val(settings.plotOpt_apiUrl);
|
panel.find('#amily2_opt_api_url').val(settings.plotOpt_apiUrl);
|
||||||
panel.find('#amily2_opt_api_key').val(settings.plotOpt_apiKey);
|
panel.find('#amily2_opt_api_key').val(settings.plotOpt_apiKey);
|
||||||
|
|
||||||
@@ -2279,6 +2280,21 @@ export function initializePlotOptimizationBindings() {
|
|||||||
opt_loadSettings(panel);
|
opt_loadSettings(panel);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refreshWorldbookUI = () => {
|
||||||
|
if (panel.is(':visible')) {
|
||||||
|
console.log(`[${extensionName}] 检测到世界书变更,正在刷新列表...`);
|
||||||
|
opt_loadWorldbooks(panel).then(() => {
|
||||||
|
opt_loadWorldbookEntries(panel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.on(event_types.WORLDINFO_UPDATED, refreshWorldbookUI);
|
||||||
|
// 尝试监听更多可能的世界书事件,确保第一时间更新
|
||||||
|
if (event_types.WORLDINFO_ENTRY_UPDATED) eventSource.on(event_types.WORLDINFO_ENTRY_UPDATED, refreshWorldbookUI);
|
||||||
|
if (event_types.WORLDINFO_ENTRY_CREATED) eventSource.on(event_types.WORLDINFO_ENTRY_CREATED, refreshWorldbookUI);
|
||||||
|
if (event_types.WORLDINFO_ENTRY_DELETED) eventSource.on(event_types.WORLDINFO_ENTRY_DELETED, refreshWorldbookUI);
|
||||||
|
|
||||||
const handleSettingChange = function(element) {
|
const handleSettingChange = function(element) {
|
||||||
const el = $(element);
|
const el = $(element);
|
||||||
const key_part = (element.name || element.id).replace('amily2_opt_', '');
|
const key_part = (element.name || element.id).replace('amily2_opt_', '');
|
||||||
|
|||||||
Reference in New Issue
Block a user