修复登录失败原因提示 #374
@@ -5596,3 +5596,10 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- 原因:健康检查只能证明“有服务响应”,不能证明服务属于当前工作树;旧 `.app/dev-stack.json` 可能没有当前 `repoRoot`、`instanceId` 和服务级 dataDir 身份。
|
||||
- 处理:先读取 `.app/dev-stack.json`,核对顶层 `repoRoot + instanceId`,再核对服务 `repoRoot + instanceId + dataDir + pid + port`;AGC Vite marker 还必须带 `repoRoot + processId + port`。任何字段缺失或不匹配都拒绝静默复用,改为启动当前工作树自己的服务或明确提示清理。
|
||||
- 验证:`scripts/dev.test.ts`、`apps/ai-game-creator-shell/tests/start-dev-stack.test.ts` 覆盖 snapshot identity 和旧状态拒绝复用;运行时记录实际端口、进程命令行和 dataDir,不要只记录 HTTP 200。
|
||||
|
||||
## 2026-09-15 登录失败提示必须保留接口返回原因
|
||||
|
||||
- **现象**:账号登录失败时页面只显示“登录失败”,用户无法判断是手机号、验证码、密码还是服务状态问题。
|
||||
- **原因**:统一错误解析器只处理标准 `error.message/details` 结构;部分网关或旧兼容响应使用字符串 `error`,解析失败后回落到登录接口传入的通用文案。
|
||||
- **处理**:`parseApiErrorMessage` 同时支持字符串 `error`,标准嵌套结构保持原有优先级;未知或空响应继续使用通用兜底。
|
||||
- **验证**:`src/services/apiClient.test.ts` 新增字符串错误响应回归用例,定向测试 32 项通过,`npm run typecheck` 通过。
|
||||
|
||||
@@ -177,23 +177,34 @@ export function parseApiErrorMessage(rawText: string, fallbackMessage: string) {
|
||||
const parsed = JSON.parse(rawText) as
|
||||
| ApiErrorResponse
|
||||
| {
|
||||
error?: {
|
||||
message?: string;
|
||||
code?: string;
|
||||
details?: Record<string, unknown> | null;
|
||||
};
|
||||
error?:
|
||||
| string
|
||||
| {
|
||||
message?: string;
|
||||
code?: string;
|
||||
details?: Record<string, unknown> | null;
|
||||
};
|
||||
message?: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
const detailMessage = readApiErrorDetailMessage(parsed.error?.details);
|
||||
const detailMessage =
|
||||
typeof parsed.error === 'object' && parsed.error !== null
|
||||
? readApiErrorDetailMessage(parsed.error.details)
|
||||
: '';
|
||||
|
||||
if (detailMessage) {
|
||||
return detailMessage;
|
||||
}
|
||||
|
||||
if (typeof parsed.error === 'string' && parsed.error.trim()) {
|
||||
return parsed.error.trim();
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsed.error?.message === 'string' &&
|
||||
typeof parsed.error === 'object' &&
|
||||
parsed.error !== null &&
|
||||
typeof parsed.error.message === 'string' &&
|
||||
parsed.error.message.trim()
|
||||
) {
|
||||
return parsed.error.message.trim();
|
||||
@@ -209,7 +220,10 @@ export function parseApiErrorMessage(rawText: string, fallbackMessage: string) {
|
||||
}
|
||||
|
||||
const errorCode =
|
||||
typeof parsed.error?.code === 'string' && parsed.error.code.trim()
|
||||
typeof parsed.error === 'object' &&
|
||||
parsed.error !== null &&
|
||||
typeof parsed.error.code === 'string' &&
|
||||
parsed.error.code.trim()
|
||||
? parsed.error.code.trim()
|
||||
: 'code' in parsed &&
|
||||
typeof parsed.code === 'string' &&
|
||||
|
||||
@@ -1031,6 +1031,32 @@ describe('apiClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves a concrete reason from legacy string error responses', async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
createResponseMock({
|
||||
status: 401,
|
||||
body: JSON.stringify({ error: '手机号或密码错误' }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
requestJson(
|
||||
'/api/auth/entry',
|
||||
{
|
||||
method: 'POST',
|
||||
},
|
||||
'登录失败',
|
||||
{ skipAuth: true, skipRefresh: true },
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
message: '手机号或密码错误',
|
||||
status: 401,
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers api error details.reason over details.message for diagnostics', async () => {
|
||||
setStoredAccessToken('details-reason-first-token', { emit: false });
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
|
||||
Reference in New Issue
Block a user