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'{H(alt)}' 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 '' 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 = '' 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)}

{H(value)}

' ) return ''.join(out) def platform_overview_html(overview): facts = overview_rows(overview.get('facts'), 'fact') outcomes = overview_rows(arr(overview.get('outcomes'))[:3], 'reason') actions = overview_rows(overview.get('actions'), 'check') groups = ''.join([ f'

交易与产品事实

{facts}
' if facts else '', f'

购后体验验证

{outcomes}
' if outcomes else '', f'

平台经营建议

{actions}
' if actions else '', ]) verdict = concise(overview.get('verdict'), 70, False) sample = concise(overview.get('sampleNote'), 64, False) return '
' + ( 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 '')+'
') 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'
{H(concise(x.get("label"), 16, False))}{H(concise(x.get("value"), 18, False))}
') 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'
  • {H(q.get("type"))}{H(concise(q.get("text"), 90, False))}
  • ') panels.append(f'
    第 {H(x.get("stageNo") or i + 1)} 阶段

    {H(x.get("label"))}

    {H(concise(x.get("headline"), 54))}

    {facts}
    ' + ('' if quotes else '') + '
    ') return '
    ' + ''.join(tabs) + '
    ' + ''.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'
    {H(x.get("priority"))} · {H(concise(x.get("label"), 24, False))}{H(action)}
    ') 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))}

    {H(concise(x.get("consumerValue"),62,False))}

    {H(concise(x.get("evidenceSource"),36,False))}
    ') return '
    '+''.join(rows)+'
    ' 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+'
    页面营销表达
    ' 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'

    {H(role)}

    主图 {n:02d}
    {_chips(signals)}'+(f'

    专业动作 · {H(action)}

    ' if action else '')+'
    ' cards.append('
    '+_visual_figure(e,'商品主图 '+str(n),'main-ratio')+copy+'
    ') continuity=_continuity_v92(exp,a) folded=f'
    查看主图 × 详情 × 评论承接关系{continuity}
    ' if continuity else '' return _opinion(opinion)+'
    '+''.join(cards)+'
    '+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'

    {H(typ)}

    {_chips(signals)}'+(f'

    专业动作 · {H(action)}

    ' if action else '')+'
    ') collected=len(entries) note=f'
    详情图 {collected} 张 · 默认展示 {len(cards)} 个信息任务代表图;其余图片保留为证据。
    ' return _opinion(opinion)+('
    '+''.join(cards)+'
    ' if cards else '
    详情图片已采集,当前未形成可靠分组。
    ')+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(''+H(title)+'
    '