Files
writer-work-flow/apps/web/lib/review/useAccept.ts
Yaojia Wang d80d41a08d refactor(web): ApiErrorEnvelope 改由生成 schema 派生,防契约漂移
手写 ApiErrorEnvelope 改由 components["schemas"]["ErrorEnvelope"] 派生;accept 端点专属 details.missing_conflict_indices 就地收窄。运行时行为不变(既有 useAccept 测试全绿)。
2026-07-08 13:27:32 +02:00

82 lines
3.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<AcceptOutcome>;
}
// 错误信封形状从后端 OpenAPI 生成的 schema 派生,避免与契约漂移(不再手抄字段)。
type ApiErrorEnvelope = components["schemas"]["ErrorEnvelope"];
// accept 端点 409 CONFLICT_UNRESOLVED 在 details 里携带缺判下标;
// 生成 schema 的 details 为松散 `{[k]: unknown}`,此处就地收窄该端点专属字段。
interface ConflictErrorDetails {
missing_conflict_indices?: number[];
}
// 验收:乐观置 accepting → 成功置 accepted呈现「本次将更新」清单
// 失败回滚状态 + toast409 CONFLICT_UNRESOLVED 解析缺判下标供高亮。
export function useAccept(): UseAccept {
const [status, setStatus] = useState<AcceptStatus>("idle");
const [result, setResult] = useState<AcceptResponse | null>(null);
const toast = useToast();
const accept = useCallback<UseAccept["accept"]>(
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 };
}