From 19c802bdcca43d94c16acb171584fffee5cf7486 Mon Sep 17 00:00:00 2001
From: Hao19911125 <99091644+Hao19911125@users.noreply.github.com>
Date: Tue, 7 Apr 2026 10:18:03 +0800
Subject: [PATCH] Add memory LLM preset switching
---
index.js | 2 +
panel.html | 38 +++++++
panel.js | 304 +++++++++++++++++++++++++++++++++++++++++++++++++++--
style.css | 25 +++++
4 files changed, 359 insertions(+), 10 deletions(-)
diff --git a/index.js b/index.js
index 9c6bdaf..bd3da9d 100644
--- a/index.js
+++ b/index.js
@@ -454,6 +454,8 @@ const defaultSettings = {
llmApiUrl: "",
llmApiKey: "",
llmModel: "",
+ llmPresets: {},
+ llmActivePreset: "",
// Embedding API 配置
embeddingApiUrl: "",
diff --git a/panel.html b/panel.html
index 1112259..6472f20 100644
--- a/panel.html
+++ b/panel.html
@@ -569,6 +569,44 @@
+
+
+
+
+
+
+
+
+
- _patchSettings({ llmApiUrl: value.trim() }),
- );
- bindText("bme-setting-llm-key", (value) =>
- _patchSettings({ llmApiKey: value.trim() }),
- );
- bindText("bme-setting-llm-model", (value) =>
- _patchSettings({ llmModel: value.trim() }),
- );
+ const llmPresetSelect = document.getElementById("bme-llm-preset-select");
+ if (llmPresetSelect && llmPresetSelect.dataset.bmeBound !== "true") {
+ llmPresetSelect.addEventListener("change", () => {
+ const selectedName = String(llmPresetSelect.value || "");
+ if (!selectedName) {
+ const currentActivePreset = String(
+ (_getSettings?.() || {}).llmActivePreset || "",
+ );
+ if (currentActivePreset) {
+ _patchSettings({ llmActivePreset: "" });
+ }
+ _syncLlmPresetControls("");
+ return;
+ }
+
+ const settings = _normalizeLlmPresetSettings(_getSettings?.() || {});
+ const preset = settings.llmPresets?.[selectedName];
+ if (!preset) {
+ _patchSettings({ llmActivePreset: "" });
+ _populateLlmPresetSelect(settings.llmPresets || {}, "");
+ _syncLlmPresetControls("");
+ toastr.warning("选中的模板不存在,已切回手动模式", "ST-BME");
+ return;
+ }
+
+ _patchSettings({
+ llmApiUrl: preset.llmApiUrl,
+ llmApiKey: preset.llmApiKey,
+ llmModel: preset.llmModel,
+ llmActivePreset: selectedName,
+ });
+ _setInputValue("bme-setting-llm-url", preset.llmApiUrl);
+ _setInputValue("bme-setting-llm-key", preset.llmApiKey);
+ _setInputValue("bme-setting-llm-model", preset.llmModel);
+ _clearFetchedLlmModels();
+ _syncLlmPresetControls(selectedName);
+ });
+ llmPresetSelect.dataset.bmeBound = "true";
+ }
+
+ const llmPresetSaveBtn = document.getElementById("bme-llm-preset-save");
+ if (llmPresetSaveBtn && llmPresetSaveBtn.dataset.bmeBound !== "true") {
+ llmPresetSaveBtn.addEventListener("click", () => {
+ const settings = _normalizeLlmPresetSettings(_getSettings?.() || {});
+ const activePreset = String(settings.llmActivePreset || "");
+ if (!activePreset) {
+ document.getElementById("bme-llm-preset-save-as")?.click();
+ return;
+ }
+
+ const nextPresets = {
+ ...(settings.llmPresets || {}),
+ [activePreset]: _getLlmConfigInputSnapshot(),
+ };
+ _patchSettings({ llmPresets: nextPresets });
+ _populateLlmPresetSelect(nextPresets, activePreset);
+ _syncLlmPresetControls(activePreset);
+ toastr.success("当前模板已保存", "ST-BME");
+ });
+ llmPresetSaveBtn.dataset.bmeBound = "true";
+ }
+
+ const llmPresetSaveAsBtn = document.getElementById("bme-llm-preset-save-as");
+ if (llmPresetSaveAsBtn && llmPresetSaveAsBtn.dataset.bmeBound !== "true") {
+ llmPresetSaveAsBtn.addEventListener("click", () => {
+ const settings = _normalizeLlmPresetSettings(_getSettings?.() || {});
+ const activePreset = String(settings.llmActivePreset || "");
+ const suggestedName = activePreset
+ ? `${activePreset} 副本`
+ : "新模板";
+ const nextName = window.prompt("请输入新模板名称", suggestedName);
+ if (nextName == null) return;
+
+ const trimmedName = String(nextName).trim();
+ if (!trimmedName) {
+ toastr.info("模板名称不能为空", "ST-BME");
+ return;
+ }
+ if (trimmedName in (settings.llmPresets || {})) {
+ toastr.info("模板名称已存在,请换一个", "ST-BME");
+ return;
+ }
+
+ const nextPresets = {
+ ...(settings.llmPresets || {}),
+ [trimmedName]: _getLlmConfigInputSnapshot(),
+ };
+ _patchSettings({
+ llmPresets: nextPresets,
+ llmActivePreset: trimmedName,
+ });
+ _populateLlmPresetSelect(nextPresets, trimmedName);
+ _syncLlmPresetControls(trimmedName);
+ toastr.success("已另存为新模板", "ST-BME");
+ });
+ llmPresetSaveAsBtn.dataset.bmeBound = "true";
+ }
+
+ const llmPresetDeleteBtn = document.getElementById("bme-llm-preset-delete");
+ if (llmPresetDeleteBtn && llmPresetDeleteBtn.dataset.bmeBound !== "true") {
+ llmPresetDeleteBtn.addEventListener("click", () => {
+ const settings = _normalizeLlmPresetSettings(_getSettings?.() || {});
+ const activePreset = String(settings.llmActivePreset || "");
+ if (!activePreset) {
+ toastr.info("当前处于手动模式,没有可删除的模板", "ST-BME");
+ return;
+ }
+
+ const confirmed = window.confirm(
+ `确定要删除模板“${activePreset}”吗?当前输入框里的值会保留。`,
+ );
+ if (!confirmed) return;
+
+ const nextPresets = { ...(settings.llmPresets || {}) };
+ delete nextPresets[activePreset];
+ _patchSettings({
+ llmPresets: nextPresets,
+ llmActivePreset: "",
+ });
+ _populateLlmPresetSelect(nextPresets, "");
+ _syncLlmPresetControls("");
+ toastr.success("模板已删除", "ST-BME");
+ });
+ llmPresetDeleteBtn.dataset.bmeBound = "true";
+ }
+
+ bindText("bme-setting-llm-url", (value) => {
+ _patchSettings({ llmApiUrl: value.trim() });
+ _markLlmPresetDirty({ clearFetchedModels: true });
+ });
+ bindText("bme-setting-llm-key", (value) => {
+ _patchSettings({ llmApiKey: value.trim() });
+ _markLlmPresetDirty({ clearFetchedModels: true });
+ });
+ bindText("bme-setting-llm-model", (value) => {
+ _patchSettings({ llmModel: value.trim() });
+ _markLlmPresetDirty();
+ });
bindNumber("bme-setting-timeout-ms", 300000, 1000, 3600000, (value) =>
_patchSettings({ timeoutMs: value }),
);
@@ -5600,6 +5735,155 @@ function _patchSettings(patch = {}, options = {}) {
return settings;
}
+function _normalizeLlmPresetSettings(settings = _getSettings?.() || {}) {
+ const rawPresets = settings?.llmPresets;
+ const normalizedPresets = {};
+ let changed =
+ !rawPresets ||
+ typeof rawPresets !== "object" ||
+ Array.isArray(rawPresets);
+
+ if (!changed) {
+ for (const [name, preset] of Object.entries(rawPresets)) {
+ if (!String(name || "").trim()) {
+ changed = true;
+ continue;
+ }
+ if (
+ !preset ||
+ typeof preset !== "object" ||
+ Array.isArray(preset) ||
+ typeof preset.llmApiUrl !== "string" ||
+ typeof preset.llmApiKey !== "string" ||
+ typeof preset.llmModel !== "string"
+ ) {
+ changed = true;
+ continue;
+ }
+ normalizedPresets[name] = {
+ llmApiUrl: preset.llmApiUrl,
+ llmApiKey: preset.llmApiKey,
+ llmModel: preset.llmModel,
+ };
+ }
+ }
+
+ let activePreset =
+ typeof settings?.llmActivePreset === "string" ? settings.llmActivePreset : "";
+ if (activePreset && !Object.prototype.hasOwnProperty.call(normalizedPresets, activePreset)) {
+ activePreset = "";
+ changed = true;
+ }
+ if (typeof settings?.llmActivePreset !== "string") {
+ changed = true;
+ }
+
+ if (!changed) {
+ return settings;
+ }
+
+ return _patchSettings({
+ llmPresets: normalizedPresets,
+ llmActivePreset: activePreset,
+ });
+}
+
+function _resolveActiveLlmPresetName(settings = _getSettings?.() || {}) {
+ const activePreset = String(settings?.llmActivePreset || "");
+ if (!activePreset) return "";
+ const preset = settings?.llmPresets?.[activePreset];
+ if (!preset) return "";
+ return _isSameLlmConfigSnapshot(
+ {
+ llmApiUrl: String(settings?.llmApiUrl || ""),
+ llmApiKey: String(settings?.llmApiKey || ""),
+ llmModel: String(settings?.llmModel || ""),
+ },
+ preset,
+ )
+ ? activePreset
+ : "";
+}
+
+function _isSameLlmConfigSnapshot(left = {}, right = {}) {
+ return (
+ String(left?.llmApiUrl || "") === String(right?.llmApiUrl || "") &&
+ String(left?.llmApiKey || "") === String(right?.llmApiKey || "") &&
+ String(left?.llmModel || "") === String(right?.llmModel || "")
+ );
+}
+
+function _getLlmConfigInputSnapshot() {
+ const settings = _getSettings?.() || {};
+ return {
+ llmApiUrl: String(
+ document.getElementById("bme-setting-llm-url")?.value ?? settings.llmApiUrl ?? "",
+ ).trim(),
+ llmApiKey: String(
+ document.getElementById("bme-setting-llm-key")?.value ?? settings.llmApiKey ?? "",
+ ).trim(),
+ llmModel: String(
+ document.getElementById("bme-setting-llm-model")?.value ?? settings.llmModel ?? "",
+ ).trim(),
+ };
+}
+
+function _populateLlmPresetSelect(presets = {}, activePreset = "") {
+ const select = document.getElementById("bme-llm-preset-select");
+ if (!select) return;
+
+ while (select.options.length > 1) {
+ select.remove(1);
+ }
+
+ Object.keys(presets)
+ .sort((left, right) => left.localeCompare(right, "zh-Hans-CN"))
+ .forEach((name) => {
+ const option = document.createElement("option");
+ option.value = name;
+ option.textContent = name;
+ select.appendChild(option);
+ });
+
+ select.value = activePreset || "";
+}
+
+function _syncLlmPresetControls(activePreset = "") {
+ const select = document.getElementById("bme-llm-preset-select");
+ if (select) {
+ select.value = activePreset || "";
+ }
+
+ const deleteBtn = document.getElementById("bme-llm-preset-delete");
+ if (deleteBtn) {
+ deleteBtn.disabled = !activePreset;
+ deleteBtn.title = activePreset ? "删除当前模板" : "手动模式下没有可删除的模板";
+ }
+}
+
+function _clearFetchedLlmModels() {
+ fetchedMemoryLLMModels.length = 0;
+ const modelSelect = document.getElementById("bme-select-llm-model");
+ if (!modelSelect) return;
+ while (modelSelect.options.length > 1) {
+ modelSelect.remove(1);
+ }
+ modelSelect.value = "";
+ modelSelect.style.display = "none";
+}
+
+function _markLlmPresetDirty(options = {}) {
+ if (options.clearFetchedModels) {
+ _clearFetchedLlmModels();
+ }
+
+ const activePreset = String((_getSettings?.() || {}).llmActivePreset || "");
+ if (activePreset) {
+ _patchSettings({ llmActivePreset: "" });
+ }
+ _syncLlmPresetControls("");
+}
+
function _highlightThemeChoice(themeName) {
if (!panelEl) return;
panelEl.querySelectorAll(".bme-theme-option").forEach((opt) => {
diff --git a/style.css b/style.css
index 9cd1492..e6788db 100644
--- a/style.css
+++ b/style.css
@@ -1423,6 +1423,22 @@
margin-bottom: 12px;
}
+.bme-llm-preset-controls {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.bme-llm-preset-controls select {
+ flex: 1;
+ min-width: 0;
+}
+
+.bme-llm-preset-controls .bme-config-secondary-btn {
+ flex-shrink: 0;
+}
+
.bme-config-secondary-btn {
display: inline-flex;
align-items: center;
@@ -2880,6 +2896,15 @@
width: 100%;
}
+ .bme-llm-preset-controls {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .bme-llm-preset-controls .bme-config-secondary-btn {
+ width: 100%;
+ }
+
.bme-inline-checkbox {
min-height: 44px;
gap: 12px;