修复登录失败原因提示 #374

Merged
kdletters merged 1 commits from codex/fix-login-error-details into master 2026-09-15 19:27:14 +08:00
3 changed files with 55 additions and 8 deletions
@@ -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` 通过。
+22 -8
View File
@@ -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' &&
+26
View File
@@ -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(