This commit is contained in:
qsc
2026-09-05 02:36:59 +08:00
parent 9c9808adf7
commit 694f6a9604
17 changed files with 1300 additions and 466 deletions
+353 -24
View File
@@ -1,6 +1,15 @@
const bridge = window.AstrBotPluginPage;
const state = { bindings: [], subscriptions: [], aliases: [], kungfu: [], servers: [], events: {} };
const editing = { aliasServer: null, kungfuPzid: null };
const state = {
bindings: [],
subscriptions: [],
aliases: [],
kungfu: [],
servers: [],
events: {},
session_control: { mode: "all", entries: [] },
};
const editing = { bindingSession: null, controlSession: null, aliasServer: null, kungfuPzid: null };
const restoreConfirmationTimers = new WeakMap();
let toastTimer;
const byId = (id) => document.getElementById(id);
@@ -69,24 +78,226 @@ function inlineAliasEditor(aliases, label, onSave, onCancel) {
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 renderSummary() {
byId("binding-count").textContent = String(state.bindings.length);
byId("subscription-count").textContent = String(state.subscriptions.filter((item) => item.enabled).length);
byId("alias-count").textContent = String(state.aliases.reduce((total, item) => total + item.aliases.length, 0));
byId("kungfu-count").textContent = String(state.kungfu.length);
}
function renderServerOptions() {
const list = byId("server-options");
list.replaceChildren(...state.servers.map((server) => {
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() {
@@ -104,19 +315,50 @@ function renderBindings() {
server.dataset.label = "绑定区服";
actions.dataset.label = "操作";
session.textContent = item.session_id;
server.textContent = item.server;
actions.className = "actions";
actions.append(
button("编辑", "", () => {
byId("binding-session").value = item.session_id;
byId("binding-server").value = item.server;
byId("binding-server").focus();
}),
button("解除绑定", "link-button--danger", async () => {
if (!window.confirm(`确认解除会话 ${item.session_id} 的区服绑定?`)) return;
await mutate("bindings/delete", { session_id: item.session_id }, "绑定已解除");
}),
);
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;
}));
@@ -283,8 +525,9 @@ function renderKungfu() {
}
function render() {
renderSummary();
renderServerOptions();
renderSessionOptions();
renderSessionControl();
renderBindings();
renderSubscriptions();
renderAliases();
@@ -309,6 +552,50 @@ async function mutate(endpoint, payload, successMessage) {
}
}
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) => {
@@ -333,6 +620,48 @@ byId("binding-form").addEventListener("submit", async (event) => {
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("refresh").addEventListener("click", async (event) => {
const control = event.currentTarget;
control.disabled = true;