This commit is contained in:
qsc
2026-09-07 14:31:57 +08:00
parent 2cf54a5e6c
commit 670164b804
11 changed files with 658 additions and 202 deletions
+144 -9
View File
@@ -6,18 +6,20 @@ const state = {
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_entries: 256, image_max_mb: 512 },
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 };
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null, subscriptionSession: null };
let subscriptionSaving = false;
const restoreConfirmationTimers = new WeakMap();
let toastTimer;
@@ -478,11 +480,63 @@ function renderLegacyBilei() {
}));
}
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(4, "暂无事件订阅会话"));
body.replaceChildren(emptyRow(5, "暂无事件订阅会话,点击“添加推送会话”开始配置"));
return;
}
body.replaceChildren(...state.subscriptions.map((item) => {
@@ -491,10 +545,14 @@ function renderSubscriptions() {
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");
@@ -514,7 +572,34 @@ function renderSubscriptions() {
tags.textContent = "无";
}
actions.append(tags);
row.append(session, server, enabled, actions);
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;
}));
}
@@ -739,17 +824,20 @@ function renderCache() {
const stats = cache.stats || {};
const apiDefault = cache.defaults?.api ?? 300;
const imageDefault = cache.defaults?.image ?? 600;
const memoryLimit = cache.limits?.api_memory_entries ?? stats.api_memory_limit ?? 256;
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}`;
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-limit").value = String(memoryLimit);
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 = `${stats.api_memory_count || 0} / ${memoryLimit}`;
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");
@@ -845,6 +933,52 @@ document.querySelectorAll(".tab").forEach((tab) => {
});
});
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", {
@@ -929,7 +1063,8 @@ byId("cache-limit-form").addEventListener("submit", async (event) => {
submit.disabled = true;
try {
await bridge.apiPost("cache/limits/save", {
api_memory_entries: Number(byId("api-memory-limit").value),
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();
+41 -8
View File
@@ -106,7 +106,7 @@
<div class="section-intro section-intro--actions">
<div>
<h2>查询与图片缓存</h2>
<p>接口数据以 JSON 保存到 SQLite,渲染图片保存到插件数据目录。图片命中时会直接发送,不再请求其上游接口;缓存时间使用秒,填写 0 可关闭对应缓存。</p>
<p>接口数据以 JSON 保存到 SQLite,渲染图片保存到插件数据目录。图片命中时会直接发送,不再请求其上游接口;缓存时间使用秒,填写 0 可关闭对应缓存。每 60 秒自动清理过期缓存,接口请求失败时不使用过期数据。</p>
</div>
<div class="cache-clear-actions">
<button class="button button--secondary" id="clear-api-cache" type="button">清空接口缓存</button>
@@ -115,9 +115,9 @@
</div>
<div class="cache-summary" aria-label="缓存统计">
<div class="cache-stat"><span>接口缓存(SQLite</span><strong id="api-cache-count">0 条</strong><small id="api-cache-size">0 B</small></div>
<div class="cache-stat"><span>接口缓存(SQLite</span><strong id="api-cache-count">0 / 256 </strong><small id="api-cache-size">0 B</small></div>
<div class="cache-stat"><span>图片缓存</span><strong id="image-cache-count">0 张</strong><small id="image-cache-size">0 B</small></div>
<div class="cache-stat"><span>接口内存缓存</span><strong id="api-memory-summary">0 / 256 条</strong><small>最久未使用优先淘汰</small></div>
<div class="cache-stat"><span>接口内存缓存</span><strong id="api-memory-summary">0 B / 16 MB</strong><small id="api-memory-count">0 条,最久未使用优先淘汰</small></div>
<div class="cache-stat"><span>默认缓存时间</span><strong id="cache-default-summary">300 / 600 秒</strong><small>接口 / 图片</small></div>
</div>
@@ -135,8 +135,12 @@
<form class="cache-defaults cache-limits" id="cache-limit-form">
<label>
<span>接口内存缓存最大条数</span>
<input id="api-memory-limit" type="number" min="1" max="100000" step="1" required />
<span>接口内存缓存最大容量(MB</span>
<input id="api-memory-size-limit" type="number" min="1" max="1024" step="1" required />
</label>
<label>
<span>SQLite 接口缓存最大条数</span>
<input id="api-entry-limit" type="number" min="1" max="100000" step="1" required />
</label>
<label>
<span>图片缓存最大容量(MB</span>
@@ -212,12 +216,41 @@
</section>
<section class="panel" id="subscriptions-panel" role="tabpanel" hidden>
<div class="section-intro">
<div><h2>所有会话事件状态</h2><p>总开关和订阅事件均来自本地订阅表</p></div>
<div class="section-intro section-intro--actions">
<div><h2>会话事件推送</h2><p>为会话配置推送总开关和订阅事件,保存后立即生效。推送仍受会话访问模式及绑定区服限制;删除仅移除该会话的推送配置</p></div>
<button class="button button--primary" id="add-subscription" type="button">添加推送会话</button>
</div>
<form class="subscription-editor" id="subscription-form" hidden>
<fieldset id="subscription-fields">
<legend id="subscription-editor-title">添加推送会话</legend>
<div class="subscription-settings">
<label>
<span>会话 ID</span>
<input id="subscription-session" name="session_id" list="session-options" maxlength="512" placeholder="选择已有会话或输入完整会话 ID" required />
</label>
<label class="subscription-choice">
<input id="subscription-enabled" type="checkbox" />
<span>开启事件推送</span>
</label>
</div>
<p class="subscription-hint">关闭总开关会保留事件选择;未选择事件时不会收到推送。令牌事件需在插件配置中填写事件版令牌。</p>
<div class="subscription-toolbar">
<strong id="subscription-selection-count" aria-live="polite">已选择 0 项</strong>
<div>
<button class="link-button" id="subscription-select-all" type="button">全选</button>
<button class="link-button" id="subscription-clear-all" type="button">清空选择</button>
</div>
</div>
<div id="subscription-events"></div>
<div class="subscription-form-actions">
<button class="button button--primary" type="submit">保存推送配置</button>
<button class="button button--secondary" id="cancel-subscription" type="button">取消</button>
</div>
</fieldset>
</form>
<div class="table-wrap">
<table>
<thead><tr><th>会话 ID</th><th>绑定区服</th><th>总开关</th><th>已订阅事件</th></tr></thead>
<thead><tr><th>会话 ID</th><th>绑定区服</th><th>总开关</th><th>已订阅事件</th><th class="actions">操作</th></tr></thead>
<tbody id="subscriptions-body"></tbody>
</table>
</div>
+22
View File
@@ -86,6 +86,24 @@ input:focus, select:focus { border-color: var(--focus); box-shadow: 0 0 0 3px co
.section-intro { padding: 22px; border-bottom: 1px solid var(--border); }
.section-intro--actions { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
.section-intro h2 { margin: 0; font-size: 18px; }
.subscription-editor { padding: 22px; border-bottom: 1px solid var(--border); background: var(--surface-subtle); }
.subscription-editor[hidden] { display: none; }
.subscription-editor fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
.subscription-editor legend { margin-bottom: 14px; font-size: 16px; font-weight: 700; }
.subscription-settings { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 24px; }
.subscription-choice { display: flex; align-items: center; gap: 9px; cursor: pointer; text-align: left; }
.subscription-choice input { width: 17px; height: 17px; padding: 0; flex: 0 0 auto; accent-color: var(--accent); }
.subscription-choice span { overflow-wrap: anywhere; }
.subscription-hint { margin: 14px 0; color: var(--muted); font-size: 13px; }
.subscription-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 16px 0; font-size: 13px; }
.subscription-editor .subscription-event-group { margin-top: 16px; }
.subscription-event-group legend { margin-bottom: 10px; color: var(--muted); font-size: 13px; }
.subscription-event-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 8px; }
.subscription-event-grid .subscription-choice { padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); }
.subscription-event-grid .subscription-choice:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.subscription-form-actions { display: flex; gap: 12px; margin-top: 22px; }
.subscription-session-cell { overflow-wrap: anywhere; max-width: 300px; }
#subscription-session[readonly] { color: var(--muted); background: var(--surface-subtle); }
.cache-clear-actions { display: flex; flex: 0 0 auto; justify-content: flex-end; gap: 10px; }
.cache-summary { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--border); background: var(--surface-subtle); }
.cache-stat { display: flex; min-height: 100px; flex-direction: column; justify-content: center; padding: 18px 22px; }
@@ -93,6 +111,7 @@ input:focus, select:focus { border-color: var(--focus); box-shadow: 0 0 0 3px co
.cache-stat span, .cache-stat small { color: var(--muted); font-size: 12px; font-weight: 650; }
.cache-stat strong { margin: 3px 0; font-size: 21px; font-variant-numeric: tabular-nums; }
.cache-defaults { display: grid; grid-template-columns: minmax(240px, 1fr) minmax(240px, 1fr) auto; align-items: end; gap: 16px; padding: 22px; border-bottom: 1px solid var(--border); }
.cache-limits { grid-template-columns: repeat(3, minmax(180px, 1fr)) auto; }
.cache-group { border-bottom: 1px solid var(--border); }
.cache-group:last-child { border-bottom: 0; }
.cache-group summary { padding: 18px 22px; cursor: pointer; font-size: 16px; font-weight: 750; }
@@ -136,6 +155,9 @@ tbody tr.is-editing { background: color-mix(in srgb, var(--accent-soft) 62%, tra
.toast.is-error { border-left-color: var(--danger); }
@media (max-width: 760px) {
.subscription-settings { grid-template-columns: 1fr; gap: 16px; }
.subscription-event-grid { grid-template-columns: 1fr; }
.subscription-session-cell { max-width: none; }
.shell { width: min(100% - 24px, 1260px); padding-top: 20px; }
.page-header { align-items: stretch; flex-direction: column; }
.section-intro--actions { align-items: stretch; flex-direction: column; }