From 382f46826724b7a9c76e131cf524acd4f6406e22 Mon Sep 17 00:00:00 2001 From: czj <13261895355@163.com> Date: Wed, 1 Jul 2026 13:08:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BC=98=E5=8C=96=E6=B5=81?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/__tests__/stream-events.test.ts | 42 ++++++++++++ src/lib/workflow/stream-events.ts | 67 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/lib/workflow/__tests__/stream-events.test.ts create mode 100644 src/lib/workflow/stream-events.ts diff --git a/src/lib/workflow/__tests__/stream-events.test.ts b/src/lib/workflow/__tests__/stream-events.test.ts new file mode 100644 index 0000000..4074ddd --- /dev/null +++ b/src/lib/workflow/__tests__/stream-events.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; + +import { + encodeOptimizationStreamEvent, + parseOptimizationStreamChunk, + type OptimizationStreamEvent, +} from "../stream-events"; + +describe("optimization stream events", () => { + it("encodes each event as one JSON line", () => { + const event: OptimizationStreamEvent = { + type: "draft_started", + job_id: "job_123", + message: "正在生成优化草稿", + }; + + expect(encodeOptimizationStreamEvent(event)).toBe( + '{"type":"draft_started","job_id":"job_123","message":"正在生成优化草稿"}\n', + ); + }); + + it("parses chunked NDJSON while preserving incomplete lines", () => { + const first = parseOptimizationStreamChunk( + "", + '{"type":"job_created","job":{"id":"job_', + ); + + expect(first.events).toEqual([]); + expect(first.remainder).toBe('{"type":"job_created","job":{"id":"job_'); + + const second = parseOptimizationStreamChunk( + first.remainder, + '123"}}\n{"type":"draft_started","job_id":"job_123","message":"正在生成"}\n{"type":"qa_started"', + ); + + expect(second.events).toEqual([ + { type: "job_created", job: { id: "job_123" } }, + { type: "draft_started", job_id: "job_123", message: "正在生成" }, + ]); + expect(second.remainder).toBe('{"type":"qa_started"'); + }); +}); diff --git a/src/lib/workflow/stream-events.ts b/src/lib/workflow/stream-events.ts new file mode 100644 index 0000000..327d50c --- /dev/null +++ b/src/lib/workflow/stream-events.ts @@ -0,0 +1,67 @@ +import type { + OptimizationFactCard, + OptimizedArticle, + QaReport, +} from "../domain/types"; + +export type OptimizationStreamStage = + | "input" + | "job" + | "fact_card" + | "draft" + | "qa" + | "rewrite" + | "final"; + +export type OptimizationStreamEvent = + | { type: "job_created"; job: { id: string } } + | { + type: "fact_card_ready"; + job_id: string; + fact_card: OptimizationFactCard; + } + | { type: "draft_started"; job_id: string; message: string } + | { type: "draft_ready"; job_id: string; article: OptimizedArticle } + | { type: "qa_started"; job_id: string; message: string } + | { type: "qa_ready"; job_id: string; qa_report: QaReport } + | { type: "rewrite_started"; job_id: string; round: number } + | { + type: "rewrite_ready"; + job_id: string; + round: number; + article: OptimizedArticle; + } + | { + type: "final_ready"; + job_id: string; + optimized_article: OptimizedArticle; + qa_report: QaReport; + export_paths: Record; + } + | { + type: "failed"; + job_id?: string; + stage: OptimizationStreamStage; + error: string; + }; + +export function encodeOptimizationStreamEvent( + event: OptimizationStreamEvent, +) { + return `${JSON.stringify(event)}\n`; +} + +export function parseOptimizationStreamChunk( + previousRemainder: string, + chunk: string, +) { + const text = previousRemainder + chunk; + const lines = text.split(/\n/); + const remainder = lines.pop() ?? ""; + const events = lines + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => JSON.parse(line) as OptimizationStreamEvent); + + return { events, remainder }; +}