新增一键优化流式编排
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { OptimizationStreamEvent } from "../stream-events";
|
||||||
|
import { runStreamingOptimizationWorkflow } from "../streaming-optimizer";
|
||||||
|
|
||||||
|
const workflowMocks = vi.hoisted(() => ({
|
||||||
|
optimizeArticle: vi.fn(),
|
||||||
|
inspectQualityWithLlm: vi.fn(),
|
||||||
|
rewriteFailedSections: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../article-optimizer", () => ({
|
||||||
|
optimizeArticle: workflowMocks.optimizeArticle,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../quality-inspector", () => ({
|
||||||
|
inspectQualityWithLlm: workflowMocks.inspectQualityWithLlm,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../targeted-rewriter", () => ({
|
||||||
|
rewriteFailedSections: workflowMocks.rewriteFailedSections,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
title: "",
|
||||||
|
body: "示例科技提供GEO内容优化服务。",
|
||||||
|
images: [],
|
||||||
|
platform: "official_site" as const,
|
||||||
|
user_instructions: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const factCard = {
|
||||||
|
company_full_name: "",
|
||||||
|
company_short_names: ["示例科技"],
|
||||||
|
brand_names: [],
|
||||||
|
product_names: ["GEO内容优化平台"],
|
||||||
|
target_industry: "GEO内容优化",
|
||||||
|
target_audience: "市场团队",
|
||||||
|
experience_years: null,
|
||||||
|
core_claims: ["提供GEO内容优化服务"],
|
||||||
|
forbidden_claims: [],
|
||||||
|
image_topics: [],
|
||||||
|
uncertain_items: ["公司全称需要确认"],
|
||||||
|
is_ready_for_optimization: false,
|
||||||
|
confirmed_by_user: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const draftArticle = {
|
||||||
|
title: "示例科技 GEO 内容优化方案",
|
||||||
|
summary: "面向市场团队的GEO内容优化说明。",
|
||||||
|
body_markdown: "## 服务能力\n示例科技提供GEO内容优化服务。",
|
||||||
|
image_suggestions: [],
|
||||||
|
changed_sections: ["标题", "正文"],
|
||||||
|
requires_user_confirmation: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const failCheck = {
|
||||||
|
rule_id: "body_quality" as const,
|
||||||
|
status: "fail" as const,
|
||||||
|
evidence: "句子不够顺。",
|
||||||
|
reason: "正文需要润色。",
|
||||||
|
suggested_fix: "润色正文。",
|
||||||
|
target_agent: "body",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("runStreamingOptimizationWorkflow", () => {
|
||||||
|
it("emits draft and final QA events when no rewrite is needed", async () => {
|
||||||
|
workflowMocks.optimizeArticle.mockResolvedValueOnce(draftArticle);
|
||||||
|
workflowMocks.inspectQualityWithLlm.mockResolvedValueOnce({
|
||||||
|
overall_status: "pass",
|
||||||
|
checks: [],
|
||||||
|
});
|
||||||
|
const events: OptimizationStreamEvent[] = [];
|
||||||
|
|
||||||
|
const result = await runStreamingOptimizationWorkflow({
|
||||||
|
jobId: "job_stream",
|
||||||
|
input,
|
||||||
|
factCard,
|
||||||
|
onEvent: (event) => events.push(event),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(events.map((event) => event.type)).toEqual([
|
||||||
|
"draft_started",
|
||||||
|
"draft_ready",
|
||||||
|
"qa_started",
|
||||||
|
"qa_ready",
|
||||||
|
]);
|
||||||
|
expect(result.article.title).toBe("示例科技 GEO 内容优化方案");
|
||||||
|
expect(result.qaReport.overall_status).toBe("pass");
|
||||||
|
expect(result.rewriteRounds).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits rewrite events when QA fails", async () => {
|
||||||
|
workflowMocks.optimizeArticle.mockResolvedValueOnce(draftArticle);
|
||||||
|
workflowMocks.inspectQualityWithLlm
|
||||||
|
.mockResolvedValueOnce({ overall_status: "fail", checks: [failCheck] })
|
||||||
|
.mockResolvedValueOnce({ overall_status: "pass", checks: [] });
|
||||||
|
workflowMocks.rewriteFailedSections.mockResolvedValueOnce({
|
||||||
|
...draftArticle,
|
||||||
|
body_markdown: "## 服务能力\n示例科技提供清晰的GEO内容优化服务。",
|
||||||
|
});
|
||||||
|
const events: OptimizationStreamEvent[] = [];
|
||||||
|
|
||||||
|
const result = await runStreamingOptimizationWorkflow({
|
||||||
|
jobId: "job_stream",
|
||||||
|
input,
|
||||||
|
factCard,
|
||||||
|
onEvent: (event) => events.push(event),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(events.map((event) => event.type)).toEqual([
|
||||||
|
"draft_started",
|
||||||
|
"draft_ready",
|
||||||
|
"qa_started",
|
||||||
|
"qa_ready",
|
||||||
|
"rewrite_started",
|
||||||
|
"rewrite_ready",
|
||||||
|
"qa_started",
|
||||||
|
"qa_ready",
|
||||||
|
]);
|
||||||
|
expect(result.article.body_markdown).toContain("清晰的GEO内容优化服务");
|
||||||
|
expect(result.rewriteRounds).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import type { ArticleInput, OptimizationFactCard } from "../domain/types";
|
||||||
|
|
||||||
|
import { optimizeArticle } from "./article-optimizer";
|
||||||
|
import { inspectQualityWithLlm } from "./quality-inspector";
|
||||||
|
import type { OptimizationStreamEvent } from "./stream-events";
|
||||||
|
import { rewriteFailedSections } from "./targeted-rewriter";
|
||||||
|
|
||||||
|
export interface RunStreamingOptimizationWorkflowInput {
|
||||||
|
jobId: string;
|
||||||
|
input: ArticleInput;
|
||||||
|
factCard: OptimizationFactCard;
|
||||||
|
onEvent: (event: OptimizationStreamEvent) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runStreamingOptimizationWorkflow({
|
||||||
|
jobId,
|
||||||
|
input,
|
||||||
|
factCard,
|
||||||
|
onEvent,
|
||||||
|
}: RunStreamingOptimizationWorkflowInput) {
|
||||||
|
await onEvent({
|
||||||
|
type: "draft_started",
|
||||||
|
job_id: jobId,
|
||||||
|
message: "正在生成优化草稿",
|
||||||
|
});
|
||||||
|
let article = await optimizeArticle({ input, factCard });
|
||||||
|
await onEvent({ type: "draft_ready", job_id: jobId, article });
|
||||||
|
|
||||||
|
await onEvent({
|
||||||
|
type: "qa_started",
|
||||||
|
job_id: jobId,
|
||||||
|
message: "正在检查质量",
|
||||||
|
});
|
||||||
|
let qaReport = await inspectQualityWithLlm({
|
||||||
|
article,
|
||||||
|
factCard,
|
||||||
|
platform: input.platform,
|
||||||
|
sourceImages: input.images,
|
||||||
|
});
|
||||||
|
await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
|
||||||
|
|
||||||
|
let rewriteRounds = 0;
|
||||||
|
while (qaReport.overall_status === "fail" && rewriteRounds < 2) {
|
||||||
|
const nextRound = rewriteRounds + 1;
|
||||||
|
const failedChecks = qaReport.checks.filter((check) => check.status === "fail");
|
||||||
|
await onEvent({
|
||||||
|
type: "rewrite_started",
|
||||||
|
job_id: jobId,
|
||||||
|
round: nextRound,
|
||||||
|
});
|
||||||
|
article = await rewriteFailedSections({ article, factCard, failedChecks });
|
||||||
|
rewriteRounds = nextRound;
|
||||||
|
await onEvent({
|
||||||
|
type: "rewrite_ready",
|
||||||
|
job_id: jobId,
|
||||||
|
round: nextRound,
|
||||||
|
article,
|
||||||
|
});
|
||||||
|
|
||||||
|
await onEvent({
|
||||||
|
type: "qa_started",
|
||||||
|
job_id: jobId,
|
||||||
|
message: `正在复检第 ${nextRound} 轮修复`,
|
||||||
|
});
|
||||||
|
qaReport = await inspectQualityWithLlm({
|
||||||
|
article,
|
||||||
|
factCard,
|
||||||
|
platform: input.platform,
|
||||||
|
sourceImages: input.images,
|
||||||
|
});
|
||||||
|
await onEvent({ type: "qa_ready", job_id: jobId, qa_report: qaReport });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
article,
|
||||||
|
qaReport,
|
||||||
|
rewriteRounds,
|
||||||
|
stoppedAfterMaxRewrites:
|
||||||
|
qaReport.overall_status === "fail" && rewriteRounds >= 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user