mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 16:15:50 +00:00
Add files via upload
This commit is contained in:
89
core/super-memory/bindings.js
Normal file
89
core/super-memory/bindings.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
85
core/super-memory/index.html
Normal file
85
core/super-memory/index.html
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<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>
|
||||||
|
<div class="sm-control-block">
|
||||||
|
<label>索引注入深度 (@Depth):</label>
|
||||||
|
<input type="number" id="sm-index-depth" class="sm-input" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="sm-control-block">
|
||||||
|
<label>详情注入深度 (@Depth):</label>
|
||||||
|
<input type="number" id="sm-detail-depth" class="sm-input" value="2">
|
||||||
|
</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>
|
||||||
228
core/super-memory/lorebook-bridge.js
Normal file
228
core/super-memory/lorebook-bridge.js
Normal file
@@ -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<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 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<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) {
|
||||||
|
if(row && row[0]) activeKeys.add(row[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.comment && entry.comment.startsWith(tablePrefix)) {
|
||||||
|
const entryKey = entry.comment.substring(tablePrefix.length).trim();
|
||||||
|
if (!activeKeys.has(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 ? '启用' : '清除'}。`);
|
||||||
|
}
|
||||||
203
core/super-memory/manager.js
Normal file
203
core/super-memory/manager.js
Normal file
@@ -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] 全量同步完成。');
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user