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