"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; error: string | null; 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 [error, setError] = useState(null); // 并发锁:保存在途时 ref 同步置位,拦下连点/快速切换造成的乱序覆盖(state 异步不可靠)。 const savingRef = useRef(false); useEffect(() => { let cancelled = false; setLoading(true); setError(null); void (async () => { const { data: body, error: err } = await api.GET(INJECTION_PATH, { params: { path: { project_id: projectId, chapter_no: chapterNo } }, }); if (cancelled) return; if (err || !body) { setError("注入信息暂不可用"); setData(null); } else { setData(body); } setLoading(false); })(); return () => { cancelled = true; }; }, [projectId, chapterNo]); const save = useCallback( async (override: InjectionOverrideRequest) => { // 已有保存在途 → 早返回(避免并发 PUT 互相覆盖、回放结果乱序)。 if (savingRef.current) return; savingRef.current = true; setSaving(true); setError(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) { setError("保存注入设置失败,请重试"); } else { setData(body); // 以服务端确定结果为准(不变量 #6)。 } } 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], ); return { data, loading, saving, error, togglePin, exclude, restore, setRecentN }; }