fix: validate reference brand by goods info
This commit is contained in:
parent
f83dc0aff3
commit
5fb1b5e652
@ -1,4 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createCipheriv, createHash } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { getSystemSetting } from "@/lib/db/queries";
|
||||
@ -6,6 +8,10 @@ import {
|
||||
AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY,
|
||||
normalizeAgentWorkflowMaterialApiConfig,
|
||||
} from "@/lib/agents/workflow-material-config";
|
||||
import {
|
||||
AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
|
||||
normalizeAgentWorkflowGoodsInfoApiConfig,
|
||||
} from "@/lib/agents/workflow-goods-info-config";
|
||||
import type {
|
||||
AgentDetailReplacementReferenceItem,
|
||||
AgentModeReferenceCandidate,
|
||||
@ -82,10 +88,20 @@ type ExternalTaskDetailResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
type ExternalGoodsInfoResponse = {
|
||||
success?: boolean;
|
||||
errMsg?: string | null;
|
||||
data?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
function normalizeBaseUrl(value: string) {
|
||||
return String(value || "").trim().replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function sanitizeText(value: unknown, maxLength: number) {
|
||||
return String(value || "").trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function normalizeComparableName(value: unknown) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
@ -127,6 +143,115 @@ function getExternalBrandName(value: {
|
||||
);
|
||||
}
|
||||
|
||||
function createGoodsInfoSignature(timespan: string, secret: string) {
|
||||
const secretBuffer = Buffer.from(String(secret || "").trim(), "utf8");
|
||||
if (secretBuffer.length !== 8) {
|
||||
throw new Error("商品信息接口 Secret 长度必须为 8 位");
|
||||
}
|
||||
|
||||
const plaintext = String(timespan || "").trim();
|
||||
let encryptedHex = "";
|
||||
|
||||
try {
|
||||
const cipher = createCipheriv("des-cbc", secretBuffer, secretBuffer);
|
||||
cipher.setAutoPadding(true);
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(plaintext, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
encryptedHex = encrypted.toString("hex").toUpperCase();
|
||||
} catch (error) {
|
||||
const opensslOutput = execFileSync(
|
||||
"openssl",
|
||||
[
|
||||
"enc",
|
||||
"-des-cbc",
|
||||
"-provider",
|
||||
"legacy",
|
||||
"-K",
|
||||
secretBuffer.toString("hex"),
|
||||
"-iv",
|
||||
secretBuffer.toString("hex"),
|
||||
"-nosalt",
|
||||
],
|
||||
{
|
||||
input: Buffer.from(plaintext, "utf8"),
|
||||
}
|
||||
);
|
||||
encryptedHex = Buffer.from(opensslOutput).toString("hex").toUpperCase();
|
||||
console.warn(
|
||||
"[Agent Workflow Reference Search] Fallback to openssl legacy DES for goods info signature:",
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
}
|
||||
|
||||
return createHash("md5").update(encryptedHex, "utf8").digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
function getGoodsInfoBrandName(data: Record<string, unknown>) {
|
||||
return (
|
||||
sanitizeText(data["品牌"], 64) ||
|
||||
sanitizeText(data["品牌名称"], 64) ||
|
||||
sanitizeText(data["商品品牌"], 64) ||
|
||||
sanitizeText(data["品牌名"], 64) ||
|
||||
sanitizeText(data["所属品牌"], 64) ||
|
||||
sanitizeText(data["brandName"], 64) ||
|
||||
sanitizeText(data["brand"], 64) ||
|
||||
sanitizeText(data["productBrand"], 64) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchGoodsBrandByKeyword(params: {
|
||||
keyword: string;
|
||||
config: ReturnType<typeof normalizeAgentWorkflowGoodsInfoApiConfig>;
|
||||
}) {
|
||||
const baseUrl = normalizeBaseUrl(params.config.baseUrl);
|
||||
const clientKey = String(params.config.clientKey || "").trim();
|
||||
const secret = String(params.config.secret || "").trim();
|
||||
|
||||
if (!params.config.enabled || !baseUrl || !clientKey || !secret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timespan = String(Math.floor(Date.now() / 1000));
|
||||
const sign = createGoodsInfoSignature(timespan, secret);
|
||||
const commonHeaders = {
|
||||
Key: clientKey,
|
||||
Timespan: timespan,
|
||||
Sign: sign,
|
||||
Accept: "application/json",
|
||||
};
|
||||
const requestPaths = [
|
||||
`/api/GoodsInfo/SearchByProcode?procode=${encodeURIComponent(params.keyword)}`,
|
||||
`/api/GoodsInfo/SearchBySkuoid?skuoid=${encodeURIComponent(params.keyword)}`,
|
||||
];
|
||||
|
||||
for (const requestPath of requestPaths) {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${requestPath}`, {
|
||||
method: "GET",
|
||||
headers: commonHeaders,
|
||||
cache: "no-store",
|
||||
});
|
||||
const result = (await response.json().catch(() => ({}))) as ExternalGoodsInfoResponse;
|
||||
if (response.ok && result.success === true && result.data) {
|
||||
const brandName = getGoodsInfoBrandName(result.data);
|
||||
if (brandName) {
|
||||
return brandName;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Agent Workflow Reference Search] Goods brand query failed:", {
|
||||
requestPath,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapUpstreamErrorStatus(code: string) {
|
||||
if (
|
||||
code === "SKU_NOT_FOUND" ||
|
||||
@ -227,11 +352,18 @@ function sortCandidatesByUploadedAtDesc(candidates: AgentModeReferenceCandidate[
|
||||
function isCandidateMatchedBrand(
|
||||
candidate: AgentModeReferenceCandidate,
|
||||
brandName?: string
|
||||
) {
|
||||
return isBrandNameMatched(candidate.brandName, brandName);
|
||||
}
|
||||
|
||||
function isBrandNameMatched(
|
||||
candidateBrandName?: string | null,
|
||||
brandName?: string
|
||||
) {
|
||||
const normalizedRequestedBrand = normalizeComparableName(brandName);
|
||||
if (!normalizedRequestedBrand) return true;
|
||||
|
||||
const normalizedBrandName = normalizeComparableName(candidate.brandName);
|
||||
const normalizedBrandName = normalizeComparableName(candidateBrandName);
|
||||
if (!normalizedBrandName) return false;
|
||||
|
||||
return (
|
||||
@ -269,6 +401,12 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const setting = await getSystemSetting({ key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY });
|
||||
const config = normalizeAgentWorkflowMaterialApiConfig(setting?.value);
|
||||
const goodsInfoSetting = await getSystemSetting({
|
||||
key: AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY,
|
||||
});
|
||||
const goodsInfoConfig = normalizeAgentWorkflowGoodsInfoApiConfig(
|
||||
goodsInfoSetting?.value
|
||||
);
|
||||
if (!config.enabled) {
|
||||
return jsonNoStore(
|
||||
{ error: "素材搜索功能未开启,请先在系统设置中启用" },
|
||||
@ -285,12 +423,39 @@ export async function GET(request: NextRequest) {
|
||||
const normalizedKeyword = parsed.keyword.trim().toUpperCase();
|
||||
const queryType = parsed.queryType as AgentWorkflowMaterialQueryType;
|
||||
const requestedBrandName = String(parsed.brandName || "").trim();
|
||||
const verifiedGoodsBrandName = requestedBrandName
|
||||
? await fetchGoodsBrandByKeyword({
|
||||
keyword: normalizedKeyword,
|
||||
config: goodsInfoConfig,
|
||||
}).catch((error) => {
|
||||
console.warn("[Agent Workflow Reference Search] Goods brand lookup failed:", error);
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
if (
|
||||
requestedBrandName &&
|
||||
verifiedGoodsBrandName &&
|
||||
!isBrandNameMatched(verifiedGoodsBrandName, requestedBrandName)
|
||||
) {
|
||||
return jsonNoStore(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "BRAND_REFERENCE_NOT_FOUND",
|
||||
message: `当前搜索商品属于品牌「${verifiedGoodsBrandName}」,与当前选择品牌「${requestedBrandName}」不一致`,
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
const upstreamParams = new URLSearchParams({
|
||||
resourceType: "task-detail",
|
||||
[queryType]: normalizedKeyword,
|
||||
});
|
||||
if (requestedBrandName) {
|
||||
upstreamParams.set("brandName", requestedBrandName);
|
||||
upstreamParams.set("brand", requestedBrandName);
|
||||
upstreamParams.set("productBrand", requestedBrandName);
|
||||
}
|
||||
const upstreamUrl = `${normalizeBaseUrl(
|
||||
config.baseUrl
|
||||
@ -323,9 +488,15 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const upstreamBrandName = getExternalBrandName(upstreamResult.data);
|
||||
const fallbackBrandName =
|
||||
upstreamBrandName ||
|
||||
(verifiedGoodsBrandName &&
|
||||
isBrandNameMatched(verifiedGoodsBrandName, requestedBrandName)
|
||||
? verifiedGoodsBrandName
|
||||
: null);
|
||||
const sortedCandidates = sortCandidatesByUploadedAtDesc(
|
||||
(upstreamResult.data.candidates || []).map((item, index) =>
|
||||
normalizeCandidate(item, index, upstreamBrandName)
|
||||
normalizeCandidate(item, index, fallbackBrandName)
|
||||
)
|
||||
);
|
||||
const normalizedCandidates = filterCandidatesByBrand(sortedCandidates, requestedBrandName)
|
||||
@ -378,7 +549,7 @@ export async function GET(request: NextRequest) {
|
||||
matchedSku: String(upstreamResult.data.sku || "").trim() || null,
|
||||
brandName:
|
||||
selectedCandidate.brandName ||
|
||||
upstreamBrandName ||
|
||||
fallbackBrandName ||
|
||||
requestedBrandName ||
|
||||
null,
|
||||
categoryName: selectedCandidate.categoryName || String(upstreamResult.data.categoryName || "").trim() || null,
|
||||
|
||||
@ -1347,7 +1347,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const skuReferenceTarget = presetModeConfig.sku.referenceImageSource;
|
||||
const detailReferenceSourceLabel = getTaskDetailReferenceSourceLabel(detailReferenceTarget);
|
||||
const skuReferenceSourceLabel = getTaskDetailReferenceSourceLabel(skuReferenceTarget);
|
||||
const workflowBrandName = String(workflowContext?.brand || "").trim();
|
||||
const getCurrentWorkflowBrandName = useCallback(
|
||||
() => String((getLatestWorkflowContext?.() ?? workflowContext)?.brand || "").trim(),
|
||||
[getLatestWorkflowContext, workflowContext]
|
||||
);
|
||||
const workflowBrandName = getCurrentWorkflowBrandName();
|
||||
const shouldAllowModelOverride =
|
||||
isDirectModelGenerationMode || !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride;
|
||||
const brandModelAttachmentKeys = useMemo(() => {
|
||||
@ -3969,7 +3973,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}
|
||||
const handleDetailReplacementSearch = useCallback(async () => {
|
||||
const normalized = detailReplacementQueryInput.trim().toUpperCase();
|
||||
if (!workflowBrandName) {
|
||||
const currentWorkflowBrandName = getCurrentWorkflowBrandName();
|
||||
if (!currentWorkflowBrandName) {
|
||||
toast.error("请先在第一步完成品牌选择后再查询参考图");
|
||||
return;
|
||||
}
|
||||
@ -3989,9 +3994,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
queryType: "spu",
|
||||
target: detailReferenceTarget,
|
||||
});
|
||||
if (workflowBrandName) {
|
||||
searchParams.set("brandName", workflowBrandName);
|
||||
}
|
||||
searchParams.set("brandName", currentWorkflowBrandName);
|
||||
const response = await fetch(
|
||||
`/api/agent-workflow/reference-search?${searchParams.toString()}`,
|
||||
{ cache: "no-store" }
|
||||
@ -4026,8 +4029,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
detailReferenceSourceLabel,
|
||||
detailReferenceTarget,
|
||||
detailReplacementQueryInput,
|
||||
getCurrentWorkflowBrandName,
|
||||
updateDetailReplacementState,
|
||||
workflowBrandName,
|
||||
]);
|
||||
const handleSelectDetailReplacementCandidate = useCallback((candidateId: string) => {
|
||||
updateDetailReplacementState((current) => {
|
||||
@ -4069,7 +4072,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
);
|
||||
const handleSkuReplacementSearch = useCallback(async () => {
|
||||
const normalized = skuReplacementQueryInput.trim().toUpperCase();
|
||||
if (!workflowBrandName) {
|
||||
const currentWorkflowBrandName = getCurrentWorkflowBrandName();
|
||||
if (!currentWorkflowBrandName) {
|
||||
toast.error("请先在第一步完成品牌选择后再查询参考图");
|
||||
return;
|
||||
}
|
||||
@ -4089,9 +4093,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
queryType: skuReplacementQueryType,
|
||||
target: skuReferenceTarget,
|
||||
});
|
||||
if (workflowBrandName) {
|
||||
searchParams.set("brandName", workflowBrandName);
|
||||
}
|
||||
searchParams.set("brandName", currentWorkflowBrandName);
|
||||
const response = await fetch(
|
||||
`/api/agent-workflow/reference-search?${searchParams.toString()}`,
|
||||
{ cache: "no-store" }
|
||||
@ -4135,8 +4137,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
skuReferenceTarget,
|
||||
skuReplacementQueryInput,
|
||||
skuReplacementQueryType,
|
||||
getCurrentWorkflowBrandName,
|
||||
updateSkuReplacementState,
|
||||
workflowBrandName,
|
||||
]);
|
||||
const handleSelectSkuReplacementCandidate = useCallback((candidateId: string) => {
|
||||
updateSkuReplacementState((current) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user