- 续审 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)
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
"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(呈现「本次将更新」清单);
|
||
// 失败回滚状态 + toast;409 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 };
|
||
}
|