'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { SKU_DIMENSION_MODEL, analyzeAlibaba1688SkuDimensions, buildDimensionAnalysisAttributes, extractExplicitDimensionValues, normalizeDimensionPayload, requestDimensionChunk, } = require('../src/services/alibaba1688SkuDimensionAiService'); function productRecord(skuCount = 2, overrides = {}) { const skus = Array.from({ length: skuCount }, (_, index) => ({ index: index + 1, name: `原木色 ${index + 1}号 120×60×75cm`, specs: [ { name: '颜色', value: '原木色' }, { name: '尺寸', value: `规格${index + 1}` }, ], })); return { productId: '1073252781818', title: '家用书桌', category: '住宅家具', skuCount, product: { attributes: [{ name: '材质', value: '实木' }], skus, }, ...overrides, }; } function aiConfig() { return { enabled: true, apiKey: 'server-only-key', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', timeoutMs: 120000, }; } test('normalizes out-of-order Qwen dimensions by stable SKU references and converts units', () => { const requestRows = [ { skuRef: 'S1', sourceSkuKey: '1:尺寸=大号', sourceIndex: 1 }, { skuRef: 'S2', sourceSkuKey: '2:尺寸=小号', sourceIndex: 2 }, ]; const result = normalizeDimensionPayload({ items: [ { skuRef: 'S2', lengthCm: '1.2m', widthCm: '600mm', heightCm: '75cm' }, { skuRef: 'S1', lengthCm: 140, widthCm: 70, heightCm: 76 }, ], }, requestRows); assert.deepEqual(result.map((row) => row.skuRef), ['S1', 'S2']); assert.deepEqual( [result[1].lengthCm, result[1].widthCm, result[1].heightCm], [120, 60, 75] ); assert.throws( () => normalizeDimensionPayload({ items: [{ skuRef: 'S1', lengthCm: 1, widthCm: 2, heightCm: 3 }] }, requestRows), error => error.code === 'ALIBABA_1688_SKU_DIMENSION_AI_INCOMPLETE' ); }); test('uses qwen3.7-flash with JSON mode, disabled thinking and a backend-only key', async () => { let captured; const requestRows = [{ skuRef: 'S1', sourceSkuKey: '1:尺寸=120cm', sourceIndex: 1, skuName: '120×60×75cm', specs: [{ name: '尺寸', value: '120cm' }], }]; const result = await requestDimensionChunk(aiConfig(), { productId: '1073252781818', title: '家用书桌', skus: requestRows, }, requestRows, { httpClient: { async post(url, body, config) { captured = { url, body, config }; return { data: { choices: [{ message: { content: '{"items":[{"skuRef":"S1","lengthCm":120,"widthCm":60,"heightCm":75}]}' } }], usage: { total_tokens: 99 }, }, }; }, }, }); assert.equal(captured.body.model, SKU_DIMENSION_MODEL); assert.equal(captured.body.model, 'qwen3.7-flash'); assert.deepEqual(captured.body.response_format, { type: 'json_object' }); assert.equal(captured.body.enable_thinking, false); assert.equal(captured.body.temperature, 0); assert.equal(captured.config.timeout, 60000); assert.equal(captured.config.headers.Authorization, 'Bearer server-only-key'); assert.equal(result.rows[0].heightCm, 75); assert.equal(result.usage.totalTokens, 99); }); test('explicit SKU dimensions are hard constraints and trigger a corrected model retry', async () => { const requestRows = [{ skuRef: 'S1', sourceSkuKey: '1:颜色=双杆款-60cm', sourceIndex: 1, skuName: '双杆款-60cm', specs: [{ name: '颜色', value: '双杆款-60cm' }], }]; assert.deepEqual(extractExplicitDimensionValues(requestRows[0]), [60]); assert.deepEqual(extractExplicitDimensionValues({ skuName: '120×60×75cm' }), [120, 60, 75]); const prompts = []; const responses = [ { lengthCm: 152, widthCm: 26.3, heightCm: 38 }, { lengthCm: 60, widthCm: 26.3, heightCm: 152 }, ]; const result = await requestDimensionChunk(aiConfig(), { productId: '994837637916', title: '落地挂衣架', skus: requestRows, }, requestRows, { delay: async () => {}, httpClient: { async post(_url, body) { prompts.push(body.messages[0].content); const dimensions = responses[prompts.length - 1]; return { data: { choices: [{ message: { content: JSON.stringify({ items: [{ skuRef: 'S1', ...dimensions }], }) } }], }, }; }, }, }); assert.equal(prompts.length, 2); assert.match(prompts[1], /上次结果未通过校验/); assert.equal(result.rows[0].lengthCm, 60); }); test('logistics volume and weight tables are excluded from product dimension evidence', () => { const attributes = buildDimensionAnalysisAttributes({ attributes: [ { name: '颜色', value: '长(cm) 宽(cm) 高(cm) 体积(cm³) 重量(g)' }, { name: '双杆款-60cm', value: '152 26.30 38 151908.800 2800' }, { name: '规格', value: '60/85' }, { name: '材质', value: '楠竹' }, ], }); assert.deepEqual(attributes, [ { name: '规格', value: '60/85' }, { name: '材质', value: '楠竹' }, ]); }); test('analyzes every canonical SKU in batches and aggregates usage', async () => { const product = productRecord(31); const chunkSizes = []; const result = await analyzeAlibaba1688SkuDimensions(product.productId, { getProduct: async () => product, getEncoding: async () => null, getConfig: async () => aiConfig(), concurrency: 1, requestChunk: async (_config, context, rows) => { chunkSizes.push(rows.length); assert.ok(context.skus.every((row) => Array.isArray(row.explicitDimensionValuesCm))); return { rows: rows.map((row) => ({ ...row, lengthCm: 120, widthCm: 60, heightCm: 75, confidence: 'high', evidence: 'SKU明确标注', })), usage: { promptTokens: rows.length, completionTokens: rows.length * 2, totalTokens: rows.length * 3 }, }; }, }); assert.deepEqual(chunkSizes, [30, 1]); assert.equal(result.model, 'qwen3.7-flash'); assert.equal(result.rows.length, 31); assert.equal(result.batchCount, 2); assert.equal(result.usage.totalTokens, 93); assert.equal(new Set(result.rows.map((row) => row.sourceSkuKey)).size, 31); }); test('already encoded and previously requested products never call Qwen again', async () => { for (const encoding of [ { status: 'encoded', request: { skuItems: [{ lengthCm: 120, widthCm: 60, heightCm: 75 }, { lengthCm: 140, widthCm: 70, heightCm: 76 }] }, }, { status: 'failed', request: { skuItems: [{ lengthCm: 120, widthCm: 60, heightCm: 75 }, { lengthCm: 140, widthCm: 70, heightCm: 76 }] }, }, ]) { let modelCalls = 0; const result = await analyzeAlibaba1688SkuDimensions('1073252781818', { getProduct: async () => productRecord(), getEncoding: async () => encoding, getConfig: async () => { modelCalls += 1; return aiConfig(); }, requestChunk: async () => { modelCalls += 1; throw new Error('must not call Qwen'); }, }); assert.equal(modelCalls, 0); assert.equal(result.rows.length, 2); assert.equal(result.alreadyEncoded, encoding.status === 'encoded'); } }); test('a legacy failed request with missing dimensions gets one safe AI completion path', async () => { const product = productRecord(); let modelCalls = 0; const result = await analyzeAlibaba1688SkuDimensions(product.productId, { getProduct: async () => product, getEncoding: async () => ({ status: 'failed', request: { skuItems: [ { productName: 'first', colorSpec: 'first-spec' }, { productName: 'second', colorSpec: 'second-spec', lengthCm: 140 }, ], }, }), getConfig: async () => aiConfig(), requestChunk: async (_config, _context, rows) => { modelCalls += 1; return { rows: rows.map(row => ({ ...row, lengthCm: row.sourceIndex === 1 ? 120 : 140, widthCm: 60, heightCm: 75, confidence: 'medium', evidence: 'legacy request completion', })), usage: { promptTokens: 1, completionTokens: 1, totalTokens: 2 }, }; }, }); assert.equal(modelCalls, 1); assert.equal(result.alreadyRequested, true); assert.equal(result.source, 'qwen'); assert.equal(result.rows.length, 2); }); test('saved failed requests are rejected before AI when their fixed SKU snapshot changed', async () => { const product = productRecord(); let modelCalls = 0; await assert.rejects( analyzeAlibaba1688SkuDimensions(product.productId, { getProduct: async () => product, getEncoding: async () => ({ status: 'failed', sourceSkuCount: 2, sourceSkuFingerprint: 'a'.repeat(64), request: { skuItems: [{}, {}] }, }), getConfig: async () => aiConfig(), requestChunk: async () => { modelCalls += 1; throw new Error('must not call Qwen'); }, }), error => error.code === 'ALIBABA_1688_SKU_CHANGED_SINCE_ENCODING_REQUEST' ); assert.equal(modelCalls, 0); }); test('an incomplete active request waits for lease expiry before AI retry', async () => { const product = productRecord(); const encoding = { status: 'encoding', startedAt: '2026-08-19T00:00:00.000Z', request: { skuItems: [{}, {}] }, }; await assert.rejects( analyzeAlibaba1688SkuDimensions(product.productId, { getProduct: async () => product, getEncoding: async () => encoding, now: new Date('2026-08-19T00:01:00.000Z').getTime(), }), error => error.code === 'ALIBABA_1688_ENCODING_IN_PROGRESS' ); let modelCalls = 0; const result = await analyzeAlibaba1688SkuDimensions(product.productId, { getProduct: async () => product, getEncoding: async () => encoding, now: new Date('2026-08-19T01:00:00.000Z').getTime(), getConfig: async () => aiConfig(), requestChunk: async (_config, _context, rows) => { modelCalls += 1; return { rows: rows.map(row => ({ ...row, lengthCm: 1, widthCm: 2, heightCm: 3 })), usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, }; }, }); assert.equal(modelCalls, 1); assert.equal(result.alreadyRequested, true); }); test('discards AI dimensions if the canonical SKU fingerprint changes during analysis', async () => { const before = productRecord(); const after = productRecord(2, { product: { ...before.product, skus: before.product.skus.map((sku, index) => ( index === 0 ? { ...sku, name: '重新采集后的规格', specs: [{ name: '尺寸', value: '新规格' }] } : sku )), }, }); await assert.rejects( analyzeAlibaba1688SkuDimensions(before.productId, { getProduct: async () => before, getLatestProduct: async () => after, getEncoding: async () => null, getConfig: async () => aiConfig(), requestChunk: async (_config, _context, rows) => ({ rows: rows.map((row) => ({ ...row, lengthCm: 1, widthCm: 2, heightCm: 3 })), usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }, }), }), error => error.code === 'ALIBABA_1688_SKU_CHANGED_DURING_AI_ANALYSIS' ); });