fix: filter detail references by brand
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-27 21:08:33 +08:00
parent 55bed5087c
commit 807aba1e85
3 changed files with 91 additions and 36 deletions

View File

@ -18,6 +18,7 @@ const querySchema = z.object({
keyword: z.string().trim().min(1).max(64), keyword: z.string().trim().min(1).max(64),
queryType: z.enum(["sku", "spu"]).default("spu"), queryType: z.enum(["sku", "spu"]).default("spu"),
target: z.enum(["main", "detail", "sku"]), target: z.enum(["main", "detail", "sku"]),
brandName: z.string().trim().max(64).optional(),
storeName: z.string().trim().max(64).optional(), storeName: z.string().trim().max(64).optional(),
}); });
@ -25,6 +26,10 @@ type ExternalTaskDetailCandidate = {
materialId?: string; materialId?: string;
taskNo?: string; taskNo?: string;
productModel?: string; productModel?: string;
brandName?: string;
brand?: string;
productBrand?: string;
brand_name?: string;
categoryName?: string; categoryName?: string;
shopName?: string; shopName?: string;
sceneName?: string; sceneName?: string;
@ -44,6 +49,9 @@ type ExternalTaskDetailResponse = {
resourceType?: string; resourceType?: string;
spu?: string | null; spu?: string | null;
sku?: string | null; sku?: string | null;
brandName?: string | null;
brand?: string | null;
productBrand?: string | null;
categoryName?: string | null; categoryName?: string | null;
shopName?: string | null; shopName?: string | null;
candidateCount?: number; candidateCount?: number;
@ -59,7 +67,7 @@ function normalizeBaseUrl(value: string) {
return String(value || "").trim().replace(/\/+$/, ""); return String(value || "").trim().replace(/\/+$/, "");
} }
function normalizeShopName(value: unknown) { function normalizeComparableName(value: unknown) {
return String(value || "") return String(value || "")
.trim() .trim()
.toLowerCase() .toLowerCase()
@ -104,12 +112,21 @@ function normalizeCandidate(
const materialId = String(value.materialId || "").trim() || null; const materialId = String(value.materialId || "").trim() || null;
const sceneName = String(value.sceneName || "").trim() || null; const sceneName = String(value.sceneName || "").trim() || null;
const id = taskNo || materialId || `candidate-${index + 1}`; const id = taskNo || materialId || `candidate-${index + 1}`;
const brandName =
String(
value.brandName ||
value.brand ||
value.productBrand ||
value.brand_name ||
""
).trim() || null;
return { return {
id, id,
materialId, materialId,
taskNo, taskNo,
productModel: String(value.productModel || "").trim() || null, productModel: String(value.productModel || "").trim() || null,
brandName,
categoryName: String(value.categoryName || "").trim() || null, categoryName: String(value.categoryName || "").trim() || null,
shopName: String(value.shopName || "").trim() || null, shopName: String(value.shopName || "").trim() || null,
sceneName, sceneName,
@ -171,23 +188,39 @@ function sortCandidatesByUploadedAtDesc(candidates: AgentModeReferenceCandidate[
}); });
} }
function isCandidateMatchedStore( function isCandidateMatchedBrand(
candidate: AgentModeReferenceCandidate, candidate: AgentModeReferenceCandidate,
storeName?: string brandName?: string
) { ) {
const normalizedRequestedStore = normalizeShopName(storeName); const normalizedRequestedBrand = normalizeComparableName(brandName);
if (!normalizedRequestedStore) return true; if (!normalizedRequestedBrand) return true;
const normalizedShopName = normalizeShopName(candidate.shopName); const normalizedBrandName = normalizeComparableName(candidate.brandName);
const normalizedCategoryName = normalizeShopName(candidate.categoryName); if (!normalizedBrandName) return false;
return ( return (
normalizedShopName === normalizedRequestedStore || normalizedBrandName === normalizedRequestedBrand ||
normalizedCategoryName === normalizedRequestedStore || normalizedBrandName.includes(normalizedRequestedBrand) ||
(Boolean(normalizedShopName) && normalizedShopName.includes(normalizedRequestedStore)) || normalizedRequestedBrand.includes(normalizedBrandName)
(Boolean(normalizedShopName) && normalizedRequestedStore.includes(normalizedShopName)) || );
(Boolean(normalizedCategoryName) && normalizedCategoryName.includes(normalizedRequestedStore)) || }
(Boolean(normalizedCategoryName) && normalizedRequestedStore.includes(normalizedCategoryName))
function filterCandidatesByBrand(
candidates: AgentModeReferenceCandidate[],
brandName?: string
) {
const normalizedRequestedBrand = normalizeComparableName(brandName);
if (!normalizedRequestedBrand) return candidates;
const hasBrandMetadata = candidates.some((candidate) =>
Boolean(normalizeComparableName(candidate.brandName))
);
if (!hasBrandMetadata) {
return candidates;
}
return candidates.filter((candidate) =>
isCandidateMatchedBrand(candidate, normalizedRequestedBrand)
); );
} }
@ -202,6 +235,7 @@ export async function GET(request: NextRequest) {
keyword: request.nextUrl.searchParams.get("keyword"), keyword: request.nextUrl.searchParams.get("keyword"),
queryType: request.nextUrl.searchParams.get("queryType") || "spu", queryType: request.nextUrl.searchParams.get("queryType") || "spu",
target: request.nextUrl.searchParams.get("target"), target: request.nextUrl.searchParams.get("target"),
brandName: request.nextUrl.searchParams.get("brandName") || undefined,
storeName: request.nextUrl.searchParams.get("storeName") || undefined, storeName: request.nextUrl.searchParams.get("storeName") || undefined,
}); });
@ -254,11 +288,11 @@ export async function GET(request: NextRequest) {
); );
} }
const requestedStoreName = String(parsed.storeName || "").trim(); const requestedBrandName = String(parsed.brandName || "").trim();
const normalizedCandidates = sortCandidatesByUploadedAtDesc( const sortedCandidates = sortCandidatesByUploadedAtDesc(
(upstreamResult.data.candidates || []).map((item, index) => normalizeCandidate(item, index)) (upstreamResult.data.candidates || []).map((item, index) => normalizeCandidate(item, index))
) );
.filter((candidate) => isCandidateMatchedStore(candidate, requestedStoreName)) const normalizedCandidates = filterCandidatesByBrand(sortedCandidates, requestedBrandName)
.filter((candidate) => getRenderableCandidateTargetUrls(candidate, parsed.target).length > 0); .filter((candidate) => getRenderableCandidateTargetUrls(candidate, parsed.target).length > 0);
if (normalizedCandidates.length === 0) { if (normalizedCandidates.length === 0) {
@ -266,15 +300,15 @@ export async function GET(request: NextRequest) {
{ {
success: false, success: false,
error: { error: {
code: requestedStoreName code: requestedBrandName
? "STORE_REFERENCE_NOT_FOUND" ? "BRAND_REFERENCE_NOT_FOUND"
: parsed.target === "main" : parsed.target === "main"
? "MAIN_REFERENCE_NOT_FOUND" ? "MAIN_REFERENCE_NOT_FOUND"
: parsed.target === "detail" : parsed.target === "detail"
? "DETAIL_REFERENCE_NOT_FOUND" ? "DETAIL_REFERENCE_NOT_FOUND"
: "SKU_REFERENCE_NOT_FOUND", : "SKU_REFERENCE_NOT_FOUND",
message: requestedStoreName message: requestedBrandName
? `当前店铺「${requestedStoreName}」下没有可用的${ ? `当前品牌「${requestedBrandName}」下没有可用的${
parsed.target === "main" parsed.target === "main"
? "主图参考图" ? "主图参考图"
: parsed.target === "detail" : parsed.target === "detail"
@ -306,6 +340,16 @@ export async function GET(request: NextRequest) {
target: parsed.target, target: parsed.target,
matchedSpu: String(upstreamResult.data.spu || "").trim() || null, matchedSpu: String(upstreamResult.data.spu || "").trim() || null,
matchedSku: String(upstreamResult.data.sku || "").trim() || null, matchedSku: String(upstreamResult.data.sku || "").trim() || null,
brandName:
selectedCandidate.brandName ||
String(
upstreamResult.data.brandName ||
upstreamResult.data.brand ||
upstreamResult.data.productBrand ||
""
).trim() ||
requestedBrandName ||
null,
categoryName: selectedCandidate.categoryName || String(upstreamResult.data.categoryName || "").trim() || null, categoryName: selectedCandidate.categoryName || String(upstreamResult.data.categoryName || "").trim() || null,
shopName: selectedCandidate.shopName || String(upstreamResult.data.shopName || "").trim() || null, shopName: selectedCandidate.shopName || String(upstreamResult.data.shopName || "").trim() || null,
candidateCount: normalizedCandidates.length, candidateCount: normalizedCandidates.length,

View File

@ -244,6 +244,7 @@ interface AgentModeReferenceSearchResponse {
target: AgentTaskDetailReferenceSource; target: AgentTaskDetailReferenceSource;
matchedSpu?: string | null; matchedSpu?: string | null;
matchedSku?: string | null; matchedSku?: string | null;
brandName?: string | null;
categoryName?: string | null; categoryName?: string | null;
shopName?: string | null; shopName?: string | null;
candidateCount: number; candidateCount: number;
@ -1346,7 +1347,7 @@ export function AgentGenerationWorkspaceDarkV2({
const skuReferenceTarget = presetModeConfig.sku.referenceImageSource; const skuReferenceTarget = presetModeConfig.sku.referenceImageSource;
const detailReferenceSourceLabel = getTaskDetailReferenceSourceLabel(detailReferenceTarget); const detailReferenceSourceLabel = getTaskDetailReferenceSourceLabel(detailReferenceTarget);
const skuReferenceSourceLabel = getTaskDetailReferenceSourceLabel(skuReferenceTarget); const skuReferenceSourceLabel = getTaskDetailReferenceSourceLabel(skuReferenceTarget);
const workflowStoreName = String(workflowContext?.store || "").trim(); const workflowBrandName = String(workflowContext?.brand || "").trim();
const shouldAllowModelOverride = const shouldAllowModelOverride =
isDirectModelGenerationMode || !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride; isDirectModelGenerationMode || !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride;
const brandModelAttachmentKeys = useMemo(() => { const brandModelAttachmentKeys = useMemo(() => {
@ -3968,8 +3969,8 @@ export function AgentGenerationWorkspaceDarkV2({
} }
const handleDetailReplacementSearch = useCallback(async () => { const handleDetailReplacementSearch = useCallback(async () => {
const normalized = detailReplacementQueryInput.trim().toUpperCase(); const normalized = detailReplacementQueryInput.trim().toUpperCase();
if (!workflowStoreName) { if (!workflowBrandName) {
toast.error("请先在第一步完成品牌和店铺选择后再查询参考图"); toast.error("请先在第一步完成品牌选择后再查询参考图");
return; return;
} }
if (!normalized) { if (!normalized) {
@ -3988,8 +3989,8 @@ export function AgentGenerationWorkspaceDarkV2({
queryType: "spu", queryType: "spu",
target: detailReferenceTarget, target: detailReferenceTarget,
}); });
if (workflowStoreName) { if (workflowBrandName) {
searchParams.set("storeName", workflowStoreName); searchParams.set("brandName", workflowBrandName);
} }
const response = await fetch( const response = await fetch(
`/api/agent-workflow/reference-search?${searchParams.toString()}`, `/api/agent-workflow/reference-search?${searchParams.toString()}`,
@ -4026,7 +4027,7 @@ export function AgentGenerationWorkspaceDarkV2({
detailReferenceTarget, detailReferenceTarget,
detailReplacementQueryInput, detailReplacementQueryInput,
updateDetailReplacementState, updateDetailReplacementState,
workflowStoreName, workflowBrandName,
]); ]);
const handleSelectDetailReplacementCandidate = useCallback((candidateId: string) => { const handleSelectDetailReplacementCandidate = useCallback((candidateId: string) => {
updateDetailReplacementState((current) => { updateDetailReplacementState((current) => {
@ -4068,8 +4069,8 @@ export function AgentGenerationWorkspaceDarkV2({
); );
const handleSkuReplacementSearch = useCallback(async () => { const handleSkuReplacementSearch = useCallback(async () => {
const normalized = skuReplacementQueryInput.trim().toUpperCase(); const normalized = skuReplacementQueryInput.trim().toUpperCase();
if (!workflowStoreName) { if (!workflowBrandName) {
toast.error("请先在第一步完成品牌和店铺选择后再查询参考图"); toast.error("请先在第一步完成品牌选择后再查询参考图");
return; return;
} }
if (!normalized) { if (!normalized) {
@ -4088,8 +4089,8 @@ export function AgentGenerationWorkspaceDarkV2({
queryType: skuReplacementQueryType, queryType: skuReplacementQueryType,
target: skuReferenceTarget, target: skuReferenceTarget,
}); });
if (workflowStoreName) { if (workflowBrandName) {
searchParams.set("storeName", workflowStoreName); searchParams.set("brandName", workflowBrandName);
} }
const response = await fetch( const response = await fetch(
`/api/agent-workflow/reference-search?${searchParams.toString()}`, `/api/agent-workflow/reference-search?${searchParams.toString()}`,
@ -4135,7 +4136,7 @@ export function AgentGenerationWorkspaceDarkV2({
skuReplacementQueryInput, skuReplacementQueryInput,
skuReplacementQueryType, skuReplacementQueryType,
updateSkuReplacementState, updateSkuReplacementState,
workflowStoreName, workflowBrandName,
]); ]);
const handleSelectSkuReplacementCandidate = useCallback((candidateId: string) => { const handleSelectSkuReplacementCandidate = useCallback((candidateId: string) => {
updateSkuReplacementState((current) => { updateSkuReplacementState((current) => {
@ -5115,8 +5116,8 @@ export function AgentGenerationWorkspaceDarkV2({
SPU / SPU /
{detailReplacementSearchState.lastQuery || {detailReplacementSearchState.lastQuery ||
detailReplacementQueryInput.trim().toUpperCase()} detailReplacementQueryInput.trim().toUpperCase()}
{workflowStoreName ? ( {workflowBrandName ? (
<span className="ml-2 text-white/40">{`店铺 / ${workflowStoreName}`}</span> <span className="ml-2 text-white/40">{`品牌 / ${workflowBrandName}`}</span>
) : null} ) : null}
<span className="ml-2 text-emerald-200/70"> <span className="ml-2 text-emerald-200/70">
{`已接入真实${detailReferenceSourceLabel}参考图接口`} {`已接入真实${detailReferenceSourceLabel}参考图接口`}
@ -5158,6 +5159,7 @@ export function AgentGenerationWorkspaceDarkV2({
const summaryBadges = [ const summaryBadges = [
`${detailReferenceSourceLabel}`, `${detailReferenceSourceLabel}`,
`${getCandidateTargetCount(candidate, detailReferenceTarget)}张`, `${getCandidateTargetCount(candidate, detailReferenceTarget)}张`,
candidate.brandName || null,
candidate.categoryName || null, candidate.categoryName || null,
].filter((item): item is string => Boolean(item)); ].filter((item): item is string => Boolean(item));
return ( return (
@ -5221,6 +5223,9 @@ export function AgentGenerationWorkspaceDarkV2({
</div> </div>
</div> </div>
<div className="mt-1.5 flex flex-col gap-0.5 text-[9px] text-white/50"> <div className="mt-1.5 flex flex-col gap-0.5 text-[9px] text-white/50">
{candidate.brandName ? (
<div className="truncate">品牌:{candidate.brandName}</div>
) : null}
<div className="truncate">店铺:{candidate.shopName || "未记录"}</div> <div className="truncate">店铺:{candidate.shopName || "未记录"}</div>
<div className="truncate">更新时间:{uploadedAtText}</div> <div className="truncate">更新时间:{uploadedAtText}</div>
</div> </div>
@ -5867,8 +5872,8 @@ export function AgentGenerationWorkspaceDarkV2({
{skuReplacementQueryType === "spu" ? "SPU" : "SKU"} / {skuReplacementQueryType === "spu" ? "SPU" : "SKU"} /
{skuReplacementSearchState.lastQuery || {skuReplacementSearchState.lastQuery ||
skuReplacementQueryInput.trim().toUpperCase()} skuReplacementQueryInput.trim().toUpperCase()}
{workflowStoreName ? ( {workflowBrandName ? (
<span className="ml-2 text-white/40">{`店铺 / ${workflowStoreName}`}</span> <span className="ml-2 text-white/40">{`品牌 / ${workflowBrandName}`}</span>
) : null} ) : null}
<span className="ml-2 text-emerald-200/70"> <span className="ml-2 text-emerald-200/70">
{`已接入真实${skuReferenceSourceLabel}参考图接口`} {`已接入真实${skuReferenceSourceLabel}参考图接口`}
@ -5930,6 +5935,11 @@ export function AgentGenerationWorkspaceDarkV2({
{formatCandidateMeta(candidate, skuReferenceTarget) || `点击切换这套${skuReferenceSourceLabel}参考图`} {formatCandidateMeta(candidate, skuReferenceTarget) || `点击切换这套${skuReferenceSourceLabel}参考图`}
</div> </div>
<div className="flex flex-wrap gap-1.5 text-[11px] text-white/48"> <div className="flex flex-wrap gap-1.5 text-[11px] text-white/48">
{candidate.brandName ? (
<span className="rounded-full bg-white/[0.05] px-2 py-1">
品牌:{candidate.brandName}
</span>
) : null}
{candidate.shopName ? ( {candidate.shopName ? (
<span className="rounded-full bg-white/[0.05] px-2 py-1"> <span className="rounded-full bg-white/[0.05] px-2 py-1">
{candidate.shopName} {candidate.shopName}

View File

@ -385,6 +385,7 @@ export interface AgentModeReferenceCandidate {
materialId?: string | null; materialId?: string | null;
taskNo?: string | null; taskNo?: string | null;
productModel?: string | null; productModel?: string | null;
brandName?: string | null;
categoryName?: string | null; categoryName?: string | null;
shopName?: string | null; shopName?: string | null;
sceneName?: string | null; sceneName?: string | null;