From de539aecb06d83e0ad8f4d918d38043d817cccfd Mon Sep 17 00:00:00 2001 From: czj <13261895355@163.com> Date: Wed, 8 Jul 2026 13:08:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BC=98=E5=8C=96=E6=A1=88?= =?UTF-8?q?=E4=BE=8B=E9=A2=86=E5=9F=9F=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/cases/__tests__/validation.test.ts | 55 +++++++++ src/lib/cases/types.ts | 117 ++++++++++++++++++++ src/lib/cases/validation.ts | 62 +++++++++++ src/lib/domain/__tests__/validation.test.ts | 16 +++ src/lib/domain/types.ts | 1 + src/lib/domain/validation.ts | 7 ++ 6 files changed, 258 insertions(+) create mode 100644 src/lib/cases/__tests__/validation.test.ts create mode 100644 src/lib/cases/types.ts create mode 100644 src/lib/cases/validation.ts diff --git a/src/lib/cases/__tests__/validation.test.ts b/src/lib/cases/__tests__/validation.test.ts new file mode 100644 index 0000000..ec74046 --- /dev/null +++ b/src/lib/cases/__tests__/validation.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; + +import { + caseListFiltersSchema, + caseMetadataPatchSchema, + caseTypeSchema, + resultVersionStatusSchema, +} from "../validation"; + +describe("case validation", () => { + it("accepts the supported case types and result-version statuses", () => { + expect(caseTypeSchema.parse("article")).toBe("article"); + expect(caseTypeSchema.parse("human_copy")).toBe("human_copy"); + expect(resultVersionStatusSchema.parse("optimized")).toBe("optimized"); + expect(resultVersionStatusSchema.parse("failed")).toBe("failed"); + }); + + it("keeps optional ownership metadata empty and editable", () => { + expect(caseMetadataPatchSchema.parse({})).toEqual({}); + expect( + caseMetadataPatchSchema.parse({ + customer_name: "伟思德鲁", + brand_name: "IPMS", + project_tags: ["推荐榜单", "2026"], + notes: "客户偏好保留专业语气。", + }), + ).toEqual({ + customer_name: "伟思德鲁", + brand_name: "IPMS", + project_tags: ["推荐榜单", "2026"], + notes: "客户偏好保留专业语气。", + }); + }); + + it("parses default list filters without archived cases", () => { + expect(caseListFiltersSchema.parse({})).toEqual({ + include_archived: false, + }); + expect( + caseListFiltersSchema.parse({ + case_type: "human_copy", + status: "optimized", + project_tag: "朋友圈", + q: "自然表达", + include_archived: "true", + }), + ).toMatchObject({ + case_type: "human_copy", + status: "optimized", + project_tag: "朋友圈", + q: "自然表达", + include_archived: true, + }); + }); +}); diff --git a/src/lib/cases/types.ts b/src/lib/cases/types.ts new file mode 100644 index 0000000..a28860c --- /dev/null +++ b/src/lib/cases/types.ts @@ -0,0 +1,117 @@ +import type { + CopyOptimizationRequest, + CopyOptimizationResult, + OptimizedArticle, + QaReport, +} from "../domain/types"; + +export type OptimizationCaseType = "article" | "human_copy"; +export type OptimizationCaseStatus = + | "running" + | "optimized" + | "failed" + | "archived"; +export type ResultVersionStatus = "optimized" | "failed"; + +export interface ProcessSummaryStep { + stage: string; + started_at: string; + ended_at: string; + duration_ms: number; + status: "success" | "failed"; + error_summary?: string; + rewrite_round?: number; + produced_result_version: boolean; +} + +export interface OptimizationCase { + id: string; + case_type: OptimizationCaseType; + title: string; + summary: string; + status: OptimizationCaseStatus; + customer_name: string; + brand_name: string; + project_tags: string[]; + notes: string; + publish_target: string; + source_excerpt: string; + result_excerpt: string; + latest_result_version_id: string | null; + latest_version_number: number | null; + last_error_stage: string | null; + last_error_summary: string | null; + archived_at: string | null; + created_at: string; + updated_at: string; +} + +export interface CaseInput { + case_id: string; + case_type: OptimizationCaseType; + article_job_id: string | null; + payload: ArticleCaseInputPayload | HumanCopyCaseInputPayload; + created_at: string; + updated_at: string; +} + +export interface ArticleCaseInputPayload { + source_title: string; + source_body: string; + image_inputs: Array<{ type: "description" | "link"; content: string }>; + publish_platform: string; + user_instructions: string; + fact_card?: unknown; +} + +export interface HumanCopyCaseInputPayload extends CopyOptimizationRequest {} + +export interface ArticleResultVersionPayload { + article: OptimizedArticle; + qa_report: QaReport; + export_paths: Record; +} + +export interface HumanCopyResultVersionPayload extends CopyOptimizationResult {} + +export interface OptimizationResultVersion { + id: string; + case_id: string; + case_type: OptimizationCaseType; + version: number; + status: ResultVersionStatus; + article_job_id: string | null; + article_revision: number | null; + result_summary: string; + payload: ArticleResultVersionPayload | HumanCopyResultVersionPayload | null; + process_summary: ProcessSummaryStep[]; + llm_audit_summary: unknown[]; + error_stage: string | null; + error_summary: string | null; + created_at: string; +} + +export interface OptimizationCaseDetail { + case: OptimizationCase; + input: CaseInput | null; + versions: OptimizationResultVersion[]; +} + +export interface CaseListFilters { + q?: string; + case_type?: OptimizationCaseType; + status?: OptimizationCaseStatus; + publish_target?: string; + project_tag?: string; + created_from?: string; + created_to?: string; + include_archived: boolean; +} + +export interface CaseMetadataPatch { + title?: string; + customer_name?: string; + brand_name?: string; + project_tags?: string[]; + notes?: string; +} diff --git a/src/lib/cases/validation.ts b/src/lib/cases/validation.ts new file mode 100644 index 0000000..39d0705 --- /dev/null +++ b/src/lib/cases/validation.ts @@ -0,0 +1,62 @@ +import { z } from "zod"; + +import type { + CaseListFilters, + CaseMetadataPatch, + OptimizationCaseStatus, + OptimizationCaseType, + ResultVersionStatus, +} from "./types"; + +export const caseTypeSchema = z.enum([ + "article", + "human_copy", +]) satisfies z.ZodType; + +export const caseStatusSchema = z.enum([ + "running", + "optimized", + "failed", + "archived", +]) satisfies z.ZodType; + +export const resultVersionStatusSchema = z.enum([ + "optimized", + "failed", +]) satisfies z.ZodType; + +const optionalTrimmedText = z + .preprocess((value) => (value == null ? undefined : value), z.string().trim()) + .optional(); + +function booleanQuery(value: unknown) { + if (value === true || value === "true") return true; + if (value === false || value === "false") return false; + return value; +} + +export const caseMetadataPatchSchema = z.object({ + title: optionalTrimmedText, + customer_name: optionalTrimmedText, + brand_name: optionalTrimmedText, + project_tags: z.array(z.string().trim().min(1)).optional(), + notes: optionalTrimmedText, +}) satisfies z.ZodType; + +export const caseListFiltersSchema = z.object({ + q: optionalTrimmedText, + case_type: caseTypeSchema.optional(), + status: caseStatusSchema.optional(), + publish_target: optionalTrimmedText, + project_tag: optionalTrimmedText, + created_from: optionalTrimmedText, + created_to: optionalTrimmedText, + include_archived: z.preprocess(booleanQuery, z.boolean().default(false)), +}) satisfies z.ZodType; + +export const resultVersionPublicationInputSchema = z.object({ + publish_target: z.string().trim().min(1), + url: z.string().trim().url(), + published_at: z.string().datetime(), + notes: z.string().trim().default(""), +}); diff --git a/src/lib/domain/__tests__/validation.test.ts b/src/lib/domain/__tests__/validation.test.ts index 9a12cb9..c645177 100644 --- a/src/lib/domain/__tests__/validation.test.ts +++ b/src/lib/domain/__tests__/validation.test.ts @@ -66,6 +66,22 @@ describe("domain validation", () => { goal: "保留原意,减少 AI 味", intensity: "light", user_instructions: "保留口语感", + publish_target: "未指定", + }); + }); + + it("validates human-copy publish target", () => { + expect( + copyOptimizationRequestSchema.parse({ + source_text: "这是一段普通文案。", + goal: "", + intensity: "light", + user_instructions: "", + publish_target: "朋友圈", + }), + ).toMatchObject({ + goal: "保留原意,减少 AI 味", + publish_target: "朋友圈", }); }); diff --git a/src/lib/domain/types.ts b/src/lib/domain/types.ts index c56f1a1..1c91b76 100644 --- a/src/lib/domain/types.ts +++ b/src/lib/domain/types.ts @@ -106,6 +106,7 @@ export interface CopyOptimizationRequest { goal: string; intensity: CopyOptimizationIntensity; user_instructions: string; + publish_target: string; } export interface CopyChangeNote { diff --git a/src/lib/domain/validation.ts b/src/lib/domain/validation.ts index 3fca694..c4198b1 100644 --- a/src/lib/domain/validation.ts +++ b/src/lib/domain/validation.ts @@ -453,6 +453,13 @@ export const copyOptimizationRequestSchema = z.object({ .default("保留原意,减少 AI 味"), intensity: copyOptimizationIntensitySchema.default("light"), user_instructions: z.string().trim().default(""), + publish_target: z + .preprocess((value) => { + if (typeof value !== "string") return value; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : "未指定"; + }, z.string().trim().min(1)) + .default("未指定"), }) satisfies z.ZodType; const copyChangeNoteSchema = z.object({