"use client"; import { useCallback, useState } from "react"; import { api } from "@/lib/api/client"; import { useToast } from "@/components/Toast"; import { friendlyFromApiError, type FriendlyError, } from "@/lib/errors/messages"; import type { OutlineChapterView } from "@/lib/api/types"; export type OutlineStatus = "idle" | "generating" | "ready" | "error"; export interface UseOutline { chapters: OutlineChapterView[]; status: OutlineStatus; // 直接透传 friendlyFromApiError 的结果(含 text + 可选 actionHref/actionLabel), // 不再拍平成固定的内部 code,UI 据 actionHref 是否存在决定显示跳转链接。 error: FriendlyError | null; generate: (projectId: string, volume: number) => Promise; } // 生成大纲:POST .../outline(body {volume})。无凭据/网关失败 → 503 LLM_UNAVAILABLE // (流前 JSON 信封),经 error 检出、引导去设置(同 draft/review 范式)。 export function useOutline(initial: OutlineChapterView[]): UseOutline { const [chapters, setChapters] = useState(initial); const [status, setStatus] = useState( initial.length > 0 ? "ready" : "idle", ); const [error, setError] = useState(null); const toast = useToast(); const generate = useCallback( async (projectId, volume) => { setStatus("generating"); setError(null); const { data, error: apiError } = await api.POST( "/projects/{project_id}/outline", { params: { path: { project_id: projectId } }, body: { volume }, }, ); if (apiError || !data) { // surface 真实错因(如 LLM_UNAVAILABLE→去设置 provider;422→校验文案), // 而非永远的通用「大纲生成失败」(QA #2:原 env.error.code 漏读 422 的 detail 信封)。 // 直接透传 friendly(保留 actionHref/actionLabel),不再拍平成固定 OUTLINE_FAILED。 const friendly = friendlyFromApiError(apiError); setError(friendly); setStatus("error"); toast(friendly.text, "error"); return; } // 只替换目标卷的条目,保留其它卷(「AI 排大纲」是单卷操作,整列覆盖会误删别卷)。 const generated = data.chapters ?? []; setChapters((prev) => [ ...prev.filter((ch) => ch.volume !== volume), ...generated, ]); setStatus("ready"); toast("大纲已生成", "success"); }, [toast], ); return { chapters, status, error, generate }; }