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,
|
resolveAutoExtractionPlanController,
|
||||||
runExtractionController,
|
runExtractionController,
|
||||||
} from "./maintenance/extraction-controller.js";
|
} from "./maintenance/extraction-controller.js";
|
||||||
|
import {
|
||||||
|
DEFAULT_TRIGGER_KEYWORDS,
|
||||||
|
getSmartTriggerDecision,
|
||||||
|
} from "./maintenance/smart-trigger.js";
|
||||||
import {
|
import {
|
||||||
debugDebug,
|
debugDebug,
|
||||||
debugLog,
|
debugLog,
|
||||||
@@ -140,7 +144,6 @@ import {
|
|||||||
refreshPanelLiveStateController,
|
refreshPanelLiveStateController,
|
||||||
} from "./ui/panel-bridge.js";
|
} from "./ui/panel-bridge.js";
|
||||||
import {
|
import {
|
||||||
createDefaultTaskProfiles,
|
|
||||||
migrateLegacyTaskProfiles,
|
migrateLegacyTaskProfiles,
|
||||||
} from "./prompting/prompt-profiles.js";
|
} from "./prompting/prompt-profiles.js";
|
||||||
import { inspectTaskRegexReuse } from "./prompting/task-regex.js";
|
import { inspectTaskRegexReuse } from "./prompting/task-regex.js";
|
||||||
@@ -167,6 +170,11 @@ import {
|
|||||||
writePersistedRecallToUserMessage,
|
writePersistedRecallToUserMessage,
|
||||||
} from "./retrieval/recall-persistence.js";
|
} from "./retrieval/recall-persistence.js";
|
||||||
import { resolveConfiguredTimeoutMs } from "./runtime/request-timeout.js";
|
import { resolveConfiguredTimeoutMs } from "./runtime/request-timeout.js";
|
||||||
|
import {
|
||||||
|
defaultSettings,
|
||||||
|
getPersistedSettingsSnapshot,
|
||||||
|
mergePersistedSettings,
|
||||||
|
} from "./runtime/settings-defaults.js";
|
||||||
import { retrieve } from "./retrieval/retriever.js";
|
import { retrieve } from "./retrieval/retriever.js";
|
||||||
import {
|
import {
|
||||||
appendBatchJournal,
|
appendBatchJournal,
|
||||||
@@ -239,6 +247,8 @@ import {
|
|||||||
validateVectorConfig,
|
validateVectorConfig,
|
||||||
} from "./vector/vector-index.js";
|
} from "./vector/vector-index.js";
|
||||||
|
|
||||||
|
export { DEFAULT_TRIGGER_KEYWORDS, getSmartTriggerDecision };
|
||||||
|
|
||||||
// 操控面板模块(动态加载,防止加载失败崩溃整个扩展)
|
// 操控面板模块(动态加载,防止加载失败崩溃整个扩展)
|
||||||
let _panelModule = null;
|
let _panelModule = null;
|
||||||
let _themesModule = 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;
|
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() {
|
function getSettings() {
|
||||||
const loadedSettings = migrateLegacyAutoMaintenanceSettings(
|
const mergedSettings = mergePersistedSettings(
|
||||||
extension_settings[MODULE_NAME] || {},
|
extension_settings[MODULE_NAME] || {},
|
||||||
);
|
);
|
||||||
const mergedSettings = {
|
|
||||||
...defaultSettings,
|
|
||||||
...loadedSettings,
|
|
||||||
};
|
|
||||||
const migrated = migrateLegacyTaskProfiles(mergedSettings);
|
const migrated = migrateLegacyTaskProfiles(mergedSettings);
|
||||||
mergedSettings.taskProfilesVersion = migrated.taskProfilesVersion;
|
mergedSettings.taskProfilesVersion = migrated.taskProfilesVersion;
|
||||||
mergedSettings.taskProfiles = migrated.taskProfiles;
|
mergedSettings.taskProfiles = migrated.taskProfiles;
|
||||||
@@ -6800,25 +6628,6 @@ async function resetVectorStateForConfigChange(reason = "向量配置已变更")
|
|||||||
saveGraphToChat({ reason: "vector-config-reset" });
|
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) {
|
function encodeBase64Utf8(text) {
|
||||||
const bytes = new TextEncoder().encode(String(text ?? ""));
|
const bytes = new TextEncoder().encode(String(text ?? ""));
|
||||||
const chunkSize = 0x8000;
|
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) {
|
function getLatestUserChatMessage(chat) {
|
||||||
if (!Array.isArray(chat)) return null;
|
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: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: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: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",
|
"test:stable": "node scripts/run-test-suite.mjs",
|
||||||
"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:all": "npm run test:stable",
|
||||||
|
"check": "node scripts/check-syntax.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"triviumdb": "^0.4.41"
|
"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 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() {
|
import {
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
defaultSettings,
|
||||||
const indexPath = path.resolve(__dirname, "../index.js");
|
mergePersistedSettings,
|
||||||
const source = await fs.readFile(indexPath, "utf8");
|
} from "../runtime/settings-defaults.js";
|
||||||
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();
|
|
||||||
|
|
||||||
assert.equal(defaultSettings.extractContextTurns, 2);
|
assert.equal(defaultSettings.extractContextTurns, 2);
|
||||||
assert.equal(defaultSettings.extractAutoDelayLatestAssistant, false);
|
assert.equal(defaultSettings.extractAutoDelayLatestAssistant, false);
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ import {
|
|||||||
} from "../retrieval/recall-persistence.js";
|
} from "../retrieval/recall-persistence.js";
|
||||||
import { getNodeDisplayName } from "../graph/node-labels.js";
|
import { getNodeDisplayName } from "../graph/node-labels.js";
|
||||||
import { normalizeGraphRuntimeState } from "../runtime/runtime-state.js";
|
import { normalizeGraphRuntimeState } from "../runtime/runtime-state.js";
|
||||||
|
import {
|
||||||
|
defaultSettings,
|
||||||
|
getPersistedSettingsSnapshot,
|
||||||
|
mergePersistedSettings,
|
||||||
|
} from "../runtime/settings-defaults.js";
|
||||||
import {
|
import {
|
||||||
clampFloat,
|
clampFloat,
|
||||||
clampInt,
|
clampInt,
|
||||||
@@ -276,6 +281,9 @@ async function createGraphPersistenceHarness({
|
|||||||
extension_settings: {
|
extension_settings: {
|
||||||
[MODULE_NAME]: {},
|
[MODULE_NAME]: {},
|
||||||
},
|
},
|
||||||
|
defaultSettings,
|
||||||
|
getPersistedSettingsSnapshot,
|
||||||
|
mergePersistedSettings,
|
||||||
migrateLegacyTaskProfiles(settings = {}) {
|
migrateLegacyTaskProfiles(settings = {}) {
|
||||||
return {
|
return {
|
||||||
taskProfilesVersion: Number(settings?.taskProfilesVersion || 0),
|
taskProfilesVersion: Number(settings?.taskProfilesVersion || 0),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
GRAPH_PERSISTENCE_META_KEY,
|
GRAPH_PERSISTENCE_META_KEY,
|
||||||
MODULE_NAME,
|
MODULE_NAME,
|
||||||
} from "../../graph/graph-persistence.js";
|
} from "../../graph/graph-persistence.js";
|
||||||
|
import { getSmartTriggerDecision } from "../../maintenance/smart-trigger.js";
|
||||||
import {
|
import {
|
||||||
buildPersistedRecallRecord,
|
buildPersistedRecallRecord,
|
||||||
bumpPersistedRecallGenerationCount,
|
bumpPersistedRecallGenerationCount,
|
||||||
@@ -40,6 +41,10 @@ import {
|
|||||||
normalizeStageNoticeLevel,
|
normalizeStageNoticeLevel,
|
||||||
shouldRunRecallForTransaction,
|
shouldRunRecallForTransaction,
|
||||||
} from "../../ui/ui-status.js";
|
} from "../../ui/ui-status.js";
|
||||||
|
import {
|
||||||
|
defaultSettings,
|
||||||
|
mergePersistedSettings,
|
||||||
|
} from "../../runtime/settings-defaults.js";
|
||||||
|
|
||||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const indexPath = path.resolve(moduleDir, "../../index.js");
|
const indexPath = path.resolve(moduleDir, "../../index.js");
|
||||||
@@ -81,7 +86,8 @@ export function createGenerationRecallHarness(options = {}) {
|
|||||||
result: null,
|
result: null,
|
||||||
currentGraph: {},
|
currentGraph: {},
|
||||||
_panelModule: null,
|
_panelModule: null,
|
||||||
defaultSettings: {},
|
defaultSettings,
|
||||||
|
mergePersistedSettings,
|
||||||
settings: {},
|
settings: {},
|
||||||
graphPersistenceState: createGraphPersistenceState(),
|
graphPersistenceState: createGraphPersistenceState(),
|
||||||
extension_settings: { [MODULE_NAME]: {} },
|
extension_settings: { [MODULE_NAME]: {} },
|
||||||
@@ -121,6 +127,7 @@ export function createGenerationRecallHarness(options = {}) {
|
|||||||
[...chat].reverse().find((message) => message?.is_user) || null,
|
[...chat].reverse().find((message) => message?.is_user) || null,
|
||||||
getLastNonSystemChatMessage: (chat = []) =>
|
getLastNonSystemChatMessage: (chat = []) =>
|
||||||
[...chat].reverse().find((message) => !message?.is_system) || null,
|
[...chat].reverse().find((message) => !message?.is_system) || null,
|
||||||
|
getSmartTriggerDecision,
|
||||||
getSendTextareaValue: () => context.__sendTextareaValue,
|
getSendTextareaValue: () => context.__sendTextareaValue,
|
||||||
getRecallUserMessageSourceLabel: (source = "") => source,
|
getRecallUserMessageSourceLabel: (source = "") => source,
|
||||||
getRecallUserMessageSourceLabelController: (source = "") => source,
|
getRecallUserMessageSourceLabelController: (source = "") => source,
|
||||||
|
|||||||
@@ -1,35 +1,6 @@
|
|||||||
import assert from "node:assert/strict";
|
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() {
|
import { getSmartTriggerDecision } from "../maintenance/smart-trigger.js";
|
||||||
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();
|
|
||||||
|
|
||||||
const noTrigger = getSmartTriggerDecision(
|
const noTrigger = getSmartTriggerDecision(
|
||||||
[
|
[
|
||||||
@@ -61,7 +32,7 @@ const customTrigger = getSmartTriggerDecision(
|
|||||||
{ triggerPatterns: "真相|背叛", smartTriggerThreshold: 2 },
|
{ triggerPatterns: "真相|背叛", smartTriggerThreshold: 2 },
|
||||||
);
|
);
|
||||||
assert.equal(customTrigger.triggered, true);
|
assert.equal(customTrigger.triggered, true);
|
||||||
assert.ok(customTrigger.reasons.some((r) => r.includes("自定义触发")));
|
assert.ok(customTrigger.reasons.some((reason) => reason.includes("自定义触发")));
|
||||||
|
|
||||||
const ignoresProcessedMessages = getSmartTriggerDecision(
|
const ignoresProcessedMessages = getSmartTriggerDecision(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user