import type { AgentWorkflowMaterialApiConfig, AgentWorkflowMaterialImage, AgentWorkflowMaterialQueryType, AgentWorkflowMaterialResourceType, AgentWorkflowMaterialSkuOption, AgentWorkflowMaterialSearchResult, AgentWorkflowMaterialSelectionState, AgentWorkflowMaterialSelections, } from "@/types/agent"; export const AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY = "agent_workflow_material_api_config"; export const AGENT_WORKFLOW_MATERIAL_DEFAULT_BASE_URL = "https://sc.jiansun.vip/api/external"; export const AGENT_WORKFLOW_MATERIAL_DEFAULT_ENDPOINT_PATH = "/standard-model-white-background"; export const AGENT_WORKFLOW_MATERIAL_DEFAULT_RESOURCE_BASE_URL = "https://s3.jiansun.vip"; export const AGENT_WORKFLOW_MATERIAL_FALLBACK_RESOURCE_BASE_URL = "https://s3.yydd.ai"; export const AGENT_WORKFLOW_MATERIAL_DEFAULT_TIMEOUT_MS = 30_000; export const AGENT_WORKFLOW_MATERIAL_MIN_TIMEOUT_MS = 3_000; export const AGENT_WORKFLOW_MATERIAL_MAX_TIMEOUT_MS = 120_000; export const defaultAgentWorkflowMaterialApiConfig: AgentWorkflowMaterialApiConfig = { enabled: false, baseUrl: AGENT_WORKFLOW_MATERIAL_DEFAULT_BASE_URL, endpointPath: AGENT_WORKFLOW_MATERIAL_DEFAULT_ENDPOINT_PATH, apiKey: "", whiteBackgroundEnabled: true, taskDetailEnabled: true, resourceBaseUrl: AGENT_WORKFLOW_MATERIAL_DEFAULT_RESOURCE_BASE_URL, fallbackResourceBaseUrl: AGENT_WORKFLOW_MATERIAL_FALLBACK_RESOURCE_BASE_URL, requestTimeoutMs: AGENT_WORKFLOW_MATERIAL_DEFAULT_TIMEOUT_MS, productDetailEnabled: false, productDetailBaseUrl: "", productDetailApiKey: "", productDetailPath: "", productDetailMethod: "GET", productDetailDefaultChannel: "taobao", productDetailSyncAnalysis: true, productDetailForceReanalyze: false, }; function sanitizeText(value: unknown, maxLength: number) { return String(value || "").trim().slice(0, maxLength); } function normalizeBaseUrl(value: unknown, fallback: string) { return (sanitizeText(value, 500) || fallback).replace(/\/+$/, ""); } function normalizeEndpointPath(value: unknown) { const normalized = sanitizeText(value, 500) || AGENT_WORKFLOW_MATERIAL_DEFAULT_ENDPOINT_PATH; if (/^https?:\/\//i.test(normalized)) { return normalized; } return `/${normalized.replace(/^\/+/, "")}`; } function normalizeTimeoutMs(value: unknown) { const parsed = Number(value); if (!Number.isFinite(parsed)) { return AGENT_WORKFLOW_MATERIAL_DEFAULT_TIMEOUT_MS; } return Math.max( AGENT_WORKFLOW_MATERIAL_MIN_TIMEOUT_MS, Math.min(AGENT_WORKFLOW_MATERIAL_MAX_TIMEOUT_MS, Math.round(parsed)) ); } function normalizeMaterialImage( value: unknown, index: number ): AgentWorkflowMaterialImage | null { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; } const raw = value as Record; const imageUrl = sanitizeText(raw.imageUrl, 2000); if (!imageUrl) return null; const id = sanitizeText(raw.id, 128) || `image-${index + 1}`; const group = raw.group === "other" ? "other" : "fixed"; const previewUrl = sanitizeText(raw.previewUrl, 2000) || null; return { id, key: sanitizeText(raw.key, 64) || id, label: sanitizeText(raw.label, 128) || `图片${index + 1}`, fileName: sanitizeText(raw.fileName, 255) || `image-${index + 1}`, imageUrl, previewUrl, assetId: sanitizeText(raw.assetId, 128) || null, mimeType: sanitizeText(raw.mimeType, 128) || null, appearanceSummary: sanitizeText(raw.appearanceSummary, 255) || null, sourceType: raw.sourceType === "upload" ? "upload" : "search", group, sortOrder: Number.isFinite(Number(raw.sortOrder)) ? Math.max(0, Math.min(999, Math.round(Number(raw.sortOrder)))) : index, }; } function normalizeMaterialSearchResult(value: unknown): AgentWorkflowMaterialSearchResult | null { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; } const raw = value as Record; const skuOptions: AgentWorkflowMaterialSkuOption[] = Array.isArray(raw.skuOptions) ? raw.skuOptions .map((item) => { if (!item || typeof item !== "object" || Array.isArray(item)) { return null; } const skuRaw = item as Record; const skuName = sanitizeText(skuRaw.skuName, 64); if (!skuName) return null; const normalizedOption: AgentWorkflowMaterialSkuOption = { skuName, skuId: sanitizeText(skuRaw.skuId, 128) || null, hasWhiteBackground: skuRaw.hasWhiteBackground === true, whiteBackgroundCount: Number.isFinite(Number(skuRaw.whiteBackgroundCount)) ? Math.max(0, Math.min(999, Math.round(Number(skuRaw.whiteBackgroundCount)))) : 0, previewImageUrl: sanitizeText(skuRaw.previewImageUrl, 2000) || null, previewFileName: sanitizeText(skuRaw.previewFileName, 255) || null, }; return normalizedOption; }) .filter((item): item is AgentWorkflowMaterialSkuOption => item !== null) .slice(0, 100) : []; const images = Array.isArray(raw.images) ? raw.images .map((item, index) => normalizeMaterialImage(item, index)) .filter((item): item is AgentWorkflowMaterialImage => Boolean(item)) .slice(0, 32) : []; if (images.length === 0 && skuOptions.length === 0) { return null; } return { query: sanitizeText(raw.query, 64), queryType: raw.queryType === "spu" ? "spu" : "sku", matchedSku: sanitizeText(raw.matchedSku, 64), matchedSpu: sanitizeText(raw.matchedSpu, 64) || null, materialId: sanitizeText(raw.materialId, 128) || null, productModel: sanitizeText(raw.productModel, 128) || null, zipUrl: sanitizeText(raw.zipUrl, 2000) || null, fixedCount: Number.isFinite(Number(raw.fixedCount)) ? Math.max(0, Math.min(999, Math.round(Number(raw.fixedCount)))) : images.filter((item) => item.group === "fixed").length, otherCount: Number.isFinite(Number(raw.otherCount)) ? Math.max(0, Math.min(999, Math.round(Number(raw.otherCount)))) : images.filter((item) => item.group === "other").length, totalCount: Number.isFinite(Number(raw.totalCount)) ? Math.max(0, Math.min(999, Math.round(Number(raw.totalCount)))) : images.length, images: images .slice() .sort((left, right) => left.sortOrder - right.sortOrder), recommendedImageIds: Array.isArray(raw.recommendedImageIds) ? Array.from( new Set( raw.recommendedImageIds .map((item) => sanitizeText(item, 128)) .filter((item) => item && images.some((image) => image.id === item)) .slice(0, 16) ) ) : [], skuOptions, goodsInfo: raw.goodsInfo && typeof raw.goodsInfo === "object" && !Array.isArray(raw.goodsInfo) ? { productName: sanitizeText((raw.goodsInfo as Record).productName, 255) || null, sizeInfo: sanitizeText((raw.goodsInfo as Record).sizeInfo, 255) || null, materialInfo: sanitizeText((raw.goodsInfo as Record).materialInfo, 255) || null, designer: sanitizeText((raw.goodsInfo as Record).designer, 128) || null, buyer: sanitizeText((raw.goodsInfo as Record).buyer, 128) || null, boardType: sanitizeText((raw.goodsInfo as Record).boardType, 128) || null, appearanceSummary: sanitizeText((raw.goodsInfo as Record).appearanceSummary, 255) || null, } : null, }; } export function createEmptyAgentWorkflowMaterialSelection(): AgentWorkflowMaterialSelectionState { return { result: null, selectedImageIds: [], }; } export function createEmptyAgentWorkflowMaterialSelections(): AgentWorkflowMaterialSelections { return { primary: createEmptyAgentWorkflowMaterialSelection(), secondary: createEmptyAgentWorkflowMaterialSelection(), }; } function normalizeMaterialSelectionState(value: unknown): AgentWorkflowMaterialSelectionState { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; const result = normalizeMaterialSearchResult(raw.result); const validIds = new Set((result?.images || []).map((item) => item.id)); const selectedImageIds = Array.isArray(raw.selectedImageIds) ? Array.from( new Set( raw.selectedImageIds .map((item) => sanitizeText(item, 128)) .filter((item) => item && validIds.has(item)) .slice(0, 32) ) ) : []; return { result, selectedImageIds, }; } export function normalizeAgentWorkflowMaterialSelections( value: unknown ): AgentWorkflowMaterialSelections { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; return { primary: normalizeMaterialSelectionState(raw.primary), secondary: normalizeMaterialSelectionState(raw.secondary), }; } export function normalizeAgentWorkflowMaterialApiConfig( value: unknown ): AgentWorkflowMaterialApiConfig { if (!value || typeof value !== "object" || Array.isArray(value)) { return defaultAgentWorkflowMaterialApiConfig; } const raw = value as Partial; return { enabled: raw.enabled === true, baseUrl: normalizeBaseUrl( raw.baseUrl, defaultAgentWorkflowMaterialApiConfig.baseUrl ), endpointPath: normalizeEndpointPath(raw.endpointPath), apiKey: sanitizeText(raw.apiKey, 500), whiteBackgroundEnabled: raw.whiteBackgroundEnabled !== false, taskDetailEnabled: raw.taskDetailEnabled !== false, resourceBaseUrl: normalizeBaseUrl( raw.resourceBaseUrl, AGENT_WORKFLOW_MATERIAL_DEFAULT_RESOURCE_BASE_URL ), fallbackResourceBaseUrl: normalizeBaseUrl( raw.fallbackResourceBaseUrl, AGENT_WORKFLOW_MATERIAL_FALLBACK_RESOURCE_BASE_URL ), requestTimeoutMs: normalizeTimeoutMs(raw.requestTimeoutMs), 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, }; } export function isAgentWorkflowMaterialResourceEnabled( config: AgentWorkflowMaterialApiConfig, resourceType: AgentWorkflowMaterialResourceType ) { if (resourceType === "task-detail") { return config.taskDetailEnabled !== false; } return config.whiteBackgroundEnabled !== false; } export function buildAgentWorkflowMaterialApiUrl( config: AgentWorkflowMaterialApiConfig, params: { resourceType: AgentWorkflowMaterialResourceType; queryType: AgentWorkflowMaterialQueryType; keyword: string; } ) { const endpointPath = normalizeEndpointPath(config.endpointPath); const baseUrl = normalizeBaseUrl(config.baseUrl, AGENT_WORKFLOW_MATERIAL_DEFAULT_BASE_URL); const requestUrl = /^https?:\/\//i.test(endpointPath) ? endpointPath : `${baseUrl}/${endpointPath.replace(/^\/+/, "")}`; const url = new URL(requestUrl); url.searchParams.set("resourceType", params.resourceType); url.searchParams.set(params.queryType, params.keyword.trim().toUpperCase()); return url.toString(); } export function normalizeAgentWorkflowMaterialApiConfigForPublic( value: unknown ): AgentWorkflowMaterialApiConfig { const config = normalizeAgentWorkflowMaterialApiConfig(value); return { ...config, apiKey: "", productDetailApiKey: "", }; }