Files
writer-work-flow/apps/web/lib/review/useAccept.ts
Yaojia Wang 68f194a043 feat: M2 — 写→审(一致性)→裁决→验收(事务);未决冲突禁验收
- 续审 Agent 声明(AgentSpec) + 结构化输出契约(ContinuityReview/Conflict 五类)
- LangGraph 并行审子图(可扩四审) + collect 落 chapter_reviews 留痕 + review SSE(section/conflict)
- 验收-side Repository:章节 accepted 版本晋升 + digest append-only + 审稿留痕/裁决
- API:review(SSE) + reviews 历史 + accept(单原子事务:晋升 version + 终稿 digest + 裁决留痕)
- 冲突 gate:未决裁决拦截(CONFLICT_UNRESOLVED);digest 从终稿提炼(不变量#4)
- 前端:审稿报告页 + 冲突就地标注 + 裁决(采纳/忽略/手改) + 未决禁验收 + 「本次将更新」清单
- M2 E2E:真实 DB + 多档位 mock 网关零 token 走通 写→审→裁决→验收→摘要入库
- 多 agent 协同台账(PROGRESS.md) + 共享记忆(memory/contracts·decisions·gotchas)
2026-06-18 11:38:28 +02:00

77 lines
2.4 KiB
TypeScript
Raw 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 { 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[];
}
export interface UseAccept {
status: AcceptStatus;
result: AcceptResponse | null;
accept: (
projectId: string,
chapterNo: number,
finalText: string,
drafts: readonly DecisionDraft[],
) => Promise<AcceptOutcome>;
}
interface ApiErrorEnvelope {
error?: {
code?: string;
message?: string;
details?: { missing_conflict_indices?: number[] } | null;
};
}
// 验收:乐观置 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 missing = env?.error?.details?.missing_conflict_indices ?? [];
if (code === "CONFLICT_UNRESOLVED") {
toast("尚有冲突未裁决,请先处理高亮项", "error");
return { result: null, missingIndices: missing };
}
toast("验收失败,请重试(正文未丢失)", "error");
return { result: null, missingIndices: [] };
}
setResult(data);
setStatus("accepted");
toast("本章已验收", "success");
return { result: data, missingIndices: [] };
},
[toast],
);
return { status, result, accept };
}