新增一键流式优化接口
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { requireApiAccess } from "../../../../lib/api/auth";
|
||||
import { getRepositoryFromRuntime } from "../../../../lib/db/repository";
|
||||
import { optimizationFactCardSchema } from "../../../../lib/domain/validation";
|
||||
import { LlmValidationError } from "../../../../lib/llm/client";
|
||||
import { getExportStoreFromRuntime } from "../../../../lib/workflow/export-store";
|
||||
import { extractCandidateFactCard } from "../../../../lib/workflow/fact-extractor";
|
||||
import {
|
||||
normalizeInput,
|
||||
type RawArticleInput,
|
||||
} from "../../../../lib/workflow/input-normalizer";
|
||||
import {
|
||||
encodeOptimizationStreamEvent,
|
||||
type OptimizationStreamEvent,
|
||||
type OptimizationStreamStage,
|
||||
} from "../../../../lib/workflow/stream-events";
|
||||
import { runStreamingOptimizationWorkflow } from "../../../../lib/workflow/streaming-optimizer";
|
||||
|
||||
interface OptimizeStreamPayload extends RawArticleInput {
|
||||
fact_card?: unknown;
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const access = requireApiAccess(request);
|
||||
if (!access.ok) {
|
||||
return access.response;
|
||||
}
|
||||
|
||||
let payload: OptimizeStreamPayload;
|
||||
try {
|
||||
payload = (await request.json()) as OptimizeStreamPayload;
|
||||
} catch {
|
||||
return NextResponse.json({ error: "请求体不是合法 JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!hasOptimizableBody(payload)) {
|
||||
return NextResponse.json(
|
||||
{ error: "请输入需要优化的文章内容" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
let normalized: ReturnType<typeof normalizeInput>;
|
||||
try {
|
||||
normalized = normalizeInput(payload);
|
||||
} catch (error) {
|
||||
return jsonError(error, getErrorStatus(error));
|
||||
}
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
const send = (event: OptimizationStreamEvent) => {
|
||||
controller.enqueue(encoder.encode(encodeOptimizationStreamEvent(event)));
|
||||
};
|
||||
|
||||
let jobId: string | undefined;
|
||||
let stage: OptimizationStreamStage = "job";
|
||||
|
||||
try {
|
||||
const repository = getRepositoryFromRuntime();
|
||||
const job = await repository.createArticleJob({
|
||||
source_title: normalized.articleInput.title,
|
||||
source_body: normalized.articleInput.body,
|
||||
image_inputs: normalized.articleInput.images,
|
||||
publish_platform: normalized.articleInput.platform,
|
||||
user_instructions: normalized.articleInput.user_instructions,
|
||||
});
|
||||
jobId = job.id;
|
||||
send({ type: "job_created", job: { id: job.id } });
|
||||
|
||||
stage = "fact_card";
|
||||
const factCard = optimizationFactCardSchema.parse(
|
||||
payload.fact_card ??
|
||||
(await extractCandidateFactCard(normalized.articleInput)),
|
||||
);
|
||||
const savedFactCard = await repository.saveFactCard(job.id, factCard);
|
||||
send({
|
||||
type: "fact_card_ready",
|
||||
job_id: job.id,
|
||||
fact_card: savedFactCard,
|
||||
});
|
||||
|
||||
const result = await runStreamingOptimizationWorkflow({
|
||||
jobId: job.id,
|
||||
input: normalized.articleInput,
|
||||
factCard: savedFactCard,
|
||||
onEvent: (event) => {
|
||||
stage = stageForEvent(event, stage);
|
||||
send(event);
|
||||
},
|
||||
});
|
||||
|
||||
stage = "final";
|
||||
const optimizedArticle = await repository.saveOptimizedArticle(
|
||||
job.id,
|
||||
result.article,
|
||||
);
|
||||
const qaReport = await repository.saveQaReport(
|
||||
job.id,
|
||||
optimizedArticle.revision ?? 1,
|
||||
result.qaReport,
|
||||
);
|
||||
const exportStore = getExportStoreFromRuntime();
|
||||
const exportPaths = await exportStore.writeJobExports({
|
||||
jobId: job.id,
|
||||
article: optimizedArticle,
|
||||
qaReport,
|
||||
});
|
||||
await repository.updateArticleJob(job.id, {
|
||||
status: "optimized",
|
||||
export_paths: exportPaths,
|
||||
});
|
||||
send({
|
||||
type: "final_ready",
|
||||
job_id: job.id,
|
||||
optimized_article: optimizedArticle,
|
||||
qa_report: qaReport,
|
||||
export_paths: exportPaths,
|
||||
});
|
||||
} catch (error) {
|
||||
send({
|
||||
type: "failed",
|
||||
job_id: jobId,
|
||||
stage,
|
||||
error: error instanceof Error ? error.message : "优化失败",
|
||||
});
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"content-type": "application/x-ndjson; charset=utf-8",
|
||||
"cache-control": "no-cache, no-transform",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function hasOptimizableBody(payload: unknown): payload is OptimizeStreamPayload {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = (payload as Partial<OptimizeStreamPayload>).body;
|
||||
return typeof body === "string" && body.trim().length > 0;
|
||||
}
|
||||
|
||||
function stageForEvent(
|
||||
event: OptimizationStreamEvent,
|
||||
fallback: OptimizationStreamStage,
|
||||
): OptimizationStreamStage {
|
||||
switch (event.type) {
|
||||
case "draft_started":
|
||||
case "draft_ready":
|
||||
return "draft";
|
||||
case "qa_started":
|
||||
case "qa_ready":
|
||||
return "qa";
|
||||
case "rewrite_started":
|
||||
case "rewrite_ready":
|
||||
return "rewrite";
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function jsonError(error: unknown, status: number) {
|
||||
const message = error instanceof Error ? error.message : "Request failed";
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
|
||||
function getErrorStatus(error: unknown) {
|
||||
if (error instanceof LlmValidationError) return 502;
|
||||
if (error instanceof Error && /^LLM\b|provider/i.test(error.message)) return 502;
|
||||
return 400;
|
||||
}
|
||||
Reference in New Issue
Block a user