新增优化案例领域模型
This commit is contained in:
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: "朋友圈",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ export interface CopyOptimizationRequest {
|
||||
goal: string;
|
||||
intensity: CopyOptimizationIntensity;
|
||||
user_instructions: string;
|
||||
publish_target: string;
|
||||
}
|
||||
|
||||
export interface CopyChangeNote {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user