mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Harden post-refactor test and check guardrails
This commit is contained in:
329
index.js
329
index.js
@@ -75,6 +75,10 @@ import {
|
||||
resolveAutoExtractionPlanController,
|
||||
runExtractionController,
|
||||
} from "./maintenance/extraction-controller.js";
|
||||
import {
|
||||
DEFAULT_TRIGGER_KEYWORDS,
|
||||
getSmartTriggerDecision,
|
||||
} from "./maintenance/smart-trigger.js";
|
||||
import {
|
||||
debugDebug,
|
||||
debugLog,
|
||||
@@ -140,7 +144,6 @@ import {
|
||||
refreshPanelLiveStateController,
|
||||
} from "./ui/panel-bridge.js";
|
||||
import {
|
||||
createDefaultTaskProfiles,
|
||||
migrateLegacyTaskProfiles,
|
||||
} from "./prompting/prompt-profiles.js";
|
||||
import { inspectTaskRegexReuse } from "./prompting/task-regex.js";
|
||||
@@ -167,6 +170,11 @@ import {
|
||||
writePersistedRecallToUserMessage,
|
||||
} from "./retrieval/recall-persistence.js";
|
||||
import { resolveConfiguredTimeoutMs } from "./runtime/request-timeout.js";
|
||||
import {
|
||||
defaultSettings,
|
||||
getPersistedSettingsSnapshot,
|
||||
mergePersistedSettings,
|
||||
} from "./runtime/settings-defaults.js";
|
||||
import { retrieve } from "./retrieval/retriever.js";
|
||||
import {
|
||||
appendBatchJournal,
|
||||
@@ -239,6 +247,8 @@ import {
|
||||
validateVectorConfig,
|
||||
} from "./vector/vector-index.js";
|
||||
|
||||
export { DEFAULT_TRIGGER_KEYWORDS, getSmartTriggerDecision };
|
||||
|
||||
// 操控面板模块(动态加载,防止加载失败崩溃整个扩展)
|
||||
let _panelModule = null;
|
||||
let _themesModule = null;
|
||||
@@ -383,148 +393,6 @@ function readRuntimeDebugSnapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 默认设置 ====================
|
||||
|
||||
const defaultSettings = {
|
||||
enabled: true,
|
||||
debugLoggingEnabled: false,
|
||||
timeoutMs: 300000,
|
||||
hideOldMessagesEnabled: false,
|
||||
hideOldMessagesKeepLastN: 12,
|
||||
|
||||
// 提取设置
|
||||
extractEvery: 1, // 每 N 条 assistant 回复提取一次
|
||||
extractContextTurns: 2, // 提取时包含的上下文楼层数
|
||||
extractAutoDelayLatestAssistant: false, // 自动提取时晚一条 AI 楼再处理
|
||||
|
||||
// 召回设置
|
||||
recallEnabled: true,
|
||||
recallCardUserInputDisplayMode: "beautify_only",
|
||||
worldInfoFilterMode: "default",
|
||||
worldInfoFilterCustomKeywords: "",
|
||||
recallTopK: 20, // 向量预筛 Top-K
|
||||
recallMaxNodes: 8, // LLM 召回最大节点数
|
||||
recallEnableLLM: true, // 是否启用 LLM 精确召回
|
||||
recallEnableVectorPrefilter: true, // 是否启用向量预筛
|
||||
recallEnableGraphDiffusion: true, // 是否启用图扩散
|
||||
recallDiffusionTopK: 100, // 图扩散阶段保留的候选上限
|
||||
recallLlmCandidatePool: 30, // 传给 LLM 精排的候选池大小
|
||||
recallLlmContextMessages: 4, // 传给 LLM 精排的最近非系统消息数
|
||||
recallEnableMultiIntent: true,
|
||||
recallMultiIntentMaxSegments: 4,
|
||||
recallEnableContextQueryBlend: true,
|
||||
recallContextAssistantWeight: 0.2,
|
||||
recallContextPreviousUserWeight: 0.1,
|
||||
recallEnableLexicalBoost: true,
|
||||
recallLexicalWeight: 0.18,
|
||||
recallTeleportAlpha: 0.15,
|
||||
recallEnableTemporalLinks: true,
|
||||
recallTemporalLinkStrength: 0.2,
|
||||
recallEnableDiversitySampling: true,
|
||||
recallDppCandidateMultiplier: 3,
|
||||
recallDppQualityWeight: 1.0,
|
||||
recallEnableCooccurrenceBoost: false,
|
||||
recallCooccurrenceScale: 0.1,
|
||||
recallCooccurrenceMaxNeighbors: 10,
|
||||
recallEnableResidualRecall: false,
|
||||
recallResidualBasisMaxNodes: 24,
|
||||
recallNmfTopics: 15,
|
||||
recallNmfNoveltyThreshold: 0.4,
|
||||
recallResidualThreshold: 0.3,
|
||||
recallResidualTopK: 5,
|
||||
enableScopedMemory: true,
|
||||
enablePovMemory: true,
|
||||
enableRegionScopedObjective: true,
|
||||
recallCharacterPovWeight: 1.25,
|
||||
recallUserPovWeight: 1.05,
|
||||
recallObjectiveCurrentRegionWeight: 1.15,
|
||||
recallObjectiveAdjacentRegionWeight: 0.9,
|
||||
recallObjectiveGlobalWeight: 0.75,
|
||||
injectUserPovMemory: true,
|
||||
injectObjectiveGlobalMemory: true,
|
||||
|
||||
// 注入设置
|
||||
injectPosition: "atDepth", // 注入位置
|
||||
injectDepth: 9999, // IN_CHAT@Depth 注入深度,数值越大越靠前
|
||||
injectRole: 0, // 0=system, 1=user, 2=assistant
|
||||
|
||||
// 混合评分权重
|
||||
graphWeight: 0.6,
|
||||
vectorWeight: 0.3,
|
||||
importanceWeight: 0.1,
|
||||
|
||||
// 记忆 LLM(留空时复用当前酒馆模型)
|
||||
llmApiUrl: "",
|
||||
llmApiKey: "",
|
||||
llmModel: "",
|
||||
llmPresets: {},
|
||||
llmActivePreset: "",
|
||||
|
||||
// Embedding API 配置
|
||||
embeddingApiUrl: "",
|
||||
embeddingApiKey: "",
|
||||
embeddingModel: "text-embedding-3-small",
|
||||
embeddingTransportMode: "direct",
|
||||
embeddingBackendSource: "openai",
|
||||
embeddingBackendModel: "text-embedding-3-small",
|
||||
embeddingBackendApiUrl: "",
|
||||
embeddingAutoSuffix: true,
|
||||
|
||||
// Schema
|
||||
nodeTypeSchema: null, // null 表示使用默认
|
||||
|
||||
// 自定义提示词
|
||||
extractPrompt: "",
|
||||
recallPrompt: "",
|
||||
consolidationPrompt: "",
|
||||
compressPrompt: "",
|
||||
synopsisPrompt: "",
|
||||
reflectionPrompt: "",
|
||||
taskProfilesVersion: 3,
|
||||
taskProfiles: createDefaultTaskProfiles(),
|
||||
|
||||
// ====== v2 增强设置 ======
|
||||
|
||||
// ③ 记忆整合(合并精确对照 + 记忆进化)
|
||||
enableConsolidation: true, // 启用记忆整合
|
||||
consolidationNeighborCount: 5, // 近邻搜索数量
|
||||
consolidationThreshold: 0.85, // 冲突判定相似度阈值
|
||||
|
||||
// ⑨ 全局故事概要
|
||||
enableSynopsis: true, // 启用全局概要
|
||||
synopsisEveryN: 5, // 每 N 次提取后更新概要
|
||||
|
||||
// ⑥ 认知边界过滤(P1)
|
||||
enableVisibility: true, // 启用认知边界
|
||||
// ⑦ 双记忆交叉检索(P1)
|
||||
enableCrossRecall: true, // 启用交叉检索
|
||||
|
||||
// ① 惊奇度分割(P2)
|
||||
enableSmartTrigger: false, // 启用惊奇度分割
|
||||
triggerPatterns: "", // 自定义触发正则
|
||||
smartTriggerThreshold: 2, // 轻量触发阈值
|
||||
|
||||
// ⑤ 主动遗忘(P2)
|
||||
enableSleepCycle: false, // 启用主动遗忘
|
||||
forgetThreshold: 0.5, // 保留价值阈值
|
||||
sleepEveryN: 10, // 每 N 次提取后执行
|
||||
|
||||
// ⑧ 概率触发回忆(P2)
|
||||
enableProbRecall: false, // 启用概率触发
|
||||
probRecallChance: 0.15, // 触发概率
|
||||
|
||||
// ⑩ 反思条目(P2)
|
||||
enableReflection: true, // 启用反思
|
||||
reflectEveryN: 10, // 每 N 次提取后反思
|
||||
consolidationAutoMinNewNodes: 2,
|
||||
enableAutoCompression: true,
|
||||
compressionEveryN: 10,
|
||||
|
||||
// UI 面板
|
||||
noticeDisplayMode: "normal", // normal|compact
|
||||
panelTheme: "crimson", // 面板主题 crimson|cyan|amber|violet|paperDawn|glacierSky
|
||||
};
|
||||
|
||||
// ==================== 状态 ====================
|
||||
|
||||
let currentGraph = null;
|
||||
@@ -2989,50 +2857,10 @@ function installSendIntentHooks() {
|
||||
|
||||
// ==================== 设置管理 ====================
|
||||
|
||||
function migrateLegacyAutoMaintenanceSettings(loaded = {}) {
|
||||
if (!loaded || typeof loaded !== "object" || Array.isArray(loaded)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const migrated = { ...loaded };
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
migrated,
|
||||
"consolidationAutoMinNewNodes",
|
||||
) &&
|
||||
Object.prototype.hasOwnProperty.call(migrated, "maintenanceAutoMinNewNodes")
|
||||
) {
|
||||
migrated.consolidationAutoMinNewNodes = clampInt(
|
||||
migrated.maintenanceAutoMinNewNodes,
|
||||
defaultSettings.consolidationAutoMinNewNodes,
|
||||
1,
|
||||
50,
|
||||
);
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(migrated, "enableAutoCompression")) {
|
||||
const parsedEveryN = Math.floor(Number(migrated.compressionEveryN));
|
||||
migrated.enableAutoCompression = !(
|
||||
Number.isFinite(parsedEveryN) && parsedEveryN <= 0
|
||||
);
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(migrated, "compressionEveryN") &&
|
||||
Math.floor(Number(migrated.compressionEveryN)) <= 0
|
||||
) {
|
||||
migrated.compressionEveryN = defaultSettings.compressionEveryN;
|
||||
}
|
||||
delete migrated.maintenanceAutoMinNewNodes;
|
||||
return migrated;
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
const loadedSettings = migrateLegacyAutoMaintenanceSettings(
|
||||
const mergedSettings = mergePersistedSettings(
|
||||
extension_settings[MODULE_NAME] || {},
|
||||
);
|
||||
const mergedSettings = {
|
||||
...defaultSettings,
|
||||
...loadedSettings,
|
||||
};
|
||||
const migrated = migrateLegacyTaskProfiles(mergedSettings);
|
||||
mergedSettings.taskProfilesVersion = migrated.taskProfilesVersion;
|
||||
mergedSettings.taskProfiles = migrated.taskProfiles;
|
||||
@@ -6800,25 +6628,6 @@ async function resetVectorStateForConfigChange(reason = "向量配置已变更")
|
||||
saveGraphToChat({ reason: "vector-config-reset" });
|
||||
}
|
||||
|
||||
function getPersistedSettingsSnapshot(settings = getSettings()) {
|
||||
const persisted = {};
|
||||
for (const key of Object.keys(defaultSettings)) {
|
||||
persisted[key] = settings[key];
|
||||
}
|
||||
return persisted;
|
||||
}
|
||||
|
||||
function mergePersistedSettings(loaded = {}) {
|
||||
const compatibleLoaded = migrateLegacyAutoMaintenanceSettings(loaded);
|
||||
const merged = { ...defaultSettings };
|
||||
for (const key of Object.keys(defaultSettings)) {
|
||||
if (Object.prototype.hasOwnProperty.call(compatibleLoaded, key)) {
|
||||
merged[key] = compatibleLoaded[key];
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function encodeBase64Utf8(text) {
|
||||
const bytes = new TextEncoder().encode(String(text ?? ""));
|
||||
const chunkSize = 0x8000;
|
||||
@@ -7722,120 +7531,6 @@ function handleGraphShadowSnapshotVisibilityChange() {
|
||||
|
||||
// ==================== 核心流程 ====================
|
||||
|
||||
const DEFAULT_TRIGGER_KEYWORDS = [
|
||||
"突然",
|
||||
"没想到",
|
||||
"原来",
|
||||
"其实",
|
||||
"发现",
|
||||
"背叛",
|
||||
"死亡",
|
||||
"复活",
|
||||
"恢复记忆",
|
||||
"失忆",
|
||||
"告白",
|
||||
"暴露",
|
||||
"秘密",
|
||||
"计划",
|
||||
"规则",
|
||||
"契约",
|
||||
"位置",
|
||||
"地点",
|
||||
"离开",
|
||||
"来到",
|
||||
];
|
||||
|
||||
export function getSmartTriggerDecision(
|
||||
chat,
|
||||
lastProcessed,
|
||||
settings,
|
||||
endFloor = null,
|
||||
) {
|
||||
const startFloor = Math.max(0, (lastProcessed ?? -1) + 1);
|
||||
const normalizedEndFloor = Number.isFinite(Number(endFloor))
|
||||
? Math.max(startFloor - 1, Math.floor(Number(endFloor)))
|
||||
: null;
|
||||
const pendingMessages = chat
|
||||
.slice(
|
||||
startFloor,
|
||||
normalizedEndFloor == null ? undefined : normalizedEndFloor + 1,
|
||||
)
|
||||
.map((msg, offset) => ({
|
||||
msg,
|
||||
index: startFloor + offset,
|
||||
}))
|
||||
.filter(({ msg, index }) => !isSystemMessageForExtraction(msg, { index, chat }))
|
||||
.map(({ msg }) => ({
|
||||
role: msg.is_user ? "user" : "assistant",
|
||||
content: msg.mes || "",
|
||||
}))
|
||||
.filter((msg) => msg.content.trim().length > 0);
|
||||
|
||||
if (pendingMessages.length === 0) {
|
||||
return { triggered: false, score: 0, reasons: [] };
|
||||
}
|
||||
|
||||
const reasons = [];
|
||||
let score = 0;
|
||||
const combinedText = pendingMessages.map((m) => m.content).join("\n");
|
||||
|
||||
const keywordHits = DEFAULT_TRIGGER_KEYWORDS.filter((keyword) =>
|
||||
combinedText.includes(keyword),
|
||||
);
|
||||
if (keywordHits.length > 0) {
|
||||
score += Math.min(2, keywordHits.length);
|
||||
reasons.push(`关键词: ${keywordHits.slice(0, 3).join(", ")}`);
|
||||
}
|
||||
|
||||
const customPatterns = String(settings.triggerPatterns || "")
|
||||
.split(/\r?\n|,/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
for (const pattern of customPatterns) {
|
||||
try {
|
||||
const regex = new RegExp(pattern, "i");
|
||||
if (regex.test(combinedText)) {
|
||||
score += 2;
|
||||
reasons.push(`自定义触发: ${pattern}`);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// 忽略无效正则,避免影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
const roleSwitchCount = pendingMessages.reduce((count, message, index) => {
|
||||
if (index === 0) return count;
|
||||
return count + (message.role !== pendingMessages[index - 1].role ? 1 : 0);
|
||||
}, 0);
|
||||
if (roleSwitchCount >= 2) {
|
||||
score += 1;
|
||||
reasons.push("多轮往返互动");
|
||||
}
|
||||
|
||||
const punctuationHits = (combinedText.match(/[!?!?]/g) || []).length;
|
||||
if (punctuationHits >= 2) {
|
||||
score += 1;
|
||||
reasons.push("情绪/冲突波动");
|
||||
}
|
||||
|
||||
const entityLikeHits =
|
||||
combinedText.match(
|
||||
/[A-Z][a-z]{2,}|[\u4e00-\u9fff]{2,6}(先生|小姐|王国|城|镇|村|学院|组织|公司|小队|军团)/g,
|
||||
) || [];
|
||||
if (entityLikeHits.length > 0) {
|
||||
score += 1;
|
||||
reasons.push("疑似新实体/新地点");
|
||||
}
|
||||
|
||||
const threshold = Math.max(1, settings.smartTriggerThreshold || 2);
|
||||
return {
|
||||
triggered: score >= threshold,
|
||||
score,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestUserChatMessage(chat) {
|
||||
if (!Array.isArray(chat)) return null;
|
||||
|
||||
|
||||
121
maintenance/smart-trigger.js
Normal file
121
maintenance/smart-trigger.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { isSystemMessageForExtraction } from "./chat-history.js";
|
||||
|
||||
export const DEFAULT_TRIGGER_KEYWORDS = [
|
||||
"突然",
|
||||
"没想到",
|
||||
"原来",
|
||||
"其实",
|
||||
"发现",
|
||||
"背叛",
|
||||
"死亡",
|
||||
"复活",
|
||||
"恢复记忆",
|
||||
"失忆",
|
||||
"告白",
|
||||
"暴露",
|
||||
"秘密",
|
||||
"计划",
|
||||
"规则",
|
||||
"契约",
|
||||
"位置",
|
||||
"地点",
|
||||
"离开",
|
||||
"来到",
|
||||
];
|
||||
|
||||
export function getSmartTriggerDecision(
|
||||
chat,
|
||||
lastProcessed,
|
||||
settings,
|
||||
endFloor = null,
|
||||
) {
|
||||
const safeChat = Array.isArray(chat) ? chat : [];
|
||||
const startFloor = Math.max(0, (lastProcessed ?? -1) + 1);
|
||||
const normalizedEndFloor =
|
||||
endFloor == null || endFloor === ""
|
||||
? null
|
||||
: Number.isFinite(Number(endFloor))
|
||||
? Math.max(startFloor - 1, Math.floor(Number(endFloor)))
|
||||
: null;
|
||||
const pendingMessages = safeChat
|
||||
.slice(
|
||||
startFloor,
|
||||
normalizedEndFloor == null ? undefined : normalizedEndFloor + 1,
|
||||
)
|
||||
.map((msg, offset) => ({
|
||||
msg,
|
||||
index: startFloor + offset,
|
||||
}))
|
||||
.filter(({ msg, index }) =>
|
||||
!isSystemMessageForExtraction(msg, { index, chat: safeChat }),
|
||||
)
|
||||
.map(({ msg }) => ({
|
||||
role: msg.is_user ? "user" : "assistant",
|
||||
content: msg.mes || "",
|
||||
}))
|
||||
.filter((msg) => msg.content.trim().length > 0);
|
||||
|
||||
if (pendingMessages.length === 0) {
|
||||
return { triggered: false, score: 0, reasons: [] };
|
||||
}
|
||||
|
||||
const reasons = [];
|
||||
let score = 0;
|
||||
const combinedText = pendingMessages.map((message) => message.content).join("\n");
|
||||
|
||||
const keywordHits = DEFAULT_TRIGGER_KEYWORDS.filter((keyword) =>
|
||||
combinedText.includes(keyword),
|
||||
);
|
||||
if (keywordHits.length > 0) {
|
||||
score += Math.min(2, keywordHits.length);
|
||||
reasons.push(`关键词: ${keywordHits.slice(0, 3).join(", ")}`);
|
||||
}
|
||||
|
||||
const customPatterns = String(settings?.triggerPatterns || "")
|
||||
.split(/\r?\n|,/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
for (const pattern of customPatterns) {
|
||||
try {
|
||||
const regex = new RegExp(pattern, "i");
|
||||
if (regex.test(combinedText)) {
|
||||
score += 2;
|
||||
reasons.push(`自定义触发: ${pattern}`);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// 忽略无效正则,避免影响主流程
|
||||
}
|
||||
}
|
||||
|
||||
const roleSwitchCount = pendingMessages.reduce((count, message, index) => {
|
||||
if (index === 0) return count;
|
||||
return count + (message.role !== pendingMessages[index - 1].role ? 1 : 0);
|
||||
}, 0);
|
||||
if (roleSwitchCount >= 2) {
|
||||
score += 1;
|
||||
reasons.push("多轮往返互动");
|
||||
}
|
||||
|
||||
const punctuationHits = (combinedText.match(/[!?!?]/g) || []).length;
|
||||
if (punctuationHits >= 2) {
|
||||
score += 1;
|
||||
reasons.push("情绪/冲突波动");
|
||||
}
|
||||
|
||||
const entityLikeHits =
|
||||
combinedText.match(
|
||||
/[A-Z][a-z]{2,}|[\u4e00-\u9fff]{2,6}(先生|小姐|王国|城|镇|村|学院|组织|公司|小队|军团)/g,
|
||||
) || [];
|
||||
if (entityLikeHits.length > 0) {
|
||||
score += 1;
|
||||
reasons.push("疑似新实体/新地点");
|
||||
}
|
||||
|
||||
const threshold = Math.max(1, settings?.smartTriggerThreshold || 2);
|
||||
return {
|
||||
triggered: score >= threshold,
|
||||
score,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
@@ -12,8 +12,9 @@
|
||||
"test:trivial-input": "node tests/trivial-user-input.mjs",
|
||||
"test:indexeddb": "npm run test:indexeddb-persistence && npm run test:indexeddb-sync && npm run test:indexeddb-migration",
|
||||
"test:persistence-matrix": "npm run test:p0 && npm run test:runtime-history && npm run test:graph-persistence && npm run test:indexeddb",
|
||||
"test:all": "npm run test:persistence-matrix && npm run test:maintenance-journal && npm run test:trivial-input",
|
||||
"check": "node --check index.js && node --check sync/bme-db.js && node --check ui/hide-engine.js && node --check ui/panel.js && node --check ui/ui-status.js && node --check host/event-binding.js"
|
||||
"test:stable": "node scripts/run-test-suite.mjs",
|
||||
"test:all": "npm run test:stable",
|
||||
"check": "node scripts/check-syntax.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"triviumdb": "^0.4.41"
|
||||
|
||||
189
runtime/settings-defaults.js
Normal file
189
runtime/settings-defaults.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import { createDefaultTaskProfiles } from "../prompting/prompt-profiles.js";
|
||||
|
||||
function clampIntValue(value, fallback = 0, min = 0, max = 9999) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
||||
}
|
||||
|
||||
export const defaultSettings = {
|
||||
enabled: true,
|
||||
debugLoggingEnabled: false,
|
||||
timeoutMs: 300000,
|
||||
hideOldMessagesEnabled: false,
|
||||
hideOldMessagesKeepLastN: 12,
|
||||
|
||||
// 提取设置
|
||||
extractEvery: 1,
|
||||
extractContextTurns: 2,
|
||||
extractAutoDelayLatestAssistant: false,
|
||||
|
||||
// 召回设置
|
||||
recallEnabled: true,
|
||||
recallCardUserInputDisplayMode: "beautify_only",
|
||||
worldInfoFilterMode: "default",
|
||||
worldInfoFilterCustomKeywords: "",
|
||||
recallTopK: 20,
|
||||
recallMaxNodes: 8,
|
||||
recallEnableLLM: true,
|
||||
recallEnableVectorPrefilter: true,
|
||||
recallEnableGraphDiffusion: true,
|
||||
recallDiffusionTopK: 100,
|
||||
recallLlmCandidatePool: 30,
|
||||
recallLlmContextMessages: 4,
|
||||
recallEnableMultiIntent: true,
|
||||
recallMultiIntentMaxSegments: 4,
|
||||
recallEnableContextQueryBlend: true,
|
||||
recallContextAssistantWeight: 0.2,
|
||||
recallContextPreviousUserWeight: 0.1,
|
||||
recallEnableLexicalBoost: true,
|
||||
recallLexicalWeight: 0.18,
|
||||
recallTeleportAlpha: 0.15,
|
||||
recallEnableTemporalLinks: true,
|
||||
recallTemporalLinkStrength: 0.2,
|
||||
recallEnableDiversitySampling: true,
|
||||
recallDppCandidateMultiplier: 3,
|
||||
recallDppQualityWeight: 1.0,
|
||||
recallEnableCooccurrenceBoost: false,
|
||||
recallCooccurrenceScale: 0.1,
|
||||
recallCooccurrenceMaxNeighbors: 10,
|
||||
recallEnableResidualRecall: false,
|
||||
recallResidualBasisMaxNodes: 24,
|
||||
recallNmfTopics: 15,
|
||||
recallNmfNoveltyThreshold: 0.4,
|
||||
recallResidualThreshold: 0.3,
|
||||
recallResidualTopK: 5,
|
||||
enableScopedMemory: true,
|
||||
enablePovMemory: true,
|
||||
enableRegionScopedObjective: true,
|
||||
recallCharacterPovWeight: 1.25,
|
||||
recallUserPovWeight: 1.05,
|
||||
recallObjectiveCurrentRegionWeight: 1.15,
|
||||
recallObjectiveAdjacentRegionWeight: 0.9,
|
||||
recallObjectiveGlobalWeight: 0.75,
|
||||
injectUserPovMemory: true,
|
||||
injectObjectiveGlobalMemory: true,
|
||||
|
||||
// 注入设置
|
||||
injectPosition: "atDepth",
|
||||
injectDepth: 9999,
|
||||
injectRole: 0,
|
||||
|
||||
// 混合评分权重
|
||||
graphWeight: 0.6,
|
||||
vectorWeight: 0.3,
|
||||
importanceWeight: 0.1,
|
||||
|
||||
// 记忆 LLM(留空时复用当前酒馆模型)
|
||||
llmApiUrl: "",
|
||||
llmApiKey: "",
|
||||
llmModel: "",
|
||||
llmPresets: {},
|
||||
llmActivePreset: "",
|
||||
|
||||
// Embedding API 配置
|
||||
embeddingApiUrl: "",
|
||||
embeddingApiKey: "",
|
||||
embeddingModel: "text-embedding-3-small",
|
||||
embeddingTransportMode: "direct",
|
||||
embeddingBackendSource: "openai",
|
||||
embeddingBackendModel: "text-embedding-3-small",
|
||||
embeddingBackendApiUrl: "",
|
||||
embeddingAutoSuffix: true,
|
||||
|
||||
// Schema
|
||||
nodeTypeSchema: null,
|
||||
|
||||
// 自定义提示词
|
||||
extractPrompt: "",
|
||||
recallPrompt: "",
|
||||
consolidationPrompt: "",
|
||||
compressPrompt: "",
|
||||
synopsisPrompt: "",
|
||||
reflectionPrompt: "",
|
||||
taskProfilesVersion: 3,
|
||||
taskProfiles: createDefaultTaskProfiles(),
|
||||
|
||||
// ====== v2 增强设置 ======
|
||||
enableConsolidation: true,
|
||||
consolidationNeighborCount: 5,
|
||||
consolidationThreshold: 0.85,
|
||||
enableSynopsis: true,
|
||||
synopsisEveryN: 5,
|
||||
enableVisibility: true,
|
||||
enableCrossRecall: true,
|
||||
enableSmartTrigger: false,
|
||||
triggerPatterns: "",
|
||||
smartTriggerThreshold: 2,
|
||||
enableSleepCycle: false,
|
||||
forgetThreshold: 0.5,
|
||||
sleepEveryN: 10,
|
||||
enableProbRecall: false,
|
||||
probRecallChance: 0.15,
|
||||
enableReflection: true,
|
||||
reflectEveryN: 10,
|
||||
consolidationAutoMinNewNodes: 2,
|
||||
enableAutoCompression: true,
|
||||
compressionEveryN: 10,
|
||||
|
||||
// UI 面板
|
||||
noticeDisplayMode: "normal",
|
||||
panelTheme: "crimson",
|
||||
};
|
||||
|
||||
const DEFAULT_SETTING_KEYS = Object.freeze(Object.keys(defaultSettings));
|
||||
|
||||
export function migrateLegacyAutoMaintenanceSettings(loaded = {}) {
|
||||
if (!loaded || typeof loaded !== "object" || Array.isArray(loaded)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const migrated = { ...loaded };
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
migrated,
|
||||
"consolidationAutoMinNewNodes",
|
||||
) &&
|
||||
Object.prototype.hasOwnProperty.call(migrated, "maintenanceAutoMinNewNodes")
|
||||
) {
|
||||
migrated.consolidationAutoMinNewNodes = clampIntValue(
|
||||
migrated.maintenanceAutoMinNewNodes,
|
||||
defaultSettings.consolidationAutoMinNewNodes,
|
||||
1,
|
||||
50,
|
||||
);
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(migrated, "enableAutoCompression")) {
|
||||
const parsedEveryN = Math.floor(Number(migrated.compressionEveryN));
|
||||
migrated.enableAutoCompression = !(
|
||||
Number.isFinite(parsedEveryN) && parsedEveryN <= 0
|
||||
);
|
||||
}
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(migrated, "compressionEveryN") &&
|
||||
Math.floor(Number(migrated.compressionEveryN)) <= 0
|
||||
) {
|
||||
migrated.compressionEveryN = defaultSettings.compressionEveryN;
|
||||
}
|
||||
delete migrated.maintenanceAutoMinNewNodes;
|
||||
return migrated;
|
||||
}
|
||||
|
||||
export function mergePersistedSettings(loaded = {}) {
|
||||
const compatibleLoaded = migrateLegacyAutoMaintenanceSettings(loaded);
|
||||
const merged = { ...defaultSettings };
|
||||
for (const key of DEFAULT_SETTING_KEYS) {
|
||||
if (Object.prototype.hasOwnProperty.call(compatibleLoaded, key)) {
|
||||
merged[key] = compatibleLoaded[key];
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function getPersistedSettingsSnapshot(settings = defaultSettings) {
|
||||
const persisted = {};
|
||||
for (const key of DEFAULT_SETTING_KEYS) {
|
||||
persisted[key] = settings[key];
|
||||
}
|
||||
return persisted;
|
||||
}
|
||||
95
scripts/check-syntax.mjs
Normal file
95
scripts/check-syntax.mjs
Normal file
@@ -0,0 +1,95 @@
|
||||
import { readdir, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const SOURCE_ROOTS = [
|
||||
"index.js",
|
||||
"ena-planner",
|
||||
"graph",
|
||||
"host",
|
||||
"llm",
|
||||
"maintenance",
|
||||
"prompting",
|
||||
"retrieval",
|
||||
"runtime",
|
||||
"scripts",
|
||||
"sync",
|
||||
"ui",
|
||||
"vector",
|
||||
];
|
||||
|
||||
async function collectFiles(targetPath) {
|
||||
const absolutePath = path.resolve(process.cwd(), targetPath);
|
||||
const fileStat = await stat(absolutePath);
|
||||
if (fileStat.isFile()) {
|
||||
return [absolutePath];
|
||||
}
|
||||
|
||||
const files = [];
|
||||
const entries = await readdir(absolutePath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const nextRelative = path.join(targetPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await collectFiles(nextRelative)));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && /\.(js|mjs)$/.test(entry.name)) {
|
||||
files.push(path.resolve(process.cwd(), nextRelative));
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function toPosixPath(filePath) {
|
||||
return path.relative(process.cwd(), filePath).split(path.sep).join("/");
|
||||
}
|
||||
|
||||
async function runNodeCheck(filePath) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, ["--check", filePath], {
|
||||
cwd: process.cwd(),
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
reject(new Error(`${filePath} terminated by signal ${signal}`));
|
||||
return;
|
||||
}
|
||||
if (code !== 0) {
|
||||
reject(new Error(`${filePath} exited with code ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const files = [];
|
||||
for (const root of SOURCE_ROOTS) {
|
||||
files.push(...(await collectFiles(root)));
|
||||
}
|
||||
|
||||
const uniqueFiles = Array.from(new Set(files)).sort((left, right) =>
|
||||
toPosixPath(left).localeCompare(toPosixPath(right), "en"),
|
||||
);
|
||||
console.log(`[ST-BME][check] syntax-checking ${uniqueFiles.length} files`);
|
||||
|
||||
for (const filePath of uniqueFiles) {
|
||||
console.log(`[ST-BME][check] -> ${toPosixPath(filePath)}`);
|
||||
await runNodeCheck(filePath);
|
||||
}
|
||||
|
||||
console.log("[ST-BME][check] syntax checks passed");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
"[ST-BME][check] failed:",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
65
scripts/run-test-suite.mjs
Normal file
65
scripts/run-test-suite.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import { readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const TEST_ROOT = path.resolve(process.cwd(), "tests");
|
||||
const EXCLUDED_TESTS = new Set(["triviumdb-poc.mjs"]);
|
||||
|
||||
function toPosixPath(filePath) {
|
||||
return filePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
async function collectStableTests() {
|
||||
const entries = await readdir(TEST_ROOT, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".mjs"))
|
||||
.map((entry) => entry.name)
|
||||
.filter((name) => !EXCLUDED_TESTS.has(name))
|
||||
.sort((left, right) => left.localeCompare(right, "en"));
|
||||
}
|
||||
|
||||
async function runNodeFile(relativePath) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [relativePath], {
|
||||
cwd: process.cwd(),
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
reject(new Error(`${relativePath} terminated by signal ${signal}`));
|
||||
return;
|
||||
}
|
||||
if (code !== 0) {
|
||||
reject(new Error(`${relativePath} exited with code ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tests = await collectStableTests();
|
||||
console.log(
|
||||
`[ST-BME][test-suite] running ${tests.length} stable tests (excluded: ${Array.from(EXCLUDED_TESTS).join(", ")})`,
|
||||
);
|
||||
|
||||
for (const testName of tests) {
|
||||
const relativePath = toPosixPath(path.join("tests", testName));
|
||||
console.log(`[ST-BME][test-suite] -> ${relativePath}`);
|
||||
await runNodeFile(relativePath);
|
||||
}
|
||||
|
||||
console.log("[ST-BME][test-suite] all stable tests passed");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(
|
||||
"[ST-BME][test-suite] failed:",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -1,86 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import vm from "node:vm";
|
||||
|
||||
async function loadDefaultSettings() {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const indexPath = path.resolve(__dirname, "../index.js");
|
||||
const source = await fs.readFile(indexPath, "utf8");
|
||||
const settingsMatch = source.match(/const defaultSettings = \{[\s\S]*?^\};/m);
|
||||
|
||||
if (!settingsMatch) {
|
||||
throw new Error("无法从 index.js 提取 defaultSettings");
|
||||
}
|
||||
|
||||
const context = vm.createContext({
|
||||
createDefaultTaskProfiles() {
|
||||
return {
|
||||
extract: { activeProfileId: "default", profiles: [] },
|
||||
recall: { activeProfileId: "default", profiles: [] },
|
||||
compress: { activeProfileId: "default", profiles: [] },
|
||||
synopsis: { activeProfileId: "default", profiles: [] },
|
||||
reflection: { activeProfileId: "default", profiles: [] },
|
||||
consolidation: { activeProfileId: "default", profiles: [] },
|
||||
};
|
||||
},
|
||||
});
|
||||
const script = new vm.Script(`
|
||||
${settingsMatch[0]}
|
||||
this.defaultSettings = defaultSettings;
|
||||
`);
|
||||
script.runInContext(context);
|
||||
return context.defaultSettings;
|
||||
}
|
||||
|
||||
async function loadSettingsCompatHelpers() {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const indexPath = path.resolve(__dirname, "../index.js");
|
||||
const source = await fs.readFile(indexPath, "utf8");
|
||||
const settingsMatch = source.match(/const defaultSettings = \{[\s\S]*?^\};/m);
|
||||
const compatMatch = source.match(
|
||||
/function migrateLegacyAutoMaintenanceSettings\(loaded = \{\}\) \{[\s\S]*?^}\r?\n/m,
|
||||
);
|
||||
const mergeMatch = source.match(
|
||||
/function mergePersistedSettings\(loaded = \{\}\) \{[\s\S]*?^}\r?\n/m,
|
||||
);
|
||||
|
||||
if (!settingsMatch || !compatMatch || !mergeMatch) {
|
||||
throw new Error("无法从 index.js 提取设置兼容辅助函数");
|
||||
}
|
||||
|
||||
const context = vm.createContext({
|
||||
clampInt: (value, fallback = 0, min = 0, max = 9999) => {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
||||
},
|
||||
createDefaultTaskProfiles() {
|
||||
return {
|
||||
extract: { activeProfileId: "default", profiles: [] },
|
||||
recall: { activeProfileId: "default", profiles: [] },
|
||||
compress: { activeProfileId: "default", profiles: [] },
|
||||
synopsis: { activeProfileId: "default", profiles: [] },
|
||||
reflection: { activeProfileId: "default", profiles: [] },
|
||||
consolidation: { activeProfileId: "default", profiles: [] },
|
||||
};
|
||||
},
|
||||
});
|
||||
const script = new vm.Script(`
|
||||
${settingsMatch[0]}
|
||||
${compatMatch[0]}
|
||||
${mergeMatch[0]}
|
||||
this.mergePersistedSettings = mergePersistedSettings;
|
||||
`);
|
||||
script.runInContext(context);
|
||||
return {
|
||||
mergePersistedSettings: context.mergePersistedSettings,
|
||||
};
|
||||
}
|
||||
|
||||
const defaultSettings = await loadDefaultSettings();
|
||||
const { mergePersistedSettings } = await loadSettingsCompatHelpers();
|
||||
import {
|
||||
defaultSettings,
|
||||
mergePersistedSettings,
|
||||
} from "../runtime/settings-defaults.js";
|
||||
|
||||
assert.equal(defaultSettings.extractContextTurns, 2);
|
||||
assert.equal(defaultSettings.extractAutoDelayLatestAssistant, false);
|
||||
|
||||
@@ -49,6 +49,11 @@ import {
|
||||
} from "../retrieval/recall-persistence.js";
|
||||
import { getNodeDisplayName } from "../graph/node-labels.js";
|
||||
import { normalizeGraphRuntimeState } from "../runtime/runtime-state.js";
|
||||
import {
|
||||
defaultSettings,
|
||||
getPersistedSettingsSnapshot,
|
||||
mergePersistedSettings,
|
||||
} from "../runtime/settings-defaults.js";
|
||||
import {
|
||||
clampFloat,
|
||||
clampInt,
|
||||
@@ -276,6 +281,9 @@ async function createGraphPersistenceHarness({
|
||||
extension_settings: {
|
||||
[MODULE_NAME]: {},
|
||||
},
|
||||
defaultSettings,
|
||||
getPersistedSettingsSnapshot,
|
||||
mergePersistedSettings,
|
||||
migrateLegacyTaskProfiles(settings = {}) {
|
||||
return {
|
||||
taskProfilesVersion: Number(settings?.taskProfilesVersion || 0),
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
GRAPH_PERSISTENCE_META_KEY,
|
||||
MODULE_NAME,
|
||||
} from "../../graph/graph-persistence.js";
|
||||
import { getSmartTriggerDecision } from "../../maintenance/smart-trigger.js";
|
||||
import {
|
||||
buildPersistedRecallRecord,
|
||||
bumpPersistedRecallGenerationCount,
|
||||
@@ -40,6 +41,10 @@ import {
|
||||
normalizeStageNoticeLevel,
|
||||
shouldRunRecallForTransaction,
|
||||
} from "../../ui/ui-status.js";
|
||||
import {
|
||||
defaultSettings,
|
||||
mergePersistedSettings,
|
||||
} from "../../runtime/settings-defaults.js";
|
||||
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const indexPath = path.resolve(moduleDir, "../../index.js");
|
||||
@@ -81,7 +86,8 @@ export function createGenerationRecallHarness(options = {}) {
|
||||
result: null,
|
||||
currentGraph: {},
|
||||
_panelModule: null,
|
||||
defaultSettings: {},
|
||||
defaultSettings,
|
||||
mergePersistedSettings,
|
||||
settings: {},
|
||||
graphPersistenceState: createGraphPersistenceState(),
|
||||
extension_settings: { [MODULE_NAME]: {} },
|
||||
@@ -121,6 +127,7 @@ export function createGenerationRecallHarness(options = {}) {
|
||||
[...chat].reverse().find((message) => message?.is_user) || null,
|
||||
getLastNonSystemChatMessage: (chat = []) =>
|
||||
[...chat].reverse().find((message) => !message?.is_system) || null,
|
||||
getSmartTriggerDecision,
|
||||
getSendTextareaValue: () => context.__sendTextareaValue,
|
||||
getRecallUserMessageSourceLabel: (source = "") => source,
|
||||
getRecallUserMessageSourceLabelController: (source = "") => source,
|
||||
|
||||
@@ -1,35 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import vm from "node:vm";
|
||||
|
||||
async function loadSmartTriggerDecision() {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const indexPath = path.resolve(__dirname, "../index.js");
|
||||
const source = await fs.readFile(indexPath, "utf8");
|
||||
const keywordMatch = source.match(
|
||||
/const DEFAULT_TRIGGER_KEYWORDS = \[[\s\S]*?\];/m,
|
||||
);
|
||||
const fnMatch = source.match(
|
||||
/export function getSmartTriggerDecision\(chat, lastProcessed, settings\) \{[\s\S]*?^\}/m,
|
||||
);
|
||||
|
||||
if (!keywordMatch || !fnMatch) {
|
||||
throw new Error("无法从 index.js 提取 smart trigger 实现");
|
||||
}
|
||||
|
||||
const context = vm.createContext({});
|
||||
const script = new vm.Script(`
|
||||
${keywordMatch[0]}
|
||||
${fnMatch[0].replace("export function", "function")}
|
||||
this.getSmartTriggerDecision = getSmartTriggerDecision;
|
||||
`);
|
||||
script.runInContext(context);
|
||||
return context.getSmartTriggerDecision;
|
||||
}
|
||||
|
||||
const getSmartTriggerDecision = await loadSmartTriggerDecision();
|
||||
import { getSmartTriggerDecision } from "../maintenance/smart-trigger.js";
|
||||
|
||||
const noTrigger = getSmartTriggerDecision(
|
||||
[
|
||||
@@ -61,7 +32,7 @@ const customTrigger = getSmartTriggerDecision(
|
||||
{ triggerPatterns: "真相|背叛", smartTriggerThreshold: 2 },
|
||||
);
|
||||
assert.equal(customTrigger.triggered, true);
|
||||
assert.ok(customTrigger.reasons.some((r) => r.includes("自定义触发")));
|
||||
assert.ok(customTrigger.reasons.some((reason) => reason.includes("自定义触发")));
|
||||
|
||||
const ignoresProcessedMessages = getSmartTriggerDecision(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user