const bridge = window.AstrBotPluginPage; const state = { bindings: [], subscriptions: [], aliases: [], kungfu: [], servers: [], events: {}, free_event_actions: [], session_control: { mode: "all", entries: [] }, legacy_bilei: [], token_stats: null, cache: { defaults: { api: 300, image: 300 }, limits: { api_memory_max_mb: 16, api_max_entries: 1024, image_max_mb: 512 }, api: [], images: [], stats: {}, }, }; const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null, subscriptionSession: null }; let subscriptionSaving = false; const restoreConfirmationTimers = new WeakMap(); let toastTimer; const byId = (id) => document.getElementById(id); function formatUsageCount(value) { return Number.isSafeInteger(value) && value >= 0 ? value.toLocaleString("zh-CN") : "—"; } function formatBytes(value) { const bytes = Number(value) || 0; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`; return `${(bytes / 1024 ** 3).toFixed(1)} GB`; } function renderTokenStats() { const stats = state.token_stats; byId("token-level").textContent = Number.isSafeInteger(stats?.level) ? `LV.${stats.level}` : "—"; byId("token-used").textContent = formatUsageCount(stats?.used); byId("token-remaining").textContent = formatUsageCount(stats?.remaining); const status = byId("token-valid"); status.classList.remove("token-status--valid", "token-status--invalid"); if (stats?.valid === true) { status.textContent = "有效"; status.classList.add("token-status--valid"); } else if (stats?.valid === false) { status.textContent = "无效"; status.classList.add("token-status--invalid"); } else { status.textContent = "未获取"; } } function showToast(message, isError = false) { const toast = byId("toast"); toast.textContent = message; toast.classList.toggle("is-error", isError); toast.classList.add("is-visible"); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove("is-visible"), 2600); } function button(label, className, onClick) { const element = document.createElement("button"); element.type = "button"; element.className = `link-button ${className || ""}`.trim(); element.textContent = label; element.addEventListener("click", onClick); return element; } function emptyRow(columnCount, message) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = columnCount; cell.className = "empty"; cell.textContent = message; row.append(cell); return row; } function aliasText(aliases) { return aliases.length ? aliases.join("、") : "无"; } function inlineAliasEditor(aliases, label, onSave, onCancel) { const input = document.createElement("input"); input.className = "inline-editor"; input.value = aliases.join(", "); input.placeholder = "多个别名用逗号分隔"; input.setAttribute("aria-label", label); const saveButton = button("保存", "", async () => { saveButton.disabled = true; cancelButton.disabled = true; const saved = await onSave(input.value); if (!saved) { saveButton.disabled = false; cancelButton.disabled = false; input.focus(); } }); const cancelButton = button("取消", "", onCancel); input.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); saveButton.click(); } else if (event.key === "Escape") { event.preventDefault(); cancelButton.click(); } }); queueMicrotask(() => input.focus()); return { input, controls: [saveButton, cancelButton] }; } function createServerSelect(selectedServer = "", label = "绑定区服") { const select = document.createElement("select"); select.className = "inline-editor"; select.required = true; select.setAttribute("aria-label", label); const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "请选择标准区服"; placeholder.disabled = true; placeholder.defaultSelected = true; select.append(placeholder); state.servers.forEach((server) => { const option = document.createElement("option"); option.value = server; option.textContent = server; select.append(option); }); select.value = state.servers.includes(selectedServer) ? selectedServer : ""; return select; } function inlineServerEditor(item, onSave, onCancel) { const select = createServerSelect(item.server, `${item.session_id}的绑定区服`); const saveButton = button("保存", "", async () => { if (!select.reportValidity()) return; select.disabled = true; saveButton.disabled = true; cancelButton.disabled = true; const saved = await onSave(select.value); if (!saved) { select.disabled = false; saveButton.disabled = false; cancelButton.disabled = false; select.focus(); } }); const cancelButton = button("取消", "", onCancel); select.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); saveButton.click(); } else if (event.key === "Escape") { event.preventDefault(); cancelButton.click(); } }); queueMicrotask(() => select.focus()); return { select, controls: [saveButton, cancelButton] }; } function bindingMap() { return new Map(state.bindings.map((item) => [item.session_id, item.server])); } function renderServerOptions() { const select = byId("binding-server"); const currentValue = select.value; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "请选择标准区服"; placeholder.disabled = true; placeholder.defaultSelected = true; select.replaceChildren(placeholder, ...state.servers.map((server) => { const option = document.createElement("option"); option.value = server; option.textContent = server; return option; })); select.value = state.servers.includes(currentValue) ? currentValue : ""; } function renderSessionOptions() { const sessionIds = new Set([ ...state.bindings.map((item) => item.session_id), ...state.subscriptions.map((item) => item.session_id), ...state.session_control.entries.map((item) => item.session_id), ]); byId("session-options").replaceChildren(...[...sessionIds].sort().map((sessionId) => { const option = document.createElement("option"); option.value = sessionId; return option; })); } function controlModeCopy(mode) { if (mode === "whitelist") { return "只有白名单中的会话可以使用插件和接收事件推送;白名单为空时不放行任何会话。"; } if (mode === "blacklist") { return "黑名单中的会话会被拦截;黑名单为空时放行全部会话。"; } return "所有会话都可以使用插件并接收已订阅的事件推送;下方名单暂不生效。"; } function controlModeLabel(mode) { if (mode === "whitelist") return "白名单"; if (mode === "blacklist") return "黑名单"; return "全部会话"; } function updateModeSelection(selectedMode) { const activeMode = state.session_control?.mode || "all"; document.querySelectorAll(".mode-option").forEach((option) => { const input = option.querySelector('input[name="control_mode"]'); option.classList.toggle("is-selected", input?.value === selectedMode); option.classList.toggle("is-active-mode", input?.value === activeMode); }); const saveButton = byId("control-mode-save"); saveButton.textContent = selectedMode === activeMode ? "当前模式已生效" : `切换为${controlModeLabel(selectedMode)}`; } function renderSessionControl() { const control = state.session_control || { mode: "all", entries: [] }; document.querySelectorAll('input[name="control_mode"]').forEach((input) => { input.checked = input.value === control.mode; }); updateModeSelection(control.mode); byId("control-mode-label").textContent = controlModeLabel(control.mode); byId("control-mode-hint").textContent = controlModeCopy(control.mode); const body = byId("control-entries-body"); if (!control.entries.length) { body.replaceChildren(emptyRow(4, "暂无白名单或黑名单会话")); return; } body.replaceChildren(...control.entries.map((item) => { const row = document.createElement("tr"); const session = document.createElement("td"); const listType = document.createElement("td"); const remark = document.createElement("td"); const actions = document.createElement("td"); session.dataset.label = "会话 ID"; listType.dataset.label = "名单类型"; remark.dataset.label = "备注"; actions.dataset.label = "操作"; actions.className = "actions"; session.textContent = item.session_id; if (editing.controlSession === item.session_id) { row.classList.add("is-editing"); const typeSelect = document.createElement("select"); typeSelect.className = "inline-editor inline-editor--compact"; typeSelect.setAttribute("aria-label", `${item.session_id}的名单类型`); typeSelect.append( new Option("白名单", "whitelist"), new Option("黑名单", "blacklist"), ); typeSelect.value = item.list_type; const remarkInput = document.createElement("input"); remarkInput.className = "inline-editor"; remarkInput.maxLength = 200; remarkInput.value = item.remark || ""; remarkInput.placeholder = "备注(可选)"; remarkInput.setAttribute("aria-label", `${item.session_id}的备注`); const saveButton = button("保存", "", async () => { typeSelect.disabled = true; remarkInput.disabled = true; saveButton.disabled = true; cancelButton.disabled = true; editing.controlSession = null; const saved = await mutate( "session-control/save", { session_id: item.session_id, list_type: typeSelect.value, remark: remarkInput.value }, "会话名单已保存", ); if (!saved) { editing.controlSession = item.session_id; renderSessionControl(); } }); const cancelButton = button("取消", "", () => { editing.controlSession = null; renderSessionControl(); }); remarkInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); saveButton.click(); } else if (event.key === "Escape") { event.preventDefault(); cancelButton.click(); } }); remark.append(remarkInput); listType.append(typeSelect); actions.append(saveButton, cancelButton); queueMicrotask(() => typeSelect.focus()); } else { const badge = document.createElement("span"); badge.className = `list-badge list-badge--${item.list_type}`; badge.textContent = item.list_type === "whitelist" ? "白名单" : "黑名单"; listType.append(badge); remark.textContent = item.remark || "—"; actions.append( button("编辑", "", () => { editing.controlSession = item.session_id; renderSessionControl(); }), button("删除", "link-button--danger", async (event) => { const deleteButton = event.currentTarget; deleteButton.disabled = true; const deleted = await mutate( "session-control/delete", { session_id: item.session_id }, "会话名单已删除", ); if (!deleted) deleteButton.disabled = false; }), ); } row.append(session, listType, remark, actions); return row; })); } function renderBindings() { const body = byId("bindings-body"); if (!state.bindings.length) { body.replaceChildren(emptyRow(3, "暂无会话绑定")); return; } body.replaceChildren(...state.bindings.map((item) => { const row = document.createElement("tr"); const session = document.createElement("td"); const server = document.createElement("td"); const actions = document.createElement("td"); session.dataset.label = "会话 ID"; server.dataset.label = "绑定区服"; actions.dataset.label = "操作"; session.textContent = item.session_id; actions.className = "actions"; if (editing.bindingSession === item.session_id) { row.classList.add("is-editing"); const editor = inlineServerEditor( item, async (selectedServer) => { editing.bindingSession = null; const saved = await mutate( "bindings/save", { session_id: item.session_id, server: selectedServer }, "绑定信息已保存", ); if (!saved) { editing.bindingSession = item.session_id; renderBindings(); } return saved; }, () => { editing.bindingSession = null; renderBindings(); }, ); server.append(editor.select); actions.append(...editor.controls); } else { server.textContent = item.server; actions.append( button("编辑", "", () => { editing.bindingSession = item.session_id; renderBindings(); }), button("解除绑定", "link-button--danger", async (event) => { const control = event.currentTarget; control.disabled = true; const deleted = await mutate( "bindings/delete", { session_id: item.session_id }, "绑定已解除", ); if (!deleted) control.disabled = false; }), ); } row.append(session, server, actions); return row; })); } function renderLegacyBilei() { const records = state.legacy_bilei || []; const body = byId("legacy-bilei-body"); byId("legacy-bilei-count").textContent = String(records.length); if (!records.length) { body.replaceChildren(emptyRow(7, "没有待迁移的旧避雷数据")); return; } body.replaceChildren(...records.map((item) => { const row = document.createElement("tr"); const id = document.createElement("td"); const name = document.createElement("td"); const note = document.createElement("td"); const time = document.createElement("td"); const user = document.createElement("td"); const target = document.createElement("td"); const actions = document.createElement("td"); id.dataset.label = "ID"; name.dataset.label = "避雷名称"; note.dataset.label = "避雷备注"; time.dataset.label = "时间"; user.dataset.label = "记录人"; target.dataset.label = "目标会话"; actions.dataset.label = "操作"; id.textContent = String(item.id); name.textContent = item.name || "—"; note.textContent = item.text || "—"; note.className = "legacy-note-cell"; time.textContent = item.time || "—"; user.textContent = item.user || "—"; target.className = "legacy-session-cell"; actions.className = "actions"; const sessionInput = document.createElement("input"); sessionInput.className = "inline-editor"; sessionInput.maxLength = 512; sessionInput.required = true; sessionInput.setAttribute("list", "session-options"); sessionInput.setAttribute("aria-label", `避雷记录 ${item.id} 的目标会话`); sessionInput.placeholder = "选择已有会话或直接输入"; target.append(sessionInput); const migrateButton = button("迁移", "", async () => { if (!sessionInput.reportValidity()) return; sessionInput.disabled = true; migrateButton.disabled = true; const migrated = await mutate( "bilei/legacy/migrate", { id: item.id, session_id: sessionInput.value }, `避雷记录 ${item.id} 已迁移`, ); if (!migrated) { sessionInput.disabled = false; migrateButton.disabled = false; sessionInput.focus(); } }); sessionInput.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); migrateButton.click(); } }); actions.append(migrateButton); row.append(id, name, note, time, user, target, actions); return row; })); } function openSubscriptionEditor(item = null) { if (subscriptionSaving) return; editing.subscriptionSession = item?.session_id ?? null; const form = byId("subscription-form"); form.reset(); byId("subscription-editor-title").textContent = item ? "编辑推送配置" : "添加推送会话"; const sessionInput = byId("subscription-session"); sessionInput.value = item?.session_id || ""; sessionInput.readOnly = Boolean(item); byId("subscription-enabled").checked = item?.enabled ?? false; const selected = new Set(item?.actions || []); const freeActions = new Set(state.free_event_actions); const groups = [ { title: "免费事件", free: true }, { title: "令牌事件", free: false }, ]; byId("subscription-events").replaceChildren(...groups.map((group) => { const fieldset = document.createElement("fieldset"); fieldset.className = "subscription-event-group"; const legend = document.createElement("legend"); legend.textContent = group.title; const grid = document.createElement("div"); grid.className = "subscription-event-grid"; Object.entries(state.events).forEach(([action, name]) => { if (freeActions.has(Number(action)) !== group.free) return; const label = document.createElement("label"); label.className = "subscription-choice"; const input = document.createElement("input"); input.type = "checkbox"; input.name = "subscription_action"; input.value = action; input.checked = selected.has(Number(action)); const text = document.createElement("span"); text.textContent = `${action} ${name}`; label.append(input, text); grid.append(label); }); fieldset.append(legend, grid); return fieldset; })); updateSubscriptionSelectionCount(); form.hidden = false; form.scrollIntoView({ block: "nearest" }); (item ? byId("subscription-enabled") : sessionInput).focus({ preventScroll: true }); } function updateSubscriptionSelectionCount() { const count = byId("subscription-events").querySelectorAll("input:checked").length; byId("subscription-selection-count").textContent = `已选择 ${count} 项`; } function renderSubscriptions() { const body = byId("subscriptions-body"); const bindings = bindingMap(); if (!state.subscriptions.length) { body.replaceChildren(emptyRow(5, "暂无事件订阅会话,点击“添加推送会话”开始配置")); return; } body.replaceChildren(...state.subscriptions.map((item) => { const row = document.createElement("tr"); const session = document.createElement("td"); const server = document.createElement("td"); const enabled = document.createElement("td"); const actions = document.createElement("td"); const controls = document.createElement("td"); session.dataset.label = "会话 ID"; server.dataset.label = "绑定区服"; enabled.dataset.label = "总开关"; actions.dataset.label = "已订阅事件"; controls.dataset.label = "操作"; controls.className = "actions"; session.className = "subscription-session-cell"; session.textContent = item.session_id; server.textContent = bindings.get(item.session_id) || "未绑定(全部区服)"; const stateLabel = document.createElement("span"); stateLabel.className = `state ${item.enabled ? "state--on" : "state--off"}`; stateLabel.textContent = item.enabled ? "开启" : "关闭"; enabled.append(stateLabel); const tags = document.createElement("div"); tags.className = "tags"; if (item.actions.length) { item.actions.forEach((action) => { const tag = document.createElement("span"); tag.className = "tag"; tag.textContent = `${action} ${state.events[String(action)] || "未知事件"}`; tags.append(tag); }); } else { tags.textContent = "无"; } actions.append(tags); controls.append( button("编辑", "", () => openSubscriptionEditor(item)), button("删除", "link-button--danger", async (event) => { if (subscriptionSaving) return; const control = event.currentTarget; control.disabled = true; subscriptionSaving = true; byId("subscription-fields").disabled = true; byId("add-subscription").disabled = true; try { const deleted = await mutate( "subscriptions/delete", { session_id: item.session_id }, "会话推送配置已删除", ); if (deleted && editing.subscriptionSession === item.session_id) { byId("subscription-form").hidden = true; editing.subscriptionSession = null; } } finally { subscriptionSaving = false; control.disabled = false; byId("subscription-fields").disabled = false; byId("add-subscription").disabled = false; } }), ); row.append(session, server, enabled, actions, controls); return row; })); } function renderAliases() { const body = byId("aliases-body"); const aliasesByServer = new Map( state.aliases.map((item) => [item.server, item.aliases]), ); const servers = [...new Set([ ...state.servers, ...aliasesByServer.keys(), ])].sort((left, right) => left.localeCompare(right, "zh-CN")); if (!servers.length) { body.replaceChildren(emptyRow(3, "暂无标准区服数据")); return; } body.replaceChildren(...servers.map((serverName) => { const item = { server: serverName, aliases: aliasesByServer.get(serverName) || [], }; const row = document.createElement("tr"); const server = document.createElement("td"); const aliases = document.createElement("td"); const actions = document.createElement("td"); server.dataset.label = "标准区服名"; aliases.dataset.label = "别名"; actions.dataset.label = "操作"; server.textContent = item.server; aliases.className = "alias-cell"; actions.className = "actions"; if (editing.aliasServer === item.server) { row.classList.add("is-editing"); const editor = inlineAliasEditor( item.aliases, `${item.server}的区服别名`, async (value) => { editing.aliasServer = null; const saved = await mutate( "aliases/save", { server: item.server, aliases: value }, "区服别名已保存", ); if (!saved) { editing.aliasServer = item.server; renderAliases(); } return saved; }, () => { editing.aliasServer = null; renderAliases(); }, ); aliases.append(editor.input); actions.append(...editor.controls); } else { aliases.textContent = aliasText(item.aliases); actions.append(button("编辑", "", () => { editing.aliasServer = item.server; renderAliases(); })); } row.append(server, aliases, actions); return row; })); } function renderKungfu() { const body = byId("kungfu-body"); if (!state.kungfu.length) { body.replaceChildren(emptyRow(3, "暂无心法配置")); return; } body.replaceChildren(...state.kungfu.map((item) => { const row = document.createElement("tr"); const name = document.createElement("td"); const aliases = document.createElement("td"); const actions = document.createElement("td"); name.dataset.label = "标准心法名"; aliases.dataset.label = "别名"; actions.dataset.label = "操作"; name.textContent = item.name; aliases.className = "alias-cell"; actions.className = "actions"; if (editing.kungfuPzid === item.pzid) { row.classList.add("is-editing"); const editor = inlineAliasEditor( item.aliases, `${item.name}的心法别名`, async (value) => { editing.kungfuPzid = null; const saved = await mutate( "kungfu/save", { pzid: item.pzid, aliases: value }, "心法别名已保存", ); if (!saved) { editing.kungfuPzid = item.pzid; renderKungfu(); } return saved; }, () => { editing.kungfuPzid = null; renderKungfu(); }, ); aliases.append(editor.input); actions.append(...editor.controls); } else { aliases.textContent = aliasText(item.aliases); actions.append(button("编辑", "", () => { editing.kungfuPzid = item.pzid; renderKungfu(); })); } row.append(name, aliases, actions); return row; })); } function cacheSettingRow(cacheType, item) { const row = document.createElement("tr"); const name = document.createElement("td"); const ttl = document.createElement("td"); const status = document.createElement("td"); const actions = document.createElement("td"); name.dataset.label = cacheType === "api" ? "接口路径" : "图片指令"; ttl.dataset.label = "缓存时间(秒)"; status.dataset.label = "配置状态"; actions.dataset.label = "操作"; name.textContent = item.name; name.className = "cache-name-cell"; actions.className = "actions"; const input = document.createElement("input"); input.className = "inline-editor inline-editor--ttl"; input.type = "number"; input.min = "0"; input.max = "2592000"; input.step = "1"; input.required = true; input.value = String(item.ttl_seconds); input.setAttribute("aria-label", `${item.name}缓存时间(秒)`); ttl.append(input); const badge = document.createElement("span"); badge.className = `cache-badge ${item.overridden ? "cache-badge--custom" : ""}`.trim(); badge.textContent = item.overridden ? "独立设置" : item.safe_default ? "安全默认" : "继承默认"; status.append(badge); const saveButton = button("保存", "", async () => { if (!input.reportValidity()) return; saveButton.disabled = true; restoreButton.disabled = true; const saved = await mutate( "cache/settings/save", { cache_type: cacheType, cache_name: item.name, ttl_seconds: Number(input.value) }, `${item.name}缓存时间已保存`, ); if (!saved) { saveButton.disabled = false; restoreButton.disabled = false; } }); const restoreButton = button("恢复默认", "", async () => { restoreButton.disabled = true; saveButton.disabled = true; const saved = await mutate( "cache/settings/save", { cache_type: cacheType, cache_name: item.name, inherit: true }, `${item.name}已恢复默认时间`, ); if (!saved) { restoreButton.disabled = false; saveButton.disabled = false; } }); const clearButton = button("清除此项", "link-button--danger", async () => { clearButton.disabled = true; const cleared = await mutate( "cache/item/clear", { cache_type: cacheType, cache_name: item.name }, `${item.name}缓存已清除,下次调用将重新生成`, ); if (!cleared) clearButton.disabled = false; }); restoreButton.disabled = !item.overridden; input.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); saveButton.click(); } }); actions.append(saveButton, restoreButton, clearButton); row.append(name, ttl, status, actions); return row; } function renderCacheTable(cacheType) { const isApi = cacheType === "api"; const items = isApi ? state.cache.api : state.cache.images; const filter = byId(isApi ? "api-cache-filter" : "image-cache-filter") .value.trim().toLocaleLowerCase("zh-CN"); const visible = items.filter((item) => item.name.toLocaleLowerCase("zh-CN").includes(filter)); const body = byId(isApi ? "api-cache-settings-body" : "image-cache-settings-body"); body.replaceChildren(...( visible.length ? visible.map((item) => cacheSettingRow(cacheType, item)) : [emptyRow(4, filter ? "没有匹配的缓存项目" : "暂无缓存项目")] )); } function renderCache() { const cache = state.cache || { defaults: {}, limits: {}, api: [], images: [], stats: {} }; const stats = cache.stats || {}; const apiDefault = cache.defaults?.api ?? 300; const imageDefault = cache.defaults?.image ?? 600; const memoryLimitMb = cache.limits?.api_memory_max_mb ?? 16; const apiEntryLimit = cache.limits?.api_max_entries ?? stats.api_entry_limit ?? 256; const imageLimitMb = cache.limits?.image_max_mb ?? 512; byId("api-cache-count").textContent = `${stats.api_count || 0} / ${apiEntryLimit} 条`; byId("api-cache-size").textContent = formatBytes(stats.api_size_bytes); byId("image-cache-count").textContent = `${stats.image_count || 0} 张`; byId("image-cache-size").textContent = `${formatBytes(stats.image_size_bytes)} / ${formatBytes(stats.image_limit_bytes)}`; byId("api-default-ttl").value = String(apiDefault); byId("image-default-ttl").value = String(imageDefault); byId("api-memory-size-limit").value = String(memoryLimitMb); byId("api-entry-limit").value = String(apiEntryLimit); byId("image-size-limit").value = String(imageLimitMb); byId("api-memory-summary").textContent = `${formatBytes(stats.api_memory_size_bytes)} / ${formatBytes(stats.api_memory_limit_bytes)}`; byId("api-memory-count").textContent = `${stats.api_memory_count || 0} 条,最久未使用优先淘汰`; byId("cache-default-summary").textContent = `${apiDefault} / ${imageDefault} 秒`; renderCacheTable("api"); renderCacheTable("image"); } function render() { renderTokenStats(); renderServerOptions(); renderSessionOptions(); renderSessionControl(); renderLegacyBilei(); renderBindings(); renderSubscriptions(); renderAliases(); renderKungfu(); renderCache(); } async function loadData() { const data = await bridge.apiGet("dashboard"); Object.assign(state, data); render(); } async function mutate(endpoint, payload, successMessage) { try { await bridge.apiPost(endpoint, payload); await loadData(); showToast(successMessage); return true; } catch (error) { showToast(error?.message || "操作失败", true); return false; } } function resetRestoreConfirmation(control) { const timer = restoreConfirmationTimers.get(control); if (timer) clearTimeout(timer); restoreConfirmationTimers.delete(control); delete control.dataset.confirming; control.classList.remove("button--danger"); control.textContent = "恢复默认"; } function confirmRestoreInPage(control, confirmation) { if (control.dataset.confirming === "true") { resetRestoreConfirmation(control); return true; } control.dataset.confirming = "true"; control.classList.add("button--danger"); control.textContent = "再次点击确认"; showToast(confirmation); restoreConfirmationTimers.set( control, setTimeout(() => resetRestoreConfirmation(control), 5000), ); return false; } async function restoreDefaults(control, endpoint, confirmation, successMessage, resetEditing) { if (!confirmRestoreInPage(control, confirmation)) return; const originalLabel = control.textContent; control.disabled = true; control.textContent = "恢复中…"; resetEditing(); try { await bridge.apiPost(endpoint, {}); await loadData(); showToast(successMessage); } catch (error) { showToast(error?.message || "恢复默认失败", true); } finally { control.disabled = false; control.textContent = originalLabel; } } document.querySelectorAll(".tab").forEach((tab) => { tab.addEventListener("click", () => { document.querySelectorAll(".tab").forEach((item) => { const active = item === tab; item.classList.toggle("is-active", active); item.setAttribute("aria-selected", String(active)); }); document.querySelectorAll(".panel").forEach((panel) => { const active = panel.id === `${tab.dataset.tab}-panel`; panel.classList.toggle("is-active", active); panel.hidden = !active; }); }); }); byId("add-subscription").addEventListener("click", () => openSubscriptionEditor()); byId("cancel-subscription").addEventListener("click", () => { byId("subscription-form").hidden = true; editing.subscriptionSession = null; }); byId("subscription-events").addEventListener("change", updateSubscriptionSelectionCount); byId("subscription-select-all").addEventListener("click", () => { byId("subscription-events").querySelectorAll("input").forEach((input) => { input.checked = true; }); updateSubscriptionSelectionCount(); }); byId("subscription-clear-all").addEventListener("click", () => { byId("subscription-events").querySelectorAll("input").forEach((input) => { input.checked = false; }); updateSubscriptionSelectionCount(); }); byId("subscription-form").addEventListener("submit", async (event) => { event.preventDefault(); if (subscriptionSaving) return; const form = event.currentTarget; const sessionId = (editing.subscriptionSession ?? byId("subscription-session").value).trim(); if (!sessionId) { showToast("会话 ID 不能为空", true); byId("subscription-session").focus(); return; } const payload = { session_id: sessionId, enabled: byId("subscription-enabled").checked, actions: [...byId("subscription-events").querySelectorAll("input:checked")].map((input) => Number(input.value)), mode: editing.subscriptionSession === null ? "create" : "update", }; subscriptionSaving = true; byId("subscription-fields").disabled = true; byId("add-subscription").disabled = true; try { const saved = await mutate("subscriptions/save", payload, "会话推送配置已保存"); if (saved) { form.hidden = true; editing.subscriptionSession = null; } } finally { subscriptionSaving = false; byId("subscription-fields").disabled = false; byId("add-subscription").disabled = false; } }); byId("binding-form").addEventListener("submit", async (event) => { event.preventDefault(); const saved = await mutate("bindings/save", { session_id: byId("binding-session").value, server: byId("binding-server").value, }, "绑定信息已保存"); if (saved) event.currentTarget.reset(); }); document.querySelectorAll('input[name="control_mode"]').forEach((input) => { input.addEventListener("change", () => { updateModeSelection(input.value); }); }); byId("control-mode-form").addEventListener("submit", async (event) => { event.preventDefault(); const selected = new FormData(event.currentTarget).get("control_mode"); await mutate("session-control/mode", { mode: selected }, "会话控制模式已保存"); }); byId("control-entry-form").addEventListener("submit", async (event) => { event.preventDefault(); const saved = await mutate("session-control/save", { session_id: byId("control-session").value, list_type: byId("control-list-type").value, remark: byId("control-remark").value, }, "会话名单已保存"); if (saved) event.currentTarget.reset(); }); byId("restore-aliases").addEventListener("click", async (event) => { await restoreDefaults( event.currentTarget, "aliases/restore", "再次点击按钮,确认使用内置 JSON 覆盖当前全部区服别名", "区服别名已恢复默认", () => { editing.aliasServer = null; }, ); }); byId("restore-kungfu").addEventListener("click", async (event) => { await restoreDefaults( event.currentTarget, "kungfu/restore", "再次点击按钮,确认使用内置 JSON 覆盖当前全部心法及别名", "心法别名已恢复默认", () => { editing.kungfuPzid = null; }, ); }); byId("api-cache-filter").addEventListener("input", () => renderCacheTable("api")); byId("image-cache-filter").addEventListener("input", () => renderCacheTable("image")); byId("cache-default-form").addEventListener("submit", async (event) => { event.preventDefault(); const submit = event.currentTarget.querySelector('button[type="submit"]'); submit.disabled = true; try { await bridge.apiPost("cache/settings/save", { cache_type: "api", cache_name: "*", ttl_seconds: Number(byId("api-default-ttl").value), }); await bridge.apiPost("cache/settings/save", { cache_type: "image", cache_name: "*", ttl_seconds: Number(byId("image-default-ttl").value), }); await loadData(); showToast("默认缓存时间已保存"); } catch (error) { showToast(error?.message || "默认缓存时间保存失败", true); } finally { submit.disabled = false; } }); byId("cache-limit-form").addEventListener("submit", async (event) => { event.preventDefault(); const submit = event.currentTarget.querySelector('button[type="submit"]'); submit.disabled = true; try { await bridge.apiPost("cache/limits/save", { api_memory_max_mb: Number(byId("api-memory-size-limit").value), api_max_entries: Number(byId("api-entry-limit").value), image_max_mb: Number(byId("image-size-limit").value), }); await loadData(); showToast("缓存容量限制已保存并立即生效"); } catch (error) { showToast(error?.message || "缓存容量限制保存失败", true); } finally { submit.disabled = false; } }); async function clearCache(cacheType, control) { control.disabled = true; try { const result = await bridge.apiPost("cache/clear", { cache_type: cacheType }); await loadData(); const removed = result?.removed?.[cacheType] ?? 0; showToast(`${cacheType === "api" ? "接口" : "图片"}缓存已清空,共清理 ${removed} 项`); } catch (error) { showToast(error?.message || "缓存清理失败", true); } finally { control.disabled = false; } } byId("clear-api-cache").addEventListener("click", (event) => { clearCache("api", event.currentTarget); }); byId("clear-image-cache").addEventListener("click", (event) => { clearCache("image", event.currentTarget); }); byId("refresh").addEventListener("click", async (event) => { const control = event.currentTarget; control.disabled = true; try { await bridge.apiPost("servers/refresh", {}); await loadData(); showToast("区服目录与页面数据已刷新"); } catch (error) { showToast(error?.message || "刷新失败", true); } finally { control.disabled = false; } }); await bridge.ready(); try { await loadData(); } catch (error) { showToast(error?.message || "管理数据加载失败", true); }