"use client"; import { useCallback, useState } from "react"; import { api } from "@/lib/api/client"; import { useToast } from "@/components/Toast"; import type { components } from "@/lib/api/schema"; import type { AcceptResponse } from "@/lib/api/types"; import { buildAcceptRequest, type DecisionDraft } from "./decisions"; export type AcceptStatus = "idle" | "accepting" | "accepted" | "error"; export interface AcceptOutcome { result: AcceptResponse | null; // 409 CONFLICT_UNRESOLVED 缺判下标(高亮报告卡)。 missingIndices: number[]; // 后端验收闸报「有未裁决冲突」——即便本页当前未显示冲突(快照过期/未审稿)。 // 调用方据此回拉持久化最新审稿、回灌冲突供裁决(修「显示 0 冲突却被拦」)。 conflictUnresolved: boolean; } export interface UseAccept { status: AcceptStatus; result: AcceptResponse | null; accept: ( projectId: string, chapterNo: number, finalText: string, drafts: readonly DecisionDraft[], ) => Promise; } // 错误信封形状从后端 OpenAPI 生成的 schema 派生,避免与契约漂移(不再手抄字段)。 type ApiErrorEnvelope = components["schemas"]["ErrorEnvelope"]; // accept 端点 409 CONFLICT_UNRESOLVED 在 details 里携带缺判下标; // 生成 schema 的 details 为松散 `{[k]: unknown}`,此处就地收窄该端点专属字段。 interface ConflictErrorDetails { missing_conflict_indices?: number[]; } // 验收:乐观置 accepting → 成功置 accepted(呈现「本次将更新」清单); // 失败回滚状态 + toast;409 CONFLICT_UNRESOLVED 解析缺判下标供高亮。 export function useAccept(): UseAccept { const [status, setStatus] = useState("idle"); const [result, setResult] = useState(null); const toast = useToast(); const accept = useCallback( async (projectId, chapterNo, finalText, drafts) => { setStatus("accepting"); const body = buildAcceptRequest(finalText, drafts); const { data, error } = await api.POST( "/projects/{project_id}/chapters/{chapter_no}/accept", { params: { path: { project_id: projectId, chapter_no: chapterNo } }, body, }, ); if (error || !data) { setStatus("error"); const env = error as ApiErrorEnvelope | undefined; const code = env?.error?.code; const details = env?.error?.details as ConflictErrorDetails | undefined; const missing = details?.missing_conflict_indices ?? []; if (code === "CONFLICT_UNRESOLVED") { return { result: null, missingIndices: missing, conflictUnresolved: true }; } toast("验收失败,请重试(正文未丢失)", "error"); return { result: null, missingIndices: [], conflictUnresolved: false }; } setResult(data); setStatus("accepted"); toast("本章已验收", "success"); return { result: data, missingIndices: [], conflictUnresolved: false }; }, [toast], ); return { status, result, accept }; }