feat: connect product detail image references
This commit is contained in:
parent
19970915fd
commit
e6d4c31120
@ -150,6 +150,7 @@ import type {
|
||||
AgentWorkflowFourthCategoryEntry,
|
||||
AgentWorkflowGoodsInfoApiConfig,
|
||||
AgentWorkflowMaterialApiConfig,
|
||||
AgentWorkflowProductDetailChannel,
|
||||
AgentWorkflowOptionUiConfig,
|
||||
AgentPsdTextColorConfig,
|
||||
AgentWorkflowStoreEntry,
|
||||
@ -6523,6 +6524,153 @@ export function AgentWorkflowSettingsClient({
|
||||
placeholder="输入 API Key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">启用商品ID详情图接口</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
详情模式可按商品ID和渠道拉取参考详情图,密钥仅保存在服务端配置中
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={materialApiConfig.productDetailEnabled === true}
|
||||
onCheckedChange={(checked) =>
|
||||
setMaterialApiConfig((prev) => ({
|
||||
...prev,
|
||||
productDetailEnabled: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">商品ID详情图接口基地址</Label>
|
||||
<Input
|
||||
value={materialApiConfig.productDetailBaseUrl || ""}
|
||||
onChange={(e) =>
|
||||
setMaterialApiConfig((prev) => ({
|
||||
...prev,
|
||||
productDetailBaseUrl: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="http://服务地址:端口"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">调用方式</Label>
|
||||
<Select
|
||||
value={materialApiConfig.productDetailMethod || "GET"}
|
||||
onValueChange={(value) =>
|
||||
setMaterialApiConfig((prev) => ({
|
||||
...prev,
|
||||
productDetailMethod: value === "POST" ? "POST" : "GET",
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择调用方式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET /open-api/detail-images</SelectItem>
|
||||
<SelectItem value="POST">POST /open-api/detail-images/query</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">默认渠道</Label>
|
||||
<Select
|
||||
value={materialApiConfig.productDetailDefaultChannel || "taobao"}
|
||||
onValueChange={(value) =>
|
||||
setMaterialApiConfig((prev) => ({
|
||||
...prev,
|
||||
productDetailDefaultChannel: value as AgentWorkflowProductDetailChannel,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择默认渠道" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="taobao">淘宝 taobao</SelectItem>
|
||||
<SelectItem value="1688">1688</SelectItem>
|
||||
<SelectItem value="douyin">抖音 douyin</SelectItem>
|
||||
<SelectItem value="jd">京东 jd</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">接口路径 / 完整地址</Label>
|
||||
<Input
|
||||
value={materialApiConfig.productDetailPath || ""}
|
||||
onChange={(e) =>
|
||||
setMaterialApiConfig((prev) => ({
|
||||
...prev,
|
||||
productDetailPath: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="/open-api/detail-images"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
GET 默认使用 /open-api/detail-images,POST 默认使用 /open-api/detail-images/query;也支持 {"{{item_id}}"}、{"{{productId}}"}、{"{{channel}}"} 占位符。
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">商品ID接口 API Key</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={materialApiConfig.productDetailApiKey || ""}
|
||||
onChange={(e) =>
|
||||
setMaterialApiConfig((prev) => ({
|
||||
...prev,
|
||||
productDetailApiKey: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="留空则复用上方 API Key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">同步分析补全</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
默认随查询触发详情分析并回写
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={materialApiConfig.productDetailSyncAnalysis !== false}
|
||||
onCheckedChange={(checked) =>
|
||||
setMaterialApiConfig((prev) => ({
|
||||
...prev,
|
||||
productDetailSyncAnalysis: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">强制重跑分析</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
通常保持关闭,只在管理员排查时开启
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={materialApiConfig.productDetailForceReanalyze === true}
|
||||
onCheckedChange={(checked) =>
|
||||
setMaterialApiConfig((prev) => ({
|
||||
...prev,
|
||||
productDetailForceReanalyze: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
@ -92,7 +92,7 @@ const agentModePayloadSchema = z
|
||||
.object({
|
||||
modeType: z.enum(["detail_replacement", "sku_replacement"]),
|
||||
query: z.string().max(64).optional(),
|
||||
queryType: z.enum(["sku", "spu"]).optional(),
|
||||
queryType: z.enum(["sku", "spu", "productId"]).optional(),
|
||||
selectedCandidateId: z.string().min(1).max(128).nullable().optional(),
|
||||
references: z.array(agentModeReferenceSchema).max(40),
|
||||
sourceImages: z
|
||||
|
||||
@ -16,15 +16,18 @@ import type {
|
||||
AgentDetailReplacementReferenceItem,
|
||||
AgentModeReferenceCandidate,
|
||||
AgentWorkflowMaterialQueryType,
|
||||
AgentWorkflowProductDetailChannel,
|
||||
AgentWorkflowReferenceQueryType,
|
||||
} 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"),
|
||||
queryType: z.enum(["sku", "spu", "productId"]).default("spu"),
|
||||
target: z.enum(["main", "detail", "sku"]),
|
||||
brandName: z.string().trim().max(64).optional(),
|
||||
channel: z.enum(["taobao", "1688", "douyin", "jd"]).optional(),
|
||||
});
|
||||
|
||||
function jsonNoStore(
|
||||
@ -472,6 +475,285 @@ async function requestTaskDetailAttempt(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function buildProductDetailRequestUrl(params: {
|
||||
productId: string;
|
||||
channel: AgentWorkflowProductDetailChannel;
|
||||
method: "GET" | "POST";
|
||||
config: ReturnType<typeof normalizeAgentWorkflowMaterialApiConfig>;
|
||||
}) {
|
||||
const baseUrl = normalizeBaseUrl(params.config.productDetailBaseUrl || "");
|
||||
const rawPath =
|
||||
String(params.config.productDetailPath || "").trim() ||
|
||||
(params.method === "POST"
|
||||
? "/open-api/detail-images/query"
|
||||
: "/open-api/detail-images");
|
||||
if (!baseUrl && !/^https?:\/\//i.test(rawPath)) {
|
||||
throw new Error("商品ID详情图接口基地址未配置");
|
||||
}
|
||||
|
||||
let requestUrl = /^https?:\/\//i.test(rawPath)
|
||||
? rawPath
|
||||
: `${baseUrl}/${rawPath.replace(/^\/+/, "")}`;
|
||||
const encodedProductId = encodeURIComponent(params.productId);
|
||||
const encodedChannel = encodeURIComponent(params.channel);
|
||||
const beforeReplace = requestUrl;
|
||||
requestUrl = requestUrl
|
||||
.replace(/\{\{\s*item_id\s*\}\}/gi, encodedProductId)
|
||||
.replace(/\{\s*item_id\s*\}/gi, encodedProductId)
|
||||
.replace(/\{\{\s*productId\s*\}\}/gi, encodedProductId)
|
||||
.replace(/\{\s*productId\s*\}/gi, encodedProductId)
|
||||
.replace(/\{\{\s*channel\s*\}\}/gi, encodedChannel)
|
||||
.replace(/\{\s*channel\s*\}/gi, encodedChannel);
|
||||
|
||||
if (params.method === "GET") {
|
||||
const url = new URL(requestUrl);
|
||||
if (!url.searchParams.has("item_id")) {
|
||||
url.searchParams.set("item_id", params.productId);
|
||||
}
|
||||
if (!url.searchParams.has("channel")) {
|
||||
url.searchParams.set("channel", params.channel);
|
||||
}
|
||||
if (!url.searchParams.has("refresh")) {
|
||||
url.searchParams.set("refresh", "false");
|
||||
}
|
||||
if (!url.searchParams.has("sync_analysis")) {
|
||||
url.searchParams.set(
|
||||
"sync_analysis",
|
||||
params.config.productDetailSyncAnalysis === false ? "false" : "true"
|
||||
);
|
||||
}
|
||||
if (!url.searchParams.has("force_reanalyze")) {
|
||||
url.searchParams.set(
|
||||
"force_reanalyze",
|
||||
params.config.productDetailForceReanalyze === true ? "true" : "false"
|
||||
);
|
||||
}
|
||||
requestUrl = url.toString();
|
||||
} else if (requestUrl === beforeReplace) {
|
||||
return requestUrl;
|
||||
}
|
||||
|
||||
return requestUrl;
|
||||
}
|
||||
|
||||
function getProductDetailResponseData(value: unknown) {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
return raw.data && typeof raw.data === "object" && !Array.isArray(raw.data)
|
||||
? (raw.data as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function extractProductDetailImageUrls(value: unknown) {
|
||||
const matchedUrls = new Set<string>();
|
||||
const keyPattern = /(detail|desc|description|product).*?(image|img|pic|url|urls)|(image|img|pic|url|urls).*?(detail|desc|description|product)|^(images?|imageUrls?|detailImages?|detailImageUrls?|pics?|urls?)$/i;
|
||||
|
||||
const visit = (input: unknown, keyHint = "", forced = false, depth = 0) => {
|
||||
if (depth > 8 || matchedUrls.size >= 64) return;
|
||||
|
||||
if (typeof input === "string") {
|
||||
const normalized = input.trim();
|
||||
if ((forced || keyPattern.test(keyHint)) && /^https?:\/\//i.test(normalized)) {
|
||||
matchedUrls.add(normalized);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
const nextForced = forced || keyPattern.test(keyHint);
|
||||
input.forEach((item) => visit(item, keyHint, nextForced, depth + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!input || typeof input !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(input as Record<string, unknown>).forEach(([key, child]) => {
|
||||
visit(child, key, forced || keyPattern.test(key), depth + 1);
|
||||
});
|
||||
};
|
||||
|
||||
visit(value, Array.isArray(value) ? "images" : "", Array.isArray(value), 0);
|
||||
return Array.from(matchedUrls);
|
||||
}
|
||||
|
||||
async function requestProductDetailReferences(params: {
|
||||
productId: string;
|
||||
channel: AgentWorkflowProductDetailChannel;
|
||||
config: ReturnType<typeof normalizeAgentWorkflowMaterialApiConfig>;
|
||||
}) {
|
||||
if (!params.config.productDetailEnabled) {
|
||||
return jsonNoStore(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "PRODUCT_DETAIL_API_DISABLED",
|
||||
message: "商品ID详情图接口未启用,请先在系统设置中配置",
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!params.config.productDetailBaseUrl && !params.config.productDetailPath) {
|
||||
return jsonNoStore(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "PRODUCT_DETAIL_API_NOT_CONFIGURED",
|
||||
message: "商品ID详情图接口地址未配置,请先在系统设置中填写",
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const apiKey = String(params.config.productDetailApiKey || params.config.apiKey || "").trim();
|
||||
if (!apiKey) {
|
||||
return jsonNoStore(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "PRODUCT_DETAIL_API_KEY_MISSING",
|
||||
message: "商品ID详情图接口 API Key 未配置,请先在系统设置中填写",
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const method = params.config.productDetailMethod === "POST" ? "POST" : "GET";
|
||||
const requestUrl = buildProductDetailRequestUrl({
|
||||
...params,
|
||||
method,
|
||||
});
|
||||
const requestBody = {
|
||||
item_id: params.productId,
|
||||
channel: params.channel,
|
||||
refresh: false,
|
||||
sync_analysis: params.config.productDetailSyncAnalysis !== false,
|
||||
force_reanalyze: params.config.productDetailForceReanalyze === true,
|
||||
};
|
||||
const response = await fetch(requestUrl, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"X-API-KEY": apiKey,
|
||||
...(method === "POST" ? { "Content-Type": "application/json" } : {}),
|
||||
},
|
||||
body: method === "POST" ? JSON.stringify(requestBody) : undefined,
|
||||
cache: "no-store",
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
const responseRecord =
|
||||
result && typeof result === "object" && !Array.isArray(result)
|
||||
? (result as Record<string, unknown>)
|
||||
: {};
|
||||
if (responseRecord.success === false) {
|
||||
const errorCode = sanitizeText(responseRecord.error, 128) || "PRODUCT_DETAIL_API_ERROR";
|
||||
const errorMessage =
|
||||
sanitizeText(responseRecord.message, 255) ||
|
||||
sanitizeText(responseRecord.error, 255) ||
|
||||
"商品ID详情图查询失败";
|
||||
return jsonNoStore(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: errorCode,
|
||||
message: errorMessage,
|
||||
},
|
||||
},
|
||||
{ status: response.ok ? 502 : response.status }
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
return jsonNoStore(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "PRODUCT_DETAIL_API_ERROR",
|
||||
message: `商品ID详情图查询失败(${response.status})`,
|
||||
},
|
||||
},
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
const productData = getProductDetailResponseData(result);
|
||||
const detailImageUrls = normalizeUrlList(productData.detailImages);
|
||||
const fallbackDetailImageUrls =
|
||||
detailImageUrls.length > 0 ? detailImageUrls : extractProductDetailImageUrls(result);
|
||||
if (fallbackDetailImageUrls.length === 0) {
|
||||
return jsonNoStore(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "PRODUCT_DETAIL_IMAGES_NOT_FOUND",
|
||||
message: "商品ID接口未返回可用详情图片",
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
const mainImageUrls = normalizeUrlList(productData.mainImages);
|
||||
const resolvedItemId =
|
||||
sanitizeText(productData.itemId, 128) ||
|
||||
sanitizeText(productData.sourceItemId, 128) ||
|
||||
params.productId;
|
||||
const title = sanitizeText(productData.title, 255);
|
||||
const shopName = sanitizeText(productData.shopName, 128) || null;
|
||||
const channelLabel =
|
||||
sanitizeText(productData.channelLabel, 32) ||
|
||||
(params.channel === "taobao"
|
||||
? "淘宝"
|
||||
: params.channel === "douyin"
|
||||
? "抖音"
|
||||
: params.channel === "jd"
|
||||
? "京东"
|
||||
: "1688");
|
||||
|
||||
const candidate: AgentModeReferenceCandidate = {
|
||||
id: `product-id-${params.channel}-${resolvedItemId}`,
|
||||
materialId: resolvedItemId,
|
||||
taskNo: sanitizeText(responseRecord.request_id, 128) || null,
|
||||
productModel: title || resolvedItemId,
|
||||
brandName: null,
|
||||
categoryName: null,
|
||||
shopName,
|
||||
sceneName: title || `${channelLabel}商品 ${resolvedItemId}`,
|
||||
uploadedAt: sanitizeText(productData.fetchedAt, 64) || new Date().toISOString(),
|
||||
isFullSet: true,
|
||||
mainImageUrls,
|
||||
skuImageUrls: [],
|
||||
detailImageUrls: fallbackDetailImageUrls,
|
||||
allImageUrls: Array.from(new Set([...mainImageUrls, ...fallbackDetailImageUrls])),
|
||||
};
|
||||
const references = buildReferenceItems({
|
||||
candidate,
|
||||
target: "detail",
|
||||
});
|
||||
|
||||
return jsonNoStore({
|
||||
success: true,
|
||||
data: {
|
||||
query: resolvedItemId,
|
||||
queryType: "productId" as const,
|
||||
target: "detail" as const,
|
||||
matchedSpu: null,
|
||||
matchedSku: resolvedItemId,
|
||||
brandName: null,
|
||||
categoryName: null,
|
||||
shopName,
|
||||
channel: params.channel,
|
||||
candidateCount: 1,
|
||||
candidates: [candidate],
|
||||
selectedCandidateId: candidate.id,
|
||||
references,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
@ -484,6 +766,7 @@ export async function GET(request: NextRequest) {
|
||||
queryType: request.nextUrl.searchParams.get("queryType") || "spu",
|
||||
target: request.nextUrl.searchParams.get("target"),
|
||||
brandName: request.nextUrl.searchParams.get("brandName") || undefined,
|
||||
channel: request.nextUrl.searchParams.get("channel") || undefined,
|
||||
});
|
||||
|
||||
const setting = await getSystemSetting({ key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY });
|
||||
@ -494,6 +777,31 @@ export async function GET(request: NextRequest) {
|
||||
const goodsInfoConfig = normalizeAgentWorkflowGoodsInfoApiConfig(
|
||||
goodsInfoSetting?.value
|
||||
);
|
||||
const queryType = parsed.queryType as AgentWorkflowReferenceQueryType;
|
||||
const normalizedKeyword =
|
||||
queryType === "productId" ? parsed.keyword.trim() : parsed.keyword.trim().toUpperCase();
|
||||
if (queryType === "productId") {
|
||||
if (parsed.target !== "detail") {
|
||||
return jsonNoStore(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: "UNSUPPORTED_PRODUCT_ID_TARGET",
|
||||
message: "商品ID查询当前仅支持详情图",
|
||||
},
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const productDetailChannel =
|
||||
parsed.channel || config.productDetailDefaultChannel || "taobao";
|
||||
return requestProductDetailReferences({
|
||||
productId: normalizedKeyword,
|
||||
channel: productDetailChannel,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
if (!config.enabled) {
|
||||
return jsonNoStore(
|
||||
{ error: "素材搜索功能未开启,请先在系统设置中启用" },
|
||||
@ -507,8 +815,7 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedKeyword = parsed.keyword.trim().toUpperCase();
|
||||
const queryType = parsed.queryType as AgentWorkflowMaterialQueryType;
|
||||
const materialQueryType = queryType as AgentWorkflowMaterialQueryType;
|
||||
const requestedBrandName = String(parsed.brandName || "").trim();
|
||||
const verifiedGoodsBrandName = requestedBrandName
|
||||
? await fetchGoodsBrandByKeyword({
|
||||
@ -533,8 +840,8 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const queryTypesToTry = Array.from(
|
||||
new Set<AgentWorkflowMaterialQueryType>([
|
||||
queryType,
|
||||
queryType === "spu" ? "sku" : "spu",
|
||||
materialQueryType,
|
||||
materialQueryType === "spu" ? "sku" : "spu",
|
||||
])
|
||||
);
|
||||
|
||||
@ -642,10 +949,10 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedAttempt.queryType !== queryType) {
|
||||
if (selectedAttempt.queryType !== materialQueryType) {
|
||||
console.info("[Agent Workflow Reference Search] Query type fallback succeeded:", {
|
||||
keyword: normalizedKeyword,
|
||||
requestedQueryType: queryType,
|
||||
requestedQueryType: materialQueryType,
|
||||
resolvedQueryType: selectedAttempt.queryType,
|
||||
target: parsed.target,
|
||||
});
|
||||
|
||||
@ -1635,6 +1635,14 @@ const updateSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
baseUrl: z.string().trim().max(255).optional(),
|
||||
apiKey: z.string().trim().max(500).optional(),
|
||||
productDetailEnabled: z.boolean().optional(),
|
||||
productDetailBaseUrl: z.string().trim().max(500).optional(),
|
||||
productDetailApiKey: z.string().trim().max(500).optional(),
|
||||
productDetailPath: z.string().trim().max(500).optional(),
|
||||
productDetailMethod: z.enum(["GET", "POST"]).optional(),
|
||||
productDetailDefaultChannel: z.enum(["taobao", "1688", "douyin", "jd"]).optional(),
|
||||
productDetailSyncAnalysis: z.boolean().optional(),
|
||||
productDetailForceReanalyze: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
agentWorkflowGoodsInfoApiConfig: z
|
||||
|
||||
@ -111,6 +111,8 @@ import type {
|
||||
AgentWorkflowBusinessModeConfig,
|
||||
AgentWorkflowBusinessModeSettings,
|
||||
AgentWorkflowMaterialQueryType,
|
||||
AgentWorkflowProductDetailChannel,
|
||||
AgentWorkflowReferenceQueryType,
|
||||
AgentWorkflowPageFeatureSettings,
|
||||
AgentWorkflowSelectedBrandModel,
|
||||
} from "@/types/agent";
|
||||
@ -177,6 +179,15 @@ const MAX_CHAT_ATTACHMENT_NAME_LENGTH = 100;
|
||||
const MAX_AGENT_MODE_QUERY_LENGTH = 64;
|
||||
const MAX_AGENT_MODE_REFERENCE_COUNT = 40;
|
||||
const DIRECT_MODEL_UPLOAD_SLOT_COUNT = 4;
|
||||
const PRODUCT_DETAIL_CHANNEL_OPTIONS: Array<{
|
||||
value: AgentWorkflowProductDetailChannel;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "taobao", label: "淘宝" },
|
||||
{ value: "1688", label: "1688" },
|
||||
{ value: "douyin", label: "抖音" },
|
||||
{ value: "jd", label: "京东" },
|
||||
];
|
||||
const CHAT_UUID_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const ALLOWED_CHAT_MEDIA_TYPES = new Set([
|
||||
@ -253,13 +264,14 @@ interface AgentModeReferenceSearchResponse {
|
||||
success?: boolean;
|
||||
data?: {
|
||||
query: string;
|
||||
queryType: AgentWorkflowMaterialQueryType;
|
||||
queryType: AgentWorkflowReferenceQueryType;
|
||||
target: AgentTaskDetailReferenceSource;
|
||||
matchedSpu?: string | null;
|
||||
matchedSku?: string | null;
|
||||
brandName?: string | null;
|
||||
categoryName?: string | null;
|
||||
shopName?: string | null;
|
||||
channel?: AgentWorkflowProductDetailChannel | null;
|
||||
candidateCount: number;
|
||||
candidates: AgentModeReferenceCandidate[];
|
||||
selectedCandidateId?: string | null;
|
||||
@ -1265,6 +1277,14 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
isLoading: false,
|
||||
lastQuery: null,
|
||||
});
|
||||
const detailReferenceUploadInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [detailReferenceUploadState, setDetailReferenceUploadState] = useState<{
|
||||
isUploading: boolean;
|
||||
count: number;
|
||||
}>({
|
||||
isUploading: false,
|
||||
count: 0,
|
||||
});
|
||||
const [detailReferenceStitchDialogOpen, setDetailReferenceStitchDialogOpen] = useState(false);
|
||||
const [activeDetailReferenceCardId, setActiveDetailReferenceCardId] = useState<string | null>(null);
|
||||
const [skuReplacementSearchState, setSkuReplacementSearchState] = useState<{
|
||||
@ -1412,6 +1432,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
: "reference_replace";
|
||||
const detailReplacementReferenceCards = detailReplacementState.references;
|
||||
const detailReplacementQueryType = detailReplacementState.queryType || "spu";
|
||||
const detailReplacementProductDetailChannel =
|
||||
detailReplacementState.productDetailChannel || "taobao";
|
||||
const skuReplacementState = resolvedAgentModeState.skuReplacement;
|
||||
const skuReplacementReferenceCards = skuReplacementState.references;
|
||||
const presetModeConfig = useMemo(
|
||||
@ -1939,7 +1961,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
isDetailReplacementPreset: boolean;
|
||||
isSkuReplacementPreset: boolean;
|
||||
detailReplacementQueryInput: string;
|
||||
detailReplacementQueryType: AgentWorkflowMaterialQueryType;
|
||||
detailReplacementQueryType: AgentWorkflowReferenceQueryType;
|
||||
detailReplacementSelectedCandidateId: string | null;
|
||||
detailReplacementReferenceCards: typeof detailReplacementReferenceCards;
|
||||
detailReplacementProductSourceImages: typeof detailReplacementProductSourceImages;
|
||||
@ -2696,20 +2718,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
),
|
||||
detail_replacement: Array.from(
|
||||
new Set([
|
||||
...detailReplacementResultUrls,
|
||||
...excludeDeletedResultUrls(
|
||||
resultCollections?.detail_replacement,
|
||||
normalizedDeletedResultCollections.detail_replacement
|
||||
),
|
||||
...detailReplacementResultUrls,
|
||||
])
|
||||
),
|
||||
sku_replacement: Array.from(
|
||||
new Set([
|
||||
...skuReplacementResultUrls,
|
||||
...excludeDeletedResultUrls(
|
||||
resultCollections?.sku_replacement,
|
||||
normalizedDeletedResultCollections.sku_replacement
|
||||
),
|
||||
...skuReplacementResultUrls,
|
||||
])
|
||||
),
|
||||
};
|
||||
@ -4701,15 +4723,143 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
setPendingGeneration(null);
|
||||
}
|
||||
}
|
||||
const handleDetailReplacementQueryTypeChange = useCallback(
|
||||
(queryType: AgentWorkflowReferenceQueryType) => {
|
||||
updateDetailReplacementState((current) => ({
|
||||
...current,
|
||||
queryType,
|
||||
candidates: current.queryType === queryType ? current.candidates : [],
|
||||
selectedCandidateId:
|
||||
current.queryType === queryType ? current.selectedCandidateId : null,
|
||||
references: current.queryType === queryType ? current.references : [],
|
||||
}));
|
||||
},
|
||||
[updateDetailReplacementState]
|
||||
);
|
||||
const handleDetailReplacementProductDetailChannelChange = useCallback(
|
||||
(channel: AgentWorkflowProductDetailChannel) => {
|
||||
updateDetailReplacementState((current) => ({
|
||||
...current,
|
||||
productDetailChannel: channel,
|
||||
}));
|
||||
},
|
||||
[updateDetailReplacementState]
|
||||
);
|
||||
const handleDetailReferenceUploadChange = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const inputElement = event.currentTarget;
|
||||
const rawFiles = Array.from(inputElement.files || []);
|
||||
const imageFiles = rawFiles.filter((file) => file.type.startsWith("image/"));
|
||||
if (rawFiles.length > 0 && imageFiles.length === 0) {
|
||||
toast.error("请上传图片文件");
|
||||
inputElement.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const availableSlotCount = Math.max(
|
||||
MAX_AGENT_MODE_REFERENCE_COUNT - detailReplacementReferenceCards.length,
|
||||
0
|
||||
);
|
||||
if (availableSlotCount <= 0) {
|
||||
toast.error(`最多支持 ${MAX_AGENT_MODE_REFERENCE_COUNT} 张参考图,请先删除部分图片`);
|
||||
inputElement.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadFiles = imageFiles.slice(0, availableSlotCount);
|
||||
if (imageFiles.length > uploadFiles.length) {
|
||||
toast.warning(`已达到参考图上限,本次只上传前 ${uploadFiles.length} 张`);
|
||||
}
|
||||
if (uploadFiles.length === 0) {
|
||||
inputElement.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
setDetailReferenceUploadState({
|
||||
isUploading: true,
|
||||
count: uploadFiles.length,
|
||||
});
|
||||
|
||||
try {
|
||||
const uploadedItems = await Promise.all(
|
||||
uploadFiles.map(async (file, index) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const response = await fetchUploadWithRetry(formData);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !data?.url) {
|
||||
throw new Error(data?.error || `${file.name || `第 ${index + 1} 张`}上传失败`);
|
||||
}
|
||||
return {
|
||||
index,
|
||||
file,
|
||||
url: String(data.url),
|
||||
};
|
||||
})
|
||||
);
|
||||
const timestamp = Date.now();
|
||||
const orderedItems = uploadedItems.slice().sort((left, right) => left.index - right.index);
|
||||
|
||||
updateDetailReplacementState((current) => {
|
||||
const baseLength = current.references.length;
|
||||
const nextReferences = [
|
||||
...current.references,
|
||||
...orderedItems.map((item, index) => ({
|
||||
id: `detail-upload-${timestamp}-${index + 1}-${generateUUID().slice(0, 8)}`,
|
||||
label: item.file.name
|
||||
? `上传详情图 ${baseLength + index + 1} · ${item.file.name}`
|
||||
: `上传详情图 ${baseLength + index + 1}`,
|
||||
imageUrl: item.url,
|
||||
candidateId: null,
|
||||
sortOrder: baseLength + index,
|
||||
passthrough: false,
|
||||
assignedProductIndex: null,
|
||||
})),
|
||||
]
|
||||
.slice(0, MAX_AGENT_MODE_REFERENCE_COUNT)
|
||||
.map((card, index) => ({
|
||||
...card,
|
||||
sortOrder: index,
|
||||
}));
|
||||
|
||||
return {
|
||||
...current,
|
||||
references: nextReferences,
|
||||
};
|
||||
});
|
||||
toast.success(`已按文件选择顺序上传 ${orderedItems.length} 张参考详情图`);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "上传参考详情图失败");
|
||||
} finally {
|
||||
setDetailReferenceUploadState({
|
||||
isUploading: false,
|
||||
count: 0,
|
||||
});
|
||||
inputElement.value = "";
|
||||
}
|
||||
},
|
||||
[detailReplacementReferenceCards.length, updateDetailReplacementState]
|
||||
);
|
||||
const handleDetailReplacementSearch = useCallback(async () => {
|
||||
const normalized = detailReplacementQueryInput.trim().toUpperCase();
|
||||
const normalized =
|
||||
detailReplacementQueryType === "productId"
|
||||
? detailReplacementQueryInput.trim()
|
||||
: detailReplacementQueryInput.trim().toUpperCase();
|
||||
const currentWorkflowBrandName = getCurrentWorkflowBrandName();
|
||||
if (!currentWorkflowBrandName) {
|
||||
if (!currentWorkflowBrandName && detailReplacementQueryType !== "productId") {
|
||||
toast.error("请先在第一步完成品牌选择后再查询参考图");
|
||||
return;
|
||||
}
|
||||
if (!normalized) {
|
||||
toast.error(`请输入 SPU 或 SKU 后再查询${detailReferenceSourceLabel}`);
|
||||
toast.error(
|
||||
`请输入${
|
||||
detailReplacementQueryType === "productId"
|
||||
? "商品ID"
|
||||
: detailReplacementQueryType === "sku"
|
||||
? "SKU"
|
||||
: "SPU"
|
||||
}后再查询${detailReferenceSourceLabel}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -4724,7 +4874,12 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
queryType: detailReplacementQueryType,
|
||||
target: detailReferenceTarget,
|
||||
});
|
||||
searchParams.set("brandName", currentWorkflowBrandName);
|
||||
if (currentWorkflowBrandName) {
|
||||
searchParams.set("brandName", currentWorkflowBrandName);
|
||||
}
|
||||
if (detailReplacementQueryType === "productId") {
|
||||
searchParams.set("channel", detailReplacementProductDetailChannel);
|
||||
}
|
||||
const response = await fetch(
|
||||
`/api/agent-workflow/reference-search?${searchParams.toString()}`,
|
||||
{ cache: "no-store" }
|
||||
@ -4738,6 +4893,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
updateDetailReplacementState({
|
||||
query: normalized,
|
||||
queryType: result.data.queryType || detailReplacementQueryType,
|
||||
productDetailChannel:
|
||||
result.data.channel || detailReplacementProductDetailChannel,
|
||||
candidates: result.data.candidates || [],
|
||||
selectedCandidateId:
|
||||
result.data.selectedCandidateId || result.data.candidates?.[0]?.id || null,
|
||||
@ -4758,6 +4915,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}, [
|
||||
detailReferenceSourceLabel,
|
||||
detailReferenceTarget,
|
||||
detailReplacementProductDetailChannel,
|
||||
detailReplacementQueryInput,
|
||||
detailReplacementQueryType,
|
||||
getCurrentWorkflowBrandName,
|
||||
@ -4850,7 +5008,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
|
||||
updateSkuReplacementState({
|
||||
query: normalized,
|
||||
queryType: result.data.queryType || skuReplacementQueryType,
|
||||
queryType:
|
||||
result.data.queryType === "spu" || result.data.queryType === "sku"
|
||||
? result.data.queryType
|
||||
: skuReplacementQueryType,
|
||||
candidates: result.data.candidates || [],
|
||||
selectedCandidateId:
|
||||
result.data.selectedCandidateId || result.data.candidates?.[0]?.id || null,
|
||||
@ -5836,7 +5997,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">参考详情搜索</div>
|
||||
<div className="text-xs text-white/40">
|
||||
支持按 SPU / SKU 自动识别候选整套,并选择一套{detailReferenceSourceLabel}参考图进入下方结果区。
|
||||
支持按 SPU / SKU 自动识别候选整套,也可以上传参考详情图或按商品ID拉取详情图。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/55">
|
||||
@ -5846,11 +6007,58 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-5">
|
||||
<div className="inline-flex rounded-full border border-emerald-400/35 bg-emerald-500/[0.10] px-3 py-1.5 text-xs text-emerald-200">
|
||||
{detailReplacementQueryType === "sku" ? "当前按SKU命中" : "支持SPU/SKU自动识别"}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{([
|
||||
["spu", "按SPU查询"],
|
||||
["sku", "按SKU查询"],
|
||||
["productId", "按商品ID查询"],
|
||||
] as Array<[AgentWorkflowReferenceQueryType, string]>).map(([queryType, label]) => (
|
||||
<button
|
||||
key={queryType}
|
||||
type="button"
|
||||
onClick={() => handleDetailReplacementQueryTypeChange(queryType)}
|
||||
className={cn(
|
||||
"rounded-full border px-3 py-1.5 text-xs transition-colors",
|
||||
detailReplacementQueryType === queryType
|
||||
? "border-emerald-400/45 bg-emerald-500/[0.12] text-emerald-200"
|
||||
: "border-white/10 bg-white/[0.03] text-white/58 hover:border-white/18 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 lg:flex-row">
|
||||
<input
|
||||
ref={detailReferenceUploadInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleDetailReferenceUploadChange}
|
||||
/>
|
||||
{detailReplacementQueryType === "productId" ? (
|
||||
<Select
|
||||
value={detailReplacementProductDetailChannel}
|
||||
onValueChange={(value) =>
|
||||
handleDetailReplacementProductDetailChannelChange(
|
||||
value as AgentWorkflowProductDetailChannel
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-12 rounded-[22px] border-white/10 bg-black/20 text-sm text-white focus:border-emerald-400/30 lg:w-[150px]">
|
||||
<SelectValue placeholder="渠道" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRODUCT_DETAIL_CHANNEL_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : null}
|
||||
<Input
|
||||
value={detailReplacementQueryInput}
|
||||
onChange={(e) =>
|
||||
@ -5858,7 +6066,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
query: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={`输入 SPU 或 SKU,查询任务整套中的${detailReferenceSourceLabel}参考图`}
|
||||
placeholder={
|
||||
detailReplacementQueryType === "productId"
|
||||
? "输入商品ID,查询并拉取详情图片"
|
||||
: `输入 ${detailReplacementQueryType === "sku" ? "SKU" : "SPU"},查询任务整套中的${detailReferenceSourceLabel}参考图`
|
||||
}
|
||||
className="h-12 rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30"
|
||||
/>
|
||||
<Button
|
||||
@ -5876,6 +6088,25 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
`查询${detailReferenceSourceLabel}`
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => detailReferenceUploadInputRef.current?.click()}
|
||||
disabled={detailReferenceUploadState.isUploading}
|
||||
className="h-12 rounded-[22px] border-white/10 bg-white/[0.03] px-5 text-sm text-white hover:bg-white/[0.08]"
|
||||
>
|
||||
{detailReferenceUploadState.isUploading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
上传 {detailReferenceUploadState.count} 张
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
上传参考图
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{detailReplacementReferenceCards.length > 0 ? (
|
||||
<Button
|
||||
type="button"
|
||||
@ -5901,14 +6132,31 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
{(detailReplacementSearchState.lastQuery || detailReplacementQueryInput.trim()) ? (
|
||||
<div className="rounded-[18px] border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-white/48">
|
||||
当前参考图查询:
|
||||
{detailReplacementQueryType === "sku" ? "SKU" : "SPU"} /
|
||||
{detailReplacementQueryType === "productId"
|
||||
? "商品ID"
|
||||
: detailReplacementQueryType === "sku"
|
||||
? "SKU"
|
||||
: "SPU"} /
|
||||
{detailReplacementSearchState.lastQuery ||
|
||||
detailReplacementQueryInput.trim().toUpperCase()}
|
||||
(detailReplacementQueryType === "productId"
|
||||
? detailReplacementQueryInput.trim()
|
||||
: detailReplacementQueryInput.trim().toUpperCase())}
|
||||
{workflowBrandName ? (
|
||||
<span className="ml-2 text-white/40">{`品牌 / ${workflowBrandName}`}</span>
|
||||
) : null}
|
||||
{detailReplacementQueryType === "productId" ? (
|
||||
<span className="ml-2 text-white/40">
|
||||
{`渠道 / ${
|
||||
PRODUCT_DETAIL_CHANNEL_OPTIONS.find(
|
||||
(option) => option.value === detailReplacementProductDetailChannel
|
||||
)?.label || detailReplacementProductDetailChannel
|
||||
}`}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="ml-2 text-emerald-200/70">
|
||||
{`已接入真实${detailReferenceSourceLabel}参考图接口`}
|
||||
{detailReplacementQueryType === "productId"
|
||||
? "商品ID详情图接口已服务端接入"
|
||||
: `已接入真实${detailReferenceSourceLabel}参考图接口`}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@ -3,6 +3,8 @@ import type {
|
||||
AgentModeReferenceCandidate,
|
||||
AgentModeWorkspaceState,
|
||||
AgentWorkflowMaterialQueryType,
|
||||
AgentWorkflowProductDetailChannel,
|
||||
AgentWorkflowReferenceQueryType,
|
||||
} from "@/types/agent";
|
||||
import {
|
||||
createEmptyAgentDetailBriefForm,
|
||||
@ -17,6 +19,7 @@ export function createEmptyAgentModeWorkspaceState(): AgentModeWorkspaceState {
|
||||
method: "reference_replace",
|
||||
query: "",
|
||||
queryType: "spu",
|
||||
productDetailChannel: "taobao",
|
||||
candidates: [],
|
||||
selectedCandidateId: null,
|
||||
references: [],
|
||||
@ -61,6 +64,7 @@ function normalizeReferenceItems(value: unknown): AgentDetailReplacementReferenc
|
||||
sortOrder: Number.isFinite(Number(raw.sortOrder))
|
||||
? Math.max(0, Math.min(999, Math.round(Number(raw.sortOrder))))
|
||||
: index,
|
||||
passthrough: raw.passthrough === true,
|
||||
assignedProductIndex: Number.isFinite(Number(raw.assignedProductIndex))
|
||||
? Math.max(0, Math.min(9, Math.round(Number(raw.assignedProductIndex))))
|
||||
: null,
|
||||
@ -73,6 +77,17 @@ function normalizeQueryType(value: unknown, fallback: AgentWorkflowMaterialQuery
|
||||
return value === "sku" || value === "spu" ? value : fallback;
|
||||
}
|
||||
|
||||
function normalizeReferenceQueryType(
|
||||
value: unknown,
|
||||
fallback: AgentWorkflowReferenceQueryType
|
||||
) {
|
||||
return value === "sku" || value === "spu" || value === "productId" ? value : fallback;
|
||||
}
|
||||
|
||||
function normalizeProductDetailChannel(value: unknown): AgentWorkflowProductDetailChannel {
|
||||
return value === "1688" || value === "douyin" || value === "jd" ? value : "taobao";
|
||||
}
|
||||
|
||||
function normalizeGenerationSlots(value: unknown) {
|
||||
if (!Array.isArray(value)) return createEmptyAgentDetailSlots();
|
||||
const base = createEmptyAgentDetailSlots();
|
||||
@ -138,6 +153,7 @@ function normalizeCandidate(value: unknown, index: number): AgentModeReferenceCa
|
||||
materialId: String(raw.materialId || "").trim() || null,
|
||||
taskNo: String(raw.taskNo || "").trim() || null,
|
||||
productModel: String(raw.productModel || "").trim() || null,
|
||||
brandName: String(raw.brandName || "").trim() || null,
|
||||
categoryName: String(raw.categoryName || "").trim() || null,
|
||||
shopName: String(raw.shopName || "").trim() || null,
|
||||
sceneName: String(raw.sceneName || "").trim() || null,
|
||||
@ -175,7 +191,8 @@ export function normalizeAgentModeWorkspaceState(value: unknown): AgentModeWorks
|
||||
detailReplacement: {
|
||||
method: detailRaw.method === "ai_detail_generate" ? "ai_detail_generate" : "reference_replace",
|
||||
query: String(detailRaw.query || detailRaw.spuQuery || "").trim(),
|
||||
queryType: normalizeQueryType(detailRaw.queryType, "spu"),
|
||||
queryType: normalizeReferenceQueryType(detailRaw.queryType, "spu"),
|
||||
productDetailChannel: normalizeProductDetailChannel(detailRaw.productDetailChannel),
|
||||
candidates: normalizeCandidates(detailRaw.candidates),
|
||||
selectedCandidateId: String(detailRaw.selectedCandidateId || "").trim() || null,
|
||||
references: normalizeReferenceItems(detailRaw.references),
|
||||
|
||||
@ -13,6 +13,14 @@ export const defaultAgentWorkflowMaterialApiConfig: AgentWorkflowMaterialApiConf
|
||||
enabled: false,
|
||||
baseUrl: "https://sc.jiansun.vip/api/external",
|
||||
apiKey: "",
|
||||
productDetailEnabled: false,
|
||||
productDetailBaseUrl: "",
|
||||
productDetailApiKey: "",
|
||||
productDetailPath: "",
|
||||
productDetailMethod: "GET",
|
||||
productDetailDefaultChannel: "taobao",
|
||||
productDetailSyncAnalysis: true,
|
||||
productDetailForceReanalyze: false,
|
||||
};
|
||||
|
||||
function sanitizeText(value: unknown, maxLength: number) {
|
||||
@ -206,6 +214,19 @@ export function normalizeAgentWorkflowMaterialApiConfig(
|
||||
baseUrl:
|
||||
sanitizeText(raw.baseUrl, 255) || defaultAgentWorkflowMaterialApiConfig.baseUrl,
|
||||
apiKey: sanitizeText(raw.apiKey, 500),
|
||||
productDetailEnabled: raw.productDetailEnabled === true,
|
||||
productDetailBaseUrl: sanitizeText(raw.productDetailBaseUrl, 500),
|
||||
productDetailApiKey: sanitizeText(raw.productDetailApiKey, 500),
|
||||
productDetailPath: sanitizeText(raw.productDetailPath, 500),
|
||||
productDetailMethod: raw.productDetailMethod === "POST" ? "POST" : "GET",
|
||||
productDetailDefaultChannel:
|
||||
raw.productDetailDefaultChannel === "1688" ||
|
||||
raw.productDetailDefaultChannel === "douyin" ||
|
||||
raw.productDetailDefaultChannel === "jd"
|
||||
? raw.productDetailDefaultChannel
|
||||
: "taobao",
|
||||
productDetailSyncAnalysis: raw.productDetailSyncAnalysis !== false,
|
||||
productDetailForceReanalyze: raw.productDetailForceReanalyze === true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -216,5 +237,6 @@ export function normalizeAgentWorkflowMaterialApiConfigForPublic(
|
||||
return {
|
||||
...config,
|
||||
apiKey: "",
|
||||
productDetailApiKey: "",
|
||||
};
|
||||
}
|
||||
|
||||
@ -209,6 +209,14 @@ export interface AgentWorkflowMaterialApiConfig {
|
||||
enabled: boolean;
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
productDetailEnabled?: boolean;
|
||||
productDetailBaseUrl?: string;
|
||||
productDetailApiKey?: string;
|
||||
productDetailPath?: string;
|
||||
productDetailMethod?: "GET" | "POST";
|
||||
productDetailDefaultChannel?: AgentWorkflowProductDetailChannel;
|
||||
productDetailSyncAnalysis?: boolean;
|
||||
productDetailForceReanalyze?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentWorkflowGoodsInfoApiConfig {
|
||||
@ -219,6 +227,8 @@ export interface AgentWorkflowGoodsInfoApiConfig {
|
||||
}
|
||||
|
||||
export type AgentWorkflowMaterialQueryType = "sku" | "spu";
|
||||
export type AgentWorkflowReferenceQueryType = AgentWorkflowMaterialQueryType | "productId";
|
||||
export type AgentWorkflowProductDetailChannel = "taobao" | "1688" | "douyin" | "jd";
|
||||
|
||||
export interface AgentWorkflowBrandModelImage {
|
||||
id: string;
|
||||
@ -417,7 +427,8 @@ export interface AgentModeReferenceCandidate {
|
||||
export interface AgentDetailReplacementState {
|
||||
method: AgentDetailMethod;
|
||||
query: string;
|
||||
queryType: AgentWorkflowMaterialQueryType;
|
||||
queryType: AgentWorkflowReferenceQueryType;
|
||||
productDetailChannel?: AgentWorkflowProductDetailChannel;
|
||||
candidates: AgentModeReferenceCandidate[];
|
||||
selectedCandidateId?: string | null;
|
||||
references: AgentDetailReplacementReferenceItem[];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user