新增优化案例领域模型

This commit is contained in:
czj
2026-07-08 13:08:12 +08:00
parent 8b10969c49
commit de539aecb0
6 changed files with 258 additions and 0 deletions
@@ -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,
});
});
});
+117
View File
@@ -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<string, string>;
}
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;
}
+62
View File
@@ -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<OptimizationCaseType>;
export const caseStatusSchema = z.enum([
"running",
"optimized",
"failed",
"archived",
]) satisfies z.ZodType<OptimizationCaseStatus>;
export const resultVersionStatusSchema = z.enum([
"optimized",
"failed",
]) satisfies z.ZodType<ResultVersionStatus>;
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<CaseMetadataPatch>;
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<CaseListFilters>;
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(""),
});
@@ -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: "朋友圈",
});
});
+1
View File
@@ -106,6 +106,7 @@ export interface CopyOptimizationRequest {
goal: string;
intensity: CopyOptimizationIntensity;
user_instructions: string;
publish_target: string;
}
export interface CopyChangeNote {
+7
View File
@@ -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<CopyOptimizationRequest>;
const copyChangeNoteSchema = z.object({