From 492d511551224744806ec52ca6198f0b0da05542 Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 30 Aug 2026 21:14:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=90=8E=E7=9A=84=E6=9C=8D=E5=8A=A1=E5=88=87?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fe/src/views/Setup/Setup.js | 104 +++++++++++------- fe/src/views/Setup/SetupNavigation.js | 147 ++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 36 deletions(-) create mode 100644 fe/src/views/Setup/SetupNavigation.js diff --git a/fe/src/views/Setup/Setup.js b/fe/src/views/Setup/Setup.js index e521334..67a3e14 100644 --- a/fe/src/views/Setup/Setup.js +++ b/fe/src/views/Setup/Setup.js @@ -12,6 +12,8 @@ import {Plus} from '@element-plus/icons-vue' import {http} from "@/utils/axios"; +import {buildFinalURL as buildSetupFinalURL, probeService} from './SetupNavigation.js' + const setupRequest = (payload) => http.post("/api/v1/setup", payload).then((response) => { if (!response || typeof response.errorNo !== "number") { throw new Error(lang.setup_failed) @@ -374,48 +376,78 @@ export function usePage() { return null }) } - const buildFinalURL = (sslData) => { - const scheme = "https:" - const configuredDomain = String(domainSettings.web_domain || "").trim() - if (!configuredDomain) { - return window.location.href - } + const buildFinalURL = (sslData) => buildSetupFinalURL(sslData, domainSettings.web_domain) - let target - try { - target = new URL(`${scheme}//${configuredDomain}`) - } catch (error) { - return window.location.href - } + let statusTimer = null + let statusCheckRunning = false + let statusCheckStartedAt = 0 + const statusCheckTimeout = 10 * 60 * 1000 - const currentHost = window.location.hostname.toLowerCase() - const targetHost = target.hostname.toLowerCase() - const currentPort = window.location.port - const isSameHost = currentHost === targetHost - const targetPort = Number(sslData?.httpsPort) || 443 - const proxyMode = sslData?.tlsMode === "proxy" - if (isSameHost && currentPort && !proxyMode) { - target.port = targetPort > 0 ? String(targetPort) : currentPort - } else if (targetPort > 0 && ((scheme === "http:" && targetPort !== 80) || (scheme === "https:" && targetPort !== 443))) { - target.port = String(targetPort) + const stopStatusCheck = () => { + if (statusTimer !== null) { + window.clearTimeout(statusTimer) + statusTimer = null } - - target.pathname = window.location.pathname - target.search = window.location.search - target.hash = window.location.hash - return target.toString() + statusCheckRunning = false + statusCheckStartedAt = 0 } - const checkStatus = () => { - setupRequest({"action": "get", "step": "ssl"}).then((res) => { - if (res.errorNo === 0 && res.data.ready) { - window.location.href = buildFinalURL(res.data) - return + const navigateToFormalService = (sslData) => { + stopStatusCheck() + fullscreenLoading.value = false + const target = buildFinalURL(sslData) + if (target && target !== window.location.href) { + window.location.href = target + } + } + + const scheduleStatusCheck = () => { + if (statusTimer !== null) { + window.clearTimeout(statusTimer) + } + statusTimer = window.setTimeout(() => { + statusTimer = null + checkStatus() + }, 1000) + } + + const checkStatus = async () => { + if (statusCheckRunning) { + return + } + if (!statusCheckStartedAt) { + statusCheckStartedAt = Date.now() + } + if (Date.now() - statusCheckStartedAt > statusCheckTimeout) { + stopStatusCheck() + fullscreenLoading.value = false + ElMessage.error(lang.setup_failed) + return + } + + statusCheckRunning = true + let sslData = sslRuntime.value || {} + try { + const response = await setupRequest({"action": "get", "step": "ssl"}) + if (response.errorNo === 0) { + sslData = response.data || sslData + sslRuntime.value = sslData + if (sslData.ready) { + navigateToFormalService(sslData) + return + } } - setTimeout(checkStatus, 1000) - }).catch(() => { - setTimeout(checkStatus, 1000) - }) + } catch (error) { + // 服务切换期间失败属于预期状态。 + } finally { + statusCheckRunning = false + } + + if (await probeService(sslData, domainSettings.web_domain)) { + navigateToFormalService(sslData) + return + } + scheduleStatusCheck() } const setDomainConfig = () => { diff --git a/fe/src/views/Setup/SetupNavigation.js b/fe/src/views/Setup/SetupNavigation.js new file mode 100644 index 0000000..e6786ac --- /dev/null +++ b/fe/src/views/Setup/SetupNavigation.js @@ -0,0 +1,147 @@ +const DEFAULT_HTTPS_PORT = 443 +const REDIRECT_STATUS_MIN = 300 +const REDIRECT_STATUS_MAX = 399 + +const currentLocation = () => { + if (typeof window === 'undefined' || !window.location) { + return null + } + return window.location +} + +const normalizeDomain = (value) => { + const domain = String(value || '').trim() + if (!domain) { + return '' + } + return domain.replace(/^[a-z][a-z\d+.-]*:\/\//i, '').replace(/\/.*$/, '') +} + +const isNonDefaultPort = (protocol, port) => { + if (!port) { + return false + } + return protocol === 'https:' ? port !== String(DEFAULT_HTTPS_PORT) : port !== '80' +} + +export const isProxyTLSMode = (sslData) => sslData?.tlsMode === 'proxy' + +export const buildFinalURL = (sslData, webDomain, location = currentLocation()) => { + if (!location) { + return '' + } + + const configuredDomain = normalizeDomain(webDomain) + if (!configuredDomain) { + return location.href + } + + let target + try { + target = new URL(`https://${configuredDomain}`) + } catch (error) { + return location.href + } + + const proxyMode = isProxyTLSMode(sslData) + const configuredPort = target.port + const targetPort = Number(sslData?.httpsPort) + if (!proxyMode) { + target.port = targetPort > 0 && targetPort !== DEFAULT_HTTPS_PORT ? String(targetPort) : '' + } else if (!isNonDefaultPort(target.protocol, configuredPort)) { + target.port = '' + } + + target.pathname = location.pathname + target.search = location.search + target.hash = location.hash + return target.toString() +} + +const createURL = (value, path) => { + try { + const url = new URL(path, value) + url.search = '' + url.hash = '' + return url.toString() + } catch (error) { + return '' + } +} + +export const getServiceProbeURLs = (sslData, webDomain, location = currentLocation()) => { + if (!location) { + return [] + } + + const urls = [] + const add = (value, path) => { + const url = createURL(value, path) + if (url && !urls.includes(url)) { + urls.push(url) + } + } + + add(location.href, '/api/ping') + add(location.href, '/api/v1/ping') + + const finalURL = buildFinalURL(sslData, webDomain, location) + if (finalURL) { + add(finalURL, '/api/ping') + add(finalURL, '/api/v1/ping') + } + return urls +} + +export const isServiceProbeSuccess = (response) => { + if (!response) { + return false + } + return response.status >= 200 && response.status < 300 +} + +export const isServiceRedirect = (response, location = currentLocation()) => { + if (!response || !location) { + return false + } + if (response.status < REDIRECT_STATUS_MIN || response.status > REDIRECT_STATUS_MAX) { + return false + } + + try { + return new URL(response.url).origin === location.origin + } catch (error) { + return false + } +} + +export const probeService = async (sslData, webDomain, location = currentLocation()) => { + if (!location || typeof fetch !== 'function') { + return false + } + + const proxyMode = isProxyTLSMode(sslData) + const urls = getServiceProbeURLs(sslData, webDomain, location) + for (const url of urls) { + try { + const response = await fetch(url, { + method: 'GET', + credentials: 'include', + cache: 'no-store', + redirect: 'manual', + headers: { + Accept: 'application/json' + } + }) + if (isServiceProbeSuccess(response)) { + return true + } + if (!proxyMode && isServiceRedirect(response, location)) { + return true + } + } catch (error) { + // 服务切换期间失败属于预期状态。 + } + } + return false +} \ No newline at end of file