mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 08:55:50 +00:00
Merge branch 'Wx-2025:main' into main
This commit is contained in:
@@ -46,7 +46,7 @@ const UPDATE_CHECK_URL =
|
||||
"https://raw.githubusercontent.com/Wx-2025/ST-Amily2-Chat-Optimisation/refs/heads/main/amily2_update_info.json";
|
||||
|
||||
const MESSAGE_BOARD_URL =
|
||||
"https://raw.githubusercontent.com/Wx-2025/ST-Amily2-Chat-Optimisation/refs/heads/main/amily2_message_board.json";
|
||||
"https://amilyservice.amily49.cc/amily2_message_board.json";
|
||||
|
||||
export async function fetchMessageBoardContent() {
|
||||
if (!MESSAGE_BOARD_URL) {
|
||||
|
||||
195
core/context-optimizer.js
Normal file
195
core/context-optimizer.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import { log } from "./table-system/logger.js";
|
||||
import { getContext } from "/scripts/extensions.js";
|
||||
import { eventSource, event_types } from "/script.js";
|
||||
|
||||
function collectDataToBuffer(buffer, tableName, rowObj) {
|
||||
if (!buffer[tableName]) {
|
||||
buffer[tableName] = {
|
||||
headers: Object.keys(rowObj),
|
||||
rows: []
|
||||
};
|
||||
} else {
|
||||
const newKeys = Object.keys(rowObj);
|
||||
newKeys.forEach(k => {
|
||||
if (!buffer[tableName].headers.includes(k)) {
|
||||
buffer[tableName].headers.push(k);
|
||||
}
|
||||
});
|
||||
}
|
||||
buffer[tableName].rows.push(rowObj);
|
||||
}
|
||||
|
||||
function flushBufferToMarkdown(buffer) {
|
||||
let output = "";
|
||||
const tableNames = Object.keys(buffer);
|
||||
|
||||
if (tableNames.length === 0) return "";
|
||||
|
||||
for (const tableName of tableNames) {
|
||||
const { headers, rows } = buffer[tableName];
|
||||
if (rows.length === 0) continue;
|
||||
|
||||
const firstColKey = headers[0];
|
||||
const firstColVal = rows[0] ? rows[0][firstColKey] : '';
|
||||
const isIndexCol = (firstColKey && (firstColKey.includes('索引') || firstColKey.includes('Index'))) ||
|
||||
(typeof firstColVal === 'string' && /^\s*M\d+/.test(firstColVal));
|
||||
|
||||
if (isIndexCol) {
|
||||
rows.sort((a, b) => {
|
||||
const valA = String(a[firstColKey] || '');
|
||||
const valB = String(b[firstColKey] || '');
|
||||
return valA.localeCompare(valB, undefined, { numeric: true });
|
||||
});
|
||||
} else {
|
||||
|
||||
rows.reverse();
|
||||
}
|
||||
|
||||
output += `\n# ${tableName}档案\n`;
|
||||
output += `| ${headers.join(' | ')} |\n`;
|
||||
output += `|${headers.map(() => '---').join('|')}|\n`;
|
||||
|
||||
for (const rowObj of rows) {
|
||||
const rowArr = headers.map(h => {
|
||||
const val = rowObj[h];
|
||||
let safeVal = (val === undefined || val === null) ? '' : String(val);
|
||||
safeVal = safeVal.replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
||||
return safeVal;
|
||||
});
|
||||
output += `| ${rowArr.join(' | ')} |\n`;
|
||||
}
|
||||
output += `\n`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function processText(text) {
|
||||
const blockRegex = /【(.*?)档案[::]\s*.*?】\s*((?:-\s*.*?[::].*?(?:\r?\n|$))+)/g;
|
||||
const itemRegex = /-\s*(.*?)[::]\s*(.*?)(?:\r?\n|$)/g;
|
||||
|
||||
const buffer = {};
|
||||
let found = false;
|
||||
|
||||
const cleanText = text.replace(blockRegex, (match, tableName, content) => {
|
||||
found = true;
|
||||
const rowObj = {};
|
||||
|
||||
let itemMatch;
|
||||
itemRegex.lastIndex = 0;
|
||||
|
||||
while ((itemMatch = itemRegex.exec(content)) !== null) {
|
||||
const key = itemMatch[1].trim();
|
||||
const val = itemMatch[2].trim();
|
||||
if (key) {
|
||||
rowObj[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(rowObj).length > 0) {
|
||||
collectDataToBuffer(buffer, tableName, rowObj);
|
||||
}
|
||||
|
||||
return ""; // 移除原始文本
|
||||
});
|
||||
|
||||
return { cleanText, buffer, found };
|
||||
}
|
||||
|
||||
function handlePromptProcessing(data) {
|
||||
if (!data) return;
|
||||
|
||||
if (typeof data.prompt === 'string') {
|
||||
const { cleanText, buffer, found } = processText(data.prompt);
|
||||
if (found) {
|
||||
const mergedTable = flushBufferToMarkdown(buffer);
|
||||
if (mergedTable) {
|
||||
data.prompt = cleanText + "\n" + mergedTable;
|
||||
log('[ContextOptimizer] 已优化上下文:合并了分散的世界书条目 (Text Mode)。', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
} else if (Array.isArray(data.chat)) {
|
||||
console.log('[ContextOptimizer] 检测到 Chat Completion 格式...');
|
||||
|
||||
const newChat = [];
|
||||
let modifiedCount = 0;
|
||||
|
||||
for (const msg of data.chat) {
|
||||
const newMsg = { ...msg };
|
||||
|
||||
if (typeof newMsg.content === 'string') {
|
||||
const { cleanText, buffer, found } = processText(newMsg.content);
|
||||
|
||||
if (found) {
|
||||
const mergedTable = flushBufferToMarkdown(buffer);
|
||||
if (mergedTable) {
|
||||
newMsg.content = cleanText + "\n" + mergedTable;
|
||||
modifiedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
newChat.push(newMsg);
|
||||
}
|
||||
|
||||
if (modifiedCount > 0) {
|
||||
console.log(`[ContextOptimizer] 已原地优化 ${modifiedCount} 条消息中的表格数据。`);
|
||||
|
||||
// 全量替换,确保生效
|
||||
data.chat.splice(0, data.chat.length, ...newChat);
|
||||
log('[ContextOptimizer] 已优化上下文:合并了分散的世界书条目 (Chat Mode - In Place)。', 'success');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册监听器
|
||||
*/
|
||||
export function registerContextOptimizerMacros() {
|
||||
console.log('[ContextOptimizer] 正在注册监听器...');
|
||||
const context = getContext();
|
||||
|
||||
if (context) {
|
||||
console.log('[ContextOptimizer] Context APIs:', Object.keys(context));
|
||||
}
|
||||
|
||||
if (context && context.registerChatCompletionModifier) {
|
||||
context.registerChatCompletionModifier((chat) => {
|
||||
console.log('[ContextOptimizer] ChatCompletionModifier 触发');
|
||||
const data = { chat: chat };
|
||||
handlePromptProcessing(data);
|
||||
return data.chat;
|
||||
});
|
||||
log('[ContextOptimizer] 已注册 Chat Completion Modifier。', 'success');
|
||||
|
||||
} else if (context && context.registerPromptModifier) {
|
||||
context.registerPromptModifier((prompt) => {
|
||||
console.log('[ContextOptimizer] PromptModifier 触发');
|
||||
const data = { prompt: prompt };
|
||||
handlePromptProcessing(data);
|
||||
return data.prompt;
|
||||
});
|
||||
log('[ContextOptimizer] 已注册 Prompt Modifier (正则模式)。', 'success');
|
||||
|
||||
} else if (eventSource) {
|
||||
eventSource.on('chat_completion_prompt_ready', (...args) => {
|
||||
if (args[0] && typeof args[0] === 'object') {
|
||||
handlePromptProcessing(args[0]);
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.on(event_types.GENERATION_STARTED, (...args) => {
|
||||
if (args.length > 1 && args[1] && typeof args[1].prompt === 'string') {
|
||||
handlePromptProcessing(args[1]);
|
||||
} else if (args[0] && typeof args[0].prompt === 'string') {
|
||||
handlePromptProcessing(args[0]);
|
||||
}
|
||||
});
|
||||
|
||||
log('[ContextOptimizer] 已绑定事件监听 (Text/Chat 双模式)。', 'info');
|
||||
} else {
|
||||
console.error('[ContextOptimizer] 无法获取 eventSource。');
|
||||
}
|
||||
}
|
||||
export function resetContextBuffer() {
|
||||
}
|
||||
@@ -18,6 +18,21 @@ import { callAI, generateRandomSeed } from "./api.js";
|
||||
import { callNgmsAI } from "./api/Ngms_api.js";
|
||||
import { executeAutoHide } from "./autoHideManager.js";
|
||||
|
||||
let reloadEditor = () => {
|
||||
console.warn("[大史官] reloadEditor 函数不可用,可能是旧版本。已使用空函数代替。");
|
||||
};
|
||||
(async () => {
|
||||
try {
|
||||
const { reloadEditor: importedReloadEditor } = await import("/scripts/world-info.js");
|
||||
if (importedReloadEditor) {
|
||||
reloadEditor = importedReloadEditor;
|
||||
console.log("[大史官] 已成功动态导入 reloadEditor。");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[大史官] 动态导入 reloadEditor 失败,将使用空函数。错误信息:", error.message);
|
||||
}
|
||||
})();
|
||||
|
||||
let isExpeditionRunning = false;
|
||||
let manualStopRequested = false;
|
||||
|
||||
@@ -105,6 +120,16 @@ export async function getLoresForWorldbook(bookName) {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export async function executeManualSummary(startFloor, endFloor, isAuto = false) {
|
||||
return new Promise(async (resolve) => {
|
||||
const toastTitle = isAuto ? "微言录 (自动)" : "微言录 (手动)";
|
||||
@@ -154,9 +179,9 @@ export async function executeManualSummary(startFloor, endFloor, isAuto = false)
|
||||
const generateModalHtml = (msgList) => {
|
||||
const messageHtml = msgList.map(msg => `
|
||||
<details class="historiography-message-item" data-author-type="${msg.authorType}">
|
||||
<summary>【第 ${msg.floor} 楼】 ${msg.author}</summary>
|
||||
<summary>【第 ${msg.floor} 楼】 ${escapeHtml(msg.author)}</summary>
|
||||
<div class="historiography-editor-container">
|
||||
<textarea class="text_pole" data-floor="${msg.floor}">${msg.content}</textarea>
|
||||
<textarea class="text_pole" data-floor="${msg.floor}">${escapeHtml(msg.content)}</textarea>
|
||||
</div>
|
||||
</details>
|
||||
`).join('');
|
||||
@@ -613,6 +638,7 @@ export async function executeRefinement(worldbook, loreKey) {
|
||||
|
||||
entry.content = finalContent;
|
||||
await saveWorldInfo(worldbook, bookData, true);
|
||||
reloadEditor(worldbook);
|
||||
toastr.success(`史册已成功重铸,并保存于《${worldbook}》!`, "宏史卷重铸完毕");
|
||||
},
|
||||
onRegenerate: async (dialog) => {
|
||||
@@ -816,3 +842,135 @@ export async function executeCompilation(worldbook, loreKeys) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 史册归档与回溯系统 ==========
|
||||
|
||||
async function getTargetLorebookName() {
|
||||
const settings = extension_settings[extensionName];
|
||||
const context = getContext();
|
||||
let targetLorebookName = null;
|
||||
switch (settings.lorebookTarget) {
|
||||
case "character_main":
|
||||
targetLorebookName = characters[context.characterId]?.data?.extensions?.world;
|
||||
break;
|
||||
case "dedicated":
|
||||
const chatIdentifier = await getChatIdentifier();
|
||||
targetLorebookName = `Amily2-Lore-${chatIdentifier}`;
|
||||
break;
|
||||
}
|
||||
return targetLorebookName;
|
||||
}
|
||||
|
||||
export async function archiveCurrentLedger() {
|
||||
try {
|
||||
const targetLorebookName = await getTargetLorebookName();
|
||||
if (!targetLorebookName) {
|
||||
toastr.error("无法确定目标世界书,归档失败。", "圣谕不明");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bookData = await loadWorldInfo(targetLorebookName);
|
||||
if (!bookData || !bookData.entries) {
|
||||
toastr.error(`无法读取世界书《${targetLorebookName}》。`, "国史馆");
|
||||
return false;
|
||||
}
|
||||
|
||||
const ledgerEntryKey = Object.keys(bookData.entries).find(
|
||||
(key) => bookData.entries[key].comment === RUNNING_LOG_COMMENT && !bookData.entries[key].disable
|
||||
);
|
||||
|
||||
if (!ledgerEntryKey) {
|
||||
toastr.info("当前没有活跃的【对话流水总帐】,无需归档。", "国史馆");
|
||||
return false;
|
||||
}
|
||||
|
||||
const entry = bookData.entries[ledgerEntryKey];
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||
const newComment = `${RUNNING_LOG_COMMENT}_归档_${timestamp}`;
|
||||
|
||||
entry.comment = newComment;
|
||||
entry.disable = true;
|
||||
|
||||
await saveWorldInfo(targetLorebookName, bookData, true);
|
||||
reloadEditor(targetLorebookName);
|
||||
toastr.success(`已将当前流水总帐归档为:\n${newComment}`, "归档成功");
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error("[大史官] 归档失败:", error);
|
||||
toastr.error(`归档失败: ${error.message}`, "国史馆");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getArchivedLedgers() {
|
||||
try {
|
||||
const targetLorebookName = await getTargetLorebookName();
|
||||
if (!targetLorebookName) return [];
|
||||
|
||||
const bookData = await loadWorldInfo(targetLorebookName);
|
||||
if (!bookData || !bookData.entries) return [];
|
||||
|
||||
const archivedLedgers = Object.entries(bookData.entries)
|
||||
.filter(([, entry]) => entry.comment && entry.comment.startsWith(`${RUNNING_LOG_COMMENT}_归档_`))
|
||||
.map(([key, entry]) => ({
|
||||
key: key,
|
||||
comment: entry.comment
|
||||
}))
|
||||
.sort((a, b) => b.comment.localeCompare(a.comment)); // 按时间倒序排列
|
||||
|
||||
return archivedLedgers;
|
||||
|
||||
} catch (error) {
|
||||
console.error("[大史官] 获取归档列表失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreArchivedLedger(targetLoreKey) {
|
||||
try {
|
||||
const targetLorebookName = await getTargetLorebookName();
|
||||
if (!targetLorebookName) {
|
||||
toastr.error("无法确定目标世界书,回溯失败。", "圣谕不明");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bookData = await loadWorldInfo(targetLorebookName);
|
||||
if (!bookData || !bookData.entries) {
|
||||
toastr.error(`无法读取世界书《${targetLorebookName}》。`, "国史馆");
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetEntry = bookData.entries[targetLoreKey];
|
||||
if (!targetEntry) {
|
||||
toastr.error("找不到指定的归档史册。", "圣谕有误");
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentActiveKey = Object.keys(bookData.entries).find(
|
||||
(key) => bookData.entries[key].comment === RUNNING_LOG_COMMENT && !bookData.entries[key].disable
|
||||
);
|
||||
|
||||
if (currentActiveKey) {
|
||||
if (currentActiveKey !== targetLoreKey) {
|
||||
const activeEntry = bookData.entries[currentActiveKey];
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||
activeEntry.comment = `${RUNNING_LOG_COMMENT}_归档_${timestamp}`;
|
||||
activeEntry.disable = true;
|
||||
toastr.info(`已自动归档原有的活跃史册为: ${activeEntry.comment}`, "自动归档");
|
||||
}
|
||||
}
|
||||
targetEntry.comment = RUNNING_LOG_COMMENT;
|
||||
targetEntry.disable = false;
|
||||
|
||||
await saveWorldInfo(targetLorebookName, bookData, true);
|
||||
reloadEditor(targetLorebookName);
|
||||
toastr.success("史册回溯成功!时光已倒流,旧史重现。", "回溯成功");
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error("[大史官] 回溯失败:", error);
|
||||
toastr.error(`回溯失败: ${error.message}`, "国史馆");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
68
core/super-memory/bindings.js
Normal file
68
core/super-memory/bindings.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { extension_settings } from "/scripts/extensions.js";
|
||||
import { saveSettingsDebounced } from "/script.js";
|
||||
import { initializeSuperMemory, purgeSuperMemory } from "./manager.js";
|
||||
|
||||
export function bindSuperMemoryEvents() {
|
||||
const panel = $('#amily2_super_memory_panel');
|
||||
if (panel.length === 0) return;
|
||||
|
||||
panel.on('click', '.sm-nav-item', function() {
|
||||
const tab = $(this).data('tab');
|
||||
|
||||
panel.find('.sm-nav-item').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
panel.find('.sm-tab-pane').removeClass('active');
|
||||
panel.find(`#sm-${tab}-tab`).addClass('active');
|
||||
});
|
||||
|
||||
panel.on('change', 'input[type="checkbox"]', function() {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
|
||||
const id = this.id;
|
||||
let key = null;
|
||||
|
||||
if (id === 'sm-system-enabled') key = 'super_memory_enabled';
|
||||
if (id === 'sm-bridge-enabled') key = 'superMemory_bridgeEnabled';
|
||||
|
||||
if (key) {
|
||||
extension_settings[extensionName][key] = this.checked;
|
||||
saveSettingsDebounced();
|
||||
console.log(`[Amily2-SuperMemory] Setting updated: ${key} = ${this.checked}`);
|
||||
}
|
||||
});
|
||||
|
||||
loadSuperMemorySettings();
|
||||
|
||||
console.log('[Amily2-SuperMemory] Events bound successfully.');
|
||||
}
|
||||
|
||||
function loadSuperMemorySettings() {
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
|
||||
$('#sm-system-enabled').prop('checked', settings.super_memory_enabled ?? false);
|
||||
$('#sm-bridge-enabled').prop('checked', settings.superMemory_bridgeEnabled ?? false);
|
||||
}
|
||||
|
||||
window.sm_initializeSystem = async function() {
|
||||
toastr.info('超级记忆系统正在初始化...');
|
||||
$('#sm-system-status').text('初始化中...').css('color', 'yellow');
|
||||
|
||||
try {
|
||||
await initializeSuperMemory();
|
||||
toastr.success('超级记忆系统初始化完成。');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toastr.error('初始化失败,请检查控制台。');
|
||||
$('#sm-system-status').text('错误').css('color', 'red');
|
||||
}
|
||||
};
|
||||
|
||||
window.sm_purgeMemory = async function() {
|
||||
if (confirm('您确定要清空所有由Amily2管理的超级记忆数据吗?\n这将删除世界书中所有以表格世界书的条目。')) {
|
||||
toastr.info('正在清空记忆...');
|
||||
await purgeSuperMemory();
|
||||
$('#sm-system-status').text('已清空').css('color', '#ffc107');
|
||||
}
|
||||
};
|
||||
77
core/super-memory/index.html
Normal file
77
core/super-memory/index.html
Normal file
@@ -0,0 +1,77 @@
|
||||
<div class="amily2-header">
|
||||
<div class="additional-features-title interactable" title="Amily2 究极长期记忆系统">
|
||||
<i class="fas fa-brain"></i> 灵台 · 记忆中枢
|
||||
</div>
|
||||
<button id="amily2_back_to_main_from_super_memory" class="menu_button secondary small_button interactable">
|
||||
返回主殿 <i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<hr class="header-divider">
|
||||
|
||||
<div id="sm-modal-container">
|
||||
<div class="sm-intro-box">
|
||||
<h3><i class="fas fa-microchip"></i> 究极长期记忆 (Super Memory)</h3>
|
||||
<p>欢迎来到 Amily2 的核心记忆中枢。这里掌管着世界的记忆,连接着每一个角色、每一个物品与每一段传说。</p>
|
||||
<p>通过“三级金字塔”注入策略,我们将实现极致的 Token 节省与无限的记忆深度。</p>
|
||||
</div>
|
||||
|
||||
<div class="sm-navigation-deck">
|
||||
<button class="sm-nav-item active" data-tab="dashboard">概览</button>
|
||||
<button class="sm-nav-item" data-tab="config">配置</button>
|
||||
<button class="sm-nav-item" data-tab="relation">关联网络</button>
|
||||
</div>
|
||||
|
||||
<div class="sm-scroll">
|
||||
<!-- Dashboard Tab -->
|
||||
<div id="sm-dashboard-tab" class="sm-tab-pane active">
|
||||
<fieldset class="sm-settings-group">
|
||||
<legend><i class="fas fa-tachometer-alt"></i> 状态监控</legend>
|
||||
<div class="sm-control-block">
|
||||
<label>记忆系统状态:</label>
|
||||
<span id="sm-system-status" class="sm-status-indicator">未初始化</span>
|
||||
</div>
|
||||
<div class="sm-control-block">
|
||||
<label>当前索引 (Tier 1):</label>
|
||||
<span id="sm-index-count">0 条目</span>
|
||||
</div>
|
||||
<div class="sm-control-block">
|
||||
<label>已触发详情 (Tier 2):</label>
|
||||
<span id="sm-detail-count">0 条目</span>
|
||||
</div>
|
||||
<div class="sm-button-group">
|
||||
<button class="sm-action-button success" onclick="sm_initializeSystem()">初始化系统</button>
|
||||
<button class="sm-action-button danger" onclick="sm_purgeMemory()">清空记忆</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Config Tab -->
|
||||
<div id="sm-config-tab" class="sm-tab-pane">
|
||||
<fieldset class="sm-settings-group">
|
||||
<legend><i class="fas fa-cogs"></i> 记忆策略配置</legend>
|
||||
<div class="sm-control-block">
|
||||
<label>启用 Super Memory (总开关):</label>
|
||||
<label class="sm-toggle-switch">
|
||||
<input type="checkbox" id="sm-system-enabled">
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sm-control-block">
|
||||
<label>启用世界书桥接:</label>
|
||||
<label class="sm-toggle-switch">
|
||||
<input type="checkbox" id="sm-bridge-enabled">
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Relation Tab -->
|
||||
<div id="sm-relation-tab" class="sm-tab-pane">
|
||||
<fieldset class="sm-settings-group">
|
||||
<legend><i class="fas fa-project-diagram"></i> 关联网络 (The Mesh)</legend>
|
||||
<p>关联触发逻辑正在开发中...</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
244
core/super-memory/lorebook-bridge.js
Normal file
244
core/super-memory/lorebook-bridge.js
Normal file
@@ -0,0 +1,244 @@
|
||||
import { amilyHelper } from "../tavern-helper/main.js";
|
||||
import { extension_settings, getContext } from "/scripts/extensions.js";
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { this_chid, characters } from "/script.js";
|
||||
|
||||
export function getMemoryBookName() {
|
||||
let charName = "Global";
|
||||
const context = getContext();
|
||||
|
||||
if (this_chid !== undefined && characters[this_chid]) {
|
||||
charName = characters[this_chid].name;
|
||||
} else if (context.characterId !== undefined && characters[context.characterId]) {
|
||||
charName = characters[context.characterId].name;
|
||||
}
|
||||
|
||||
const safeCharName = charName.replace(/[<>:"/\\|?*]/g, '_');
|
||||
return `Amily2_Memory_${safeCharName}`;
|
||||
}
|
||||
|
||||
export async function syncToLorebook(tableName, data, indexText, role, headers, rowStatuses, depth = 100) {
|
||||
console.log(`[Amily2-Bridge] 开始同步表格: ${tableName} (Depth: ${depth})`);
|
||||
|
||||
await ensureMemoryBook();
|
||||
|
||||
const bookName = getMemoryBookName();
|
||||
|
||||
let entries = await amilyHelper.getLorebookEntries(bookName);
|
||||
if (!entries) entries = [];
|
||||
|
||||
const entriesToUpdate = [];
|
||||
const entriesToCreate = [];
|
||||
|
||||
const processEntry = (comment, keys, content, type = 'selective', enabled = true) => {
|
||||
const existingEntry = entries.find(e => e.comment === comment);
|
||||
if (existingEntry) {
|
||||
existingEntry.content = content;
|
||||
existingEntry.key = keys;
|
||||
// existingEntry.order = depth; // 【V153.0】不再覆盖用户的深度/排序设置
|
||||
|
||||
if (type === 'constant') {
|
||||
existingEntry.constant = true;
|
||||
} else {
|
||||
existingEntry.constant = false;
|
||||
}
|
||||
if (existingEntry.enabled !== undefined) {
|
||||
existingEntry.enabled = enabled;
|
||||
} else {
|
||||
existingEntry.disable = !enabled;
|
||||
}
|
||||
entriesToUpdate.push(existingEntry);
|
||||
} else {
|
||||
entriesToCreate.push({
|
||||
comment: comment,
|
||||
keys: keys,
|
||||
content: content,
|
||||
type: type,
|
||||
position: 1,
|
||||
order: depth,
|
||||
enabled: enabled
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (role === 'database') {
|
||||
const indexKey = [`Amily_Index_${tableName}`];
|
||||
const indexComment = `[Amily2] Index for ${tableName}`;
|
||||
let indexContent = `【${tableName} 索引】\n${indexText.trim()}`;
|
||||
|
||||
if (tableName.includes("大纲")) {
|
||||
indexContent += `\n<system_note>\n【Amily2-前瞻性预测指令】\n为了实现“提前一轮触发绿灯世界书”的高级记忆回流,请在生成本轮回复时,根据当前剧情走向,**大胆预测并自然提及**下一轮可能出现的索引编码。可自行增加一个标签并使用<!--到-->html注释掉该编码。这将激活系统的预加载机制,确保剧情发展的连贯性。\n</system_note>`;
|
||||
}
|
||||
|
||||
processEntry(indexComment, indexKey, indexContent, 'constant');
|
||||
}
|
||||
|
||||
data.forEach((row, index) => {
|
||||
if (!row || row.length === 0) return;
|
||||
|
||||
const rawVal = row[0];
|
||||
// 【V152.0】修复Falsy检查漏洞 (支持数字0作为主键)
|
||||
if (rawVal === undefined || rawVal === null) return;
|
||||
|
||||
const primaryVal = String(rawVal).trim();
|
||||
if (primaryVal === '') return;
|
||||
|
||||
const isPendingDeletion = rowStatuses && rowStatuses[index] === 'pending-deletion';
|
||||
const isEnabled = !isPendingDeletion;
|
||||
|
||||
const triggerKeys = [primaryVal];
|
||||
const entryComment = `[Amily2] Detail: ${tableName} - ${primaryVal}`;
|
||||
|
||||
let finalHeaders = headers;
|
||||
if (!finalHeaders || finalHeaders.length < row.length) {
|
||||
finalHeaders = [];
|
||||
for(let i=0; i<row.length; i++) {
|
||||
finalHeaders.push((headers && headers[i]) ? headers[i] : `Col_${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
const optimizationEnabled = settings.context_optimization_enabled !== false;
|
||||
|
||||
let entryContent;
|
||||
|
||||
if (optimizationEnabled) {
|
||||
const primaryVal = row[0] || 'Unknown';
|
||||
entryContent = `【${tableName}档案: ${primaryVal}】\n`;
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
const key = finalHeaders[i] || `Col_${i}`;
|
||||
const val = row[i] || '';
|
||||
entryContent += `- ${key}: ${val}\n`;
|
||||
}
|
||||
} else {
|
||||
let textContent = `【${tableName} 详情】\n`;
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
const key = finalHeaders[i] || `Col_${i}`;
|
||||
const val = row[i] || '';
|
||||
textContent += `- ${key}: ${val}\n`;
|
||||
}
|
||||
entryContent = textContent.trim();
|
||||
}
|
||||
|
||||
processEntry(entryComment, triggerKeys, entryContent.trim(), 'selective', isEnabled);
|
||||
});
|
||||
|
||||
const entriesToDelete = [];
|
||||
const tablePrefix = `[Amily2] Detail: ${tableName} -`;
|
||||
|
||||
const activeKeys = new Set();
|
||||
for(const row of data) {
|
||||
// 【V152.0】修复Falsy检查漏洞 (支持数字0作为主键)
|
||||
if(row && row.length > 0) {
|
||||
const rVal = row[0];
|
||||
if (rVal !== undefined && rVal !== null) {
|
||||
const sVal = String(rVal).trim();
|
||||
if (sVal !== '') {
|
||||
activeKeys.add(sVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Amily2-Bridge-GC] ${tableName} 的活跃主键 (Active Keys):`, Array.from(activeKeys));
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.comment && entry.comment.startsWith(tablePrefix)) {
|
||||
const entryKey = entry.comment.substring(tablePrefix.length).trim();
|
||||
|
||||
if (!activeKeys.has(entryKey)) {
|
||||
console.log(`[Amily2-Bridge-GC] 发现残留条目 (将删除): ${entry.comment} (Key: ${entryKey})`);
|
||||
entriesToDelete.push(entry.uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entriesToDelete.length > 0) {
|
||||
console.log(`[Amily2-Bridge] 清理 ${entriesToDelete.length} 个废弃条目...`);
|
||||
await amilyHelper.deleteLorebookEntries(bookName, entriesToDelete);
|
||||
}
|
||||
|
||||
if (entriesToUpdate.length > 0) {
|
||||
console.log(`[Amily2-Bridge] 更新 ${entriesToUpdate.length} 个条目...`);
|
||||
await amilyHelper.setLorebookEntries(bookName, entriesToUpdate);
|
||||
}
|
||||
|
||||
if (entriesToCreate.length > 0) {
|
||||
console.log(`[Amily2-Bridge] 创建 ${entriesToCreate.length} 个新条目...`);
|
||||
await amilyHelper.createLorebookEntries(bookName, entriesToCreate);
|
||||
}
|
||||
console.log(`[Amily2-Bridge] 同步完成: ${tableName}`);
|
||||
}
|
||||
|
||||
export async function ensureMemoryBook() {
|
||||
const bookName = getMemoryBookName();
|
||||
const books = await amilyHelper.getLorebooks();
|
||||
|
||||
if (!books.includes(bookName)) {
|
||||
console.log(`[Amily2-Bridge] 创建角色专用世界书: ${bookName}`);
|
||||
await amilyHelper.createLorebook(bookName);
|
||||
}
|
||||
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
const shouldBind = settings.superMemory_autoBind === true;
|
||||
|
||||
if (shouldBind && bookName.startsWith("Amily2_Memory_") && bookName !== "Amily2_Memory_Global") {
|
||||
console.log(`[Amily2-Bridge] 自动绑定世界书到当前角色...`);
|
||||
await amilyHelper.bindLorebookToCharacter(bookName);
|
||||
} else if (!shouldBind) {
|
||||
console.log(`[Amily2-Bridge] 跳过自动绑定 (设置已禁用)。请手动在世界书管理中激活: ${bookName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createEntryTemplate() {
|
||||
return {
|
||||
uid: Date.now() + Math.floor(Math.random() * 1000),
|
||||
key: [],
|
||||
keysecondary: [],
|
||||
comment: "",
|
||||
content: "",
|
||||
constant: false,
|
||||
selective: true,
|
||||
order: 100,
|
||||
position: 1,
|
||||
enabled: true
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateTransientHint(hint) {
|
||||
console.log('[Amily2-Bridge] 更新瞬时记忆提示...');
|
||||
await ensureMemoryBook();
|
||||
const bookName = getMemoryBookName();
|
||||
|
||||
const comment = "[Amily2] Active Memory Hint";
|
||||
const content = hint ? `\n<system_note>\n【重要记忆回响】\n${hint}\n</system_note>\n` : "";
|
||||
const enabled = !!hint;
|
||||
|
||||
let entries = await amilyHelper.getLorebookEntries(bookName);
|
||||
if (!entries) entries = [];
|
||||
|
||||
const existingEntry = entries.find(e => e.comment === comment);
|
||||
|
||||
if (existingEntry) {
|
||||
existingEntry.content = content;
|
||||
existingEntry.enabled = enabled;
|
||||
existingEntry.order = 0;
|
||||
existingEntry.constant = true;
|
||||
|
||||
await amilyHelper.setLorebookEntries(bookName, [existingEntry]);
|
||||
} else if (hint) {
|
||||
const newEntry = {
|
||||
comment: comment,
|
||||
keys: [],
|
||||
content: content,
|
||||
constant: true,
|
||||
selective: false,
|
||||
order: 0,
|
||||
position: 0,
|
||||
enabled: true
|
||||
};
|
||||
await amilyHelper.createLorebookEntries(bookName, [newEntry]);
|
||||
}
|
||||
|
||||
console.log(`[Amily2-Bridge] 瞬时记忆提示已${enabled ? '启用' : '清除'}。`);
|
||||
}
|
||||
1
core/super-memory/manager.js
Normal file
1
core/super-memory/manager.js
Normal file
File diff suppressed because one or more lines are too long
77
core/super-memory/smart-indexer.js
Normal file
77
core/super-memory/smart-indexer.js
Normal file
@@ -0,0 +1,77 @@
|
||||
export function generateIndex(data, role, tableName = "") {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
if (headers.length === 0) return "";
|
||||
|
||||
const indexColumns = identifyIndexColumns(data, headers);
|
||||
|
||||
let indexLines = [];
|
||||
indexLines.push(`| ${indexColumns.join(' | ')} |`);
|
||||
indexLines.push(`| ${indexColumns.map(() => '---').join(' | ')} |`);
|
||||
|
||||
let processedData = [...data];
|
||||
|
||||
const firstColKey = headers[0];
|
||||
const firstColVal = data[0] ? data[0][firstColKey] : '';
|
||||
const isIndexCol = (firstColKey && (firstColKey.includes('索引') || firstColKey.includes('Index'))) ||
|
||||
(typeof firstColVal === 'string' && /^\s*M\d+/.test(firstColVal)) ||
|
||||
(tableName && (tableName.includes('总结') || tableName.includes('大纲')));
|
||||
|
||||
if (isIndexCol) {
|
||||
processedData.sort((a, b) => {
|
||||
const valA = String(a[firstColKey] || '');
|
||||
const valB = String(b[firstColKey] || '');
|
||||
return valA.localeCompare(valB, undefined, { numeric: true });
|
||||
});
|
||||
}
|
||||
|
||||
for (const row of processedData) {
|
||||
const lineParts = indexColumns.map(col => {
|
||||
let val = row[col];
|
||||
if (val === undefined || val === null) return "";
|
||||
val = String(val).trim();
|
||||
if (val.length > 15) val = val.substring(0, 12) + "...";
|
||||
return val;
|
||||
});
|
||||
indexLines.push(`| ${lineParts.join(' | ')} |`);
|
||||
}
|
||||
|
||||
return indexLines.join('\n');
|
||||
}
|
||||
|
||||
function identifyIndexColumns(data, headers) {
|
||||
if (headers.length <= 2) return headers;
|
||||
|
||||
const candidates = [];
|
||||
const maxColumns = 3;
|
||||
|
||||
for (const header of headers) {
|
||||
if (candidates.length >= maxColumns) break;
|
||||
|
||||
let totalLen = 0;
|
||||
let count = 0;
|
||||
for (const row of data) {
|
||||
if (row[header]) {
|
||||
totalLen += String(row[header]).length;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
const avgLen = count > 0 ? totalLen / count : 0;
|
||||
|
||||
const isLongText = avgLen > 20;
|
||||
const isBlacklisted = /desc|bio|detail|history|经历|描述|详情/i.test(header);
|
||||
|
||||
if (!isLongText && !isBlacklisted) {
|
||||
candidates.push(header);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return headers.slice(0, Math.min(headers.length, maxColumns));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
@@ -90,15 +90,18 @@ export async function injectTableData(chat, contextSize, abort, type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.table_injection_enabled) {
|
||||
setExtensionPrompt(INJECTION_KEY, '', 0, 0, false, 'SYSTEM');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const injectionContent = generateTableContent();
|
||||
let injectionContent = generateTableContent();
|
||||
|
||||
if (!injectionContent) {
|
||||
if (!settings.table_injection_enabled) {
|
||||
setExtensionPrompt(INJECTION_KEY, '', 0, 0, false, 'SYSTEM');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!injectionContent || injectionContent.trim() === '') {
|
||||
// 理论上不会走到这里,除非宏都没了
|
||||
setExtensionPrompt(INJECTION_KEY, '', 0, 0, false, 'SYSTEM');
|
||||
return;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -96,9 +96,47 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
}
|
||||
|
||||
try {
|
||||
let textToProcess = latestMessage.mes;
|
||||
// --- 延迟填表逻辑 (V151.0) ---
|
||||
const delay = parseInt(settings.secondary_filler_delay || 0, 10);
|
||||
const chat = context.chat;
|
||||
let targetMessage;
|
||||
let targetIndex;
|
||||
|
||||
if (delay > 0) {
|
||||
// 如果有延迟,我们需要找到“延迟前”的那条消息
|
||||
// chat.length - 1 是当前最新消息的索引
|
||||
// 目标索引 = (chat.length - 1) - delay
|
||||
targetIndex = (chat.length - 1) - delay;
|
||||
|
||||
if (targetIndex < 0) {
|
||||
console.log(`[Amily2-副API] 延迟模式(${delay}): 历史楼层不足,跳过填表。`);
|
||||
return;
|
||||
}
|
||||
|
||||
targetMessage = chat[targetIndex];
|
||||
|
||||
// 检查目标消息是否是AI消息(通常填表针对AI回复)
|
||||
// 如果目标消息是用户的消息,而我们只想填AI的表,这可能是一个问题。
|
||||
// 但如果用户设置了延迟,他们可能期望每隔几层填一次,或者只填AI层。
|
||||
// 现有的 `fillWithSecondaryApi` 是在 `CHAT_COMPLETION` 后调用的,此时最新消息通常是AI消息。
|
||||
// 如果延迟是奇数(例如1),目标消息可能是用户消息。
|
||||
// 假设延迟是偶数(例如2),目标消息是上一条AI消息。
|
||||
|
||||
// 为了安全起见,如果目标消息是用户消息,我们可能应该跳过?或者依然填表(记录用户消息的表)?
|
||||
// 目前表系统通常绑定在AI回复上。
|
||||
// 如果 targetMessage.is_user,我们尝试往回找最近的一条AI消息?
|
||||
// 不,这会乱套。严格按照楼层索引来。
|
||||
|
||||
console.log(`[Amily2-副API] 延迟模式生效: 当前总楼层 ${chat.length}, 延迟 ${delay}, 目标楼层索引 ${targetIndex}`);
|
||||
} else {
|
||||
// 无延迟,使用传入的最新消息
|
||||
targetMessage = latestMessage;
|
||||
targetIndex = chat.length - 1;
|
||||
}
|
||||
|
||||
let textToProcess = targetMessage.mes;
|
||||
if (!textToProcess || !textToProcess.trim()) {
|
||||
console.log("[Amily2-副API] 消息内容为空,跳过填表任务。");
|
||||
console.log("[Amily2-副API] 目标消息内容为空,跳过填表任务。");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,15 +158,15 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const userName = context.name1 || '用户';
|
||||
const characterName = context.name2 || '角色';
|
||||
|
||||
const chat = context.chat;
|
||||
|
||||
// 寻找目标消息之前的最后一条用户消息
|
||||
let lastUserMessage = null;
|
||||
let lastUserMessageIndex = -1;
|
||||
for (let i = chat.length - 2; i >= 0; i--) {
|
||||
|
||||
// 从 targetIndex - 1 开始往前找
|
||||
for (let i = targetIndex - 1; i >= 0; i--) {
|
||||
if (chat[i].is_user) {
|
||||
lastUserMessage = chat[i];
|
||||
lastUserMessageIndex = i;
|
||||
@@ -136,8 +174,8 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
}
|
||||
}
|
||||
|
||||
const currentInteractionContent = (lastUserMessage ? `${userName}(用户)最新消息:${lastUserMessage.mes}\n` : '') +
|
||||
`${characterName}(AI)最新消息,[核心处理内容]:${textToProcess}`;
|
||||
const currentInteractionContent = (lastUserMessage ? `${userName}(用户)消息:${lastUserMessage.mes}\n` : '') +
|
||||
`${characterName}(AI)消息,[核心处理内容]:${textToProcess}`;
|
||||
|
||||
let mixedOrder;
|
||||
try {
|
||||
@@ -185,7 +223,10 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
const historyMessagesToGet = contextReadingLevel > 2 ? contextReadingLevel - 2 : 0;
|
||||
|
||||
if (historyMessagesToGet > 0) {
|
||||
const historyEndIndex = lastUserMessageIndex !== -1 ? lastUserMessageIndex : chat.length - 1;
|
||||
// 这里的 historyEndIndex 应该是我们上面计算出的 lastUserMessageIndex
|
||||
// 如果没找到用户消息,则使用 targetIndex - 1
|
||||
const historyEndIndex = lastUserMessageIndex !== -1 ? lastUserMessageIndex : Math.max(0, targetIndex - 1);
|
||||
|
||||
const historyContext = await getHistoryContext(historyMessagesToGet, historyEndIndex, tagsToExtract, exclusionRules);
|
||||
if (historyContext) {
|
||||
messages.push({ role: "system", content: historyContext });
|
||||
@@ -205,12 +246,10 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
}
|
||||
}
|
||||
|
||||
const fillingMode = settings.filling_mode || 'main-api';
|
||||
if (fillingMode === 'secondary-api') {
|
||||
console.groupCollapsed(`[Amily2 分步填表] 即将发送至 API 的内容`);
|
||||
console.dir(messages);
|
||||
console.groupEnd();
|
||||
}
|
||||
console.groupCollapsed(`[Amily2 分步填表] 即将发送至 API 的内容`);
|
||||
console.log("发送给AI的提示词: ", JSON.stringify(messages, null, 2));
|
||||
console.dir(messages);
|
||||
console.groupEnd();
|
||||
|
||||
let rawContent;
|
||||
if (settings.nccsEnabled) {
|
||||
@@ -230,14 +269,20 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
|
||||
|
||||
updateTableFromText(rawContent);
|
||||
|
||||
const currentContext = getContext();
|
||||
if (currentContext.chat && currentContext.chat.length > 0) {
|
||||
const lastMessage = currentContext.chat[currentContext.chat.length - 1];
|
||||
if (saveStateToMessage(getMemoryState(), lastMessage)) {
|
||||
renderTables();
|
||||
updateOrInsertTableInChat();
|
||||
}
|
||||
// 保存到目标消息
|
||||
if (saveStateToMessage(getMemoryState(), targetMessage)) {
|
||||
// 如果目标消息不是最新消息,我们可能需要重新渲染整个聊天记录或者特定消息的表格?
|
||||
// renderTables() 通常重新渲染所有可见表格
|
||||
renderTables();
|
||||
// updateOrInsertTableInChat 通常插入到DOM中
|
||||
// 我们可能需要传递 targetIndex 给 updateOrInsertTableInChat 吗?
|
||||
// 目前 updateOrInsertTableInChat 似乎是查找 .mes_text 并插入。
|
||||
// 如果我们更新了历史消息的数据,我们需要确保 DOM 也更新。
|
||||
// 由于 SillyTavern 的消息渲染机制,如果消息已经在屏幕上,仅仅修改数据可能不会自动更新 DOM。
|
||||
// 但是 renderTables() 应该会处理这个。
|
||||
updateOrInsertTableInChat();
|
||||
}
|
||||
|
||||
saveChat();
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -147,4 +147,12 @@ export const tableSystemDefaultSettings = {
|
||||
batch_filler_flow_template: DEFAULT_AI_FLOW_TEMPLATE,
|
||||
|
||||
filling_mode: 'main-api',
|
||||
context_optimization_enabled: true, // 【V144.0】上下文优化(世界书合并)开关
|
||||
|
||||
// 【V146.5】分步填表相关设置
|
||||
context_reading_level: 4,
|
||||
secondary_filler_delay: 0,
|
||||
table_independent_rules_enabled: false,
|
||||
table_tags_to_extract: '',
|
||||
table_exclusion_rules: [],
|
||||
};
|
||||
|
||||
@@ -31,7 +31,9 @@ import {
|
||||
name2,
|
||||
addOneMessage,
|
||||
messageFormatting,
|
||||
substituteParamsExtended
|
||||
substituteParamsExtended,
|
||||
saveCharacterDebounced,
|
||||
this_chid
|
||||
} from "/script.js";
|
||||
import { getContext } from "/scripts/extensions.js";
|
||||
import { executeSlashCommandsWithOptions } from '/scripts/slash-commands.js';
|
||||
@@ -430,6 +432,7 @@ class AmilyHelper {
|
||||
existingEntry.position = positionMap[entryUpdate.position] ?? 4;
|
||||
}
|
||||
if (entryUpdate.depth !== undefined) existingEntry.depth = entryUpdate.depth;
|
||||
if (entryUpdate.scanDepth !== undefined) existingEntry.scanDepth = entryUpdate.scanDepth;
|
||||
if (entryUpdate.order !== undefined) existingEntry.order = entryUpdate.order;
|
||||
if (entryUpdate.exclude_recursion !== undefined) existingEntry.excludeRecursion = entryUpdate.exclude_recursion;
|
||||
if (entryUpdate.prevent_recursion !== undefined) existingEntry.preventRecursion = entryUpdate.prevent_recursion;
|
||||
@@ -474,6 +477,7 @@ class AmilyHelper {
|
||||
constant: newEntryData.type === 'constant' ? true : (newEntryData.constant || false),
|
||||
position: typeof newEntryData.position === 'string' ? (positionMap[newEntryData.position] ?? 4) : (newEntryData.position ?? 4),
|
||||
depth: newEntryData.depth ?? 998,
|
||||
scanDepth: newEntryData.scanDepth ?? null,
|
||||
disable: !(newEntryData.enabled ?? true),
|
||||
});
|
||||
if (newEntryData.type === 'selective') newEntry.constant = false;
|
||||
@@ -487,6 +491,34 @@ class AmilyHelper {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteLorebookEntries(bookName, uids) {
|
||||
try {
|
||||
const bookData = await loadWorldInfo(bookName);
|
||||
if (!bookData || !bookData.entries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let deletedCount = 0;
|
||||
for (const uid of uids) {
|
||||
if (bookData.entries[uid]) {
|
||||
delete bookData.entries[uid];
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
await saveWorldInfo(bookName, bookData, true);
|
||||
reloadEditor(bookName);
|
||||
console.log(`[Amily助手] 已从世界书《${bookName}》删除 ${deletedCount} 个条目`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`[Amily助手] 删除世界书《${bookName}》条目时出错:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async createLorebook(bookName) {
|
||||
try {
|
||||
if (world_names.includes(bookName)) {
|
||||
@@ -535,6 +567,42 @@ class AmilyHelper {
|
||||
getLastMessageId() {
|
||||
return chat.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定世界书绑定到当前角色
|
||||
* @param {string} bookName 世界书名称
|
||||
*/
|
||||
async bindLorebookToCharacter(bookName) {
|
||||
if (this_chid === undefined || !characters[this_chid]) {
|
||||
console.warn('[Amily助手] 无法绑定世界书:未选中角色');
|
||||
return false;
|
||||
}
|
||||
|
||||
const char = characters[this_chid];
|
||||
if (!char.data) char.data = {};
|
||||
if (!char.data.extensions) char.data.extensions = {};
|
||||
|
||||
// 确保 world 字段是数组
|
||||
let worlds = char.data.extensions.world;
|
||||
if (!Array.isArray(worlds)) {
|
||||
worlds = worlds ? [worlds] : [];
|
||||
}
|
||||
|
||||
if (!worlds.includes(bookName)) {
|
||||
worlds.push(bookName);
|
||||
char.data.extensions.world = worlds;
|
||||
console.log(`[Amily助手] 已将世界书《${bookName}》绑定到角色 ${char.name}`);
|
||||
|
||||
if (typeof saveCharacterDebounced === 'function') {
|
||||
saveCharacterDebounced();
|
||||
return true;
|
||||
} else {
|
||||
console.warn('[Amily助手] 无法保存角色数据:saveCharacterDebounced 不可用');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true; // 已经绑定
|
||||
}
|
||||
}
|
||||
|
||||
export const amilyHelper = new AmilyHelper();
|
||||
|
||||
Reference in New Issue
Block a user