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

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 optimizeStream } from "../jobs/optimize-stream/route";
import { POST as createJob } from "../jobs/route"; import { POST as createJob } from "../jobs/route";
import { POST as recordPerformance } from "../publications/[publicationId]/performance/route"; import { POST as recordPerformance } from "../publications/[publicationId]/performance/route";
import { createSqliteRepository } from "../../../lib/db/sqlite-repository";
const validFactCard = { const validFactCard = {
company_full_name: "Example Technology Co., Ltd.", company_full_name: "Example Technology Co., Ltd.",
@@ -89,6 +90,8 @@ interface StreamEventResponse {
type: string; type: string;
job_id?: string; job_id?: 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 }; fact_card?: { company_full_name: string; confirmed_by_user?: boolean };
article?: { title: string; body_markdown?: string }; article?: { title: string; body_markdown?: string };
optimized_article?: { title: string }; optimized_article?: { title: string };
@@ -244,6 +247,44 @@ describe("job API routes", () => {
).toBe("流式优化标题"); ).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 () => { it("uses an edited fact card without extracting a new one", async () => {
llmMocks.generateValidatedJson llmMocks.generateValidatedJson
.mockResolvedValueOnce({ .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 () => { it("rejects unresolved uncertain items when confirming a fact card", async () => {
const { job } = await createJobFixture(); const { job } = await createJobFixture();
const response = await confirmFactCard( const response = await confirmFactCard(
@@ -387,6 +462,52 @@ describe("job API routes", () => {
expect(body.timing.steps.every((step) => step.duration_ms >= 0)).toBe(true); 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 () => { it("scores a revision, registers publication, and records manual performance", async () => {
const { job } = await createJobFixture(); const { job } = await createJobFixture();
await confirmFactCard( await confirmFactCard(
+78 -1
View File
@@ -1,7 +1,10 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { requireApiAccess } from "../../../../../lib/api/auth"; 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 { LlmValidationError } from "../../../../../lib/llm/client";
import { getExportStoreFromRuntime } from "../../../../../lib/workflow/export-store"; import { getExportStoreFromRuntime } from "../../../../../lib/workflow/export-store";
import { runOptimizationWorkflow } from "../../../../../lib/workflow/orchestrator"; import { runOptimizationWorkflow } from "../../../../../lib/workflow/orchestrator";
@@ -36,6 +39,8 @@ export async function POST(request: Request, context: RouteContext) {
} }
const requestStartedAt = Date.now(); const requestStartedAt = Date.now();
const caseId = await ensureArticleCase(repository, job);
const llmAuditSummary: LlmAuditSummary[] = [];
startWorkflowProgress(jobId); startWorkflowProgress(jobId);
try { try {
const result = await runOptimizationWorkflow({ const result = await runOptimizationWorkflow({
@@ -50,6 +55,7 @@ export async function POST(request: Request, context: RouteContext) {
onProgress: (event) => { onProgress: (event) => {
recordWorkflowProgress(jobId, event); recordWorkflowProgress(jobId, event);
}, },
onAuditSummary: (summary) => llmAuditSummary.push(summary),
}); });
const optimizedArticle = await repository.saveOptimizedArticle( const optimizedArticle = await repository.saveOptimizedArticle(
jobId, jobId,
@@ -70,8 +76,27 @@ export async function POST(request: Request, context: RouteContext) {
status: "optimized", status: "optimized",
export_paths: exportPaths, 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({ return NextResponse.json({
case: { id: caseId, case_type: "article" },
resultVersion: { id: resultVersion.id, version: resultVersion.version },
optimizedArticle, optimizedArticle,
qaReport, qaReport,
exportPaths, exportPaths,
@@ -81,6 +106,28 @@ export async function POST(request: Request, context: RouteContext) {
}); });
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "LLM optimization failed"; 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( return NextResponse.json(
{ {
error: message, 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) { function getErrorStatus(error: unknown) {
if (error instanceof LlmValidationError) return 502; if (error instanceof LlmValidationError) return 502;
if (error instanceof Error && /^LLM\b|provider/i.test(error.message)) 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 { NextResponse } from "next/server";
import { requireApiAccess } from "../../../../lib/api/auth"; 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 { getRepositoryFromRuntime } from "../../../../lib/db/repository";
import { optimizationFactCardSchema } from "../../../../lib/domain/validation"; import { optimizationFactCardSchema } from "../../../../lib/domain/validation";
import type { LlmAuditSummary } from "../../../../lib/llm/audit";
import { LlmValidationError } from "../../../../lib/llm/client"; import { LlmValidationError } from "../../../../lib/llm/client";
import { getExportStoreFromRuntime } from "../../../../lib/workflow/export-store"; import { getExportStoreFromRuntime } from "../../../../lib/workflow/export-store";
import { extractCandidateFactCard } from "../../../../lib/workflow/fact-extractor"; import { extractCandidateFactCard } from "../../../../lib/workflow/fact-extractor";
@@ -56,11 +59,26 @@ export async function POST(request: Request) {
}; };
let jobId: string | undefined; let jobId: string | undefined;
let caseId: string | undefined;
let stage: OptimizationStreamStage = "job"; let stage: OptimizationStreamStage = "job";
const llmAuditSummary: LlmAuditSummary[] = [];
const processSummary: ProcessSummaryStep[] = [];
const requestStartedAt = Date.now();
try { try {
const repository = getRepositoryFromRuntime(); 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({ const job = await repository.createArticleJob({
case_id: optimizationCase.id,
source_title: normalized.articleInput.title, source_title: normalized.articleInput.title,
source_body: normalized.articleInput.body, source_body: normalized.articleInput.body,
image_inputs: normalized.articleInput.images, image_inputs: normalized.articleInput.images,
@@ -68,12 +86,40 @@ export async function POST(request: Request) {
user_instructions: normalized.articleInput.user_instructions, user_instructions: normalized.articleInput.user_instructions,
}); });
jobId = job.id; 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"; stage = "fact_card";
const factCardStartedAt = Date.now();
const factCard = optimizationFactCardSchema.parse( const factCard = optimizationFactCardSchema.parse(
payload.fact_card ?? 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); const savedFactCard = await repository.saveFactCard(job.id, factCard);
send({ send({
@@ -90,6 +136,7 @@ export async function POST(request: Request) {
stage = stageForEvent(event, stage); stage = stageForEvent(event, stage);
send(event); send(event);
}, },
onAuditSummary: (summary) => llmAuditSummary.push(summary),
}); });
stage = "final"; stage = "final";
@@ -112,19 +159,75 @@ export async function POST(request: Request) {
status: "optimized", status: "optimized",
export_paths: exportPaths, 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({ send({
type: "final_ready", type: "final_ready",
job_id: job.id, job_id: job.id,
case: { id: optimizationCase.id, case_type: "article" },
result_version: { id: resultVersion.id, version: resultVersion.version },
optimized_article: optimizedArticle, optimized_article: optimizedArticle,
qa_report: qaReport, qa_report: qaReport,
export_paths: exportPaths, export_paths: exportPaths,
}); });
} catch (error) { } 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({ send({
type: "failed", type: "failed",
job_id: jobId, job_id: jobId,
case: caseId ? { id: caseId, case_type: "article" } : undefined,
result_version: failedVersion,
stage, stage,
error: error instanceof Error ? error.message : "优化失败", error: message,
}); });
} finally { } finally {
controller.close(); controller.close();
+52 -1
View File
@@ -1,7 +1,9 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { requireApiAccess } from "../../../lib/api/auth"; import { requireApiAccess } from "../../../lib/api/auth";
import { buildArticleCaseSummary, createProcessStep } from "../../../lib/cases/summaries";
import { getRepositoryFromRuntime } from "../../../lib/db/repository"; import { getRepositoryFromRuntime } from "../../../lib/db/repository";
import type { LlmAuditSummary } from "../../../lib/llm/audit";
import { LlmValidationError } from "../../../lib/llm/client"; import { LlmValidationError } from "../../../lib/llm/client";
import { extractCandidateFactCard } from "../../../lib/workflow/fact-extractor"; import { extractCandidateFactCard } from "../../../lib/workflow/fact-extractor";
import { normalizeInput, type RawArticleInput } from "../../../lib/workflow/input-normalizer"; 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 payload = (await request.json()) as RawArticleInput;
const normalized = normalizeInput(payload); const normalized = normalizeInput(payload);
const repository = getRepositoryFromRuntime(); 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({ const job = await repository.createArticleJob({
case_id: optimizationCase.id,
source_title: normalized.articleInput.title, source_title: normalized.articleInput.title,
source_body: normalized.articleInput.body, source_body: normalized.articleInput.body,
image_inputs: normalized.articleInput.images, image_inputs: normalized.articleInput.images,
publish_platform: normalized.articleInput.platform, publish_platform: normalized.articleInput.platform,
user_instructions: normalized.articleInput.user_instructions, 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 factStartedAt = Date.now();
const llmAuditSummary: LlmAuditSummary[] = [];
const timing = { const timing = {
total_ms: 0, total_ms: 0,
steps: [] as Array<{ label: string; duration_ms: number }>, steps: [] as Array<{ label: string; duration_ms: number }>,
}; };
try { try {
const candidateFactCard = await extractCandidateFactCard(normalized.articleInput); const candidateFactCard = await extractCandidateFactCard(
normalized.articleInput,
{ onAuditSummary: (summary) => llmAuditSummary.push(summary) },
);
const duration = Date.now() - factStartedAt; const duration = Date.now() - factStartedAt;
timing.total_ms = duration; timing.total_ms = duration;
timing.steps.push({ label: "事实卡提取", duration_ms: duration }); timing.steps.push({ label: "事实卡提取", duration_ms: duration });
return NextResponse.json({ job, candidateFactCard, timing }, { status: 201 }); return NextResponse.json({ job, candidateFactCard, timing }, { status: 201 });
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Request failed";
const duration = Date.now() - factStartedAt; const duration = Date.now() - factStartedAt;
timing.total_ms = duration; timing.total_ms = duration;
timing.steps.push({ label: "事实卡提取", duration_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); return jsonError(error, getErrorStatus(error), timing);
} }
} catch (error) { } catch (error) {
+4 -1
View File
@@ -4,7 +4,7 @@ import type {
OptimizedArticle, OptimizedArticle,
} from "../domain/types"; } from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation"; import { optimizedArticleSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client"; import { generateValidatedJson, type GenerateInput } from "../llm/client";
import { import {
ARTICLE_OPTIMIZER_SYSTEM_PROMPT, ARTICLE_OPTIMIZER_SYSTEM_PROMPT,
buildArticleOptimizerPrompt, buildArticleOptimizerPrompt,
@@ -13,11 +13,13 @@ import {
export interface OptimizeArticleInput { export interface OptimizeArticleInput {
input: ArticleInput; input: ArticleInput;
factCard: OptimizationFactCard; factCard: OptimizationFactCard;
onAuditSummary?: GenerateInput["onAuditSummary"];
} }
export async function optimizeArticle({ export async function optimizeArticle({
input, input,
factCard, factCard,
onAuditSummary,
}: OptimizeArticleInput): Promise<OptimizedArticle> { }: OptimizeArticleInput): Promise<OptimizedArticle> {
const llmArticle = await generateValidatedJson({ const llmArticle = await generateValidatedJson({
schema: optimizedArticleSchema, schema: optimizedArticleSchema,
@@ -25,6 +27,7 @@ export async function optimizeArticle({
prompt: buildArticleOptimizerPrompt(input, factCard), prompt: buildArticleOptimizerPrompt(input, factCard),
temperature: 0.2, temperature: 0.2,
task: "article_optimizer", task: "article_optimizer",
onAuditSummary,
}); });
return optimizedArticleSchema.parse({ return optimizedArticleSchema.parse({
+3 -1
View File
@@ -1,6 +1,6 @@
import type { ArticleInput, CandidateFactCard } from "../domain/types"; import type { ArticleInput, CandidateFactCard } from "../domain/types";
import { candidateFactCardSchema } from "../domain/validation"; import { candidateFactCardSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client"; import { generateValidatedJson, type GenerateInput } from "../llm/client";
import { import {
FACT_EXTRACTOR_SYSTEM_PROMPT, FACT_EXTRACTOR_SYSTEM_PROMPT,
buildFactExtractorPrompt, buildFactExtractorPrompt,
@@ -8,6 +8,7 @@ import {
export async function extractCandidateFactCard( export async function extractCandidateFactCard(
input: ArticleInput, input: ArticleInput,
options: { onAuditSummary?: GenerateInput["onAuditSummary"] } = {},
): Promise<CandidateFactCard> { ): Promise<CandidateFactCard> {
return generateValidatedJson({ return generateValidatedJson({
schema: candidateFactCardSchema, schema: candidateFactCardSchema,
@@ -15,5 +16,6 @@ export async function extractCandidateFactCard(
prompt: buildFactExtractorPrompt(input), prompt: buildFactExtractorPrompt(input),
temperature: 0.1, temperature: 0.1,
task: "fact_extractor", task: "fact_extractor",
onAuditSummary: options.onAuditSummary,
}); });
} }
+7 -2
View File
@@ -1,4 +1,5 @@
import type { ArticleInput, OptimizationFactCard } from "../domain/types"; import type { ArticleInput, OptimizationFactCard } from "../domain/types";
import type { GenerateInput } from "../llm/client";
import { optimizeArticle } from "./article-optimizer"; import { optimizeArticle } from "./article-optimizer";
import { inspectQualityWithLlm } from "./quality-inspector"; import { inspectQualityWithLlm } from "./quality-inspector";
@@ -8,6 +9,7 @@ export interface RunOptimizationWorkflowInput {
input: ArticleInput; input: ArticleInput;
factCard: OptimizationFactCard; factCard: OptimizationFactCard;
onProgress?: (event: WorkflowProgressEvent) => void | Promise<void>; onProgress?: (event: WorkflowProgressEvent) => void | Promise<void>;
onAuditSummary?: GenerateInput["onAuditSummary"];
} }
export interface WorkflowTimingStep { export interface WorkflowTimingStep {
@@ -68,13 +70,14 @@ export async function runOptimizationWorkflow({
input, input,
factCard, factCard,
onProgress, onProgress,
onAuditSummary,
}: RunOptimizationWorkflowInput) { }: RunOptimizationWorkflowInput) {
const startedAt = Date.now(); const startedAt = Date.now();
const timingSteps: WorkflowTimingStep[] = []; const timingSteps: WorkflowTimingStep[] = [];
let article = await timedStep( let article = await timedStep(
"生成优化稿", "生成优化稿",
timingSteps, timingSteps,
() => optimizeArticle({ input, factCard }), () => optimizeArticle({ input, factCard, onAuditSummary }),
onProgress, onProgress,
); );
let qaReport = await timedStep( let qaReport = await timedStep(
@@ -86,6 +89,7 @@ export async function runOptimizationWorkflow({
factCard, factCard,
platform: input.platform, platform: input.platform,
sourceImages: input.images, sourceImages: input.images,
onAuditSummary,
}), }),
onProgress, onProgress,
); );
@@ -97,7 +101,7 @@ export async function runOptimizationWorkflow({
article = await timedStep( article = await timedStep(
`定向修复第 ${nextRound}`, `定向修复第 ${nextRound}`,
timingSteps, timingSteps,
() => rewriteFailedSections({ article, factCard, failedChecks }), () => rewriteFailedSections({ article, factCard, failedChecks, onAuditSummary }),
onProgress, onProgress,
); );
rewriteRounds = nextRound; rewriteRounds = nextRound;
@@ -110,6 +114,7 @@ export async function runOptimizationWorkflow({
factCard, factCard,
platform: input.platform, platform: input.platform,
sourceImages: input.images, sourceImages: input.images,
onAuditSummary,
}), }),
onProgress, onProgress,
); );
+3 -1
View File
@@ -9,7 +9,7 @@ import type {
QualityRuleId, QualityRuleId,
} from "../domain/types"; } from "../domain/types";
import { qaCheckSchema, qaReportSchema } from "../domain/validation"; import { qaCheckSchema, qaReportSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client"; import { generateValidatedJson, type GenerateInput } from "../llm/client";
import { import {
QUALITY_INSPECTOR_SYSTEM_PROMPT, QUALITY_INSPECTOR_SYSTEM_PROMPT,
buildQualityInspectorPrompt, buildQualityInspectorPrompt,
@@ -43,6 +43,7 @@ export interface InspectQualityInput {
factCard: OptimizationFactCard; factCard: OptimizationFactCard;
platform: PublishPlatform; platform: PublishPlatform;
sourceImages: ImageInput[]; sourceImages: ImageInput[];
onAuditSummary?: GenerateInput["onAuditSummary"];
} }
export function inspectQuality(input: InspectQualityInput): QaReport { export function inspectQuality(input: InspectQualityInput): QaReport {
@@ -71,6 +72,7 @@ export async function inspectQualityWithLlm(
}), }),
temperature: 0.1, temperature: 0.1,
task: "quality_inspector", task: "quality_inspector",
onAuditSummary: input.onAuditSummary,
}); });
const patchedChecks = deterministicReport.checks.map((deterministicCheck) => { const patchedChecks = deterministicReport.checks.map((deterministicCheck) => {
+9 -1
View File
@@ -14,7 +14,11 @@ export type OptimizationStreamStage =
| "final"; | "final";
export type OptimizationStreamEvent = 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"; type: "fact_card_ready";
job_id: string; job_id: string;
@@ -34,6 +38,8 @@ export type OptimizationStreamEvent =
| { | {
type: "final_ready"; type: "final_ready";
job_id: string; job_id: string;
case?: { id: string; case_type: "article" };
result_version?: { id: string; version: number };
optimized_article: OptimizedArticle; optimized_article: OptimizedArticle;
qa_report: QaReport; qa_report: QaReport;
export_paths: Record<string, string>; export_paths: Record<string, string>;
@@ -41,6 +47,8 @@ export type OptimizationStreamEvent =
| { | {
type: "failed"; type: "failed";
job_id?: string; job_id?: string;
case?: { id: string; case_type: "article" };
result_version?: { id: string; version: number };
stage: OptimizationStreamStage; stage: OptimizationStreamStage;
error: string; error: string;
}; };
+59 -2
View File
@@ -1,4 +1,7 @@
import type { ArticleInput, OptimizationFactCard } from "../domain/types"; 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 { optimizeArticle } from "./article-optimizer";
import { inspectQualityWithLlm } from "./quality-inspector"; import { inspectQualityWithLlm } from "./quality-inspector";
@@ -10,6 +13,7 @@ export interface RunStreamingOptimizationWorkflowInput {
input: ArticleInput; input: ArticleInput;
factCard: OptimizationFactCard; factCard: OptimizationFactCard;
onEvent: (event: OptimizationStreamEvent) => void | Promise<void>; onEvent: (event: OptimizationStreamEvent) => void | Promise<void>;
onAuditSummary?: GenerateInput["onAuditSummary"];
} }
export async function runStreamingOptimizationWorkflow({ export async function runStreamingOptimizationWorkflow({
@@ -17,13 +21,26 @@ export async function runStreamingOptimizationWorkflow({
input, input,
factCard, factCard,
onEvent, onEvent,
onAuditSummary,
}: RunStreamingOptimizationWorkflowInput) { }: RunStreamingOptimizationWorkflowInput) {
const processSummary: ProcessSummaryStep[] = [];
await onEvent({ await onEvent({
type: "draft_started", type: "draft_started",
job_id: jobId, job_id: jobId,
message: "正在生成优化草稿", 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({ type: "draft_ready", job_id: jobId, article });
await onEvent({ await onEvent({
@@ -31,12 +48,23 @@ export async function runStreamingOptimizationWorkflow({
job_id: jobId, job_id: jobId,
message: "正在检查质量", message: "正在检查质量",
}); });
stageStartedAt = Date.now();
let qaReport = await inspectQualityWithLlm({ let qaReport = await inspectQualityWithLlm({
article, article,
factCard, factCard,
platform: input.platform, platform: input.platform,
sourceImages: input.images, 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 }); await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
let rewriteRounds = 0; let rewriteRounds = 0;
@@ -48,8 +76,24 @@ export async function runStreamingOptimizationWorkflow({
job_id: jobId, job_id: jobId,
round: nextRound, round: nextRound,
}); });
article = await rewriteFailedSections({ article, factCard, failedChecks }); stageStartedAt = Date.now();
article = await rewriteFailedSections({
article,
factCard,
failedChecks,
onAuditSummary,
});
rewriteRounds = nextRound; rewriteRounds = nextRound;
processSummary.push(
createProcessStep({
stage: "rewrite",
startedAt: stageStartedAt,
endedAt: Date.now(),
status: "success",
rewriteRound: nextRound,
producedResultVersion: false,
}),
);
await onEvent({ await onEvent({
type: "rewrite_ready", type: "rewrite_ready",
job_id: jobId, job_id: jobId,
@@ -62,12 +106,24 @@ export async function runStreamingOptimizationWorkflow({
job_id: jobId, job_id: jobId,
message: `正在复检第 ${nextRound} 轮修复`, message: `正在复检第 ${nextRound} 轮修复`,
}); });
stageStartedAt = Date.now();
qaReport = await inspectQualityWithLlm({ qaReport = await inspectQualityWithLlm({
article, article,
factCard, factCard,
platform: input.platform, platform: input.platform,
sourceImages: input.images, 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 }); await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
} }
@@ -77,5 +133,6 @@ export async function runStreamingOptimizationWorkflow({
rewriteRounds, rewriteRounds,
stoppedAfterMaxRewrites: stoppedAfterMaxRewrites:
qaReport.overall_status === "fail" && rewriteRounds >= 2, qaReport.overall_status === "fail" && rewriteRounds >= 2,
processSummary,
}; };
} }
+4 -1
View File
@@ -1,6 +1,6 @@
import type { OptimizationFactCard, OptimizedArticle, QaCheck } from "../domain/types"; import type { OptimizationFactCard, OptimizedArticle, QaCheck } from "../domain/types";
import { optimizedArticleSchema } from "../domain/validation"; import { optimizedArticleSchema } from "../domain/validation";
import { generateValidatedJson } from "../llm/client"; import { generateValidatedJson, type GenerateInput } from "../llm/client";
import { import {
TARGETED_REWRITER_SYSTEM_PROMPT, TARGETED_REWRITER_SYSTEM_PROMPT,
buildTargetedRewritePrompt, buildTargetedRewritePrompt,
@@ -10,12 +10,14 @@ export interface RewriteFailedSectionsInput {
article: OptimizedArticle; article: OptimizedArticle;
factCard: OptimizationFactCard; factCard: OptimizationFactCard;
failedChecks: QaCheck[]; failedChecks: QaCheck[];
onAuditSummary?: GenerateInput["onAuditSummary"];
} }
export async function rewriteFailedSections({ export async function rewriteFailedSections({
article, article,
factCard, factCard,
failedChecks, failedChecks,
onAuditSummary,
}: RewriteFailedSectionsInput): Promise<OptimizedArticle> { }: RewriteFailedSectionsInput): Promise<OptimizedArticle> {
const llmArticle = await generateValidatedJson({ const llmArticle = await generateValidatedJson({
schema: optimizedArticleSchema, schema: optimizedArticleSchema,
@@ -23,6 +25,7 @@ export async function rewriteFailedSections({
prompt: buildTargetedRewritePrompt({ article, factCard, failedChecks }), prompt: buildTargetedRewritePrompt({ article, factCard, failedChecks }),
temperature: 0.15, temperature: 0.15,
task: "targeted_rewriter", task: "targeted_rewriter",
onAuditSummary,
}); });
return optimizedArticleSchema.parse({ return optimizedArticleSchema.parse({