merge: 前端错误文案 friendlyFromApiError(QA #2/#11,去通用 toast)

This commit is contained in:
Yaojia Wang
2026-06-24 18:01:15 +02:00
6 changed files with 49 additions and 13 deletions

View File

@@ -6,7 +6,7 @@ import { ConflictCard } from "@/components/review/ConflictCard";
import { useToast } from "@/components/Toast";
import { api } from "@/lib/api/client";
import { buildResumeRequest } from "@/lib/chain/chain";
import { friendlyError } from "@/lib/errors/messages";
import { friendlyFromApiError } from "@/lib/errors/messages";
import type { ReviewConflict } from "@/lib/review/sse";
import {
allResolved,
@@ -99,7 +99,7 @@ export function ChainAdjudication({
},
);
if (error) {
toast(friendlyError(undefined).text, "error");
toast(friendlyFromApiError(error).text, "error");
setResuming(false);
return;
}

View File

@@ -6,7 +6,7 @@ import { AppShell } from "@/components/AppShell";
import { useToast } from "@/components/Toast";
import { api } from "@/lib/api/client";
import { chainPhase, chainResultView, type ChainKind } from "@/lib/chain/chain";
import { friendlyError } from "@/lib/errors/messages";
import { friendlyFromApiError } from "@/lib/errors/messages";
import { useJobPoll } from "@/lib/jobs/useJobPoll";
import type { ProjectResponse } from "@/lib/api/types";
import { ChainAdjudication } from "./ChainAdjudication";
@@ -58,7 +58,7 @@ export function ChainPage({ project }: ChainPageProps) {
},
);
if (error || !data) {
toast(friendlyError(undefined).text, "error");
toast(friendlyFromApiError(error).text, "error");
return;
}
setJobId(data.job_id);

View File

@@ -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("出错了,请稍后重试。");
});
});

View File

@@ -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);
}

View File

@@ -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→去设置 provider422→校验文案
// 而非永远的通用「大纲生成失败」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 ?? []);

File diff suppressed because one or more lines are too long