新增优化案例API

This commit is contained in:
czj
2026-07-08 13:48:03 +08:00
parent e867f4f07e
commit f09fd5afe8
8 changed files with 874 additions and 7 deletions
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import { requireApiAccess } from "../../../../../lib/api/auth";
import { getRepositoryFromRuntime } from "../../../../../lib/db/repository";
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 optimizationCase = await repository.archiveOptimizationCase(caseId);
if (!optimizationCase) {
return NextResponse.json({ error: "Case not found" }, { status: 404 });
}
return NextResponse.json({ case: optimizationCase });
}
+298
View File
@@ -0,0 +1,298 @@
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) },
);
}
}
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import { requireApiAccess } from "../../../../../lib/api/auth";
import { getRepositoryFromRuntime } from "../../../../../lib/db/repository";
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 optimizationCase = await repository.restoreOptimizationCase(caseId);
if (!optimizationCase) {
return NextResponse.json({ error: "Case not found" }, { status: 404 });
}
return NextResponse.json({ case: optimizationCase });
}
+45
View File
@@ -0,0 +1,45 @@
import { NextResponse } from "next/server";
import { requireApiAccess } from "../../../../lib/api/auth";
import { caseMetadataPatchSchema } from "../../../../lib/cases/validation";
import { getRepositoryFromRuntime } from "../../../../lib/db/repository";
interface RouteContext {
params: Promise<{ caseId: string }>;
}
export async function GET(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) {
return NextResponse.json({ error: "Case not found" }, { status: 404 });
}
return NextResponse.json(detail);
}
export async function PATCH(request: Request, context: RouteContext) {
const access = requireApiAccess(request);
if (!access.ok) {
return access.response;
}
const { caseId } = await context.params;
const repository = getRepositoryFromRuntime();
const changes = caseMetadataPatchSchema.parse(await request.json());
const optimizationCase = await repository.updateOptimizationCaseMetadata(
caseId,
changes,
);
if (!optimizationCase) {
return NextResponse.json({ error: "Case not found" }, { status: 404 });
}
return NextResponse.json({ case: optimizationCase });
}
@@ -0,0 +1,63 @@
import { NextResponse } from "next/server";
import { requireApiAccess } from "../../../../../../../lib/api/auth";
import { resultVersionPublicationInputSchema } from "../../../../../../../lib/cases/validation";
import { getRepositoryFromRuntime } from "../../../../../../../lib/db/repository";
interface RouteContext {
params: Promise<{ caseId: string; versionId: string }>;
}
export async function GET(request: Request, context: RouteContext) {
const access = requireApiAccess(request);
if (!access.ok) {
return access.response;
}
const { caseId, versionId } = await context.params;
const repository = getRepositoryFromRuntime();
const version = await repository.getOptimizationResultVersion(versionId);
if (!version || version.case_id !== caseId) {
return NextResponse.json(
{ error: "Result version not found" },
{ status: 404 },
);
}
return NextResponse.json({
publications: await repository.listPublicationRecordsForResultVersion(
version.id,
),
});
}
export async function POST(request: Request, context: RouteContext) {
const access = requireApiAccess(request);
if (!access.ok) {
return access.response;
}
const { caseId, versionId } = await context.params;
const repository = getRepositoryFromRuntime();
const version = await repository.getOptimizationResultVersion(versionId);
if (!version || version.case_id !== caseId) {
return NextResponse.json(
{ error: "Result version not found" },
{ status: 404 },
);
}
const input = resultVersionPublicationInputSchema.parse(await request.json());
const publication = await repository.createPublicationRecord({
result_version_id: version.id,
job_id: version.article_job_id,
revision: version.article_revision,
publish_target: input.publish_target,
url: input.url,
published_at: input.published_at,
status: "published",
notes: input.notes,
});
return NextResponse.json({ publication }, { status: 201 });
}