修复AGC启动检查失败不给可见反馈的回退
- hydrateAuth 的 catch 恢复改动前的可见反馈:登录状态检查超时等非载体错误先写 authCheckError/loginStatus 再原样抛出 - 系统变体先按类型化载荷(reason/status/detail/serverMessage)拼出提示文案,再原样抛出交全局 unhandledrejection 上报 - appSurface 用例改为断言启动超时后登录页显示原因并保留「重试登录状态检查」
This commit is contained in:
@@ -35,10 +35,16 @@ import {
|
||||
captureClientError,
|
||||
installWebviewLogBridge,
|
||||
} from '../services/errorReporting';
|
||||
import type { AuthNetworkFailure } from '../services/generated/AuthNetworkFailure';
|
||||
import type { AuthResponseInvalid } from '../services/generated/AuthResponseInvalid';
|
||||
import type { AuthServiceUnavailable } from '../services/generated/AuthServiceUnavailable';
|
||||
import type { ClientSessionPersistFailed } from '../services/generated/ClientSessionPersistFailed';
|
||||
import type { PasswordLoginRejected } from '../services/generated/PasswordLoginRejected';
|
||||
import type { PhoneCodeLoginRejected } from '../services/generated/PhoneCodeLoginRejected';
|
||||
import type { RuntimeSessionInstallFailed } from '../services/generated/RuntimeSessionInstallFailed';
|
||||
import type { SendCodeRejected } from '../services/generated/SendCodeRejected';
|
||||
import type { ServerAddressRejected } from '../services/generated/ServerAddressRejected';
|
||||
import type { UnexpectedRejection } from '../services/generated/UnexpectedRejection';
|
||||
import {
|
||||
beginPlatformSessionClearTransition,
|
||||
beginPlatformSessionTransition,
|
||||
@@ -269,7 +275,13 @@ export function AuthenticatedClient({
|
||||
if (!isActiveRun()) return;
|
||||
// 失败一律先离开检查态:系统变体虽然要原样抛出上报,界面也不能卡在 loading。
|
||||
setAuthStatus('unauthenticated');
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) throw error;
|
||||
// 启动检查超时等我们自己的失败:改动前会显示原因 + 重试按钮,这里恢复同样的可见反馈。
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) {
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
const failure = error.error;
|
||||
switch (failure.type) {
|
||||
// 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。
|
||||
@@ -368,15 +380,83 @@ export function AuthenticatedClient({
|
||||
case 'sessionInvalidated':
|
||||
case 'permissionDenied':
|
||||
break;
|
||||
// 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkFailure':
|
||||
case 'authServiceUnavailable':
|
||||
case 'unexpectedRejection':
|
||||
case 'authResponseInvalid':
|
||||
case 'clientSessionPersistFailed':
|
||||
case 'runtimeSessionInstallFailed':
|
||||
case 'authClientInitFailed':
|
||||
// 系统失败:调用方处理不了,先按原始事实给用户可见反馈,再原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkFailure': {
|
||||
const payload = failure as AuthNetworkFailure;
|
||||
const text =
|
||||
payload.reason === 'timeout'
|
||||
? '登录服务响应超时,请检查服务器地址和网络后重试'
|
||||
: '无法连接登录服务,请确认配套后端或 API 代理已启动后重试';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
case 'authServiceUnavailable': {
|
||||
const payload = failure as AuthServiceUnavailable;
|
||||
const text = `登录服务暂时不可用(HTTP ${payload.status})`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
case 'unexpectedRejection': {
|
||||
const payload = failure as UnexpectedRejection;
|
||||
const text = payload.serverMessage
|
||||
? `登录服务请求失败(HTTP ${payload.status}):${payload.serverMessage}`
|
||||
: `登录服务请求失败(HTTP ${payload.status})`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
case 'authResponseInvalid': {
|
||||
const payload = failure as AuthResponseInvalid;
|
||||
let text: string;
|
||||
switch (payload.reason) {
|
||||
case 'notJson':
|
||||
text = '登录服务响应不是合法 JSON';
|
||||
break;
|
||||
case 'invalidBody':
|
||||
text = '登录服务响应结构不符合契约';
|
||||
break;
|
||||
case 'missingRefreshCookie':
|
||||
text = '登录服务未返回新的续期凭据';
|
||||
break;
|
||||
case 'missingUserIdentity':
|
||||
text = '登录服务未返回用户身份';
|
||||
break;
|
||||
case 'serverRejected':
|
||||
text = payload.serverMessage
|
||||
? `登录服务拒绝了本次请求:${payload.serverMessage}`
|
||||
: '登录服务拒绝了本次请求';
|
||||
break;
|
||||
default: {
|
||||
expectNever(payload.reason);
|
||||
text = '登录服务响应不符合契约';
|
||||
}
|
||||
}
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
case 'clientSessionPersistFailed': {
|
||||
const payload = failure as ClientSessionPersistFailed;
|
||||
const text = `本机登录凭据读写失败:${payload.detail}`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
case 'runtimeSessionInstallFailed': {
|
||||
const payload = failure as RuntimeSessionInstallFailed;
|
||||
const text = `本机运行时会话安装失败:${payload.detail}`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
case 'authClientInitFailed': {
|
||||
const text = '创建登录网络客户端失败';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
throw error;
|
||||
}
|
||||
default: {
|
||||
expectNever(failure);
|
||||
throw error;
|
||||
|
||||
@@ -127,9 +127,15 @@ export function registerAuthTests() {
|
||||
).toBe(true),
|
||||
{ timeout: 20_000 },
|
||||
);
|
||||
// 启动检查失败在改动前就会显示原因 + 重试按钮;上报是叠加的,不是替换。
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'检查登录状态超时,请检查服务器地址和网络后重试',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: '重试登录状态检查' }),
|
||||
).toBeNull();
|
||||
).not.toBeNull();
|
||||
await screen.findByRole('main', { name: '登录' });
|
||||
} finally {
|
||||
window.removeEventListener('unhandledrejection', onRejection);
|
||||
|
||||
Reference in New Issue
Block a user