Files
writer-work-flow/apps/web/lib/toolbox/useGenerator.ts
Yaojia Wang 59f2f60a7e 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,空候选/错误路径不留痕。
2026-07-09 17:15:49 +02:00

177 lines
5.6 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 {
extractIngestConflicts,
generationErrorMessage,
type IngestConflicts,
} from "@/lib/generation/cards";
import { mapPreview, type PreviewModel } from "./toolbox";
import { buildIngestRequest, type IngestBuildInput } from "./ingest";
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;
// 原始结构化产物(供入库按选中下标形变;与 preview.items 同源对齐)。
rawPreview: Record<string, unknown> | undefined;
outputKind: string | null;
ingestStatus: IngestStatus;
// 409 冲突详情(待作者裁决);裁决后带 acknowledge 重发。
conflicts: IngestConflicts | null;
created: string[];
generate: (
projectId: string,
toolKey: string,
body: ToolGenerateRequest,
) => Promise<GeneratorArtifact | null>;
// 入库选中行acknowledge 用于过 409已查看冲突后重发
ingest: (
projectId: string,
toolKey: string,
input: IngestBuildInput,
) => Promise<boolean>;
reset: () => void;
}
// 通用生成器状态机idle→generating→preview+ 入库 409 裁决流,仿 useCharacterGen。
// 声明驱动generate/ingest 不感知具体工具,按 tool_key + descriptor 形变后调通用端点。
export function useGenerator(): UseGenerator {
const [genStatus, setGenStatus] = useState<GenStatus>("idle");
const [preview, setPreview] = useState<PreviewModel | null>(null);
const [rawPreview, setRawPreview] = useState<
Record<string, unknown> | undefined
>(undefined);
const [outputKind, setOutputKind] = useState<string | null>(null);
const [ingestStatus, setIngestStatus] = useState<IngestStatus>("idle");
const [conflicts, setConflicts] = useState<IngestConflicts | null>(null);
const [created, setCreated] = useState<string[]>([]);
const toast = useToast();
const generate = useCallback<UseGenerator["generate"]>(
async (projectId, toolKey, body) => {
setGenStatus("generating");
setPreview(null);
setRawPreview(undefined);
setOutputKind(null);
setConflicts(null);
setIngestStatus("idle");
setCreated([]);
try {
const { data, error } = await api.POST(
"/projects/{project_id}/skills/{tool_key}/generate",
{
params: { path: { project_id: projectId, tool_key: toolKey } },
body,
},
);
if (error || !data) {
setGenStatus("error");
toast(generationErrorMessage(error), "error");
return null;
}
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],
);
const ingest = useCallback<UseGenerator["ingest"]>(
async (projectId, toolKey, input) => {
const body = buildIngestRequest(input);
if (!body) {
toast("该生成器不支持入库。", "error");
return false;
}
if (input.rows.length === 0) {
toast("请至少选择一项入库。", "error");
return false;
}
setIngestStatus("ingesting");
try {
const { data, error } = await api.POST(
"/projects/{project_id}/skills/{tool_key}/ingest",
{
params: { path: { project_id: projectId, tool_key: toolKey } },
body,
},
);
if (error || !data) {
const conflict = extractIngestConflicts(error);
if (conflict) {
// 409 CONFLICT_UNRESOLVED展示冲突让作者裁决再带 acknowledge 重发。
setConflicts(conflict);
setIngestStatus("conflict");
return false;
}
setIngestStatus("error");
toast(generationErrorMessage(error), "error");
return false;
}
setCreated(data.created ?? []);
setConflicts(null);
setIngestStatus("done");
toast(`已入库 ${data.created?.length ?? 0} 项至 ${data.table}`, "success");
if (data.rejected_tables && data.rejected_tables.length > 0) {
toast(`越权写表被丢弃:${data.rejected_tables.join("、")}`, "info");
}
return true;
} catch {
setIngestStatus("error");
toast("入库请求异常,请检查网络。", "error");
return false;
}
},
[toast],
);
const reset = useCallback((): void => {
setGenStatus("idle");
setPreview(null);
setRawPreview(undefined);
setOutputKind(null);
setIngestStatus("idle");
setConflicts(null);
setCreated([]);
}, []);
return {
genStatus,
preview,
rawPreview,
outputKind,
ingestStatus,
conflicts,
created,
generate,
ingest,
reset,
};
}