|
|
@@ -33,31 +33,106 @@ import { setupAntdTabsListenerPatch } from './setupAntdTabsListenerPatch';
|
|
|
import { setupAntdPopupBehavior } from './setupAntdPopupBehavior';
|
|
|
import { closeSmartWindow, focusMainWindow, initWindowRole, isSmartWindow } from '@/core/logics/appWindow';
|
|
|
import { initSessionSync } from '@/core/logics/sessionSync';
|
|
|
-import { isServerBindingEnabled, isServerSetupMissing } from '@/core/settings/serverSetting';
|
|
|
+import { isServerSetupMissing } from '@/core/settings/serverSetting';
|
|
|
|
|
|
-async function waitForBackendReady(maxRetries: number = 60, interval: number = 2000): Promise<boolean> {
|
|
|
+const STARTUP_PROBE_RETRIES = 10;
|
|
|
+const STARTUP_PROBE_INTERVAL = 3000;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 更新启动 loading 页上的提示。
|
|
|
+ *
|
|
|
+ * <p>此时 Vue 还没 mount,只能直接操作 index.html 里的静态 DOM。
|
|
|
+ * 重连中显示红色文案 + 文字前的小旋转图标;成功或最终失败时移除旋转图标。
|
|
|
+ */
|
|
|
+function updateStartupLoadingTip(text: string, state: 'retrying' | 'success' | 'failed' = 'retrying') {
|
|
|
+ const tip = document.querySelector<HTMLElement>('.app-loading-tip');
|
|
|
+ if (!tip) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ tip.textContent = text;
|
|
|
+ tip.classList.toggle('is-retrying', state === 'retrying');
|
|
|
+ tip.classList.toggle('is-failed', state === 'failed');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 等待后端就绪:10 次 × 3 秒(约 30 秒),并在 loading 页实时展示重试次数。
|
|
|
+ *
|
|
|
+ * <p>按固定节拍重试而不是「探测超时 + 再等一个间隔」,单次探测上限也是 3 秒,
|
|
|
+ * 保证整体耗时不随失败次数膨胀;10 次仍未连上就停转,由调用方展示重连/修改服务地址入口。
|
|
|
+ */
|
|
|
+async function waitForBackendReady(
|
|
|
+ maxRetries: number = STARTUP_PROBE_RETRIES,
|
|
|
+ interval: number = STARTUP_PROBE_INTERVAL,
|
|
|
+): Promise<boolean> {
|
|
|
console.log('[bootstrap] waiting for backend to be ready...');
|
|
|
- for (let i = 0; i < maxRetries; i++) {
|
|
|
+ const startedAt = Date.now();
|
|
|
+ const deadline = startedAt + maxRetries * interval;
|
|
|
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
|
+ const remaining = deadline - Date.now();
|
|
|
+ if (remaining <= 0) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ updateStartupLoadingTip(`正在连接后端服务…(第 ${attempt}/${maxRetries} 次)`);
|
|
|
try {
|
|
|
- const response = await healthApi();
|
|
|
- console.log(response);
|
|
|
+ const response = await healthApi(Math.min(interval, remaining));
|
|
|
if (response?.code === 200) {
|
|
|
- console.log(`[bootstrap] backend ready after ${i + 1} attempts`);
|
|
|
+ console.log(`[bootstrap] backend ready after ${attempt} attempts`);
|
|
|
+ updateStartupLoadingTip('后端服务已连接,正在加载…', 'success');
|
|
|
return true;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
// Backend not ready yet, continue waiting
|
|
|
}
|
|
|
|
|
|
- if (i < maxRetries - 1) {
|
|
|
- await new Promise((resolve) => setTimeout(resolve, interval));
|
|
|
+ const nextAt = startedAt + attempt * interval;
|
|
|
+ const wait = Math.min(nextAt - Date.now(), deadline - Date.now());
|
|
|
+ if (attempt < maxRetries && wait > 0) {
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, wait));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- console.warn(`[bootstrap] backend not ready after ${maxRetries} attempts, continuing anyway`);
|
|
|
+ console.warn(`[bootstrap] backend not ready after ${maxRetries} attempts, waiting for manual reconnect`);
|
|
|
+ updateStartupLoadingTip(`后端服务连接失败(已重试 ${maxRetries} 次),可重连或修改服务地址`, 'failed');
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+/** 显示 / 隐藏重试失败后的「重连」「修改服务地址」操作区 */
|
|
|
+function setReconnectActionsVisible(visible: boolean) {
|
|
|
+ document.querySelector<HTMLElement>('.app-loading-actions')?.classList.toggle('is-visible', visible);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 10 次探测失败后停在 loading 页,等待用户重连或修改服务地址。
|
|
|
+ *
|
|
|
+ * <p>重连会重新执行一轮 10 次探测;连上后整页 reload,走正常启动流程。
|
|
|
+ * 修改服务地址则进入授权页的地址卡片(带 error=unreachable),避免绑错地址后无路可走。
|
|
|
+ */
|
|
|
+function showReconnectState(onServerSetup: () => void) {
|
|
|
+ const reconnect = document.querySelector<HTMLButtonElement>('.app-loading-reconnect');
|
|
|
+ const serverSetup = document.querySelector<HTMLButtonElement>('.app-loading-server-setup');
|
|
|
+ setReconnectActionsVisible(true);
|
|
|
+
|
|
|
+ if (reconnect) {
|
|
|
+ reconnect.disabled = false;
|
|
|
+ reconnect.onclick = async () => {
|
|
|
+ reconnect.disabled = true;
|
|
|
+ setReconnectActionsVisible(false);
|
|
|
+ const ready = await waitForBackendReady();
|
|
|
+ if (ready) {
|
|
|
+ updateStartupLoadingTip('后端服务已连接,正在重新加载…', 'success');
|
|
|
+ window.location.reload();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ reconnect.disabled = false;
|
|
|
+ setReconnectActionsVisible(true);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (serverSetup) {
|
|
|
+ serverSetup.onclick = () => onServerSetup();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function getStartupRouteInfo() {
|
|
|
// hash 模式:路径在 location.hash 中(如 #/data/index)
|
|
|
const hash = window.location.hash?.replace(/^#/, '') || '';
|
|
|
@@ -258,22 +333,27 @@ async function bootstrap() {
|
|
|
}
|
|
|
|
|
|
// Wait for backend to be ready
|
|
|
- // 绑定地址是刚测试通过的,按「本地后端冷启动」等两分钟没有意义,收紧到约 1 分钟。
|
|
|
+ // 统一 10 次 × 3 秒(约 30 秒),loading 页展示重试次数;
|
|
|
// 直接打开授权页(含旧的服务地址页地址)时不做探测:页面对失败已有自己的展示。
|
|
|
- const bindingEnabled = isServerBindingEnabled();
|
|
|
const startupPath = getStartupRouteInfo().path;
|
|
|
const skipBackendProbe = startupPath === PageEnum.BASE_AUTHORIZATION || startupPath === PageEnum.BASE_SERVER_SETUP;
|
|
|
- const backendReady = skipBackendProbe
|
|
|
- ? true
|
|
|
- : await waitForBackendReady(bindingEnabled ? 6 : 60, bindingEnabled ? 1500 : 2000);
|
|
|
-
|
|
|
- // 地址已绑定但服务不可达:回到入口页并切到服务地址卡片,而不是带着坏地址继续跑授权校验
|
|
|
- if (!backendReady && bindingEnabled) {
|
|
|
- await router.replace({ path: PageEnum.BASE_AUTHORIZATION, query: { error: 'unreachable' } });
|
|
|
- await router.isReady();
|
|
|
- setupGlobDirectives(app);
|
|
|
- setupErrorHandle(app);
|
|
|
- app.mount('#app');
|
|
|
+ const backendReady = skipBackendProbe ? true : await waitForBackendReady();
|
|
|
+
|
|
|
+ // 后端不可达:停在 loading 页提供「重连 / 修改服务地址」,不先带着坏地址进入授权校验
|
|
|
+ if (!backendReady) {
|
|
|
+ showReconnectState(async () => {
|
|
|
+ // 智能窗口只承载工作台:地址/授权问题交回主窗口处理
|
|
|
+ if (isSmartWindow()) {
|
|
|
+ await focusMainWindow();
|
|
|
+ void closeSmartWindow();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await router.replace({ path: PageEnum.BASE_AUTHORIZATION, query: { error: 'unreachable' } });
|
|
|
+ await router.isReady();
|
|
|
+ setupGlobDirectives(app);
|
|
|
+ setupErrorHandle(app);
|
|
|
+ app.mount('#app');
|
|
|
+ });
|
|
|
return;
|
|
|
}
|
|
|
|