接入文章优化案例自动保存
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user