Initial commit of Tmall new-product analysis loop v9.2.5.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'server'))
|
||||
|
||||
from analysis import analyze, ensure_experience_solution, normalize, _review_issue, _buyer_evidence, detect_category, _category_parameter_facts, core_solution_diagnostics, repair_core_solution_with_evidence
|
||||
from renderer import render
|
||||
from server import TASK_JS, task_evidence_summary, task_page
|
||||
|
||||
|
||||
class EvidenceOnlyReportTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.analysis = {
|
||||
'facts': {
|
||||
'product': {'title': '用户评价·51', 'itemId': '900653753390'},
|
||||
'sales': {'sold': '700+', 'currentPrice': '395.25'},
|
||||
'attributes': [
|
||||
{'name': '1024', 'value': '"lg",'},
|
||||
{'name': 'console.log("onload]', 'value': 'performance.now()'},
|
||||
{'name': '面料支数', 'value': '60支'},
|
||||
{'name': '床单面料材质', 'value': '全棉'},
|
||||
{'name': '适用床尺寸', 'value': '1.2m床三件套'},
|
||||
],
|
||||
'reviews': [
|
||||
{'content': '2025年4月18日已购:【四件套】测试规格'},
|
||||
{'content': '花色好看,摸起来很舒服', 'images': ['https://img.alicdn.com/review-rate.jpg'], 'source': 'dom_visible'},
|
||||
{'content': '1米2床单尺寸小,床边不够'},
|
||||
],
|
||||
'questions': [
|
||||
{'question': '问大家·5 问 有味道吗 更多回答 问 盖上舒服吗 更多回答'},
|
||||
{'question': '问 有味道吗 更多回答'},
|
||||
],
|
||||
'collection': {
|
||||
'reviewCollectionComplete': False,
|
||||
'publicReviewCount': '51',
|
||||
'reviewStopReason': 'no_progress_after_4_rounds',
|
||||
'reviewPagesVisited': 35,
|
||||
},
|
||||
'images': {'main': [
|
||||
'https://img.alicdn.com/tps-120-60.png',
|
||||
'https://img.alicdn.com/item_pic.jpg',
|
||||
], 'detail': [], 'sku': [], 'buyerShow': [], 'all': []},
|
||||
},
|
||||
'experienceSolution': {},
|
||||
'meta': {},
|
||||
}
|
||||
|
||||
def test_empty_model_result_does_not_generate_fallback_report(self):
|
||||
result = ensure_experience_solution(self.analysis)
|
||||
self.assertFalse(result['meta']['fallbackReport'])
|
||||
self.assertFalse(result['meta']['reportReady'])
|
||||
self.assertIn('不输出兜底报告', result['meta']['reportBlockedReason'])
|
||||
self.assertEqual('', result['facts']['product']['title'])
|
||||
self.assertEqual(['面料支数', '床单面料材质', '适用床尺寸'], [x['name'] for x in result['facts']['attributes']])
|
||||
self.assertEqual(2, result['baseline']['reviewCount'])
|
||||
self.assertEqual(2, result['baseline']['questionCount'])
|
||||
cards = result['experienceSolution']['ownerOverview']['cards']
|
||||
self.assertTrue(cards)
|
||||
self.assertEqual('下一款明确卖点', cards[0]['label'])
|
||||
self.assertIn('ATTR_0003', cards[0]['evidenceIds'])
|
||||
self.assertEqual([], result['experienceSolution']['productExperience']['gates'])
|
||||
with self.assertRaisesRegex(ValueError, '不输出兜底报告'):
|
||||
render(result, 'fixture')
|
||||
|
||||
def test_analysis_runs_without_model_channel(self):
|
||||
raw = dict(self.analysis['facts'])
|
||||
raw['_model_available'] = False
|
||||
result = analyze(raw)
|
||||
self.assertFalse(result['meta']['modelUsed'])
|
||||
self.assertEqual('9.2.5', result['meta']['engineVersion'])
|
||||
self.assertEqual('home_textile', result['meta']['industryTemplate'])
|
||||
self.assertFalse(result['meta']['fallbackReport'])
|
||||
self.assertFalse(result['meta']['reportReady'])
|
||||
|
||||
def test_historical_fallback_json_is_rejected(self):
|
||||
result = ensure_experience_solution({
|
||||
'facts': self.analysis['facts'],
|
||||
'experienceSolution': {'reportSummary': {'title': '旧报告', 'verdict': '旧结论'}},
|
||||
'meta': {'fallbackReport': True},
|
||||
})
|
||||
self.assertFalse(result['meta']['reportReady'])
|
||||
self.assertIn('历史分析标记为兜底结果', result['meta']['reportBlockedReason'])
|
||||
|
||||
def test_dormitory_spec_never_invents_12m_issue_and_title_is_clean(self):
|
||||
facts = {
|
||||
'product': {'title': '宿舍七件套新生床品-tmall.com天猫', 'itemId': '2'},
|
||||
'sales': {'sold': '1万+', 'currentPrice': '637.5'},
|
||||
'attributes': [
|
||||
{'name': '套件组成', 'value': '被芯、床垫、床单、被套、枕芯、枕套、床帘'},
|
||||
{'name': '适用床尺寸', 'value': '80×190cm、90×190cm、90×200cm;床垫厚4cm/6cm'},
|
||||
{'name': '面料材质', 'value': '聚酯纤维'},
|
||||
{'name': '适用人群', 'value': '学生'},
|
||||
],
|
||||
'reviews': [
|
||||
{'content': '七件配齐,开学直接用,尺寸合适'},
|
||||
{'content': '枕套实物和页面有色差', 'sku': '90×190cm / 4cm床垫'},
|
||||
],
|
||||
'questions': [], 'images': {},
|
||||
}
|
||||
clean = normalize(facts);issue = _review_issue(clean)
|
||||
self.assertEqual('宿舍七件套新生床品', clean['product']['title'])
|
||||
self.assertEqual('实物色差', issue['label'])
|
||||
self.assertNotIn('1.2m', str(issue))
|
||||
self.assertNotIn('1米2', str(issue))
|
||||
|
||||
def test_real_12m_comment_keeps_12m_evidence(self):
|
||||
clean = normalize(self.analysis['facts']);issue = _review_issue(clean)
|
||||
self.assertIn('1米2床单尺寸小,床边不够', issue['text'])
|
||||
self.assertEqual('尺寸与床型适配', issue['label'])
|
||||
|
||||
def test_apparel_uses_dynamic_category_dimensions(self):
|
||||
raw=normalize({'product':{'title':'女士宽松连衣裙'},'attributes':[{'name':'尺码','value':'S M L XL'},{'name':'面料成分','value':'棉 95% 氨纶 5%'},{'name':'版型','value':'宽松'}],'reviews':[{'content':'尺码合身,版型显瘦'}],'images':{}})
|
||||
route=detect_category(raw)
|
||||
facts=_category_parameter_facts(raw)
|
||||
self.assertEqual('apparel',route['key'])
|
||||
self.assertIn('尺码与适配',[x['label'] for x in facts])
|
||||
self.assertIn('面料与成分',[x['label'] for x in facts])
|
||||
self.assertNotIn('套件 BOM',[x['label'] for x in facts])
|
||||
|
||||
def test_food_beauty_and_digital_route_to_their_own_dimensions(self):
|
||||
fixtures=[
|
||||
('food',{'product':{'title':'坚果零食礼盒'},'attributes':[{'name':'净含量','value':'750g'},{'name':'配料表','value':'坚果'}]}),
|
||||
('beauty',{'product':{'title':'敏感肌保湿精华'},'attributes':[{'name':'主要成分','value':'透明质酸'},{'name':'适用肤质','value':'敏感肌'}]}),
|
||||
('digital',{'product':{'title':'无线蓝牙耳机'},'attributes':[{'name':'兼容系统','value':'iOS/Android'},{'name':'续航时间','value':'30小时'}]}),
|
||||
]
|
||||
for expected,data in fixtures:
|
||||
data.update({'reviews':[],'images':{}})
|
||||
clean=normalize(data); route=detect_category(clean)
|
||||
self.assertEqual(expected,route['key'])
|
||||
self.assertTrue(_category_parameter_facts(clean))
|
||||
|
||||
def test_complete_evidence_backed_ai_output_is_rendered_and_sanitized(self):
|
||||
facts = {
|
||||
'product': {'title': '全棉三件套-taobao.com淘宝网', 'itemId': '3'},
|
||||
'sales': {},
|
||||
'attributes': [
|
||||
{'name': '床单面料材质', 'value': '全棉'},
|
||||
{'name': '套件组成', 'value': '床单、被套、枕套'},
|
||||
{'name': '适用床尺寸', 'value': '0.9m床三件套'},
|
||||
],
|
||||
'reviews': [{'content': '摸起来柔软舒服'}],
|
||||
'questions': [{'question': '会不会起球?'}],
|
||||
'images': {},
|
||||
}
|
||||
exp = {
|
||||
'reportSummary': {'title': '省心型大众爆款,提升转化', 'verdict': '先完成样品验证'},
|
||||
'ownerOverview': {'chapterTitle': '页面事实与评论结果已形成', 'cards': [
|
||||
{'label': '客户为什么购买', 'headline': '决定成交', 'evidenceIds': ['REV_000001']},
|
||||
{'label': '页面产品定义', 'headline': '全棉三件套', 'evidenceIds': ['ATTR_0001', 'ATTR_0002']},
|
||||
{'label': '下一款怎么开', 'headline': '先校准规格', 'evidenceIds': ['ATTR_0003']},
|
||||
]},
|
||||
'customerExperience': {
|
||||
'chapterTitle': '1 条评论确认柔软触感', 'journeyTitle': '只显示评论已确认结果',
|
||||
'stats': [{'label': '有效评论', 'value': '1'}],
|
||||
'stages': [
|
||||
{'stageNo': '01', 'label': '实际使用', 'positiveConfirmation': '1 条确认柔软舒服', 'evidenceIds': ['REV_000001']},
|
||||
{'stageNo': '02', 'label': '选择规格', 'positiveConfirmation': '页面标注0.9m床三件套', 'evidenceIds': ['ATTR_0003']},
|
||||
],
|
||||
'signals': {'positive': [], 'questions': [], 'risks': [
|
||||
{'label': '起球产品缺陷', 'evidenceIds': ['QA_00001']},
|
||||
]},
|
||||
},
|
||||
'productExperience': {'chapterTitle': '材质、BOM与床型必须一致', 'parameterFacts': [
|
||||
{'label': '材质 / 成分', 'value': '全棉', 'evidenceIds': ['ATTR_0001']},
|
||||
{'label': '套件 BOM', 'value': '床单、被套、枕套', 'evidenceIds': ['ATTR_0002']},
|
||||
], 'gates': [
|
||||
{'priority': 'P0', 'label': '材质核对', 'action': '核对标签与样品', 'evidenceIds': ['ATTR_0001']},
|
||||
{'priority': 'P0', 'label': '规格实测', 'action': '按0.9m床铺装', 'evidenceIds': ['ATTR_0003']},
|
||||
]},
|
||||
'newProductPlans': {'plans': [
|
||||
{'type': '主销款', 'name': '主销', 'positioning': '保持全棉三件套定义', 'evidenceIds': ['ATTR_0001']},
|
||||
{'name': '无证据升级', 'evidenceIds': []},
|
||||
{'type': '升级款', 'name': '第三套', 'positioning': '按BOM校准', 'evidenceIds': ['ATTR_0002']},
|
||||
], 'chapterTitle': '只输出有证据的开品方向'},
|
||||
'validationLoop': {'chapterTitle': '三道验证门决定是否上架', 'rows': [
|
||||
{'stage': '商品定义', 'owner': '商品企划 / 商品运营', 'metrics': '核对定义', 'successMeaning': '参数一致', 'nextAction': '进入打样', 'evidenceIds': ['ATTR_0001']},
|
||||
{'stage': '规格实测', 'owner': '供应链 / 品控', 'metrics': '实床铺装', 'successMeaning': '规格适配', 'nextAction': '锁定SKU', 'evidenceIds': ['ATTR_0003']},
|
||||
{'stage': '页面验收', 'owner': '视觉与内容', 'metrics': '核对页面', 'successMeaning': '页面一致', 'nextAction': '上架', 'evidenceIds': ['ATTR_0002']},
|
||||
]},
|
||||
}
|
||||
result = ensure_experience_solution({'facts': facts, 'experienceSolution': exp, 'meta': {}})
|
||||
output = str(result['experienceSolution'])
|
||||
labels = [x['label'] for x in result['experienceSolution']['productExperience']['parameterFacts']]
|
||||
signals = result['experienceSolution']['customerExperience']['signals']
|
||||
self.assertTrue(result['meta']['reportReady'])
|
||||
self.assertEqual('全棉三件套', result['facts']['product']['title'])
|
||||
self.assertIn('套件 BOM', labels)
|
||||
self.assertIn('材质 / 成分', labels)
|
||||
self.assertEqual([], signals['risks'])
|
||||
self.assertTrue(signals['questions'][0]['label'].startswith('购买前关注:'))
|
||||
self.assertEqual(2, len(result['experienceSolution']['newProductPlans']['plans']))
|
||||
self.assertNotIn('无证据升级', output)
|
||||
self.assertNotIn('项目负责人', output)
|
||||
self.assertNotIn('客户为什么购买', output)
|
||||
self.assertNotIn('提升转化', output)
|
||||
self.assertNotIn('决定成交', output)
|
||||
self.assertNotIn('爆款', output)
|
||||
doc = render(result, 'evidence-only')
|
||||
self.assertIn('老板速览', doc)
|
||||
self.assertIn('下一款先做什么', doc)
|
||||
self.assertIn('V9.2.5', doc)
|
||||
self.assertNotIn('项目负责人', doc)
|
||||
|
||||
def test_incomplete_final_editor_output_is_repaired_from_real_evidence(self):
|
||||
facts = {
|
||||
'product': {'title': '学生宿舍全棉三件套', 'itemId': 'repair-1'},
|
||||
'sales': {'sold': '1000+', 'currentPrice': '129'},
|
||||
'attributes': [
|
||||
{'name': '床单面料材质', 'value': '全棉'},
|
||||
{'name': '套件组成', 'value': '床单、被套、枕套'},
|
||||
{'name': '适用床尺寸', 'value': '0.9m床三件套'},
|
||||
{'name': '产品等级', 'value': '合格品'},
|
||||
],
|
||||
'reviews': [
|
||||
{'content': '面料柔软,尺寸合适'},
|
||||
{'content': '花色好看,宿舍床铺上正好'},
|
||||
],
|
||||
'questions': [{'question': '洗后会不会缩水?'}],
|
||||
'images': {},
|
||||
}
|
||||
repaired = repair_core_solution_with_evidence({}, facts)
|
||||
result = ensure_experience_solution({'facts': facts, 'experienceSolution': repaired, 'meta': {}})
|
||||
self.assertTrue(result['meta']['reportReady'])
|
||||
self.assertTrue(result['meta']['reportValidation']['ok'])
|
||||
self.assertEqual([], result['meta']['reportValidation']['missing'])
|
||||
self.assertFalse(result['meta']['fallbackReport'])
|
||||
self.assertIn('evidenceIds', result['experienceSolution']['validationLoop']['rows'][0])
|
||||
|
||||
|
||||
def test_legacy_grouped_images_are_preserved_without_strict_provenance(self):
|
||||
raw={
|
||||
'product':{'title':'测试商品','itemId':'1'},
|
||||
'reviews':[],
|
||||
'images':{
|
||||
'main':['https://img.alicdn.com/item_pic.jpg'],
|
||||
'detail':['https://img.alicdn.com/detail_01.jpg'],
|
||||
'sku':['https://img.alicdn.com/sku_01.jpg'],
|
||||
'buyerShow':[]
|
||||
}
|
||||
}
|
||||
clean=normalize(raw)
|
||||
self.assertEqual(['https://img.alicdn.com/item_pic.jpg'], clean['images']['main'])
|
||||
self.assertEqual(['https://img.alicdn.com/detail_01.jpg'], clean['images']['detail'])
|
||||
self.assertEqual(['https://img.alicdn.com/sku_01.jpg'], clean['images']['sku'])
|
||||
self.assertEqual('legacy-review-only', clean['images']['classification']['version'])
|
||||
sources={x['group']:x['source'] for x in clean['images']['provenance'] if x['group'] in ('main','detail','sku')}
|
||||
self.assertEqual('legacy_group_main', sources['main'])
|
||||
self.assertEqual('legacy_group_detail', sources['detail'])
|
||||
self.assertEqual('legacy_group_sku', sources['sku'])
|
||||
|
||||
def test_validation_reports_exact_missing_fields(self):
|
||||
validation = core_solution_diagnostics({'reportSummary': {'title': '有标题'}})
|
||||
self.assertFalse(validation['ok'])
|
||||
self.assertNotIn('reportSummary.title', validation['missing'])
|
||||
self.assertIn('reportSummary.verdict', validation['missing'])
|
||||
self.assertIn('validationLoop.rows', validation['missing'])
|
||||
|
||||
def test_task_progress_exposes_real_evidence_counts(self):
|
||||
raw = self.analysis['facts']
|
||||
summary = task_evidence_summary(raw)
|
||||
self.assertEqual(3, summary['counts']['parameters'])
|
||||
self.assertEqual(2, summary['counts']['reviews'])
|
||||
self.assertEqual(1, summary['counts']['main'])
|
||||
self.assertEqual(0, summary['counts']['detail'])
|
||||
self.assertEqual(1, summary['counts']['buyerShow'])
|
||||
self.assertEqual('51', summary['publicReviewCount'])
|
||||
self.assertFalse(summary['reviewComplete'])
|
||||
page = task_page('fixture')
|
||||
self.assertIn('本次证据', page)
|
||||
self.assertIn('生成进度', page)
|
||||
self.assertIn('evidenceStats', page)
|
||||
self.assertIn("not_collected:'未采到'", TASK_JS)
|
||||
self.assertIn("visual:'商品主图核验'", TASK_JS)
|
||||
self.assertIn("detail:'商品详情图核验'", TASK_JS)
|
||||
self.assertIn("editor:'最终报告汇总与校验'", TASK_JS)
|
||||
self.assertIn("failed:'失败'", TASK_JS)
|
||||
self.assertNotIn("visual:'主图成交'", TASK_JS)
|
||||
|
||||
def test_strict_image_groups_are_disjoint_and_traceable(self):
|
||||
shared = 'https://img.alicdn.com/shared-review.jpg'
|
||||
buyer_two = 'https://img.alicdn.com/second-review.jpg'
|
||||
main = 'https://img.alicdn.com/verified-main.jpg'
|
||||
structured_main = 'https://img.alicdn.com/structured-main.jpg'
|
||||
detail = 'https://img.alicdn.com/verified-detail.jpg'
|
||||
structured_detail = 'https://img.alicdn.com/structured-detail.jpg'
|
||||
facts = {
|
||||
'meta': {'imageClassificationVersion': 'strict-v1'},
|
||||
'product': {'title': '真实商品标题', 'itemId': '1'},
|
||||
'sales': {}, 'attributes': [], 'questions': [],
|
||||
'reviews': [{'content': '评论原文', 'images': [shared, buyer_two], 'source': 'dom_visible'}],
|
||||
'images': {
|
||||
'main': [shared, main, structured_main], 'detail': [shared, detail, structured_detail], 'sku': [], 'buyerShow': [shared, buyer_two],
|
||||
'provenance': [
|
||||
{'url': shared, 'group': 'buyerShow', 'source': 'review_record'},
|
||||
{'url': buyer_two, 'group': 'buyerShow', 'source': 'review_record'},
|
||||
{'url': shared, 'group': 'main', 'source': 'dom_main_gallery'},
|
||||
{'url': main, 'group': 'main', 'source': 'dom_main_gallery'},
|
||||
{'url': structured_main, 'group': 'main', 'source': 'structured_main_gallery'},
|
||||
{'url': detail, 'group': 'detail', 'source': 'dom_product_description'},
|
||||
{'url': structured_detail, 'group': 'detail', 'source': 'structured_product_description'},
|
||||
],
|
||||
},
|
||||
}
|
||||
clean = normalize(facts)
|
||||
groups = clean['images']
|
||||
self.assertEqual([shared, buyer_two], groups['buyerShow'])
|
||||
self.assertEqual([main, structured_main], groups['main'])
|
||||
self.assertEqual([detail, structured_detail], groups['detail'])
|
||||
self.assertTrue(groups['classification']['groupsAreDisjoint'])
|
||||
self.assertEqual(6, len(set(groups['buyerShow'] + groups['main'] + groups['detail'])))
|
||||
buyer_items = _buyer_evidence(clean)
|
||||
buyer = buyer_items[0]
|
||||
self.assertEqual('REV_000001', buyer['reviewEvidenceId'])
|
||||
self.assertEqual(2, len(buyer_items))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,60 @@
|
||||
import sys, unittest
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'server'))
|
||||
from renderer import render, image_source, img_tag
|
||||
from unittest.mock import patch
|
||||
import tempfile
|
||||
import server as server_module
|
||||
from analysis import ensure_experience_solution
|
||||
|
||||
class V910RegressionTest(unittest.TestCase):
|
||||
def base_result(self):
|
||||
facts={'product':{'title':'测试商品'},'sales':{},'attributes':[{'name':'材质','value':'全棉'},{'name':'规格','value':'700g'}], 'reviews':[{'content':'使用后很顺滑'}], 'questions':[], 'sku':[], 'images':{'main':['https://img.example.com/a.jpg'],'sku':['https://img.example.com/s.jpg']}}
|
||||
exp={'reportSummary':{'title':'测试报告','verdict':'先核对商品结构'},'ownerOverview':{'chapterTitle':'页面事实与评论结果已形成','cards':[{'label':'结论','headline':'先核对规格','evidenceIds':['ATTR_0001']},{'label':'产品','headline':'材质明确','evidenceIds':['ATTR_0001']},{'label':'动作','headline':'统一页面口径','evidenceIds':['ATTR_0002']}]},'customerExperience':{'stages':[{'label':'使用','positiveConfirmation':'顺滑','evidenceIds':['REV_000001']},{'label':'规格','positiveConfirmation':'700g','evidenceIds':['ATTR_0002']}], 'consumerInsights':{'coreSatisfaction':[{'driver':'顺滑体验','evidenceIds':['REV_000001']}]}},'productExperience':{'parameterFacts':[{'label':'材质','value':'全棉','evidenceIds':['ATTR_0001']},{'label':'规格','value':'700g','evidenceIds':['ATTR_0002']}], 'gates':[{'label':'材质','action':'核对','evidenceIds':['ATTR_0001']},{'label':'规格','action':'核对','evidenceIds':['ATTR_0002']}]},'newProductPlans':{'plans':[{'type':'稳健优化','name':'规格清晰化','whyThisPlan':'减少理解成本','evidenceIds':['ATTR_0002']}]},'validationLoop':{'rows':[{'stage':'定义','owner':'商品运营','metrics':'核对','successMeaning':'一致','nextAction':'进入下一步','evidenceIds':['ATTR_0001']},{'stage':'规格','owner':'供应链','metrics':'核对','successMeaning':'一致','nextAction':'锁定','evidenceIds':['ATTR_0002']},{'stage':'页面','owner':'视觉','metrics':'核对','successMeaning':'一致','nextAction':'发布','evidenceIds':['ATTR_0002']} ]}}
|
||||
r=ensure_experience_solution({'facts':facts,'experienceSolution':exp,'meta':{}})
|
||||
for e in r['evidenceLedger']:
|
||||
if e.get('id')=='IMG_MAIN_0001':
|
||||
e.setdefault('meta',{})['localUrl']='/reports/assets/test/main/IMG_MAIN_0001.jpg';e['meta']['sourceUrl']=e['value']
|
||||
return r
|
||||
|
||||
def test_missing_sku_not_rendered_as_zero(self):
|
||||
doc=render(self.base_result(),'test')
|
||||
self.assertNotIn('SKU 0',doc)
|
||||
self.assertIn('结构化SKU 未完整采集',doc)
|
||||
self.assertIn('SKU图片数量不等于SKU数量',doc)
|
||||
|
||||
def test_renderer_prefers_local_image_and_keeps_remote_fallback(self):
|
||||
entry={'value':'https://img.example.com/a.jpg','meta':{'localUrl':'/reports/assets/test/main/IMG_MAIN_0001.jpg','sourceUrl':'https://img.example.com/a.jpg'}}
|
||||
local,remote=image_source(entry)
|
||||
html=img_tag(local,'商品主图',remote)
|
||||
self.assertIn('/reports/assets/test/main/IMG_MAIN_0001.jpg',html)
|
||||
self.assertIn('https://img.example.com/a.jpg',html)
|
||||
self.assertIn('data-fallback',html)
|
||||
|
||||
def test_renderer_never_prints_python_dict_literal(self):
|
||||
r=self.base_result()
|
||||
r['experienceSolution']['titleAnalysis']={
|
||||
'currentExpression':[{'value':'信息明确','evidenceIds':['ATTR_0001']}],
|
||||
'reinforce':[{'topic':'规格需拆清','evidenceIds':['ATTR_0002']}]
|
||||
}
|
||||
doc=render(r,'test')
|
||||
self.assertNotIn("{'point':",doc)
|
||||
self.assertIn('信息明确',doc)
|
||||
|
||||
def test_cache_report_images_writes_local_url_and_preserves_source(self):
|
||||
class FakeResponse:
|
||||
headers={'Content-Type':'image/jpeg'}
|
||||
def __enter__(self):return self
|
||||
def __exit__(self,*a):return False
|
||||
def read(self,n=-1):return b'\xff\xd8\xffFAKEJPEG'
|
||||
result={'evidenceLedger':[{'id':'IMG_MAIN_0001','type':'image','value':'https://img.example.com/a.jpg','meta':{'group':'main'}}]}
|
||||
with tempfile.TemporaryDirectory() as td, patch.object(server_module,'IMAGE_ASSET_ROOT',Path(td)), patch('server.urllib.request.urlopen',return_value=FakeResponse()):
|
||||
out=server_module.cache_report_images(result,'task1')
|
||||
meta=out['evidenceLedger'][0]['meta']
|
||||
self.assertEqual('success',meta['cacheStatus'])
|
||||
self.assertEqual('https://img.example.com/a.jpg',meta['sourceUrl'])
|
||||
self.assertTrue(meta['localUrl'].startswith('/reports/assets/task1/main/'))
|
||||
self.assertTrue(list((Path(td)/'task1'/'main').glob('IMG_MAIN_0001.*')))
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,75 @@
|
||||
import sys, unittest
|
||||
from pathlib import Path
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'server'))
|
||||
from analysis import ensure_experience_solution, _evidence_backed_launch_fallback, evidence_ledger, normalize
|
||||
from renderer import render, img_tag, image_source
|
||||
|
||||
class V920ImageDrivenReportTest(unittest.TestCase):
|
||||
def fixture(self):
|
||||
facts={
|
||||
'meta':{'imageClassificationVersion':'strict-v1'},
|
||||
'product':{'title':'学生宿舍七件套纯棉床笠床单四季子母被'},
|
||||
'sales':{'currentPrice':'637.5','sold':'1万+'},
|
||||
'attributes':[{'name':'面料支数','value':'40支'},{'name':'床单面料材质','value':'全棉'},{'name':'适用床尺寸','value':'90×190cm'},{'name':'套件组成','value':'七件套'}],
|
||||
'sku':[],
|
||||
'reviews':[{'content':'面料柔软透气,宿舍用很方便'},{'content':'枕头有点偏软'}],
|
||||
'questions':[],
|
||||
'images':{'main':['https://img.example.com/m1.jpg','https://img.example.com/m2.jpg'],'detail':['https://img.example.com/d1.jpg','https://img.example.com/d2.jpg'],'sku':['https://img.example.com/s1.jpg'],'buyerShow':['https://img.example.com/b1.jpg'],'provenance':[{'url':'https://img.example.com/m1.jpg','group':'main','source':'dom_main_gallery'},{'url':'https://img.example.com/m2.jpg','group':'main','source':'dom_main_gallery'},{'url':'https://img.example.com/d1.jpg','group':'detail','source':'dom_product_description'},{'url':'https://img.example.com/d2.jpg','group':'detail','source':'dom_product_description'},{'url':'https://img.example.com/s1.jpg','group':'sku','source':'dom_sku'},{'url':'https://img.example.com/b1.jpg','group':'buyerShow','source':'review_record'}]}
|
||||
}
|
||||
led=evidence_ledger(normalize(facts)); ids={x['id'] for x in led}
|
||||
exp={
|
||||
'reportSummary':{'title':'宿舍七件套商品分析报告','verdict':'当前围绕宿舍一站式买齐形成商品逻辑,下一款优先把规格与睡感选择说清。','coverImageId':'IMG_MAIN_0001'},
|
||||
'ownerOverview':{'cards':[{'label':'商品定位','headline':'宿舍一站式七件套','evidenceIds':['P_TITLE']},{'label':'主要产品资产','headline':'40支全棉 + 七件套','evidenceIds':['ATTR_0001','ATTR_0002']},{'label':'下一款优先方向','headline':'规格与睡感更易选','evidenceIds':['ATTR_0003','REV_000002']}]},
|
||||
'titleAnalysis':{'professionalOpinion':'场景与套件范围清楚,建议进一步强化40支品质参数。','currentExpression':[{'label':'场景','value':'学生宿舍','evidenceIds':['P_TITLE']}],'reinforce':[{'topic':'40支全棉','reason':'品质参数可前置','evidenceIds':['ATTR_0001','ATTR_0002']}],'recommendedActions':[{'action':'统一标题与SKU口径','evidenceIds':['P_TITLE']}]},
|
||||
'visualCommerce':{'professionalOpinion':'主图先建立套装认知,再解释具体产品。','sequence':['一站式','套件清单'], 'items':[{'imageEvidenceId':'IMG_MAIN_0001','role':'一站式套装','message':'建立宿舍整套购买认知','professionalAdvice':'场景更贴近真实宿舍','evidenceIds':['IMG_MAIN_0001']},{'imageEvidenceId':'IMG_MAIN_0002','role':'套件清单','message':'解释具体包含内容','professionalAdvice':'补尺寸差异','evidenceIds':['IMG_MAIN_0002']}]},
|
||||
'detailCommerce':{'professionalOpinion':'详情负责承接材质和规格证明。','contentGroups':[{'type':'材质品质','representativeImageId':'IMG_DETAIL_0001','message':'解释40支全棉','professionalAdvice':'前置品质证明','evidenceIds':['IMG_DETAIL_0001','ATTR_0001']}]},
|
||||
'customerExperience':{'professionalOpinion':'评论验证了柔软透气,同时出现睡感差异。','consumerInsights':{'experienceSignals':[{'type':'稳定体验','insight':'柔软透气','implication':'继续保留全棉','evidenceIds':['REV_000001']},{'type':'体验差异','insight':'枕头偏软','implication':'增加睡感说明','evidenceIds':['REV_000002']}]},'stages':[{'label':'使用','positiveConfirmation':'柔软透气','evidenceIds':['REV_000001']},{'label':'睡感','positiveConfirmation':'软硬有差异','evidenceIds':['REV_000002']}]},
|
||||
'productExperience':{'parameterFacts':[{'label':'支数','value':'40支','evidenceIds':['ATTR_0001']},{'label':'材质','value':'全棉','evidenceIds':['ATTR_0002']}], 'gates':[{'label':'规格','action':'核对','evidenceIds':['ATTR_0003']},{'label':'套件','action':'核对','evidenceIds':['ATTR_0004']}]},
|
||||
'newProductPlans':{'professionalOpinion':'下一款围绕规格更清楚与睡感区分优化。','plans':[{'type':'现有基础优化','name':'宿舍七件套2.0','whyThisPlan':'保留全棉与七件套逻辑,强化规格选择。','visualReferenceImageIds':['IMG_MAIN_0001'],'productChanges':[{'action':'明确床型尺寸','evidenceIds':['ATTR_0003']}],'skuStrategy':'款式→尺寸→花色','mainImagePlan':['首图保留一站式'],'detailPagePlan':['紧接材质证明'],'evidenceIds':['P_TITLE','ATTR_0001','ATTR_0003','IMG_MAIN_0001']}]},
|
||||
'validationLoop':{'rows':[{'stage':'定义','owner':'商品运营','metrics':'核对','successMeaning':'一致','nextAction':'打样','evidenceIds':['P_TITLE']},{'stage':'规格','owner':'供应链','metrics':'实测','successMeaning':'一致','nextAction':'锁定','evidenceIds':['ATTR_0003']},{'stage':'页面','owner':'视觉','metrics':'核对','successMeaning':'一致','nextAction':'发布','evidenceIds':['IMG_MAIN_0001']}]}
|
||||
}
|
||||
return ensure_experience_solution({'facts':facts,'experienceSolution':exp,'meta':{}})
|
||||
|
||||
def test_renderer_is_image_driven_and_professional(self):
|
||||
doc=render(self.fixture(),'v920')
|
||||
for text in ('老板速览','下一款先做什么','主图到底在卖什么','详情页证明了什么','先解决适配,再解决审美','消费者真正感受到什么'):
|
||||
self.assertIn(text,doc)
|
||||
self.assertIn('v92-main-grid',doc)
|
||||
self.assertIn('v925-detail-list',doc)
|
||||
self.assertIn('v92-sku-mosaic',doc)
|
||||
self.assertIn('v92-buyer-grid',doc)
|
||||
self.assertIn('v925-plan-grid',doc)
|
||||
self.assertNotIn('[数据事实]',doc)
|
||||
self.assertNotIn('[分析判断]',doc)
|
||||
self.assertNotIn('[设计建议]',doc)
|
||||
self.assertNotIn('整体有效点',doc)
|
||||
self.assertNotIn('主要问题',doc)
|
||||
|
||||
def test_cover_has_no_card_border_wrapper(self):
|
||||
doc=render(self.fixture(),'v920')
|
||||
self.assertIn('class="v92-cover"',doc)
|
||||
self.assertIn('cover-ratio',doc)
|
||||
self.assertNotIn('hero-shot',doc)
|
||||
|
||||
def test_launch_fallback_never_empty_with_real_evidence(self):
|
||||
facts=normalize(self.fixture()['facts'])
|
||||
ids={x['id'] for x in evidence_ledger(facts)}
|
||||
plans=_evidence_backed_launch_fallback(facts,ids)
|
||||
self.assertGreaterEqual(len(plans.get('plans') or []),1)
|
||||
self.assertTrue(plans['plans'][0]['evidenceIds'])
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
||||
|
||||
def test_img_tag_remote_fallback_does_not_force_no_referrer():
|
||||
html = img_tag('https://img.example.com/a.jpg', '图', 'https://img.example.com/b.jpg')
|
||||
assert 'referrerpolicy="no-referrer"' not in html
|
||||
assert 'data-fallback="https://img.example.com/b.jpg"' in html
|
||||
|
||||
|
||||
def test_image_source_keeps_remote_when_cache_failed():
|
||||
e={'type':'image','value':'https://img.example.com/a.jpg','meta':{'sourceUrl':'https://img.example.com/a.jpg','cacheStatus':'failed'}}
|
||||
src,fb=image_source(e)
|
||||
assert src=='https://img.example.com/a.jpg'
|
||||
assert fb=='https://img.example.com/a.jpg'
|
||||
@@ -0,0 +1,21 @@
|
||||
import json, re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
EXPECTED='9.2.5'
|
||||
|
||||
def test_version_consistency():
|
||||
assert (ROOT/'VERSION.txt').read_text().strip()==EXPECTED
|
||||
for rel in ['extension/manifest.json','collector-extension-standalone/manifest.json','codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/manifest.json']:
|
||||
data=json.loads((ROOT/rel).read_text(encoding='utf-8'))
|
||||
assert data['version']==EXPECTED, rel
|
||||
popup=(ROOT/'extension/popup.js').read_text(encoding='utf-8')
|
||||
assert f"EXPECTED_SERVER_VERSION='{EXPECTED}'" in popup
|
||||
server=(ROOT/'server/server.py').read_text(encoding='utf-8')
|
||||
assert f"SERVER_VERSION='{EXPECTED}'" in server or f'SERVER_VERSION = \'{EXPECTED}\'' in server
|
||||
analysis=(ROOT/'server/analysis.py').read_text(encoding='utf-8')
|
||||
assert f"ENGINE_VERSION='{EXPECTED}'" in analysis
|
||||
ai=(ROOT/'server/ai_client.py').read_text(encoding='utf-8')
|
||||
assert f"SERVER_VERSION = '{EXPECTED}'" in ai
|
||||
start=(ROOT/'start-tmall-ai.command').read_text(encoding='utf-8')
|
||||
assert f'VERSION="{EXPECTED}"' in start
|
||||
Reference in New Issue
Block a user