'use strict'; const axios = require('axios'); const { getSimilarProductsRuntimeConfig } = require('./similarProductsSettings'); const { getSourceImages } = require('./pddOpenPlatformDraftPublisher'); const { generatePddPropertyRecommendations } = require('./selectionFissionAiSettings'); const { mergePropertyRecommendations, resolveExcelMappedProperties, } = require('./pdd1688PropertyMappingService'); function normalizeText(value, maxLength = 1000) { return String(value ?? '').trim().slice(0, maxLength); } function normalizeImageUrl(value) { return normalizeText(value, 4000).replace(/_sum\.jpg(?=([?#]|$))/gi, ''); } function collectPddAttributeImages(item = {}) { const sourceImages = getSourceImages(item); const sourceProduct = item?.metadata?.sourceRow?.product || item?.selectedSpuPayload || {}; const pricingBySku = new Map((item.pricingRows || []).map((row) => [ normalizeText(row.skuCode, 160).toUpperCase(), row, ])); const sceneBySku = new Map((item.skuSceneImages?.rows || []).map((row) => [ normalizeText(row.skuCode, 160).toUpperCase(), row, ])); const selectedCodes = new Set((item.skuSceneImages?.selectedSkuCodes || []) .map((value) => normalizeText(value, 160).toUpperCase()) .filter(Boolean)); const skuRows = (item.skuSnapshot?.rows || []).filter((row) => ( selectedCodes.size === 0 || selectedCodes.has(normalizeText(row.skuCode, 160).toUpperCase()) )); const groups = [ { kind: 'main', label: '主图', urls: sourceImages.mainImages }, { kind: 'detail', label: '详情图', urls: Array.isArray(sourceProduct.detailImages) ? sourceProduct.detailImages : sourceImages.detailImages, }, { kind: 'sku', label: 'SKU图', urls: skuRows.map((row) => { const code = normalizeText(row.skuCode, 160).toUpperCase(); return pricingBySku.get(code)?.generatedImageUrl || sceneBySku.get(code)?.generatedImageUrl || row.skuImageUrl; }), }, ]; const seen = new Set(); return groups.flatMap((group) => { let groupIndex = 0; return (group.urls || []).map(normalizeImageUrl).filter((url) => { if (!url || seen.has(url)) return false; seen.add(url); return true; }).map((url) => { groupIndex += 1; return { kind: group.kind, label: group.label, index: groupIndex, url }; }); }); } async function recognizeImageText(image, options = {}) { const config = options.config || await getSimilarProductsRuntimeConfig(); if (!config.endpointUrl || !config.token) { const error = new Error('以图搜图OCR接口地址或API Key未配置'); error.status = 409; error.code = 'PDD_ATTRIBUTE_OCR_NOT_CONFIGURED'; throw error; } const httpClient = options.httpClient || axios; const response = await httpClient.post(config.endpointUrl, { image_url: image.url, top_k: 1, score_threshold: 0.99, search_mode: 'appearance', include_raw: false, }, { timeout: config.timeoutMs, headers: { Authorization: `Bearer ${config.token}`, 'X-API-Key': config.token, 'X-Image-OCR': 'true', 'Content-Type': 'application/json', }, maxBodyLength: 15 * 1024 * 1024, }); const payload = response?.data || {}; if (payload.success !== true) { const error = new Error(payload.message || payload.error || 'OCR图片文字识别失败'); error.status = response?.status || 502; error.code = payload.error || 'PDD_ATTRIBUTE_OCR_FAILED'; throw error; } const ocr = payload.data?.ocr || {}; if (ocr.requested !== true || ocr.success !== true) { const error = new Error(ocr.error || 'OCR服务未返回有效文字识别结果'); error.status = 502; error.code = 'PDD_ATTRIBUTE_OCR_FAILED'; throw error; } return { ...image, success: true, fullText: normalizeText(ocr.fullText, 12000), textCount: Number(ocr.textCount || 0), durationMs: Number(ocr.durationMs || 0), requestId: normalizeText(payload.request_id, 160), }; } async function preparePddDraftAttributes(context = {}, options = {}) { const updateProgress = options.updateProgress || (async () => {}); const images = collectPddAttributeImages(context.item); const ocrResults = []; await updateProgress({ status: 'running', stage: 'ocr', progress: 2, message: `准备读取 ${images.length} 张商品图片`, totalImages: images.length, completedImages: 0, }); for (let index = 0; index < images.length; index += 1) { const image = images[index]; await updateProgress({ status: 'running', stage: 'ocr', progress: Math.round(5 + (index / Math.max(images.length, 1)) * 70), message: `当前正在读取第${image.index}张${image.label}的文字信息`, currentImage: { kind: image.kind, label: image.label, index: image.index, url: image.url }, totalImages: images.length, completedImages: index, }); try { ocrResults.push(await recognizeImageText(image, options)); } catch (error) { ocrResults.push({ ...image, success: false, fullText: '', error: normalizeText(error.message, 1000), errorCode: normalizeText(error.code, 120), }); } } const successCount = ocrResults.filter((row) => row.success).length; if (images.length > 0 && successCount === 0) { const error = new Error('所有商品图片OCR识别均失败,未调用AI填充属性'); error.status = 502; error.code = 'PDD_ATTRIBUTE_OCR_ALL_FAILED'; error.data = { ocrResults }; throw error; } await updateProgress({ status: 'running', stage: 'mapping', progress: 78, message: '正在应用拼多多-1688属性映射规则', totalImages: images.length, completedImages: images.length, }); const excelResult = resolveExcelMappedProperties({ categoryName: context.categoryName, categoryPath: context.categoryPathNames, sourceCategory: context.sourceCategory, sourceDescription: context.sourceDescription, sourceAttributes: context.sourceAttributes, categoryRule: context.categoryRule, }); await updateProgress({ status: 'running', stage: 'ai', progress: 84, message: '正在通过qwen3.8-max分析并补全拼多多商品属性', totalImages: images.length, completedImages: images.length, excelMappedCount: excelResult.properties.length, }); const aiResult = await (options.generateRecommendations || generatePddPropertyRecommendations)({ categoryName: context.categoryName, categoryPath: context.categoryPathNames, productTitle: context.productTitle, sourceCategory: context.sourceCategory, sourceAttributes: context.sourceAttributes, categoryRule: context.categoryRule, lockedRefPids: excelResult.lockedRefPids, ocrResults: ocrResults.filter((row) => row.success), }); const properties = mergePropertyRecommendations( context.categoryRule, excelResult, aiResult.properties ); const result = { status: 'completed', generatedAt: new Date().toISOString(), categoryMapping: excelResult.categoryMapping, model: aiResult.model, properties, ocr: { total: images.length, successCount, failureCount: images.length - successCount, rows: ocrResults, }, summary: { excelMappedCount: properties.filter((row) => row.source === 'excel').length, aiFilledCount: properties.filter((row) => row.source === 'ai').length, totalFilledCount: properties.length, }, }; await updateProgress({ status: 'completed', stage: 'confirmation', progress: 100, message: '智能属性已生成,请运营确认最终上架属性', totalImages: images.length, completedImages: images.length, result, }); return result; } module.exports = { collectPddAttributeImages, preparePddDraftAttributes, recognizeImageText, };