修复登录失败原因提示 (#374)
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m1s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 4m27s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m44s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m34s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 3m56s
Project CI / AI game creator shell Rust crates (push) Successful in 2m4s
Project CI / Frontend tests (push) Successful in 4m29s
Project CI / Repository checks (push) Successful in 3m42s
Project CI / Backend tests (push) Successful in 7m0s
Project CI / AI game creator shell web tests (push) Successful in 2m44s
Project CI / Native shell tests (push) Successful in 7m25s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m1s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 4m27s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 4m44s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m34s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 3m56s
Project CI / AI game creator shell Rust crates (push) Successful in 2m4s
Project CI / Frontend tests (push) Successful in 4m29s
Project CI / Repository checks (push) Successful in 3m42s
Project CI / Backend tests (push) Successful in 7m0s
Project CI / AI game creator shell web tests (push) Successful in 2m44s
Project CI / Native shell tests (push) Successful in 7m25s
登录失败时,部分网关或兼容接口返回字符串形式的 error,前端解析失败后只显示通用“登录失败”,用户无法判断具体原因。 本次修改: - 统一错误解析器支持字符串 error,并保留标准嵌套错误的优先级。 - 增加登录接口返回“手机号或密码错误”的回归测试。 - 同步认证排障记录。 验证: - API 客户端测试 32 项通过 - 认证服务与登录弹窗测试 55 项通过 - npm run typecheck 通过 - npm run lint:eslint 通过 - npm run check:encoding 通过 Reviewed-on: #374 Co-authored-by: kdletters <kdletters@qq.com> Co-committed-by: kdletters <kdletters@qq.com>
This commit was merged in pull request #374.
This commit is contained in:
@@ -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