feat(ai-edit): 开发 AI 填报功能

This commit is contained in:
R524809
2026-09-23 17:18:09 +08:00
parent 95a2a37f1b
commit b3958166dd
35 changed files with 2832 additions and 278 deletions
+1
View File
@@ -4,3 +4,4 @@ export * from './stores';
export * from './watermarks';
export * from './media';
export * from './product-editor';
export * from './product-ai';
+116
View File
@@ -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: '对照字段类型不正确' });
}
});
+12
View File
@@ -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),