diff --git a/super-memory/bindings.js b/super-memory/bindings.js new file mode 100644 index 0000000..591de3f --- /dev/null +++ b/super-memory/bindings.js @@ -0,0 +1,89 @@ +import { extensionName } from "../../utils/settings.js"; +import { extension_settings } from "/scripts/extensions.js"; +import { saveSettingsDebounced } from "/script.js"; +import { initializeSuperMemory } 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}`); + } + }); + + panel.on('change', 'input[type="number"], input[type="text"]', function() { + if (!extension_settings[extensionName]) extension_settings[extensionName] = {}; + + const id = this.id; + let key = null; + + if (id === 'sm-index-depth') key = 'superMemory_indexDepth'; + if (id === 'sm-detail-depth') key = 'superMemory_detailDepth'; + + if (key) { + let value = this.value; + if (this.type === 'number') value = parseInt(value, 10); + + extension_settings[extensionName][key] = value; + saveSettingsDebounced(); + console.log(`[Amily2-SuperMemory] Setting updated: ${key} = ${value}`); + } + }); + + 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); + + $('#sm-index-depth').val(settings.superMemory_indexDepth ?? 0); + $('#sm-detail-depth').val(settings.superMemory_detailDepth ?? 2); +} + +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 = function() { + if (confirm('您确定要清空所有超级记忆数据吗?')) { + toastr.warning('记忆已清空。'); + $('#sm-system-status').text('未初始化').css('color', '#ffc107'); + } +}; diff --git a/super-memory/index.html b/super-memory/index.html new file mode 100644 index 0000000..dcb5240 --- /dev/null +++ b/super-memory/index.html @@ -0,0 +1,85 @@ +
+
+ 灵台 · 记忆中枢 +
+ +
+
+ +
+
+

究极长期记忆 (Super Memory)

+

欢迎来到 Amily2 的核心记忆中枢。这里掌管着世界的记忆,连接着每一个角色、每一个物品与每一段传说。

+

通过“三级金字塔”注入策略,我们将实现极致的 Token 节省与无限的记忆深度。

+
+ +
+ + + +
+ +
+ +
+
+ 状态监控 +
+ + 未初始化 +
+
+ + 0 条目 +
+
+ + 0 条目 +
+
+ + +
+
+
+ + +
+
+ 记忆策略配置 +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ 关联网络 (The Mesh) +

关联触发逻辑正在开发中...

+
+
+
+
diff --git a/super-memory/lorebook-bridge.js b/super-memory/lorebook-bridge.js new file mode 100644 index 0000000..dfdcfcf --- /dev/null +++ b/super-memory/lorebook-bridge.js @@ -0,0 +1,228 @@ +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"; + +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; + + 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\n【Amily2-前瞻性预测指令】\n为了实现“提前一轮触发绿灯世界书”的高级记忆回流,请在生成本轮回复时,根据当前剧情走向,**大胆预测并自然提及**下一轮可能出现的索引编码。可自行增加一个标签并使用html注释掉该编码。这将激活系统的预加载机制,确保剧情发展的连贯性。\n`; + } + + processEntry(indexComment, indexKey, indexContent, 'constant'); + } + + data.forEach((row, index) => { + if (!row || row.length === 0) return; + + const primaryVal = row[0]; + + 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 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\n【重要记忆回响】\n${hint}\n\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 ? '启用' : '清除'}。`); +} diff --git a/super-memory/manager.js b/super-memory/manager.js new file mode 100644 index 0000000..38f633d --- /dev/null +++ b/super-memory/manager.js @@ -0,0 +1,203 @@ +import { extension_settings, getContext } from "/scripts/extensions.js"; +import { extensionName } from "../../utils/settings.js"; +import { amilyHelper } from "../tavern-helper/main.js"; +import { generateIndex } from "./smart-indexer.js"; +import { syncToLorebook, ensureMemoryBook, updateTransientHint } from "./lorebook-bridge.js"; +import { getMemoryState, loadMemoryState, saveMemoryState } from "../table-system/manager.js"; +import { eventSource, event_types } from "/script.js"; + +let isInitialized = false; +let updateQueue = []; +let isProcessing = false; +let lastChatId = null; + +const METADATA_KEY = 'Amily2_Memory_Data'; + +export async function initializeSuperMemory() { + const settings = extension_settings[extensionName] || {}; + if (settings.super_memory_enabled === false) { + console.log('[Amily2-SuperMemory] 功能已禁用 (super_memory_enabled = false)。'); + if (window.$) $('#sm-system-status').text('已禁用').css('color', 'gray'); + return; + } + + if (isInitialized) return; + console.log('[Amily2-SuperMemory] 初始化核心管理器...'); + + if (!amilyHelper) { + console.error('[Amily2-SuperMemory] 致命错误:AmilyHelper 未就绪。'); + return; + } + + document.addEventListener('AMILY2_TABLE_UPDATED', handleTableUpdate); + + eventSource.on(event_types.CHAT_CHANGED, async () => { + const settings = extension_settings[extensionName] || {}; + if (settings.super_memory_enabled === false) return; + + console.log('[Amily2-SuperMemory] 检测到聊天切换,正在刷新记忆状态...'); + await checkWorldBookStatus(); + + await tryRestoreStateFromMetadata(); + + await forceSyncAll(); + }); + + await checkWorldBookStatus(); + + await tryRestoreStateFromMetadata(); + + await forceSyncAll(); + + isInitialized = true; + console.log('[Amily2-SuperMemory] 核心管理器初始化完成。'); + + if (window.$) { + $('#sm-system-status').text('运行中').css('color', '#4caf50'); + } +} + +async function checkWorldBookStatus() { + try { + await ensureMemoryBook(); + } catch (error) { + console.error('[Amily2-SuperMemory] 检查世界书状态失败:', error); + } +} + +function handleTableUpdate(event) { + const settings = extension_settings[extensionName] || {}; + if (settings.super_memory_enabled === false) return; + + const { tableName, data, role, hint, headers, rowStatuses } = event.detail; + console.log(`[Amily2-SuperMemory] 检测到表格更新: ${tableName} (Role: ${role})`); + + updateQueue.push({ tableName, data, role, hint, headers, rowStatuses }); + processQueue(); +} + +async function processQueue() { + if (isProcessing || updateQueue.length === 0) return; + isProcessing = true; + + try { + while (updateQueue.length > 0) { + const task = updateQueue.shift(); + await processUpdateTask(task); + } + + await saveStateToMetadata(); + + } catch (error) { + console.error('[Amily2-SuperMemory] 处理更新队列失败:', error); + } finally { + isProcessing = false; + } +} + +async function processUpdateTask(task) { + const { tableName, data, role, hint, headers, rowStatuses } = task; + + const activeData = data.filter((_, i) => !rowStatuses || rowStatuses[i] !== 'pending-deletion'); + const indexText = generateIndex(activeData, role, tableName); + + const allTables = getMemoryState(); + const tableIndex = allTables.findIndex(t => t.name === tableName); + const depth = 8001 + (tableIndex >= 0 ? tableIndex : 99); + + await syncToLorebook(tableName, data, indexText, role, headers, rowStatuses, depth); + + if (hint) { + console.log(`[Amily2-SuperMemory] 应用主动记忆提示: ${hint}`); + await updateTransientHint(hint); + } + + console.log(`[Amily2-SuperMemory] 任务完成: ${tableName}`); + + updateDashboardCounters(); +} + +async function saveStateToMetadata() { + const context = getContext(); + if (!context.chat || context.chat.length === 0) return; + + const lastMsgIndex = context.chat.length - 1; + const lastMsg = context.chat[lastMsgIndex]; + + const currentState = getMemoryState(); + + if (!lastMsg.metadata) lastMsg.metadata = {}; + + lastMsg.metadata[METADATA_KEY] = JSON.parse(JSON.stringify(currentState)); + + if (context.saveChat) { + await context.saveChat(); + } + + console.log(`[Amily2-SuperMemory] 状态已保存至消息 #${lastMsgIndex}`); +} + +export async function tryRestoreStateFromMetadata() { + const context = getContext(); + if (!context.chat || context.chat.length === 0) return; + + let foundState = null; + let foundIndex = -1; + + for (let i = context.chat.length - 1; i >= 0; i--) { + const msg = context.chat[i]; + if (msg.metadata && msg.metadata[METADATA_KEY]) { + foundState = msg.metadata[METADATA_KEY]; + foundIndex = i; + break; + } + } + + if (foundState) { + console.log(`[Amily2-SuperMemory] 发现历史状态 (Msg #${foundIndex}),正在恢复...`); + if (typeof loadMemoryState === 'function') { + loadMemoryState(foundState); + await forceSyncAll(); + } else { + console.warn('[Amily2-SuperMemory] table-system 缺少 loadMemoryState 方法,无法恢复状态。'); + } + } else { + console.log('[Amily2-SuperMemory] 未在聊天记录中发现历史状态,使用默认/当前状态。'); + } +} + +function updateDashboardCounters() { + const tables = getMemoryState(); + if (tables && window.$) { + $('#sm-index-count').text(`${tables.length} 个索引`); + const totalRows = tables.reduce((acc, t) => acc + (t.rows ? t.rows.length : 0), 0); + $('#sm-detail-count').text(`${totalRows} 个详情`); + } +} + +export async function forceSyncAll() { + console.log('[Amily2-SuperMemory] 正在执行全量同步...'); + const tables = getMemoryState(); + + if (!tables || tables.length === 0) { + console.warn('[Amily2-SuperMemory] 没有可同步的表格数据。'); + return; + } + + for (const table of tables) { + let role = 'database'; + if (table.name.includes('时空') || table.name.includes('世界钟')) role = 'anchor'; + if (table.name.includes('日志') || table.name.includes('Log')) role = 'log'; + + updateQueue.push({ + tableName: table.name, + data: table.rows, + headers: table.headers, + rowStatuses: table.rowStatuses || [], + role: role + }); + } + + await processQueue(); + console.log('[Amily2-SuperMemory] 全量同步完成。'); +} diff --git a/super-memory/smart-indexer.js b/super-memory/smart-indexer.js new file mode 100644 index 0000000..57cd6a7 --- /dev/null +++ b/super-memory/smart-indexer.js @@ -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; +}