'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { QWEN_PROVIDER, AGNES_PROVIDER, DEFAULT_QWEN_BASE_URL, DEFAULT_QWEN_MODEL, DEFAULT_AGNES_BASE_URL, DEFAULT_AGNES_MODEL, _test: { resolveAiConfig, buildAiRequestPayload, parseModelJson, mapChatDataMeta, aggregateChatDimensionRows, createChatBatches, normalizeAnalysis, mergeBatchAnalysesLocally, reconcileMergedAnalysis, sumUsages, getAiErrorDetail, isRetryableAiError, getAiRetryDelayMs, sanitizePublicError, }, } = require('../src/services/csQualityService'); test('chat data metadata exposes the actual available date range', () => { assert.deepEqual(mapChatDataMeta({ minDate: '2026-06-22', latestDate: '2026-07-31 23:59:59', messageCount: '792771', }), { tableName: 'rpt.pdd_GoodsChatMessages', minDate: '2026-06-22', latestDate: '2026-07-31', messageCount: 792771, }); }); function message(uid, content, index = 1) { return { uid, nickname: `买家-${uid}`, msgId: `${uid}-${index}`, msgTime: `2026-07-30 10:00:${String(index).padStart(2, '0')}`, directionLabel: '买家', csname: '客服A', content, }; } function result(overrides = {}) { return { consultation_type: '售前', primary_reason: '产品问题', secondary_reason: '尺寸问题', occurrence_count: 1, affected_customer_count: 1, affected_conversation_ids: ['u1'], buyer_intent: '咨询尺寸', evidence: [{ conversation_id: 'u1', nickname: '买家-u1', message_time: '2026-07-30 10:00:01', text: '多高' }], image_urls: [], product_improvement_areas: ['详情页'], improvement_suggestion: '补充尺寸图', confidence: 0.9, ...overrides, }; } test('customer-service quality defaults to the verified Qwen model and endpoint', () => { const config = resolveAiConfig({ apiKey: 'test-key' }); assert.equal(QWEN_PROVIDER, 'aliyun-model-studio'); assert.equal(config.provider, QWEN_PROVIDER); assert.equal(config.baseUrl, DEFAULT_QWEN_BASE_URL); assert.equal(config.model, DEFAULT_QWEN_MODEL); assert.equal(config.batchConcurrency, 2); assert.equal(config.batchMaxChars, 45000); assert.equal(config.mergeMaxBatches, 8); assert.equal(config.requestRetryCount, 2); assert.equal(config.retryBaseDelayMs, 1500); }); test('customer-service quality resolves the Agnes China endpoint and request format', () => { const config = resolveAiConfig({ provider: AGNES_PROVIDER, apiKey: 'test-key', }); assert.equal(config.provider, AGNES_PROVIDER); assert.equal(config.baseUrl, DEFAULT_AGNES_BASE_URL); assert.equal(config.model, DEFAULT_AGNES_MODEL); assert.equal(config.timeoutMs, 600000); const payload = buildAiRequestPayload(config, [{ role: 'user', content: 'test' }], 2048); assert.equal(payload.model, DEFAULT_AGNES_MODEL); assert.equal(payload.max_tokens, 2048); assert.equal(payload.temperature, 0); assert.equal(payload.stream, false); assert.equal('max_completion_tokens' in payload, false); assert.equal('response_format' in payload, false); }); test('Qwen requests retain the provider-specific JSON parameters', () => { const config = resolveAiConfig({ apiKey: 'test-key' }); const payload = buildAiRequestPayload(config, [{ role: 'user', content: 'test' }], 2048); assert.equal(payload.max_completion_tokens, 2048); assert.deepEqual(payload.response_format, { type: 'json_object' }); assert.equal(payload.enable_thinking, false); assert.equal('max_tokens' in payload, false); }); test('chat batching keeps every customer conversation intact', () => { const rows = [ message('u1', '甲'.repeat(700), 1), message('u1', '乙'.repeat(700), 2), message('u2', '丙'.repeat(700), 1), message('u3', '丁'.repeat(700), 1), ]; const batches = createChatBatches(rows, 1300); assert.equal(batches.length, 3); assert.deepEqual(batches[0].conversationIds, ['u1']); assert.equal(batches[0].messageCount, 2); assert.ok(batches.every((batch) => !( batch.conversationIds.includes('u1') && batch.conversationIds.length > 1 ))); assert.deepEqual(batches.flatMap((batch) => batch.conversationIds), ['u1', 'u2', 'u3']); assert.equal(batches[0].startDate, '2026-07-30'); assert.equal(batches[0].endDate, '2026-07-30'); }); test('chat dimension snapshot derives goods and customers in one aggregation', () => { const snapshot = aggregateChatDimensionRows([ { goodsId: 'g1', uid: 'u1', csname: '客服A', goodsName: '商品1', shopName: '店铺1', nickname: '客户1', messageCount: 2, firstMsgTime: '2026-07-29T08:00:00.000Z', lastMsgTime: '2026-07-29T08:01:00.000Z', }, { goodsId: 'g1', uid: 'u1', csname: '客服B', goodsName: '商品1', shopName: '店铺1', nickname: '客户1', messageCount: 3, firstMsgTime: '2026-07-29T08:02:00.000Z', lastMsgTime: '2026-07-29T08:04:00.000Z', }, { goodsId: 'g1', uid: 'u2', csname: '客服A', goodsName: '商品1', shopName: '店铺1', nickname: '客户2', messageCount: 1, firstMsgTime: '2026-07-29T09:00:00.000Z', lastMsgTime: '2026-07-29T09:00:00.000Z', }, { goodsId: 'g2', uid: 'u3', csname: '客服C', goodsName: '商品2', shopName: '店铺2', nickname: '客户3', messageCount: 4, firstMsgTime: '2026-07-29T10:00:00.000Z', lastMsgTime: '2026-07-29T10:03:00.000Z', }, ]); assert.equal(snapshot.goodsRows.length, 2); assert.equal(snapshot.goodsRows[0].goodsId, 'g1'); assert.equal(snapshot.goodsRows[0].messageCount, 6); assert.equal(snapshot.goodsRows[0].userCount, 2); assert.equal(snapshot.goodsRows[0].csCount, 2); assert.equal(snapshot.customersByGoods.g1.length, 2); assert.equal(snapshot.customersByGoods.g1[0].uid, 'u1'); assert.equal(snapshot.customersByGoods.g1[0].messageCount, 5); assert.equal(snapshot.customersByGoods.g1[0].csname, '客服B'); assert.equal(snapshot.customersByGoods.g1[0].firstMsgTime, '2026-07-29T08:00:00.000Z'); assert.equal(snapshot.customersByGoods.g1[0].lastMsgTime, '2026-07-29T08:04:00.000Z'); }); test('local merge sums disjoint batch counts and deduplicates evidence', () => { const analyses = [ { summary: { pre_sale_conversation_count: 1, after_sale_conversation_count: 0, mixed_conversation_count: 0, main_findings: ['尺寸咨询较多'] }, results: [result()], }, { summary: { pre_sale_conversation_count: 2, after_sale_conversation_count: 0, mixed_conversation_count: 0, main_findings: ['尺寸咨询较多'] }, results: [result({ occurrence_count: 3, affected_customer_count: 2, affected_conversation_ids: ['u1', 'u2'], evidence: [ { conversation_id: 'u1', nickname: '买家-u1', message_time: '2026-07-30 10:00:01', text: '多高' }, { conversation_id: 'u2', nickname: '买家-u2', message_time: '2026-07-30 10:00:02', text: '多宽' }, ], })], }, ]; const merged = mergeBatchAnalysesLocally(analyses, { customerCount: 3, messageCount: 12 }); assert.equal(merged.results.length, 1); assert.equal(merged.results[0].occurrence_count, 4); assert.equal(merged.results[0].affected_customer_count, 3); assert.deepEqual(merged.results[0].affected_conversation_ids, ['u1', 'u2']); assert.equal(merged.results[0].evidence.length, 2); assert.equal(merged.summary.conversation_count, 3); assert.equal(merged.summary.pre_sale_conversation_count, 3); assert.deepEqual(merged.summary.main_findings, ['尺寸咨询较多']); }); test('model merge cannot change deterministic counts or drop a label group', () => { const local = mergeBatchAnalysesLocally([{ summary: { pre_sale_conversation_count: 2, after_sale_conversation_count: 0, mixed_conversation_count: 0, main_findings: [] }, results: [result({ occurrence_count: 4, affected_customer_count: 2 })], }], { customerCount: 2, messageCount: 8 }); const model = { schema_version: '1.0', conversation_type: '售前', summary: { conversation_count: 99, pre_sale_conversation_count: 99, after_sale_conversation_count: 0, mixed_conversation_count: 0, main_findings: ['优化后的结论'] }, results: [result({ occurrence_count: 99, affected_customer_count: 99, buyer_intent: '优化后的诉求' })], }; const reconciled = reconcileMergedAnalysis(model, local, { customerCount: 2, messageCount: 8 }); assert.equal(reconciled.results[0].occurrence_count, 4); assert.equal(reconciled.results[0].affected_customer_count, 2); assert.equal(reconciled.results[0].buyer_intent, '优化后的诉求'); assert.deepEqual(reconciled.results[0].affected_conversation_ids, ['u1']); assert.equal(reconciled.summary.conversation_count, 2); assert.equal(reconciled.summary.pre_sale_conversation_count, 2); }); test('analysis normalization keeps every valid affected conversation id', () => { const normalized = normalizeAnalysis({ summary: {}, results: [{ consultation_type: '售前', primary_reason: '产品问题', secondary_reason: '尺寸问题', occurrence_count: 3, affected_customer_count: 3, affected_conversation_ids: ['u1', 'u2', 'not-in-batch'], evidence: [{ conversation_id: 'u3', text: '尺寸是多少' }], }], }, { customerCount: 3, messageCount: 10 }, { allowedConversationIds: ['u1', 'u2', 'u3'], }); assert.deepEqual(normalized.results[0].affected_conversation_ids, ['u1', 'u2', 'u3']); assert.equal(normalized.results[0].affected_customer_count, 3); }); test('Qwen JSON parsing, usage aggregation and error extraction are provider-safe', () => { assert.deepEqual(parseModelJson('```json\n{"ok":true}\n```'), { ok: true }); assert.deepEqual(sumUsages([ { prompt_tokens: 10, completion_tokens: 3, total_tokens: 13 }, { input_tokens: 7, output_tokens: 2 }, ]), { prompt_tokens: 17, completion_tokens: 5, total_tokens: 22 }); assert.equal(getAiErrorDetail({ response: { data: { error: { message: 'rate limited' } } } }), 'rate limited'); assert.equal(getAiErrorDetail({ message: 'Bearer sk-secret-example-1234567890 failed' }), 'Bearer [redacted] failed'); assert.equal(isRetryableAiError({ code: 'ETIMEDOUT' }), true); assert.equal(isRetryableAiError({ response: { status: 429 } }), true); assert.equal(isRetryableAiError({ response: { status: 401 } }), false); assert.equal(getAiRetryDelayMs({}, 1, 1500), 1500); assert.equal(getAiRetryDelayMs({}, 2, 1500), 3000); assert.equal(getAiRetryDelayMs({ response: { headers: { 'retry-after': '2' } } }, 1, 1500), 2000); assert.equal(sanitizePublicError('千问模型请求失败:qwen-timeout'), 'AI服务请求失败:AI服务'); assert.equal(sanitizePublicError('https://apihub.agnes-ai.com/v1 failed'), 'https://AI服务/v1 failed'); });