704 lines
21 KiB
TypeScript
704 lines
21 KiB
TypeScript
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";
|
|
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,
|
|
AgentWorkflowMaterialQueryType,
|
|
} from "@/types/agent";
|
|
|
|
export const maxDuration = 180;
|
|
|
|
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(),
|
|
});
|
|
|
|
function jsonNoStore(
|
|
body: Parameters<typeof NextResponse.json>[0],
|
|
init?: ResponseInit
|
|
) {
|
|
return NextResponse.json(body, {
|
|
...init,
|
|
headers: {
|
|
"Cache-Control": "no-store, max-age=0",
|
|
...(init?.headers || {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
type ExternalTaskDetailCandidate = {
|
|
materialId?: string;
|
|
taskNo?: string;
|
|
productModel?: string;
|
|
brandName?: string;
|
|
brand?: string;
|
|
productBrand?: string;
|
|
brand_name?: string;
|
|
brand_name_cn?: string;
|
|
brandTitle?: string;
|
|
brand_title?: string;
|
|
categoryName?: string;
|
|
shopName?: string;
|
|
sceneName?: string;
|
|
uploadedAt?: string;
|
|
isFullSet?: boolean;
|
|
downloadUrls?: {
|
|
main?: string[];
|
|
sku?: string[];
|
|
detail?: string[];
|
|
all?: string[];
|
|
};
|
|
};
|
|
|
|
type ExternalTaskDetailResponse = {
|
|
success?: boolean;
|
|
data?: {
|
|
resourceType?: string;
|
|
spu?: string | null;
|
|
sku?: string | null;
|
|
brandName?: string | null;
|
|
brand?: string | null;
|
|
productBrand?: string | null;
|
|
brand_name?: string | null;
|
|
brand_name_cn?: string | null;
|
|
brandTitle?: string | null;
|
|
brand_title?: string | null;
|
|
categoryName?: string | null;
|
|
shopName?: string | null;
|
|
candidateCount?: number;
|
|
candidates?: ExternalTaskDetailCandidate[];
|
|
};
|
|
error?: {
|
|
code?: string;
|
|
message?: string;
|
|
};
|
|
};
|
|
|
|
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()
|
|
.toLowerCase()
|
|
.replace(/\s+/g, "");
|
|
}
|
|
|
|
function normalizeUrlList(value: unknown) {
|
|
if (!Array.isArray(value)) return [] as string[];
|
|
return Array.from(
|
|
new Set(
|
|
value
|
|
.map((item) => String(item || "").trim())
|
|
.filter((item) => /^https?:\/\//i.test(item))
|
|
)
|
|
).slice(0, 64);
|
|
}
|
|
|
|
function getExternalBrandName(value: {
|
|
brandName?: string | null;
|
|
brand?: string | null;
|
|
productBrand?: string | null;
|
|
brand_name?: string | null;
|
|
brand_name_cn?: string | null;
|
|
brandTitle?: string | null;
|
|
brand_title?: string | null;
|
|
}) {
|
|
return (
|
|
String(
|
|
value.brandName ||
|
|
value.brand ||
|
|
value.productBrand ||
|
|
value.brand_name ||
|
|
value.brand_name_cn ||
|
|
value.brandTitle ||
|
|
value.brand_title ||
|
|
""
|
|
).trim() || null
|
|
);
|
|
}
|
|
|
|
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" ||
|
|
code === "SPU_NOT_FOUND" ||
|
|
code === "TASK_DETAIL_NOT_FOUND" ||
|
|
code === "FULL_SET_NOT_FOUND"
|
|
) {
|
|
return 404;
|
|
}
|
|
if (code === "INVALID_API_KEY") {
|
|
return 500;
|
|
}
|
|
if (code === "SKU_NOT_UNIQUE") {
|
|
return 409;
|
|
}
|
|
return 502;
|
|
}
|
|
|
|
function normalizeCandidate(
|
|
value: ExternalTaskDetailCandidate,
|
|
index: number,
|
|
fallbackBrandName?: string | null
|
|
): AgentModeReferenceCandidate {
|
|
const uploadedAt = String(value.uploadedAt || "").trim() || null;
|
|
const taskNo = String(value.taskNo || "").trim() || null;
|
|
const materialId = String(value.materialId || "").trim() || null;
|
|
const sceneName = String(value.sceneName || "").trim() || null;
|
|
const id = taskNo || materialId || `candidate-${index + 1}`;
|
|
const brandName = getExternalBrandName(value) || fallbackBrandName || 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,
|
|
uploadedAt,
|
|
isFullSet: value.isFullSet !== false,
|
|
mainImageUrls: normalizeUrlList(value.downloadUrls?.main),
|
|
skuImageUrls: normalizeUrlList(value.downloadUrls?.sku),
|
|
detailImageUrls: normalizeUrlList(value.downloadUrls?.detail),
|
|
allImageUrls: normalizeUrlList(value.downloadUrls?.all),
|
|
};
|
|
}
|
|
|
|
function getCandidateTargetUrls(
|
|
candidate: AgentModeReferenceCandidate,
|
|
target: "main" | "detail" | "sku"
|
|
) {
|
|
if (target === "main") return candidate.mainImageUrls;
|
|
return target === "detail" ? candidate.detailImageUrls : candidate.skuImageUrls;
|
|
}
|
|
|
|
function getRenderableCandidateTargetUrls(
|
|
candidate: AgentModeReferenceCandidate,
|
|
target: "main" | "detail" | "sku"
|
|
) {
|
|
return getCandidateTargetUrls(candidate, target);
|
|
}
|
|
|
|
function buildReferenceItems(params: {
|
|
candidate: AgentModeReferenceCandidate;
|
|
target: "main" | "detail" | "sku";
|
|
}): AgentDetailReplacementReferenceItem[] {
|
|
const urls = getRenderableCandidateTargetUrls(params.candidate, params.target);
|
|
const targetLabel =
|
|
params.target === "main"
|
|
? "主图参考图"
|
|
: params.target === "detail"
|
|
? "详情参考图"
|
|
: "SKU参考图";
|
|
const prefix =
|
|
params.candidate.sceneName ||
|
|
params.candidate.taskNo ||
|
|
params.candidate.productModel ||
|
|
"候选整套";
|
|
|
|
return urls.map((url, index) => ({
|
|
id: `${params.candidate.id}-${params.target}-${index + 1}`,
|
|
label: `${prefix} ${targetLabel} ${String(index + 1).padStart(2, "0")}`,
|
|
imageUrl: url,
|
|
candidateId: params.candidate.id,
|
|
sortOrder: index,
|
|
passthrough: false,
|
|
}));
|
|
}
|
|
|
|
function sortCandidatesByUploadedAtDesc(candidates: AgentModeReferenceCandidate[]) {
|
|
return candidates.slice().sort((left, right) => {
|
|
const leftTime = left.uploadedAt ? new Date(left.uploadedAt).getTime() : 0;
|
|
const rightTime = right.uploadedAt ? new Date(right.uploadedAt).getTime() : 0;
|
|
return rightTime - leftTime;
|
|
});
|
|
}
|
|
|
|
function isCandidateMatchedBrand(
|
|
candidate: AgentModeReferenceCandidate,
|
|
brandName?: string
|
|
) {
|
|
return (
|
|
isBrandNameMatched(candidate.brandName, brandName) ||
|
|
isBrandNameMatched(candidate.shopName, brandName)
|
|
);
|
|
}
|
|
|
|
function isBrandNameMatched(
|
|
candidateBrandName?: string | null,
|
|
brandName?: string
|
|
) {
|
|
const normalizedRequestedBrand = normalizeComparableName(brandName);
|
|
if (!normalizedRequestedBrand) return true;
|
|
|
|
const normalizedBrandName = normalizeComparableName(candidateBrandName);
|
|
if (!normalizedBrandName) return false;
|
|
|
|
if (normalizedBrandName === normalizedRequestedBrand) return true;
|
|
|
|
// 只允许“候选品牌包含当前品牌”的弱匹配,避免短品牌名反向包含导致串品牌。
|
|
return (
|
|
normalizedRequestedBrand.length >= 2 &&
|
|
normalizedBrandName.includes(normalizedRequestedBrand)
|
|
);
|
|
}
|
|
|
|
function filterCandidatesByBrand(
|
|
candidates: AgentModeReferenceCandidate[],
|
|
brandName?: string
|
|
) {
|
|
const normalizedRequestedBrand = normalizeComparableName(brandName);
|
|
if (!normalizedRequestedBrand) return candidates;
|
|
|
|
return candidates.filter((candidate) =>
|
|
isCandidateMatchedBrand(candidate, normalizedRequestedBrand)
|
|
);
|
|
}
|
|
|
|
async function requestTaskDetailAttempt(params: {
|
|
keyword: string;
|
|
queryType: AgentWorkflowMaterialQueryType;
|
|
target: "main" | "detail" | "sku";
|
|
requestedBrandName: string;
|
|
verifiedGoodsBrandName?: string | null;
|
|
config: ReturnType<typeof normalizeAgentWorkflowMaterialApiConfig>;
|
|
}) {
|
|
const upstreamParams = new URLSearchParams({
|
|
resourceType: "task-detail",
|
|
[params.queryType]: params.keyword,
|
|
});
|
|
const upstreamUrl = `${normalizeBaseUrl(
|
|
params.config.baseUrl
|
|
)}/standard-model-white-background?${upstreamParams.toString()}`;
|
|
|
|
const upstreamResponse = await fetch(upstreamUrl, {
|
|
method: "GET",
|
|
headers: {
|
|
"X-API-Key": params.config.apiKey,
|
|
},
|
|
cache: "no-store",
|
|
});
|
|
const upstreamResult = (await upstreamResponse.json().catch(() => ({}))) as ExternalTaskDetailResponse;
|
|
const upstreamBrandName = getExternalBrandName(upstreamResult.data || {});
|
|
const rawCandidates = upstreamResult.data?.candidates || [];
|
|
const candidateBrandSamples = Array.from(
|
|
new Set(
|
|
rawCandidates
|
|
.flatMap((candidate) => [
|
|
getExternalBrandName(candidate),
|
|
String(candidate.shopName || "").trim() || null,
|
|
])
|
|
.filter((brandName): brandName is string => Boolean(brandName))
|
|
)
|
|
).slice(0, 8);
|
|
|
|
const hasVerifiedRequestedBrand =
|
|
Boolean(params.verifiedGoodsBrandName) &&
|
|
isBrandNameMatched(params.verifiedGoodsBrandName, params.requestedBrandName);
|
|
const hasUpstreamRequestedBrand =
|
|
Boolean(upstreamBrandName) &&
|
|
isBrandNameMatched(upstreamBrandName, params.requestedBrandName);
|
|
|
|
let fallbackBrandName: string | null = null;
|
|
if (params.requestedBrandName) {
|
|
if (hasVerifiedRequestedBrand) {
|
|
fallbackBrandName = params.verifiedGoodsBrandName || null;
|
|
} else if (hasUpstreamRequestedBrand) {
|
|
fallbackBrandName = upstreamBrandName || null;
|
|
}
|
|
} else {
|
|
fallbackBrandName = upstreamBrandName;
|
|
}
|
|
|
|
const normalizedCandidates =
|
|
upstreamResponse.ok && upstreamResult.success === true && upstreamResult.data
|
|
? filterCandidatesByBrand(
|
|
sortCandidatesByUploadedAtDesc(
|
|
rawCandidates.map((item, index) =>
|
|
normalizeCandidate(item, index, fallbackBrandName)
|
|
)
|
|
),
|
|
params.requestedBrandName,
|
|
).filter(
|
|
(candidate) => getRenderableCandidateTargetUrls(candidate, params.target).length > 0
|
|
)
|
|
: [];
|
|
|
|
return {
|
|
queryType: params.queryType,
|
|
upstreamResponse,
|
|
upstreamResult,
|
|
upstreamBrandName,
|
|
rawCandidates,
|
|
candidateBrandSamples,
|
|
fallbackBrandName,
|
|
normalizedCandidates,
|
|
};
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return jsonNoStore({ error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const parsed = querySchema.parse({
|
|
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,
|
|
});
|
|
|
|
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: "素材搜索功能未开启,请先在系统设置中启用" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
if (!config.apiKey) {
|
|
return jsonNoStore(
|
|
{ error: "素材搜索 API Key 未配置,请先在系统设置中保存" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
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)
|
|
) {
|
|
console.warn("[Agent Workflow Reference Search] Goods brand hint mismatched selected brand, keep searching with upstream data:", {
|
|
keyword: normalizedKeyword,
|
|
requestedBrandName,
|
|
verifiedGoodsBrandName,
|
|
});
|
|
}
|
|
|
|
const queryTypesToTry = Array.from(
|
|
new Set<AgentWorkflowMaterialQueryType>([
|
|
queryType,
|
|
queryType === "spu" ? "sku" : "spu",
|
|
])
|
|
);
|
|
|
|
let selectedAttempt:
|
|
| Awaited<ReturnType<typeof requestTaskDetailAttempt>>
|
|
| null = null;
|
|
let firstUpstreamFailure:
|
|
| Awaited<ReturnType<typeof requestTaskDetailAttempt>>
|
|
| null = null;
|
|
let firstEmptyAttempt:
|
|
| Awaited<ReturnType<typeof requestTaskDetailAttempt>>
|
|
| null = null;
|
|
|
|
for (const currentQueryType of queryTypesToTry) {
|
|
const attempt = await requestTaskDetailAttempt({
|
|
keyword: normalizedKeyword,
|
|
queryType: currentQueryType,
|
|
target: parsed.target,
|
|
requestedBrandName,
|
|
verifiedGoodsBrandName,
|
|
config,
|
|
});
|
|
|
|
if (requestedBrandName) {
|
|
console.info("[Agent Workflow Reference Search] Brand gate:", {
|
|
keyword: normalizedKeyword,
|
|
queryType: currentQueryType,
|
|
target: parsed.target,
|
|
requestedBrandName,
|
|
verifiedGoodsBrandName,
|
|
upstreamBrandName: attempt.upstreamBrandName,
|
|
candidateCount: attempt.rawCandidates.length,
|
|
candidateBrandSamples: attempt.candidateBrandSamples,
|
|
});
|
|
}
|
|
|
|
if (
|
|
attempt.upstreamResponse.ok &&
|
|
attempt.upstreamResult.success === true &&
|
|
attempt.upstreamResult.data
|
|
) {
|
|
if (!firstEmptyAttempt) {
|
|
firstEmptyAttempt = attempt;
|
|
}
|
|
if (attempt.normalizedCandidates.length > 0) {
|
|
selectedAttempt = attempt;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!firstUpstreamFailure) {
|
|
firstUpstreamFailure = attempt;
|
|
}
|
|
}
|
|
|
|
if (!selectedAttempt) {
|
|
if (firstEmptyAttempt) {
|
|
return jsonNoStore(
|
|
{
|
|
success: false,
|
|
error: {
|
|
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: requestedBrandName
|
|
? `当前品牌「${requestedBrandName}」下没有可用的${
|
|
parsed.target === "main"
|
|
? "主图参考图"
|
|
: parsed.target === "detail"
|
|
? "详情参考图"
|
|
: "SKU参考图"
|
|
}`
|
|
: parsed.target === "main"
|
|
? "当前整套素材中没有可用的主图参考图"
|
|
: parsed.target === "detail"
|
|
? "当前整套素材中没有可用的详情参考图"
|
|
: "当前整套素材中没有可用的 SKU 参考图",
|
|
},
|
|
},
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
const errorCode = String(
|
|
firstUpstreamFailure?.upstreamResult.error?.code || "UPSTREAM_ERROR"
|
|
).trim();
|
|
const errorMessage =
|
|
String(firstUpstreamFailure?.upstreamResult.error?.message || "").trim() ||
|
|
`参考图搜索失败(${firstUpstreamFailure?.upstreamResponse.status || 500})`;
|
|
|
|
return jsonNoStore(
|
|
{
|
|
success: false,
|
|
error: {
|
|
code: errorCode,
|
|
message: errorMessage,
|
|
},
|
|
},
|
|
{ status: mapUpstreamErrorStatus(errorCode) }
|
|
);
|
|
}
|
|
|
|
if (selectedAttempt.queryType !== queryType) {
|
|
console.info("[Agent Workflow Reference Search] Query type fallback succeeded:", {
|
|
keyword: normalizedKeyword,
|
|
requestedQueryType: queryType,
|
|
resolvedQueryType: selectedAttempt.queryType,
|
|
target: parsed.target,
|
|
});
|
|
}
|
|
|
|
const normalizedCandidates = selectedAttempt.normalizedCandidates;
|
|
const selectedCandidate = normalizedCandidates[0];
|
|
const references = buildReferenceItems({
|
|
candidate: selectedCandidate,
|
|
target: parsed.target,
|
|
});
|
|
|
|
return jsonNoStore({
|
|
success: true,
|
|
data: {
|
|
query: normalizedKeyword,
|
|
queryType: selectedAttempt.queryType,
|
|
target: parsed.target,
|
|
matchedSpu: String(selectedAttempt.upstreamResult.data?.spu || "").trim() || null,
|
|
matchedSku: String(selectedAttempt.upstreamResult.data?.sku || "").trim() || null,
|
|
brandName:
|
|
selectedCandidate.brandName ||
|
|
selectedAttempt.fallbackBrandName ||
|
|
selectedCandidate.shopName ||
|
|
null,
|
|
categoryName:
|
|
selectedCandidate.categoryName ||
|
|
String(selectedAttempt.upstreamResult.data?.categoryName || "").trim() ||
|
|
null,
|
|
shopName:
|
|
selectedCandidate.shopName ||
|
|
String(selectedAttempt.upstreamResult.data?.shopName || "").trim() ||
|
|
null,
|
|
candidateCount: normalizedCandidates.length,
|
|
candidates: normalizedCandidates,
|
|
selectedCandidateId: selectedCandidate.id,
|
|
references,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("[Agent Workflow Reference Search] Error:", error);
|
|
|
|
if (error instanceof z.ZodError) {
|
|
return jsonNoStore(
|
|
{ error: "搜索参数无效", details: error.errors },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
return jsonNoStore(
|
|
{ error: "参考图搜索失败,请稍后重试" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|