新增优化案例领域模型
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 味",
|
goal: "保留原意,减少 AI 味",
|
||||||
intensity: "light",
|
intensity: "light",
|
||||||
user_instructions: "保留口语感",
|
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;
|
goal: string;
|
||||||
intensity: CopyOptimizationIntensity;
|
intensity: CopyOptimizationIntensity;
|
||||||
user_instructions: string;
|
user_instructions: string;
|
||||||
|
publish_target: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopyChangeNote {
|
export interface CopyChangeNote {
|
||||||
|
|||||||
@@ -453,6 +453,13 @@ export const copyOptimizationRequestSchema = z.object({
|
|||||||
.default("保留原意,减少 AI 味"),
|
.default("保留原意,减少 AI 味"),
|
||||||
intensity: copyOptimizationIntensitySchema.default("light"),
|
intensity: copyOptimizationIntensitySchema.default("light"),
|
||||||
user_instructions: z.string().trim().default(""),
|
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>;
|
}) satisfies z.ZodType<CopyOptimizationRequest>;
|
||||||
|
|
||||||
const copyChangeNoteSchema = z.object({
|
const copyChangeNoteSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user