feat(ai-edit): 开发 AI 填报功能
This commit is contained in:
@@ -4,3 +4,4 @@ export * from './stores';
|
||||
export * from './watermarks';
|
||||
export * from './media';
|
||||
export * from './product-editor';
|
||||
export * from './product-ai';
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export type ProductCopyField = 'title' | 'tags' | 'description';
|
||||
export type ProductCopyTranslation = {
|
||||
variant_id: string | null;
|
||||
field: ProductCopyField;
|
||||
russian_value: string | string[];
|
||||
chinese_value: string | string[];
|
||||
source_task_id: string | null;
|
||||
};
|
||||
|
||||
export type AiCopyOutput = {
|
||||
title?: { ru: string; zh: string };
|
||||
tags?: Array<{ ru: string; zh: string }>;
|
||||
description?: { ru: string; zh: string };
|
||||
};
|
||||
|
||||
export type ProductAiAutofillResult = {
|
||||
category?: { category_id: string; path: string };
|
||||
category_candidates?: Array<{ category_id: string; path: string }>;
|
||||
model?: string;
|
||||
offers?: Array<{ variant_id: string; offer_id: string }>;
|
||||
package_spec?: {
|
||||
length_mm: string;
|
||||
width_mm: string;
|
||||
height_mm: string;
|
||||
shipping_weight_g: string;
|
||||
evidence: string;
|
||||
};
|
||||
spu_copy?: AiCopyOutput;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
export type ProductAiSkuCopyResult = {
|
||||
variant_id: string;
|
||||
copy: AiCopyOutput;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 仅兼容已完成的历史属性填报任务;应用不再创建或执行此类任务。
|
||||
*/
|
||||
export type ProductAiAttributeSuggestion = {
|
||||
scope: 'common' | 'variant';
|
||||
variant_id: string | null;
|
||||
platform_attribute_id: string;
|
||||
complex_id: string;
|
||||
values: string[];
|
||||
evidence: string;
|
||||
};
|
||||
|
||||
export type ProductAiAttributeFillResult = {
|
||||
attributes: ProductAiAttributeSuggestion[];
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
export type ProductAiTask = {
|
||||
id: string;
|
||||
/** `attribute_fill` 仅用于读取历史任务记录。 */
|
||||
kind: 'autofill' | 'sku_copy' | 'attribute_fill';
|
||||
status: 'queued' | 'running' | 'succeeded' | 'partial' | 'failed';
|
||||
input_revision: {
|
||||
product: number;
|
||||
profile: number;
|
||||
source_snapshot: number;
|
||||
};
|
||||
variant_id: string | null;
|
||||
result:
|
||||
| ProductAiAutofillResult
|
||||
| ProductAiSkuCopyResult
|
||||
| ProductAiAttributeFillResult
|
||||
| null;
|
||||
error_code: string | null;
|
||||
error_message: string | null;
|
||||
};
|
||||
|
||||
export const requestProductAiTaskSchema = z
|
||||
.object({
|
||||
hint: z.string().trim().max(1000).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const productCopyTranslationSchema = z
|
||||
.object({
|
||||
variant_id: z.uuid().nullable(),
|
||||
field: z.enum(['title', 'tags', 'description']),
|
||||
russian_value: z.union([
|
||||
z.string().max(100_000),
|
||||
z.array(z.string().max(100)).max(30),
|
||||
]),
|
||||
chinese_value: z.union([
|
||||
z.string().max(100_000),
|
||||
z.array(z.string().max(100)).max(30),
|
||||
]),
|
||||
source_task_id: z.uuid().nullable(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, context) => {
|
||||
if (
|
||||
typeof value.russian_value !== typeof value.chinese_value ||
|
||||
Array.isArray(value.russian_value) !==
|
||||
Array.isArray(value.chinese_value)
|
||||
) {
|
||||
context.addIssue({ code: 'custom', message: '中俄对照结构不一致' });
|
||||
}
|
||||
if (
|
||||
Array.isArray(value.russian_value) &&
|
||||
Array.isArray(value.chinese_value) &&
|
||||
value.russian_value.length !== value.chinese_value.length
|
||||
) {
|
||||
context.addIssue({ code: 'custom', message: '中俄标签数量不一致' });
|
||||
}
|
||||
if ((value.field === 'tags') !== Array.isArray(value.russian_value)) {
|
||||
context.addIssue({ code: 'custom', message: '对照字段类型不正确' });
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
productCopyTranslationSchema,
|
||||
type ProductCopyTranslation,
|
||||
} from './product-ai';
|
||||
|
||||
export type ProductAiSourceAttribute = {
|
||||
key: string;
|
||||
@@ -227,6 +231,8 @@ export type ProductEditorSnapshot = {
|
||||
display_name: string | null;
|
||||
sort_order: number;
|
||||
purchase_cost_cny: string | null;
|
||||
/** SKU 发布标题覆盖;空值继承当前平台 SPU 标题。 */
|
||||
title: string | null;
|
||||
/** 当前平台下该 SKU 对 SPU 主题标签的覆盖;空数组表示继承 SPU。 */
|
||||
tags: string[];
|
||||
/** 当前平台下该 SKU 对 SPU 产品描述的覆盖;null 表示继承 SPU。 */
|
||||
@@ -282,6 +288,7 @@ export type ProductEditorSnapshot = {
|
||||
source_material_state: ProductAiSourceMaterialState;
|
||||
captured_category_match: CapturedCategoryMatch;
|
||||
ai_input: ProductAiInput;
|
||||
copy_translations: ProductCopyTranslation[];
|
||||
warnings: Array<{ code: string; message: string }>;
|
||||
};
|
||||
|
||||
@@ -510,6 +517,10 @@ export const saveProductEditorDraftRequestSchema = z
|
||||
})
|
||||
.strict(),
|
||||
attributes: z.array(productAttributeDraft).max(1_000).optional(),
|
||||
copy_translations: z
|
||||
.array(productCopyTranslationSchema)
|
||||
.max(1_000)
|
||||
.optional(),
|
||||
variants: z
|
||||
.array(
|
||||
z
|
||||
@@ -531,6 +542,7 @@ export const saveProductEditorDraftRequestSchema = z
|
||||
.string()
|
||||
.regex(/^\d+(\.\d{1,4})?$/)
|
||||
.nullable(),
|
||||
title: nullableText(1_000).optional(),
|
||||
tags: z
|
||||
.array(z.string().trim().min(1).max(100))
|
||||
.max(30),
|
||||
|
||||
Reference in New Issue
Block a user