Add memory LLM preset switching

This commit is contained in:
Hao19911125
2026-04-07 10:18:03 +08:00
parent 0b189d1413
commit 19c802bdcc
4 changed files with 359 additions and 10 deletions

View File

@@ -454,6 +454,8 @@ const defaultSettings = {
llmApiUrl: "",
llmApiKey: "",
llmModel: "",
llmPresets: {},
llmActivePreset: "",
// Embedding API 配置
embeddingApiUrl: "",

View File

@@ -569,6 +569,44 @@
</div>
</div>
</div>
<div class="bme-config-row bme-llm-preset-row">
<label for="bme-llm-preset-select">LLM 配置模板</label>
<div class="bme-llm-preset-controls">
<select
id="bme-llm-preset-select"
class="bme-config-input"
>
<option value="">-- 手动模式 --</option>
</select>
<button
id="bme-llm-preset-save"
class="bme-config-secondary-btn"
title="覆盖保存当前模板"
type="button"
>
<i class="fa-solid fa-floppy-disk"></i>
<span>保存</span>
</button>
<button
id="bme-llm-preset-save-as"
class="bme-config-secondary-btn"
title="另存为新模板"
type="button"
>
<i class="fa-solid fa-file-circle-plus"></i>
<span>另存为</span>
</button>
<button
id="bme-llm-preset-delete"
class="bme-config-secondary-btn"
title="删除当前模板"
type="button"
>
<i class="fa-solid fa-trash-can"></i>
<span>删除</span>
</button>
</div>
</div>
<div class="bme-config-row">
<label for="bme-setting-llm-url">LLM API 地址</label>
<input

304
panel.js
View File

@@ -1866,7 +1866,11 @@ function _bindActions() {
}
function _refreshConfigTab() {
const settings = _getSettings?.() || {};
let settings = _normalizeLlmPresetSettings(_getSettings?.() || {});
const resolvedActiveLlmPreset = _resolveActiveLlmPresetName(settings);
if (resolvedActiveLlmPreset !== String(settings.llmActivePreset || "")) {
settings = _patchSettings({ llmActivePreset: resolvedActiveLlmPreset });
}
_refreshPlannerLauncher();
_setCheckboxValue("bme-setting-enabled", settings.enabled ?? true);
@@ -2148,6 +2152,8 @@ function _refreshConfigTab() {
_setInputValue("bme-setting-llm-url", settings.llmApiUrl || "");
_setInputValue("bme-setting-llm-key", settings.llmApiKey || "");
_setInputValue("bme-setting-llm-model", settings.llmModel || "");
_populateLlmPresetSelect(settings.llmPresets || {}, resolvedActiveLlmPreset);
_syncLlmPresetControls(resolvedActiveLlmPreset);
_setInputValue("bme-setting-timeout-ms", settings.timeoutMs ?? 300000);
_setInputValue("bme-setting-embed-url", settings.embeddingApiUrl || "");
@@ -2562,15 +2568,144 @@ function _bindConfigControls() {
_patchSettings({ reflectEveryN: value }),
);
bindText("bme-setting-llm-url", (value) =>
_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) => {

View File

@@ -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;