(() => { "use strict"; const api = window.gsm3Api; const byId = (id) => document.getElementById(id); const views = { overview: ["NETWORK OVERVIEW", "网络概览"], profiles: ["CONFIGURATION", "Profiles"], web: ["SELF-HOSTED CONSOLE", "Web 控制台"], topology: ["LIVE TOPOLOGY", "拓扑与节点"], routes: ["ROUTING TABLE", "路由"], actions: ["RUNTIME RPC", "运行时操作"], security: ["ZERO TRUST", "Secure Mode"], diagnostics: ["DIAGNOSTICS", "诊断"], }; const statusLabels = { unknown: "未知", stopped: "已停止", starting: "启动中", running: "运行中", stopping: "停止中", error: "异常", }; const capabilityLabels = { supportsConfigFile: "配置文件", supportsJsonOutput: "JSON 输出", supportsSecureMode: "Secure Mode", supportsCredentials: "临时凭据", supportsAcl: "ACL", }; let state = { profiles: [], selectedProfileId: "", snapshot: null, capabilities: null, installation: null, webStatus: null, activeView: "overview", unsubscribe: null, isBusy: false, confirmAction: null, }; let activeModal = null; let securityUI = null; const setState = (patch) => { state = { ...state, ...patch }; }; const currentView = () => state.profiles.find((item) => item.profile?.id === state.selectedProfileId) || null; const currentProfile = () => currentView()?.profile || null; const currentInstance = () => currentView()?.instance || null; const createElement = (tag, options = {}) => { const element = document.createElement(tag); if (options.className) element.className = options.className; if (options.text !== undefined) element.textContent = String(options.text); if (options.title) element.title = options.title; return element; }; const resetForm = (form) => { if (typeof form.reset === "function") { form.reset(); return; } form.querySelectorAll("input, select, textarea").forEach((control) => { const type = String(control.type || "").toLowerCase(); if (type === "checkbox" || type === "radio") { control.checked = typeof control.defaultChecked === "boolean" ? control.defaultChecked : control.hasAttribute("checked"); return; } if (control.tagName === "SELECT") { const options = [...control.options]; const defaultOption = options.find((option) => option.defaultSelected || option.hasAttribute("selected")); control.value = defaultOption?.value || options[0]?.value || ""; return; } control.value = typeof control.defaultValue === "string" ? control.defaultValue : ""; }); }; const announce = (message) => { byId("announcement").textContent = message; }; const describeError = (error) => { if (error instanceof Error && error.message) return error.message; return "操作失败,请稍后重试"; }; const showError = (error, notify = true) => { const message = describeError(error); const banner = byId("errorBanner"); banner.textContent = message; banner.hidden = false; announce(message); if (notify) api.notify("error", message); }; const clearError = () => { const banner = byId("errorBanner"); banner.hidden = true; banner.textContent = ""; }; const setBusy = (isBusy) => { setState({ isBusy }); document.body.setAttribute("aria-busy", String(isBusy)); document.querySelectorAll("button[data-runtime-control]").forEach((button) => { button.disabled = isBusy; }); renderControls(); renderWebStatus(); securityUI?.render(); }; const formatBytes = (bytes) => { const value = Number(bytes || 0); if (!Number.isFinite(value) || value <= 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; const unitIndex = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1); const normalized = value / (1024 ** unitIndex); return `${normalized.toFixed(normalized >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; }; const setView = (viewName) => { if (!Object.prototype.hasOwnProperty.call(views, viewName)) return; setState({ activeView: viewName }); document.querySelectorAll("[data-view]").forEach((section) => { const isActive = section.dataset.view === viewName; section.hidden = !isActive; section.classList.toggle("is-active", isActive); }); document.querySelectorAll("[data-view-target]").forEach((button) => { const isActive = button.dataset.viewTarget === viewName; button.classList.toggle("is-active", isActive); if (isActive) button.setAttribute("aria-current", "page"); else button.removeAttribute("aria-current"); }); byId("activeViewEyebrow").textContent = views[viewName][0]; byId("activeViewTitle").textContent = views[viewName][1]; }; const renderProfileSelect = () => { const select = byId("profileSelect"); const options = state.profiles.map((item) => { const option = createElement("option", { text: item.profile.name }); option.value = item.profile.id; return option; }); if (options.length === 0) { const option = createElement("option", { text: "尚未创建 Profile" }); option.value = ""; options.push(option); } select.replaceChildren(...options); select.value = state.selectedProfileId; select.disabled = state.profiles.length === 0; }; const makeStatusPill = (status) => { const pill = createElement("span", { className: "status-pill", text: statusLabels[status] || status || "未知" }); pill.dataset.state = status || "unknown"; return pill; }; const makeRowButton = (label, action, profileId, className = "button button-quiet") => { const button = createElement("button", { className, text: label }); button.type = "button"; button.dataset.profileAction = action; button.dataset.profileId = profileId; return button; }; const renderProfilesTable = () => { const body = byId("profilesTableBody"); const rows = state.profiles.map((item) => { const profile = item.profile; const row = document.createElement("tr"); const profileCell = document.createElement("td"); const name = createElement("strong", { text: profile.name }); const migrationLabel = profile.migration?.source === "tunnel-helper" ? " · 已迁移" : ""; const detail = createElement("div", { className: "hint", text: `${profile.settings?.hostname || profile.id}${migrationLabel}`, }); profileCell.append(name, detail); const networkCell = createElement("td", { text: profile.networkName }); const statusCell = document.createElement("td"); statusCell.append(makeStatusPill(item.instance?.status || "stopped")); const versionCell = createElement("td", { className: "mono", text: profile.capabilities?.version || "未检测" }); const actionCell = createElement("td", { className: "align-right" }); const actions = createElement("div", { className: "row-actions" }); actions.append( makeRowButton("选择", "select", profile.id), makeRowButton("编辑", "edit", profile.id), makeRowButton("删除", "delete", profile.id, "button button-danger"), ); actionCell.append(actions); row.append(profileCell, networkCell, statusCell, versionCell, actionCell); return row; }); body.replaceChildren(...rows); byId("profilesEmpty").hidden = rows.length > 0; }; const renderSummary = () => { const profile = currentProfile(); const details = byId("profileSummary").querySelectorAll("dd"); const values = profile ? [ profile.name, profile.networkName, profile.capabilities?.version || "未检测", profile.settings?.rpcPortal || "自动分配", ] : ["—", "—", "—", "—"]; details.forEach((detail, index) => { detail.textContent = values[index]; }); }; const renderCapabilities = () => { const capabilities = state.capabilities || currentProfile()?.capabilities || null; const container = byId("capabilityList"); if (!capabilities) { container.replaceChildren(createElement("p", { className: "empty-state", text: "尚未完成能力检测" })); return; } const items = Object.entries(capabilityLabels).map(([key, label]) => { const item = createElement("span", { className: "capability-item", text: label }); item.classList.toggle("is-supported", Boolean(capabilities[key])); return item; }); container.replaceChildren(...items); }; const renderSnapshot = () => { const snapshot = state.snapshot; const node = snapshot?.node || {}; const traffic = snapshot?.traffic || {}; byId("metricIpv4").textContent = node.virtualIpv4 || "—"; byId("metricHostname").textContent = node.hostname || "等待节点信息"; byId("metricPeers").textContent = String(snapshot?.peers?.length || 0); byId("metricDirectPeers").textContent = `${snapshot?.peers?.filter((peer) => peer.direct).length || 0} 个直连`; byId("metricRx").textContent = formatBytes(traffic.rxBytes); byId("metricTx").textContent = formatBytes(traffic.txBytes); byId("metricNat").textContent = node.natType ? `NAT ${node.natType}` : "NAT 未知"; byId("snapshotFreshness").textContent = snapshot?.capturedAt ? `更新于 ${new Date(snapshot.capturedAt).toLocaleTimeString("zh-CN")}` : "尚无快照"; renderPeers(snapshot?.peers || []); renderRoutes(snapshot?.routes || []); renderDiagnostics(snapshot); }; const renderPeers = (peers) => { const rows = peers.map((peer) => { const row = document.createElement("tr"); const cells = [ peer.hostname || peer.id, peer.virtualIpv4 || "—", Number.isFinite(peer.latencyMs) ? `${peer.latencyMs} ms` : "—", peer.natType || "—", peer.tunnelProtocol || "—", peer.direct ? "直连" : "中继", ].map((value) => createElement("td", { text: value })); row.append(...cells); return row; }); byId("peersTableBody").replaceChildren(...rows); byId("peersEmpty").hidden = rows.length > 0; }; const renderRoutes = (routes) => { const rows = routes.map((route) => { const row = document.createElement("tr"); const cells = [ route.destination, route.nextHop || "—", route.interface || "—", route.metric ?? "—", route.proxy ? "代理" : "直达", ].map((value) => createElement("td", { text: value })); row.append(...cells); return row; }); byId("routesTableBody").replaceChildren(...rows); byId("routesEmpty").hidden = rows.length > 0; }; const renderDiagnostics = (snapshot = state.snapshot) => { const capabilities = state.capabilities || currentProfile()?.capabilities || {}; const warnings = [ ...(capabilities.compatibilityWarnings || []), ...(snapshot?.warnings || []), ...(snapshot?.error ? [snapshot.error] : []), ]; const listItems = (warnings.length > 0 ? warnings : ["当前未发现兼容性提醒。"]) .map((warning) => createElement("li", { text: warning })); byId("diagnosticWarnings").replaceChildren(...listItems); byId("rawSnapshot").textContent = JSON.stringify(snapshot || {}, null, 2); const warningBanner = byId("warningBanner"); warningBanner.hidden = warnings.length === 0; warningBanner.textContent = warnings.length > 0 ? warnings[0] : ""; }; const renderControls = () => { const profile = currentProfile(); const status = currentInstance()?.status || state.snapshot?.state || "stopped"; const hasProfile = Boolean(profile); const statusPill = byId("instanceStatus"); statusPill.textContent = hasProfile ? (statusLabels[status] || status) : "未选择"; statusPill.dataset.state = hasProfile ? status : "unknown"; byId("startButton").disabled = state.isBusy || !hasProfile || ["running", "starting", "stopping"].includes(status); byId("stopButton").disabled = state.isBusy || !hasProfile || ["stopped", "stopping", "starting"].includes(status); byId("restartButton").disabled = state.isBusy || !hasProfile || ["starting", "stopping"].includes(status); byId("editCurrentProfileButton").disabled = !hasProfile; byId("refreshCapabilitiesButton").disabled = state.isBusy || !hasProfile; const supportsActions = status === "running" && Boolean((state.capabilities || profile?.capabilities)?.supportsJsonOutput); document.querySelectorAll(".action-form input, .action-form select, .action-form button").forEach((control) => { control.disabled = state.isBusy || !supportsActions; }); }; const renderInstallation = () => { const status = state.installation; const installation = status?.installation || null; byId("installVersion").textContent = status?.recommendedVersion || "—"; byId("installTarget").textContent = status ? `${status.platform}/${status.architecture}${status.artifactName ? ` · ${status.artifactName}` : ""}` : "—"; byId("installDirectory").textContent = installation?.directory || "尚未安装"; const stateTag = byId("installState"); stateTag.textContent = installation ? `已安装 ${installation.version}` : status?.supported ? "可安装" : "不支持"; const installButton = byId("installEasyTierButton"); installButton.disabled = state.isBusy || !status?.supported; installButton.dataset.force = installation ? "true" : "false"; installButton.textContent = installation ? "重新安装推荐版本" : "安装推荐版本"; installButton.title = status?.unsupportedReason || ""; }; const renderWebStatus = () => { const web = state.webStatus; const instanceStatus = web?.instance?.status || "stopped"; const statusPill = byId("webServiceStatus"); statusPill.textContent = web?.configured ? (statusLabels[instanceStatus] || instanceStatus) : "未配置"; statusPill.dataset.state = web?.configured ? instanceStatus : "unknown"; byId("webServiceDescription").textContent = web ? `${web.version ? `EasyTier Web ${web.version}` : "EasyTier Web 程序未就绪"} · ${web.healthy ? "API 已响应" : "API 未响应"}` : "正在读取服务状态…"; byId("webHealthTag").textContent = web?.healthy ? "API 正常" : instanceStatus === "running" ? "等待 API" : "未运行"; byId("webConnectionUri").value = web?.configServerUri || "udp://127.0.0.1:22020/<用户名>"; byId("webDatabasePath").textContent = web?.databasePath || "—"; byId("webLogsDirectory").textContent = web?.logsDirectory || "—"; const openButton = byId("webOpenButton"); if (web?.managementUrl) { openButton.href = web.managementUrl; openButton.setAttribute("aria-disabled", "false"); openButton.title = `打开 ${web.managementUrl}`; } else { openButton.removeAttribute("href"); openButton.setAttribute("aria-disabled", "true"); openButton.title = "请先保存 API Host"; } const canManage = Boolean(web?.configured && web?.binaryAvailable); byId("webStartButton").disabled = state.isBusy || !canManage || ["running", "starting", "stopping"].includes(instanceStatus); byId("webStopButton").disabled = state.isBusy || !canManage || ["stopped", "stopping", "starting"].includes(instanceStatus); byId("webRestartButton").disabled = state.isBusy || !canManage || ["starting", "stopping"].includes(instanceStatus); document.querySelectorAll("#webSettingsForm input, #webSettingsForm select, #webSettingsForm button").forEach((control) => { control.disabled = state.isBusy; }); const warnings = web?.warnings || []; byId("webWarnings").replaceChildren(...warnings.map((warning) => createElement("li", { text: warning }))); byId("webWarningPanel").hidden = warnings.length === 0; }; const populateWebForm = () => { const settings = state.webStatus?.settings; if (!settings) return; byId("webBinaryPath").value = settings.binaryPath || state.installation?.installation?.webEmbedPath || "easytier-web-embed"; const apiServerAddress = settings.apiServerAddress || "127.0.0.1"; byId("webApiServerAddress").value = apiServerAddress; byId("webApiServerPort").value = String(settings.apiServerPort || 11211); const apiHostName = apiServerAddress.includes(":") ? `[${apiServerAddress}]` : apiServerAddress; byId("webApiHost").value = settings.apiHost || `http://${apiHostName}:${settings.apiServerPort || 11211}`; byId("webConfigServerProtocol").value = settings.configServerProtocol || "udp"; byId("webConfigServerPort").value = String(settings.configServerPort || 22020); byId("webAutoStart").checked = Boolean(settings.autoStart); byId("webDisableRegistration").checked = Boolean(settings.disableRegistration); byId("webAllowAutoCreateUser").checked = Boolean(settings.allowAutoCreateUser); }; const renderAll = () => { renderProfileSelect(); renderProfilesTable(); renderSummary(); renderCapabilities(); renderSnapshot(); renderControls(); renderInstallation(); renderWebStatus(); securityUI?.render(); }; const updateViewInState = (nextView) => { const profiles = state.profiles.map((item) => item.profile.id === nextView.profile.id ? nextView : item); setState({ profiles }); }; const updateProfileAndSnapshot = (result) => { const existingView = currentView(); if (existingView && result?.profile) { updateViewInState({ ...existingView, profile: result.profile }); setState({ capabilities: result.profile.capabilities || state.capabilities }); } if (result?.snapshot) setState({ snapshot: result.snapshot }); renderAll(); }; const loadProfiles = async (preferredProfileId = state.selectedProfileId) => { clearError(); const profiles = await api.getProfiles(); const selectedProfileId = profiles.some((item) => item.profile.id === preferredProfileId) ? preferredProfileId : profiles[0]?.profile.id || ""; setState({ profiles, selectedProfileId }); if (!selectedProfileId) securityUI?.reset(); renderAll(); if (selectedProfileId) await selectProfile(selectedProfileId, false); }; const loadWebConsole = async (populateForm = true) => { const [installation, webStatus] = await Promise.all([ api.getInstallation(), api.getWebConsole(), ]); setState({ installation, webStatus }); renderInstallation(); renderWebStatus(); if (populateForm) populateWebForm(); }; const selectProfile = async (profileId, refreshList = true) => { if (!profileId) return; clearError(); if (state.unsubscribe) state.unsubscribe(); setState({ selectedProfileId: profileId, snapshot: null, capabilities: null, unsubscribe: null }); securityUI?.reset(profileId); renderAll(); try { const [viewResult, capabilityResult, snapshotResult] = await Promise.allSettled([ api.getProfile(profileId), api.getCapabilities(profileId), api.getSnapshot(profileId), ]); if (state.selectedProfileId !== profileId) return; if (viewResult.status === "fulfilled") updateViewInState(viewResult.value); if (capabilityResult.status === "fulfilled") setState({ capabilities: capabilityResult.value }); if (snapshotResult.status === "fulfilled") setState({ snapshot: snapshotResult.value }); renderAll(); securityUI?.load(profileId).catch((error) => showError(error, false)); const unsubscribe = await api.subscribe( profileId, (snapshot) => { if (state.selectedProfileId !== profileId) return; setState({ snapshot }); const view = currentView(); if (view?.instance && snapshot?.state && view.instance.status !== snapshot.state) { updateViewInState({ ...view, instance: { ...view.instance, status: snapshot.state }, }); } renderAll(); }, (error) => { if (state.selectedProfileId === profileId) showError(error, false); }, ); if (state.selectedProfileId === profileId) setState({ unsubscribe }); else unsubscribe(); if (refreshList && viewResult.status !== "fulfilled") await loadProfiles(profileId); } catch (error) { showError(error); } }; const runLifecycle = async (operation) => { const profileId = state.selectedProfileId; if (!profileId || state.isBusy) return; setBusy(true); clearError(); try { const handlers = { start: () => api.startInstance(profileId), stop: () => api.stopInstance(profileId), restart: () => api.restartInstance(profileId), }; const nextView = await handlers[operation](); updateViewInState(nextView); renderAll(); const label = operation === "start" ? "启动" : operation === "stop" ? "停止" : "重启"; api.notify("success", `${currentProfile()?.name || "EasyTier"} ${label}请求已完成`); announce(`${label}操作完成`); window.setTimeout(() => selectProfile(profileId, false), 500); } catch (error) { showError(error); } finally { setBusy(false); } }; const installEasyTier = async () => { if (state.isBusy) return; setBusy(true); clearError(); try { const force = byId("installEasyTierButton").dataset.force === "true"; const result = await api.installRecommended(force); setState({ installation: result.installation, webStatus: result.web }); populateWebForm(); renderInstallation(); renderWebStatus(); api.notify("success", `EasyTier ${result.installation?.installation?.version || "推荐版本"} 已安装`); announce("EasyTier 推荐版本安装完成"); } catch (error) { showError(error); } finally { setBusy(false); } }; const saveWebSettings = async (event) => { event.preventDefault(); if (state.isBusy) return; const form = event.currentTarget; const data = new FormData(form); const errorElement = byId("webSettingsError"); if (!form.checkValidity()) { errorElement.textContent = "请检查 EasyTier Web 必填项、URL 与端口格式。"; errorElement.hidden = false; form.reportValidity(); return; } const settings = { binaryPath: String(data.get("binaryPath") || "").trim(), apiServerAddress: String(data.get("apiServerAddress") || "").trim(), apiServerPort: Number(data.get("apiServerPort")), apiHost: String(data.get("apiHost") || "").trim(), configServerProtocol: String(data.get("configServerProtocol") || "udp"), configServerPort: Number(data.get("configServerPort")), autoStart: data.get("autoStart") === "on", disableRegistration: data.get("disableRegistration") === "on", allowAutoCreateUser: data.get("allowAutoCreateUser") === "on", }; errorElement.hidden = true; setBusy(true); clearError(); try { const webStatus = await api.updateWebConsole(settings, true); setState({ webStatus }); populateWebForm(); renderWebStatus(); api.notify("success", "EasyTier Web 配置已保存并应用"); announce("EasyTier Web 配置已保存"); } catch (error) { errorElement.textContent = describeError(error); errorElement.hidden = false; } finally { setBusy(false); } }; const runWebLifecycle = async (operation) => { if (state.isBusy) return; setBusy(true); clearError(); try { const handlers = { start: () => api.startWebConsole(), stop: () => api.stopWebConsole(), restart: () => api.restartWebConsole(), }; const webStatus = await handlers[operation](); setState({ webStatus }); renderWebStatus(); const label = operation === "start" ? "启动" : operation === "stop" ? "停止" : "重启"; api.notify("success", `EasyTier Web ${label}操作已完成`); announce(`EasyTier Web ${label}操作已完成`); window.setTimeout(() => loadWebConsole(false).catch((error) => showError(error, false)), 800); } catch (error) { showError(error); } finally { setBusy(false); } }; const openModal = (modal) => { const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null; document.querySelector(".app-shell")?.setAttribute("inert", ""); document.body.classList.add("modal-open"); modal.hidden = false; activeModal = { modal, previousFocus }; window.requestAnimationFrame(() => { modal.classList.add("is-visible"); modal.querySelector("input:not([type='hidden']), button, select, textarea")?.focus(); }); }; const closeModal = (modal) => { modal.classList.remove("is-visible"); modal.querySelectorAll('input[type="password"]').forEach((input) => { input.value = ""; }); const previousFocus = activeModal?.modal === modal ? activeModal.previousFocus : null; if (activeModal?.modal === modal) activeModal = null; window.setTimeout(() => { modal.hidden = true; document.body.classList.remove("modal-open"); document.querySelector(".app-shell")?.removeAttribute("inert"); previousFocus?.focus(); }, 160); }; const openProfileModal = (profileId = "") => { const profile = state.profiles.find((item) => item.profile.id === profileId)?.profile || null; const form = byId("profileForm"); resetForm(form); byId("profileFormError").hidden = true; byId("profileModalTitle").textContent = profile ? "编辑 Profile" : "新建 Profile"; byId("profileId").value = profile?.id || ""; byId("profileName").value = profile?.name || ""; byId("profilePreset").value = profile?.preset || "game-node"; byId("networkName").value = profile?.networkName || ""; byId("hostname").value = profile?.settings?.hostname || ""; byId("corePath").value = profile?.binary?.corePath || state.installation?.installation?.corePath || "easytier-core"; byId("cliPath").value = profile?.binary?.cliPath || state.installation?.installation?.cliPath || ""; byId("networkSecret").value = ""; byId("networkSecret").required = !profile; byId("peerUris").value = (profile?.settings?.peers || []).map((peer) => peer.uri).join("\n"); byId("autoStart").checked = Boolean(profile?.autoStart); openModal(byId("profileModal")); }; const saveProfile = async (event) => { event.preventDefault(); const form = event.currentTarget; const data = new FormData(form); const id = String(data.get("id") || ""); const existing = state.profiles.find((item) => item.profile.id === id)?.profile || null; const requiredFields = [ ["name", "Profile 名称"], ["networkName", "网络名称"], ["hostname", "节点主机名"], ["corePath", "easytier-core 路径"], ]; const missingFields = requiredFields .filter(([key]) => !String(data.get(key) || "").trim()) .map(([, label]) => label); const errorElement = byId("profileFormError"); if (missingFields.length > 0) { errorElement.textContent = `请填写:${missingFields.join("、")}。`; errorElement.hidden = false; return; } const peers = String(data.get("peerUris") || "") .split(/\r?\n/) .map((uri) => uri.trim()) .filter(Boolean) .map((uri) => existing?.settings?.peers?.find((peer) => peer.uri === uri) || { uri }); const binary = { corePath: String(data.get("corePath")).trim(), ...(String(data.get("cliPath") || "").trim() ? { cliPath: String(data.get("cliPath")).trim() } : {}), }; const profile = existing ? { ...existing, name: String(data.get("name")).trim(), preset: String(data.get("preset")), networkName: String(data.get("networkName")).trim(), autoStart: data.get("autoStart") === "on", binary, settings: { ...existing.settings, hostname: String(data.get("hostname")).trim(), peers }, } : { name: String(data.get("name")).trim(), description: "", preset: String(data.get("preset")), networkName: String(data.get("networkName")).trim(), autoStart: data.get("autoStart") === "on", binary, settings: { hostname: String(data.get("hostname")).trim(), peers }, }; const networkSecret = String(data.get("networkSecret") || ""); if (!existing && !networkSecret) { errorElement.textContent = "新建 Profile 时必须提供网络密钥。"; errorElement.hidden = false; return; } const secrets = networkSecret ? { networkSecret } : {}; errorElement.hidden = true; setBusy(true); try { const savedView = existing ? await api.updateProfile(existing.id, profile, secrets) : await api.createProfile(profile, secrets); closeModal(byId("profileModal")); api.notify("success", `Profile ${savedView.profile.name} 已保存`); await loadProfiles(savedView.profile.id); } catch (error) { errorElement.textContent = describeError(error); errorElement.hidden = false; } finally { setBusy(false); } }; const openConfirm = ({ title, message, action, showDeleteInstance = false }) => { byId("confirmTitle").textContent = title; byId("confirmMessage").textContent = message; byId("deleteInstanceRow").hidden = !showDeleteInstance; byId("deleteManagedInstance").checked = true; setState({ confirmAction: action }); openModal(byId("confirmModal")); byId("cancelConfirmButton").focus(); }; const deleteProfile = (profileId) => { const profile = state.profiles.find((item) => item.profile.id === profileId)?.profile; if (!profile) return; openConfirm({ title: "删除 Profile", message: `将永久删除“${profile.name}”的配置。此操作不可撤销。`, showDeleteInstance: true, action: async () => { await api.deleteProfile(profileId, byId("deleteManagedInstance").checked); api.notify("success", `Profile ${profile.name} 已删除`); await loadProfiles(""); }, }); }; const runRuntimeAction = async (action) => { const profileId = state.selectedProfileId; if (!profileId || state.isBusy) return; setBusy(true); clearError(); try { const result = await api.runAction(profileId, action); updateProfileAndSnapshot(result); api.notify("success", "EasyTier 运行时配置已更新"); } catch (error) { showError(error); } finally { setBusy(false); } }; const bindRuntimeForms = () => { ["connectorForm", "mappedListenerForm"].forEach((formId) => { byId(formId).addEventListener("submit", (event) => { event.preventDefault(); const prefix = formId === "connectorForm" ? "connector" : "mapped-listener"; const operation = event.submitter?.dataset.action || "add"; const uri = String(new FormData(event.currentTarget).get("uri") || "").trim(); if (uri) runRuntimeAction({ type: `${prefix}-${operation}`, uri }); }); }); byId("portForwardForm").addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); const operation = event.submitter?.dataset.action || "add"; const id = String(data.get("id") || "").trim(); if (!id) return; if (operation === "remove") runRuntimeAction({ type: "port-forward-remove", id }); else runRuntimeAction({ type: "port-forward-add", value: { id, proto: String(data.get("proto")), bindAddr: String(data.get("bindAddr") || "").trim(), dstAddr: String(data.get("dstAddr") || "").trim(), }, }); }); byId("whitelistForm").addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); const protocol = String(data.get("protocol")); const values = String(data.get("values") || "").split(",").map((value) => value.trim()).filter(Boolean); runRuntimeAction({ type: `${protocol}-whitelist-set`, values }); }); byId("loggerForm").addEventListener("submit", (event) => { event.preventDefault(); const level = String(new FormData(event.currentTarget).get("level") || "info"); runRuntimeAction({ type: "logger-set", value: { console: { level } } }); }); }; const bindEvents = () => { document.querySelectorAll("[data-view-target]").forEach((button) => { button.addEventListener("click", () => setView(button.dataset.viewTarget)); }); byId("profileSelect").addEventListener("change", (event) => selectProfile(event.target.value)); byId("createProfileButton").addEventListener("click", () => openProfileModal()); byId("createProfileSecondaryButton").addEventListener("click", () => openProfileModal()); byId("editCurrentProfileButton").addEventListener("click", () => openProfileModal(state.selectedProfileId)); byId("profileForm").addEventListener("submit", saveProfile); document.querySelectorAll("[data-close-modal]").forEach((button) => { button.addEventListener("click", () => { const modal = button.closest(".modal-backdrop"); if (!(modal instanceof HTMLElement)) return; if (modal === byId("credentialSecretModal")) securityUI?.clearSecret(); closeModal(modal); }); }); byId("startButton").dataset.runtimeControl = "true"; byId("stopButton").dataset.runtimeControl = "true"; byId("restartButton").dataset.runtimeControl = "true"; byId("startButton").addEventListener("click", () => runLifecycle("start")); byId("stopButton").addEventListener("click", () => runLifecycle("stop")); byId("restartButton").addEventListener("click", () => runLifecycle("restart")); byId("refreshButton").addEventListener("click", () => Promise.all([ loadProfiles(state.selectedProfileId), loadWebConsole(false), ]).catch(showError)); byId("webRefreshButton").addEventListener("click", () => loadWebConsole(true).catch(showError)); byId("installEasyTierButton").addEventListener("click", installEasyTier); byId("webSettingsForm").addEventListener("submit", saveWebSettings); byId("webStartButton").addEventListener("click", () => runWebLifecycle("start")); byId("webStopButton").addEventListener("click", () => runWebLifecycle("stop")); byId("webRestartButton").addEventListener("click", () => runWebLifecycle("restart")); byId("webOpenButton").addEventListener("click", (event) => { if (event.currentTarget.getAttribute("aria-disabled") === "true") event.preventDefault(); }); byId("copyWebConnectionButton").addEventListener("click", async () => { try { await navigator.clipboard.writeText(byId("webConnectionUri").value); announce("Core 配置服务器 URI 已复制"); api.notify("success", "Core 配置服务器 URI 已复制"); } catch { showError(new Error("无法复制连接 URI,请检查浏览器权限"), false); } }); byId("refreshCapabilitiesButton").addEventListener("click", async () => { if (!state.selectedProfileId) return; setBusy(true); try { const capabilities = await api.getCapabilities(state.selectedProfileId, true); setState({ capabilities }); await loadProfiles(state.selectedProfileId); announce("能力检测已刷新"); } catch (error) { showError(error); } finally { setBusy(false); } }); byId("profilesTableBody").addEventListener("click", (event) => { const button = event.target.closest("[data-profile-action]"); if (!button) return; const profileId = button.dataset.profileId; if (button.dataset.profileAction === "select") selectProfile(profileId); if (button.dataset.profileAction === "edit") openProfileModal(profileId); if (button.dataset.profileAction === "delete") deleteProfile(profileId); }); byId("cancelConfirmButton").addEventListener("click", () => closeModal(byId("confirmModal"))); byId("acceptConfirmButton").addEventListener("click", async () => { const action = state.confirmAction; if (!action) return; byId("acceptConfirmButton").disabled = true; try { await action(); closeModal(byId("confirmModal")); setState({ confirmAction: null }); } catch (error) { showError(error); } finally { byId("acceptConfirmButton").disabled = false; } }); byId("copyDiagnosticsButton").addEventListener("click", async () => { try { await navigator.clipboard.writeText(byId("rawSnapshot").textContent || "{}"); announce("诊断信息已复制"); api.notify("success", "诊断信息已复制"); } catch (error) { showError(new Error("无法复制诊断信息,请检查浏览器权限"), false); } }); bindRuntimeForms(); document.addEventListener("keydown", (event) => { if (!activeModal) { if (event.key === "Escape") api.closePlugin(); return; } if (event.key === "Escape") { event.preventDefault(); if (activeModal.modal === byId("credentialSecretModal")) securityUI?.clearSecret(); if (activeModal.modal === byId("confirmModal")) closeModal(byId("confirmModal")); else closeModal(activeModal.modal); return; } if (event.key !== "Tab") return; const focusable = [...activeModal.modal.querySelectorAll("button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex='0']")]; if (focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } }); }; const initialize = async () => { securityUI = window.createEasyTierSecurityUI({ api, byId, createElement, getContext: () => ({ profileId: state.selectedProfileId, profile: currentProfile(), instance: currentInstance(), capabilities: state.capabilities || currentProfile()?.capabilities || null, isBusy: state.isBusy, }), updateProfileView: (view) => { updateViewInState(view); setState({ capabilities: view.profile?.capabilities || state.capabilities }); renderAll(); }, resetForm, setBusy, showError, announce, openModal, openConfirm, notify: (type, message) => api.notify(type, message), }); bindEvents(); const requestedView = new URLSearchParams(window.location.search).get("view"); if (requestedView && Object.prototype.hasOwnProperty.call(views, requestedView)) setView(requestedView); securityUI.bind(); renderAll(); try { await api.ready(); byId("bridgeHealthDot").classList.add("is-online"); byId("bridgeHealthText").textContent = "已连接 GSM3 面板"; await Promise.all([loadProfiles(), loadWebConsole(true)]); api.notify("success", "EasyTier 管理控制台已就绪"); } catch (error) { byId("bridgeHealthText").textContent = "面板认证失败"; showError(error, false); } }; initialize(); })();