mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 09:15:50 +00:00
ci: auto build & obfuscate [2026-04-06 00:50:28] (Jenkins #7)
This commit is contained in:
58
core/super-memory/SuperMemoryService.js
Normal file
58
core/super-memory/SuperMemoryService.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* SuperMemoryService
|
||||
* 超级记忆 Bus 服务 — 统一对外入口
|
||||
*
|
||||
* 职责:
|
||||
* 1. 将 super-memory/manager.js 的能力通过 Amily2Bus 暴露给其他模块
|
||||
* 2. 向后兼容:保留具名导出,现有直接 import 无需立即修改
|
||||
*
|
||||
* Bus 注册名:'SuperMemory'
|
||||
*
|
||||
* 公开接口(query('SuperMemory')):
|
||||
* initialize() — 初始化超级记忆系统
|
||||
* forceSyncAll() — 全量同步到世界书
|
||||
* tryRestoreStateFromMetadata() — 从聊天元数据恢复状态
|
||||
* awaitSync() — 等待当前同步队列完成(Pipeline Stage 4 使用)
|
||||
* purge() — 清空记忆世界书
|
||||
*/
|
||||
|
||||
import {
|
||||
initializeSuperMemory,
|
||||
tryRestoreStateFromMetadata,
|
||||
forceSyncAll,
|
||||
awaitSync,
|
||||
purgeSuperMemory,
|
||||
pushUpdate,
|
||||
} from './manager.js';
|
||||
|
||||
// ── Bus 注册 ──────────────────────────────────────────────────────────────
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const _ctx = window.Amily2Bus?.register('SuperMemory');
|
||||
if (!_ctx) {
|
||||
console.warn('[SuperMemory] Amily2Bus 尚未就绪,服务注册跳过。');
|
||||
return;
|
||||
}
|
||||
_ctx.expose({
|
||||
initialize: () => initializeSuperMemory(),
|
||||
forceSyncAll: () => forceSyncAll(),
|
||||
tryRestoreStateFromMetadata: () => tryRestoreStateFromMetadata(),
|
||||
awaitSync: () => awaitSync(),
|
||||
purge: () => purgeSuperMemory(),
|
||||
pushUpdate: (payload) => pushUpdate(payload),
|
||||
});
|
||||
_ctx.log('SuperMemoryService', 'info', 'SuperMemory 服务已注册到 Bus。');
|
||||
} catch (e) {
|
||||
console.error('[SuperMemory] Bus 注册失败:', e);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// ── 向后兼容具名导出 ──────────────────────────────────────────────────────
|
||||
export {
|
||||
initializeSuperMemory,
|
||||
tryRestoreStateFromMetadata,
|
||||
forceSyncAll,
|
||||
awaitSync,
|
||||
purgeSuperMemory,
|
||||
pushUpdate,
|
||||
};
|
||||
@@ -1,210 +1,222 @@
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { extension_settings } from "/scripts/extensions.js";
|
||||
import { saveSettingsDebounced } from "/script.js";
|
||||
import { initializeSuperMemory, purgeSuperMemory } from "./manager.js";
|
||||
import { defaultSettings as ragDefaultSettings } from "../rag-settings.js";
|
||||
import { getMemoryState } from "../table-system/manager.js";
|
||||
|
||||
const RAG_MODULE_NAME = 'hanlinyuan-rag-core';
|
||||
|
||||
function getRagSettings() {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
if (!extension_settings[extensionName][RAG_MODULE_NAME]) {
|
||||
extension_settings[extensionName][RAG_MODULE_NAME] = structuredClone(ragDefaultSettings);
|
||||
}
|
||||
return extension_settings[extensionName][RAG_MODULE_NAME];
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
// 处理 Checkbox 变更
|
||||
panel.on('change', 'input[type="checkbox"]', function() {
|
||||
if ($(this).hasClass('sm-table-setting-check')) return; // Skip table settings checks here
|
||||
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
|
||||
const id = this.id;
|
||||
|
||||
// Super Memory 自身设置
|
||||
if (id === 'sm-system-enabled') {
|
||||
extension_settings[extensionName]['super_memory_enabled'] = this.checked;
|
||||
saveSettingsDebounced();
|
||||
return;
|
||||
}
|
||||
if (id === 'sm-bridge-enabled') {
|
||||
extension_settings[extensionName]['superMemory_bridgeEnabled'] = this.checked;
|
||||
saveSettingsDebounced();
|
||||
return;
|
||||
}
|
||||
|
||||
// RAG 设置 (归档 & 关联图谱)
|
||||
const ragSettings = getRagSettings();
|
||||
|
||||
if (id === 'sm-archive-enabled') {
|
||||
if (!ragSettings.archive) ragSettings.archive = {};
|
||||
ragSettings.archive.enabled = this.checked;
|
||||
}
|
||||
else if (id === 'sm-relationship-graph-enabled') {
|
||||
if (!ragSettings.relationshipGraph) ragSettings.relationshipGraph = {};
|
||||
ragSettings.relationshipGraph.enabled = this.checked;
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
console.log(`[Amily2-SuperMemory] Checkbox updated: ${id} = ${this.checked}`);
|
||||
});
|
||||
|
||||
// 处理 Input 变更 (归档阈值等)
|
||||
panel.on('change', 'input[type="number"], input[type="text"]', function() {
|
||||
const id = this.id;
|
||||
const ragSettings = getRagSettings();
|
||||
if (!ragSettings.archive) ragSettings.archive = {};
|
||||
|
||||
if (id === 'sm-archive-threshold') {
|
||||
ragSettings.archive.threshold = parseInt(this.value, 10);
|
||||
}
|
||||
else if (id === 'sm-archive-batch-size') {
|
||||
ragSettings.archive.batchSize = parseInt(this.value, 10);
|
||||
}
|
||||
else if (id === 'sm-archive-target-table') {
|
||||
ragSettings.archive.targetTable = this.value;
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
console.log(`[Amily2-SuperMemory] Input updated: ${id} = ${this.value}`);
|
||||
});
|
||||
|
||||
// 绑定刷新表格列表按钮
|
||||
panel.on('click', '#sm-refresh-table-list', function() {
|
||||
renderTableSettingsList();
|
||||
});
|
||||
|
||||
// 绑定表格专属配置的 Checkbox
|
||||
panel.on('change', '.sm-table-setting-check', function() {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
if (!extension_settings[extensionName].superMemory_tableSettings) {
|
||||
extension_settings[extensionName].superMemory_tableSettings = {};
|
||||
}
|
||||
|
||||
const tableName = $(this).data('table');
|
||||
const type = $(this).data('type'); // 'sync' or 'constant'
|
||||
const checked = this.checked;
|
||||
|
||||
if (!extension_settings[extensionName].superMemory_tableSettings[tableName]) {
|
||||
extension_settings[extensionName].superMemory_tableSettings[tableName] = {};
|
||||
}
|
||||
|
||||
extension_settings[extensionName].superMemory_tableSettings[tableName][type] = checked;
|
||||
saveSettingsDebounced();
|
||||
console.log(`[Amily2-SuperMemory] Table setting updated: ${tableName}.${type} = ${checked}`);
|
||||
});
|
||||
|
||||
loadSuperMemorySettings();
|
||||
|
||||
console.log('[Amily2-SuperMemory] Events bound successfully.');
|
||||
}
|
||||
|
||||
function renderTableSettingsList() {
|
||||
const container = $('#sm-table-settings-list');
|
||||
container.html('<div style="text-align: center; color: #888; padding: 20px;">正在加载...</div>');
|
||||
|
||||
const tables = getMemoryState();
|
||||
if (!tables || tables.length === 0) {
|
||||
container.html('<div style="text-align: center; color: #888; padding: 20px;">暂无表格数据。请先在聊天中使用表格功能。</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = extension_settings[extensionName]?.superMemory_tableSettings || {};
|
||||
|
||||
let html = '';
|
||||
tables.forEach(table => {
|
||||
const tableName = table.name;
|
||||
const tableConfig = settings[tableName] || {};
|
||||
|
||||
// Default values: Sync=True, Constant=True
|
||||
const isSyncEnabled = tableConfig.sync !== false;
|
||||
const isConstant = tableConfig.constant !== false;
|
||||
|
||||
html += `
|
||||
<div class="sm-control-block" style="border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px; margin-bottom: 10px;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px; color: #e0e0e0;">${tableName}</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<label class="sm-toggle-switch" style="transform: scale(0.8); margin-right: 5px;">
|
||||
<input type="checkbox" class="sm-table-setting-check" data-table="${tableName}" data-type="sync" ${isSyncEnabled ? 'checked' : ''}>
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
<span style="font-size: 0.9em; color: #ccc;">写入世界书</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<label class="sm-toggle-switch" style="transform: scale(0.8); margin-right: 5px;">
|
||||
<input type="checkbox" class="sm-table-setting-check" data-table="${tableName}" data-type="constant" ${isConstant ? 'checked' : ''}>
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
<span style="font-size: 0.9em; color: #ccc;">索引绿灯(常驻)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.html(html);
|
||||
}
|
||||
|
||||
function loadSuperMemorySettings() {
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
const ragSettings = getRagSettings();
|
||||
|
||||
// Super Memory 设置
|
||||
$('#sm-system-enabled').prop('checked', settings.super_memory_enabled ?? false);
|
||||
$('#sm-bridge-enabled').prop('checked', settings.superMemory_bridgeEnabled ?? false);
|
||||
|
||||
// 归档设置
|
||||
if (ragSettings.archive) {
|
||||
$('#sm-archive-enabled').prop('checked', ragSettings.archive.enabled ?? false);
|
||||
$('#sm-archive-threshold').val(ragSettings.archive.threshold ?? 20);
|
||||
$('#sm-archive-batch-size').val(ragSettings.archive.batchSize ?? 10);
|
||||
$('#sm-archive-target-table').val(ragSettings.archive.targetTable ?? '总结表');
|
||||
}
|
||||
|
||||
// 关联图谱设置
|
||||
if (ragSettings.relationshipGraph) {
|
||||
$('#sm-relationship-graph-enabled').prop('checked', ragSettings.relationshipGraph.enabled ?? false);
|
||||
}
|
||||
|
||||
// 渲染表格列表
|
||||
renderTableSettingsList();
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
||||
import { extensionName } from "../../utils/settings.js";
|
||||
import { extension_settings } from "/scripts/extensions.js";
|
||||
import { saveSettingsDebounced } from "/script.js";
|
||||
import { initializeSuperMemory, purgeSuperMemory } from "./manager.js";
|
||||
import { defaultSettings as ragDefaultSettings } from "../rag-settings.js";
|
||||
import { getMemoryState } from "../table-system/manager.js";
|
||||
|
||||
const RAG_MODULE_NAME = 'hanlinyuan-rag-core';
|
||||
|
||||
function getRagSettings() {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
if (!extension_settings[extensionName][RAG_MODULE_NAME]) {
|
||||
extension_settings[extensionName][RAG_MODULE_NAME] = structuredClone(ragDefaultSettings);
|
||||
}
|
||||
return extension_settings[extensionName][RAG_MODULE_NAME];
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
// 处理 Checkbox 变更
|
||||
panel.on('change', 'input[type="checkbox"]', function() {
|
||||
if ($(this).hasClass('sm-table-setting-check')) return; // Skip table settings checks here
|
||||
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
|
||||
const id = this.id;
|
||||
|
||||
// Super Memory 自身设置
|
||||
if (id === 'sm-system-enabled') {
|
||||
extension_settings[extensionName]['super_memory_enabled'] = this.checked;
|
||||
saveSettingsDebounced();
|
||||
return;
|
||||
}
|
||||
if (id === 'sm-bridge-enabled') {
|
||||
extension_settings[extensionName]['superMemory_bridgeEnabled'] = this.checked;
|
||||
saveSettingsDebounced();
|
||||
return;
|
||||
}
|
||||
|
||||
// RAG 设置 (归档 & 关联图谱)
|
||||
const ragSettings = getRagSettings();
|
||||
|
||||
if (id === 'sm-archive-enabled') {
|
||||
if (!ragSettings.archive) ragSettings.archive = {};
|
||||
ragSettings.archive.enabled = this.checked;
|
||||
}
|
||||
else if (id === 'sm-relationship-graph-enabled') {
|
||||
if (!ragSettings.relationshipGraph) ragSettings.relationshipGraph = {};
|
||||
ragSettings.relationshipGraph.enabled = this.checked;
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
console.log(`[Amily2-SuperMemory] Checkbox updated: ${id} = ${this.checked}`);
|
||||
});
|
||||
|
||||
// 处理 Input 变更 (归档阈值等)
|
||||
panel.on('change', 'input[type="number"], input[type="text"]', function() {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
const id = this.id;
|
||||
|
||||
// SuperMemory 自身设置
|
||||
if (id === 'sm-min-trigger-floor') {
|
||||
extension_settings[extensionName]['superMemory_minTriggerFloor'] = Math.max(0, parseInt(this.value, 10) || 0);
|
||||
saveSettingsDebounced();
|
||||
console.log(`[Amily2-SuperMemory] Input updated: ${id} = ${this.value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// RAG 归档设置
|
||||
const ragSettings = getRagSettings();
|
||||
if (!ragSettings.archive) ragSettings.archive = {};
|
||||
|
||||
if (id === 'sm-archive-threshold') {
|
||||
ragSettings.archive.threshold = parseInt(this.value, 10);
|
||||
}
|
||||
else if (id === 'sm-archive-batch-size') {
|
||||
ragSettings.archive.batchSize = parseInt(this.value, 10);
|
||||
}
|
||||
else if (id === 'sm-archive-target-table') {
|
||||
ragSettings.archive.targetTable = this.value;
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
console.log(`[Amily2-SuperMemory] Input updated: ${id} = ${this.value}`);
|
||||
});
|
||||
|
||||
// 绑定刷新表格列表按钮
|
||||
panel.on('click', '#sm-refresh-table-list', function() {
|
||||
renderTableSettingsList();
|
||||
});
|
||||
|
||||
// 绑定表格专属配置的 Checkbox
|
||||
panel.on('change', '.sm-table-setting-check', function() {
|
||||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||||
if (!extension_settings[extensionName].superMemory_tableSettings) {
|
||||
extension_settings[extensionName].superMemory_tableSettings = {};
|
||||
}
|
||||
|
||||
const tableName = $(this).data('table');
|
||||
const type = $(this).data('type'); // 'sync' or 'constant'
|
||||
const checked = this.checked;
|
||||
|
||||
if (!extension_settings[extensionName].superMemory_tableSettings[tableName]) {
|
||||
extension_settings[extensionName].superMemory_tableSettings[tableName] = {};
|
||||
}
|
||||
|
||||
extension_settings[extensionName].superMemory_tableSettings[tableName][type] = checked;
|
||||
saveSettingsDebounced();
|
||||
console.log(`[Amily2-SuperMemory] Table setting updated: ${tableName}.${type} = ${checked}`);
|
||||
});
|
||||
|
||||
loadSuperMemorySettings();
|
||||
|
||||
console.log('[Amily2-SuperMemory] Events bound successfully.');
|
||||
}
|
||||
|
||||
function renderTableSettingsList() {
|
||||
const container = $('#sm-table-settings-list');
|
||||
container.html('<div style="text-align: center; color: #888; padding: 20px;">正在加载...</div>');
|
||||
|
||||
const tables = getMemoryState();
|
||||
if (!tables || tables.length === 0) {
|
||||
container.html('<div style="text-align: center; color: #888; padding: 20px;">暂无表格数据。请先在聊天中使用表格功能。</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = extension_settings[extensionName]?.superMemory_tableSettings || {};
|
||||
|
||||
let html = '';
|
||||
tables.forEach(table => {
|
||||
const tableName = table.name;
|
||||
const tableConfig = settings[tableName] || {};
|
||||
|
||||
// Default values: Sync=True, Constant=True
|
||||
const isSyncEnabled = tableConfig.sync !== false;
|
||||
const isConstant = tableConfig.constant !== false;
|
||||
|
||||
html += `
|
||||
<div class="sm-control-block" style="border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px; margin-bottom: 10px;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px; color: #e0e0e0;">${tableName}</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<label class="sm-toggle-switch" style="transform: scale(0.8); margin-right: 5px;">
|
||||
<input type="checkbox" class="sm-table-setting-check" data-table="${tableName}" data-type="sync" ${isSyncEnabled ? 'checked' : ''}>
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
<span style="font-size: 0.9em; color: #ccc;">写入世界书</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<label class="sm-toggle-switch" style="transform: scale(0.8); margin-right: 5px;">
|
||||
<input type="checkbox" class="sm-table-setting-check" data-table="${tableName}" data-type="constant" ${isConstant ? 'checked' : ''}>
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
<span style="font-size: 0.9em; color: #ccc;">索引绿灯(常驻)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.html(html);
|
||||
}
|
||||
|
||||
function loadSuperMemorySettings() {
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
const ragSettings = getRagSettings();
|
||||
|
||||
// Super Memory 设置
|
||||
$('#sm-system-enabled').prop('checked', settings.super_memory_enabled ?? false);
|
||||
$('#sm-bridge-enabled').prop('checked', settings.superMemory_bridgeEnabled ?? false);
|
||||
$('#sm-min-trigger-floor').val(settings.superMemory_minTriggerFloor ?? 0);
|
||||
|
||||
// 归档设置
|
||||
if (ragSettings.archive) {
|
||||
$('#sm-archive-enabled').prop('checked', ragSettings.archive.enabled ?? false);
|
||||
$('#sm-archive-threshold').val(ragSettings.archive.threshold ?? 20);
|
||||
$('#sm-archive-batch-size').val(ragSettings.archive.batchSize ?? 10);
|
||||
$('#sm-archive-target-table').val(ragSettings.archive.targetTable ?? '总结表');
|
||||
}
|
||||
|
||||
// 关联图谱设置
|
||||
if (ragSettings.relationshipGraph) {
|
||||
$('#sm-relationship-graph-enabled').prop('checked', ragSettings.relationshipGraph.enabled ?? false);
|
||||
}
|
||||
|
||||
// 渲染表格列表
|
||||
renderTableSettingsList();
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,122 +1,129 @@
|
||||
<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>
|
||||
|
||||
<fieldset class="sm-settings-group">
|
||||
<legend><i class="fas fa-list-alt"></i> 表格专属配置</legend>
|
||||
<div class="sm-control-block" style="display: block;">
|
||||
<p style="font-size: 0.9em; color: #aaa; margin-bottom: 10px;">在此处配置特定表格的同步策略。</p>
|
||||
<div id="sm-table-settings-list" style="max-height: 300px; overflow-y: auto; padding-right: 5px;">
|
||||
<!-- Table items will be injected here -->
|
||||
<div style="text-align: center; color: #888; padding: 20px;">正在加载表格列表...</div>
|
||||
</div>
|
||||
<button id="sm-refresh-table-list" class="sm-action-button secondary" style="width: 100%; margin-top: 10px;">刷新表格列表</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="sm-settings-group">
|
||||
<legend><i class="fas fa-archive"></i> 历史归档配置</legend>
|
||||
<div class="sm-control-block">
|
||||
<label>启用自动归档:</label>
|
||||
<label class="sm-toggle-switch">
|
||||
<input type="checkbox" id="sm-archive-enabled">
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sm-control-block">
|
||||
<label>触发阈值 (行数):</label>
|
||||
<input type="number" id="sm-archive-threshold" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="20">
|
||||
</div>
|
||||
<div class="sm-control-block">
|
||||
<label title="每次触发归档时,一次性迁移的行数。">归档批次 (行数):</label>
|
||||
<input type="number" id="sm-archive-batch-size" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="10">
|
||||
</div>
|
||||
<small style="color: #888; font-size: 0.8em; display: block; margin-top: -5px; margin-bottom: 10px; padding-left: 5px;">
|
||||
阈值是 20,批次是 10。当表格达到 21 行时,会把最早的 10 行向量化,表格与世界书剩下 11 条。
|
||||
</small>
|
||||
<div class="sm-control-block">
|
||||
<label>目标表格名称:</label>
|
||||
<input type="text" id="sm-archive-target-table" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="总结表">
|
||||
</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>
|
||||
<div class="sm-control-block">
|
||||
<label>启用角色关联图谱:</label>
|
||||
<label class="sm-toggle-switch">
|
||||
<input type="checkbox" id="sm-relationship-graph-enabled">
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p>关联触发逻辑正在开发中...</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 title="聊天消息数低于此值时,跳过记忆同步。表格未填写时同步是无意义的,设置合理的楼层数可以节省 Token。0 = 不限制。">最低触发楼层:</label>
|
||||
<input type="number" id="sm-min-trigger-floor" min="0" step="1" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px; width: 80px;" value="0">
|
||||
</div>
|
||||
<small style="color: #888; font-size: 0.8em; display: block; margin-top: -5px; margin-bottom: 10px; padding-left: 5px;">
|
||||
聊天楼层低于此数值时不触发记忆同步,避免表格空白期浪费 Token。设为 0 则不限制。
|
||||
</small>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="sm-settings-group">
|
||||
<legend><i class="fas fa-list-alt"></i> 表格专属配置</legend>
|
||||
<div class="sm-control-block" style="display: block;">
|
||||
<p style="font-size: 0.9em; color: #aaa; margin-bottom: 10px;">在此处配置特定表格的同步策略。</p>
|
||||
<div id="sm-table-settings-list" style="max-height: 300px; overflow-y: auto; padding-right: 5px;">
|
||||
<!-- Table items will be injected here -->
|
||||
<div style="text-align: center; color: #888; padding: 20px;">正在加载表格列表...</div>
|
||||
</div>
|
||||
<button id="sm-refresh-table-list" class="sm-action-button secondary" style="width: 100%; margin-top: 10px;">刷新表格列表</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="sm-settings-group">
|
||||
<legend><i class="fas fa-archive"></i> 历史归档配置</legend>
|
||||
<div class="sm-control-block">
|
||||
<label>启用自动归档:</label>
|
||||
<label class="sm-toggle-switch">
|
||||
<input type="checkbox" id="sm-archive-enabled">
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sm-control-block">
|
||||
<label>触发阈值 (行数):</label>
|
||||
<input type="number" id="sm-archive-threshold" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="20">
|
||||
</div>
|
||||
<div class="sm-control-block">
|
||||
<label title="每次触发归档时,一次性迁移的行数。">归档批次 (行数):</label>
|
||||
<input type="number" id="sm-archive-batch-size" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="10">
|
||||
</div>
|
||||
<small style="color: #888; font-size: 0.8em; display: block; margin-top: -5px; margin-bottom: 10px; padding-left: 5px;">
|
||||
阈值是 20,批次是 10。当表格达到 21 行时,会把最早的 10 行向量化,表格与世界书剩下 11 条。
|
||||
</small>
|
||||
<div class="sm-control-block">
|
||||
<label>目标表格名称:</label>
|
||||
<input type="text" id="sm-archive-target-table" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="总结表">
|
||||
</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>
|
||||
<div class="sm-control-block">
|
||||
<label>启用角色关联图谱:</label>
|
||||
<label class="sm-toggle-switch">
|
||||
<input type="checkbox" id="sm-relationship-graph-enabled">
|
||||
<span class="sm-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p>关联触发逻辑正在开发中...</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,289 +1,293 @@
|
||||
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, isIndexConstant = true) {
|
||||
console.log(`[Amily2-Bridge] 开始同步表格: ${tableName} (Depth: ${depth}, IndexConstant: ${isIndexConstant})`);
|
||||
|
||||
await ensureMemoryBook();
|
||||
|
||||
const bookName = getMemoryBookName();
|
||||
|
||||
let entries = await amilyHelper.getLorebookEntries(bookName);
|
||||
if (!entries) entries = [];
|
||||
|
||||
const entriesToUpdate = [];
|
||||
const entriesToCreate = [];
|
||||
|
||||
const arraysEqual = (a, b) => {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
const sA = [...a].sort();
|
||||
const sB = [...b].sort();
|
||||
return sA.every((val, index) => val === sB[index]);
|
||||
};
|
||||
|
||||
const processEntry = (comment, keys, content, type = 'selective', enabled = true, excludeRecursion = false, specificOrder = null, specificDepth = null) => {
|
||||
const existingEntry = entries.find(e => e.comment === comment);
|
||||
if (existingEntry) {
|
||||
let isChanged = false;
|
||||
|
||||
if (existingEntry.content !== content) isChanged = true;
|
||||
if (!arraysEqual(existingEntry.key, keys)) isChanged = true;
|
||||
if (existingEntry.enabled !== enabled) isChanged = true;
|
||||
|
||||
const shouldBeConstant = (type === 'constant');
|
||||
if (!!existingEntry.constant !== shouldBeConstant) isChanged = true;
|
||||
|
||||
if (!!existingEntry.exclude_recursion !== excludeRecursion) isChanged = true;
|
||||
|
||||
if (specificOrder !== null && existingEntry.order !== specificOrder) isChanged = true;
|
||||
if (specificDepth !== null && existingEntry.depth !== specificDepth) isChanged = true;
|
||||
|
||||
if (isChanged) {
|
||||
existingEntry.content = content;
|
||||
existingEntry.key = keys;
|
||||
|
||||
existingEntry.exclude_recursion = excludeRecursion;
|
||||
existingEntry.prevent_recursion = excludeRecursion;
|
||||
existingEntry.excludeRecursion = excludeRecursion;
|
||||
existingEntry.preventRecursion = excludeRecursion;
|
||||
|
||||
if (specificOrder !== null) {
|
||||
existingEntry.order = specificOrder;
|
||||
existingEntry.position = 4;
|
||||
}
|
||||
if (specificDepth !== null) {
|
||||
existingEntry.depth = specificDepth;
|
||||
}
|
||||
|
||||
if (type === 'constant') {
|
||||
existingEntry.constant = true;
|
||||
} else {
|
||||
existingEntry.constant = false;
|
||||
}
|
||||
existingEntry.enabled = enabled;
|
||||
delete existingEntry.disable;
|
||||
delete existingEntry.disabled;
|
||||
|
||||
entriesToUpdate.push(existingEntry);
|
||||
}
|
||||
} else {
|
||||
entriesToCreate.push({
|
||||
comment: comment,
|
||||
keys: keys,
|
||||
content: content,
|
||||
type: type,
|
||||
position: 4,
|
||||
order: specificOrder !== null ? specificOrder : depth,
|
||||
depth: specificDepth !== null ? specificDepth : depth,
|
||||
enabled: enabled,
|
||||
exclude_recursion: excludeRecursion,
|
||||
prevent_recursion: excludeRecursion,
|
||||
excludeRecursion: excludeRecursion,
|
||||
preventRecursion: excludeRecursion
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
const indexType = isIndexConstant ? 'constant' : 'selective';
|
||||
processEntry(indexComment, indexKey, indexContent, indexType, true, true, 0, 0);
|
||||
}
|
||||
|
||||
data.forEach((row, index) => {
|
||||
if (!row || row.length === 0) return;
|
||||
|
||||
const rawVal = row[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) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (entriesToDelete.length === 0 && entriesToUpdate.length === 0 && entriesToCreate.length === 0) {
|
||||
console.log(`[Amily2-Bridge] ${tableName} 无需变更 (数据一致)。`);
|
||||
}
|
||||
|
||||
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 ? '启用' : '清除'}。`);
|
||||
}
|
||||
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";
|
||||
import { withLoreLock } from "../lore-service.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}`;
|
||||
}
|
||||
|
||||
/** 无锁内核:在已持有写锁时调用(避免嵌套死锁) */
|
||||
async function _doEnsureBook(bookName) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncToLorebook(tableName, data, indexText, role, headers, rowStatuses, depth = 100, isIndexConstant = true) {
|
||||
console.log(`[Amily2-Bridge] 开始同步表格: ${tableName} (Depth: ${depth}, IndexConstant: ${isIndexConstant})`);
|
||||
return withLoreLock(`syncToLorebook(${tableName})`, async () => {
|
||||
await _doEnsureBook(getMemoryBookName());
|
||||
|
||||
const bookName = getMemoryBookName();
|
||||
|
||||
let entries = await amilyHelper.getLorebookEntries(bookName);
|
||||
if (!entries) entries = [];
|
||||
|
||||
const entriesToUpdate = [];
|
||||
const entriesToCreate = [];
|
||||
|
||||
const arraysEqual = (a, b) => {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
const sA = [...a].sort();
|
||||
const sB = [...b].sort();
|
||||
return sA.every((val, index) => val === sB[index]);
|
||||
};
|
||||
|
||||
const processEntry = (comment, keys, content, type = 'selective', enabled = true, excludeRecursion = false, specificOrder = null, specificDepth = null) => {
|
||||
const existingEntry = entries.find(e => e.comment === comment);
|
||||
if (existingEntry) {
|
||||
let isChanged = false;
|
||||
|
||||
if (existingEntry.content !== content) isChanged = true;
|
||||
if (!arraysEqual(existingEntry.key, keys)) isChanged = true;
|
||||
if (existingEntry.enabled !== enabled) isChanged = true;
|
||||
|
||||
const shouldBeConstant = (type === 'constant');
|
||||
if (!!existingEntry.constant !== shouldBeConstant) isChanged = true;
|
||||
|
||||
if (!!existingEntry.exclude_recursion !== excludeRecursion) isChanged = true;
|
||||
|
||||
if (specificOrder !== null && existingEntry.order !== specificOrder) isChanged = true;
|
||||
if (specificDepth !== null && existingEntry.depth !== specificDepth) isChanged = true;
|
||||
|
||||
if (isChanged) {
|
||||
existingEntry.content = content;
|
||||
existingEntry.key = keys;
|
||||
|
||||
existingEntry.exclude_recursion = excludeRecursion;
|
||||
existingEntry.prevent_recursion = excludeRecursion;
|
||||
existingEntry.excludeRecursion = excludeRecursion;
|
||||
existingEntry.preventRecursion = excludeRecursion;
|
||||
|
||||
if (specificOrder !== null) {
|
||||
existingEntry.order = specificOrder;
|
||||
existingEntry.position = 4;
|
||||
}
|
||||
if (specificDepth !== null) {
|
||||
existingEntry.depth = specificDepth;
|
||||
}
|
||||
|
||||
if (type === 'constant') {
|
||||
existingEntry.constant = true;
|
||||
} else {
|
||||
existingEntry.constant = false;
|
||||
}
|
||||
existingEntry.enabled = enabled;
|
||||
delete existingEntry.disable;
|
||||
delete existingEntry.disabled;
|
||||
|
||||
entriesToUpdate.push(existingEntry);
|
||||
}
|
||||
} else {
|
||||
entriesToCreate.push({
|
||||
comment: comment,
|
||||
keys: keys,
|
||||
content: content,
|
||||
type: type,
|
||||
position: 4,
|
||||
order: specificOrder !== null ? specificOrder : depth,
|
||||
depth: specificDepth !== null ? specificDepth : depth,
|
||||
enabled: enabled,
|
||||
exclude_recursion: excludeRecursion,
|
||||
prevent_recursion: excludeRecursion,
|
||||
excludeRecursion: excludeRecursion,
|
||||
preventRecursion: excludeRecursion
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
const indexType = isIndexConstant ? 'constant' : 'selective';
|
||||
processEntry(indexComment, indexKey, indexContent, indexType, true, true, 0, 0);
|
||||
}
|
||||
|
||||
data.forEach((row, index) => {
|
||||
if (!row || row.length === 0) return;
|
||||
|
||||
const rawVal = row[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) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (entriesToDelete.length === 0 && entriesToUpdate.length === 0 && entriesToCreate.length === 0) {
|
||||
console.log(`[Amily2-Bridge] ${tableName} 无需变更 (数据一致)。`);
|
||||
}
|
||||
|
||||
console.log(`[Amily2-Bridge] 同步完成: ${tableName}`);
|
||||
}); // end withLoreLock
|
||||
}
|
||||
|
||||
export async function ensureMemoryBook() {
|
||||
const bookName = getMemoryBookName();
|
||||
return withLoreLock(`ensureMemoryBook(${bookName})`, () => _doEnsureBook(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,276 +1,319 @@
|
||||
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, getMemoryBookName } 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 userType = parseInt(localStorage.getItem("plugin_user_type") || "0");
|
||||
if (userType < 2) {
|
||||
console.warn('[Amily2-SuperMemory] 权限不足 (Type < 2),拒绝初始化超级记忆系统。');
|
||||
if (window.$) $('#sm-system-status').text('未授权').css('color', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (window.$) $('#sm-system-status').text('运行中').css('color', '#4caf50');
|
||||
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 consolidatedTasks = new Map();
|
||||
const currentBatch = [...updateQueue];
|
||||
updateQueue.length = 0; // 清空队列
|
||||
|
||||
for (const task of currentBatch) {
|
||||
consolidatedTasks.set(task.tableName, task);
|
||||
}
|
||||
|
||||
if (currentBatch.length > consolidatedTasks.size) {
|
||||
console.log(`[Amily2-SuperMemory] 队列优化: 将 ${currentBatch.length} 个事件合并为 ${consolidatedTasks.size} 个操作。`);
|
||||
}
|
||||
|
||||
for (const task of consolidatedTasks.values()) {
|
||||
await processUpdateTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
await saveStateToMetadata();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2-SuperMemory] 处理更新队列失败:', error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
if (updateQueue.length > 0) {
|
||||
processQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processUpdateTask(task) {
|
||||
const { tableName, data, role, hint, headers, rowStatuses } = task;
|
||||
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
const tableSettings = settings.superMemory_tableSettings?.[tableName] || {};
|
||||
|
||||
if (tableSettings.sync === false) {
|
||||
console.log(`[Amily2-SuperMemory] 表格 ${tableName} 已配置为不写入世界书,跳过同步。`);
|
||||
return;
|
||||
}
|
||||
|
||||
const isIndexConstant = tableSettings.constant !== false;
|
||||
|
||||
const activeData = data.filter((_, i) => !rowStatuses || rowStatuses[i] !== 'pending-deletion');
|
||||
const indexText = generateIndex(activeData, headers, 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, isIndexConstant);
|
||||
|
||||
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] 全量同步完成。');
|
||||
}
|
||||
|
||||
export async function purgeSuperMemory() {
|
||||
try {
|
||||
console.log('[Amily2-SuperMemory] 开始清空记忆...');
|
||||
const bookName = getMemoryBookName();
|
||||
const entries = await amilyHelper.getLorebookEntries(bookName);
|
||||
|
||||
if (!entries || entries.length === 0) {
|
||||
console.log('[Amily2-SuperMemory] 世界书为空,无需清理。');
|
||||
return;
|
||||
}
|
||||
|
||||
const entriesToDelete = [];
|
||||
const prefixes = ['[Amily2]', '【Amily2'];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.comment && prefixes.some(p => entry.comment.startsWith(p))) {
|
||||
entriesToDelete.push(entry.uid);
|
||||
}
|
||||
}
|
||||
|
||||
if (entriesToDelete.length > 0) {
|
||||
await amilyHelper.deleteLorebookEntries(bookName, entriesToDelete);
|
||||
console.log(`[Amily2-SuperMemory] 已清空 ${entriesToDelete.length} 个条目。`);
|
||||
if (window.toastr) toastr.success(`已清空 ${entriesToDelete.length} 条记忆数据`);
|
||||
} else {
|
||||
if (window.toastr) toastr.info('没有发现需要清空的Amily2记忆数据');
|
||||
}
|
||||
|
||||
updateDashboardCounters();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2-SuperMemory] 清空失败:', error);
|
||||
if (window.toastr) toastr.error('清空失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
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, getMemoryBookName } from "./lorebook-bridge.js";
|
||||
import { getMemoryState, loadMemoryState, saveMemoryState } from "../table-system/manager.js";
|
||||
import { TABLE_UPDATED_EVENT } from "../table-system/events-schema.js";
|
||||
import { eventSource, event_types } from "/script.js";
|
||||
|
||||
/* ── [AMILY2-MODIFIED] ── pipeline integration: awaitSync() export ── */
|
||||
let isInitialized = false;
|
||||
let updateQueue = [];
|
||||
let isProcessing = false;
|
||||
let lastChatId = null;
|
||||
let _syncPromise = null; // tracks the running processQueue() promise for pipeline awaiting
|
||||
|
||||
const METADATA_KEY = 'Amily2_Memory_Data';
|
||||
|
||||
/**
|
||||
* [AMILY2-MODIFIED] Pipeline integration:
|
||||
* Allows MessagePipeline Stage 4 to await the super-memory sync triggered
|
||||
* by the AMILY2_TABLE_UPDATED CustomEvent during Stage 3.
|
||||
*/
|
||||
export async function awaitSync() {
|
||||
if (_syncPromise) await _syncPromise;
|
||||
}
|
||||
|
||||
export async function initializeSuperMemory() {
|
||||
const userType = parseInt(localStorage.getItem("plugin_user_type") || "0");
|
||||
if (userType < 2) {
|
||||
console.warn('[Amily2-SuperMemory] 权限不足 (Type < 2),拒绝初始化超级记忆系统。');
|
||||
if (window.$) $('#sm-system-status').text('未授权').css('color', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (window.$) $('#sm-system-status').text('运行中').css('color', '#4caf50');
|
||||
return;
|
||||
}
|
||||
console.log('[Amily2-SuperMemory] 初始化核心管理器...');
|
||||
|
||||
if (!amilyHelper) {
|
||||
console.error('[Amily2-SuperMemory] 致命错误:AmilyHelper 未就绪。');
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener(TABLE_UPDATED_EVENT, 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bus 直调路径:由 TableSystem 通过 query('SuperMemory').pushUpdate(payload) 调用。
|
||||
* 接受纯对象 payload(events-schema.js 中 createTableUpdateEvent 的 detail 结构)。
|
||||
*/
|
||||
export function pushUpdate(payload) {
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
if (settings.super_memory_enabled === false) return;
|
||||
|
||||
// 楼层数检查:聊天消息数不足时跳过同步
|
||||
const minFloor = settings.superMemory_minTriggerFloor ?? 0;
|
||||
if (minFloor > 0) {
|
||||
const chatLength = getContext()?.chat?.length ?? 0;
|
||||
if (chatLength < minFloor) {
|
||||
console.log(`[Amily2-SuperMemory] 当前楼层 ${chatLength} < 最低触发楼层 ${minFloor},跳过同步。`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { tableName, data, role, headers, rowStatuses } = payload;
|
||||
console.log(`[Amily2-SuperMemory] 收到表格更新 (Bus): ${tableName} (Role: ${role})`);
|
||||
|
||||
updateQueue.push({ tableName, data, role, headers, rowStatuses });
|
||||
_syncPromise = processQueue();
|
||||
}
|
||||
|
||||
/** CustomEvent 降级路径(Bus 未就绪时的兜底监听器) */
|
||||
function handleTableUpdate(event) {
|
||||
pushUpdate(event.detail);
|
||||
}
|
||||
|
||||
async function processQueue() {
|
||||
if (isProcessing || updateQueue.length === 0) return;
|
||||
isProcessing = true;
|
||||
|
||||
try {
|
||||
while (updateQueue.length > 0) {
|
||||
|
||||
const consolidatedTasks = new Map();
|
||||
const currentBatch = [...updateQueue];
|
||||
updateQueue.length = 0; // 清空队列
|
||||
|
||||
for (const task of currentBatch) {
|
||||
consolidatedTasks.set(task.tableName, task);
|
||||
}
|
||||
|
||||
if (currentBatch.length > consolidatedTasks.size) {
|
||||
console.log(`[Amily2-SuperMemory] 队列优化: 将 ${currentBatch.length} 个事件合并为 ${consolidatedTasks.size} 个操作。`);
|
||||
}
|
||||
|
||||
for (const task of consolidatedTasks.values()) {
|
||||
await processUpdateTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
await saveStateToMetadata();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2-SuperMemory] 处理更新队列失败:', error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
if (updateQueue.length > 0) {
|
||||
processQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processUpdateTask(task) {
|
||||
const { tableName, data, role, hint, headers, rowStatuses } = task;
|
||||
|
||||
const settings = extension_settings[extensionName] || {};
|
||||
const tableSettings = settings.superMemory_tableSettings?.[tableName] || {};
|
||||
|
||||
if (tableSettings.sync === false) {
|
||||
console.log(`[Amily2-SuperMemory] 表格 ${tableName} 已配置为不写入世界书,跳过同步。`);
|
||||
return;
|
||||
}
|
||||
|
||||
const isIndexConstant = tableSettings.constant !== false;
|
||||
|
||||
const activeData = data.filter((_, i) => !rowStatuses || rowStatuses[i] !== 'pending-deletion');
|
||||
const indexText = generateIndex(activeData, headers, 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, isIndexConstant);
|
||||
|
||||
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 settings = extension_settings[extensionName] || {};
|
||||
const minFloor = settings.superMemory_minTriggerFloor ?? 0;
|
||||
if (minFloor > 0) {
|
||||
const chatLength = getContext()?.chat?.length ?? 0;
|
||||
if (chatLength < minFloor) {
|
||||
console.log(`[Amily2-SuperMemory] 全量同步跳过:当前楼层 ${chatLength} < 最低触发楼层 ${minFloor}。`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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] 全量同步完成。');
|
||||
}
|
||||
|
||||
export async function purgeSuperMemory() {
|
||||
try {
|
||||
console.log('[Amily2-SuperMemory] 开始清空记忆...');
|
||||
const bookName = getMemoryBookName();
|
||||
const entries = await amilyHelper.getLorebookEntries(bookName);
|
||||
|
||||
if (!entries || entries.length === 0) {
|
||||
console.log('[Amily2-SuperMemory] 世界书为空,无需清理。');
|
||||
return;
|
||||
}
|
||||
|
||||
const entriesToDelete = [];
|
||||
const prefixes = ['[Amily2]', '【Amily2'];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.comment && prefixes.some(p => entry.comment.startsWith(p))) {
|
||||
entriesToDelete.push(entry.uid);
|
||||
}
|
||||
}
|
||||
|
||||
if (entriesToDelete.length > 0) {
|
||||
await amilyHelper.deleteLorebookEntries(bookName, entriesToDelete);
|
||||
console.log(`[Amily2-SuperMemory] 已清空 ${entriesToDelete.length} 个条目。`);
|
||||
if (window.toastr) toastr.success(`已清空 ${entriesToDelete.length} 条记忆数据`);
|
||||
} else {
|
||||
if (window.toastr) toastr.info('没有发现需要清空的Amily2记忆数据');
|
||||
}
|
||||
|
||||
updateDashboardCounters();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Amily2-SuperMemory] 清空失败:', error);
|
||||
if (window.toastr) toastr.error('清空失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
export function generateIndex(data, headers, role, tableName = "") {
|
||||
if (!Array.isArray(data) || data.length === 0 || !Array.isArray(headers) || headers.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const indexColumnIndices = identifyIndexColumns(data, headers);
|
||||
const indexColumnHeaders = indexColumnIndices.map(i => headers[i]);
|
||||
|
||||
let indexLines = [];
|
||||
indexLines.push(`| ${indexColumnHeaders.join(' | ')} |`);
|
||||
indexLines.push(`| ${indexColumnHeaders.map(() => '---').join(' | ')} |`);
|
||||
|
||||
let processedData = [...data];
|
||||
|
||||
const firstColIndex = 0;
|
||||
const firstColHeader = headers[firstColIndex];
|
||||
const firstColVal = data[0] ? data[0][firstColIndex] : '';
|
||||
const isIndexCol = (firstColHeader && (firstColHeader.includes('索引') || firstColHeader.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[firstColIndex] || '');
|
||||
const valB = String(b[firstColIndex] || '');
|
||||
return valA.localeCompare(valB, undefined, { numeric: true });
|
||||
});
|
||||
}
|
||||
|
||||
for (const row of processedData) {
|
||||
const lineParts = indexColumnIndices.map(colIndex => {
|
||||
let val = row[colIndex];
|
||||
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.map((_, i) => i);
|
||||
|
||||
const candidates = [];
|
||||
const maxColumns = 3;
|
||||
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
if (candidates.length >= maxColumns) break;
|
||||
|
||||
const header = headers[i];
|
||||
let totalLen = 0;
|
||||
let count = 0;
|
||||
for (const row of data) {
|
||||
if (row[i]) {
|
||||
totalLen += String(row[i]).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(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return headers.map((_, i) => i).slice(0, Math.min(headers.length, maxColumns));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
export function generateIndex(data, headers, role, tableName = "") {
|
||||
if (!Array.isArray(data) || data.length === 0 || !Array.isArray(headers) || headers.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const indexColumnIndices = identifyIndexColumns(data, headers);
|
||||
const indexColumnHeaders = indexColumnIndices.map(i => headers[i]);
|
||||
|
||||
let indexLines = [];
|
||||
indexLines.push(`| ${indexColumnHeaders.join(' | ')} |`);
|
||||
indexLines.push(`| ${indexColumnHeaders.map(() => '---').join(' | ')} |`);
|
||||
|
||||
let processedData = [...data];
|
||||
|
||||
const firstColIndex = 0;
|
||||
const firstColHeader = headers[firstColIndex];
|
||||
const firstColVal = data[0] ? data[0][firstColIndex] : '';
|
||||
const isIndexCol = (firstColHeader && (firstColHeader.includes('索引') || firstColHeader.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[firstColIndex] || '');
|
||||
const valB = String(b[firstColIndex] || '');
|
||||
return valA.localeCompare(valB, undefined, { numeric: true });
|
||||
});
|
||||
}
|
||||
|
||||
for (const row of processedData) {
|
||||
const lineParts = indexColumnIndices.map(colIndex => {
|
||||
let val = row[colIndex];
|
||||
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.map((_, i) => i);
|
||||
|
||||
const candidates = [];
|
||||
const maxColumns = 3;
|
||||
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
if (candidates.length >= maxColumns) break;
|
||||
|
||||
const header = headers[i];
|
||||
let totalLen = 0;
|
||||
let count = 0;
|
||||
for (const row of data) {
|
||||
if (row[i]) {
|
||||
totalLen += String(row[i]).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(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return headers.map((_, i) => i).slice(0, Math.min(headers.length, maxColumns));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user