305 lines
9.2 KiB
TypeScript
305 lines
9.2 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
|
|
import { requireApiAccess } from "../../../../../lib/api/auth";
|
|
import type {
|
|
ArticleCaseInputPayload,
|
|
OptimizationCaseDetail,
|
|
ProcessSummaryStep,
|
|
} from "../../../../../lib/cases/types";
|
|
import { createProcessStep, excerpt } from "../../../../../lib/cases/summaries";
|
|
import {
|
|
getRepositoryFromRuntime,
|
|
type AppRepository,
|
|
} from "../../../../../lib/db/repository";
|
|
import {
|
|
articleInputSchema,
|
|
copyOptimizationRequestSchema,
|
|
optimizationFactCardSchema,
|
|
} from "../../../../../lib/domain/validation";
|
|
import type { LlmAuditSummary } from "../../../../../lib/llm/audit";
|
|
import { LlmValidationError } from "../../../../../lib/llm/client";
|
|
import { getExportStoreFromRuntime } from "../../../../../lib/workflow/export-store";
|
|
import { extractCandidateFactCard } from "../../../../../lib/workflow/fact-extractor";
|
|
import { runOptimizationWorkflow } from "../../../../../lib/workflow/orchestrator";
|
|
import { optimizeRenweiCopy } from "../../../../../lib/workflow/renwei-copy-optimizer";
|
|
|
|
interface RouteContext {
|
|
params: Promise<{ caseId: string }>;
|
|
}
|
|
|
|
export async function POST(request: Request, context: RouteContext) {
|
|
const access = requireApiAccess(request);
|
|
if (!access.ok) {
|
|
return access.response;
|
|
}
|
|
|
|
const { caseId } = await context.params;
|
|
const repository = getRepositoryFromRuntime();
|
|
const detail = await repository.getOptimizationCaseDetail(caseId);
|
|
if (!detail?.input) {
|
|
return NextResponse.json({ error: "Case not found" }, { status: 404 });
|
|
}
|
|
const caseDetail = { ...detail, input: detail.input } satisfies CaseDetailWithInput;
|
|
|
|
if (detail.case.case_type === "article") {
|
|
return rerunArticleCase(repository, caseDetail);
|
|
}
|
|
|
|
if (detail.case.case_type === "human_copy") {
|
|
return rerunHumanCopyCase(repository, caseDetail);
|
|
}
|
|
|
|
return NextResponse.json({ error: "Unsupported case type" }, { status: 400 });
|
|
}
|
|
|
|
type CaseDetailWithInput = OptimizationCaseDetail & {
|
|
input: NonNullable<OptimizationCaseDetail["input"]>;
|
|
};
|
|
|
|
async function rerunHumanCopyCase(
|
|
repository: AppRepository,
|
|
detail: CaseDetailWithInput,
|
|
) {
|
|
const payload = copyOptimizationRequestSchema.parse(detail.input.payload);
|
|
const startedAt = Date.now();
|
|
const llmAuditSummary: LlmAuditSummary[] = [];
|
|
|
|
try {
|
|
const result = await optimizeRenweiCopy(payload, {
|
|
onAuditSummary: (summary) => {
|
|
llmAuditSummary.push(summary);
|
|
},
|
|
});
|
|
const resultVersion = await repository.createOptimizationResultVersion({
|
|
case_id: detail.case.id,
|
|
case_type: "human_copy",
|
|
status: "optimized",
|
|
article_job_id: null,
|
|
article_revision: null,
|
|
result_summary: excerpt(result.optimized_text),
|
|
payload: result,
|
|
process_summary: [
|
|
createProcessStep({
|
|
stage: "human_copy_optimize",
|
|
startedAt,
|
|
endedAt: Date.now(),
|
|
status: "success",
|
|
producedResultVersion: true,
|
|
}),
|
|
],
|
|
llm_audit_summary: llmAuditSummary,
|
|
error_stage: null,
|
|
error_summary: null,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
case: { id: detail.case.id, case_type: "human_copy" },
|
|
result_version: { id: resultVersion.id, version: resultVersion.version },
|
|
result,
|
|
},
|
|
{ status: 201 },
|
|
);
|
|
} catch (error) {
|
|
const message = errorMessage(error);
|
|
const resultVersion = await repository.createOptimizationResultVersion({
|
|
case_id: detail.case.id,
|
|
case_type: "human_copy",
|
|
status: "failed",
|
|
article_job_id: null,
|
|
article_revision: null,
|
|
result_summary: "",
|
|
payload: null,
|
|
process_summary: [
|
|
createProcessStep({
|
|
stage: "human_copy_optimize",
|
|
startedAt,
|
|
endedAt: Date.now(),
|
|
status: "failed",
|
|
errorSummary: message,
|
|
producedResultVersion: false,
|
|
}),
|
|
],
|
|
llm_audit_summary: llmAuditSummary,
|
|
error_stage: "human_copy_optimize",
|
|
error_summary: message,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: message,
|
|
case: { id: detail.case.id, case_type: "human_copy" },
|
|
result_version: { id: resultVersion.id, version: resultVersion.version },
|
|
},
|
|
{ status: getErrorStatus(error) },
|
|
);
|
|
}
|
|
}
|
|
|
|
function errorMessage(error: unknown) {
|
|
return error instanceof z.ZodError
|
|
? "请输入需要优化的文案"
|
|
: error instanceof Error
|
|
? error.message
|
|
: "文案优化失败";
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
async function rerunArticleCase(
|
|
repository: AppRepository,
|
|
detail: CaseDetailWithInput,
|
|
) {
|
|
const payload = detail.input.payload as ArticleCaseInputPayload;
|
|
const articleInput = articleInputSchema.parse({
|
|
title: payload.source_title,
|
|
body: payload.source_body,
|
|
images: payload.image_inputs,
|
|
platform: payload.publish_platform,
|
|
user_instructions: payload.user_instructions,
|
|
});
|
|
const llmAuditSummary: LlmAuditSummary[] = [];
|
|
const processSummary: ProcessSummaryStep[] = [];
|
|
const requestStartedAt = Date.now();
|
|
let jobId: string | null = null;
|
|
let stage = "job";
|
|
|
|
try {
|
|
const job = await repository.createArticleJob({
|
|
case_id: detail.case.id,
|
|
source_title: articleInput.title,
|
|
source_body: articleInput.body,
|
|
image_inputs: articleInput.images,
|
|
publish_platform: articleInput.platform,
|
|
user_instructions: articleInput.user_instructions,
|
|
});
|
|
jobId = job.id;
|
|
|
|
stage = "fact_card";
|
|
const factCardStartedAt = Date.now();
|
|
const existingFactCard = detail.input.article_job_id
|
|
? await repository.getFactCard(detail.input.article_job_id)
|
|
: null;
|
|
const factCard = optimizationFactCardSchema.parse(
|
|
payload.fact_card ??
|
|
existingFactCard ??
|
|
(await extractCandidateFactCard(articleInput, {
|
|
onAuditSummary: (summary) => {
|
|
llmAuditSummary.push(summary);
|
|
},
|
|
})),
|
|
);
|
|
processSummary.push(
|
|
createProcessStep({
|
|
stage: "fact_card",
|
|
startedAt: factCardStartedAt,
|
|
endedAt: Date.now(),
|
|
status: "success",
|
|
producedResultVersion: false,
|
|
}),
|
|
);
|
|
const savedFactCard = await repository.saveFactCard(job.id, factCard);
|
|
|
|
stage = "optimize";
|
|
const result = await runOptimizationWorkflow({
|
|
input: articleInput,
|
|
factCard: savedFactCard,
|
|
onAuditSummary: (summary) => {
|
|
llmAuditSummary.push(summary);
|
|
},
|
|
});
|
|
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,
|
|
});
|
|
const resultVersion = await repository.createOptimizationResultVersion({
|
|
case_id: detail.case.id,
|
|
case_type: "article",
|
|
status: "optimized",
|
|
article_job_id: job.id,
|
|
article_revision: optimizedArticle.revision ?? 1,
|
|
result_summary: optimizedArticle.summary,
|
|
payload: {
|
|
article: optimizedArticle,
|
|
qa_report: qaReport,
|
|
export_paths: exportPaths,
|
|
},
|
|
process_summary: processSummary,
|
|
llm_audit_summary: llmAuditSummary,
|
|
error_stage: null,
|
|
error_summary: null,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
case: { id: detail.case.id, case_type: "article" },
|
|
result_version: { id: resultVersion.id, version: resultVersion.version },
|
|
optimizedArticle,
|
|
qaReport,
|
|
exportPaths,
|
|
rewriteRounds: result.rewrite_rounds,
|
|
stoppedAfterMaxRewrites: result.stopped_after_max_rewrites,
|
|
timing: result.timing,
|
|
},
|
|
{ status: 201 },
|
|
);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "文章优化失败";
|
|
const resultVersion = await repository.createOptimizationResultVersion({
|
|
case_id: detail.case.id,
|
|
case_type: "article",
|
|
status: "failed",
|
|
article_job_id: jobId,
|
|
article_revision: null,
|
|
result_summary: "",
|
|
payload: null,
|
|
process_summary: [
|
|
...processSummary,
|
|
createProcessStep({
|
|
stage,
|
|
startedAt: requestStartedAt,
|
|
endedAt: Date.now(),
|
|
status: "failed",
|
|
errorSummary: message,
|
|
producedResultVersion: false,
|
|
}),
|
|
],
|
|
llm_audit_summary: llmAuditSummary,
|
|
error_stage: stage,
|
|
error_summary: message,
|
|
});
|
|
if (jobId) {
|
|
await repository.updateArticleJob(jobId, { status: "failed" });
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: message,
|
|
case: { id: detail.case.id, case_type: "article" },
|
|
result_version: { id: resultVersion.id, version: resultVersion.version },
|
|
},
|
|
{ status: getErrorStatus(error) },
|
|
);
|
|
}
|
|
}
|