feat: extend workflow brand hierarchy to five levels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-02 13:59:33 +08:00
parent 7eda4720d3
commit ea6a9a9d76
13 changed files with 1298 additions and 64 deletions

View File

@ -92,11 +92,13 @@ import type {
AgentPresetLibrarySection,
AgentWorkflowBrandModelConfig,
AgentWorkflowBrandStoreEntry,
AgentWorkflowFourthCategoryEntry,
AgentWorkflowGoodsInfoApiConfig,
AgentWorkflowMaterialApiConfig,
AgentWorkflowOptionUiConfig,
AgentPsdTextColorConfig,
AgentWorkflowStoreEntry,
AgentWorkflowThirdCategoryEntry,
AgentWorkflowShowcaseConfig,
AgentWorkflowStepUiConfig,
AgentWorkflowUiConfig,
@ -465,15 +467,86 @@ function buildWorkflowInputOptionsFromBrandEntries(
return;
}
const uniqueThirdCategoryEntries = new Map<string, AgentWorkflowThirdCategoryEntry>();
(storeEntry.thirdCategoryEntries || []).forEach((thirdEntry, thirdIndex) => {
const categoryName = String(thirdEntry.categoryName || "").trim();
if (!categoryName) {
return;
}
const uniqueFourthCategoryEntries = new Map<string, AgentWorkflowFourthCategoryEntry>();
(thirdEntry.fourthCategoryEntries || []).forEach((fourthEntry, fourthIndex) => {
const fourthCategoryName = String(fourthEntry.categoryName || "").trim();
if (!fourthCategoryName) {
return;
}
uniqueFourthCategoryEntries.set(fourthCategoryName.toLowerCase(), {
categoryName: fourthCategoryName,
fifthCategories: Array.from(
new Set(
(fourthEntry.fifthCategories || [])
.map((fifthCategory) => fifthCategory.trim())
.filter(Boolean)
)
),
sortOrder: fourthIndex,
});
});
const fourthCategoryEntries = Array.from(uniqueFourthCategoryEntries.values()).map(
(fourthEntry, fourthIndex) => ({
...fourthEntry,
sortOrder: fourthIndex,
})
);
uniqueThirdCategoryEntries.set(categoryName.toLowerCase(), {
categoryName,
fourthCategories:
fourthCategoryEntries.length > 0
? fourthCategoryEntries.map((fourthEntry) => fourthEntry.categoryName)
: Array.from(
new Set(
(thirdEntry.fourthCategories || [])
.map((fourthCategory) => fourthCategory.trim())
.filter(Boolean)
)
),
fourthCategoryEntries,
sortOrder: thirdIndex,
});
});
const thirdCategoryEntries = Array.from(uniqueThirdCategoryEntries.values()).map((thirdEntry, thirdIndex) => ({
...thirdEntry,
fourthCategories:
(thirdEntry.fourthCategoryEntries || []).length > 0
? (thirdEntry.fourthCategoryEntries || []).map((fourthEntry) => fourthEntry.categoryName)
: Array.from(
new Set(
(thirdEntry.fourthCategories || [])
.map((fourthCategory) => fourthCategory.trim())
.filter(Boolean)
)
),
sortOrder: thirdIndex,
}));
uniqueStoreEntries.set(storeName.toLowerCase(), {
storeName,
thirdCategories: Array.from(
new Set(
(storeEntry.thirdCategories || [])
.map((thirdCategory) => thirdCategory.trim())
.filter(Boolean)
)
),
thirdCategories:
thirdCategoryEntries.length > 0
? thirdCategoryEntries.map((thirdEntry) => thirdEntry.categoryName)
: Array.from(
new Set(
(storeEntry.thirdCategories || [])
.map((thirdCategory) => thirdCategory.trim())
.filter(Boolean)
)
),
thirdCategoryEntries,
sortOrder: storeIndex,
});
});
@ -513,10 +586,51 @@ function buildWorkflowInputOptionsFromBrandEntries(
)
)
),
fourthCategories: Array.from(
new Set(
normalizedEntries.flatMap((entry) =>
entry.storeEntries.flatMap((storeEntry) =>
(storeEntry.thirdCategoryEntries || []).flatMap(
(thirdEntry) => thirdEntry.fourthCategories || []
)
)
)
)
),
fifthCategories: Array.from(
new Set(
normalizedEntries.flatMap((entry) =>
entry.storeEntries.flatMap((storeEntry) =>
(storeEntry.thirdCategoryEntries || []).flatMap((thirdEntry) =>
(thirdEntry.fourthCategoryEntries || []).flatMap(
(fourthEntry) => fourthEntry.fifthCategories || []
)
)
)
)
)
),
priceBands: normalizedPriceBands,
};
}
function createStoreDraftKey(brandName: string, storeName: string) {
return `${brandName}::${storeName}`;
}
function createThirdCategoryDraftKey(brandName: string, storeName: string, thirdCategory: string) {
return `${brandName}::${storeName}::${thirdCategory}`;
}
function createFourthCategoryDraftKey(
brandName: string,
storeName: string,
thirdCategory: string,
fourthCategory: string
) {
return `${brandName}::${storeName}::${thirdCategory}::${fourthCategory}`;
}
interface AgentWorkflowSettingsClientProps {
backHref?: string;
backLabel?: string;
@ -573,6 +687,8 @@ export function AgentWorkflowSettingsClient({
const [priceBandDraft, setPriceBandDraft] = useState("");
const [storeDrafts, setStoreDrafts] = useState<Record<string, string>>({});
const [thirdCategoryDrafts, setThirdCategoryDrafts] = useState<Record<string, string>>({});
const [fourthCategoryDrafts, setFourthCategoryDrafts] = useState<Record<string, string>>({});
const [fifthCategoryDrafts, setFifthCategoryDrafts] = useState<Record<string, string>>({});
const [blockedOptionSearches, setBlockedOptionSearches] = useState<Record<string, string>>({});
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
@ -717,6 +833,24 @@ export function AgentWorkflowSettingsClient({
});
return next;
});
setFourthCategoryDrafts((prev) => {
const next = { ...prev };
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::`)) {
delete next[key];
}
});
return next;
});
setFifthCategoryDrafts((prev) => {
const next = { ...prev };
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::`)) {
delete next[key];
}
});
return next;
});
};
const handleAddStoreToBrand = (brandName: string) => {
@ -752,6 +886,7 @@ export function AgentWorkflowSettingsClient({
{
storeName: nextValue,
thirdCategories: [],
thirdCategoryEntries: [],
sortOrder: (entry.storeEntries || []).length,
},
],
@ -780,13 +915,35 @@ export function AgentWorkflowSettingsClient({
);
setThirdCategoryDrafts((prev) => {
const next = { ...prev };
delete next[`${brandName}::${storeName}`];
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::${storeName}`)) {
delete next[key];
}
});
return next;
});
setFourthCategoryDrafts((prev) => {
const next = { ...prev };
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::${storeName}`)) {
delete next[key];
}
});
return next;
});
setFifthCategoryDrafts((prev) => {
const next = { ...prev };
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::${storeName}`)) {
delete next[key];
}
});
return next;
});
};
const handleAddThirdCategoryToStore = (brandName: string, storeName: string) => {
const draftKey = `${brandName}::${storeName}`;
const draftKey = createStoreDraftKey(brandName, storeName);
const nextValue = normalizeInputOptionValue(thirdCategoryDrafts[draftKey] || "");
if (!nextValue) {
toast.error("请输入三级分类名称");
@ -798,6 +955,9 @@ export function AgentWorkflowSettingsClient({
(storeEntry) => storeEntry.storeName === storeName
);
if (
(targetStoreEntry?.thirdCategoryEntries || []).some(
(thirdEntry) => thirdEntry.categoryName.toLowerCase() === nextValue.toLowerCase()
) ||
targetStoreEntry?.thirdCategories.some(
(thirdCategory) => thirdCategory.toLowerCase() === nextValue.toLowerCase()
)
@ -816,6 +976,18 @@ export function AgentWorkflowSettingsClient({
? {
...storeEntry,
thirdCategories: [...(storeEntry.thirdCategories || []), nextValue],
thirdCategoryEntries: [
...((storeEntry.thirdCategoryEntries || []).map((thirdEntry, thirdIndex) => ({
...thirdEntry,
sortOrder: thirdIndex,
})) || []),
{
categoryName: nextValue,
fourthCategories: [],
fourthCategoryEntries: [],
sortOrder: (storeEntry.thirdCategoryEntries || []).length,
},
],
}
: storeEntry
),
@ -845,6 +1017,259 @@ export function AgentWorkflowSettingsClient({
thirdCategories: (storeEntry.thirdCategories || []).filter(
(item) => item !== thirdCategory
),
thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).filter(
(item) => item.categoryName !== thirdCategory
),
}
: storeEntry
),
};
})
);
setFourthCategoryDrafts((prev) => {
const next = { ...prev };
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::${storeName}::${thirdCategory}`)) {
delete next[key];
}
});
return next;
});
setFifthCategoryDrafts((prev) => {
const next = { ...prev };
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::${storeName}::${thirdCategory}`)) {
delete next[key];
}
});
return next;
});
};
const handleAddFourthCategoryToThirdCategory = (
brandName: string,
storeName: string,
thirdCategory: string
) => {
const draftKey = createThirdCategoryDraftKey(brandName, storeName, thirdCategory);
const nextValue = normalizeInputOptionValue(fourthCategoryDrafts[draftKey] || "");
if (!nextValue) {
toast.error("请输入四级分类名称");
return;
}
const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName);
const targetStoreEntry = targetEntry?.storeEntries?.find(
(storeEntry) => storeEntry.storeName === storeName
);
const targetThirdEntry = targetStoreEntry?.thirdCategoryEntries?.find(
(thirdEntry) => thirdEntry.categoryName === thirdCategory
);
if (
(targetThirdEntry?.fourthCategoryEntries || []).some(
(fourthEntry) => fourthEntry.categoryName.toLowerCase() === nextValue.toLowerCase()
) ||
(targetThirdEntry?.fourthCategories || []).some(
(fourthCategory) => fourthCategory.toLowerCase() === nextValue.toLowerCase()
)
) {
toast.error("该四级分类已存在");
return;
}
updateInputOptions(
brandStoreEntries.map((entry) => {
if (entry.brandName !== brandName) return entry;
return {
...entry,
storeEntries: (entry.storeEntries || []).map((storeEntry) =>
storeEntry.storeName === storeName
? {
...storeEntry,
thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) =>
thirdEntry.categoryName === thirdCategory
? {
...thirdEntry,
fourthCategories: [...(thirdEntry.fourthCategories || []), nextValue],
fourthCategoryEntries: [
...((thirdEntry.fourthCategoryEntries || []).map((fourthEntry, fourthIndex) => ({
...fourthEntry,
sortOrder: fourthIndex,
})) || []),
{
categoryName: nextValue,
fifthCategories: [],
sortOrder: (thirdEntry.fourthCategoryEntries || []).length,
},
],
}
: thirdEntry
),
}
: storeEntry
),
};
})
);
setFourthCategoryDrafts((prev) => ({
...prev,
[draftKey]: "",
}));
};
const handleRemoveFourthCategoryFromThirdCategory = (
brandName: string,
storeName: string,
thirdCategory: string,
fourthCategory: string
) => {
updateInputOptions(
brandStoreEntries.map((entry) => {
if (entry.brandName !== brandName) return entry;
return {
...entry,
storeEntries: (entry.storeEntries || []).map((storeEntry) =>
storeEntry.storeName === storeName
? {
...storeEntry,
thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) =>
thirdEntry.categoryName === thirdCategory
? {
...thirdEntry,
fourthCategories: (thirdEntry.fourthCategories || []).filter(
(item) => item !== fourthCategory
),
fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).filter(
(item) => item.categoryName !== fourthCategory
),
}
: thirdEntry
),
}
: storeEntry
),
};
})
);
setFifthCategoryDrafts((prev) => {
const next = { ...prev };
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::${storeName}::${thirdCategory}::${fourthCategory}`)) {
delete next[key];
}
});
return next;
});
};
const handleAddFifthCategoryToFourthCategory = (
brandName: string,
storeName: string,
thirdCategory: string,
fourthCategory: string
) => {
const draftKey = createFourthCategoryDraftKey(
brandName,
storeName,
thirdCategory,
fourthCategory
);
const nextValue = normalizeInputOptionValue(fifthCategoryDrafts[draftKey] || "");
if (!nextValue) {
toast.error("请输入五级分类名称");
return;
}
const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName);
const targetStoreEntry = targetEntry?.storeEntries?.find(
(storeEntry) => storeEntry.storeName === storeName
);
const targetThirdEntry = targetStoreEntry?.thirdCategoryEntries?.find(
(thirdEntry) => thirdEntry.categoryName === thirdCategory
);
const targetFourthEntry = targetThirdEntry?.fourthCategoryEntries?.find(
(fourthEntry) => fourthEntry.categoryName === fourthCategory
);
if (
targetFourthEntry?.fifthCategories.some(
(fifthCategory) => fifthCategory.toLowerCase() === nextValue.toLowerCase()
)
) {
toast.error("该五级分类已存在");
return;
}
updateInputOptions(
brandStoreEntries.map((entry) => {
if (entry.brandName !== brandName) return entry;
return {
...entry,
storeEntries: (entry.storeEntries || []).map((storeEntry) =>
storeEntry.storeName === storeName
? {
...storeEntry,
thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) =>
thirdEntry.categoryName === thirdCategory
? {
...thirdEntry,
fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).map(
(fourthEntry) =>
fourthEntry.categoryName === fourthCategory
? {
...fourthEntry,
fifthCategories: [...(fourthEntry.fifthCategories || []), nextValue],
}
: fourthEntry
),
}
: thirdEntry
),
}
: storeEntry
),
};
})
);
setFifthCategoryDrafts((prev) => ({
...prev,
[draftKey]: "",
}));
};
const handleRemoveFifthCategoryFromFourthCategory = (
brandName: string,
storeName: string,
thirdCategory: string,
fourthCategory: string,
fifthCategory: string
) => {
updateInputOptions(
brandStoreEntries.map((entry) => {
if (entry.brandName !== brandName) return entry;
return {
...entry,
storeEntries: (entry.storeEntries || []).map((storeEntry) =>
storeEntry.storeName === storeName
? {
...storeEntry,
thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) =>
thirdEntry.categoryName === thirdCategory
? {
...thirdEntry,
fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).map(
(fourthEntry) =>
fourthEntry.categoryName === fourthCategory
? {
...fourthEntry,
fifthCategories: (fourthEntry.fifthCategories || []).filter(
(item) => item !== fifthCategory
),
}
: fourthEntry
),
}
: thirdEntry
),
}
: storeEntry
),
@ -2726,7 +3151,7 @@ export function AgentWorkflowSettingsClient({
</div>
<div>
<CardTitle className="text-base">基础下拉选项</CardTitle>
<CardDescription>配置品牌、店铺、三级分类的层级关系,以及第一页可选的价格带区间</CardDescription>
<CardDescription>配置品牌、店铺到五级分类的层级关系,以及第一页可选的价格带区间</CardDescription>
</div>
</div>
</CardHeader>
@ -2841,7 +3266,7 @@ export function AgentWorkflowSettingsClient({
</button>
</div>
<span className="text-xs text-muted-foreground">
{(storeEntry.thirdCategories || []).length} 个三级分类
{(storeEntry.thirdCategoryEntries || []).length} 个三级分类
</span>
</div>
</div>
@ -2883,32 +3308,214 @@ export function AgentWorkflowSettingsClient({
</Button>
</div>
{(storeEntry.thirdCategories || []).length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{(storeEntry.thirdCategories || []).map(
(thirdCategory) => (
{(storeEntry.thirdCategoryEntries || []).length > 0 ? (
<div className="mt-3 space-y-3">
{(storeEntry.thirdCategoryEntries || []).map((thirdEntry) => {
const fourthDraftKey = createThirdCategoryDraftKey(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName
);
return (
<div
key={`${entry.brandName}-${storeEntry.storeName}-${thirdCategory}`}
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm"
key={`${entry.brandName}-${storeEntry.storeName}-${thirdEntry.categoryName}`}
className="rounded-xl border bg-background/80 p-3"
>
<span>{thirdCategory}</span>
<button
type="button"
onClick={() =>
handleRemoveThirdCategoryFromStore(
entry.brandName,
storeEntry.storeName,
thirdCategory
)
}
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
aria-label={`删除三级分类 ${thirdCategory}`}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm font-medium">
<span>{thirdEntry.categoryName}</span>
<button
type="button"
onClick={() =>
handleRemoveThirdCategoryFromStore(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName
)
}
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
aria-label={`删除三级分类 ${thirdEntry.categoryName}`}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
<span className="text-xs text-muted-foreground">
{(thirdEntry.fourthCategoryEntries || []).length} 个四级分类
</span>
</div>
</div>
<div className="mt-3 rounded-xl border bg-muted/20 p-3">
<div className="flex flex-col gap-2 sm:flex-row">
<Input
value={fourthCategoryDrafts[fourthDraftKey] || ""}
onChange={(e) =>
setFourthCategoryDrafts((prev) => ({
...prev,
[fourthDraftKey]: e.target.value,
}))
}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddFourthCategoryToThirdCategory(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName
);
}
}}
placeholder={`给三级分类“${thirdEntry.categoryName}”添加四级分类`}
/>
<Button
type="button"
onClick={() =>
handleAddFourthCategoryToThirdCategory(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName
)
}
className="shrink-0"
variant="outline"
>
<Plus className="mr-2 h-4 w-4" />
添加四级分类
</Button>
</div>
{(thirdEntry.fourthCategoryEntries || []).length > 0 ? (
<div className="mt-3 space-y-3">
{(thirdEntry.fourthCategoryEntries || []).map(
(fourthEntry) => {
const fifthDraftKey = createFourthCategoryDraftKey(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName,
fourthEntry.categoryName
);
return (
<div
key={`${entry.brandName}-${storeEntry.storeName}-${thirdEntry.categoryName}-${fourthEntry.categoryName}`}
className="rounded-xl border bg-background/80 p-3"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm font-medium">
<span>{fourthEntry.categoryName}</span>
<button
type="button"
onClick={() =>
handleRemoveFourthCategoryFromThirdCategory(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName,
fourthEntry.categoryName
)
}
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
aria-label={`删除四级分类 ${fourthEntry.categoryName}`}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
<span className="text-xs text-muted-foreground">
{(fourthEntry.fifthCategories || []).length} 个五级分类
</span>
</div>
</div>
<div className="mt-3 rounded-xl border bg-muted/20 p-3">
<div className="flex flex-col gap-2 sm:flex-row">
<Input
value={fifthCategoryDrafts[fifthDraftKey] || ""}
onChange={(e) =>
setFifthCategoryDrafts((prev) => ({
...prev,
[fifthDraftKey]: e.target.value,
}))
}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddFifthCategoryToFourthCategory(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName,
fourthEntry.categoryName
);
}
}}
placeholder={`给四级分类“${fourthEntry.categoryName}”添加五级分类`}
/>
<Button
type="button"
onClick={() =>
handleAddFifthCategoryToFourthCategory(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName,
fourthEntry.categoryName
)
}
className="shrink-0"
variant="outline"
>
<Plus className="mr-2 h-4 w-4" />
添加五级分类
</Button>
</div>
{(fourthEntry.fifthCategories || []).length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{(fourthEntry.fifthCategories || []).map(
(fifthCategory) => (
<div
key={`${entry.brandName}-${storeEntry.storeName}-${thirdEntry.categoryName}-${fourthEntry.categoryName}-${fifthCategory}`}
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm"
>
<span>{fifthCategory}</span>
<button
type="button"
onClick={() =>
handleRemoveFifthCategoryFromFourthCategory(
entry.brandName,
storeEntry.storeName,
thirdEntry.categoryName,
fourthEntry.categoryName,
fifthCategory
)
}
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
aria-label={`删除五级分类 ${fifthCategory}`}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
)
)}
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
这个四级分类下还没有添加任何五级分类。
</div>
)}
</div>
</div>
);
}
)}
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
这个三级分类下还没有添加任何四级分类。
</div>
)}
</div>
</div>
)
)}
);
})}
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
@ -2931,7 +3538,7 @@ export function AgentWorkflowSettingsClient({
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺和三级分类。
还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺以及三级到五级分类。
</div>
)}
</div>

View File

@ -731,6 +731,8 @@ const updateSchema = z.object({
brands: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
stores: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
thirdCategories: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
fourthCategories: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
fifthCategories: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
priceBands: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
brandStoreEntries: z
.array(
@ -745,6 +747,32 @@ const updateSchema = z.object({
.array(z.string().trim().min(1).max(64))
.max(300)
.optional(),
thirdCategoryEntries: z
.array(
z.object({
categoryName: z.string().trim().min(1).max(64),
fourthCategories: z
.array(z.string().trim().min(1).max(64))
.max(300)
.optional(),
fourthCategoryEntries: z
.array(
z.object({
categoryName: z.string().trim().min(1).max(64),
fifthCategories: z
.array(z.string().trim().min(1).max(64))
.max(300)
.optional(),
sortOrder: z.number().min(0).max(999).optional(),
})
)
.max(300)
.optional(),
sortOrder: z.number().min(0).max(999).optional(),
})
)
.max(300)
.optional(),
sortOrder: z.number().min(0).max(999).optional(),
})
)

View File

@ -153,6 +153,8 @@ function hasMeaningfulWorkflowState(
String(inputs?.brand || "").trim() ||
String(inputs?.store || "").trim() ||
String(inputs?.thirdCategory || "").trim() ||
String(inputs?.fourthCategory || "").trim() ||
String(inputs?.fifthCategory || "").trim() ||
String(inputs?.priceBand || "").trim()
);
@ -322,6 +324,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
brand: "",
store: "",
thirdCategory: "",
fourthCategory: "",
fifthCategory: "",
priceBand: "",
});
const [workflowMaterials, setWorkflowMaterials] = useState<AgentWorkflowMaterialSelections>(
@ -489,6 +493,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
brand: workflowInputs.brand.trim(),
store: workflowInputs.store.trim(),
thirdCategory: workflowInputs.thirdCategory.trim(),
fourthCategory: workflowInputs.fourthCategory.trim(),
fifthCategory: workflowInputs.fifthCategory.trim(),
priceBand: workflowInputs.priceBand.trim(),
},
materials: workflowMaterials,
@ -535,6 +541,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
brand: "",
store: "",
thirdCategory: "",
fourthCategory: "",
fifthCategory: "",
priceBand: "",
});
setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections());
@ -567,6 +575,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
brand: String(persistedWorkflow.inputs?.brand || "").trim(),
store: String(persistedWorkflow.inputs?.store || "").trim(),
thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(),
fourthCategory: String(persistedWorkflow.inputs?.fourthCategory || "").trim(),
fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(),
priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(
@ -676,6 +686,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
brand: workflowInputs.brand.trim(),
store: workflowInputs.store.trim(),
thirdCategory: workflowInputs.thirdCategory.trim(),
fourthCategory: workflowInputs.fourthCategory.trim(),
fifthCategory: workflowInputs.fifthCategory.trim(),
priceBand: workflowInputs.priceBand.trim(),
},
materials: workflowMaterials,
@ -763,6 +775,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
brand: String(parsed.inputs?.brand || "").trim(),
store: String(parsed.inputs?.store || "").trim(),
thirdCategory: String(parsed.inputs?.thirdCategory || "").trim(),
fourthCategory: String(parsed.inputs?.fourthCategory || "").trim(),
fifthCategory: String(parsed.inputs?.fifthCategory || "").trim(),
priceBand: String(parsed.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(parsed.materials));
@ -890,6 +904,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
brand: String(persistedWorkflow.inputs?.brand || "").trim(),
store: String(persistedWorkflow.inputs?.store || "").trim(),
thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(),
fourthCategory: String(persistedWorkflow.inputs?.fourthCategory || "").trim(),
fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(),
priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials));

View File

@ -9,10 +9,10 @@ import {
createEmptyAgentWorkflowMaterialSelections,
normalizeAgentWorkflowMaterialSelections,
} from "@/lib/agents/workflow-material-config";
import { buildAgentWorkflowComposerContext } from "@/lib/agents/workflow-composer";
import type {
AgentWorkflowMaterialSelections,
AgentWorkflowStepKey,
AgentWorkflowComposerContext,
} from "@/types/agent";
interface AgentPageDarkProps {
@ -38,6 +38,8 @@ export function AgentPageDark({
brand: "",
store: "",
thirdCategory: "",
fourthCategory: "",
fifthCategory: "",
priceBand: "",
});
const [workflowMaterials, setWorkflowMaterials] = useState<AgentWorkflowMaterialSelections>(
@ -56,24 +58,12 @@ export function AgentPageDark({
};
// Build workflow context
const workflowContext: AgentWorkflowComposerContext | null = {
const workflowContext = buildAgentWorkflowComposerContext({
config: workflowConfig,
inputs: workflowInputs,
materials: workflowMaterials,
selections: Object.entries(workflowSelections).map(([stepKey, optionKey]) => {
const step = workflowConfig?.steps?.find((s: any) => s.key === stepKey);
const option = step?.options?.find((o: any) => o.key === optionKey);
return {
stepKey,
stepTitle: step?.title || "",
optionKey: optionKey || "",
optionName: option?.buttonName || "",
promptFragment: option?.promptFragment || "",
};
}),
referenceImages: [],
summaryText: "",
promptText: "",
};
selections: workflowSelections,
});
return (
<div className="bg-black min-h-screen">

View File

@ -11,6 +11,8 @@ import { cn } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth,
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowThirdCategoryOptionsByBrandStore,
getAgentWorkflowStoreOptionsByBrand,
@ -33,6 +35,8 @@ export interface AgentWorkflowInputs {
brand: string;
store: string;
thirdCategory: string;
fourthCategory: string;
fifthCategory: string;
priceBand: string;
}
@ -109,6 +113,27 @@ export function AgentWorkflowPanel({
}),
[config, inputs.brand, inputs.store]
);
const fourthCategoryOptions = useMemo(
() =>
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({
config,
brand: inputs.brand,
store: inputs.store,
thirdCategory: inputs.thirdCategory,
}),
[config, inputs.brand, inputs.store, inputs.thirdCategory]
);
const fifthCategoryOptions = useMemo(
() =>
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({
config,
brand: inputs.brand,
store: inputs.store,
thirdCategory: inputs.thirdCategory,
fourthCategory: inputs.fourthCategory,
}),
[config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
@ -198,6 +223,8 @@ export function AgentWorkflowPanel({
brand: "",
store: "",
thirdCategory: "",
fourthCategory: "",
fifthCategory: "",
priceBand: "",
});
onMaterialsChange({
@ -217,7 +244,7 @@ export function AgentWorkflowPanel({
<div className="space-y-5 p-4">
<section className="space-y-3">
<div className="text-sm font-medium">基础配置</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -236,6 +263,8 @@ export function AgentWorkflowPanel({
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.thirdCategory
: "",
fourthCategory: "",
fifthCategory: "",
})
}
/>
@ -254,6 +283,8 @@ export function AgentWorkflowPanel({
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
? inputs.thirdCategory
: "",
fourthCategory: "",
fifthCategory: "",
})
}
/>
@ -264,7 +295,44 @@ export function AgentWorkflowPanel({
searchPlaceholder="输入三级分类筛选"
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
disabled={!inputs.store || thirdCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
onValueChange={(value) =>
onInputsChange({
...inputs,
thirdCategory: value,
fourthCategory:
value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase()
? inputs.fourthCategory
: "",
fifthCategory: "",
})
}
/>
<SearchableSelect
value={inputs.fourthCategory}
options={fourthCategoryOptions}
placeholder="选择四级分类"
searchPlaceholder="输入四级分类筛选"
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
onValueChange={(value) =>
onInputsChange({
...inputs,
fourthCategory: value,
fifthCategory:
value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase()
? inputs.fifthCategory
: "",
})
}
/>
<SearchableSelect
value={inputs.fifthCategory}
options={fifthCategoryOptions}
placeholder="选择五级分类"
searchPlaceholder="输入五级分类筛选"
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
/>
<SearchableSelect
value={inputs.priceBand}
@ -357,6 +425,8 @@ export function AgentWorkflowPanel({
inputs.brand ||
inputs.store ||
inputs.thirdCategory ||
inputs.fourthCategory ||
inputs.fifthCategory ||
inputs.priceBand ||
materials.primary.selectedImageIds.length > 0 ||
materials.secondary.selectedImageIds.length > 0 ? (
@ -376,6 +446,12 @@ export function AgentWorkflowPanel({
{inputs.thirdCategory ? (
<span className="rounded-full bg-background px-2 py-1">三级分类:{inputs.thirdCategory}</span>
) : null}
{inputs.fourthCategory ? (
<span className="rounded-full bg-background px-2 py-1">四级分类:{inputs.fourthCategory}</span>
) : null}
{inputs.fifthCategory ? (
<span className="rounded-full bg-background px-2 py-1">五级分类:{inputs.fifthCategory}</span>
) : null}
{inputs.priceBand ? (
<span className="rounded-full bg-background px-2 py-1">价格带:{inputs.priceBand}</span>
) : null}

View File

@ -12,6 +12,8 @@ import { cn } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth,
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowThirdCategoryOptionsByBrandStore,
getAgentWorkflowStoreOptionsByBrand,
@ -34,6 +36,8 @@ export interface AgentWorkflowInputs {
brand: string;
store: string;
thirdCategory: string;
fourthCategory: string;
fifthCategory: string;
priceBand: string;
}
@ -191,6 +195,27 @@ export function AgentWorkflowPanelDark({
}),
[config, inputs.brand, inputs.store]
);
const fourthCategoryOptions = useMemo(
() =>
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({
config,
brand: inputs.brand,
store: inputs.store,
thirdCategory: inputs.thirdCategory,
}),
[config, inputs.brand, inputs.store, inputs.thirdCategory]
);
const fifthCategoryOptions = useMemo(
() =>
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({
config,
brand: inputs.brand,
store: inputs.store,
thirdCategory: inputs.thirdCategory,
fourthCategory: inputs.fourthCategory,
}),
[config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
@ -284,7 +309,7 @@ export function AgentWorkflowPanelDark({
onReset();
return;
}
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", priceBand: "" });
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", fourthCategory: "", fifthCategory: "", priceBand: "" });
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
secondary: createEmptyAgentWorkflowMaterialSelection(),
@ -318,7 +343,7 @@ export function AgentWorkflowPanelDark({
基础配置
</motion.h2>
<motion.div
className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"
className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
@ -341,6 +366,8 @@ export function AgentWorkflowPanelDark({
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.thirdCategory
: "",
fourthCategory: "",
fifthCategory: "",
})
}
/>
@ -359,6 +386,8 @@ export function AgentWorkflowPanelDark({
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
? inputs.thirdCategory
: "",
fourthCategory: "",
fifthCategory: "",
})
}
/>
@ -369,7 +398,44 @@ export function AgentWorkflowPanelDark({
searchPlaceholder="输入三级分类筛选"
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
disabled={!inputs.store || thirdCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
onValueChange={(value) =>
onInputsChange({
...inputs,
thirdCategory: value,
fourthCategory:
value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase()
? inputs.fourthCategory
: "",
fifthCategory: "",
})
}
/>
<SearchableSelect
value={inputs.fourthCategory}
options={fourthCategoryOptions}
placeholder="选择四级分类"
searchPlaceholder="输入四级分类筛选"
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
onValueChange={(value) =>
onInputsChange({
...inputs,
fourthCategory: value,
fifthCategory:
value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase()
? inputs.fifthCategory
: "",
})
}
/>
<SearchableSelect
value={inputs.fifthCategory}
options={fifthCategoryOptions}
placeholder="选择五级分类"
searchPlaceholder="输入五级分类筛选"
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
/>
<SearchableSelect
value={inputs.priceBand}

View File

@ -14,6 +14,8 @@ import {
flattenVisibleAgentWorkflowSteps,
getAgentWorkflowBlockedOptionKeysForSelections,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth,
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowThirdCategoryOptionsByBrandStore,
getAgentWorkflowSelectedOptionPath,
@ -43,6 +45,8 @@ export interface AgentWorkflowInputs {
brand: string;
store: string;
thirdCategory: string;
fourthCategory: string;
fifthCategory: string;
priceBand: string;
}
@ -274,7 +278,7 @@ export function AgentWorkflowBrandStorePanel({
onInputsChange,
theme = "dark",
title = "品牌层级选择",
description = "先选择品牌、店铺、三级分类和价格带,后续品牌模特与风格步骤会按这里的选择联动。",
description = "先选择品牌、店铺到五级分类和价格带,后续品牌模特与风格步骤会按这里的选择联动。",
}: AgentWorkflowBrandStorePanelProps) {
const brandOptions = useMemo(
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
@ -297,6 +301,27 @@ export function AgentWorkflowBrandStorePanel({
}),
[config, inputs.brand, inputs.store]
);
const fourthCategoryOptions = useMemo(
() =>
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({
config,
brand: inputs.brand,
store: inputs.store,
thirdCategory: inputs.thirdCategory,
}),
[config, inputs.brand, inputs.store, inputs.thirdCategory]
);
const fifthCategoryOptions = useMemo(
() =>
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({
config,
brand: inputs.brand,
store: inputs.store,
thirdCategory: inputs.thirdCategory,
fourthCategory: inputs.fourthCategory,
}),
[config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
@ -318,7 +343,7 @@ export function AgentWorkflowBrandStorePanel({
{description}
</div>
</div>
<div className="grid max-w-6xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<div className="grid max-w-7xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -338,6 +363,8 @@ export function AgentWorkflowBrandStorePanel({
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.thirdCategory
: "",
fourthCategory: "",
fifthCategory: "",
})
}
/>
@ -357,6 +384,8 @@ export function AgentWorkflowBrandStorePanel({
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
? inputs.thirdCategory
: "",
fourthCategory: "",
fifthCategory: "",
})
}
/>
@ -368,7 +397,46 @@ export function AgentWorkflowBrandStorePanel({
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
theme={theme}
disabled={!inputs.store || thirdCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
onValueChange={(value) =>
onInputsChange({
...inputs,
thirdCategory: value,
fourthCategory:
value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase()
? inputs.fourthCategory
: "",
fifthCategory: "",
})
}
/>
<SearchableSelect
value={inputs.fourthCategory}
options={fourthCategoryOptions}
placeholder="选择四级分类"
searchPlaceholder="输入四级分类筛选"
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
theme={theme}
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
onValueChange={(value) =>
onInputsChange({
...inputs,
fourthCategory: value,
fifthCategory:
value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase()
? inputs.fifthCategory
: "",
})
}
/>
<SearchableSelect
value={inputs.fifthCategory}
options={fifthCategoryOptions}
placeholder="选择五级分类"
searchPlaceholder="输入五级分类筛选"
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
theme={theme}
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
/>
<SearchableSelect
value={inputs.priceBand}

View File

@ -12,6 +12,8 @@ import { cn } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth,
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowThirdCategoryOptionsByBrandStore,
getAgentWorkflowStoreOptionsByBrand,
@ -34,6 +36,8 @@ export interface AgentWorkflowInputs {
brand: string;
store: string;
thirdCategory: string;
fourthCategory: string;
fifthCategory: string;
priceBand: string;
}
@ -203,6 +207,27 @@ export function AgentWorkflowPanelNew({
}),
[config, inputs.brand, inputs.store]
);
const fourthCategoryOptions = useMemo(
() =>
getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({
config,
brand: inputs.brand,
store: inputs.store,
thirdCategory: inputs.thirdCategory,
}),
[config, inputs.brand, inputs.store, inputs.thirdCategory]
);
const fifthCategoryOptions = useMemo(
() =>
getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({
config,
brand: inputs.brand,
store: inputs.store,
thirdCategory: inputs.thirdCategory,
fourthCategory: inputs.fourthCategory,
}),
[config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
@ -305,7 +330,7 @@ export function AgentWorkflowPanelNew({
onReset();
return;
}
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", priceBand: "" });
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", fourthCategory: "", fifthCategory: "", priceBand: "" });
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
secondary: createEmptyAgentWorkflowMaterialSelection(),
@ -357,7 +382,7 @@ export function AgentWorkflowPanelNew({
</span>
基础配置
</h3>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -376,6 +401,8 @@ export function AgentWorkflowPanelNew({
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.thirdCategory
: "",
fourthCategory: "",
fifthCategory: "",
})
}
/>
@ -394,6 +421,8 @@ export function AgentWorkflowPanelNew({
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
? inputs.thirdCategory
: "",
fourthCategory: "",
fifthCategory: "",
})
}
/>
@ -404,7 +433,44 @@ export function AgentWorkflowPanelNew({
searchPlaceholder="输入三级分类筛选"
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
disabled={!inputs.store || thirdCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
onValueChange={(value) =>
onInputsChange({
...inputs,
thirdCategory: value,
fourthCategory:
value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase()
? inputs.fourthCategory
: "",
fifthCategory: "",
})
}
/>
<SearchableSelect
value={inputs.fourthCategory}
options={fourthCategoryOptions}
placeholder="选择四级分类"
searchPlaceholder="输入四级分类筛选"
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
onValueChange={(value) =>
onInputsChange({
...inputs,
fourthCategory: value,
fifthCategory:
value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase()
? inputs.fifthCategory
: "",
})
}
/>
<SearchableSelect
value={inputs.fifthCategory}
options={fifthCategoryOptions}
placeholder="选择五级分类"
searchPlaceholder="输入五级分类筛选"
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
/>
<SearchableSelect
value={inputs.priceBand}

View File

@ -35,6 +35,8 @@ export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersisted
brand: "",
store: "",
thirdCategory: "",
fourthCategory: "",
fifthCategory: "",
priceBand: "",
},
materials: createEmptyAgentWorkflowMaterialSelections(),
@ -231,6 +233,8 @@ function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState {
brand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).brand || "" : "").trim(),
store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).store || "" : "").trim(),
thirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).thirdCategory || "" : "").trim(),
fourthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).fourthCategory || "" : "").trim(),
fifthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).fifthCategory || "" : "").trim(),
priceBand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).priceBand || "" : "").trim(),
},
materials: normalizeAgentWorkflowMaterialSelections(raw.materials),

View File

@ -21,6 +21,8 @@ export interface AgentWorkflowInputsValue {
brand: string;
store: string;
thirdCategory: string;
fourthCategory: string;
fifthCategory: string;
priceBand: string;
}
@ -216,6 +218,12 @@ export function buildAgentWorkflowComposerContext(params: {
if (params.inputs.thirdCategory.trim()) {
summaryParts.push(`三级分类:${params.inputs.thirdCategory.trim()}`);
}
if (params.inputs.fourthCategory.trim()) {
summaryParts.push(`四级分类:${params.inputs.fourthCategory.trim()}`);
}
if (params.inputs.fifthCategory.trim()) {
summaryParts.push(`五级分类:${params.inputs.fifthCategory.trim()}`);
}
if (params.inputs.priceBand.trim()) {
summaryParts.push(`价格带:${params.inputs.priceBand.trim()}`);
}
@ -276,6 +284,12 @@ export function buildAgentWorkflowComposerContext(params: {
if (params.inputs.thirdCategory.trim()) {
promptLines.push(`三级分类:${params.inputs.thirdCategory.trim()}`);
}
if (params.inputs.fourthCategory.trim()) {
promptLines.push(`四级分类:${params.inputs.fourthCategory.trim()}`);
}
if (params.inputs.fifthCategory.trim()) {
promptLines.push(`五级分类:${params.inputs.fifthCategory.trim()}`);
}
if (params.inputs.priceBand.trim()) {
promptLines.push(`价格带:${params.inputs.priceBand.trim()}`);
}
@ -362,6 +376,8 @@ export function buildAgentWorkflowComposerContext(params: {
brand: params.inputs.brand.trim() || undefined,
store: params.inputs.store.trim() || undefined,
thirdCategory: params.inputs.thirdCategory.trim() || undefined,
fourthCategory: params.inputs.fourthCategory.trim() || undefined,
fifthCategory: params.inputs.fifthCategory.trim() || undefined,
priceBand: params.inputs.priceBand.trim() || undefined,
selections,
referenceImages,

View File

@ -1,7 +1,9 @@
import type {
AgentWorkflowBrandStoreEntry,
AgentWorkflowFourthCategoryEntry,
AgentWorkflowOptionUiConfig,
AgentWorkflowStoreEntry,
AgentWorkflowThirdCategoryEntry,
AgentWorkflowStepKey,
AgentWorkflowStepUiConfig,
AgentWorkflowUiConfig,
@ -141,6 +143,8 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
brands: [],
stores: [],
thirdCategories: [],
fourthCategories: [],
fifthCategories: [],
priceBands: [],
brandStoreEntries: [],
},
@ -434,6 +438,143 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
const normalizeBrandStoreEntries = (input: unknown) => {
if (!Array.isArray(input)) return [] as AgentWorkflowBrandStoreEntry[];
const normalizeFourthCategoryEntries = (
rawFourthCategoryEntries: unknown,
legacyFourthCategories: string[]
): AgentWorkflowFourthCategoryEntry[] => {
const mappedEntries: Array<AgentWorkflowFourthCategoryEntry | null> = Array.isArray(
rawFourthCategoryEntries
)
? rawFourthCategoryEntries.map((fourthItem, fourthIndex) => {
if (typeof fourthItem === "string") {
const categoryName = normalizeText(fourthItem, 64);
if (!categoryName) return null;
return {
categoryName,
fifthCategories: [],
sortOrder: fourthIndex,
};
}
const fourthRecord =
fourthItem && typeof fourthItem === "object" && !Array.isArray(fourthItem)
? (fourthItem as Record<string, unknown>)
: {};
const categoryName = normalizeText(
fourthRecord.categoryName ?? fourthRecord.fourthCategoryName,
64
);
if (!categoryName) return null;
return {
categoryName,
fifthCategories: normalizeStringList(fourthRecord.fifthCategories).slice(0, 300),
sortOrder: normalizeNumber(fourthRecord.sortOrder, fourthIndex),
};
})
: [];
const normalizedFromEntries = mappedEntries
.filter((item): item is AgentWorkflowFourthCategoryEntry => item !== null)
.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0))
.map((entry, index) => ({
...entry,
fifthCategories: Array.from(
new Set((entry.fifthCategories || []).map((item) => item.trim()).filter(Boolean))
).slice(0, 300),
sortOrder: index,
}));
if (normalizedFromEntries.length > 0) {
return normalizedFromEntries;
}
return legacyFourthCategories.map((categoryName, index) => ({
categoryName,
fifthCategories: [],
sortOrder: index,
}));
};
const normalizeThirdCategoryEntries = (
rawThirdCategoryEntries: unknown,
legacyThirdCategories: string[]
): AgentWorkflowThirdCategoryEntry[] => {
const mappedEntries: Array<AgentWorkflowThirdCategoryEntry | null> = Array.isArray(
rawThirdCategoryEntries
)
? rawThirdCategoryEntries.map((thirdItem, thirdIndex) => {
if (typeof thirdItem === "string") {
const categoryName = normalizeText(thirdItem, 64);
if (!categoryName) return null;
return {
categoryName,
fourthCategories: [],
fourthCategoryEntries: [],
sortOrder: thirdIndex,
};
}
const thirdRecord =
thirdItem && typeof thirdItem === "object" && !Array.isArray(thirdItem)
? (thirdItem as Record<string, unknown>)
: {};
const categoryName = normalizeText(
thirdRecord.categoryName ?? thirdRecord.thirdCategoryName,
64
);
if (!categoryName) return null;
const legacyFourthCategories = normalizeStringList(thirdRecord.fourthCategories).slice(
0,
300
);
const fourthCategoryEntries = normalizeFourthCategoryEntries(
thirdRecord.fourthCategoryEntries,
legacyFourthCategories
);
return {
categoryName,
fourthCategories:
fourthCategoryEntries.length > 0
? fourthCategoryEntries.map((entry) => entry.categoryName)
: legacyFourthCategories,
fourthCategoryEntries,
sortOrder: normalizeNumber(thirdRecord.sortOrder, thirdIndex),
};
})
: [];
const normalizedFromEntries = mappedEntries
.filter((item): item is AgentWorkflowThirdCategoryEntry => item !== null)
.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0))
.map((entry, index) => ({
...entry,
fourthCategories: Array.from(
new Set((entry.fourthCategories || []).map((item) => item.trim()).filter(Boolean))
).slice(0, 300),
fourthCategoryEntries: (entry.fourthCategoryEntries || []).map((fourthEntry, fourthIndex) => ({
...fourthEntry,
fifthCategories: Array.from(
new Set((fourthEntry.fifthCategories || []).map((item) => item.trim()).filter(Boolean))
).slice(0, 300),
sortOrder: fourthIndex,
})),
sortOrder: index,
}));
if (normalizedFromEntries.length > 0) {
return normalizedFromEntries;
}
return legacyThirdCategories.map((categoryName, index) => ({
categoryName,
fourthCategories: [],
fourthCategoryEntries: [],
sortOrder: index,
}));
};
const normalizeStoreEntries = (
rawStoreEntries: unknown,
legacyStores: string[]
@ -456,9 +597,21 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
: {};
const storeName = normalizeText(storeRecord.storeName, 64);
if (!storeName) return null;
const legacyThirdCategories = normalizeStringList(storeRecord.thirdCategories).slice(
0,
300
);
const thirdCategoryEntries = normalizeThirdCategoryEntries(
storeRecord.thirdCategoryEntries,
legacyThirdCategories
);
return {
storeName,
thirdCategories: normalizeStringList(storeRecord.thirdCategories).slice(0, 300),
thirdCategories:
thirdCategoryEntries.length > 0
? thirdCategoryEntries.map((entry) => entry.categoryName)
: legacyThirdCategories,
thirdCategoryEntries,
sortOrder: normalizeNumber(storeRecord.sortOrder, storeIndex),
};
})
@ -472,6 +625,22 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
thirdCategories: Array.from(
new Set((entry.thirdCategories || []).map((item) => item.trim()).filter(Boolean))
).slice(0, 300),
thirdCategoryEntries: (entry.thirdCategoryEntries || []).map((thirdEntry, thirdIndex) => ({
...thirdEntry,
fourthCategories: Array.from(
new Set((thirdEntry.fourthCategories || []).map((item) => item.trim()).filter(Boolean))
).slice(0, 300),
fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).map(
(fourthEntry, fourthIndex) => ({
...fourthEntry,
fifthCategories: Array.from(
new Set((fourthEntry.fifthCategories || []).map((item) => item.trim()).filter(Boolean))
).slice(0, 300),
sortOrder: fourthIndex,
})
),
sortOrder: thirdIndex,
})),
sortOrder: index,
}));
@ -482,6 +651,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
return legacyStores.map((storeName, index) => ({
storeName,
thirdCategories: [],
thirdCategoryEntries: [],
sortOrder: index,
}));
};
@ -519,6 +689,8 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
const normalizedBrands = normalizeStringList(rawInputOptions.brands);
const normalizedStores = normalizeStringList(rawInputOptions.stores);
const normalizedThirdCategories = normalizeStringList(rawInputOptions.thirdCategories);
const normalizedFourthCategories = normalizeStringList(rawInputOptions.fourthCategories);
const normalizedFifthCategories = normalizeStringList(rawInputOptions.fifthCategories);
const normalizedPriceBands = normalizeStringList(rawInputOptions.priceBands);
const fallbackBrandStoreEntries: AgentWorkflowBrandStoreEntry[] =
normalizedBrandStoreEntries.length > 0
@ -531,6 +703,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
? normalizedStores.map((storeName, storeIndex) => ({
storeName,
thirdCategories: [],
thirdCategoryEntries: [],
sortOrder: storeIndex,
}))
: [],
@ -718,6 +891,36 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
)
)
: normalizedThirdCategories,
fourthCategories:
fallbackBrandStoreEntries.length > 0
? Array.from(
new Set(
fallbackBrandStoreEntries.flatMap((entry) =>
(entry.storeEntries || []).flatMap((storeEntry) =>
(storeEntry.thirdCategoryEntries || []).flatMap(
(thirdEntry) => thirdEntry.fourthCategories || []
)
)
)
)
)
: normalizedFourthCategories,
fifthCategories:
fallbackBrandStoreEntries.length > 0
? Array.from(
new Set(
fallbackBrandStoreEntries.flatMap((entry) =>
(entry.storeEntries || []).flatMap((storeEntry) =>
(storeEntry.thirdCategoryEntries || []).flatMap((thirdEntry) =>
(thirdEntry.fourthCategoryEntries || []).flatMap(
(fourthEntry) => fourthEntry.fifthCategories || []
)
)
)
)
)
)
: normalizedFifthCategories,
priceBands: normalizedPriceBands,
brandStoreEntries: fallbackBrandStoreEntries,
},
@ -773,6 +976,13 @@ export function getAgentWorkflowThirdCategoryOptionsByBrandStore(params: {
const matchedStoreEntry = matchedEntry?.storeEntries?.find(
(storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore
);
const thirdCategoryOptions =
matchedStoreEntry?.thirdCategoryEntries && matchedStoreEntry.thirdCategoryEntries.length > 0
? matchedStoreEntry.thirdCategoryEntries.map((entry) => entry.categoryName).filter(Boolean)
: matchedStoreEntry?.thirdCategories || [];
if (thirdCategoryOptions.length > 0) {
return thirdCategoryOptions;
}
if (matchedStoreEntry && matchedStoreEntry.thirdCategories.length > 0) {
return matchedStoreEntry.thirdCategories;
}
@ -780,6 +990,71 @@ export function getAgentWorkflowThirdCategoryOptionsByBrandStore(params: {
return params.config?.inputOptions?.thirdCategories || defaultAgentWorkflowUiConfig.inputOptions?.thirdCategories || [];
}
export function getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory(params: {
config?: AgentWorkflowUiConfig | null;
brand?: string | null;
store?: string | null;
thirdCategory?: string | null;
}) {
const entries = getAgentWorkflowBrandStoreEntries(params.config);
const normalizedBrand = String(params.brand || "").trim().toLowerCase();
const normalizedStore = String(params.store || "").trim().toLowerCase();
const normalizedThirdCategory = String(params.thirdCategory || "").trim().toLowerCase();
if (!normalizedBrand || !normalizedStore || !normalizedThirdCategory) {
return [];
}
const matchedEntry = entries.find(
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
);
const matchedStoreEntry = matchedEntry?.storeEntries?.find(
(storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore
);
const matchedThirdEntry = matchedStoreEntry?.thirdCategoryEntries?.find(
(thirdEntry) => thirdEntry.categoryName.trim().toLowerCase() === normalizedThirdCategory
);
if (matchedThirdEntry && matchedThirdEntry.fourthCategories.length > 0) {
return matchedThirdEntry.fourthCategories;
}
return params.config?.inputOptions?.fourthCategories || defaultAgentWorkflowUiConfig.inputOptions?.fourthCategories || [];
}
export function getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth(params: {
config?: AgentWorkflowUiConfig | null;
brand?: string | null;
store?: string | null;
thirdCategory?: string | null;
fourthCategory?: string | null;
}) {
const entries = getAgentWorkflowBrandStoreEntries(params.config);
const normalizedBrand = String(params.brand || "").trim().toLowerCase();
const normalizedStore = String(params.store || "").trim().toLowerCase();
const normalizedThirdCategory = String(params.thirdCategory || "").trim().toLowerCase();
const normalizedFourthCategory = String(params.fourthCategory || "").trim().toLowerCase();
if (!normalizedBrand || !normalizedStore || !normalizedThirdCategory || !normalizedFourthCategory) {
return [];
}
const matchedEntry = entries.find(
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
);
const matchedStoreEntry = matchedEntry?.storeEntries?.find(
(storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore
);
const matchedThirdEntry = matchedStoreEntry?.thirdCategoryEntries?.find(
(thirdEntry) => thirdEntry.categoryName.trim().toLowerCase() === normalizedThirdCategory
);
const matchedFourthEntry = matchedThirdEntry?.fourthCategoryEntries?.find(
(fourthEntry) => fourthEntry.categoryName.trim().toLowerCase() === normalizedFourthCategory
);
if (matchedFourthEntry && matchedFourthEntry.fifthCategories.length > 0) {
return matchedFourthEntry.fifthCategories;
}
return params.config?.inputOptions?.fifthCategories || defaultAgentWorkflowUiConfig.inputOptions?.fifthCategories || [];
}
export function getAgentWorkflowPriceBandOptions(config?: AgentWorkflowUiConfig | null) {
return config?.inputOptions?.priceBands || defaultAgentWorkflowUiConfig.inputOptions?.priceBands || [];
}

View File

@ -618,6 +618,8 @@ export const canvasProject = pgTable("CanvasProject", {
brand?: string;
store?: string;
thirdCategory?: string;
fourthCategory?: string;
fifthCategory?: string;
priceBand?: string;
};
materials?: {

View File

@ -106,15 +106,31 @@ export interface AgentWorkflowUiConfig {
brands: string[];
stores: string[];
thirdCategories?: string[];
fourthCategories?: string[];
fifthCategories?: string[];
priceBands?: string[];
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
};
steps: AgentWorkflowStepUiConfig[];
}
export interface AgentWorkflowFourthCategoryEntry {
categoryName: string;
fifthCategories: string[];
sortOrder?: number;
}
export interface AgentWorkflowThirdCategoryEntry {
categoryName: string;
fourthCategories: string[];
fourthCategoryEntries?: AgentWorkflowFourthCategoryEntry[];
sortOrder?: number;
}
export interface AgentWorkflowStoreEntry {
storeName: string;
thirdCategories: string[];
thirdCategoryEntries?: AgentWorkflowThirdCategoryEntry[];
sortOrder?: number;
}
@ -258,6 +274,8 @@ export interface AgentWorkflowComposerContext {
brand?: string;
store?: string;
thirdCategory?: string;
fourthCategory?: string;
fifthCategory?: string;
priceBand?: string;
selections: AgentWorkflowComposerSelection[];
referenceImages: AgentWorkflowReferenceImage[];
@ -392,6 +410,8 @@ export interface AgentWorkflowPersistedState {
brand: string;
store: string;
thirdCategory: string;
fourthCategory: string;
fifthCategory: string;
priceBand: string;
};
materials?: AgentWorkflowMaterialSelections;