refactor: extract event binding and panel bridge modules

This commit is contained in:
Youzini-afk
2026-03-29 17:01:47 +08:00
parent 4b769d312a
commit 079a01ee78
3 changed files with 300 additions and 169 deletions

109
event-binding.js Normal file
View File

@@ -0,0 +1,109 @@
export function registerBeforeCombinePromptsController(runtime, listener) {
const makeFirst = runtime.getEventMakeFirst();
if (typeof makeFirst === "function") {
return makeFirst(
runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS,
listener,
);
}
runtime.console.warn("[ST-BME] eventMakeFirst 不可用,回退到普通事件注册");
runtime.eventSource.on(runtime.eventTypes.GENERATE_BEFORE_COMBINE_PROMPTS, listener);
return null;
}
export function registerGenerationAfterCommandsController(runtime, listener) {
const makeFirst = runtime.getEventMakeFirst();
if (typeof makeFirst === "function") {
return makeFirst(runtime.eventTypes.GENERATION_AFTER_COMMANDS, listener);
}
runtime.console.warn(
"[ST-BME] eventMakeFirst 不可用GENERATION_AFTER_COMMANDS 回退到普通事件注册",
);
runtime.eventSource.on(runtime.eventTypes.GENERATION_AFTER_COMMANDS, listener);
return null;
}
export function scheduleSendIntentHookRetryController(runtime, delayMs = 400) {
runtime.clearTimeout(runtime.getSendIntentHookRetryTimer());
const timer = runtime.setTimeout(() => {
runtime.setSendIntentHookRetryTimer(null);
runtime.installSendIntentHooks();
}, delayMs);
runtime.setSendIntentHookRetryTimer(timer);
}
export function installSendIntentHooksController(runtime) {
for (const cleanup of runtime.consumeSendIntentHookCleanup()) {
try {
cleanup();
} catch (error) {
runtime.console.warn("[ST-BME] 清理发送意图钩子失败:", error);
}
}
const sendButton = runtime.document.getElementById("send_but");
const sendTextarea = runtime.document.getElementById("send_textarea");
if (sendButton) {
const captureSendIntent = () => {
runtime.recordRecallSendIntent(runtime.getSendTextareaValue(), "send-button");
};
sendButton.addEventListener("click", captureSendIntent, true);
sendButton.addEventListener("pointerup", captureSendIntent, true);
sendButton.addEventListener("touchend", captureSendIntent, true);
runtime.pushSendIntentHookCleanup(() => {
sendButton.removeEventListener("click", captureSendIntent, true);
sendButton.removeEventListener("pointerup", captureSendIntent, true);
sendButton.removeEventListener("touchend", captureSendIntent, true);
});
}
if (sendTextarea) {
const captureEnterIntent = (event) => {
if (
(event.key === "Enter" || event.key === "NumpadEnter") &&
!event.shiftKey
) {
runtime.recordRecallSendIntent(
runtime.getSendTextareaValue(),
"textarea-enter",
);
}
};
sendTextarea.addEventListener("keydown", captureEnterIntent, true);
runtime.pushSendIntentHookCleanup(() => {
sendTextarea.removeEventListener("keydown", captureEnterIntent, true);
});
}
if (!sendButton || !sendTextarea) {
runtime.scheduleSendIntentHookRetry();
}
}
export function registerCoreEventHooksController(runtime) {
const { eventSource, eventTypes, handlers } = runtime;
eventSource.on(eventTypes.CHAT_CHANGED, handlers.onChatChanged);
if (eventTypes.CHAT_LOADED) {
eventSource.on(eventTypes.CHAT_LOADED, handlers.onChatLoaded);
}
if (eventTypes.MESSAGE_SENT) {
eventSource.on(eventTypes.MESSAGE_SENT, handlers.onMessageSent);
}
runtime.registerGenerationAfterCommands(handlers.onGenerationAfterCommands);
runtime.registerBeforeCombinePrompts(handlers.onBeforeCombinePrompts);
eventSource.on(eventTypes.MESSAGE_RECEIVED, handlers.onMessageReceived);
eventSource.on(eventTypes.MESSAGE_DELETED, handlers.onMessageDeleted);
eventSource.on(eventTypes.MESSAGE_EDITED, handlers.onMessageEdited);
eventSource.on(eventTypes.MESSAGE_SWIPED, handlers.onMessageSwiped);
if (eventTypes.MESSAGE_UPDATED) {
eventSource.on(eventTypes.MESSAGE_UPDATED, handlers.onMessageEdited);
}
}

286
index.js
View File

@@ -56,10 +56,18 @@ import { estimateTokens, formatInjection } from "./injector.js";
import { fetchMemoryLLMModels, testLLMConnection } from "./llm.js"; import { fetchMemoryLLMModels, testLLMConnection } from "./llm.js";
import { getNodeDisplayName } from "./node-labels.js"; import { getNodeDisplayName } from "./node-labels.js";
import { showManagedBmeNotice } from "./notice.js"; import { showManagedBmeNotice } from "./notice.js";
import {
installSendIntentHooksController,
registerBeforeCombinePromptsController,
registerCoreEventHooksController,
registerGenerationAfterCommandsController,
scheduleSendIntentHookRetryController,
} from "./event-binding.js";
import { import {
createDefaultTaskProfiles, createDefaultTaskProfiles,
migrateLegacyTaskProfiles, migrateLegacyTaskProfiles,
} from "./prompt-profiles.js"; } from "./prompt-profiles.js";
import { initializePanelBridgeController } from "./panel-bridge.js";
import { resolveConfiguredTimeoutMs } from "./request-timeout.js"; import { resolveConfiguredTimeoutMs } from "./request-timeout.js";
import { retrieve } from "./retriever.js"; import { retrieve } from "./retriever.js";
import { import {
@@ -914,86 +922,57 @@ function getSendTextareaValue() {
} }
function scheduleSendIntentHookRetry(delayMs = 400) { function scheduleSendIntentHookRetry(delayMs = 400) {
clearTimeout(sendIntentHookRetryTimer); return scheduleSendIntentHookRetryController(
sendIntentHookRetryTimer = setTimeout(() => { {
sendIntentHookRetryTimer = null; clearTimeout,
installSendIntentHooks(); getSendIntentHookRetryTimer: () => sendIntentHookRetryTimer,
}, delayMs); installSendIntentHooks,
setSendIntentHookRetryTimer: (timer) => {
sendIntentHookRetryTimer = timer;
},
setTimeout,
},
delayMs,
);
} }
function registerBeforeCombinePrompts(listener) { function registerBeforeCombinePrompts(listener) {
const makeFirst = globalThis.eventMakeFirst; return registerBeforeCombinePromptsController(
if (typeof makeFirst === "function") { {
return makeFirst(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, listener); console,
} eventSource,
eventTypes: event_types,
console.warn("[ST-BME] eventMakeFirst 不可用,回退到普通事件注册"); getEventMakeFirst: () => globalThis.eventMakeFirst,
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, listener); },
return null; listener,
);
} }
function registerGenerationAfterCommands(listener) { function registerGenerationAfterCommands(listener) {
const makeFirst = globalThis.eventMakeFirst; return registerGenerationAfterCommandsController(
if (typeof makeFirst === "function") { {
return makeFirst(event_types.GENERATION_AFTER_COMMANDS, listener); console,
} eventSource,
eventTypes: event_types,
console.warn( getEventMakeFirst: () => globalThis.eventMakeFirst,
"[ST-BME] eventMakeFirst 不可用GENERATION_AFTER_COMMANDS 回退到普通事件注册", },
listener,
); );
eventSource.on(event_types.GENERATION_AFTER_COMMANDS, listener);
return null;
} }
function installSendIntentHooks() { function installSendIntentHooks() {
for (const cleanup of sendIntentHookCleanup.splice( return installSendIntentHooksController({
0, console,
sendIntentHookCleanup.length, consumeSendIntentHookCleanup: () =>
)) { sendIntentHookCleanup.splice(0, sendIntentHookCleanup.length),
try { document,
cleanup(); getSendTextareaValue,
} catch (error) { pushSendIntentHookCleanup: (cleanup) => {
console.warn("[ST-BME] 清理发送意图钩子失败:", error); sendIntentHookCleanup.push(cleanup);
} },
} recordRecallSendIntent,
scheduleSendIntentHookRetry,
const sendButton = document.getElementById("send_but"); });
const sendTextarea = document.getElementById("send_textarea");
if (sendButton) {
const captureSendIntent = () => {
recordRecallSendIntent(getSendTextareaValue(), "send-button");
};
sendButton.addEventListener("click", captureSendIntent, true);
sendButton.addEventListener("pointerup", captureSendIntent, true);
sendButton.addEventListener("touchend", captureSendIntent, true);
sendIntentHookCleanup.push(() => {
sendButton.removeEventListener("click", captureSendIntent, true);
sendButton.removeEventListener("pointerup", captureSendIntent, true);
sendButton.removeEventListener("touchend", captureSendIntent, true);
});
}
if (sendTextarea) {
const captureEnterIntent = (event) => {
if (
(event.key === "Enter" || event.key === "NumpadEnter") &&
!event.shiftKey
) {
recordRecallSendIntent(getSendTextareaValue(), "textarea-enter");
}
};
sendTextarea.addEventListener("keydown", captureEnterIntent, true);
sendIntentHookCleanup.push(() => {
sendTextarea.removeEventListener("keydown", captureEnterIntent, true);
});
}
if (!sendButton || !sendTextarea) {
scheduleSendIntentHookRetry();
}
} }
// ==================== 设置管理 ==================== // ==================== 设置管理 ====================
@@ -5146,22 +5125,23 @@ async function onReembedDirect() {
); );
// 注册事件钩子 // 注册事件钩子
eventSource.on(event_types.CHAT_CHANGED, onChatChanged); registerCoreEventHooksController({
if (event_types.CHAT_LOADED) { eventSource,
eventSource.on(event_types.CHAT_LOADED, onChatLoaded); eventTypes: event_types,
} handlers: {
if (event_types.MESSAGE_SENT) { onBeforeCombinePrompts,
eventSource.on(event_types.MESSAGE_SENT, onMessageSent); onChatChanged,
} onChatLoaded,
registerGenerationAfterCommands(onGenerationAfterCommands); onGenerationAfterCommands,
registerBeforeCombinePrompts(onBeforeCombinePrompts); onMessageDeleted,
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived); onMessageEdited,
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted); onMessageReceived,
eventSource.on(event_types.MESSAGE_EDITED, onMessageEdited); onMessageSent,
eventSource.on(event_types.MESSAGE_SWIPED, onMessageSwiped); onMessageSwiped,
if (event_types.MESSAGE_UPDATED) { },
eventSource.on(event_types.MESSAGE_UPDATED, onMessageEdited); registerBeforeCombinePrompts,
} registerGenerationAfterCommands,
});
// 加载当前聊天的图谱 // 加载当前聊天的图谱
clearPendingGraphLoadRetry(); clearPendingGraphLoadRetry();
@@ -5173,89 +5153,57 @@ async function onReembedDirect() {
// ==================== 操控面板初始化 ==================== // ==================== 操控面板初始化 ====================
try { await initializePanelBridgeController({
// 动态加载面板模块 $,
_panelModule = await import("./panel.js"); actions: {
_themesModule = await import("./themes.js"); syncGraphLoad: () =>
syncGraphLoadFromLiveContext({
// 应用主题 source: "panel-open-sync",
const settings = getSettings(); }),
_themesModule.applyTheme(settings.panelTheme || "crimson"); extract: onManualExtract,
compress: onManualCompress,
// 初始化操控面板 sleep: onManualSleep,
await _panelModule.initPanel({ synopsis: onManualSynopsis,
getGraph: () => currentGraph, export: onExportGraph,
getSettings: () => getSettings(), import: onImportGraph,
getLastExtract: () => lastExtractedItems, rebuild: onRebuild,
getLastRecall: () => lastRecalledItems, evolve: onManualEvolve,
getRuntimeStatus: () => getPanelRuntimeStatus(), testEmbedding: onTestEmbedding,
getLastExtractionStatus: () => lastExtractionStatus, testMemoryLLM: onTestMemoryLLM,
getLastVectorStatus: () => lastVectorStatus, fetchMemoryLLMModels: onFetchMemoryLLMModels,
getLastRecallStatus: () => lastRecallStatus, fetchEmbeddingModels: onFetchEmbeddingModels,
getLastBatchStatus: () => rebuildVectorIndex: () => onRebuildVectorIndex(),
currentGraph?.historyState?.lastBatchStatus || null, rebuildVectorRange: (range) => onRebuildVectorIndex(range),
getLastInjection: () => lastInjectionContent, reembedDirect: onReembedDirect,
getRuntimeDebugSnapshot: (options = {}) => reroll: onReroll,
getPanelRuntimeDebugSnapshot(options), },
getGraphPersistenceState: () => getGraphPersistenceLiveState(), console,
updateSettings: (patch) => { document,
const settings = updateModuleSettings(patch); getGraph: () => currentGraph,
if (Object.prototype.hasOwnProperty.call(patch, "panelTheme")) { getGraphPersistenceState: () => getGraphPersistenceLiveState(),
_themesModule?.applyTheme(settings.panelTheme || "crimson"); getLastBatchStatus: () => currentGraph?.historyState?.lastBatchStatus || null,
_panelModule?.updatePanelTheme(settings.panelTheme || "crimson"); getLastExtract: () => lastExtractedItems,
} getLastExtractionStatus: () => lastExtractionStatus,
return settings; getLastInjection: () => lastInjectionContent,
}, getLastRecall: () => lastRecalledItems,
actions: { getLastRecallStatus: () => lastRecallStatus,
syncGraphLoad: () => getLastVectorStatus: () => lastVectorStatus,
syncGraphLoadFromLiveContext({ getPanelModule: () => _panelModule,
source: "panel-open-sync", getRuntimeDebugSnapshot: (options = {}) =>
}), getPanelRuntimeDebugSnapshot(options),
extract: onManualExtract, getRuntimeStatus: () => getPanelRuntimeStatus(),
compress: onManualCompress, getSettings,
sleep: onManualSleep, getThemesModule: () => _themesModule,
synopsis: onManualSynopsis, importPanelModule: async () => await import("./panel.js"),
export: onExportGraph, importThemesModule: async () => await import("./themes.js"),
import: onImportGraph, setPanelModule: (module) => {
rebuild: onRebuild, _panelModule = module;
evolve: onManualEvolve, },
testEmbedding: onTestEmbedding, setThemesModule: (module) => {
testMemoryLLM: onTestMemoryLLM, _themesModule = module;
fetchMemoryLLMModels: onFetchMemoryLLMModels, },
fetchEmbeddingModels: onFetchEmbeddingModels, updateSettings: updateModuleSettings,
rebuildVectorIndex: () => onRebuildVectorIndex(), });
rebuildVectorRange: (range) => onRebuildVectorIndex(range),
reembedDirect: onReembedDirect,
reroll: onReroll,
},
});
// 注入三条杠 Options 菜单按钮
if (!document.getElementById("option_st_bme_panel")) {
const $menuItem = $(`
<a id="option_st_bme_panel">
<i class="fa-lg fa-solid fa-brain"></i>
<span>记忆图谱</span>
</a>
`).on("click", () => {
_panelModule?.openPanel();
$("#options").hide();
});
const $optionsContent = $("#options .options-content");
const $anchor = $("#option_toggle_logprobs");
if ($anchor.length > 0) {
$anchor.after($menuItem);
} else if ($optionsContent.length > 0) {
$optionsContent.append($menuItem);
}
}
console.log("[ST-BME] 操控面板初始化完成");
} catch (panelError) {
console.error("[ST-BME] 操控面板加载失败(核心功能不受影响):", panelError);
}
console.log("[ST-BME] 初始化完成"); console.log("[ST-BME] 初始化完成");
})(); })();

74
panel-bridge.js Normal file
View File

@@ -0,0 +1,74 @@
function resolvePanelTheme(settings) {
return settings?.panelTheme || "crimson";
}
function injectOptionsMenuEntry(runtime) {
if (runtime.document.getElementById("option_st_bme_panel")) {
return;
}
const $menuItem = runtime.$(`
<a id="option_st_bme_panel">
<i class="fa-lg fa-solid fa-brain"></i>
<span>记忆图谱</span>
</a>
`).on("click", () => {
runtime.getPanelModule()?.openPanel?.();
runtime.$("#options").hide();
});
const $optionsContent = runtime.$("#options .options-content");
const $anchor = runtime.$("#option_toggle_logprobs");
if ($anchor.length > 0) {
$anchor.after($menuItem);
} else if ($optionsContent.length > 0) {
$optionsContent.append($menuItem);
}
}
export async function initializePanelBridgeController(runtime) {
try {
const panelModule = await runtime.importPanelModule();
const themesModule = await runtime.importThemesModule();
runtime.setPanelModule(panelModule);
runtime.setThemesModule(themesModule);
const settings = runtime.getSettings();
const theme = resolvePanelTheme(settings);
themesModule.applyTheme(theme);
await panelModule.initPanel({
getGraph: runtime.getGraph,
getSettings: runtime.getSettings,
getLastExtract: runtime.getLastExtract,
getLastRecall: runtime.getLastRecall,
getRuntimeStatus: runtime.getRuntimeStatus,
getLastExtractionStatus: runtime.getLastExtractionStatus,
getLastVectorStatus: runtime.getLastVectorStatus,
getLastRecallStatus: runtime.getLastRecallStatus,
getLastBatchStatus: runtime.getLastBatchStatus,
getLastInjection: runtime.getLastInjection,
getRuntimeDebugSnapshot: runtime.getRuntimeDebugSnapshot,
getGraphPersistenceState: runtime.getGraphPersistenceState,
updateSettings: (patch) => {
const nextSettings = runtime.updateSettings(patch);
if (Object.prototype.hasOwnProperty.call(patch || {}, "panelTheme")) {
const nextTheme = resolvePanelTheme(nextSettings);
runtime.getThemesModule()?.applyTheme?.(nextTheme);
runtime.getPanelModule()?.updatePanelTheme?.(nextTheme);
}
return nextSettings;
},
actions: runtime.actions,
});
injectOptionsMenuEntry(runtime);
runtime.console.log("[ST-BME] 操控面板初始化完成");
} catch (panelError) {
runtime.console.error(
"[ST-BME] 操控面板加载失败(核心功能不受影响):",
panelError,
);
}
}