统一客户端维护态错误弹窗
Project CI / AI game creator shell Rust crates (push) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / Backend tests (push) Successful in 3m54s
Project CI / Frontend tests (push) Successful in 2m5s
Project CI / Native shell tests (push) Successful in 6m4s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m14s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m19s
Project CI / AI game creator shell web tests (push) Successful in 1m23s
Project CI / Repository checks (push) Successful in 1m55s

客户端维护响应统一识别并广播维护事件

AGC 与网页端根部展示维护大弹窗,收口发布、上传、资源换签和登录错误

补充维护态弹窗测试、类型检查与实施计划说明
This commit is contained in:
2026-09-23 23:16:23 +08:00
parent 093f832ef9
commit efbdf7031e
10 changed files with 318 additions and 24 deletions
+2
View File
@@ -7,6 +7,7 @@ import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
import { FloatingFeedbackEntry } from './components/common/FloatingFeedbackEntry';
import { MaintenanceNotice } from './components/common/MaintenanceNotice';
import { stabilizeMobileViewportKeyboardFocus } from './mobileViewportKeyboardFocus';
import { lockMobileViewportZoom } from './mobileViewportZoomLock';
import { resolveAppRoute } from './routing/activeAppRoutes';
@@ -48,6 +49,7 @@ void refreshNativeAppHostRuntime();
root.render(
<StrictMode>
<Suspense fallback={null}>{routeElement}</Suspense>
<MaintenanceNotice />
{route.kind === 'platform' ? <FloatingFeedbackEntry /> : null}
</StrictMode>,
);
@@ -0,0 +1,67 @@
import { useEffect, useState } from 'react';
import { MAINTENANCE_EVENT } from '../../services/apiClient';
import { PlatformActionButton } from './PlatformActionButton';
import { UnifiedModal } from './UnifiedModal';
type MaintenanceEventDetail = {
code?: string;
status?: number;
requestId?: string;
};
function isMaintenanceEvent(
event: Event,
): event is CustomEvent<MaintenanceEventDetail> {
return event.type === MAINTENANCE_EVENT;
}
/**
* 维护态的唯一客户端出口。请求层识别到维护响应后广播事件,页面不再各自展示业务兜底错误。
*/
export function MaintenanceNotice() {
const [open, setOpen] = useState(false);
useEffect(() => {
const handleMaintenance = (event: Event) => {
if (isMaintenanceEvent(event)) {
setOpen(true);
}
};
window.addEventListener(MAINTENANCE_EVENT, handleMaintenance);
return () =>
window.removeEventListener(MAINTENANCE_EVENT, handleMaintenance);
}, []);
return (
<UnifiedModal
open={open}
title="系统维护中"
description="服务正在维护,当前操作暂时无法完成。请稍后再试。"
onClose={() => setOpen(false)}
size="lg"
closeOnBackdrop={false}
showCloseButton={false}
portalTheme="light"
panelClassName="min-h-[min(42vh,28rem)]"
bodyClassName="flex flex-1 items-center justify-center px-6 py-10 sm:px-12 sm:py-16"
footerClassName="justify-center px-6 py-5 sm:px-12"
footer={
<PlatformActionButton
type="button"
onClick={() => setOpen(false)}
tone="primary"
size="md"
shape="pill"
className="min-w-36"
>
我知道了
</PlatformActionButton>
}
>
<div className="text-center text-base leading-7 text-[var(--platform-text-base)] sm:text-lg">
为避免维护期间出现不一致,相关页面错误会统一收口到此提示。
</div>
</UnifiedModal>
);
}
+38 -1
View File
@@ -12,6 +12,7 @@ import { getHostRuntime } from './host-bridge/hostBridge';
const ACCESS_TOKEN_KEY = 'genarrative.auth.access-token.v1';
export const AUTH_STATE_EVENT = 'genarrative-auth-state-changed';
export const MAINTENANCE_EVENT = 'genarrative-maintenance-detected';
const REQUEST_ID_HEADER = 'x-request-id';
const API_VERSION_HEADER = 'x-api-version';
const ROUTE_VERSION_HEADER = 'x-route-version';
@@ -505,6 +506,38 @@ function shouldRetryResponse(
);
}
export function isMaintenanceApiError(error: unknown) {
return (
error instanceof ApiClientError &&
(error.code.trim().toUpperCase() === 'MAINTENANCE' ||
(error.status === 503 && error.message.includes('维护')))
);
}
export function emitMaintenanceNotice(error?: unknown) {
if (typeof globalThis.dispatchEvent !== 'function') {
return;
}
if (error !== undefined && !isMaintenanceApiError(error)) {
return;
}
const detail =
error instanceof ApiClientError
? {
code: error.code,
status: error.status,
requestId: error.meta.requestId,
}
: undefined;
const event =
typeof CustomEvent === 'function'
? new CustomEvent(MAINTENANCE_EVENT, { detail })
: new Event(MAINTENANCE_EVENT);
globalThis.dispatchEvent(event);
}
export function isAbortError(error: unknown) {
return (
error instanceof Error &&
@@ -1001,7 +1034,7 @@ async function buildApiClientError(
undefined;
const baseMessage = parseApiErrorMessage(responseText, fallbackMessage);
return new ApiClientError({
const error = new ApiClientError({
message: requestId
? `${baseMessage}(requestId: ${requestId})`
: baseMessage,
@@ -1024,6 +1057,10 @@ async function buildApiClientError(
},
responseText,
});
if (isMaintenanceApiError(error)) {
emitMaintenanceNotice(error);
}
return error;
}
export async function requestJson<T>(