fix(qa): 前端错误文案——按 API 信封 surface 真实错因,去掉通用 toast
新增 friendlyFromApiError(apiError):兼容业务信封 {error:{code,message}}(ARCH §7.1)
与 FastAPI 422 {detail:[...]},422 映射为友好 VALIDATION 文案(不泄露技术细节)。
接入三处此前吞掉错因、永显通用文案的调用点:
- useOutline(QA #2):原 env.error.code 漏读 422 detail,永显「大纲生成失败」。
- ChainPage / ChainAdjudication(QA #11):原 friendlyError(undefined) 永显通用 toast,
现 surface 真实 code(如 LLM_UNAVAILABLE→去设置 provider / CONFLICT→提示裁决)。
+ messages.test.ts 加 3 例(业务码带动作 / 422→VALIDATION / 未知形兜底)。
门禁绿:vitest / tsc / eslint 干净。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { friendlyError } from "./messages";
|
||||
import { friendlyError, friendlyFromApiError } from "./messages";
|
||||
|
||||
describe("friendlyError", () => {
|
||||
it("maps LLM_UNAVAILABLE to copy + a settings action", () => {
|
||||
@@ -26,3 +26,21 @@ describe("friendlyError", () => {
|
||||
expect(friendlyError("WEIRD_CODE", " ").text).toBe("出错了,请稍后重试。");
|
||||
});
|
||||
});
|
||||
|
||||
describe("friendlyFromApiError", () => {
|
||||
it("surfaces a business error envelope code + action (QA #2/#11)", () => {
|
||||
const e = friendlyFromApiError({ error: { code: "LLM_UNAVAILABLE", message: "x" } });
|
||||
expect(e.text).toContain("未连接");
|
||||
expect(e.actionHref).toBe("/settings/providers");
|
||||
});
|
||||
|
||||
it("maps a FastAPI 422 detail envelope to friendly VALIDATION copy (no leak)", () => {
|
||||
const e = friendlyFromApiError({ detail: [{ msg: "Input should be >= 1", loc: ["volume"] }] });
|
||||
expect(e.text).toBe("提交内容有误,请检查后重试。");
|
||||
});
|
||||
|
||||
it("falls back to generic copy for null/unknown shapes", () => {
|
||||
expect(friendlyFromApiError(null).text).toBe("出错了,请稍后重试。");
|
||||
expect(friendlyFromApiError({ weird: 1 }).text).toBe("出错了,请稍后重试。");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,3 +44,22 @@ export function friendlyError(
|
||||
const fallback = fallbackMessage?.trim();
|
||||
return { text: fallback && fallback.length > 0 ? fallback : DEFAULT_TEXT };
|
||||
}
|
||||
|
||||
// 从原始 API 错误对象(openapi-fetch 的 `error`)提取友好文案。
|
||||
// 兼容两种信封:业务错误 `{error:{code,message}}`(ARCH §7.1)与 FastAPI 校验错误 `{detail:[...]}`。
|
||||
// 422 一律映射为友好的 VALIDATION 文案(不泄露技术性 detail)。未知形 → 通用兜底。
|
||||
export function friendlyFromApiError(apiError: unknown): FriendlyError {
|
||||
if (apiError && typeof apiError === "object") {
|
||||
const env = apiError as {
|
||||
error?: { code?: string; message?: string };
|
||||
detail?: unknown;
|
||||
};
|
||||
if (env.error?.code) {
|
||||
return friendlyError(env.error.code, env.error.message);
|
||||
}
|
||||
if (env.detail !== undefined) {
|
||||
return friendlyError("VALIDATION");
|
||||
}
|
||||
}
|
||||
return friendlyError(undefined);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api/client";
|
||||
import { useToast } from "@/components/Toast";
|
||||
import { friendlyFromApiError } from "@/lib/errors/messages";
|
||||
import type { OutlineChapterView } from "@/lib/api/types";
|
||||
|
||||
export type OutlineStatus = "idle" | "generating" | "ready" | "error";
|
||||
@@ -42,14 +43,12 @@ export function useOutline(initial: OutlineChapterView[]): UseOutline {
|
||||
},
|
||||
);
|
||||
if (apiError || !data) {
|
||||
const env = apiError as
|
||||
| { error?: { code?: string; message?: string } }
|
||||
| undefined;
|
||||
const code = env?.error?.code ?? "OUTLINE_FAILED";
|
||||
const message = env?.error?.message ?? "大纲生成失败,请重试。";
|
||||
setError({ code, message });
|
||||
// surface 真实错因(如 LLM_UNAVAILABLE→去设置 provider;422→校验文案),
|
||||
// 而非永远的通用「大纲生成失败」(QA #2:原 env.error.code 漏读 422 的 detail 信封)。
|
||||
const friendly = friendlyFromApiError(apiError);
|
||||
setError({ code: "OUTLINE_FAILED", message: friendly.text });
|
||||
setStatus("error");
|
||||
toast(message, "error");
|
||||
toast(friendly.text, "error");
|
||||
return;
|
||||
}
|
||||
setChapters(data.chapters ?? []);
|
||||
|
||||
Reference in New Issue
Block a user