feat: export optimized article files
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
saveQaReport,
|
||||
updateArticleJob,
|
||||
} from "../../../../../lib/db/repositories";
|
||||
import { writeJobExports } from "../../../../../lib/workflow/exporter";
|
||||
import { runOptimizationWorkflow } from "../../../../../lib/workflow/orchestrator";
|
||||
|
||||
interface RouteContext {
|
||||
@@ -45,13 +46,23 @@ export async function POST(_request: Request, context: RouteContext) {
|
||||
optimizedArticle.revision ?? 1,
|
||||
result.qaReport,
|
||||
);
|
||||
const exportPaths =
|
||||
qaReport.overall_status === "fail"
|
||||
? {}
|
||||
: await writeJobExports({
|
||||
jobId,
|
||||
article: optimizedArticle,
|
||||
qaReport,
|
||||
});
|
||||
updateArticleJob(undefined, jobId, {
|
||||
status: qaReport.overall_status === "fail" ? "qa_failed" : "optimized",
|
||||
export_paths: exportPaths,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
optimizedArticle,
|
||||
qaReport,
|
||||
exportPaths,
|
||||
rewriteRounds: result.rewrite_rounds,
|
||||
stoppedAfterMaxRewrites: result.stopped_after_max_rewrites,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
renderOptimizedDocx,
|
||||
renderOptimizedMarkdown,
|
||||
renderQaReportJson,
|
||||
writeJobExports,
|
||||
} from "../exporter";
|
||||
|
||||
const article = {
|
||||
title: "Example GEO Guide",
|
||||
summary: "A factual GEO guide.",
|
||||
body_markdown: "## Body\nConfirmed content.",
|
||||
image_suggestions: [{ source: "image_1", suggestion: "Use dashboard." }],
|
||||
changed_sections: ["title"],
|
||||
requires_user_confirmation: [],
|
||||
};
|
||||
|
||||
const qaReport = {
|
||||
overall_status: "pass" as const,
|
||||
checks: [
|
||||
{
|
||||
rule_id: "title_quality" as const,
|
||||
status: "pass" as const,
|
||||
evidence: "Readable title.",
|
||||
reason: "Title is clear.",
|
||||
suggested_fix: "",
|
||||
target_agent: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("exporter", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), "geo-agent-export-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("renders markdown with optimized article fields", () => {
|
||||
const markdown = renderOptimizedMarkdown(article);
|
||||
|
||||
expect(markdown).toContain("# Example GEO Guide");
|
||||
expect(markdown).toContain("A factual GEO guide.");
|
||||
expect(markdown).toContain("## Body");
|
||||
expect(markdown).toContain("image_1: Use dashboard.");
|
||||
});
|
||||
|
||||
it("serializes the QA report as JSON", () => {
|
||||
const json = renderQaReportJson(qaReport);
|
||||
|
||||
expect(JSON.parse(json)).toMatchObject({
|
||||
overall_status: "pass",
|
||||
checks: [{ rule_id: "title_quality" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a non-empty Word document buffer", async () => {
|
||||
const buffer = await renderOptimizedDocx(article);
|
||||
|
||||
expect(buffer.byteLength).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("writes markdown, Word, and JSON exports under a job directory", async () => {
|
||||
const paths = await writeJobExports({
|
||||
dataDir: tempDir,
|
||||
jobId: "job_xxx",
|
||||
article,
|
||||
qaReport,
|
||||
});
|
||||
|
||||
expect(readFileSync(paths.markdown, "utf8")).toContain("# Example GEO Guide");
|
||||
expect(statSync(paths.docx).size).toBeGreaterThan(0);
|
||||
expect(JSON.parse(readFileSync(paths.qaJson, "utf8")).overall_status).toBe(
|
||||
"pass",
|
||||
);
|
||||
expect(paths.markdown).toContain(join("exports", "job_xxx", "optimized.md"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { Document, Packer, Paragraph, TextRun } from "docx";
|
||||
|
||||
import { getAppDataDir } from "../db/connection";
|
||||
import type { OptimizedArticle, QaReport } from "../domain/types";
|
||||
|
||||
export interface WriteJobExportsInput {
|
||||
dataDir?: string;
|
||||
jobId: string;
|
||||
article: OptimizedArticle;
|
||||
qaReport: QaReport;
|
||||
}
|
||||
|
||||
export function renderOptimizedMarkdown(article: OptimizedArticle) {
|
||||
const imageSuggestions =
|
||||
article.image_suggestions.length > 0
|
||||
? article.image_suggestions
|
||||
.map((item) => `- ${item.source}: ${item.suggestion}`)
|
||||
.join("\n")
|
||||
: "- No image suggestions.";
|
||||
const confirmations =
|
||||
article.requires_user_confirmation.length > 0
|
||||
? [
|
||||
"",
|
||||
"## Requires User Confirmation",
|
||||
...article.requires_user_confirmation.map((item) => `- ${item}`),
|
||||
].join("\n")
|
||||
: "";
|
||||
|
||||
return [
|
||||
`# ${article.title}`,
|
||||
"",
|
||||
article.summary,
|
||||
"",
|
||||
article.body_markdown,
|
||||
"",
|
||||
"## Image Suggestions",
|
||||
imageSuggestions,
|
||||
confirmations,
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function renderQaReportJson(report: QaReport) {
|
||||
return `${JSON.stringify(report, null, 2)}\n`;
|
||||
}
|
||||
|
||||
export async function renderOptimizedDocx(article: OptimizedArticle) {
|
||||
const document = new Document({
|
||||
sections: [
|
||||
{
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: article.title, bold: true, size: 32 })],
|
||||
}),
|
||||
new Paragraph(article.summary),
|
||||
...article.body_markdown
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => new Paragraph(line.replace(/^#+\s*/, ""))),
|
||||
new Paragraph("Image Suggestions"),
|
||||
...article.image_suggestions.map(
|
||||
(item) => new Paragraph(`${item.source}: ${item.suggestion}`),
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return Packer.toBuffer(document);
|
||||
}
|
||||
|
||||
export async function writeJobExports({
|
||||
dataDir = getAppDataDir(),
|
||||
jobId,
|
||||
article,
|
||||
qaReport,
|
||||
}: WriteJobExportsInput) {
|
||||
const exportDir = join(dataDir, "exports", jobId);
|
||||
mkdirSync(exportDir, { recursive: true });
|
||||
|
||||
const markdown = join(exportDir, "optimized.md");
|
||||
const docx = join(exportDir, "optimized.docx");
|
||||
const qaJson = join(exportDir, "qa_report.json");
|
||||
|
||||
writeFileSync(markdown, renderOptimizedMarkdown(article), "utf8");
|
||||
writeFileSync(qaJson, renderQaReportJson(qaReport), "utf8");
|
||||
writeFileSync(docx, await renderOptimizedDocx(article));
|
||||
|
||||
return { markdown, docx, qaJson };
|
||||
}
|
||||
Reference in New Issue
Block a user