import html
import json
import re
from analysis import ensure_experience_solution
UNCERTAIN = ('可能', '或许', '推测', '无法判断', '不能判断', '证据不足', '尚不能', '不足以', '未知', '未识别', '不代表', '待验证')
def H(v): return html.escape('' if v is None else str(v))
def arr(v): return v if isinstance(v, list) else []
def safe(v): return '' if v in (None, '', [], {}) else (str(v) if not isinstance(v,(dict,list,tuple)) else '')
def text_of(v, keys=('text','point','statement','label','value','driver','problem','concern','signal','item')):
if v in (None,'',[],{}): return ''
if isinstance(v,str): return v
if isinstance(v,(int,float)): return str(v)
if isinstance(v,dict):
for k in keys:
if v.get(k) not in (None,'',[],{}): return text_of(v.get(k),keys)
return ''
return ''
def concise(v, limit=90, verified=True):
text = re.sub(r'\s+', ' ', safe(v)).strip()
if not text: return ''
parts = [x.strip() for x in re.split(r'(?<=[。!?;])', text) if x.strip()]
if verified:
text = ''.join(x for x in parts if not any(word in x for word in UNCERTAIN))
if not text: return ''
if len(text) <= limit: return text
cut = max(text.rfind(mark, 0, limit + 1) for mark in ',。;:')
if cut < limit // 2: cut = limit
return text[:cut].rstrip(',。;: ') + '…'
def evidence_index(a): return {x.get('id'): x for x in arr(a.get('evidenceLedger')) if isinstance(x, dict) and x.get('id')}
def image_entry(eid, idx, allowed_groups=None):
e = idx.get(eid) or {}; value = e.get('value')
group=(e.get('meta') or {}).get('group')
if e.get('type')!='image' or not isinstance(value,str) or not value.startswith(('http://','https://','data:image/')):return None
if allowed_groups and group not in allowed_groups:return None
return e
def image_url(eid, idx, allowed_groups=None):
e=image_entry(eid,idx,allowed_groups)
if not e:return ''
return (e.get('meta') or {}).get('localUrl') or e.get('value')
def image_source(e):
if not isinstance(e,dict):return ('','')
meta=e.get('meta') or {}; return (meta.get('localUrl') or e.get('value') or '', meta.get('sourceUrl') or e.get('value') or '')
def img_tag(url, alt='商品图片', fallback=''):
if not url:return ''
fb=fallback if fallback and fallback!=url else ''
attrs=f' data-fallback="{H(fb)}"' if fb else ''
return f''
def product_hero(visual, idx):
for item in arr(visual.get('items')):
if isinstance(item, dict):
url = image_url(safe(item.get('imageEvidenceId')), idx, {'main'})
if url: return url
return ''
def action_list(text, limit=5):
items = []
for value in re.split(r'[;。]\s*', safe(text)):
value = concise(value, 64)
if value and value not in items: items.append(value)
return '
' + ''.join(f'
{H(x)}
' for x in items[:limit]) + '
' if items else ''
def owner_cards(cards):
out = []
for x in arr(cards)[:6]:
if not isinstance(x, dict): continue
headline = concise(x.get('headline'), 46)
bullets = [concise(v, 42) for v in arr(x.get('bullets'))]
bullets = [v for v in bullets if v][:2]
if not headline: continue
detail = '
' + ''.join(f'
{H(v)}
' for v in bullets) + '
' if bullets else ''
out.append(f'{H(x.get("label"))}{H(headline)}{detail}')
return ''.join(out)
def overview_rows(items, kind):
out = []
for x in arr(items)[:6]:
if not isinstance(x, dict): continue
label = concise(x.get('label'), 24, False)
value = concise(x.get('value'), 84, False)
note = concise(x.get('note'), 42, False)
priority = concise(x.get('priority'), 14, False)
if not label or not value: continue
meta = priority or note
out.append(
f''
+ (f'{H(meta)}' if meta else '')
+ f'{H(label)}
' + (
f'产品分析结论{H(verdict)}'
+ (f'{H(sample)}' if sample else '') + '' if verdict else ''
) + f'
{groups}
'
def decision_boundary_html(base):
reviews = base.get('reviewCount', 0)
public = safe(base.get('publicReviewCount'))
coverage = f'已采有效评论 {reviews}' + (f' / 页面公开 {public}' if public else '')
status = '评论采集已确认完整' if base.get('reviewCollectionComplete') else '评论采集未确认完整'
items = [
('证据覆盖', coverage, status),
('本报告支持', '商品拆解、样品验证、规格校准、页面信息修正', '基于当前页面参数与已采评论'),
('适用范围', '不输出生产规模、利润、转化或市场容量', '当前数据不含对应经营指标'),
('补充数据', '完整评论、流量转化、退款售后、成本毛利', '补齐后再进入经营测算'),
]
return '' + ''.join(
f'{H(label)}{H(value)}{H(note)}'
for label, value, note in items
) + ''
def review_results_html(overview):
def rows(items, tone):
out=[]
for x in arr(items)[:6]:
if not isinstance(x,dict):continue
label=concise(x.get('label'),24,False);value=concise(x.get('value'),100,False);note=concise(x.get('note') or x.get('priority'),26,False)
if not label or not value:continue
out.append(f''+(f'{H(note)}' if note else '')+f'{H(label)}
{H(value)}
')
return ''.join(out)
outcomes=rows(overview.get('outcomes'),'positive')
risks=rows([x for x in arr(overview.get('checks')) if isinstance(x,dict) and x.get('priority')=='明确反馈'],'risk')
questions=rows(overview.get('questions'),'question')
return ''.join([
f'
评论确认的结果
{outcomes}
' if outcomes else '',
f'
明确问题
{risks}
' if risks else '',
f'
购买问答反馈
{questions}
' if questions else '',
])
def visual_items(items, idx):
out = []
for x in arr(items):
if not isinstance(x, dict): continue
eid=safe(x.get('imageEvidenceId'));entry=image_entry(eid,idx,{'main','detail'})
if not entry: continue
url=entry.get('value');group=(entry.get('meta') or {}).get('group');source=(entry.get('meta') or {}).get('source')
source_label='商品主图画廊' if group=='main' else '商品详情描述区域'
if source not in ('meta_og_image','dom_main_gallery','structured_main_gallery','dom_product_description','structured_product_description'):continue
headline = concise(x.get('headline'), 42)
frame = concise(x.get('frameInfo'), 90)
subject = concise(x.get('productSubject'), 90)
pattern = concise(x.get('patternColor'), 90)
text_info = concise(x.get('textInfo'), 90)
hierarchy = concise(x.get('informationHierarchy'), 90)
task = concise(x.get('contentTask') or x.get('persuasionTask'), 72)
weakness = concise(x.get('weakness'), 72)
action = concise(x.get('borrowableAction') or x.get('action') or x.get('nextProductAction'), 82)
facts = ''.join([
f'
画面内容{H(frame)}
' if frame else '',
f'
商品主体{H(subject)}
' if subject else '',
f'
花型配色{H(pattern)}
' if pattern else '',
f'
文案信息{H(text_info)}
' if text_info else '',
f'
层级关系{H(hierarchy)}
' if hierarchy else '',
f'
表达任务{H(task)}
' if task else '',
f'
明确问题{H(weakness)}
' if weakness else '',
f'
可借鉴动作{H(action)}
' if action else ''
])
out.append('' + img_tag(url, '商品主图' if group=='main' else '商品详情图') + '
来源:' + H(source_label) + '
' + H(headline or ('已核验主图' if group=='main' else '已核验详情图')) + '
' + facts + '
')
return ''.join(out)
def buyer_evidence_html(block, idx):
groups={}
for item in arr((block or {}).get('items'))[:24]:
if not isinstance(item,dict):continue
image_id=safe(item.get('imageEvidenceId'));review_id=safe(item.get('reviewEvidenceId'))
image=image_entry(image_id,idx,{'buyerShow'});review=idx.get(review_id) or {};meta=(image or {}).get('meta') or {}
if not image or review.get('type')!='review' or review_id not in arr(meta.get('reviewEvidenceIds')):continue
value=review.get('value') if isinstance(review.get('value'),dict) else {};quote=concise(value.get('content'),90,False)
if not quote:continue
details=' · '.join(x for x in (concise(value.get('sku'),42,False),concise(value.get('date'),18,False)) if x)
group=groups.setdefault(review_id,{'quote':quote,'details':details,'images':[]})
group['images'].append(image.get('value'))
if not groups:return ''
title=H((block or {}).get('title') or '评论原图与对应评价')
image_count=sum(len(x['images']) for x in groups.values())
out=[]
for review_id,group in groups.items():
gallery=''.join(
''+img_tag(url,f'{review_id} 评论原图 {i}')+f'原图 {i:02d}'
for i,url in enumerate(group['images'],1)
)
out.append('
评论证据 · '+H(review_id)+'
'+H(group['quote'])+'
'+(''+H(group['details'])+'' if group['details'] else '')+'
'+gallery+'
')
return f'评论图片证据
{title} · {len(groups)} 条评价 / {image_count} 张原图
每组图片只关联同一条评论,按原比例完整展示;不作为商品主图、详情图或 SKU 图使用。
'+''.join(out)+'
'
def stats_html(stats):
out = []
for x in arr(stats)[:6]:
if isinstance(x, dict) and safe(x.get('value')):
out.append(f'
')
return ''.join(out)
def journey_html(stages):
xs = [x for x in arr(stages) if isinstance(x, dict)]
tabs, panels = [], []
for i, x in enumerate(xs):
positive = concise(x.get('positiveConfirmation'), 70); friction = concise(x.get('friction'), 70)
if not positive and not friction: continue
active = ' active' if not tabs else ''
tabs.append(f'')
facts = (f'已确认
{H(positive)}
' if positive else '') + (f'明确问题
{H(friction)}
' if friction else '')
quotes = []
for q in arr(x.get('quotes'))[:2]:
if isinstance(q, dict) and safe(q.get('text')): quotes.append(f'
' + ''.join(panels) if tabs else ''
def signals_html(signals):
if not signals: return ''
values = []
for key, tone in [('positive', 'good'), ('questions', 'ask'), ('risks', 'risk')]:
for x in arr(signals.get(key))[:5]:
label = concise(x.get('label'), 42) if isinstance(x, dict) else ''
if label: values.append(f'{H(label)}')
return '已确认体验信号
' + ''.join(values) + '
' if values else ''
def param_html(xs):
out = []
for x in arr(xs)[:8]:
value = concise(x.get('value'), 82) if isinstance(x, dict) else ''
if value: out.append(f'
{H(x.get("label"))}{H(value)}
')
return ''.join(out)
def gates_html(xs):
out = []
for x in arr(xs)[:6]:
action = concise(x.get('action'), 92) if isinstance(x, dict) else ''
if action: out.append(f'
')
return ''.join(out)
def plans_html(xs):
out = []
for x in arr(xs)[:3]:
if not isinstance(x, dict): continue
selling = selling_points_html(x.get('sellingPoints'))
specs = []
for spec in arr(x.get('specs')):
value = concise(spec.get('value'), 82) if isinstance(spec, dict) else ''
if value: specs.append(f'
{H(spec.get("label"))}{H(value)}
')
mg = x.get('manufacturingGate') or {}; vc = x.get('visualCommerce') or {}
out.append('' + H(x.get('type')) + '
' + H(concise(x.get('name'), 30, False)) + '
' + H(concise(x.get('positioning'), 110)) + '
' + selling + '
' + ''.join(specs) + '
' + H(mg.get('title') or '制造闸门') + '
' + action_list(mg.get('content')) + '
' + H(vc.get('title') or '首图与商详') + '
' + action_list(vc.get('content')) + '
')
return ''.join(out)
def selling_points_html(xs):
out=[]
for x in arr(xs)[:5]:
if not isinstance(x,dict):continue
slogan=concise(x.get('slogan'),34,False)
value=concise(x.get('consumerValue'),54,False)
source=concise(x.get('evidenceSource'),32,False)
action=concise(x.get('productAction'),64,False)
if not slogan:continue
out.append(f'{H(slogan)}'+(f'
{H(value)}
' if value else '')+(f'{H(source)}' if source else '')+(f'{H(action)}' if action else '')+'')
return 'NEXT PRODUCT SELLING POINTS
下一款明确卖点
'+''.join(out)+'
' if out else ''
def governance_html(gov):
if not isinstance(gov,dict):return ''
items=[('报告角色',gov.get('role')),('结构标准',gov.get('structure')),('内容来源',gov.get('sourceRule')),('证据规则',gov.get('evidenceRule'))]
rows=''.join(f'{H(label)}{H(concise(value,64,False))}' for label,value in items if safe(value))
return '
'+rows+'
' if rows else ''
def attr_lookup(raw, names):
for x in arr(raw.get('attributes')):
if not isinstance(x,dict):continue
name=safe(x.get('name'))
if any(k in name for k in names) and safe(x.get('value')):return safe(x.get('value'))
return ''
def product_profile_html(raw, sales, base):
rows=[
('一级品类', safe((raw.get('product') or {}).get('category')) or '床上用品'),
('商品标题', safe((raw.get('product') or {}).get('title'))),
('价格定位', ('¥'+safe(sales.get('currentPrice'))) if safe(sales.get('currentPrice')) else ''),
('公开销量', safe(sales.get('sold')) or safe(sales.get('cumulativeSales'))),
('目标人群', attr_lookup(raw, ['适用人群','适用对象'])),
('核心场景', attr_lookup(raw, ['适用场景','适用床尺寸','床品尺寸'])),
('产品结构', attr_lookup(raw, ['件数','套件组成','组合形式','款式'])),
('材质成分', attr_lookup(raw, ['成分含量','床单面料材质','被面材质','材质','棉种类'])),
('支数密度', ' / '.join(x for x in [attr_lookup(raw,['面料支数','纱线支数']), attr_lookup(raw,['面料密度','织物密度'])] if x)),
('工艺结构', attr_lookup(raw, ['床品工艺','织造工艺','印染工艺','工艺'])),
('风格花型', ' / '.join(x for x in [attr_lookup(raw,['风格']), attr_lookup(raw,['主图案类型','颜色分类'])] if x)),
('评论样本', f'{base.get("reviewCount",0)} 条有效评论'),
]
cells=''.join(f'
{H(k)}{H(concise(v,78,False))}
' for k,v in rows if safe(v))
return '
'+cells+'
' if cells else ''
def deal_logic_html(plans):
points=[]
for plan in arr(plans.get('plans'))[:1]:
if isinstance(plan,dict):points=arr(plan.get('sellingPoints'))
rows=[]
for i,x in enumerate(points[:5],1):
if not isinstance(x,dict):continue
rows.append(f'{i:02d}{H(concise(x.get("slogan"),34,False))}
' if rows else ''
def feature_power_html(overview, product_exp):
cards=[]
for x in arr(overview.get('cards')):
if not isinstance(x,dict):continue
label=safe(x.get('label')); headline=concise(x.get('headline'),42,False)
if not headline or label in ('销售事实','页面活动'):continue
tone=safe(x.get('tone')) or 'normal'
cards.append(f'{H(label)}{H(headline)}')
for x in arr(product_exp.get('gates'))[:3]:
if not isinstance(x,dict):continue
cards.append(f'{H(x.get("priority"))}{H(concise(x.get("label"),34,False))}
{H(concise(x.get("action"),64,False))}
')
return '
'+''.join(cards[:8])+'
' if cards else ''
def marketing_html(raw):
product=raw.get('product') or {}; title=safe(product.get('title'))
slots=[
('品类词', '床品 / 三件套' if re.search(r'床品|三件套|四件套|被套|床单|床笠', title) else ''),
('人群词', attr_lookup(raw,['适用人群']) or ('学生' if '学生' in title else '')),
('场景词', attr_lookup(raw,['适用床尺寸','适用场景'])),
('材质词', attr_lookup(raw,['床单面料材质','被面材质','成分含量','材质'])),
('工艺词', attr_lookup(raw,['面料支数','面料密度','床品工艺','织造工艺'])),
('风格词', attr_lookup(raw,['风格','主图案类型'])),
]
slot_html=''.join(f'
{H(k)}{H(concise(v,62,False))}
' for k,v in slots if safe(v))
promos=''.join(f'
{H(concise(x.get("text"),58,False))}
' for x in arr(raw.get('promotions'))[:4] if isinstance(x,dict) and safe(x.get('text')))
return '
商品标题'+H(concise(title,90,False))+'
'+slot_html+'
页面营销表达
'+promos+'
'
def visual_status_html(images, verified_visual):
rows=[('主图',len(arr(images.get('main'))),bool(arr(images.get('main')))),('详情图',len(arr(images.get('detail'))),bool(arr(images.get('detail')))),('SKU 图',len(arr(images.get('sku'))),bool(arr(images.get('sku')))),('评论实拍',len(arr(images.get('buyerShow'))),bool(arr(images.get('buyerShow'))))]
cards=''.join(f'{H(k)}{n}{"已采集" if ok else "未采集"}' for k,n,ok in rows)
body=verified_visual or '
主图/详情图未形成可核验图片条目时,本章只展示来源状态,不用评论图片替代主图或详情图。
'
return '
'+cards+'
'+body+'
'
def competition_html(plans, product_exp, overview):
point=''
for plan in arr(plans.get('plans'))[:1]:
if isinstance(plan,dict) and arr(plan.get('sellingPoints')):
point=concise(plan['sellingPoints'][0].get('slogan'),42,False)
risks=[concise(x.get('label'),34,False) for x in arr(product_exp.get('gates'))[:3] if isinstance(x,dict)]
return '
核心价值'+H(point or '已确认体验可复用')+'
来自页面参数与评论聚合,不写市场推断。
基础能力类目关键参数、规格与页面需要和真实商品一致
基础能力必须被主图、详情和参数共同证明。
风险点'+H(' / '.join(x for x in risks if x) or '按评论明确问题处理')+'
风险只按真实评论和参数校验项呈现。
'
def validation_rows(xs):
out = []
for x in arr(xs)[:6]:
if isinstance(x, dict) and any(safe(x.get(k)) for k in ('stage','metrics','successMeaning','nextAction')):
stage=concise(x.get('stage'),18,False);owner=concise(x.get('owner'),24,False)
if not owner:continue
out.append(f'
{H(stage)}
{H(owner)}
{H(concise(x.get("metrics"), 62))}
{H(concise(x.get("successMeaning"), 58))}
{H(concise(x.get("nextAction"), 62))}
')
return ''.join(out)
# V9.2 图片驱动专业报告。分析可以深,默认报告必须轻。
DISPLAY_MARKERS = ('[数据事实]','[分析判断]','[设计建议]','[证据不足]','数据事实:','分析判断:','设计建议:')
def clean_display(v, limit=120):
t=text_of(v) if not isinstance(v,str) else v
if not t:return ''
t=re.sub(r'\*\*|__','',t)
for m in DISPLAY_MARKERS:t=t.replace(m,'')
t=re.sub(r'\s*/\s*',';',t)
t=re.sub(r'\s+',' ',t).strip(' ;/')
return concise(t,limit,False)
def first_text(*values, limit=100):
for v in values:
t=clean_display(v,limit)
if t:return t
return ''
def _entries(idx, group, limit=200):
return [(eid,e) for eid,e in idx.items() if e.get('type')=='image' and (e.get('meta') or {}).get('group')==group][:limit]
def _visual_figure(e, alt, css=''):
src,fb=image_source(e)
if not src:return '
图片未采集
'
return ''+img_tag(src,alt,fb)+'
图片暂不可用
'
def _section(no,title,deck,body,sid):
return f'
{H(no)}
{H(title)}
{H(deck)}
{body}
'
def _opinion(text):
text=clean_display(text,45)
return f'
{H(text)}
' if text else ''
def _chips(values, limit=3):
vals=[]
for v in arr(values)[:limit]:
t=clean_display(v.get('value') if isinstance(v,dict) else v,10)
if t and t not in vals: vals.append(t)
return '
'+H(' · '.join(vals))+'
' if vals else ''
def _owner_overview(exp, raw, base, sales):
src=arr((exp.get('ownerOverview') or {}).get('cards'))
wanted=['为什么能卖','值得继承','下一款优先']
mapped={k:None for k in wanted}
for x in src:
if not isinstance(x,dict): continue
label=clean_display(x.get('label'),16); headline=clean_display(x.get('headline'),20); note=clean_display(x.get('note'),22)
if not headline: continue
for k in wanted:
if k in label and mapped[k] is None: mapped[k]=(headline,note)
# Compatibility fallback from old labels.
legacy={'为什么能卖':['商品定位','场景价值'],'值得继承':['主要产品资产','产品价值'],'下一款优先':['下一款优先方向','可强化方向']}
for x in src:
if not isinstance(x,dict): continue
label=clean_display(x.get('label'),18); headline=clean_display(x.get('headline'),20)
for target,keys in legacy.items():
if mapped[target] is None and any(k in label for k in keys) and headline: mapped[target]=(headline,'')
pd=arr((exp.get('productValue') or {}).get('items'))
for x in pd:
if not isinstance(x,dict): continue
typ=clean_display(x.get('type'),16); val=first_text(x.get('value'),limit=30)
if typ=='场景价值' and mapped['为什么能卖'] is None: mapped['为什么能卖']=(val,'')
elif typ=='产品价值' and mapped['值得继承'] is None: mapped['值得继承']=(val,'')
elif typ=='可强化方向' and mapped['下一款优先'] is None: mapped['下一款优先']=(val,'')
title=clean_display((raw.get('product') or {}).get('title'),36)
mapped['为什么能卖']=mapped['为什么能卖'] or (title or '当前商品场景与产品组合', '')
mapped['值得继承']=mapped['值得继承'] or (f'已确认 {base.get("attributeCount",0)} 项产品参数', '')
mapped['下一款优先']=mapped['下一款优先'] or ('规格更清楚 · 品质更可证 · 图片更好选', '')
cards=[]
for k in wanted:
headline,note=mapped[k]
cards.append(f'{H(k)}{H(headline)}'+(f'
{H(note)}
' if note else '')+'')
return '
'+''.join(cards)+'
'
def _overview(exp, raw, base, sales):
cards=[]
source=arr((exp.get('ownerOverview') or {}).get('cards'))
preferred={'商品定位':None,'主要产品资产':None,'下一款优先方向':None}
for x in source:
if not isinstance(x,dict):continue
label=clean_display(x.get('label'),18);headline=clean_display(x.get('headline'),42)
if not headline:continue
low=label+headline
if preferred['商品定位'] is None and any(k in low for k in ('定位','场景','人群','商品定义')):preferred['商品定位']=headline
elif preferred['主要产品资产'] is None and any(k in low for k in ('产品','材质','资产','体验','参数')):preferred['主要产品资产']=headline
elif preferred['下一款优先方向'] is None and any(k in low for k in ('下一款','动作','优化','调整')):preferred['下一款优先方向']=headline
pd=(exp.get('productValue') or {}).get('items') or []
for x in pd:
if not isinstance(x,dict):continue
typ=clean_display(x.get('type'),16);val=clean_display(x.get('value'),42)
if typ=='场景价值' and not preferred['商品定位']:preferred['商品定位']=val
if typ=='产品价值' and not preferred['主要产品资产']:preferred['主要产品资产']=val
if typ=='可强化方向' and not preferred['下一款优先方向']:preferred['下一款优先方向']=val
title=clean_display((raw.get('product') or {}).get('title'),60)
attrs=arr(raw.get('attributes'))
attr_text=' / '.join(clean_display(x.get('value'),30) for x in attrs[:6] if isinstance(x,dict) and clean_display(x.get('value'),30))
preferred['商品定位']=preferred['商品定位'] or title or '当前商品定位以页面事实为准'
preferred['主要产品资产']=preferred['主要产品资产'] or clean_display(attr_text,70) or f'已采 {base.get("attributeCount",0)} 项商品参数'
preferred['下一款优先方向']=preferred['下一款优先方向'] or '优先把规格、图片表达和商品页面口径统一'
labels=[('商品定位',preferred['商品定位'],'先看这款商品解决什么场景'),('主要产品资产',preferred['主要产品资产'],'只保留当前证据可以确认的资产'),('下一款优先方向',preferred['下一款优先方向'],'把分析转成下一款动作')]
return '
'+''.join(f'{H(l)}{H(v)}
{H(n)}
' for l,v,n in labels)+'
'
def _title_v92(raw, exp, a):
block=exp.get('titleAnalysis') or (a.get('merchandisingDecision') or {}).get('titleAnalysis') or {}
title=clean_display(block.get('originalTitle') or (raw.get('product') or {}).get('title'),130)
opinion=first_text(block.get('professionalOpinion'),block.get('thesis'),limit=58) or '标题优先统一场景、规格与关键品质参数。'
current=[]
for x in arr(block.get('currentExpression'))[:6]:
if isinstance(x,dict): t=first_text(x.get('value'),x.get('text'),limit=10)
else: t=clean_display(x,14)
if t and t not in current: current.append(t)
if not current:
for kw in ('大学生宿舍','学生宿舍','六件套','七件套','全棉','纯棉','40支','四季'):
if kw in title: current.append(kw)
reinforce=[]
for x in arr(block.get('reinforce'))[:5]:
t=first_text(x.get('topic') if isinstance(x,dict) else x,limit=14)
if t and t not in reinforce: reinforce.append(t)
actions=[first_text(x.get('action') if isinstance(x,dict) else x,limit=28) for x in arr(block.get('recommendedActions'))[:2]]
if actions and not reinforce: reinforce=actions
return _opinion(opinion)+f'
当前标题{H(title)}
已表达{H(" · ".join(current[:5]) or "按当前标题识别")}建议强化{H(" · ".join(reinforce[:5]) or (actions[0] if actions else "规格统一 · 品质参数"))}
'
def _main_v92(exp,a,idx):
block=exp.get('visualCommerce') or {}; vd=a.get('visualDecision') or {}
opinion=first_text(block.get('professionalOpinion'),(vd.get('visualThesis') or {}).get('professionalOpinion'),limit=58) or '主图优先让用户看懂商品信息,再决定是否展开详细分析。'
items=arr(block.get('items')) or arr(vd.get('imageRoles')) or arr(vd.get('mainImageContent'))
byid={safe(x.get('imageEvidenceId')):x for x in items if isinstance(x,dict)}
cards=[]
for n,(eid,e) in enumerate(_entries(idx,'main',5),1):
x=byid.get(eid,{})
role=first_text(x.get('imageRole'),x.get('role'),x.get('contentTask'),limit=8) or f'主图{n}'
signals=arr(x.get('visualSignals'))
if not signals:
# Compatibility: old message becomes one compact signal, never a paragraph.
msg=first_text(x.get('message'),x.get('observation'),x.get('productSubject'),limit=14)
if msg: signals=[msg]
action=first_text(x.get('nextAction'),x.get('professionalAdvice'),x.get('nextProductAction'),x.get('action'),limit=18)
copy=f'
'+folded
def _status_label(kind,value):
v=safe(value)
maps={
'main':{'expressed':('已表达','ok'),'partial':('部分表达','mid'),'missing':('未覆盖','none')},
'detail':{'proven':('已证明','ok'),'partial':('部分证明','mid'),'missing':('未覆盖','none')},
'review':{'validated':('有验证','ok'),'partial':('部分验证','mid'),'missing':('暂无验证','none')},
}
if v in maps.get(kind,{}):return maps[kind][v]
low=v.lower()
if any(k in low for k in ('closed','已表达','已证明','有反馈','已解释','有验证')):return ('已覆盖','ok')
if any(k in low for k in ('partial','有限','部分','pending','待')):return ('部分','mid')
if not v:return ('—','none')
return (clean_display(v,18),'mid')
def _continuity_v92(exp,a):
block=exp.get('expressionContinuity') or {};checks=arr(block.get('coreClaims')) or arr((a.get('detailDecision') or {}).get('continuityChecks'))
rows=[]
for x in checks[:6]:
if not isinstance(x,dict):continue
claim=clean_display(x.get('claim'),30)
if not claim:continue
ml,mc=_status_label('main',x.get('mainStatus') or x.get('mainImageRole') or ('expressed' if x.get('mainImageEvidenceIds') else ''))
dl,dc=_status_label('detail',x.get('detailStatus') or x.get('detailProof') or ('proven' if x.get('detailEvidenceIds') else ''))
rl,rc=_status_label('review',x.get('reviewStatus') or x.get('reviewValidation'))
adv=first_text(x.get('professionalAdvice'),x.get('nextProductAction'),x.get('gap'),limit=45)
rows.append(f'
{H(claim)}
{H(ml)}
{H(dl)}
{H(rl)}
{H(adv or "继续保持页面口径一致")}
')
if not rows:return ''
return '
核心表达
主图
详情
评论
动作
'+''.join(rows)+'
'
def _detail_v92(exp,a,idx):
block=exp.get('detailCommerce') or {}; dd=a.get('detailDecision') or {}
opinion=first_text(block.get('professionalOpinion'),(dd.get('detailThesis') or {}).get('professionalOpinion'),limit=58) or '详情页优先展示能证明商品价值的代表图。'
groups=arr(block.get('contentGroups')) or arr(dd.get('contentGroups'))
entries=dict(_entries(idx,'detail',30)); cards=[]
for i,x in enumerate(groups[:6],1):
if not isinstance(x,dict): continue
eid=safe(x.get('representativeImageId') or x.get('imageEvidenceId')); e=entries.get(eid)
if not e and entries: e=list(entries.values())[min(i-1,len(entries)-1)]
if not e: continue
typ=first_text(x.get('type'),x.get('stage'),limit=10) or '详情信息'
signals=arr(x.get('visualSignals'))
if not signals:
msg=first_text(x.get('message'),x.get('businessMeaning'),limit=14)
if msg: signals=[msg]
action=first_text(x.get('nextAction'),x.get('professionalAdvice'),x.get('nextPageAction'),limit=24)
cards.append(''+_visual_figure(e,typ,'wide-ratio')+f'
')+note
def _sku_dimensions(raw):
product=raw.get('product') or {};title=safe(product.get('title'));attrs=arr(raw.get('attributes'));sku=arr(raw.get('sku'))
dims=[]
def add(name,vals,source=''):
vals=[clean_display(x,24) for x in vals if clean_display(x,24)]
if vals and name not in [d[0] for d in dims]:dims.append((name,vals[:5]))
if '床单' in title or '床笠' in title:add('款式',[x for x in ('床单','床笠') if x in title])
for x in attrs:
if not isinstance(x,dict):continue
n=safe(x.get('name'));v=safe(x.get('value'))
if any(k in n for k in ('尺寸','床尺寸','规格')) and v:add('尺寸',[v])
elif any(k in n for k in ('颜色','花色','图案','款式')) and v:add('花色/款式',[v])
elif any(k in n for k in ('件数','套件','组合')) and v:add('套件',[v])
if sku:
names=[]
for x in sku[:20]:
if isinstance(x,dict):
val=first_text(x.get('name'),x.get('title'),x.get('value'),x.get('properties'),limit=40)
if val:names.append(val)
if names:add('SKU规格',names)
return dims[:4]
def _sku_v92(exp,a,raw,idx):
block=exp.get('skuAnalysis') or a.get('skuVisualAnalysis') or {}
entries=_entries(idx,'sku',30); structured=arr(raw.get('sku'))
opinion=first_text(block.get('professionalOpinion'),limit=90)
dims=[]
for x in arr(block.get('selectionDimensions')):
if not isinstance(x,dict):continue
name=clean_display(x.get('name'),18);values=[clean_display(v,24) for v in arr(x.get('values')) if clean_display(v,24)]
if name:dims.append((name,values))
if not dims:dims=_sku_dimensions(raw)
order=[clean_display(x,18) for x in arr(block.get('recommendedOrder')) if clean_display(x,18)] or [x[0] for x in dims]
if not opinion:
opinion='当前SKU更适合按清晰选择层级组织:先解决规格是否适配,再处理组合和外观偏好。'
mosaic=''.join(_visual_figure(e,'SKU '+str(i+1),'square-ratio') for i,(eid,e) in enumerate(entries[:8]))
steps=''.join(f'
{i:02d}{H(name)}'+(f'{H(" / ".join(vals[:3]))}' if vals else '')+'
' for i,(name,vals) in enumerate(dims or [(x,[]) for x in order],1))
advice=first_text(block.get('recommendedAction'),limit=55) or ('建议按“'+' → '.join(order)+'”分层选择。' if order else '建议先建立规格层级,再展示外观选择。')
status='结构化SKU '+(str(len(structured))+' 个' if structured else '未完整采集')+f' · SKU图片 {len(entries)} 张'
return _opinion(opinion)+f'
{mosaic}
{steps}
{H(advice)}
{H(status)};SKU图片数量不等于SKU数量。
'
def _consumer_v92(exp,a,idx):
block=exp.get('customerExperience') or {}; cr=a.get('consumerResearch') or {}; ci=block.get('consumerInsights') or {}
opinion=first_text(block.get('professionalOpinion'),(cr.get('consumerThesis') or {}).get('statement'),limit=58) or '消费者反馈只保留可指导下一款的真实体验信号。'
signals=arr(ci.get('experienceSignals'))
if not signals:
mapping=[('稳定体验',cr.get('satisfactionDrivers')),('体验差异',cr.get('expectationGaps')),('购前关注',cr.get('prePurchaseConcerns'))]
for typ,values in mapping:
for x in arr(values)[:1]: signals.append({'type':typ,'insight':text_of(x)})
buyer=_entries(idx,'buyerShow',12)
gallery=''.join(_visual_figure(e,'买家秀 '+str(i+1),'square-ratio') for i,(eid,e) in enumerate(buyer[:4]))
rows=[]
for x in signals[:4]:
if not isinstance(x,dict): continue
typ=clean_display(x.get('type'),16) or '消费者信号'; ins=first_text(x.get('insight'),x.get('statement'),x.get('driver'),x.get('problem'),limit=38)
if ins: rows.append(f'{H(typ)}{H(ins)}')
gallery_html=gallery or '
未采集买家秀图片
'
rows_html=''.join(rows) or '
当前评论尚未形成可靠聚合洞察
'
return _opinion(opinion)+f'
{gallery_html}
{rows_html}
'
def _value_v92(exp,a):
block=exp.get('productValue') or {};items=arr(block.get('items'))
if not items:
pd=a.get('productDecision') or {};pillars=arr(pd.get('valuePillars'))
for x in pillars[:3]:
if isinstance(x,dict):items.append({'type':clean_display(x.get('name'),18) or '产品价值','value':first_text(x.get('customerValue'),x.get('businessMeaning'),limit=40),'explanation':first_text(x.get('mustKeep'),limit=55)})
if not items:
items=[{'type':'场景价值','value':'当前商品使用场景','explanation':'以标题、参数与图片证据为准'},{'type':'产品价值','value':'当前商品核心配置','explanation':'只保留页面已确认参数'},{'type':'服务价值','value':'页面已展示服务','explanation':'不推断未采集服务'},{'type':'可强化方向','value':'规格与证明表达','explanation':'优先保证图片、参数、SKU口径一致'}]
cards=[]
for x in items[:4]:
if not isinstance(x,dict):continue
typ=clean_display(x.get('type'),18);val=first_text(x.get('value'),x.get('customerValue'),limit=42);desc=first_text(x.get('explanation'),x.get('commercialValue'),x.get('mustKeep'),limit=58)
if val:cards.append(f'{H(typ)}{H(val)}'+(f'
{H(desc)}
' if desc else '')+'')
return _opinion(block.get('professionalOpinion'))+'
'+''.join(cards)+'
'
def _plan_action_list(values, limit=3):
out=[]
for x in arr(values)[:limit]:
t=first_text(x.get('action') if isinstance(x,dict) else x,x.get('item') if isinstance(x,dict) else '',limit=55)
if t:out.append(t)
return out
def _plans_v92(exp,a,idx):
block=exp.get('newProductPlans') or {}; source=arr(block.get('plans')) or arr((a.get('launchPlans') or {}).get('plans'))
opinion=first_text(block.get('professionalOpinion'),(a.get('launchPlans') or {}).get('professionalOpinion'),limit=58) or '优先从证据最强的优化方向开始,再看反馈升级与探索方向。'
cards=[]
for i,x in enumerate(source[:3],1):
if not isinstance(x,dict): continue
source_type=first_text(x.get('sourceType'),limit=10)
typ=first_text(x.get('type'),limit=14)
if not source_type:
source_type='证据驱动优化' if i==1 else ('反馈驱动升级' if i==2 else '探索性方向')
name=first_text(x.get('name'),limit=22) or ('新品方向 '+str(i))
why=first_text(x.get('whyThisPlan'),x.get('positioning'),limit=48)
product=first_text(x.get('productAction'),limit=38)
if not product:
acts=_plan_action_list(x.get('productChanges'),1) or _plan_action_list(x.get('mustChange'),1); product=acts[0] if acts else ''
page=first_text(x.get('pageAction'),limit=38)
if not page:
main=_plan_action_list(x.get('mainImagePlan'),1); detail=_plan_action_list(x.get('detailPagePlan'),1); page=';'.join((main+detail)[:2])
refs=arr(x.get('visualReferenceImageIds')); ref_e=None
for eid in refs:
if eid in idx and (idx[eid].get('meta') or {}).get('group') in ('main','detail','sku'): ref_e=idx[eid]; break
if not ref_e:
mains=_entries(idx,'main',5); ref_e=mains[min(i-1,len(mains)-1)][1] if mains else None
visual=_visual_figure(ref_e,'方案参考图','plan-ratio') if ref_e else '
当前商品视觉参考
'
body=f'
方案 {chr(64+i)} · {H(source_type)}
{H(name)}
'+(f'
产品
{H(product)}
' if product else '')+(f'
页面
{H(page)}
' if page else '')+'
'
cards.append(f'{visual}{body}')
if not cards: return _opinion(opinion)+'
当前证据未形成可追溯新品方案。
'
return _opinion(opinion)+'
'+''.join(cards)+'
'
def render(a, task_id):
a=ensure_experience_solution(a)
if not (a.get('meta') or {}).get('reportReady'): raise ValueError((a.get('meta') or {}).get('reportBlockedReason') or '本次没有可输出的证据结论')
raw=a.get('facts') or {}; product=raw.get('product') or {}; exp=a.get('experienceSolution') or {}; summary=exp.get('reportSummary') or {}
title=clean_display(summary.get('title') or product.get('title') or '商品分析报告',60)
payload=json.dumps(a,ensure_ascii=False,separators=(',',':')).replace('','<\\/').replace('\u2028','\\u2028').replace('\u2029','\\u2029')
return ''+H(title)+''