'use strict'; const assert = require('node:assert/strict'); const test = require('node:test'); const { findCategoryMapping, mergePropertyRecommendations, resolveExcelMappedProperties, } = require('../src/services/pdd1688PropertyMappingService'); const { collectPddAttributeImages, preparePddDraftAttributes, recognizeImageText, } = require('../src/services/pddDraftAttributePreparationService'); const { listPddPublishingShops, resolvePddPublishingShop, } = require('../src/services/pddPublishingShopService'); const { DEFAULT_PDD_PROPERTY_MODEL, normalizePddPropertyRecommendations, } = require('../src/services/selectionFissionAiSettings'); function createItem() { return { selectedProductTitle: '现代简约实木餐桌', metadata: { sourceRow: { product: { mainImages: ['https://img.example/shared.jpg', 'https://img.example/main-2.jpg'], detailImages: ['https://img.example/shared.jpg', 'https://img.example/detail-1.jpg'], attributes: [{ name: '主材质', value: '实木' }], }, }, }, mainImageGeneration: { rows: [] }, skuSceneImages: { selectedSkuCodes: ['SKU-1', 'SKU-2'], rows: [ { skuCode: 'SKU-1', generatedImageUrl: 'https://img.example/sku-shared.jpg' }, { skuCode: 'SKU-2', generatedImageUrl: 'https://img.example/sku-shared.jpg' }, ], }, skuSnapshot: { rows: [ { skuCode: 'SKU-1', skuImageUrl: 'https://img.example/source-1.jpg' }, { skuCode: 'SKU-2', skuImageUrl: 'https://img.example/source-2.jpg' }, ], }, pricingRows: [], }; } test('Excel contains the configured category mapping and locks mapped values', () => { assert.equal(findCategoryMapping({ categoryName: '普通餐桌' }).categoryName, '餐桌'); const categoryRule = { properties: [{ refPid: 373, name: '主材质', values: [{ vid: 12859, value: '木' }, { vid: 12860, value: '金属' }], }], }; const excel = resolveExcelMappedProperties({ categoryName: '普通餐桌', sourceAttributes: [{ name: '主材质', value: '进口橡胶木实木' }], categoryRule, }); assert.deepEqual(excel.properties.map((row) => ({ refPid: row.refPid, vid: row.vid, value: row.value, source: row.source, locked: row.locked, })), [{ refPid: 373, vid: 12859, value: '木', source: 'excel', locked: true }]); const merged = mergePropertyRecommendations(categoryRule, excel, [{ refPid: 373, vid: 12860, value: '金属', evidence: 'AI错误结果', }]); assert.equal(merged[0].value, '木'); assert.equal(merged[0].source, 'excel'); }); test('AI recommendations are limited to live PDD property IDs and enum values', () => { const categoryRule = { properties: [{ refPid: 1628, name: '环保等级', values: [{ vid: 368832, value: 'E0级' }], }], }; assert.equal(DEFAULT_PDD_PROPERTY_MODEL, 'qwen3.8-max'); assert.deepEqual(normalizePddPropertyRecommendations({ properties: [ { refPid: 1628, vid: 368832, value: 'E0级', confidence: 0.9 }, { refPid: 1628, vid: 999, value: '虚构等级' }, { refPid: 9999, value: '虚构属性' }, ], }, categoryRule), [{ refPid: 1628, vid: 368832, value: 'E0级', propertyName: '环保等级', evidence: '', confidence: 0.9, }]); }); test('main, detail and SKU OCR images are globally deduplicated', () => { const rows = collectPddAttributeImages(createItem()); assert.deepEqual(rows.map((row) => [row.kind, row.index, row.url]), [ ['main', 1, 'https://img.example/shared.jpg'], ['main', 2, 'https://img.example/main-2.jpg'], ['detail', 1, 'https://img.example/detail-1.jpg'], ['sku', 1, 'https://img.example/sku-shared.jpg'], ]); }); test('OCR request enables X-Image-OCR and extracts data.ocr', async () => { let request; const result = await recognizeImageText({ kind: 'detail', label: '详情图', index: 1, url: 'https://img.example/detail.jpg', }, { config: { endpointUrl: 'https://ocr.example/query', token: 'secret', timeoutMs: 1000 }, httpClient: { post: async (url, body, config) => { request = { url, body, config }; return { status: 200, data: { success: true, request_id: 'request-1', data: { ocr: { requested: true, success: true, fullText: '实木', textCount: 1 } }, }, }; }, }, }); assert.equal(request.config.headers['X-Image-OCR'], 'true'); assert.equal(request.body.image_url, 'https://img.example/detail.jpg'); assert.equal(result.fullText, '实木'); assert.equal(result.requestId, 'request-1'); }); test('attribute workflow reports every image before AI confirmation', async () => { const progress = []; const result = await preparePddDraftAttributes({ item: createItem(), categoryName: '普通餐桌', categoryPathNames: ['住宅家具', '桌子', '普通餐桌'], productTitle: '现代简约实木餐桌', sourceCategory: '餐桌', sourceAttributes: [{ name: '主材质', value: '实木' }], categoryRule: { properties: [{ refPid: 373, name: '主材质', values: [{ vid: 12859, value: '木' }] }], }, }, { config: { endpointUrl: 'https://ocr.example/query', token: 'secret', timeoutMs: 1000 }, httpClient: { post: async () => ({ status: 200, data: { success: true, data: { ocr: { requested: true, success: true, fullText: '实木' } } }, }), }, updateProgress: async (row) => progress.push(row), generateRecommendations: async () => ({ model: 'qwen3.8-max', properties: [] }), }); assert.equal(progress.filter((row) => row.stage === 'ocr' && row.currentImage).length, 4); assert.equal(progress.at(-1).stage, 'confirmation'); assert.equal(result.properties[0].source, 'excel'); }); test('publishing shop list exposes only safe fields and resolves credentials server-side', async () => { const row = { shopName: '拼多多-HX旗舰店107', nick: 'pdd86127727389', tokenExpiredTime: new Date('2027-01-01T00:00:00Z'), mallId: '861277273', accessToken: 'access-token', refreshToken: 'refresh-token', clientId: 'client-id', clientSecret: 'client-secret', serviceId: 639, }; const calls = []; const query = async (sql, params) => { calls.push({ sql, params }); return [row]; }; const list = await listPddPublishingShops({ query }); assert.equal(list.rows[0].shopName, row.shopName); assert.equal(list.rows[0].accessToken, undefined); assert.match(calls[0].sql, /shop_status = N'正常'/); assert.match(calls[0].sql, /NOT LIKE N'%已关闭%'/); const resolved = await resolvePddPublishingShop({ shopId: row.nick }, { query }); assert.equal(resolved.shop.shopId, row.nick); assert.equal(resolved.credential.accessToken, 'access-token'); assert.equal(resolved.credential.clientSecret, 'client-secret'); });