forked from lunanting/cangku
202 lines
9.3 KiB
JavaScript
202 lines
9.3 KiB
JavaScript
'use strict';
|
||
|
||
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const {
|
||
DEFAULT_MAIN_IMAGE_PROMPT,
|
||
DEFAULT_MODEL,
|
||
DEFAULT_PRODUCT_TITLE_PROMPT,
|
||
DEFAULT_SKU_IMAGE_PROMPT,
|
||
buildSkuNameRepairInstruction,
|
||
normalizeMainImagePrompt,
|
||
normalizeSkuImagePrompt,
|
||
normalizeProductTitleCategoryTerm,
|
||
normalizeProductTitleCandidatesPayload,
|
||
normalizeSkuNameCandidatesPayload,
|
||
} = require('../src/services/selectionFissionAiSettings');
|
||
|
||
test('selection AI reduces taxonomy paths to a usable product title category', () => {
|
||
assert.equal(normalizeProductTitleCategoryTerm('书柜 > 单个书柜'), '书柜');
|
||
assert.equal(normalizeProductTitleCategoryTerm('住宅家具 / 柜类 / 书柜'), '书柜');
|
||
assert.equal(normalizeProductTitleCategoryTerm('单个鞋柜'), '鞋柜');
|
||
assert.equal(normalizeProductTitleCategoryTerm('书柜'), '书柜');
|
||
});
|
||
|
||
test('selection AI uses the verified Qwen multimodal snapshot by default', () => {
|
||
assert.equal(DEFAULT_MODEL, 'qwen3.7-max-2026-06-08');
|
||
assert.match(DEFAULT_PRODUCT_TITLE_PROMPT, /28至30个汉字/);
|
||
assert.match(DEFAULT_PRODUCT_TITLE_PROMPT, /优先写满30字/);
|
||
assert.match(DEFAULT_PRODUCT_TITLE_PROMPT, /第三方品牌/);
|
||
assert.match(DEFAULT_PRODUCT_TITLE_PROMPT, /forbiddenBrandTerms/);
|
||
});
|
||
|
||
test('main image prompt requires styled props without changing the product', () => {
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /装饰品/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /不能输出光秃秃的裸产品/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /不能.*改变|不得改变/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /不得.*赠送|不能.*赠送/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /不得出现任何字符、符号或伪文字/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /刚性整体/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /部件位置与精确数量/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /优先保持目标商品外观结构不变/);
|
||
});
|
||
|
||
test('SKU image prompt treats the first image as the unique SKU identity', () => {
|
||
assert.match(DEFAULT_SKU_IMAGE_PROMPT, /第一张图是当前SKU唯一商品身份图/);
|
||
assert.match(DEFAULT_SKU_IMAGE_PROMPT, /只有在参考市场图生成模式下/);
|
||
assert.match(DEFAULT_SKU_IMAGE_PROMPT, /其他SKU/);
|
||
assert.match(DEFAULT_SKU_IMAGE_PROMPT, /刚性整体/);
|
||
assert.match(DEFAULT_SKU_IMAGE_PROMPT, /部件位置与精确数量/);
|
||
assert.match(DEFAULT_SKU_IMAGE_PROMPT, /优先保持SKU外观结构不变/);
|
||
});
|
||
|
||
test('legacy SKU scene prompt is migrated to SKU-first reference order', () => {
|
||
const legacyPrompt = [
|
||
'第一张图是已完成产品替换的市场主图场景,第二张图是公司SKU产品图。',
|
||
'把第二张图的SKU商品自然放入第一张图相同的场景。',
|
||
].join('\n');
|
||
assert.equal(normalizeSkuImagePrompt(legacyPrompt), legacyPrompt);
|
||
assert.equal(
|
||
normalizeSkuImagePrompt('参考已替换完成的市场主图场景,旧默认'),
|
||
DEFAULT_SKU_IMAGE_PROMPT
|
||
);
|
||
});
|
||
|
||
test('main image prompt treats the target image as the only product identity', () => {
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /第一张图是目标商品图/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /唯一商品身份来源/);
|
||
assert.match(DEFAULT_MAIN_IMAGE_PROMPT, /禁止拆分重绘或复制市场商品结构/);
|
||
});
|
||
|
||
test('legacy main image prompt is migrated to the company-first reference order', () => {
|
||
const legacyPrompt = [
|
||
'第一张图是市场参考图,第二张图是公司商品白底图。',
|
||
'彻底移除市场参考图中的原商品、文字、图标、标签、角标、价格、水印和品牌标识,只保留干净的场景、构图、视角、光线与真实阴影。',
|
||
'将第二张图中的公司商品完整替换到市场参考图原商品位置,保持商品结构、颜色和比例准确,边缘自然,不新增任何文字或图标。',
|
||
'参考市场图的陈设风格,在商品上或周围布置与品类和场景协调的书籍、摆件、收纳盒、绿植等生活化装饰品,画面饱满但不过度堆砌,不能输出光秃秃的裸产品。',
|
||
'所有装饰品必须采用无文字、无Logo设计,书籍封面和书脊只保留纯色或自然纹理,不得出现任何字符、符号或伪文字。',
|
||
'装饰品仅作为场景陈设,不得改变、遮挡或虚构商品的层板、柜门、五金、尺寸和其他主体结构,也不得表现为随商品赠送的配件。',
|
||
'输出一张可直接用于电商主图的正方形高清图片。',
|
||
].join('\n');
|
||
|
||
assert.equal(normalizeMainImagePrompt(legacyPrompt), DEFAULT_MAIN_IMAGE_PROMPT);
|
||
assert.equal(normalizeMainImagePrompt('自定义主图提示词'), '自定义主图提示词');
|
||
});
|
||
|
||
test('selection AI normalizes exactly five unique names for every SKU', () => {
|
||
const rows = normalizeSkuNameCandidatesPayload({
|
||
items: [{
|
||
skuCode: 'SKU-1',
|
||
names: ['1. 名称甲', '名称乙', '名称丙', '名称丁', '名称戊'],
|
||
}],
|
||
}, [{ skuCode: 'SKU-1', originalSkuName: '1688原始SKU名' }]);
|
||
|
||
assert.deepEqual(rows, [{
|
||
skuCode: 'SKU-1',
|
||
originalSkuName: '1688原始SKU名',
|
||
names: ['名称甲', '名称乙', '名称丙', '名称丁', '名称戊'],
|
||
}]);
|
||
});
|
||
|
||
test('selection AI rejects incomplete or duplicate candidate sets', () => {
|
||
assert.throws(
|
||
() => normalizeSkuNameCandidatesPayload({
|
||
items: [{
|
||
skuCode: 'SKU-1',
|
||
names: ['名称甲', '名称甲', '名称乙', '名称丙', '名称丁'],
|
||
}],
|
||
}, [{ skuCode: 'SKU-1' }]),
|
||
/恰好5个与原名不同的有效名称/
|
||
);
|
||
assert.throws(
|
||
() => normalizeSkuNameCandidatesPayload({
|
||
items: [{
|
||
skuCode: 'SKU-1',
|
||
names: ['原始名', '名称甲', '名称乙', '名称丙', '名称丁'],
|
||
}],
|
||
}, [{ skuCode: 'SKU-1', originalSkuName: '原始名' }]),
|
||
/恰好5个与原名不同的有效名称/
|
||
);
|
||
});
|
||
|
||
test('selection AI retry instruction requires every SKU and exactly five names', () => {
|
||
const instruction = buildSkuNameRepairInstruction([
|
||
{ skuCode: 'SKU-1' },
|
||
{ skuCode: 'SKU-2' },
|
||
], 1);
|
||
assert.match(instruction, /第2次生成/);
|
||
assert.match(instruction, /SKU-1、SKU-2/);
|
||
assert.match(instruction, /恰好5个名称/);
|
||
assert.match(instruction, /不能遗漏任何SKU/);
|
||
});
|
||
|
||
test('selection AI requires exactly five unique product title candidates', () => {
|
||
const titles = [
|
||
'现代简约家用多层落地收纳书柜客厅卧室大容量置物储物柜组合',
|
||
'家用客厅卧室多层落地收纳书柜简约大容量置物储物展示柜组合',
|
||
'简约现代客厅卧室落地收纳书柜家用多层大容量置物储物柜组合',
|
||
'客厅卧室家用多层书柜落地收纳简约现代大容量置物储物柜组合',
|
||
'多层落地收纳书柜家用客厅卧室简约现代大容量置物储物柜组合',
|
||
];
|
||
assert.deepEqual(
|
||
normalizeProductTitleCandidatesPayload({
|
||
titles: titles.map((title, index) => `${index + 1}. ${title}`),
|
||
}),
|
||
titles
|
||
);
|
||
assert.throws(
|
||
() => normalizeProductTitleCandidatesPayload({
|
||
titles: [titles[0], titles[0], ...titles.slice(1, 4)],
|
||
}),
|
||
/恰好5个有效商品标题/
|
||
);
|
||
});
|
||
|
||
test('selection AI enforces title length, category and all surging keywords', () => {
|
||
const strategy = { category: '书柜', surgingKeywords: ['落地', '收纳'] };
|
||
const validTitles = [
|
||
'现代简约家用多层落地收纳书柜客厅卧室大容量置物储物柜组合',
|
||
'家用客厅卧室多层落地收纳书柜简约大容量置物储物展示柜组合',
|
||
'简约现代客厅卧室落地收纳书柜家用多层大容量置物储物柜组合',
|
||
'客厅卧室家用多层书柜落地收纳简约现代大容量置物储物柜组合',
|
||
'多层落地收纳书柜家用客厅卧室简约现代大容量置物储物柜组合',
|
||
];
|
||
assert.deepEqual(normalizeProductTitleCandidatesPayload({ titles: validTitles }, strategy), validTitles);
|
||
assert.throws(
|
||
() => normalizeProductTitleCandidatesPayload({
|
||
titles: [...validTitles.slice(0, 4), '家用落地收纳书柜'],
|
||
}, strategy),
|
||
/28至30字/
|
||
);
|
||
assert.throws(
|
||
() => normalizeProductTitleCandidatesPayload({
|
||
titles: [...validTitles.slice(0, 4), '现代简约家用多层落地收纳置物架客厅卧室大容量储物展示柜组合'],
|
||
}, strategy),
|
||
/品类词/
|
||
);
|
||
assert.throws(
|
||
() => normalizeProductTitleCandidatesPayload({
|
||
titles: [...validTitles.slice(0, 4), '现代简约家用多层落地书柜客厅卧室大容量置物储物展示柜组合款'],
|
||
}, strategy),
|
||
/飙升词/
|
||
);
|
||
});
|
||
|
||
test('selection AI rejects titles that copy brands or models from reference links', () => {
|
||
const titles = [
|
||
'现代简约家用多层落地收纳书柜客厅卧室大容量置物储物柜组合',
|
||
'家用客厅卧室多层落地收纳书柜简约大容量置物储物展示柜组合',
|
||
'简约现代客厅卧室落地收纳书柜家用多层大容量置物储物柜组合',
|
||
'客厅卧室家用多层书柜落地收纳简约现代大容量置物储物柜组合',
|
||
'钛载S3多层落地收纳书柜家用客厅卧室大容量置物储物柜组合款',
|
||
];
|
||
assert.throws(
|
||
() => normalizeProductTitleCandidatesPayload({ titles }, {
|
||
category: '书柜',
|
||
surgingKeywords: ['落地', '收纳'],
|
||
forbiddenBrandTerms: ['钛载S3'],
|
||
}),
|
||
/参考商品品牌或型号“钛载S3”/
|
||
);
|
||
});
|