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

View File

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

View File

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