import { apiProfileUsageRank, filterUsedApiProfiles, forgetApiProfileUsage, loadApiProfileUsage, pruneApiProfileUsage, recordApiProfileUsage, seedApiProfileUsage, sortByApiProfileUsage, } from "./api-profile-usage.js"; import { readSniffModels, writeSniffModels } from "./model-sniff-cache.js"; let state = { settings: { activeProfileId: null, activePresetId: null }, profiles: [], groups: [], presets: [], personas: [], activePersonaId: null, creationDefaultId: null, }; let editingProfileId = null; /** 当前编辑框里嗅探到的型号,仅用于按 Model 输入筛选 */ let dialogKnownModels = []; let dialogSniffBase = ""; /** 本次服务进程的启动标记;对不上就不读嗅探缓存 */ let sniffBootId = ""; let sniffRestoreSeq = 0; /** 组 id → 是否展开显示该组全部配置(默认只显示曾选用);有搜索词时按匹配显示 */ const showAllProfilesByGroup = new Set(); let profileSearchQuery = ""; let editingPersonaId = null; let activeSection = "api"; let lastImportedPresetId = null; /** presetId → entries API payload */ const expandedPresetEntries = new Map(); /** presetId → 当前展开编辑的 entry id(一次一条) */ const openPresetEntryId = new Map(); const profilesListEl = document.getElementById("profiles-list"); const profileSearchEl = document.getElementById("profile-search"); const personasListEl = document.getElementById("personas-list"); const presetsListEl = document.getElementById("presets-list"); const importReportEl = document.getElementById("import-report"); const profileDialog = document.getElementById("profile-dialog"); const profileForm = document.getElementById("profile-form"); const profileDialogTitle = document.getElementById("profile-dialog-title"); const personaDialog = document.getElementById("persona-dialog"); const personaForm = document.getElementById("persona-form"); const personaDialogTitle = document.getElementById("persona-dialog-title"); const presetFileEl = document.getElementById("preset-file"); const panelTitleEl = document.getElementById("panel-title"); const panelSubtitleEl = document.getElementById("panel-subtitle"); const panelActionsEl = document.getElementById("panel-actions"); const sectionApiEl = document.getElementById("section-api"); const sectionPersonaEl = document.getElementById("section-persona"); const sectionPreferenceEl = document.getElementById("section-preference"); const sectionPresetEl = document.getElementById("section-preset"); const sectionStorageEl = document.getElementById("section-storage"); const preferenceCollectEveryEl = document.getElementById("preference-collect-every"); const activeSettingsBarEl = document.getElementById("active-settings-bar"); const settingsToastEl = document.getElementById("settings-toast"); const contextTraceKeepEl = document.getElementById("context-trace-keep"); const probeDialog = document.getElementById("preset-probe-dialog"); const probePresetNameEl = document.getElementById("probe-preset-name"); const probeMessageEl = document.getElementById("probe-message"); const probeLoreBeforeEl = document.getElementById("probe-lore-before"); const probeHistoryEl = document.getElementById("probe-history"); const probeLoreAfterEl = document.getElementById("probe-lore-after"); const probePostTurnEl = document.getElementById("probe-post-turn"); const probeSendEl = document.getElementById("probe-send"); const probeStatusEl = document.getElementById("probe-status"); const probeMessagesEl = document.getElementById("probe-messages"); const probeReplyWrapEl = document.getElementById("probe-reply-wrap"); const probeReplyEl = document.getElementById("probe-reply"); const DEFAULT_PROBE_CONTEXT = { message: "我推开门。", loreBefore: "写作要求:对本轮输入做忠实扩写;只展开已给出的动作与信息,不另起情节。", history: "扩写范围仅限本轮输入,勿补前因后果或无关对话。", loreAfter: "", postTurn: "", }; let probingPresetId = null; let probeBusy = false; async function api(path, options = {}) { const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options, }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "请求失败"); return data; } function escapeHtml(text) { return String(text) .replace(/&/g, "&") .replace(//g, ">"); } function maskKey(key) { if (!key) return "(未设置)"; if (key.length <= 8) return "****"; return `${key.slice(0, 4)}…${key.slice(-4)}`; } let toastTimer = null; function showToast(message, isError = false) { settingsToastEl.hidden = false; settingsToastEl.textContent = message; settingsToastEl.classList.toggle("error", isError); clearTimeout(toastTimer); toastTimer = setTimeout(() => { settingsToastEl.hidden = true; }, 4000); } function renderActiveBar() { const profile = state.profiles.find( (p) => p.id === state.settings.activeProfileId, ); const preset = state.presets.find( (p) => p.id === state.settings.activePresetId, ); const persona = state.personas.find((p) => p.id === state.activePersonaId); activeSettingsBarEl.innerHTML = ` 当前生效 API: ${profile ? escapeHtml(`${profile.name} · ${profile.model}`) : "未选用"} 角色: ${persona ? escapeHtml(persona.name) : "未选用"} 预设: ${preset ? escapeHtml(preset.name) : "未选用"} `; } function switchSection(section) { activeSection = section; document.querySelectorAll(".st-rail-item").forEach((el) => { el.classList.toggle("active", el.dataset.section === section); }); sectionApiEl.classList.toggle("hidden", section !== "api"); sectionPersonaEl?.classList.toggle("hidden", section !== "persona"); sectionPreferenceEl?.classList.toggle("hidden", section !== "preference"); sectionPresetEl.classList.toggle("hidden", section !== "preset"); sectionStorageEl?.classList.toggle("hidden", section !== "storage"); renderPanelHeader(); } function renderPanelHeader() { panelActionsEl.innerHTML = ""; if (activeSection === "api") { panelTitleEl.textContent = "API"; panelSubtitleEl.textContent = "一组一张卡,一行一个型号。添加型号不会改掉已有行,探测也留在原行"; const btn = document.createElement("button"); btn.type = "button"; btn.className = "btn-primary"; btn.textContent = "新增"; btn.addEventListener("click", () => openProfileDialog()); panelActionsEl.appendChild(btn); } else if (activeSection === "persona") { panelTitleEl.textContent = "用户角色"; panelSubtitleEl.textContent = "游玩用当前选用;创作用「创作默认」(建议名为 @玩家)"; const btn = document.createElement("button"); btn.type = "button"; btn.className = "btn-primary"; btn.textContent = "新增"; btn.addEventListener("click", () => openPersonaDialog()); panelActionsEl.appendChild(btn); } else if (activeSection === "preference") { panelTitleEl.textContent = "用户偏好"; panelSubtitleEl.textContent = "采集间隔;条目在「库 · 偏好库」管理"; } else if (activeSection === "preset") { panelTitleEl.textContent = "预设"; panelSubtitleEl.textContent = "导入后可启用/编辑条目;选用后注入全部 LLM 请求。再点「使用中」即不使用任何预设"; const btn = document.createElement("button"); btn.type = "button"; btn.className = "btn-primary"; btn.textContent = "导入 JSON"; btn.addEventListener("click", () => presetFileEl.click()); panelActionsEl.appendChild(btn); } else { panelTitleEl.textContent = "上下文"; panelSubtitleEl.textContent = "控制每个对话保留多少条完整 prompt 痕迹"; } } function activateButtonHtml(active, id, action, { toggleOff = false } = {}) { if (active && toggleOff) { return ``; } if (active) { return ``; } return ``; } function capabilityBadge(caps) { if (!caps || !caps.testedAt) { return `
能力:未探测(选用前建议点「探测」)
`; } const bit = (label, status) => { const cls = status === "ok" ? "cap-ok" : status === "fail" ? "cap-fail" : "cap-unk"; return `${escapeHtml(label)}:${escapeHtml(status)}`; }; const mode = caps.jsonSchema === "ok" ? "JSON Schema" : caps.forcedTool === "ok" ? "强制 Tool" : caps.jsonObject === "ok" ? "JSON Object" : "纯文本"; return `
投递:${escapeHtml(mode)} ${bit("schema", caps.jsonSchema)} ${bit("tool", caps.forcedTool)} ${bit("json", caps.jsonObject)}
`; } function escapeAttr(text) { return escapeHtml(text).replace(/"/g, """); } function profileMatchesQuery(profile, group, query) { if (!query) return true; const hay = [ group?.name, group?.baseUrl, profile.name, profile.model, profile.baseUrl, profile.reasoningEffort, ] .map((part) => String(part || "").toLowerCase()) .join("\n"); return hay.includes(query); } function capabilityLine(caps) { if (!caps || !caps.testedAt) { return `能力未探测`; } const mode = caps.jsonSchema === "ok" ? "JSON Schema" : caps.forcedTool === "ok" ? "强制 Tool" : caps.jsonObject === "ok" ? "JSON Object" : "纯文本"; return `投递 ${escapeHtml(mode)}`; } function renderModelRow(p) { const active = p.id === state.settings.activeProfileId; const used = Boolean(loadApiProfileUsage()[p.id]); const row = document.createElement("div"); row.className = `profile-model-row${active ? " is-active" : ""}`; const metaParts = [ p.name && p.name !== p.model ? p.name : "", p.reasoningEffort ? `思考 ${p.reasoningEffort}` : "", ].filter(Boolean); const forget = !active && used ? `` : ""; row.innerHTML = `
${escapeHtml(p.model || p.name || "未命名")} ${active ? '当前' : ""}
${metaParts.length ? `${escapeHtml(metaParts.join(" · "))} · ` : ""} ${capabilityLine(p.capabilities)}
${activateButtonHtml(active, p.id, "activate-profile")} ${forget}
`; return row; } function visibleGroupMembers(members, group, usage, activeId) { const query = profileSearchQuery.trim().toLowerCase(); const showAll = showAllProfilesByGroup.has(group.id) || Boolean(query); let list = showAll ? sortByApiProfileUsage(members, (profile) => apiProfileUsageRank(profile.id, usage), ) : filterUsedApiProfiles(members, usage, activeId); if (query) { list = list.filter((profile) => profileMatchesQuery(profile, group, query)); } return { list, showAll, query }; } function renderProfiles() { profilesListEl.innerHTML = ""; if (!state.profiles.length) { profilesListEl.innerHTML = '

暂无配置,点击右上角「新增配置」。

'; return; } const usage = loadApiProfileUsage(); const activeId = state.settings.activeProfileId; const query = profileSearchQuery.trim().toLowerCase(); const groups = state.groups.length ? state.groups : [{ id: "", name: "未分组", baseUrl: "" }]; const seen = new Set(); let shownGroups = 0; for (const group of groups) { const members = state.profiles.filter((p) => (p.groupId || "") === group.id); if (!members.length) continue; for (const member of members) seen.add(member.id); const { list: visible, showAll } = visibleGroupMembers( members, group, usage, activeId, ); if (query && !visible.length) continue; const hasActive = members.some((p) => p.id === activeId); const sample = members[0]; const hidden = members.length - visible.length; const countLabel = query ? `匹配 ${visible.length} / ${members.length}` : showAll ? `${members.length} 个模型` : hidden > 0 ? `曾用 ${visible.length} / 共 ${members.length}` : `${members.length} 个模型`; const toggle = !query && members.length > 1 ? `` : ""; const card = document.createElement("section"); card.className = `config-card profile-group-card${hasActive ? " active" : ""}`; card.innerHTML = `
${escapeHtml( (group.name || "G").charAt(0).toUpperCase(), )}
${hasActive ? '当前组' : ""} ${escapeHtml(countLabel)} ${toggle} ${ group.id ? `` : "" }
${escapeHtml(group.baseUrl || sample?.baseUrl || "")} ${sample ? ` · Key: ${escapeHtml(maskKey(sample.apiKey))}` : ""}
`; if (!visible.length) { const empty = document.createElement("p"); empty.className = "profile-group-empty"; empty.textContent = query ? "没有匹配的模型" : "本组还没有曾选用过的型号。点「显示全部」,或「添加型号」。"; card.appendChild(empty); } else { const rows = document.createElement("div"); rows.className = "profile-model-rows"; for (const p of visible) rows.appendChild(renderModelRow(p)); card.appendChild(rows); } profilesListEl.appendChild(card); shownGroups += 1; } const loose = state.profiles.filter((p) => !seen.has(p.id)); const looseVisible = query ? loose.filter((p) => profileMatchesQuery(p, { name: "未分组" }, query)) : loose; if (looseVisible.length) { const card = document.createElement("section"); card.className = "config-card profile-group-card"; card.innerHTML = `
?
未分组 ${looseVisible.length} 个模型
`; const rows = document.createElement("div"); rows.className = "profile-model-rows"; for (const p of looseVisible) rows.appendChild(renderModelRow(p)); card.appendChild(rows); profilesListEl.appendChild(card); shownGroups += 1; } if (!shownGroups) { profilesListEl.innerHTML = query ? `

没有匹配「${escapeHtml(profileSearchQuery.trim())}」的配置。

` : '

暂无配置,点击右上角「新增配置」。

'; } } function renderPresets() { presetsListEl.innerHTML = ""; if (!state.presets.length) { presetsListEl.innerHTML = '

暂无预设,点击右上角「导入 JSON」。

'; return; } for (const p of state.presets) { const active = p.id === state.settings.activePresetId; const expanded = expandedPresetEntries.get(p.id); const card = document.createElement("div"); card.className = `config-card${active ? " active" : ""}`; card.innerHTML = `
P
${escapeHtml(p.name)} ${active ? '当前' : ""}
${escapeHtml(p.source)} · 共 ${p.entryCount ?? "?"} 条 · 启用 ${p.enabledCount} · 注入 ${p.injectingCount ?? "?"} · ${escapeHtml(p.importedAt?.slice(0, 10) ?? "")}
${activateButtonHtml(active, p.id, "activate-preset", { toggleOff: true })}
`; presetsListEl.appendChild(card); if (expanded) { renderPresetEntriesPanel(p.id, expanded); } } } const GENERATION_FIELDS = [ { key: "temperature", label: "temperature", step: "0.01", placeholder: "如 1" }, { key: "topP", label: "top P", step: "0.01", placeholder: "如 0.95" }, { key: "topK", label: "top K", step: "1", placeholder: "可选" }, { key: "minP", label: "min P", step: "0.01", placeholder: "可选" }, { key: "frequencyPenalty", label: "frequency penalty", step: "0.01", placeholder: "可选", }, { key: "presencePenalty", label: "presence penalty", step: "0.01", placeholder: "可选", }, { key: "repetitionPenalty", label: "repetition penalty", step: "0.01", placeholder: "可选", }, { key: "maxContextTokens", label: "最大上下文", step: "1", placeholder: "输入预算", }, { key: "maxOutputTokens", label: "最大输出", step: "1", placeholder: "如 60000", }, { key: "seed", label: "seed", step: "1", placeholder: "可选" }, { key: "variants", label: "n / variants", step: "1", placeholder: "可选", }, ]; function generationFieldValue(gen, key) { const v = gen?.[key]; return v === undefined || v === null ? "" : String(v); } function renderGenerationForm(gen) { const g = gen ?? {}; const numberFields = GENERATION_FIELDS.map( (f) => ` `, ).join(""); return `
生成参数 上半 · 采样与容量;空字段表示不发送
${numberFields}
`; } function collectGenerationFromPanel(panel) { const generation = {}; for (const input of panel.querySelectorAll("[data-gen-field]")) { const key = input.dataset.genField; if (!key) continue; if (input.type === "checkbox") { if (input.checked) generation.stream = true; continue; } const raw = input.value.trim(); if (!raw) continue; if (key === "verbosity") { generation[key] = raw; } else { const n = Number(raw); if (Number.isFinite(n)) generation[key] = n; } } return generation; } function renderPresetEntriesPanel(presetId, data) { const panel = document.getElementById(`preset-entries-${presetId}`); if (!panel || !data) return; const openId = openPresetEntryId.get(presetId) ?? null; const entriesHtml = data.entries .map((entry, idx) => { const roleClass = `role-${entry.role}`; const open = openId === entry.id; const preview = (entry.content || "").trim(); const previewText = preview ? preview.length > 72 ? `${preview.slice(0, 72)}…` : preview : entry.marker ? "(marker · 无正文)" : "(空)"; const status = entry.enabled ? entry.willInject ? '将注入' : '已启用 · 不注入' : '未启用'; return `
${idx + 1} ${escapeHtml(entry.role)} ${escapeHtml(entry.name)} ${status}
${open ? "" : `

${escapeHtml(previewText)}

`}
`; }) .join(""); panel.innerHTML = `
${renderGenerationForm(data.generation)}

下半 · 共 ${data.entries.length} 条 · 启用 ${data.enabledCount ?? 0} · 注入 ${data.injectingCount ?? 0}。点条目展开编辑;启用开关即时保存。

${entriesHtml || '

无条目

'}
`; } async function refreshPresetEntries(presetId) { const data = await api(`/api/presets/${encodeURIComponent(presetId)}/entries`); expandedPresetEntries.set(presetId, data); // 同步卡片上的计数 const cardMeta = state.presets.find((p) => p.id === presetId); if (cardMeta) { cardMeta.enabledCount = data.enabledCount; cardMeta.injectingCount = data.injectingCount; cardMeta.entryCount = data.entries?.length; } renderPresets(); } async function togglePresetEntries(presetId) { if (expandedPresetEntries.has(presetId)) { expandedPresetEntries.delete(presetId); openPresetEntryId.delete(presetId); renderPresets(); return; } await refreshPresetEntries(presetId); } async function patchPresetEntry(presetId, patch) { const data = await api(`/api/presets/${encodeURIComponent(presetId)}/entries`, { method: "PATCH", body: JSON.stringify({ entry: patch }), }); expandedPresetEntries.set(presetId, data); const cardMeta = state.presets.find((p) => p.id === presetId); if (cardMeta) { cardMeta.enabledCount = data.enabledCount; cardMeta.injectingCount = data.injectingCount; cardMeta.entryCount = data.entries?.length; } renderPresets(); const n = data.reloadedSessions ?? 0; showToast( n > 0 ? `已保存,并热更新 ${n} 个会话` : "已保存(选用此预设后才会进入请求)", ); return data; } async function patchPresetGeneration(presetId, generation) { const data = await api(`/api/presets/${encodeURIComponent(presetId)}/entries`, { method: "PATCH", body: JSON.stringify({ generation }), }); expandedPresetEntries.set(presetId, data); renderPresets(); const n = data.reloadedSessions ?? 0; showToast( n > 0 ? `生成参数已保存,并热更新 ${n} 个会话` : "生成参数已保存(选用此预设后才会进入请求)", ); return data; } async function loadAll() { const data = await api("/api/settings"); state.settings = data.settings; state.profiles = data.profiles; state.groups = data.groups || []; pruneApiProfileUsage(new Set(state.profiles.map((p) => p.id))); seedApiProfileUsage(state.settings.activeProfileId); state.presets = data.presets; sniffBootId = typeof data.bootId === "string" ? data.bootId : ""; try { const personasData = await api("/api/personas"); state.personas = personasData.personas ?? []; state.activePersonaId = personasData.activeId ?? null; state.creationDefaultId = personasData.creationDefaultId ?? null; } catch { state.personas = []; state.activePersonaId = null; } try { const prefData = await api("/api/preferences"); if (preferenceCollectEveryEl) { preferenceCollectEveryEl.value = String( prefData.preferenceCollectEveryTurns ?? state.settings.preferenceCollectEveryTurns ?? 10, ); } } catch { /* ignore */ } if (contextTraceKeepEl) { contextTraceKeepEl.value = String( state.settings.contextTraceKeepLatest ?? 5, ); } renderActiveBar(); renderProfiles(); renderPersonas(); renderPresets(); } function renderPersonas() { if (!personasListEl) return; personasListEl.innerHTML = ""; if (!state.personas.length) { personasListEl.innerHTML = `
还没有用户角色。点右上角「新增」。
`; return; } for (const p of state.personas) { const active = p.id === state.activePersonaId; const creationDefault = p.id === state.creationDefaultId; const card = document.createElement("div"); card.className = `config-card persona-config-card${active ? " is-active" : ""}`; const desc = (p.description || "").trim(); card.innerHTML = `
${escapeHtml(p.name)}${ creationDefault ? `创作默认` : "" }
${ desc ? escapeHtml(desc.slice(0, 80)) + (desc.length > 80 ? "…" : "") : "(无人设正文)" }
${activateButtonHtml(active, p.id, "activate-persona")}
`; personasListEl.appendChild(card); } } function openPersonaDialog(persona = null) { editingPersonaId = persona?.id ?? null; personaDialogTitle.textContent = persona ? "编辑用户角色" : "新增用户角色"; personaForm.name.value = persona?.name ?? ""; personaForm.description.value = persona?.description ?? ""; personaForm.creationDefault.checked = persona ? persona.id === state.creationDefaultId : false; personaDialog.showModal(); } async function savePersona() { const payload = { name: personaForm.name.value, description: personaForm.description.value, activate: true, creationDefault: Boolean(personaForm.creationDefault?.checked), }; if (editingPersonaId) { await api(`/api/personas/${encodeURIComponent(editingPersonaId)}`, { method: "PUT", body: JSON.stringify(payload), }); await api(`/api/personas/${encodeURIComponent(editingPersonaId)}/activate`, { method: "POST", }); } else { await api("/api/personas", { method: "POST", body: JSON.stringify(payload), }); } personaDialog.close(); await loadAll(); showToast("用户角色已保存"); } const profileModelLockEl = document.getElementById("profile-model-lock"); const profileModelLockValueEl = document.getElementById("profile-model-lock-value"); const profileModelEditorEl = document.getElementById("profile-model-editor"); const profileModelFilterEl = document.getElementById("profile-model-filter"); const addModelDialog = document.getElementById("add-model-dialog"); const addModelGroupEl = document.getElementById("add-model-group"); const addModelFilterEl = document.getElementById("add-model-filter"); const addModelStatusEl = document.getElementById("add-model-status"); const addModelListEl = document.getElementById("add-model-list"); const addModelEmptyEl = document.getElementById("add-model-empty"); const addModelSniffBtn = document.getElementById("add-model-sniff"); let addModelSource = null; let addKnownModels = []; let addSniffBase = ""; let addSniffSeq = 0; const profileSniffStatusEl = document.getElementById("profile-sniff-status"); const profileModelPickEl = document.getElementById("profile-model-pick"); const profileModelListEl = document.getElementById("profile-model-list"); const profileModelEmptyEl = document.getElementById("profile-model-empty"); const profileSniffBtn = document.getElementById("profile-sniff"); const MODEL_MATCH_LIMIT = 48; function setSniffStatus(el, message, isError = false) { if (!el) return; el.hidden = !message; el.textContent = message || ""; el.classList.toggle("is-error", Boolean(isError)); } function uniqueModelIds(ids) { const list = []; const seen = new Set(); for (const raw of ids) { const id = String(raw ?? "").trim(); if (!id || seen.has(id)) continue; seen.add(id); list.push(id); } return list; } function normalizeBaseUrlKey(url) { return String(url || "") .trim() .replace(/\/+$/, "") .toLowerCase(); } /** 同一 Base URL + 同一 Key 共用一份缓存。Key 只存指纹,不存原文。 */ async function sniffCacheKeyFor({ baseUrl, apiKey, profileId, groupId }) { const base = normalizeBaseUrlKey(baseUrl); if (!base) return ""; const typed = String(apiKey || "").trim(); if (typed) return `${base}\nkey:${await apiKeyFingerprint(typed)}`; const ident = groupId || profileId || ""; return ident ? `${base}\ngroup:${ident}` : ""; } function createSniffContext() { const profile = state.profiles.find((item) => item.id === editingProfileId); return { baseUrl: profileForm.baseUrl.value, apiKey: profileForm.apiKey.value, profileId: editingProfileId || "", groupId: profile?.groupId || "", }; } function addSniffContext() { return { baseUrl: addModelSource?.baseUrl || "", apiKey: addModelSource?.apiKey || "", profileId: addModelSource?.id || "", groupId: addModelSource?.groupId || "", }; } function modelsForBase(baseUrl) { const base = normalizeBaseUrlKey(baseUrl); return uniqueModelIds( state.profiles .filter((profile) => normalizeBaseUrlKey(profile.baseUrl) === base) .map((profile) => profile.model), ); } function renderModelCatalog(listEl, emptyEl, { query, chosen, catalog, existing, sniffed = 0, idleEmpty = "先嗅探。输入关键词后,点一条填入完整型号。" }) { if (!listEl || !emptyEl) return; const text = query.trim(); const needle = text.toLowerCase(); if (!needle) { listEl.hidden = true; listEl.innerHTML = ""; emptyEl.hidden = false; emptyEl.textContent = sniffed ? `已有 ${sniffed} 个嗅探结果。输入关键词筛选,例如 gemini。` : idleEmpty; return; } const matched = catalog.filter((model) => model.toLowerCase().includes(needle)); const more = Math.max(0, matched.length - MODEL_MATCH_LIMIT); const shown = matched.slice(0, MODEL_MATCH_LIMIT); listEl.hidden = shown.length === 0; listEl.innerHTML = shown .map((model) => { const on = model === chosen; const owned = existing?.has(model) ? " · 已有" : ""; return ``; }) .join(""); emptyEl.hidden = shown.length > 0 && more === 0; if (!shown.length) { emptyEl.textContent = catalog.length ? `没有名称包含「${text}」的型号` : "还没有可筛选的型号。先嗅探,再输入关键词。"; } else if (more) { emptyEl.textContent = `还有 ${more} 个,继续输入以缩小`; } } function renderCreateModelChoices() { const base = normalizeBaseUrlKey(profileForm.baseUrl.value); const catalog = dialogSniffBase === base ? uniqueModelIds([...modelsForBase(base), ...dialogKnownModels]) : modelsForBase(base); renderModelCatalog(profileModelListEl, profileModelEmptyEl, { query: profileModelFilterEl?.value || "", chosen: profileForm.model.value.trim(), catalog, sniffed: dialogSniffBase === base ? dialogKnownModels.length : 0, }); } function renderAddModelChoices() { const base = normalizeBaseUrlKey(addModelSource?.baseUrl || ""); const catalog = addSniffBase === base ? uniqueModelIds([...modelsForBase(base), ...addKnownModels]) : modelsForBase(base); renderModelCatalog(addModelListEl, addModelEmptyEl, { query: addModelFilterEl?.value || "", chosen: "", catalog, sniffed: addSniffBase === base ? addKnownModels.length : 0, idleEmpty: "先嗅探。输入关键词后,点一条就会新开一行并选用。", existing: new Set( state.profiles .filter((profile) => (profile.groupId || "") === (addModelSource?.groupId || "")) .map((profile) => profile.model), ), }); } async function restoreCreateSniffCache() { const seq = ++sniffRestoreSeq; const ctx = createSniffContext(); const base = normalizeBaseUrlKey(ctx.baseUrl); const cacheKey = await sniffCacheKeyFor(ctx); if (seq !== sniffRestoreSeq) return; const models = uniqueModelIds(readSniffModels(sniffBootId, cacheKey)); dialogSniffBase = models.length ? base : ""; dialogKnownModels = models; setSniffStatus( profileSniffStatusEl, models.length ? `沿用本次启动的嗅探结果(${models.length} 个),重启程序后会清空。` : "", ); renderCreateModelChoices(); } async function restoreAddSniffCache() { const seq = ++addSniffSeq; const ctx = addSniffContext(); const base = normalizeBaseUrlKey(ctx.baseUrl); const cacheKey = await sniffCacheKeyFor(ctx); if (seq !== addSniffSeq) return; const models = uniqueModelIds(readSniffModels(sniffBootId, cacheKey)); addSniffBase = models.length ? base : ""; addKnownModels = models; setSniffStatus( addModelStatusEl, models.length ? `沿用本次启动的嗅探结果(${models.length} 个),重启程序后会清空。` : "", ); renderAddModelChoices(); } function setProfileModelMode(profile) { const editing = Boolean(profile); if (profileModelLockEl) profileModelLockEl.hidden = !editing; if (profileModelEditorEl) profileModelEditorEl.hidden = editing; if (profileModelLockValueEl) { profileModelLockValueEl.textContent = profile?.model || ""; } if (profileForm.model) profileForm.model.disabled = editing; } function openProfileDialog(profile = null) { editingProfileId = profile?.id ?? null; profileDialogTitle.textContent = profile ? "编辑这一行" : "新增 API"; profileForm.name.value = profile?.name ?? ""; profileForm.baseUrl.value = profile?.baseUrl ?? "https://api.deepseek.com/v1"; profileForm.apiKey.value = profile?.apiKey ?? ""; profileForm.model.value = profile ? profile.model : ""; profileForm.reasoningEffort.value = profile?.reasoningEffort ?? ""; if (profileModelFilterEl) profileModelFilterEl.value = ""; dialogKnownModels = []; dialogSniffBase = ""; setSniffStatus(profileSniffStatusEl, ""); setProfileModelMode(profile); renderCreateModelChoices(); profileDialog.showModal(); if (!profile) void restoreCreateSniffCache(); } async function apiKeyFingerprint(apiKey) { const data = new TextEncoder().encode(apiKey.trim()); const buf = await crypto.subtle.digest("SHA-256", data); return [...new Uint8Array(buf)] .slice(0, 8) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } async function saveProfile(activate) { const model = profileForm.model.value.trim(); if (!editingProfileId && !model) { showToast("先填写完整型号,或从筛选结果里点一条", true); return; } const payload = { name: profileForm.name.value, baseUrl: profileForm.baseUrl.value, reasoningEffort: profileForm.reasoningEffort.value, activate, }; if (!editingProfileId) payload.model = model; const apiKey = profileForm.apiKey.value.trim(); if (apiKey || !editingProfileId) { payload.apiKey = apiKey; } let result; if (editingProfileId) { result = await api(`/api/profiles/${editingProfileId}`, { method: "PUT", body: JSON.stringify(payload), }); const savedId = result.profile?.id || editingProfileId; if (activate) { const activated = await api(`/api/profiles/${encodeURIComponent(savedId)}/activate`, { method: "POST", }); result = { ...result, ...activated, profile: result.profile }; } } else { result = await api("/api/profiles", { method: "POST", body: JSON.stringify(payload), }); } profileDialog.close(); const savedId = result.profile?.id || editingProfileId || result.activeProfileId; if (activate && savedId) recordApiProfileUsage(savedId); await loadAll(); if (activate) { const n = result.reloadedSessions ?? 0; showToast(`已选用并生效${n > 0 ? `,已更新 ${n} 个活跃会话` : ""}`); } else { showToast("已保存。点击「选用并生效」后才会用于 LLM 请求。"); } } async function activateProfile(id) { const result = await api(`/api/profiles/${id}/activate`, { method: "POST" }); recordApiProfileUsage(id); await loadAll(); const n = result.reloadedSessions ?? 0; showToast(`API 配置已生效${n > 0 ? `(${n} 个会话已更新)` : ""}`); } async function activatePreset(id) { const result = await api(`/api/presets/${id}/activate`, { method: "POST" }); await loadAll(); const n = result.reloadedSessions ?? 0; showToast(`预设已生效${n > 0 ? `(${n} 个会话已更新)` : ""}`); } async function deactivatePreset() { const result = await api("/api/presets/deactivate", { method: "POST" }); await loadAll(); const n = result.reloadedSessions ?? 0; showToast(`已不使用预设${n > 0 ? `(${n} 个会话已更新)` : ""}`); } document.querySelectorAll(".st-rail-item").forEach((btn) => { btn.addEventListener("click", () => switchSection(btn.dataset.section)); }); document.getElementById("profile-cancel").addEventListener("click", () => { profileDialog.close(); }); document.getElementById("persona-cancel")?.addEventListener("click", () => { personaDialog?.close(); }); document.getElementById("btn-save-pref-every")?.addEventListener("click", async () => { try { const n = Number(preferenceCollectEveryEl?.value ?? 10); const data = await api("/api/settings", { method: "PUT", body: JSON.stringify({ preferenceCollectEveryTurns: n }), }); state.settings = data.settings; if (preferenceCollectEveryEl) { preferenceCollectEveryEl.value = String( data.settings.preferenceCollectEveryTurns ?? 10, ); } showToast( `已保存:每隔 ${data.settings.preferenceCollectEveryTurns} 回合采集偏好`, ); } catch (err) { showToast(err.message, true); } }); personaForm?.addEventListener("submit", async (e) => { e.preventDefault(); try { await savePersona(); } catch (err) { showToast(err.message, true); } }); personasListEl?.addEventListener("change", async (e) => { const input = e.target.closest("input[data-action='creation-default']"); if (!input) return; const id = input.dataset.id; try { await api(`/api/personas/${encodeURIComponent(id)}`, { method: "PUT", body: JSON.stringify({ creationDefault: Boolean(input.checked) }), }); await loadAll(); showToast(input.checked ? "已设为创作默认" : "已取消创作默认"); } catch (err) { showToast(err.message, true); await loadAll(); } }); personasListEl?.addEventListener("click", async (e) => { const btn = e.target.closest("[data-action]"); if (!btn) return; if (btn.matches("input[data-action='creation-default']")) return; const id = btn.dataset.id; const action = btn.dataset.action; try { if (action === "activate-persona") { await api(`/api/personas/${encodeURIComponent(id)}/activate`, { method: "POST", }); await loadAll(); showToast("用户角色已选用"); } else if (action === "edit-persona") { openPersonaDialog(state.personas.find((p) => p.id === id)); } else if (action === "delete-persona") { if (!confirm("删除这个用户角色?")) return; await api(`/api/personas/${encodeURIComponent(id)}`, { method: "DELETE" }); await loadAll(); showToast("已删除"); } } catch (err) { showToast(err.message, true); } }); document.getElementById("profile-save").addEventListener("click", () => { saveProfile(false).catch((err) => showToast(err.message, true)); }); profileForm.baseUrl.addEventListener("input", () => { if (editingProfileId) return; void restoreCreateSniffCache(); }); profileForm.apiKey.addEventListener("input", () => { if (editingProfileId) return; void restoreCreateSniffCache(); }); profileForm.model.addEventListener("input", () => { renderCreateModelChoices(); }); profileModelFilterEl?.addEventListener("input", () => { renderCreateModelChoices(); }); profileModelFilterEl?.addEventListener("keydown", (e) => { if (e.key === "Enter") e.preventDefault(); }); async function sniffIntoCatalog({ baseUrl, apiKey, profileId, cacheKey, seq, seqNow, baseStill, onFound, statusEl, button, }) { button.disabled = true; const prevLabel = button.textContent; button.textContent = "嗅探中…"; setSniffStatus(statusEl, "正在请求 /models…"); try { const body = { baseUrl }; if (apiKey) body.apiKey = apiKey; else if (profileId) body.profileId = profileId; const data = await api("/api/profiles/discover-models", { method: "POST", body: JSON.stringify(body), }); const found = uniqueModelIds(Array.isArray(data.models) ? data.models : []); writeSniffModels(sniffBootId, cacheKey, found); if (seq !== seqNow()) return; if (!baseStill()) return; onFound(found); const host = data.url ? data.url.replace(/^https?:\/\//, "") : "/models"; setSniffStatus( statusEl, found.length ? `找到 ${found.length} 个模型(${host})。在筛选框输入关键词,例如 gemini。` : `没有返回模型(${host})`, !found.length, ); } catch (err) { setSniffStatus(statusEl, err.message || "嗅探失败", true); } finally { button.disabled = false; button.textContent = prevLabel || "嗅探"; } } profileSniffBtn?.addEventListener("click", async () => { const baseUrl = profileForm.baseUrl.value.trim(); const apiKey = profileForm.apiKey.value.trim(); if (!baseUrl) { setSniffStatus(profileSniffStatusEl, "先填写 Base URL", true); return; } if (!apiKey && !editingProfileId) { setSniffStatus(profileSniffStatusEl, "先填写 API Key", true); return; } const seq = ++sniffRestoreSeq; const cacheKey = await sniffCacheKeyFor(createSniffContext()); const baseKey = normalizeBaseUrlKey(baseUrl); await sniffIntoCatalog({ baseUrl, apiKey, profileId: editingProfileId, cacheKey, seq, seqNow: () => sniffRestoreSeq, baseStill: () => normalizeBaseUrlKey(profileForm.baseUrl.value) === baseKey, statusEl: profileSniffStatusEl, button: profileSniffBtn, onFound(found) { dialogSniffBase = baseKey; dialogKnownModels = found; renderCreateModelChoices(); }, }); }); profileModelListEl?.addEventListener("click", (e) => { const btn = e.target.closest("[data-pick-model]"); if (!btn) return; profileForm.model.value = btn.getAttribute("data-pick-model") || ""; renderCreateModelChoices(); }); function openAddModelDialog(groupId) { const source = state.profiles.find((profile) => (profile.groupId || "") === groupId); if (!source) { showToast("这一组还没有可复制的地址和 Key", true); return; } const group = state.groups.find((item) => item.id === groupId); addModelSource = source; if (addModelGroupEl) { addModelGroupEl.textContent = `${group?.name || "这一组"} · ${source.baseUrl}`; } if (addModelFilterEl) addModelFilterEl.value = ""; addKnownModels = []; addSniffBase = ""; setSniffStatus(addModelStatusEl, ""); renderAddModelChoices(); addModelDialog?.showModal(); void restoreAddSniffCache(); } async function adoptModel(model) { const source = addModelSource; if (!source || !model) return; const existing = state.profiles.find( (profile) => (profile.groupId || "") === (source.groupId || "") && profile.model === model, ); if (existing) { addModelDialog?.close(); await activateProfile(existing.id); return; } const result = await api("/api/profiles", { method: "POST", body: JSON.stringify({ name: model, baseUrl: source.baseUrl, apiKey: source.apiKey, model, reasoningEffort: source.reasoningEffort || "", activate: true, }), }); addModelDialog?.close(); const savedId = result.profile?.id || result.activeProfileId; if (savedId) recordApiProfileUsage(savedId); await loadAll(); const n = result.reloadedSessions ?? 0; showToast(`已添加并选用 ${model}${n > 0 ? `,已更新 ${n} 个活跃会话` : ""}`); } document.getElementById("add-model-cancel")?.addEventListener("click", () => { addModelDialog?.close(); }); addModelFilterEl?.addEventListener("input", () => { renderAddModelChoices(); }); addModelFilterEl?.addEventListener("keydown", (e) => { if (e.key === "Enter") e.preventDefault(); }); document.getElementById("add-model-form")?.addEventListener("submit", (e) => { e.preventDefault(); }); addModelSniffBtn?.addEventListener("click", async () => { const source = addModelSource; if (!source?.baseUrl) return; const seq = ++addSniffSeq; const cacheKey = await sniffCacheKeyFor(addSniffContext()); const baseKey = normalizeBaseUrlKey(source.baseUrl); await sniffIntoCatalog({ baseUrl: source.baseUrl, apiKey: source.apiKey, profileId: source.id, cacheKey, seq, seqNow: () => addSniffSeq, baseStill: () => normalizeBaseUrlKey(addModelSource?.baseUrl || "") === baseKey, statusEl: addModelStatusEl, button: addModelSniffBtn, onFound(found) { addSniffBase = baseKey; addKnownModels = found; renderAddModelChoices(); }, }); }); addModelListEl?.addEventListener("click", (e) => { const btn = e.target.closest("[data-pick-model]"); if (!btn) return; adoptModel(btn.getAttribute("data-pick-model") || "").catch((err) => showToast(err.message, true), ); }); profileForm.addEventListener("submit", async (e) => { e.preventDefault(); try { await saveProfile(true); } catch (err) { showToast(err.message, true); } }); profilesListEl.addEventListener("change", async (e) => { const input = e.target.closest("[data-action='rename-group']"); if (!input) return; const id = input.dataset.id; const name = input.value.trim(); if (!id || !name) return; const current = state.groups.find((group) => group.id === id); if (current && current.name === name) return; try { const data = await api(`/api/profile-groups/${encodeURIComponent(id)}`, { method: "PUT", body: JSON.stringify({ name }), }); state.groups = data.groups || state.groups; showToast(`组名已改为 ${name}`); } catch (err) { showToast(err.message, true); await loadAll(); } }); profileSearchEl?.addEventListener("input", () => { profileSearchQuery = profileSearchEl.value || ""; renderProfiles(); }); profilesListEl.addEventListener("click", async (e) => { const btn = e.target.closest("[data-action]"); if (!btn) return; const id = btn.dataset.id; const action = btn.dataset.action; try { if (action === "toggle-group-all") { if (showAllProfilesByGroup.has(id)) showAllProfilesByGroup.delete(id); else showAllProfilesByGroup.add(id); renderProfiles(); return; } if (action === "add-model") { openAddModelDialog(id); return; } if (action === "activate-profile") { await activateProfile(id); } else if (action === "edit-profile") { openProfileDialog(state.profiles.find((p) => p.id === id)); } else if (action === "probe-capabilities") { const el = document.getElementById(`test-${id}`); el.textContent = "探测中(含连通性,会打几次短请求)…"; el.className = "test-result"; const result = await api(`/api/profiles/${id}/probe-capabilities`, { method: "POST", }); await loadAll(); const caps = result.profile?.capabilities; const chatOk = caps?.chat === "ok"; const note = !chatOk ? `连接失败${caps?.notes?.[0] ? `:${caps.notes[0]}` : ""}` : result.deliveryModeLabel ? `连接正常 · 投递将用:${result.deliveryModeLabel}` : "探测完成"; const refreshed = document.getElementById(`test-${id}`); if (refreshed) { refreshed.textContent = note; refreshed.className = `test-result ${chatOk ? "ok" : "fail"}`; } showToast(note, !chatOk); } else if (action === "forget-usage") { if (!forgetApiProfileUsage(id)) return; renderProfiles(); showToast("已移出曾用。配置还在,可在「显示全部」里再选用。"); } else if (action === "delete-profile") { if (!confirm("确定删除此 API 配置?")) return; await api(`/api/profiles/${id}`, { method: "DELETE" }); await loadAll(); showToast("已删除"); } } catch (err) { showToast(err.message, true); } }); presetsListEl.addEventListener("click", async (e) => { if (e.target.closest("[data-stop-expand]")) return; const expandHead = e.target.closest('[data-action="expand-entry"]'); if (expandHead) { const panel = expandHead.closest(".preset-entries-inner"); const presetId = panel?.dataset.presetId; const entryId = expandHead.dataset.id; if (!presetId || !entryId) return; const cur = openPresetEntryId.get(presetId); if (cur === entryId) openPresetEntryId.delete(presetId); else openPresetEntryId.set(presetId, entryId); const data = expandedPresetEntries.get(presetId); if (data) renderPresetEntriesPanel(presetId, data); return; } const btn = e.target.closest("[data-action]"); if (!btn) return; const id = btn.dataset.id; const action = btn.dataset.action; try { if (action === "activate-preset") { if (state.settings.activePresetId === id) await deactivatePreset(); else await activatePreset(id); } else if (action === "probe-preset") { openProbeDialog(id); } else if (action === "toggle-preset-entries") { await togglePresetEntries(id); } else if (action === "delete-preset") { if (!confirm("确定删除此预设?")) return; await api(`/api/presets/${id}`, { method: "DELETE" }); expandedPresetEntries.delete(id); openPresetEntryId.delete(id); await loadAll(); showToast("已删除"); } else if (action === "collapse-entry") { const panel = btn.closest(".preset-entries-inner"); const presetId = panel?.dataset.presetId; if (!presetId) return; openPresetEntryId.delete(presetId); const data = expandedPresetEntries.get(presetId); if (data) renderPresetEntriesPanel(presetId, data); } else if (action === "save-entry") { const article = btn.closest(".preset-entry"); const panel = btn.closest(".preset-entries-inner"); const presetId = panel?.dataset.presetId; if (!article || !presetId) return; const name = article.querySelector('[data-field="name"]')?.value ?? ""; const content = article.querySelector('[data-field="content"]')?.value ?? ""; openPresetEntryId.set(presetId, id); await patchPresetEntry(presetId, { id, name, content }); } else if (action === "save-generation") { const panel = btn.closest(".preset-entries-inner"); const presetId = panel?.dataset.presetId; if (!presetId) return; await patchPresetGeneration(presetId, collectGenerationFromPanel(panel)); } } catch (err) { showToast(err.message, true); } }); presetsListEl.addEventListener("keydown", (e) => { if (e.key !== "Enter" && e.key !== " ") return; const head = e.target.closest?.('[data-action="expand-entry"]'); if (!head) return; e.preventDefault(); head.click(); }); presetsListEl.addEventListener("change", async (e) => { const input = e.target.closest('input[data-action="toggle-entry"]'); if (!input) return; const article = input.closest(".preset-entry"); const panel = input.closest(".preset-entries-inner"); const presetId = panel?.dataset.presetId; const id = input.dataset.id; if (!presetId || !id) return; try { await patchPresetEntry(presetId, { id, enabled: input.checked }); } catch (err) { input.checked = !input.checked; showToast(err.message, true); } }); presetFileEl.addEventListener("change", async () => { const file = presetFileEl.files?.[0]; if (!file) return; try { const text = await file.text(); const raw = JSON.parse(text); const name = file.name.replace(/\.json$/i, ""); const report = await api("/api/presets/import", { method: "POST", body: JSON.stringify({ raw, name }), }); lastImportedPresetId = report.preset.id; importReportEl.hidden = false; importReportEl.innerHTML = `
${[ `已导入: ${report.preset.name}`, `条目: ${report.promptCount},启用: ${report.enabledCount}`, report.warnings.length ? `警告: ${report.warnings.join("; ")}` : "", ] .filter(Boolean) .join("
")}
`; document .getElementById("activate-imported-preset") ?.addEventListener("click", async () => { try { await activatePreset(lastImportedPresetId); } catch (err) { showToast(err.message, true); } }); await loadAll(); showToast("预设已导入,请点击「选用此预设并生效」"); } catch (err) { showToast(err.message, true); } finally { presetFileEl.value = ""; } }); function resetProbeForm() { probeMessageEl.value = DEFAULT_PROBE_CONTEXT.message; probeLoreBeforeEl.value = DEFAULT_PROBE_CONTEXT.loreBefore; probeHistoryEl.value = DEFAULT_PROBE_CONTEXT.history; probeLoreAfterEl.value = DEFAULT_PROBE_CONTEXT.loreAfter; probePostTurnEl.value = DEFAULT_PROBE_CONTEXT.postTurn; probeStatusEl.hidden = true; probeStatusEl.textContent = ""; probeReplyWrapEl.hidden = true; probeReplyEl.textContent = ""; probeMessagesEl.innerHTML = '

发送后显示实际拼进请求的消息。

'; } function openProbeDialog(presetId) { const preset = state.presets.find((p) => p.id === presetId); probingPresetId = presetId; probePresetNameEl.textContent = preset?.name ?? presetId; resetProbeForm(); probeDialog.showModal(); probeMessageEl.focus(); probeMessageEl.select(); } function renderProbeMessages(messages) { if (!messages?.length) { probeMessagesEl.innerHTML = '

这次没有拼出任何消息。

'; return; } probeMessagesEl.innerHTML = messages .map( (m, i) => `
${escapeHtml(m.role)} #${i + 1}
${escapeHtml(m.content)}
`, ) .join(""); } function setProbeBusy(busy) { probeBusy = busy; probeSendEl.disabled = busy; probeSendEl.textContent = busy ? "发送中…" : "发送一轮"; } async function sendProbe() { if (probeBusy || !probingPresetId) return; const message = probeMessageEl.value.trim(); if (!message) { probeStatusEl.hidden = false; probeStatusEl.textContent = "请填写本轮输入"; probeMessageEl.focus(); return; } setProbeBusy(true); probeStatusEl.hidden = false; probeStatusEl.textContent = "正在拼装上下文…"; probeReplyWrapEl.hidden = true; try { const result = await api(`/api/presets/${probingPresetId}/probe`, { method: "POST", body: JSON.stringify({ message, loreBefore: probeLoreBeforeEl.value, history: probeHistoryEl.value, loreAfter: probeLoreAfterEl.value, postTurn: probePostTurnEl.value, }), }); renderProbeMessages(result.messages); if (result.reply) { probeReplyWrapEl.hidden = false; probeReplyEl.textContent = result.reasoning ? `${result.reasoning}\n\n——\n\n${result.reply}` : result.reply; } if (result.error) { probeStatusEl.textContent = `上下文已拼装,生成失败:${result.error}`; } else if (result.skipReason) { probeStatusEl.textContent = result.skipReason; } else if (result.completed) { probeStatusEl.textContent = `已发送 ${result.messages.length} 条`; } else { probeStatusEl.textContent = `已拼装 ${result.messages.length} 条`; } } catch (err) { probeStatusEl.textContent = err.message; } finally { setProbeBusy(false); } } document.getElementById("probe-close")?.addEventListener("click", () => { probeDialog.close(); }); probeSendEl?.addEventListener("click", () => { sendProbe().catch((err) => showToast(err.message, true)); }); probeMessageEl?.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); sendProbe().catch((err) => showToast(err.message, true)); } }); document.getElementById("btn-save-context-keep")?.addEventListener("click", async () => { try { const n = Number(contextTraceKeepEl?.value ?? 5); const data = await api("/api/settings", { method: "PUT", body: JSON.stringify({ contextTraceKeepLatest: n }), }); state.settings = data.settings; if (contextTraceKeepEl) { contextTraceKeepEl.value = String(data.settings.contextTraceKeepLatest ?? 5); } showToast(`已保存:每个对话保留最新 ${data.settings.contextTraceKeepLatest} 条上下文`); } catch (err) { showToast(err.message, true); } }); document.getElementById("btn-prune-context")?.addEventListener("click", async () => { try { const r = await api("/api/settings/context-traces/prune", { method: "POST" }); showToast( `已修剪 ${r.sessions} 个会话:保留 ${r.kept} 条痕迹,清除 ${r.cleared} 条`, ); } catch (err) { showToast(err.message, true); } }); document.getElementById("btn-clear-context")?.addEventListener("click", async () => { if (!confirm("清除所有已打开会话中保存的 LLM 请求上下文?消息正文不受影响。")) { return; } try { const r = await api("/api/settings/context-traces/clear", { method: "POST" }); showToast(`已清除 ${r.sessions} 个会话中的 ${r.cleared} 条上下文痕迹`); } catch (err) { showToast(err.message, true); } }); renderPanelHeader(); loadAll().catch((err) => { showToast(`加载设置失败: ${err.message}`, true); });