refactor(frontend): 四生成 hook 结束后额外返回产物(AC-3 留痕前置)
useRefine.refine/recommunicate→RefineVersion|null、useContinue.generate→string|null、 useChapterRewrite.send→RewriteVersion|null、useGenerator.generate→GeneratorArtifact|null。 向后兼容(既有调用忽略返回值不受影响);各 hook 成功用例补返回值断言。 面板据此 if(artifact) 才 append,空候选/错误路径不留痕。
This commit is contained in:
@@ -55,8 +55,9 @@ describe("useGenerator", () => {
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGenerator());
|
||||
let returned: { outputKind: string; rawPreview: unknown } | null = null;
|
||||
await act(async () => {
|
||||
await result.current.generate("p1", "idea", { brief: "脑洞" });
|
||||
returned = await result.current.generate("p1", "idea", { brief: "脑洞" });
|
||||
});
|
||||
|
||||
expect(result.current.genStatus).toBe("preview");
|
||||
@@ -65,6 +66,9 @@ describe("useGenerator", () => {
|
||||
expect(result.current.preview?.kind).toBe("IdeaListResult");
|
||||
expect(result.current.preview?.items[0]?.heading).toBe("废柴逆袭");
|
||||
expect(toast).not.toHaveBeenCalled();
|
||||
// 追加断言:generate 返回本次产物(供 GeneratorRunner 结束后留痕)。
|
||||
expect(returned?.outputKind).toBe("IdeaListResult");
|
||||
expect(returned?.rawPreview).toEqual(preview);
|
||||
});
|
||||
|
||||
it("生成后端返回 error:genStatus=error 且弹错误 toast", async () => {
|
||||
|
||||
@@ -16,6 +16,13 @@ import type { ToolGenerateRequest } from "@/lib/api/types";
|
||||
export type GenStatus = "idle" | "generating" | "preview" | "error";
|
||||
export type IngestStatus = "idle" | "ingesting" | "conflict" | "done" | "error";
|
||||
|
||||
// 一次生成的产物(供调用方在结束后留痕;与 hook state 同源)。
|
||||
export interface GeneratorArtifact {
|
||||
preview: PreviewModel;
|
||||
rawPreview: Record<string, unknown> | undefined;
|
||||
outputKind: string;
|
||||
}
|
||||
|
||||
export interface UseGenerator {
|
||||
genStatus: GenStatus;
|
||||
preview: PreviewModel | null;
|
||||
@@ -30,7 +37,7 @@ export interface UseGenerator {
|
||||
projectId: string,
|
||||
toolKey: string,
|
||||
body: ToolGenerateRequest,
|
||||
) => Promise<void>;
|
||||
) => Promise<GeneratorArtifact | null>;
|
||||
// 入库选中行;acknowledge 用于过 409(已查看冲突后重发)。
|
||||
ingest: (
|
||||
projectId: string,
|
||||
@@ -74,15 +81,22 @@ export function useGenerator(): UseGenerator {
|
||||
if (error || !data) {
|
||||
setGenStatus("error");
|
||||
toast(generationErrorMessage(error), "error");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
setPreview(mapPreview(data.output_kind, data.preview));
|
||||
const mapped = mapPreview(data.output_kind, data.preview);
|
||||
setPreview(mapped);
|
||||
setRawPreview(data.preview);
|
||||
setOutputKind(data.output_kind);
|
||||
setGenStatus("preview");
|
||||
return {
|
||||
preview: mapped,
|
||||
rawPreview: data.preview,
|
||||
outputKind: data.output_kind,
|
||||
};
|
||||
} catch {
|
||||
setGenStatus("error");
|
||||
toast("生成请求异常,请检查网络。", "error");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[toast],
|
||||
|
||||
@@ -51,8 +51,9 @@ describe("useChapterRewrite", () => {
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useChapterRewrite());
|
||||
let returned: unknown;
|
||||
await act(async () => {
|
||||
await result.current.send("p1", 3, "节奏太慢", "旧草稿");
|
||||
returned = await result.current.send("p1", 3, "节奏太慢", "旧草稿");
|
||||
});
|
||||
|
||||
// POST body 带 feedback + prior_draft(snake_case)。
|
||||
@@ -67,6 +68,8 @@ describe("useChapterRewrite", () => {
|
||||
{ feedback: "节奏太慢", text: "新版正文" },
|
||||
]);
|
||||
expect(result.current.latest?.text).toBe("新版正文");
|
||||
// 追加断言:send 返回本版(供 ChapterRewritePanel 结束后留痕)。
|
||||
expect(returned).toEqual({ feedback: "节奏太慢", text: "新版正文" });
|
||||
});
|
||||
|
||||
it("多轮迭代:第二轮再压一版,latest 为最新版", async () => {
|
||||
|
||||
@@ -51,13 +51,13 @@ export interface UseChapterRewrite {
|
||||
// 已完成的版本栈(每轮一版)。
|
||||
versions: RewriteVersion[];
|
||||
latest: RewriteVersion | null;
|
||||
// 发起一轮整章重写:作者意见 + 当前整章草稿 → 流式重写一版。
|
||||
// 发起一轮整章重写:作者意见 + 当前整章草稿 → 流式重写一版。返回本版(done 且有正文),否则 null。
|
||||
send: (
|
||||
projectId: string,
|
||||
chapterNo: number,
|
||||
feedback: string,
|
||||
priorDraft: string,
|
||||
) => Promise<void>;
|
||||
) => Promise<RewriteVersion | null>;
|
||||
stop: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export function useChapterRewrite(): UseChapterRewrite {
|
||||
// 非 JSON 信封,沿用默认文案。
|
||||
}
|
||||
dispatch({ type: "fail", code, message });
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
@@ -137,13 +137,16 @@ export function useChapterRewrite(): UseChapterRewrite {
|
||||
if (done && textRef.current.length > 0) {
|
||||
const version: RewriteVersion = { feedback, text: textRef.current };
|
||||
setVersions((prev) => [...prev, version]);
|
||||
return version;
|
||||
}
|
||||
return null;
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
return; // 用户主动停止:state 已置 aborted。
|
||||
return null; // 用户主动停止:state 已置 aborted。
|
||||
}
|
||||
const message = err instanceof Error ? err.message : "未知网络错误";
|
||||
dispatch({ type: "fail", code: "NETWORK", message });
|
||||
return null;
|
||||
} finally {
|
||||
controllerRef.current = null;
|
||||
}
|
||||
|
||||
@@ -33,8 +33,9 @@ describe("useContinue", () => {
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useContinue());
|
||||
let returned: unknown;
|
||||
await act(async () => {
|
||||
await result.current.generate("p1", 3);
|
||||
returned = await result.current.generate("p1", 3);
|
||||
});
|
||||
|
||||
expect(post).toHaveBeenCalledWith(GEN_PATH, {
|
||||
@@ -43,6 +44,8 @@ describe("useContinue", () => {
|
||||
});
|
||||
expect(result.current.status).toBe("ready");
|
||||
expect(result.current.candidates).toEqual(["续写正文一"]);
|
||||
// 追加断言:generate 返回本条候选文本(供 ContinuePanel 留痕)。
|
||||
expect(returned).toBe("续写正文一");
|
||||
});
|
||||
|
||||
it("多候选:再次生成累加候选,不覆盖前一条", async () => {
|
||||
|
||||
@@ -13,8 +13,8 @@ export type ContinueStatus = "idle" | "generating" | "ready" | "error";
|
||||
export interface UseContinue {
|
||||
status: ContinueStatus;
|
||||
candidates: string[];
|
||||
// 生成一条续写候选(累加,不覆盖);chapterNo 指定承接哪一章。
|
||||
generate: (projectId: string, chapterNo: number) => Promise<void>;
|
||||
// 生成一条续写候选(累加,不覆盖);chapterNo 指定承接哪一章。返回本条候选文本,空/失败返回 null。
|
||||
generate: (projectId: string, chapterNo: number) => Promise<string | null>;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -45,19 +45,21 @@ export function useContinue(): UseContinue {
|
||||
if (error || !data) {
|
||||
setStatus("error");
|
||||
toast(generationErrorMessage(error), "error");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const text = extractText(data.preview).trim();
|
||||
if (text.length === 0) {
|
||||
setStatus("ready");
|
||||
toast("本次未生成有效续写,请再试一次。", "info");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
setCandidates((prev) => [...prev, text]);
|
||||
setStatus("ready");
|
||||
return text;
|
||||
} catch {
|
||||
setStatus("error");
|
||||
toast("续写请求异常,请检查网络。", "error");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[toast],
|
||||
|
||||
@@ -36,8 +36,9 @@ describe("useRefine", () => {
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRefine());
|
||||
let returned: unknown;
|
||||
await act(async () => {
|
||||
await result.current.refine("p1", 3, "原文段");
|
||||
returned = await result.current.refine("p1", 3, "原文段");
|
||||
});
|
||||
|
||||
expect(post).toHaveBeenCalledWith(REFINE_PATH, {
|
||||
@@ -52,6 +53,12 @@ describe("useRefine", () => {
|
||||
refined: "润色后的段落",
|
||||
});
|
||||
expect(result.current.versions).toHaveLength(1);
|
||||
// 追加断言:refine 返回本版产物(供 RefinePanel 结束后留痕)。
|
||||
expect(returned).toEqual({
|
||||
segment: "原文段",
|
||||
instruction: "",
|
||||
refined: "润色后的段落",
|
||||
});
|
||||
});
|
||||
|
||||
it("润色带指令:instruction 透传后端", async () => {
|
||||
|
||||
@@ -26,18 +26,19 @@ export interface UseRefine {
|
||||
latest: RefineVersion | null;
|
||||
// 最初选中的原文(accept 回填的对比基线)。
|
||||
original: string | null;
|
||||
// 回炉并返回本版产出(供调用方在结束后留痕);失败返回 null。
|
||||
refine: (
|
||||
projectId: string,
|
||||
chapterNo: number,
|
||||
segment: string,
|
||||
instruction?: string,
|
||||
) => Promise<void>;
|
||||
// 以上一版产出为输入 + 作者新意见再回炉一次。无历史版本时为空操作。
|
||||
) => Promise<RefineVersion | null>;
|
||||
// 以上一版产出为输入 + 作者新意见再回炉一次。无历史版本时为空操作(返回 null)。
|
||||
recommunicate: (
|
||||
projectId: string,
|
||||
chapterNo: number,
|
||||
instruction: string,
|
||||
) => Promise<void>;
|
||||
) => Promise<RefineVersion | null>;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -52,7 +53,7 @@ export function useRefine(): UseRefine {
|
||||
chapterNo: number,
|
||||
segment: string,
|
||||
instruction: string,
|
||||
): Promise<void> => {
|
||||
): Promise<RefineVersion | null> => {
|
||||
setStatus("refining");
|
||||
try {
|
||||
const trimmed = instruction.trim();
|
||||
@@ -66,16 +67,20 @@ export function useRefine(): UseRefine {
|
||||
if (error || !data) {
|
||||
setStatus("error");
|
||||
toast("回炉失败,请重试。", "error");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
setVersions((prev) => [
|
||||
...prev,
|
||||
{ segment, instruction: trimmed, refined: data.refined },
|
||||
]);
|
||||
const version: RefineVersion = {
|
||||
segment,
|
||||
instruction: trimmed,
|
||||
refined: data.refined,
|
||||
};
|
||||
setVersions((prev) => [...prev, version]);
|
||||
setStatus("ready");
|
||||
return version;
|
||||
} catch {
|
||||
setStatus("error");
|
||||
toast("回炉请求异常,请检查网络。", "error");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[toast],
|
||||
@@ -91,7 +96,7 @@ export function useRefine(): UseRefine {
|
||||
(projectId, chapterNo, instruction) => {
|
||||
const last = versions[versions.length - 1];
|
||||
// 无历史版本 → 无从「再沟通」;空操作,不打后端。
|
||||
if (!last) return Promise.resolve();
|
||||
if (!last) return Promise.resolve(null);
|
||||
return runRefine(projectId, chapterNo, last.refined, instruction);
|
||||
},
|
||||
[runRefine, versions],
|
||||
|
||||
Reference in New Issue
Block a user