diff --git a/src/app/api/__tests__/jobs.test.ts b/src/app/api/__tests__/jobs.test.ts index 945847b..3a042b3 100644 --- a/src/app/api/__tests__/jobs.test.ts +++ b/src/app/api/__tests__/jobs.test.ts @@ -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( diff --git a/src/app/api/jobs/[jobId]/optimize/route.ts b/src/app/api/jobs/[jobId]/optimize/route.ts index b00063d..99e5212 100644 --- a/src/app/api/jobs/[jobId]/optimize/route.ts +++ b/src/app/api/jobs/[jobId]/optimize/route.ts @@ -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; diff --git a/src/app/api/jobs/optimize-stream/route.ts b/src/app/api/jobs/optimize-stream/route.ts index 68a1604..7552eb0 100644 --- a/src/app/api/jobs/optimize-stream/route.ts +++ b/src/app/api/jobs/optimize-stream/route.ts @@ -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(); diff --git a/src/app/api/jobs/route.ts b/src/app/api/jobs/route.ts index 78180ce..172636c 100644 --- a/src/app/api/jobs/route.ts +++ b/src/app/api/jobs/route.ts @@ -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) { diff --git a/src/lib/workflow/article-optimizer.ts b/src/lib/workflow/article-optimizer.ts index 3e9d042..29cf50a 100644 --- a/src/lib/workflow/article-optimizer.ts +++ b/src/lib/workflow/article-optimizer.ts @@ -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 { 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({ diff --git a/src/lib/workflow/fact-extractor.ts b/src/lib/workflow/fact-extractor.ts index ce5f7eb..3e941d3 100644 --- a/src/lib/workflow/fact-extractor.ts +++ b/src/lib/workflow/fact-extractor.ts @@ -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 { return generateValidatedJson({ schema: candidateFactCardSchema, @@ -15,5 +16,6 @@ export async function extractCandidateFactCard( prompt: buildFactExtractorPrompt(input), temperature: 0.1, task: "fact_extractor", + onAuditSummary: options.onAuditSummary, }); } diff --git a/src/lib/workflow/orchestrator.ts b/src/lib/workflow/orchestrator.ts index 54f2dd8..4d2eae4 100644 --- a/src/lib/workflow/orchestrator.ts +++ b/src/lib/workflow/orchestrator.ts @@ -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; + 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, ); diff --git a/src/lib/workflow/quality-inspector.ts b/src/lib/workflow/quality-inspector.ts index 5a2eaa6..fc650c3 100644 --- a/src/lib/workflow/quality-inspector.ts +++ b/src/lib/workflow/quality-inspector.ts @@ -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) => { diff --git a/src/lib/workflow/stream-events.ts b/src/lib/workflow/stream-events.ts index 327d50c..63ab59c 100644 --- a/src/lib/workflow/stream-events.ts +++ b/src/lib/workflow/stream-events.ts @@ -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; @@ -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; }; diff --git a/src/lib/workflow/streaming-optimizer.ts b/src/lib/workflow/streaming-optimizer.ts index f1e69bc..a074347 100644 --- a/src/lib/workflow/streaming-optimizer.ts +++ b/src/lib/workflow/streaming-optimizer.ts @@ -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; + 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, }; } diff --git a/src/lib/workflow/targeted-rewriter.ts b/src/lib/workflow/targeted-rewriter.ts index 6580cfa..d4d6654 100644 --- a/src/lib/workflow/targeted-rewriter.ts +++ b/src/lib/workflow/targeted-rewriter.ts @@ -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 { 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({