"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "@/lib/api/client"; import { currentOverride, withExcluded, withPinToggled, withRecentN, withRestored, type InjectionEntityRef, type InjectionOverrideRequest, type InjectionResponse, } from "./injection"; export interface UseInjection { data: InjectionResponse | null; loading: boolean; saving: boolean; // 首次加载失败(满屏错误框)与每次保存失败(悬浮提示)独立追踪,互不覆盖。 loadError: string | null; saveError: string | null; clearSaveError: () => void; reload: () => void; retrySave: () => Promise; togglePin: (ref: InjectionEntityRef) => Promise; exclude: (ref: InjectionEntityRef) => Promise; restore: (ref: InjectionEntityRef) => Promise; setRecentN: (n: number) => Promise; } const INJECTION_PATH = "/projects/{project_id}/chapters/{chapter_no}/injection" as const; // 本章注入透明(B0 可控版):读确定性 SelectionTrace + 作者覆盖; // pin/排除/recent_n 经 PUT 覆盖 → 服务端回放新的确定结果(看到的=写章用的)。 // 写失败给可读文案、不改本地状态(天然回滚),不抛。 export function useInjection(projectId: string, chapterNo: number): UseInjection { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [loadError, setLoadError] = useState(null); const [saveError, setSaveError] = useState(null); // 重载计数:loadError「重试」自增 → 触发 effect 重新拉取(不需要把 loader 提到 effect 外)。 const [reloadNonce, setReloadNonce] = useState(0); // 并发锁:保存在途时 ref 同步置位,拦下连点/快速切换造成的乱序覆盖(state 异步不可靠)。 const savingRef = useRef(false); // 记下最近一次提交的覆盖,供 saveError「重试」重放(不重复构造 override)。 const lastOverrideRef = useRef(null); useEffect(() => { let cancelled = false; setLoading(true); setLoadError(null); setSaveError(null); void (async () => { try { const { data: body, error: err } = await api.GET(INJECTION_PATH, { params: { path: { project_id: projectId, chapter_no: chapterNo } }, }); if (cancelled) return; if (err || !body) { setLoadError("注入信息暂不可用"); setData(null); } else { setData(body); } } catch { // 网络层失败(后端不可达/CORS):openapi-fetch 直接抛而非返回 error 信封。 if (cancelled) return; setLoadError("注入信息暂不可用"); setData(null); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [projectId, chapterNo, reloadNonce]); const save = useCallback( async (override: InjectionOverrideRequest) => { // 已有保存在途 → 早返回(避免并发 PUT 互相覆盖、回放结果乱序)。 if (savingRef.current) return; lastOverrideRef.current = override; savingRef.current = true; setSaving(true); setSaveError(null); try { const { data: body, error: err } = await api.PUT(INJECTION_PATH, { params: { path: { project_id: projectId, chapter_no: chapterNo } }, body: override, }); if (err || !body) { // 保存失败:保留已加载的 data,仅设保存态错误(天然回滚 + 可重试)。 setSaveError("保存注入设置失败,请重试"); } else { setData(body); // 以服务端确定结果为准(不变量 #6)。 } } catch { // 网络层失败:本地状态不动(天然回滚),给可读文案、不抛。 setSaveError("保存注入设置失败,请重试"); } finally { savingRef.current = false; setSaving(false); } }, [projectId, chapterNo], ); const mutate = useCallback( (fn: (o: InjectionOverrideRequest) => InjectionOverrideRequest) => async (): Promise => { if (!data) return; await save(fn(currentOverride(data))); }, [data, save], ); const togglePin = useCallback( (ref: InjectionEntityRef) => mutate((o) => withPinToggled(o, ref))(), [mutate], ); const exclude = useCallback( (ref: InjectionEntityRef) => mutate((o) => withExcluded(o, ref))(), [mutate], ); const restore = useCallback( (ref: InjectionEntityRef) => mutate((o) => withRestored(o, ref))(), [mutate], ); const setRecentN = useCallback( (n: number) => mutate((o) => withRecentN(o, n))(), [mutate], ); const clearSaveError = useCallback(() => setSaveError(null), []); const reload = useCallback(() => setReloadNonce((n) => n + 1), []); const retrySave = useCallback(async () => { const override = lastOverrideRef.current; if (!override) return; await save(override); }, [save]); return { data, loading, saving, loadError, saveError, clearSaveError, reload, retrySave, togglePin, exclude, restore, setRecentN, }; }