接入文章优化案例自动保存

This commit is contained in:
czj
2026-07-08 13:24:50 +08:00
parent ee98c3304b
commit 79913ef7f7
11 changed files with 446 additions and 14 deletions
+121
View File
@@ -30,6 +30,7 @@ import { GET as getJobProgress } from "../jobs/[jobId]/progress/route";
import { POST as optimizeStream } from "../jobs/optimize-stream/route";
import { POST as createJob } from "../jobs/route";
import { POST as recordPerformance } from "../publications/[publicationId]/performance/route";
import { createSqliteRepository } from "../../../lib/db/sqlite-repository";
const validFactCard = {
company_full_name: "Example Technology Co., Ltd.",
@@ -89,6 +90,8 @@ interface StreamEventResponse {
type: string;
job_id?: string;
job?: { id: string };
case?: { id: string; case_type: string };
result_version?: { id: string; version: number };
fact_card?: { company_full_name: string; confirmed_by_user?: boolean };
article?: { title: string; body_markdown?: string };
optimized_article?: { title: string };
@@ -244,6 +247,44 @@ describe("job API routes", () => {
).toBe("流式优化标题");
});
it("auto-saves stream article optimization as a case and result version", async () => {
llmMocks.generateValidatedJson
.mockResolvedValueOnce(validCandidateFactCard)
.mockResolvedValueOnce({
title: "流式优化标题",
summary: "流式优化摘要。",
body_markdown:
"## 服务能力\nExample Technology Co., Ltd. 提供 GEO optimization 服务。",
image_suggestions: [],
changed_sections: ["title", "body"],
requires_user_confirmation: [],
})
.mockResolvedValueOnce({ checks: [] });
const response = await optimizeStream(
request({
body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
platform: "official_site",
}),
);
const events = await streamEvents(response);
const finalEvent = events.find((event) => event.type === "final_ready");
expect(finalEvent?.case?.case_type).toBe("article");
expect(finalEvent?.result_version?.version).toBe(1);
const repository = createSqliteRepository();
await expect(
repository.listOptimizationCases({ include_archived: false }),
).resolves.toEqual([
expect.objectContaining({
id: finalEvent?.case?.id,
case_type: "article",
status: "optimized",
}),
]);
});
it("uses an edited fact card without extracting a new one", async () => {
llmMocks.generateValidatedJson
.mockResolvedValueOnce({
@@ -325,6 +366,40 @@ describe("job API routes", () => {
);
});
it("auto-saves stream article LLM failure as a failed case", async () => {
llmMocks.generateValidatedJson
.mockResolvedValueOnce(validCandidateFactCard)
.mockRejectedValueOnce(new Error("LLM provider error: timeout"));
const response = await optimizeStream(
request({
body: "Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
platform: "official_site",
}),
);
const events = await streamEvents(response);
const failedEvent = events[events.length - 1];
expect(failedEvent.type).toBe("failed");
expect(failedEvent.case?.id).toMatch(/^case_/);
expect(failedEvent.result_version?.version).toBe(1);
const repository = createSqliteRepository();
const detail = await repository.getOptimizationCaseDetail(
failedEvent.case?.id ?? "",
);
expect(detail).toMatchObject({
case: { status: "failed", last_error_stage: "draft" },
versions: [
expect.objectContaining({
status: "failed",
error_summary: "LLM provider error: timeout",
}),
],
});
});
it("rejects unresolved uncertain items when confirming a fact card", async () => {
const { job } = await createJobFixture();
const response = await confirmFactCard(
@@ -387,6 +462,52 @@ describe("job API routes", () => {
expect(body.timing.steps.every((step) => step.duration_ms >= 0)).toBe(true);
});
it("auto-saves non-stream article optimization as a case result version", async () => {
const { job } = await createJobFixture();
await confirmFactCard(
request(validFactCard),
params<{ jobId: string }>({ jobId: job.id }),
);
llmMocks.generateValidatedJson
.mockResolvedValueOnce({
title: "非流式优化标题",
summary:
"Example Technology Co., Ltd. 面向市场团队提供 GEO optimization 服务。",
body_markdown:
"Example Technology Co., Ltd. has 8 years of GEO optimization experience.",
image_suggestions: [],
changed_sections: ["title", "body"],
requires_user_confirmation: [],
})
.mockResolvedValueOnce({ checks: [] });
const response = await optimizeJob(
request({}),
params<{ jobId: string }>({ jobId: job.id }),
);
expect(response.status).toBe(200);
const repository = createSqliteRepository();
const savedJob = await repository.getArticleJob(job.id);
expect(savedJob?.case_id).toMatch(/^case_/);
const detail = await repository.getOptimizationCaseDetail(
savedJob?.case_id ?? "",
);
expect(detail).toMatchObject({
case: { case_type: "article", status: "optimized" },
versions: [
expect.objectContaining({
version: 1,
status: "optimized",
article_job_id: job.id,
}),
],
});
});
it("scores a revision, registers publication, and records manual performance", async () => {
const { job } = await createJobFixture();
await confirmFactCard(
+78 -1
View File
@@ -1,7 +1,10 @@
import { NextResponse } from "next/server";
import { requireApiAccess } from "../../../../../lib/api/auth";
import { getRepositoryFromRuntime } from "../../../../../lib/db/repository";
import { buildArticleCaseSummary, createProcessStep } from "../../../../../lib/cases/summaries";
import { getRepositoryFromRuntime, type AppRepository } from "../../../../../lib/db/repository";
import type { ArticleJob } from "../../../../../lib/db/repositories";
import type { LlmAuditSummary } from "../../../../../lib/llm/audit";
import { LlmValidationError } from "../../../../../lib/llm/client";
import { getExportStoreFromRuntime } from "../../../../../lib/workflow/export-store";
import { runOptimizationWorkflow } from "../../../../../lib/workflow/orchestrator";
@@ -36,6 +39,8 @@ export async function POST(request: Request, context: RouteContext) {
}
const requestStartedAt = Date.now();
const caseId = await ensureArticleCase(repository, job);
const llmAuditSummary: LlmAuditSummary[] = [];
startWorkflowProgress(jobId);
try {
const result = await runOptimizationWorkflow({
@@ -50,6 +55,7 @@ export async function POST(request: Request, context: RouteContext) {
onProgress: (event) => {
recordWorkflowProgress(jobId, event);
},
onAuditSummary: (summary) => llmAuditSummary.push(summary),
});
const optimizedArticle = await repository.saveOptimizedArticle(
jobId,
@@ -70,8 +76,27 @@ export async function POST(request: Request, context: RouteContext) {
status: "optimized",
export_paths: exportPaths,
});
const resultVersion = await repository.createOptimizationResultVersion({
case_id: caseId,
case_type: "article",
status: "optimized",
article_job_id: jobId,
article_revision: optimizedArticle.revision ?? 1,
result_summary: optimizedArticle.summary,
payload: {
article: optimizedArticle,
qa_report: qaReport,
export_paths: exportPaths,
},
process_summary: [],
llm_audit_summary: llmAuditSummary,
error_stage: null,
error_summary: null,
});
return NextResponse.json({
case: { id: caseId, case_type: "article" },
resultVersion: { id: resultVersion.id, version: resultVersion.version },
optimizedArticle,
qaReport,
exportPaths,
@@ -81,6 +106,28 @@ export async function POST(request: Request, context: RouteContext) {
});
} catch (error) {
const message = error instanceof Error ? error.message : "LLM optimization failed";
await repository.createOptimizationResultVersion({
case_id: caseId,
case_type: "article",
status: "failed",
article_job_id: jobId,
article_revision: null,
result_summary: "",
payload: null,
process_summary: [
createProcessStep({
stage: "optimize",
startedAt: requestStartedAt,
endedAt: Date.now(),
status: "failed",
errorSummary: message,
producedResultVersion: false,
}),
],
llm_audit_summary: llmAuditSummary,
error_stage: "optimize",
error_summary: message,
});
return NextResponse.json(
{
error: message,
@@ -94,6 +141,36 @@ export async function POST(request: Request, context: RouteContext) {
}
}
async function ensureArticleCase(
repository: AppRepository,
job: ArticleJob,
) {
if (job.case_id) return job.case_id;
const caseSummary = buildArticleCaseSummary({
source_title: job.source_title,
source_body: job.source_body,
publish_platform: job.publish_platform,
});
const optimizationCase = await repository.createOptimizationCase({
case_type: "article",
...caseSummary,
});
await repository.saveCaseInput({
case_id: optimizationCase.id,
case_type: "article",
article_job_id: job.id,
payload: {
source_title: job.source_title,
source_body: job.source_body,
image_inputs: job.image_inputs,
publish_platform: job.publish_platform,
user_instructions: job.user_instructions,
},
});
return optimizationCase.id;
}
function getErrorStatus(error: unknown) {
if (error instanceof LlmValidationError) return 502;
if (error instanceof Error && /^LLM\b|provider/i.test(error.message)) return 502;
+106 -3
View File
@@ -1,8 +1,11 @@
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 { getExportStoreFromRuntime } from "../../../../lib/workflow/export-store";
import { extractCandidateFactCard } from "../../../../lib/workflow/fact-extractor";
@@ -56,11 +59,26 @@ export async function POST(request: Request) {
};
let jobId: string | undefined;
let caseId: string | undefined;
let stage: OptimizationStreamStage = "job";
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,
@@ -68,12 +86,40 @@ export async function POST(request: Request) {
user_instructions: normalized.articleInput.user_instructions,
});
jobId = job.id;
send({ type: "job_created", job: { id: 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" },
});
stage = "fact_card";
const factCardStartedAt = Date.now();
const factCard = optimizationFactCardSchema.parse(
payload.fact_card ??
(await extractCandidateFactCard(normalized.articleInput)),
(await extractCandidateFactCard(normalized.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);
send({
@@ -90,6 +136,7 @@ export async function POST(request: Request) {
stage = stageForEvent(event, stage);
send(event);
},
onAuditSummary: (summary) => llmAuditSummary.push(summary),
});
stage = "final";
@@ -112,19 +159,75 @@ export async function POST(request: Request) {
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,
});
send({
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,
});
} 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;
}
}
send({
type: "failed",
job_id: jobId,
case: caseId ? { id: caseId, case_type: "article" } : undefined,
result_version: failedVersion,
stage,
error: error instanceof Error ? error.message : "优化失败",
error: message,
});
} finally {
controller.close();
+52 -1
View File
@@ -1,7 +1,9 @@
import { NextResponse } from "next/server";
import { requireApiAccess } from "../../../lib/api/auth";
import { buildArticleCaseSummary, createProcessStep } from "../../../lib/cases/summaries";
import { getRepositoryFromRuntime } from "../../../lib/db/repository";
import type { LlmAuditSummary } from "../../../lib/llm/audit";
import { LlmValidationError } from "../../../lib/llm/client";
import { extractCandidateFactCard } from "../../../lib/workflow/fact-extractor";
import { normalizeInput, type RawArticleInput } from "../../../lib/workflow/input-normalizer";
@@ -16,29 +18,78 @@ export async function POST(request: Request) {
const payload = (await request.json()) as RawArticleInput;
const normalized = normalizeInput(payload);
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,
});
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,
});
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,
},
});
const factStartedAt = Date.now();
const llmAuditSummary: LlmAuditSummary[] = [];
const timing = {
total_ms: 0,
steps: [] as Array<{ label: string; duration_ms: number }>,
};
try {
const candidateFactCard = await extractCandidateFactCard(normalized.articleInput);
const candidateFactCard = await extractCandidateFactCard(
normalized.articleInput,
{ onAuditSummary: (summary) => llmAuditSummary.push(summary) },
);
const duration = Date.now() - factStartedAt;
timing.total_ms = duration;
timing.steps.push({ label: "事实卡提取", duration_ms: duration });
return NextResponse.json({ job, candidateFactCard, timing }, { status: 201 });
} catch (error) {
const message = error instanceof Error ? error.message : "Request failed";
const duration = Date.now() - factStartedAt;
timing.total_ms = duration;
timing.steps.push({ label: "事实卡提取", duration_ms: duration });
await repository.createOptimizationResultVersion({
case_id: optimizationCase.id,
case_type: "article",
status: "failed",
article_job_id: job.id,
article_revision: null,
result_summary: "",
payload: null,
process_summary: [
createProcessStep({
stage: "fact_card",
startedAt: factStartedAt,
endedAt: Date.now(),
status: "failed",
errorSummary: message,
producedResultVersion: false,
}),
],
llm_audit_summary: llmAuditSummary,
error_stage: "fact_card",
error_summary: message,
});
return jsonError(error, getErrorStatus(error), timing);
}
} catch (error) {
+4 -1
View File
@@ -4,7 +4,7 @@ import type {
OptimizedArticle,
} from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client";
import { generateValidatedJson, type GenerateInput } from "../llm/client";
import {
ARTICLE_OPTIMIZER_SYSTEM_PROMPT,
buildArticleOptimizerPrompt,
@@ -13,11 +13,13 @@ import {
export interface OptimizeArticleInput {
input: ArticleInput;
factCard: OptimizationFactCard;
onAuditSummary?: GenerateInput["onAuditSummary"];
}
export async function optimizeArticle({
input,
factCard,
onAuditSummary,
}: OptimizeArticleInput): Promise<OptimizedArticle> {
const llmArticle = await generateValidatedJson({
schema: optimizedArticleSchema,
@@ -25,6 +27,7 @@ export async function optimizeArticle({
prompt: buildArticleOptimizerPrompt(input, factCard),
temperature: 0.2,
task: "article_optimizer",
onAuditSummary,
});
return optimizedArticleSchema.parse({
+3 -1
View File
@@ -1,6 +1,6 @@
import type { ArticleInput, CandidateFactCard } from "../domain/types";
import { candidateFactCardSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client";
import { generateValidatedJson, type GenerateInput } from "../llm/client";
import {
FACT_EXTRACTOR_SYSTEM_PROMPT,
buildFactExtractorPrompt,
@@ -8,6 +8,7 @@ import {
export async function extractCandidateFactCard(
input: ArticleInput,
options: { onAuditSummary?: GenerateInput["onAuditSummary"] } = {},
): Promise<CandidateFactCard> {
return generateValidatedJson({
schema: candidateFactCardSchema,
@@ -15,5 +16,6 @@ export async function extractCandidateFactCard(
prompt: buildFactExtractorPrompt(input),
temperature: 0.1,
task: "fact_extractor",
onAuditSummary: options.onAuditSummary,
});
}
+7 -2
View File
@@ -1,4 +1,5 @@
import type { ArticleInput, OptimizationFactCard } from "../domain/types";
import type { GenerateInput } from "../llm/client";
import { optimizeArticle } from "./article-optimizer";
import { inspectQualityWithLlm } from "./quality-inspector";
@@ -8,6 +9,7 @@ export interface RunOptimizationWorkflowInput {
input: ArticleInput;
factCard: OptimizationFactCard;
onProgress?: (event: WorkflowProgressEvent) => void | Promise<void>;
onAuditSummary?: GenerateInput["onAuditSummary"];
}
export interface WorkflowTimingStep {
@@ -68,13 +70,14 @@ export async function runOptimizationWorkflow({
input,
factCard,
onProgress,
onAuditSummary,
}: RunOptimizationWorkflowInput) {
const startedAt = Date.now();
const timingSteps: WorkflowTimingStep[] = [];
let article = await timedStep(
"生成优化稿",
timingSteps,
() => optimizeArticle({ input, factCard }),
() => optimizeArticle({ input, factCard, onAuditSummary }),
onProgress,
);
let qaReport = await timedStep(
@@ -86,6 +89,7 @@ export async function runOptimizationWorkflow({
factCard,
platform: input.platform,
sourceImages: input.images,
onAuditSummary,
}),
onProgress,
);
@@ -97,7 +101,7 @@ export async function runOptimizationWorkflow({
article = await timedStep(
`定向修复第 ${nextRound}`,
timingSteps,
() => rewriteFailedSections({ article, factCard, failedChecks }),
() => rewriteFailedSections({ article, factCard, failedChecks, onAuditSummary }),
onProgress,
);
rewriteRounds = nextRound;
@@ -110,6 +114,7 @@ export async function runOptimizationWorkflow({
factCard,
platform: input.platform,
sourceImages: input.images,
onAuditSummary,
}),
onProgress,
);
+3 -1
View File
@@ -9,7 +9,7 @@ import type {
QualityRuleId,
} from "../domain/types";
import { qaCheckSchema, qaReportSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client";
import { generateValidatedJson, type GenerateInput } from "../llm/client";
import {
QUALITY_INSPECTOR_SYSTEM_PROMPT,
buildQualityInspectorPrompt,
@@ -43,6 +43,7 @@ export interface InspectQualityInput {
factCard: OptimizationFactCard;
platform: PublishPlatform;
sourceImages: ImageInput[];
onAuditSummary?: GenerateInput["onAuditSummary"];
}
export function inspectQuality(input: InspectQualityInput): QaReport {
@@ -71,6 +72,7 @@ export async function inspectQualityWithLlm(
}),
temperature: 0.1,
task: "quality_inspector",
onAuditSummary: input.onAuditSummary,
});
const patchedChecks = deterministicReport.checks.map((deterministicCheck) => {
+9 -1
View File
@@ -14,7 +14,11 @@ export type OptimizationStreamStage =
| "final";
export type OptimizationStreamEvent =
| { type: "job_created"; job: { id: string } }
| {
type: "job_created";
job: { id: string };
case?: { id: string; case_type: "article" };
}
| {
type: "fact_card_ready";
job_id: string;
@@ -34,6 +38,8 @@ export type OptimizationStreamEvent =
| {
type: "final_ready";
job_id: string;
case?: { id: string; case_type: "article" };
result_version?: { id: string; version: number };
optimized_article: OptimizedArticle;
qa_report: QaReport;
export_paths: Record<string, string>;
@@ -41,6 +47,8 @@ export type OptimizationStreamEvent =
| {
type: "failed";
job_id?: string;
case?: { id: string; case_type: "article" };
result_version?: { id: string; version: number };
stage: OptimizationStreamStage;
error: string;
};
+59 -2
View File
@@ -1,4 +1,7 @@
import type { ArticleInput, OptimizationFactCard } from "../domain/types";
import type { ProcessSummaryStep } from "../cases/types";
import { createProcessStep } from "../cases/summaries";
import type { GenerateInput } from "../llm/client";
import { optimizeArticle } from "./article-optimizer";
import { inspectQualityWithLlm } from "./quality-inspector";
@@ -10,6 +13,7 @@ export interface RunStreamingOptimizationWorkflowInput {
input: ArticleInput;
factCard: OptimizationFactCard;
onEvent: (event: OptimizationStreamEvent) => void | Promise<void>;
onAuditSummary?: GenerateInput["onAuditSummary"];
}
export async function runStreamingOptimizationWorkflow({
@@ -17,13 +21,26 @@ export async function runStreamingOptimizationWorkflow({
input,
factCard,
onEvent,
onAuditSummary,
}: RunStreamingOptimizationWorkflowInput) {
const processSummary: ProcessSummaryStep[] = [];
await onEvent({
type: "draft_started",
job_id: jobId,
message: "正在生成优化草稿",
});
let article = await optimizeArticle({ input, factCard });
let stageStartedAt = Date.now();
let article = await optimizeArticle({ input, factCard, onAuditSummary });
processSummary.push(
createProcessStep({
stage: "draft",
startedAt: stageStartedAt,
endedAt: Date.now(),
status: "success",
producedResultVersion: false,
}),
);
await onEvent({ type: "draft_ready", job_id: jobId, article });
await onEvent({
@@ -31,12 +48,23 @@ export async function runStreamingOptimizationWorkflow({
job_id: jobId,
message: "正在检查质量",
});
stageStartedAt = Date.now();
let qaReport = await inspectQualityWithLlm({
article,
factCard,
platform: input.platform,
sourceImages: input.images,
onAuditSummary,
});
processSummary.push(
createProcessStep({
stage: "qa",
startedAt: stageStartedAt,
endedAt: Date.now(),
status: "success",
producedResultVersion: false,
}),
);
await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
let rewriteRounds = 0;
@@ -48,8 +76,24 @@ export async function runStreamingOptimizationWorkflow({
job_id: jobId,
round: nextRound,
});
article = await rewriteFailedSections({ article, factCard, failedChecks });
stageStartedAt = Date.now();
article = await rewriteFailedSections({
article,
factCard,
failedChecks,
onAuditSummary,
});
rewriteRounds = nextRound;
processSummary.push(
createProcessStep({
stage: "rewrite",
startedAt: stageStartedAt,
endedAt: Date.now(),
status: "success",
rewriteRound: nextRound,
producedResultVersion: false,
}),
);
await onEvent({
type: "rewrite_ready",
job_id: jobId,
@@ -62,12 +106,24 @@ export async function runStreamingOptimizationWorkflow({
job_id: jobId,
message: `正在复检第 ${nextRound} 轮修复`,
});
stageStartedAt = Date.now();
qaReport = await inspectQualityWithLlm({
article,
factCard,
platform: input.platform,
sourceImages: input.images,
onAuditSummary,
});
processSummary.push(
createProcessStep({
stage: "qa",
startedAt: stageStartedAt,
endedAt: Date.now(),
status: "success",
rewriteRound: nextRound,
producedResultVersion: false,
}),
);
await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
}
@@ -77,5 +133,6 @@ export async function runStreamingOptimizationWorkflow({
rewriteRounds,
stoppedAfterMaxRewrites:
qaReport.overall_status === "fail" && rewriteRounds >= 2,
processSummary,
};
}
+4 -1
View File
@@ -1,6 +1,6 @@
import type { OptimizationFactCard, OptimizedArticle, QaCheck } from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client";
import { generateValidatedJson, type GenerateInput } from "../llm/client";
import {
TARGETED_REWRITER_SYSTEM_PROMPT,
buildTargetedRewritePrompt,
@@ -10,12 +10,14 @@ export interface RewriteFailedSectionsInput {
article: OptimizedArticle;
factCard: OptimizationFactCard;
failedChecks: QaCheck[];
onAuditSummary?: GenerateInput["onAuditSummary"];
}
export async function rewriteFailedSections({
article,
factCard,
failedChecks,
onAuditSummary,
}: RewriteFailedSectionsInput): Promise<OptimizedArticle> {
const llmArticle = await generateValidatedJson({
schema: optimizedArticleSchema,
@@ -23,6 +25,7 @@ export async function rewriteFailedSections({
prompt: buildTargetedRewritePrompt({ article, factCard, failedChecks }),
temperature: 0.15,
task: "targeted_rewriter",
onAuditSummary,
});
return optimizedArticleSchema.parse({