import { NextResponse } from "next/server"; import { requireApiAccess } from "../../../../lib/api/auth"; import type { ProcessSummaryStep } from "../../../../lib/cases/types"; import { buildArticleCaseSummary, createProcessStep } from "../../../../lib/cases/summaries"; import { getRepositoryFromRuntime } from "../../../../lib/db/repository"; import { optimizationFactCardSchema } from "../../../../lib/domain/validation"; import type { LlmAuditSummary } from "../../../../lib/llm/audit"; import { LlmValidationError } from "../../../../lib/llm/client"; import { getLlmTracePayloadStoreFromRuntime } from "../../../../lib/llm/trace-payload-store"; import { createLlmTraceRecorder, createNoopLlmTraceRecorder, safeTraceError, } from "../../../../lib/llm/trace-recorder"; import { getLlmTraceRepositoryFromRuntime } from "../../../../lib/llm/trace-repository"; 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; try { normalized = normalizeInput(payload); } catch (error) { return jsonError(error, getErrorStatus(error)); } const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); const send = (event: OptimizationStreamEvent) => { controller.enqueue(encoder.encode(encodeOptimizationStreamEvent(event))); }; let jobId: string | undefined; let caseId: string | undefined; let stage: OptimizationStreamStage = "job"; let traceRecorder = createNoopLlmTraceRecorder(); const llmAuditSummary: LlmAuditSummary[] = []; const processSummary: ProcessSummaryStep[] = []; const requestStartedAt = Date.now(); try { const repository = getRepositoryFromRuntime(); const caseSummary = buildArticleCaseSummary({ source_title: normalized.articleInput.title, source_body: normalized.articleInput.body, publish_platform: normalized.articleInput.platform, }); const optimizationCase = await repository.createOptimizationCase({ case_type: "article", ...caseSummary, }); caseId = optimizationCase.id; const job = await repository.createArticleJob({ case_id: optimizationCase.id, 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; await repository.saveCaseInput({ case_id: optimizationCase.id, case_type: "article", article_job_id: job.id, payload: { 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, }, }); send({ type: "job_created", job: { id: job.id }, case: { id: optimizationCase.id, case_type: "article" }, }); try { traceRecorder = await createLlmTraceRecorder({ jobId: job.id, caseId: optimizationCase.id, repository: getLlmTraceRepositoryFromRuntime(), payloadStore: getLlmTracePayloadStoreFromRuntime(), publish: (event) => send(event), }); } catch (error) { send({ type: "trace_warning", job_id: job.id, trace_completeness: "incomplete", error_summary: safeTraceError(error), }); } stage = "fact_card"; const factCardStartedAt = Date.now(); const factCard = optimizationFactCardSchema.parse( payload.fact_card ?? (await extractCandidateFactCard(normalized.articleInput, { onAuditSummary: (summary) => { llmAuditSummary.push(summary); }, onTraceEvent: traceRecorder.onLlmEvent, })), ); processSummary.push( createProcessStep({ stage: "fact_card", startedAt: factCardStartedAt, endedAt: Date.now(), status: "success", producedResultVersion: false, }), ); const savedFactCard = await repository.saveFactCard(job.id, factCard); const factCardReadyEvent: OptimizationStreamEvent = { type: "fact_card_ready", job_id: job.id, fact_card: savedFactCard, }; await traceRecorder.onWorkflowEvent(factCardReadyEvent); send(factCardReadyEvent); const result = await runStreamingOptimizationWorkflow({ jobId: job.id, input: normalized.articleInput, factCard: savedFactCard, onEvent: async (event) => { stage = stageForEvent(event, stage); await traceRecorder.onWorkflowEvent(event); send(event); }, onTraceEvent: traceRecorder.onLlmEvent, onAuditSummary: (summary) => { llmAuditSummary.push(summary); }, }); 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, }); const resultVersion = await repository.createOptimizationResultVersion({ case_id: optimizationCase.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, ...result.processSummary], llm_audit_summary: llmAuditSummary, error_stage: null, error_summary: null, }); const finalReadyEvent: OptimizationStreamEvent = { type: "final_ready", job_id: job.id, case: { id: optimizationCase.id, case_type: "article" }, result_version: { id: resultVersion.id, version: resultVersion.version }, optimized_article: optimizedArticle, qa_report: qaReport, export_paths: exportPaths, }; await traceRecorder.onWorkflowEvent(finalReadyEvent); await traceRecorder.finish({ status: "completed" }); send(finalReadyEvent); } catch (error) { const message = error instanceof Error ? error.message : "优化失败"; let failedVersion: | { id: string; version: number } | undefined; if (caseId) { try { const repository = getRepositoryFromRuntime(); const version = await repository.createOptimizationResultVersion({ case_id: caseId, case_type: "article", status: "failed", article_job_id: jobId ?? null, 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, }); failedVersion = { id: version.id, version: version.version }; } catch { failedVersion = undefined; } } const failedEvent: OptimizationStreamEvent = { type: "failed", job_id: jobId, case: caseId ? { id: caseId, case_type: "article" } : undefined, result_version: failedVersion, stage, error: message, }; await traceRecorder.onWorkflowEvent(failedEvent); await traceRecorder.finish({ status: "failed", errorStage: stage, errorSummary: message, }); send(failedEvent); } 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).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; }