import { announce, apiJson } from "../core.js"; import { setControlBusy, setControlReady, setControlUnavailable } from "../interaction-feedback.js"; const selectedName = document.getElementById("frp-selected-name"); const serviceState = document.getElementById("frp-service-state"); const feedback = document.getElementById("frp-feedback"); const serviceToggle = document.getElementById("frp-service-toggle"); const editButton = document.getElementById("frp-edit-config"); const diagnosticsButton = document.getElementById("network-diagnostics-run"); const dialog = document.getElementById("frp-config-dialog"); const configSelect = document.getElementById("frp-config-select"); const configName = document.getElementById("frp-config-name"); const configFormat = document.getElementById("frp-config-format"); const configContent = document.getElementById("frp-config-content"); const configFile = document.getElementById("frp-config-file"); const configFeedback = document.getElementById("frp-config-feedback"); const newButton = document.getElementById("frp-config-new"); const uploadButton = document.getElementById("frp-config-upload"); const deleteButton = document.getElementById("frp-config-delete"); const closeButton = document.getElementById("frp-config-close"); const saveButton = document.getElementById("frp-config-save"); const diagnosticsDialog = document.getElementById("network-diagnostics-dialog"); const diagnosticsSummary = document.getElementById("network-diagnostics-summary"); const diagnosticsChecks = document.getElementById("network-diagnostics-checks"); const diagnosticsClose = document.getElementById("network-diagnostics-close"); const SUPPORTED_SUFFIXES = new Set([".toml", ".yaml", ".yml", ".json", ".ini"]); const MAX_CONFIG_BYTES = 1024 * 1024; let state = null; let editingId = null; let generation = 0; function setMessage(element, text, kind = "idle") { element.textContent = text; element.dataset.state = kind; element.hidden = !text; } function profileById(id) { return state?.profiles?.find((profile) => profile.id === id) || null; } function serviceLabel(service) { if (!service?.installed) return "frpc 未安装"; if (service.running) return service.enabled ? "运行中(开机自动启动)" : "运行中"; if (service.state === "failed") return "启动失败"; return service.enabled ? "等待启动" : "关闭"; } function renderState(next) { state = next; const selected = profileById(next?.selected_id); selectedName.textContent = selected?.name || "无"; serviceState.textContent = serviceLabel(next?.service); const running = next?.service?.running === true; serviceToggle.textContent = running || next?.service?.enabled ? "关闭frp" : "启动frp"; if (!next?.available) { setControlUnavailable(serviceToggle, next?.error || "FRP 配置目录不可用。"); setMessage(feedback, next?.error || "FRP 配置目录不可用。", "error"); } else if (!next?.service?.installed) { setControlUnavailable(serviceToggle, "frpc 系统软件尚未安装。"); setMessage(feedback, "frpc 系统软件尚未安装。", "error"); } else if (!selected && !running) { setControlUnavailable(serviceToggle, "请先导入或新建 FRP 配置。"); setMessage(feedback, "尚未保存 FRP 配置。", "idle"); } else { setControlReady(serviceToggle); setMessage(feedback, next?.service?.last_error || "", next?.service?.last_error ? "error" : "idle"); } } function renderProfileOptions() { configSelect.replaceChildren(); if (!state?.profiles?.length) { const option = document.createElement("option"); option.value = ""; option.textContent = "暂无配置"; configSelect.append(option); configSelect.disabled = true; } else { configSelect.disabled = false; state.profiles.forEach((profile) => { const option = document.createElement("option"); option.value = profile.id; option.textContent = profile.name; configSelect.append(option); }); configSelect.value = editingId || state.selected_id || state.profiles[0].id; } deleteButton.disabled = !editingId; } async function requestText(path, options = {}) { const response = await fetch(path, { ...options, cache: "no-store" }); if (!response.ok) { let message = response.statusText; try { message = (await response.json()).detail || message; } catch {} throw new Error(message); } return response.text(); } async function loadProfile(id) { const currentGeneration = generation; const profile = profileById(id); if (!profile) { editingId = null; configName.value = ""; configContent.value = ""; renderProfileOptions(); return; } setMessage(configFeedback, "正在读取配置…"); const content = await requestText(`/api/frp/configs/${encodeURIComponent(id)}`); if (generation !== currentGeneration) return; editingId = id; configName.value = profile.name; const suffix = profile.filename.slice(profile.filename.lastIndexOf(".")).toLowerCase(); configFormat.value = suffix === ".yml" ? ".yaml" : suffix; configContent.value = content; renderProfileOptions(); setMessage(configFeedback, ""); } export async function loadFrpSettings() { const currentGeneration = ++generation; try { const next = await apiJson("/api/frp"); if (generation !== currentGeneration) return; renderState(next); } catch (error) { setMessage(feedback, `FRP 状态读取失败:${error.message}`, "error"); } } export function discardFrpDrafts() { generation += 1; editingId = null; configName.value = ""; configContent.value = ""; configFile.value = ""; setMessage(configFeedback, ""); if (dialog.open) dialog.close(); if (diagnosticsDialog.open) diagnosticsDialog.close(); } editButton.addEventListener("click", async () => { try { await loadFrpSettings(); editingId = state?.selected_id || state?.profiles?.[0]?.id || null; renderProfileOptions(); if (editingId) await loadProfile(editingId); else { configName.value = ""; configFormat.value = ".toml"; configContent.value = ""; } dialog.showModal(); (editingId ? configContent : configName).focus(); } catch (error) { announce(`打开 FRP 配置失败:${error.message}`, true); } }); closeButton.addEventListener("click", () => dialog.close()); dialog.addEventListener("close", () => { configFile.value = ""; setMessage(configFeedback, ""); }); newButton.addEventListener("click", () => { editingId = null; configSelect.value = ""; configName.value = ""; configFormat.value = ".toml"; configContent.value = ""; deleteButton.disabled = true; setMessage(configFeedback, "请输入完整配置;保存前会使用 frpc 官方校验。", "idle"); configName.focus(); }); uploadButton.addEventListener("click", () => configFile.click()); configFile.addEventListener("change", async () => { const file = configFile.files?.[0]; if (!file) return; const suffix = file.name.slice(file.name.lastIndexOf(".")).toLowerCase(); if (!SUPPORTED_SUFFIXES.has(suffix)) { setMessage(configFeedback, "请选择 TOML、YAML、JSON 或 INI 配置文件。", "error"); return; } if (file.size > MAX_CONFIG_BYTES) { setMessage(configFeedback, "配置文件不能超过 1 MiB。", "error"); return; } try { const content = await file.text(); editingId = null; configName.value = file.name.slice(0, -suffix.length); configFormat.value = suffix === ".yml" ? ".yaml" : suffix; configContent.value = content; deleteButton.disabled = true; setMessage(configFeedback, "文件已读取,点击保存后执行官方校验。", "idle"); } catch (error) { setMessage(configFeedback, `读取文件失败:${error.message}`, "error"); } finally { configFile.value = ""; } }); configSelect.addEventListener("change", async () => { const id = configSelect.value; if (!id) return; try { if (id !== state?.selected_id) { const result = await apiJson(`/api/frp/configs/${encodeURIComponent(id)}/select`, { method: "POST" }); renderState(result.frp); } await loadProfile(id); } catch (error) { setMessage(configFeedback, `切换配置失败:${error.message}`, "error"); renderProfileOptions(); } }); saveButton.addEventListener("click", async () => { const name = configName.value.trim(); const content = configContent.value; if (!name || !content.trim()) { setMessage(configFeedback, "请输入配置名称和完整配置内容。", "error"); return; } setControlBusy(saveButton, { label: "正在校验…", message: "正在使用 frpc 校验配置" }); try { const suffix = configFormat.value; const path = editingId ? `/api/frp/configs/${encodeURIComponent(editingId)}?name=${encodeURIComponent(name)}` : `/api/frp/configs?filename=${encodeURIComponent(`frpc${suffix}`)}&name=${encodeURIComponent(name)}`; const response = await fetch(path, { method: editingId ? "PUT" : "POST", headers: { "Content-Type": "text/plain; charset=utf-8" }, body: content, cache: "no-store", }); const result = await response.json(); if (!response.ok) throw new Error(result.detail || response.statusText); editingId = result.profile.id; renderState(result.frp); renderProfileOptions(); setMessage(configFeedback, result.frp.service.running ? "配置已保存并重启 FRP。" : "配置已保存并通过校验。", "success"); announce("FRP 配置已保存"); } catch (error) { setMessage(configFeedback, `配置保存失败:${error.message}`, "error"); } finally { setControlReady(saveButton); } }); deleteButton.addEventListener("click", async () => { if (!editingId) return; const profile = profileById(editingId); if (!window.confirm(`确认删除 FRP 配置“${profile?.name || "未命名"}”?`)) return; setControlBusy(deleteButton, { label: "正在删除…" }); try { const result = await apiJson(`/api/frp/configs/${encodeURIComponent(editingId)}`, { method: "DELETE" }); renderState(result.frp); editingId = result.frp.selected_id || result.frp.profiles?.[0]?.id || null; renderProfileOptions(); if (editingId) await loadProfile(editingId); else { configName.value = ""; configContent.value = ""; } announce("FRP 配置已删除"); } catch (error) { setMessage(configFeedback, `删除失败:${error.message}`, "error"); } finally { setControlReady(deleteButton); } }); serviceToggle.addEventListener("click", async () => { const stopping = state?.service?.running || state?.service?.enabled; setControlBusy(serviceToggle, { label: stopping ? "正在关闭…" : "正在启动…" }); try { const result = await apiJson(`/api/frp/service/${stopping ? "stop" : "start"}`, { method: "POST" }); renderState(result.frp); announce(stopping ? "FRP 已关闭" : "FRP 已启动"); } catch (error) { setMessage(feedback, `${stopping ? "关闭" : "启动"} FRP 失败:${error.message}`, "error"); await loadFrpSettings(); } }); diagnosticsButton.addEventListener("click", async () => { diagnosticsSummary.textContent = "正在检测网络,请稍候…"; diagnosticsSummary.dataset.state = "idle"; diagnosticsChecks.replaceChildren(); diagnosticsDialog.showModal(); setControlBusy(diagnosticsButton, { label: "正在检测…" }); try { const result = await apiJson("/api/network/diagnostics", { method: "POST" }); diagnosticsSummary.textContent = result.diagnosis; diagnosticsSummary.dataset.state = result.overall === "ok" ? "success" : "error"; result.checks.forEach((check) => { const item = document.createElement("li"); item.dataset.state = check.ok ? "success" : (check.warning ? "warning" : "error"); item.textContent = `${check.ok ? "通过" : (check.warning ? "提示" : "失败")}:${check.message}`; diagnosticsChecks.append(item); }); } catch (error) { diagnosticsSummary.textContent = `网络检测失败:${error.message}`; diagnosticsSummary.dataset.state = "error"; } finally { setControlReady(diagnosticsButton); } }); diagnosticsClose.addEventListener("click", () => diagnosticsDialog.close()); document.addEventListener("matrix:status", (event) => { if (event.detail?.frp) renderState(event.detail.frp); });