"use client"; 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"; export interface OutlineError { code: string; message: string; } export interface UseOutline { chapters: OutlineChapterView[]; status: OutlineStatus; error: OutlineError | 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 信封)。 const friendly = friendlyFromApiError(apiError); setError({ code: "OUTLINE_FAILED", message: friendly.text }); setStatus("error"); toast(friendly.text, "error"); return; } setChapters(data.chapters ?? []); setStatus("ready"); toast("大纲已生成", "success"); }, [toast], ); return { chapters, status, error, generate }; }