diff --git a/.gitignore b/.gitignore index 37bce90..b42c93c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ venv/ # generated *.log +*.zip server/reports/ server/monitor_data/ diff --git a/CHANGELOG_V9.3.0.md b/CHANGELOG_V9.3.0.md new file mode 100644 index 0000000..168bc8b --- /dev/null +++ b/CHANGELOG_V9.3.0.md @@ -0,0 +1,12 @@ +# V9.3.0 — Unified Evidence Report + +- 新增七角色完整分析的核心判断和模块化折叠结构。 +- 证据从字符编号升级为来源类型、真实内容摘要、图片预览和次要追溯ID。 +- 最终报告统一过滤空字段、空卡片、空章节、缺失图片和占位文案。 +- 评论增加主题频次可视化、问题洞察、影响和产品动作。 +- 买家秀按评论合并,避免多图被误算为多人反馈。 +- 首焦改为无装饰商品主图牌组;所有图片完整显示且不裁切。 +- 详情卡统一图片与动作对齐,奇数最后一张使用横向通栏。 +- 新品方向增加参考色标签、底部对齐的立即生成按钮和任务弹窗。 +- JSON 报告与静态重建报告统一复用同一个渲染器。 +- 同步扩展、服务、分析引擎和内置采集扩展版本至 9.3.0。 diff --git a/PROMPT_ENGINEERING_V9.3.0.md b/PROMPT_ENGINEERING_V9.3.0.md new file mode 100644 index 0000000..ec0fe38 --- /dev/null +++ b/PROMPT_ENGINEERING_V9.3.0.md @@ -0,0 +1,35 @@ +# Prompt Engineering V9.3.0 + +## 目标 + +让七角色从同一证据账本生产可决策、可执行、可追溯的商品开品报告。模型负责提炼内容,渲染器负责信息层级,不向用户展示推理过程。 + +## 七角色协作 + +1. 商品策略、电商转化、消费者洞察、产品/供应链、视觉设计分别在职责范围内输出。 +2. 商业决策角色只综合前序证据,不创造新事实。 +3. 总编审核统一检查事实、证据、对象、图片分类、冲突、重复和字数。 +4. 每条结论、风险和动作必须引用 ACTUAL_EVIDENCE。 + +## 输出规则 + +- 统一使用“核心判断—关键发现—执行动作—风险/验证”。 +- 图片先提取可核验的材质、规格、权益、功能、构图和场景信息,再形成动作。 +- 评论先按程序统计计算频次,再由模型解释体验差异、影响和产品动作。 +- 新品方向区分证据驱动、反馈驱动和探索性方向。 +- 没有真实内容的可选字段直接省略。 +- 禁止输出空字符串、空数组、空对象或“暂无、未采集、待补充、无”等占位文案。 +- evidenceIds 只用于机器追溯,不作为用户可见说明。 +- 未采集经营数据时,不推断市场规模、转化率、利润或竞品表现。 + +## 文本长度 + +- 核心判断与专业意见不超过55字。 +- 老板结论不超过28字,补充说明不超过28字。 +- 主图和详情标签每项不超过12字,动作不超过22字。 +- 评论洞察不超过35字。 +- 新品方案产品/页面动作不超过35字。 + +## 最终可见性 + +最终页面只展示有效内容。真实数值 `0`、布尔状态、评论采集停止原因和样本边界属于有效事实,必须保留。 diff --git a/README.md b/README.md index 95191c2..732281c 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,54 @@ -# 三笙 · 商品开品分析报告引擎 V9.2.5 +# 三笙 · 商品开品分析工具 V9.3.0 -V9.2.5 是“大图大字决策报告”最终版:老板先看结论,专业用户再看图片证据。大图讲事实,大字讲判断,短句讲动作。 +V9.3.0 将采集、七角色分析、证据追溯和最终报告统一为一条输出链。报告面向不熟悉数据工具的商品、运营、设计、供应链和老板用户:先看结论,再按需展开专业分析与证据。 -## 最终报告结构 +## 使用流程 -1. 老板速览:为什么能卖 / 值得继承 / 下一款优先 -2. 下一款方向:证据驱动优化 / 反馈驱动升级 / 探索性方向 -3. 标题表达:已表达 / 建议强化 -4. 主图在卖什么:完整图片 + 图片信息标签 + 1条动作 -5. 详情页证明什么:代表图 + 图片信息标签 + 1条动作 -6. SKU怎么选:SKU图 + 选择维度与顺序 -7. 消费者实际感受到什么:买家秀 + 聚合体验信号 +1. 在淘宝或天猫商品页打开扩展。 +2. 分析前选择评论样本:50、100、500、全部或自定义。 +3. 开始采集;中途停止时保留当前进度,也可直接基于现有数据分析。 +4. 等待七角色分析与总编审核完成。 +5. 在报告中查看老板速览、下一款方向、页面转化、消费者洞察和完整角色分析。 -## V9.2.5 关键变化 +## V9.3.0 输出结构 -- Prompt 从“解释图片”改为“提炼图片上的商业信息”。 -- 主图/详情新增 `visualSignals`,优先输出图片可核验的卖点与参数标签。 -- 同一结论默认只展示一次,详细推理留在 evidenceDetail。 -- 主图 × 详情 × 评论承接矩阵默认折叠。 -- 商品价值不再单独重复一章,吸收到老板速览。 -- 新品方案改为横向决策卡,只保留为什么做 / 产品动作 / 页面动作,并标记来源等级。 -- 图片统一 `object-fit: contain`,封面、主图、详情、SKU、买家秀、方案参考图均优先完整展示,不裁切商品信息。 -- SKU图片数量继续与SKU结构数量分离。 -- 保留 V9.2.3 已修复的图片 HTML 标签、图片缓存与旧采集格式兼容能力。 +1. 老板速览:为什么能卖、值得继承、下一款优先。 +2. 下一款方向:证据驱动优化、反馈驱动升级、探索性方向,并可立即生成开品任务。 +3. 标题表达:当前表达、信息缺口和强化动作。 +4. 主图分析:完整原图、信息任务、真实视觉信号和下一步动作。 +5. 详情分析:代表图片、证明内容、页面承接和完整详情证据。 +6. SKU分析:适配维度、组合顺序和供应链动作。 +7. 消费者洞察:评论主题频次、体验差异、问题影响、产品动作和买家秀验证。 +8. 七角色完整分析:核心判断、关键发现、执行动作、风险与验证。 -详细规则见 `REPORT_OUTPUT_STANDARD_V9.2.5.md`、`PROMPT_ENGINEERING_V9.2.5.md` 与 `CHANGELOG_V9.2.5.md`。 +## 统一渲染规则 -## 集成约定(V9.2.5) +- 所有图片使用 `object-fit: contain`,完整展示,不裁切。 +- 图片不添加背景、模糊副本、边框、圆角、阴影、角标或序号。 +- 空字符串、空数组、空对象、缺失图片和“暂无/未采集”占位项不进入最终页面。 +- 某个卡片或章节无有效内容时,容器、标题和导航入口一起隐藏。 +- 数值 `0` 与“用户主动停止”等真实采集状态保留。 +- 证据优先显示来源类型和实际内容摘要,内部 evidenceId 仅作为次要追溯信息。 +- JSON 动态报告和静态重建报告共用 `server/report.js`,避免两套样式漂移。 -- `server/prompt_templates.py` 提供提示词模板注册表与商品变量说明。业务代码传入结构化变量(商品、参数、图片、评论、问答、程序统计、证据账本),不再自行拼接上下文。 -- 报告数据保存为 `/reports/.json`;页面使用独立的 `server/report.html` + `server/report.js` 通过 `data` 参数加载 JSON。可直接把这两个静态文件部署到其它服务端,UI 与后端数据接口解耦。 +## 七角色 + +- 商品策略:商品定位、核心资产、差异与下一款策略假设。 +- 电商转化:标题、主图、详情、信任证明与页面优化动作。 +- 消费者洞察:稳定体验、差异、顾虑、问题与真实场景。 +- 产品 / 供应链:材质、工艺、参数、SKU、风险与打样验证。 +- 视觉设计:风格、颜色、花型、构图、拍摄和生图方向。 +- 商业决策 / 老板:机会、优先级、保留项、升级项与执行顺序。 +- 总编审核:事实、证据、对象、图片分类、冲突、重复和表达检查。 + +## 本地运行 + +双击 `start-tmall-ai.command`,或在 `server` 目录运行: + +```bash +python3 server.py +``` + +默认服务地址为 `http://127.0.0.1:17962/`。扩展与后端版本必须一致。 + +完整规则见 `REPORT_OUTPUT_STANDARD_V9.3.0.md`、`PROMPT_ENGINEERING_V9.3.0.md` 和 `CHANGELOG_V9.3.0.md`。 diff --git a/REPORT_OUTPUT_STANDARD_V9.3.0.md b/REPORT_OUTPUT_STANDARD_V9.3.0.md new file mode 100644 index 0000000..7098034 --- /dev/null +++ b/REPORT_OUTPUT_STANDARD_V9.3.0.md @@ -0,0 +1,42 @@ +# V9.3.0 最终报告输出标准 + +## 阅读顺序 + +老板速览 → 下一款方向 → 标题 → 主图 → 详情 → SKU → 消费者洞察 → 七角色完整分析。 + +只有存在有效内容的章节才进入页面,章节编号按实际出现顺序生成。 + +## 图片规则 + +- 首焦使用商品主图牌组,图片完整展示。 +- 所有商品图、详情图和买家秀统一使用 `contain`。 +- 不使用裁切、模糊背景、装饰底图、边框、圆角、阴影、图片角标和可见序号。 +- 图片缺失时不渲染空白图片容器。 +- 详情代表图按同排统一高度排列;奇数最后一张使用通栏布局。 + +## 信息层级 + +- 每章先显示核心判断,再显示事实、影响和动作。 +- 下一款方案卡的操作按钮同排底部对齐。 +- 青绿到浅蓝渐变只用于标签与关键操作,不覆盖商品图片。 +- 评论总结必须包含主题频次可视化、问题洞察和对应动作。 + +## 七角色 + +- 每个角色先显示一条核心判断。 +- 专业内容按分析模块折叠。 +- 每项依次展示结论、商业意义、动作、风险/验证和置信度。 +- 评论原话、大批量基础数据和证据详情默认折叠。 + +## 证据 + +- 用户首先看到来源类型和真实内容摘要。 +- 图片证据显示完整缩略图。 +- evidenceId 缩小显示,仅用于核验与追溯。 +- 找不到实际证据内容的引用不进入最终页面。 + +## 空内容规则 + +- 不展示空字符串、空数组、空对象、缺失图片或占位文案。 +- 子项为空则隐藏子项;卡片为空则隐藏卡片;章节为空则隐藏章节和导航入口。 +- `0`、`false`、采集停止原因和样本边界属于有效事实,不得误删。 diff --git a/VERSION.txt b/VERSION.txt index f9458b8..b13d146 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -9.2.5 +9.3.0 diff --git a/codex-plugin-toolkit/sansong-tmall-product-report/README.md b/codex-plugin-toolkit/sansong-tmall-product-report/README.md index 09c3aed..435f7dd 100644 --- a/codex-plugin-toolkit/sansong-tmall-product-report/README.md +++ b/codex-plugin-toolkit/sansong-tmall-product-report/README.md @@ -1,4 +1,4 @@ -# 三笙电商商品分析报告插件 v9.2.5 +# 三笙电商商品分析报告插件 v9.3.0 用于把淘宝/天猫当前商品采集结果转成跨类目商品分析、新品设计和 AI 视觉报告。 diff --git a/codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/README.md b/codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/README.md index 6a937f5..e4a3716 100644 --- a/codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/README.md +++ b/codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/README.md @@ -1,3 +1,3 @@ -# 三笙电商商品详情采集扩展 v9.2.5 +# 三笙电商商品详情采集扩展 v9.3.0 跨类目采集商品标题、类目候选、主图、详情、SKU、动态参数、评论和问答。家纺仅为分析模板分支,不限制采集字段。 diff --git a/codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/manifest.json b/codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/manifest.json index 08f74fd..0cd8093 100644 --- a/codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/manifest.json +++ b/codex-plugin-toolkit/sansong-tmall-product-report/assets/capture-extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "三笙商品详情采集", - "version": "9.2.5", + "version": "9.3.0", "description": "跨类目解析商品标题、主图、详情、活动、SKU、动态参数、评论与问答。", "permissions": ["activeTab", "scripting", "tabs", "alarms", "cookies"], "host_permissions": [ diff --git a/collector-extension-standalone/README.md b/collector-extension-standalone/README.md index f598679..48c68c9 100644 --- a/collector-extension-standalone/README.md +++ b/collector-extension-standalone/README.md @@ -1,4 +1,4 @@ -# 三笙电商商品详情采集扩展 v9.2.5 +# 三笙电商商品详情采集扩展 v9.3.0 跨类目采集标题、类目候选、主图、详情图、SKU 图、评论图、动态参数、变体、促销、评论与问答。字段由页面结构和文本自动发现,不绑定家纺 BOM。 diff --git a/collector-extension-standalone/manifest.json b/collector-extension-standalone/manifest.json index 08f74fd..0cd8093 100644 --- a/collector-extension-standalone/manifest.json +++ b/collector-extension-standalone/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "三笙商品详情采集", - "version": "9.2.5", + "version": "9.3.0", "description": "跨类目解析商品标题、主图、详情、活动、SKU、动态参数、评论与问答。", "permissions": ["activeTab", "scripting", "tabs", "alarms", "cookies"], "host_permissions": [ diff --git a/extension/background.js b/extension/background.js index e298d7d..60c5dcb 100644 --- a/extension/background.js +++ b/extension/background.js @@ -56,11 +56,11 @@ chrome.runtime.onMessage.addListener((m,s,send)=>{ return true; } if(m?.type==='STOP_COLLECTION'){(async()=>{try{await chrome.scripting.executeScript({target:{tabId:m.tabId},world:'MAIN',func:()=>{window.__TMALL_AI_STOP_REVIEW__=true;}});send({ok:true})}catch(e){send({ok:false,error:String(e)})}})();return true;} - if(m?.type==='RUN_CURRENT'){ run(m.tabId,true,m.collectionMode||'standard').then(x=>send(x)).catch(e=>send({ok:false,error:String(e)})); return true; } + if(m?.type==='RUN_CURRENT'){ run(m.tabId,true,m.sampleMode||'500').then(x=>send(x)).catch(e=>send({ok:false,error:String(e)})); return true; } if(m?.type==='HEALTH'){ health().then(send); return true; } }); -async function run(tabId,manual,collectionMode='standard'){ +async function run(tabId,manual,sampleMode='500'){ const tab=await chrome.tabs.get(tabId); if(!isProduct(tab.url)) throw new Error('当前标签页不是天猫/淘宝商品详情页'); const h=await health(); @@ -80,10 +80,10 @@ async function run(tabId,manual,collectionMode='standard'){ if(pageState?.login){await setStatus(tabId,{state:'login_required',message:'淘宝要求登录。请先在当前浏览器登录淘宝,再刷新商品页重试。',progress:0});throw new Error('淘宝商品页未登录:请登录后刷新当前商品页再采集')} if(pageState?.verification){await setStatus(tabId,{state:'verification_required',message:'页面出现淘宝安全验证,请手动完成后刷新重试。',progress:0});throw new Error('页面出现淘宝安全验证,请手动完成验证后重试')} if(!isProduct(pageState?.url)){throw new Error('商品页已跳转,当前不是可采集的淘宝/天猫详情页')} - await setStatus(tabId,{state:'collecting',message:'正在采集当前商品:主图、SKU、详情、参数、问答与评论。不会采集推荐商品…',progress:10}); + await setStatus(tabId,{state:'collecting',sampleMode,message:`正在采集当前商品:主图、SKU、详情、参数、问答与评论(评论样本:${sampleMode==='all'?'全部':sampleMode+' 条'})。不会采集推荐商品…`,progress:10}); await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:()=>{window.__TMALL_AI_STOP_REVIEW__=false;window.__TMALL_AI_REVIEW_PROGRESS__={count:0,round:0,stagnant:0,mode:'启动',elapsedMs:0,preview:[]};}}); let collectorDone=false,collectorError=null;const startedAt=Date.now(); - const collectorPromise=chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:collector,args:[manual,collectionMode]}).then(v=>v).catch(e=>{collectorError=e;return null}).finally(()=>{collectorDone=true}); + const collectorPromise=chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:collector,args:[manual,sampleMode]}).then(v=>v).catch(e=>{collectorError=e;return null}).finally(()=>{collectorDone=true}); while(!collectorDone){try{const rr=await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:()=>window.__TMALL_AI_REVIEW_PROGRESS__||null});const live=rr?.[0]?.result;if(live)await setStatus(tabId,{state:'collecting_reviews',message:`评论采集中:${live.count||0} 条 · ${live.mode||'处理中'}${live.stagnant?` · ${live.stagnant}轮无新增`:''}`,progress:Math.min(67,18+(Number(live.round||0)*2)),reviewLive:{...live,elapsedMs:Date.now()-startedAt}})}catch(e){}await sleep(600)} const collectorResult=await collectorPromise; if(collectorError)throw new Error('页面采集脚本执行失败:'+String(collectorError?.message||collectorError)); @@ -110,7 +110,7 @@ async function run(tabId,manual,collectionMode='standard'){ const noReviewData=!raw.reviews?.length&&!raw.collection?.networkReviewResponses; const folded=raw.collection?.foldedDefaultReviewCount?`;平台另折叠 ${raw.collection.foldedDefaultReviewCount} 条默认好评,未提供逐条内容`:''; const msg=stopped?`已终止采集,并保存当前 ${raw.reviews?.length||0} 条评论。可随时继续。`:human?`评论已保存 ${raw.reviews?.length||0} 条,页面出现验证。请手动完成验证后继续。`:noReviewData?'页面未释放可读取的评论数据。商品信息已保存;请在当前页打开“评价/评论”后再次点击继续采集。':`评论已保存 ${raw.reviews?.length||0} 条,尚未确认完整${folded}。已停止低效空转,可继续采集。`; - await setStatus(tabId,{state:stopped?'reviews_stopped':'reviews_incomplete',message:msg,progress:68,itemId:raw.product.itemId,reviewStopReason:reason,reviewLive:raw.collection?.reviewLive||null,parameterStatus:raw.product?.parameterCollection||null}); + await setStatus(tabId,{state:stopped?'reviews_stopped':'reviews_incomplete',sampleMode, message:msg,progress:68,itemId:raw.product.itemId,reviewStopReason:reason,reviewLive:raw.collection?.reviewLive||null,parameterStatus:raw.product?.parameterCollection||null}); return {ok:false,needsReviewResume:true,message:msg,reviewStopReason:reason,rawStats:{title:raw.product?.title||'',reviews:raw.reviews?.length||0,questions:raw.questions?.length||0,sku:raw.sku?.length||0,attributes:raw.attributes?.length||0,promotions:raw.promotions?.length||0,images:raw.images?.all?.length||0}}; } await setStatus(tabId,{state:'sending',message:`评论完整性已确认,共 ${raw.reviews?.length||0} 条。正在生成新品开品闭环…`,progress:78,itemId:raw.product.itemId}); @@ -121,7 +121,7 @@ async function run(tabId,manual,collectionMode='standard'){ return {ok:true,taskId:j.taskId,rawStats:{title:raw.product?.title||'',reviews:raw.reviews?.length||0,questions:raw.questions?.length||0,sku:raw.sku?.length||0,attributes:raw.attributes?.length||0,promotions:raw.promotions?.length||0,images:raw.images?.all?.length||0}}; } -async function collector(manual=false,collectionMode='standard'){ +async function collector(manual=false,sampleMode='500'){ const delay=ms=>new Promise(r=>setTimeout(r,ms)); const text=(el)=>el?.innerText?.trim()||el?.textContent?.trim()||''; const clean=s=>(s||'').replace(/\s+/g,' ').trim(); @@ -144,7 +144,7 @@ async function collector(manual=false,collectionMode='standard'){ const bodyText=clean(bodyClone?.innerText||document.body?.innerText||''); const captured=[]; const reviews=[]; const questions=[]; - const reviewCap=collectionMode==='quick'?300:(collectionMode==='full'?3000:800); + const reviewCap=sampleMode==='all'?Infinity:Math.max(1,Math.min(10000,Number(sampleMode)||500)); const reviewNetMeta={totals:[],hasMoreFalse:false,lastPageFlags:[],pageHints:[],networkReviewObjects:0,networkResponses:0}; const reviewStartedAt=Date.now(); const processedCaptureKeys=new Set(); @@ -331,7 +331,7 @@ async function collector(manual=false,collectionMode='standard'){ // 每轮即时解析已捕获网络响应 const capNow=(window.__TMALL_AI_CAPTURE__||[]).slice(-120);for(const c of capNow){try{const ck=String(c.__safeKey||[c.method||'',c.url||'',String(c.text||'').slice(0,240)].join('|'));if(processedCaptureKeys.has(ck))continue;processedCaptureKeys.add(ck);if(/review|rate|comment|feed/i.test(c.url||''))reviewNetMeta.networkResponses++;let z=(c.text||'').replace(/^\s*[\w$.]+\(/,'').replace(/\)\s*;?\s*$/,'');try{walk(JSON.parse(z),0,c.url||'')}catch(_){const m=z.match(/\{[\s\S]*\}/);if(m)try{walk(JSON.parse(m[0]),0,c.url||'')}catch(__){}}}catch(e){}} collectVisibleReviews();collectVisibleQuestions();const after=dedupReviewCount();stagnant=after<=before?stagnant+1:0;publishReviewProgress(round+1,stagnant,next?'翻页/加载更多':'短滚动',{lastDelta:after-before}); - if(collectionMode!=='full' && after>=reviewCap){collectionComplete=true;explicitEnd=true;stopReason='sample_cap';publishReviewProgress(round+1,stagnant,collectionMode==='quick'?'已采够快速样本':'已采够标准样本');break} + if(Number.isFinite(reviewCap) && after>=reviewCap){collectionComplete=true;explicitEnd=true;stopReason='sample_cap';publishReviewProgress(round+1,stagnant,`已采够 ${reviewCap} 条评论样本`);break} const bodyNow=clean(document.body?.innerText||'');const noMoreText=/没有更多(?:评价|评论)|已显示全部(?:评价|评论)|到底了|暂无更多(?:评价|评论)/.test(bodyNow);const captcha=/验证码|滑块|安全验证|访问过于频繁|操作频繁/.test(bodyNow); if(captcha){stopReason='human_verification_required';publishReviewProgress(round+1,stagnant,'需要人工验证');break} if((disabledNext||noMoreText)&&stagnant>=1){explicitEnd=true;collectionComplete=true;stopReason='explicit_last_page';publishReviewProgress(round+1,stagnant,'页面明确结束');break} @@ -581,6 +581,6 @@ async function collector(manual=false,collectionMode='standard'){ sales:{currentPrice:priceValue,sold:soldValue,ranking:rankMatch?.[1]||''}, sku:skuTexts.map(name=>({name})),attributes:attrs,promotions:promoTexts.map(text=>({text})), images:{main,detail,sku:skuImages,buyerShow:buyer,all:allImgs,provenance,classification:{version:'strict-v1',classifiedCount:allImgs.length,unclassifiedCount:Math.max(0,allPageCandidates.length-allImgs.length),groupsAreDisjoint:true}}, - reviews:reviewRows,questions:[...deQ.values()],pageText:liveBodyText.slice(0,120000),collection:{reviewActual:deReview.size,reviewSampled,reviewSampleCap:reviewCap,reviewCollectionMode:collectionMode,questionActual:deQ.size,reviewDrawerOpened:reviewDrawerWasOpened,reviewCollectionComplete:collectionComplete,reviewCompleteness:reviewSampled?'sampled':(collectionComplete?'confirmed':'unconfirmed'),reviewStopReason:reviewSampled?'sample_cap':stopReason,reviewPagesVisited:pagesVisited,publicReviewCount:publicRaw,publicReviewCountNumeric:publicNumeric,foldedDefaultReviewCount:foldedDefaultMatch?parseCount(foldedDefaultMatch[1]):null,defaultPraiseBuyerCount:defaultPraiseMatch?parseCount(defaultPraiseMatch[1]):null,networkReviewTotal:networkTotal,networkReviewObjects:reviewNetMeta.networkReviewObjects,networkReviewResponses:reviewNetMeta.networkResponses,networkExplicitEnd:reviewNetMeta.hasMoreFalse||reviewNetMeta.lastPageFlags.length>0,explicitEnd,reviewLive:window.__TMALL_AI_REVIEW_PROGRESS__||null} + reviews:reviewRows,questions:[...deQ.values()],pageText:liveBodyText.slice(0,120000),collection:{reviewActual:deReview.size,reviewSampled,reviewSampleCap:Number.isFinite(reviewCap)?reviewCap:null,reviewCollectionMode:sampleMode,questionActual:deQ.size,reviewDrawerOpened:reviewDrawerWasOpened,reviewCollectionComplete:collectionComplete,reviewCompleteness:reviewSampled?'sampled':(collectionComplete?'confirmed':'unconfirmed'),reviewStopReason:reviewSampled?'sample_cap':stopReason,reviewPagesVisited:pagesVisited,publicReviewCount:publicRaw,publicReviewCountNumeric:publicNumeric,foldedDefaultReviewCount:foldedDefaultMatch?parseCount(foldedDefaultMatch[1]):null,defaultPraiseBuyerCount:defaultPraiseMatch?parseCount(defaultPraiseMatch[1]):null,networkReviewTotal:networkTotal,networkReviewObjects:reviewNetMeta.networkReviewObjects,networkReviewResponses:reviewNetMeta.networkResponses,networkExplicitEnd:reviewNetMeta.hasMoreFalse||reviewNetMeta.lastPageFlags.length>0,explicitEnd,reviewLive:window.__TMALL_AI_REVIEW_PROGRESS__||null} }; } diff --git a/extension/manifest.json b/extension/manifest.json index 23b7efc..8735b79 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, - "name": "三笙 · 电商商品分析闭环 V9.2.5", - "version": "9.2.5", + "name": "三笙 · 电商商品分析闭环 V9.3.0", + "version": "9.3.0", "description": "跨类目电商商品分析:动态参数与SKU采集、图文分源、评论证据、开品与AI视觉方案。", "permissions": [ "scripting", @@ -20,7 +20,7 @@ }, "action": { "default_popup": "popup.html", - "default_title": "三笙 · 电商商品分析闭环 V9.2.5" + "default_title": "三笙 · 电商商品分析闭环 V9.3.0" }, "content_scripts": [ { diff --git a/extension/popup.html b/extension/popup.html index 9e1142f..fae21d5 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -16,6 +16,13 @@ body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemF

三笙 · 电商商品分析引擎

采集当前商品后自动识别类目和分析维度,生成标题、主图、详情、SKU、评论、新品设计与 AI 视觉报告。
检查本地服务…
等待当前商品页
+
采集前先选择评论样本
只限制评论样本数量;商品信息、参数、图片、问答和完整分析内容都会保留。
+ + + + + +
采集开始后不能切换样本档位;如中途停止,可按原档位继续。
@@ -27,17 +34,8 @@ body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemF
缺失项:—
+
分析入口
已有采集数据时,可直接生成报告;不会清空当前采集进度。
不会采集推荐商品。评论会自动去重、分类和抽样;采集过程中可随时停止并继续。
-
-
分析方式
-
- - - -
- -
系统优先保留差评、带图、最新评论和不同主题评论。
-
diff --git a/extension/popup.js b/extension/popup.js index de96f37..3d5b4f5 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -1,5 +1,5 @@ const SERVER='http://127.0.0.1:17962'; -const EXPECTED_SERVER_VERSION='9.2.5'; +const EXPECTED_SERVER_VERSION='9.3.0'; let backendCompatible=false; const server=document.getElementById('server'),status=document.getElementById('status'),bar=document.getElementById('bar'),runBtn=document.getElementById('run'),stopBtn=document.getElementById('stop'),reviewLive=document.getElementById('reviewLive'),liveCount=document.getElementById('liveCount'),liveNet=document.getElementById('liveNet'),liveRound=document.getElementById('liveRound'),liveStagnant=document.getElementById('liveStagnant'),liveMode=document.getElementById('liveMode'),liveElapsed=document.getElementById('liveElapsed'),livePreview=document.getElementById('livePreview'); const paramLive=document.getElementById('paramLive'),paramState=document.getElementById('paramState'),paramCount=document.getElementById('paramCount'),paramTags=document.getElementById('paramTags'),paramMissing=document.getElementById('paramMissing'); @@ -31,10 +31,15 @@ async function refresh(){ } if(lp){liveCount.textContent=lp.count??0;liveNet.textContent=lp.networkReviewObjects??0;liveRound.textContent=lp.round??0;liveStagnant.textContent=lp.stagnant??0;liveMode.textContent=lp.mode||'—';liveElapsed.textContent=Math.round((lp.elapsedMs||0)/1000)+'s';livePreview.innerHTML=(lp.preview||[]).map((x,i)=>`
${i+1}. ${String(x).replace(/[<>&]/g,c=>({'<':'<','>':'>','&':'&'}[c]))}
`).join('')||'等待评论数据…'}}else{status.textContent='已识别当前商品,等待手动开始。';bar.style.width='0%';runBtn.textContent='开始深度采集';stopBtn.style.display='none';reviewLive.style.display='none'} } -let collectionMode='standard'; +let collectionMode='500'; +const sampleButtons=[...document.querySelectorAll('[data-sample]')]; +const customSampleWrap=document.getElementById('customSampleWrap'); +const customSample=document.getElementById('customSample'); +sampleButtons.forEach(btn=>btn.addEventListener('click',()=>{sampleButtons.forEach(x=>x.classList.remove('active'));btn.classList.add('active');collectionMode=btn.dataset.sample;customSampleWrap.style.display=collectionMode==='custom'?'block':'none';})); +;(async()=>{try{const t=await active();const p=(await chrome.storage.local.get('status_'+t.id))['status_'+t.id];if((p?.state==='reviews_incomplete'||p?.state==='reviews_stopped')&&p.sampleMode){collectionMode=p.sampleMode;sampleButtons.forEach(x=>x.classList.toggle('active',x.dataset.sample===collectionMode));customSampleWrap.style.display=collectionMode==='custom'?'block':'none'}}catch(e){}})(); runBtn.onclick=async()=>{ if(!backendCompatible){status.textContent='插件与本地后端版本不一致,请先双击本包“start-tmall-ai.command”';return} - const t=await active();status.textContent='开始采集当前商品…';try{const r=await chrome.runtime.sendMessage({type:'RUN_CURRENT',tabId:t.id,collectionMode});if(r?.needsReviewResume){status.textContent=r.message||'评论还没到末页,请继续采集';runBtn.textContent='继续采集评论'}else if(!r?.ok){status.textContent='采集失败:'+(r?.error||'未知错误')}}catch(e){status.textContent='采集失败:'+String(e)}setTimeout(refresh,500)}; + const t=await active();let sample=collectionMode;if(sample==='custom'){const n=Number(customSample?.value||0);if(!Number.isInteger(n)||n<1||n>10000){status.textContent='请输入 1-10000 的评论数量';return}sample=String(n)}status.textContent=`开始采集当前商品(评论样本 ${sample==='all'?'全部':sample+' 条'})…`;try{const r=await chrome.runtime.sendMessage({type:'RUN_CURRENT',tabId:t.id,sampleMode:sample});if(r?.needsReviewResume){status.textContent=r.message||'评论采集已暂停,可继续';runBtn.textContent='继续采集评论'}else if(!r?.ok){status.textContent='采集失败:'+(r?.error||'未知错误')}}catch(e){status.textContent='采集失败:'+String(e)}setTimeout(refresh,500)}; document.getElementById('open').onclick=()=>chrome.tabs.create({url:SERVER+'/'}); document.getElementById('importMonitor')?.addEventListener('click',()=>document.getElementById('monitorFile')?.click()); document.getElementById('monitorFile')?.addEventListener('change',async e=>{ @@ -88,12 +93,11 @@ let analysisMode='standard'; function setMode(mode){ analysisMode=mode; - collectionMode=mode; modeQuick?.classList.toggle('active',mode==='quick'); modeStandard?.classList.toggle('active',mode==='standard'); modeFull?.classList.toggle('active',mode==='full'); if(analyzeNow){ - analyzeNow.textContent=mode==='quick'?'先看当前结论':mode==='standard'?'采集并生成报告':'完整采集后自动分析'; + analyzeNow.textContent='基于当前数据开始分析'; analyzeNow.disabled=false; } } diff --git a/server/ai_client.py b/server/ai_client.py index 98d4d3e..ba3d064 100644 --- a/server/ai_client.py +++ b/server/ai_client.py @@ -2,7 +2,7 @@ import json, os, re, ssl, time, urllib.request, urllib.error from pathlib import Path ROOT = Path(__file__).resolve().parent -SERVER_VERSION = '9.2.5' +SERVER_VERSION = '9.3.0' def load_config(): cfg = {} @@ -64,7 +64,7 @@ def _ssl_context(c): return ctx def _headers(c): - h={'Content-Type':'application/json','Accept':'application/json','Authorization':'Bearer '+c['api_key'],'User-Agent':'TmallAIProductAnalysis/9.2.5'} + h={'Content-Type':'application/json','Accept':'application/json','Authorization':'Bearer '+c['api_key'],'User-Agent':'TmallAIProductAnalysis/9.3.0'} h.update(c.get('extra_headers',{})) return h @@ -145,4 +145,8 @@ def chat_json(system, user, images=None, max_tokens=5000): last=RuntimeError(f'模型接口调用失败: {e}') if attempt=1;第三类创新若不是直接证据结论必须标 sourceType=探索性方向。 - productExperience.parameterFacts>=2、productExperience.gates>=2、validationLoop.rows>=3,继续用于工程证据校验,但默认报告不展示。 - 禁止用户可见字段出现[数据事实][分析判断][设计建议][证据不足]。 +- 只返回有真实内容的可选字段;不得用空字符串、空数组、空对象或“暂无/未采集/待补充”填充版面。 +- 七角色输出必须能归入“核心判断—关键发现—执行动作—风险/验证”;证据编号仅作机器追溯,不作为面向用户的说明文字。 ''' -FINAL_REPORT_REPAIR_PROMPT='''任务G-R|修复 V9.2.5 最终报告 JSON。只修复结构缺失和证据引用,不扩写正文。 +FINAL_REPORT_REPAIR_PROMPT='''任务G-R|修复 V9.3.0 最终报告 JSON。只修复结构缺失和证据引用,不扩写正文。 必须保留短文本与图片信息优先规则。 强制最小结构:reportSummary.title/verdict非空;ownerOverview.cards>=3;productExperience.parameterFacts>=2;productExperience.gates>=2;newProductPlans.plans>=1;validationLoop.rows>=3。 如果 customerExperience 有评论证据,优先保留 experienceSignals;无评论时允许为空。 所有 evidenceIds 只能来自 ACTUAL_EVIDENCE。不要输出 Markdown、解释或包装层。 +可选字段没有真实内容时直接省略,不得填入“暂无、未采集、待补充”或空容器。 ''' TOPICS={ @@ -1005,10 +1031,15 @@ def ensure_experience_solution(analysis): validation=core_solution_diagnostics(exp) analysis['meta']['reportValidation']=validation if not validation['ok']: - analysis['meta']['reportReady']=False - analysis['meta']['reportBlockedReason']='模型未生成完整且可追溯的电商商品分析结论,本次不输出兜底报告。未通过字段:'+', '.join(validation['missing']) + # 审核是质量标记,不再阻断报告输出;缺失项在报告中显式提示,供人工复核。 + analysis['meta']['reportReady']=True + analysis['meta']['reportAuditPassed']=False + analysis['meta']['reportQuality']='needs_review' + analysis['meta']['reportNotice']='审核未通过,报告仍已生成。请优先补齐:'+', '.join(validation['missing']) return analysis analysis['meta']['reportReady']=True + analysis['meta']['reportAuditPassed']=True + analysis['meta']['reportQuality']='passed' analysis['meta'].pop('reportBlockedReason',None) customer=exp.setdefault('customerExperience',{}) customer['stats']=[{'label':'有效评论','value':str(analysis['baseline'].get('reviewCount',0))},{'label':'购买问答','value':str(analysis['baseline'].get('questionCount',0))},{'label':'有效参数','value':str(analysis['baseline'].get('attributeCount',0))}] @@ -1175,7 +1206,7 @@ def analyze(raw, progress=lambda *a:None): model_configured=configured(); use_ai=model_configured if model_available is None else bool(model_available and model_configured) raw=normalize(raw); base=baseline(raw); ledger=evidence_ledger(raw); valid={x['id'] for x in ledger} route=detect_category(raw) - out={'meta':{'modelConfigured':model_configured,'modelUsed':use_ai,'engineVersion':ENGINE_VERSION,'actualOnly':True,'dynamicReport':True,'industryTemplate':route['key'],'categoryLabel':route['label'],'categoryConfidence':route['confidence'],'analysisDimensions':route['dimensions']},'facts':raw,'baseline':base,'evidenceLedger':ledger,'modelErrors':{}} + out={'meta':{'modelConfigured':model_configured,'modelUsed':use_ai,'engineVersion':ENGINE_VERSION,'roleContractVersion':ROLE_CONTRACT_VERSION,'roleContract':ROLE_CONTRACT,'actualOnly':True,'dynamicReport':True,'industryTemplate':route['key'],'categoryLabel':route['label'],'categoryConfidence':route['confidence'],'analysisDimensions':route['dimensions']},'facts':raw,'baseline':base,'evidenceLedger':ledger,'modelErrors':{}} progress('evidence','processing',28) limitations=[] @@ -1326,6 +1357,15 @@ visualSignals必须来自图片可见文字、商品、场景或已确认参数 'detailDecision':out.get('detailDecision',{}), 'consumerResearch':out.get('consumerResearch',{}), 'questionResearch':out.get('questionResearch',{}), 'commercialDecision':out.get('commercialDecision',{}), 'launchPlans':out.get('launchPlans',{}), + 'roleOutputs':{ + 'productStrategy':out.get('productDecision',{}), + 'conversion':{'title':out.get('merchandisingDecision',{}),'visual':out.get('visualDecision',{}),'detail':out.get('detailDecision',{})}, + 'consumerInsight':{'reviews':out.get('consumerResearch',{}),'questions':out.get('questionResearch',{})}, + 'productSupply':{'product':out.get('productDecision',{}),'baseline':base}, + 'visualDesign':out.get('fashionDecision',{}), + 'executiveDecision':out.get('commercialDecision',{}), + 'editorReview':{'scope':ROLE_CONTRACT['editorReview']['outputs']} + }, 'evidence':compact_evidence(ledger,{'monitoring','product','sales','sku','attribute','promotion','review','question','image'},650) } prompt=FINAL_REPORT_PROMPT @@ -1335,7 +1375,39 @@ visualSignals必须来自图片可见文字、商品、场景或已确认参数 out['experienceSolution']=require_refs(z,valid) else: out['experienceSolution']={} + # 最终编排模型不可用或被异常图片拒绝时,仍需用真实页面、参数、评论和问答生成完整本地分析,不能只输出最小结构。 + final_draft=out.get('experienceSolution') or {} + if not isinstance(final_draft,dict) or not _txt((final_draft.get('reportSummary') or {}).get('title')).strip(): + out['experienceSolution']=build_fallback_experience(raw,base) + out.setdefault('modelErrors',{})['experienceSolutionFallback']='最终编排未返回可用内容,已按真实采集证据生成本地完整分析。' + + # 将分角色产物显式挂载到结果,供报告样式、审计和后续导出复用;不复制或丢弃既有分析字段。 + out['roleOutputs']={ + 'productStrategy':out.get('productDecision',{}), + 'conversion':{'title':out.get('merchandisingDecision',{}),'visual':out.get('visualDecision',{}),'detail':out.get('detailDecision',{})}, + 'consumerInsight':{'reviews':out.get('consumerResearch',{}),'questions':out.get('questionResearch',{})}, + 'productSupply':{'product':out.get('productDecision',{}),'sku':out.get('skuDecision',{}),'baseline':base}, + 'visualDesign':out.get('fashionDecision',{}), + 'executiveDecision':out.get('commercialDecision',{}), + 'editorReview':{'status':'pending','scope':ROLE_CONTRACT['editorReview']['outputs']} + } result=ensure_experience_solution(out) + final_exp=result.get('experienceSolution') or {} + role_outputs=result.setdefault('roleOutputs',{}) + # 以总编后的最终结构补齐角色视图,确保角色层与对外报告完全一致。 + role_outputs['conversion']={ + 'title':final_exp.get('titleAnalysis') or role_outputs.get('conversion',{}).get('title',{}), + 'visual':final_exp.get('visualCommerce') or role_outputs.get('conversion',{}).get('visual',{}), + 'detail':final_exp.get('detailCommerce') or role_outputs.get('conversion',{}).get('detail',{}) + } + role_outputs['consumerInsight']=final_exp.get('customerExperience') or role_outputs.get('consumerInsight',{}) + role_outputs['productSupply']={ + 'sku':final_exp.get('skuAnalysis') or {}, + 'product':final_exp.get('productExperience') or role_outputs.get('productSupply',{}).get('product',{}), + 'baseline':base + } + role_outputs['visualDesign']=role_outputs.get('visualDesign') or {} + role_outputs['executiveDecision']=final_exp.get('ownerOverview') or role_outputs.get('executiveDecision',{}) if use_ai and not result['meta'].get('reportReady'): first_validation=result['meta'].get('reportValidation') or core_solution_diagnostics(result.get('experienceSolution')) progress('editor','processing repair',99) @@ -1358,5 +1430,12 @@ visualSignals必须来自图片可见文字、商品、场景或已确认参数 out['meta']['evidenceStructuralRepairApplied']=True out['meta']['preStructuralRepairValidation']=before result=ensure_experience_solution(out) + result.setdefault('roleOutputs',{})['editorReview']={ + 'status':'passed' if result['meta'].get('reportReady') else 'needs_repair', + 'validation':result['meta'].get('reportValidation') or {}, + 'scope':ROLE_CONTRACT['editorReview']['outputs'] + } + result.setdefault('meta',{})['roleContractVersion']=ROLE_CONTRACT_VERSION + result.setdefault('meta',{})['roleContract']=ROLE_CONTRACT progress('editor','done' if result['meta'].get('reportReady') else 'failed',99) return result diff --git a/server/audit_report.py b/server/audit_report.py new file mode 100644 index 0000000..9f11f91 --- /dev/null +++ b/server/audit_report.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Audit a generated report JSON before delivery.""" +import json +import sys +from pathlib import Path + +EXPECTED_ROLES = ( + 'productStrategy', 'conversion', 'consumerInsight', 'productSupply', + 'visualDesign', 'executiveDecision', 'editorReview', +) +VISIBLE_SOURCES = { + 'experienceSolution.reportSummary': ('reportSummary',), + 'experienceSolution.ownerOverview': ('ownerOverview',), + 'experienceSolution.newProductPlans': ('newProductPlans',), + 'experienceSolution.titleAnalysis': ('titleAnalysis',), + 'experienceSolution.visualCommerce': ('visualCommerce',), + 'experienceSolution.detailCommerce': ('detailCommerce',), + 'experienceSolution.skuAnalysis': ('skuAnalysis',), + 'experienceSolution.customerExperience': ('customerExperience',), + 'reportPlan': ('supportAnalysis',), + 'reviewBatchAnalysis': ('supportAnalysis',), + 'experienceSolution.productExperience': ('supportAnalysis',), + 'experienceSolution.validationLoop': ('supportAnalysis',), + 'experienceSolution.expressionContinuity': ('supportAnalysis',), + 'dataQuality': ('supportAnalysis',), + 'consumerResearch': ('reviews', 'supportAnalysis'), + 'questionResearch': ('supportAnalysis',), + 'commercialDecision': ('supportAnalysis',), + 'competitionDecision': ('supportAnalysis',), + 'merchandisingDecision': ('supportAnalysis',), + 'productDecision': ('supportAnalysis',), + 'visualDecision': ('supportAnalysis',), + 'roleOutputs': ('roleOutputs',), +} + +PLACEHOLDERS = {'暂无', '无', '未采集', '未评级', '待补充', '尚未结构化'} + +def has_content(value): + if value is None: + return False + if isinstance(value, str): + return bool(value.strip()) and value.strip() not in PLACEHOLDERS + if isinstance(value, (int, float, bool)): + return True + if isinstance(value, list): + return any(has_content(item) for item in value) + if isinstance(value, dict): + return any(has_content(item) for item in value.values()) + return False + +def get_path(data, path): + value = data + for part in path.split('.'): + if not isinstance(value, dict): + return None + value = value.get(part) + return value + +def evidence_refs(value): + refs=[] + if isinstance(value, dict): + ids=value.get('evidenceIds') + if isinstance(ids, list): + refs.extend(str(item) for item in ids if item) + for child in value.values(): + refs.extend(evidence_refs(child)) + elif isinstance(value, list): + for child in value: + refs.extend(evidence_refs(child)) + return refs + +def audit(data): + ledger=data.get('evidenceLedger') or [] + known={str(item.get('id')) for item in ledger if isinstance(item,dict) and item.get('id')} + refs=evidence_refs(data.get('experienceSolution') or {})+evidence_refs(data.get('roleOutputs') or {}) + unresolved=sorted(set(refs)-known) + roles=data.get('roleOutputs') or {} + missing_roles=[role for role in EXPECTED_ROLES if not has_content(roles.get(role))] + present_sources=[] + mapped_sources=[] + for path,targets in VISIBLE_SOURCES.items(): + if has_content(get_path(data,path)): + present_sources.append(path) + mapped_sources.append({'source':path,'renderers':list(targets)}) + images=[item for item in ledger if isinstance(item,dict) and item.get('type')=='image'] + invalid_images=[item.get('id') for item in images if not has_content((item.get('meta') or {}).get('localUrl') or item.get('value'))] + exp=data.get('experienceSolution') or {} + required={ + 'reportSummary':has_content(exp.get('reportSummary')), + 'ownerOverview':has_content(exp.get('ownerOverview')), + 'newProductPlans':has_content(exp.get('newProductPlans')), + } + failures=[] + failures += [f'缺少核心报告模块:{key}' for key,ok in required.items() if not ok] + failures += [f'缺少角色输出:{role}' for role in missing_roles] + failures += [f'证据引用无法解析:{eid}' for eid in unresolved] + failures += [f'图片证据无可用地址:{eid}' for eid in invalid_images] + return { + 'ok':not failures, + 'engineVersion':(data.get('meta') or {}).get('engineVersion'), + 'presentMappedSources':mapped_sources, + 'sourceCoverage':len(mapped_sources), + 'roles':{'present':len(EXPECTED_ROLES)-len(missing_roles),'expected':len(EXPECTED_ROLES),'missing':missing_roles}, + 'evidence':{'ledger':len(ledger),'references':len(refs),'unresolved':unresolved}, + 'images':{'total':len(images),'invalid':invalid_images}, + 'failures':failures, + } + +def main(): + if len(sys.argv)!=2: + raise SystemExit('用法:python3 audit_report.py ') + source=Path(sys.argv[1]) + result=audit(json.loads(source.read_text('utf-8'))) + print(json.dumps(result,ensure_ascii=False,indent=2)) + raise SystemExit(0 if result['ok'] else 1) + +if __name__=='__main__': + main() diff --git a/server/prompt_templates.py b/server/prompt_templates.py index 3eb130f..ab302f2 100644 --- a/server/prompt_templates.py +++ b/server/prompt_templates.py @@ -15,6 +15,8 @@ PRODUCT_VARIABLES = { "questions": "问大家/购买问答:问题、回答及 QA evidenceId", "programStats": "程序统计:评论数量、图片数量、参数数量等可计算指标", "evidence": "统一证据账本;模型只能引用其中的 evidenceId", + "roleOutputs": "七角色中间结论:商品策略、电商转化、消费者洞察、产品/供应链、视觉设计、老板决策与总编审核;只能交叉校验和编排,不得当作无证据事实", + "renderingPolicy": "最终可见性规则:只展示真实非空内容;证据以来源类型和内容摘要呈现,evidenceId仅用于追溯", } @dataclass(frozen=True) diff --git a/server/renderer.py b/server/renderer.py index 2b493ba..c635678 100644 --- a/server/renderer.py +++ b/server/renderer.py @@ -1,4 +1,5 @@ import html +import json import re from analysis import ensure_experience_solution @@ -669,18 +670,7 @@ def _plans_v92(exp,a,idx): 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 {}; sales=raw.get('sales') or {}; base=a.get('baseline') or {}; exp=a.get('experienceSolution') or {}; idx=evidence_index(a) - summary=exp.get('reportSummary') 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) - verdict=first_text(summary.get('verdict'),limit=58) or '基于当前商品页面、图片、参数与评论证据形成开品建议。' - cover_id=safe(summary.get('coverImageId')); cover_e=idx.get(cover_id) if cover_id else None - if not cover_e: - mains=_entries(idx,'main',1); cover_e=mains[0][1] if mains else None - price='¥'+safe(sales.get('currentPrice')) if safe(sales.get('currentPrice')) else '价格未采集' - sold=safe(sales.get('sold') or sales.get('cumulativeSales')) or '销量未采集' - meta_line=f'{price} · 公开销量 {sold} · 评论 {base.get("reviewCount",0)} · SKU图 {len(_entries(idx,"sku",999))} · 参数 {base.get("attributeCount",0)}' - nav='' - cover=_visual_figure(cover_e,'商品封面图','cover-ratio') if cover_e else '
主图未采集
' - hero=f'
PRODUCT DECISION REPORT · BIG VISUAL · V9.2.5

{H(title)}

{H(meta_line)}

{H(clean_display(verdict,45))}
{cover}
' - doc=''+H(title)+''+nav+hero+_section('01','老板 30 秒看懂','只看结果,不看分析过程',_owner_overview(exp,raw,base,sales),'decision')+_section('02','下一款先做什么','先给决策,再看证据',_plans_v92(exp,a,idx),'plan')+_section('03','标题只解决一个问题:说清楚','保留表达与动作',_title_v92(raw,exp,a),'title')+_section('04','主图到底在卖什么','图片本身就是成交语言',_main_v92(exp,a,idx),'main')+_section('05','详情页证明了什么','大图展示证明内容',_detail_v92(exp,a,idx),'detail')+_section('06','先解决适配,再解决审美','SKU看选择路径',_sku_v92(exp,a,raw,idx),'sku')+_section('07','消费者真正感受到什么','买家秀是证据',_consumer_v92(exp,a,idx),'review')+f'
Task {H(task_id)} · 三笙商品开品分析报告引擎 V9.2.5 · 未采集经营指标不作推断
' - return doc + payload=json.dumps(a,ensure_ascii=False,separators=(',',':')).replace(''+H(title)+'
' diff --git a/server/report.css b/server/report.css index 146101f..f1a940c 100644 --- a/server/report.css +++ b/server/report.css @@ -200,7 +200,7 @@ body{background:#fff;color:var(--v92-text);font-size:14px;line-height:1.55} @media(max-width:560px){.wrap{width:min(100% - 24px,1180px)}.v92-hero h1{font-size:27px}.v92-opinion{grid-template-columns:1fr;gap:5px}.v92-main-grid{grid-template-columns:1fr 1fr}.v92-sku-mosaic,.v92-buyer-grid{grid-template-columns:repeat(2,1fr)}} /* ========================================================= - V9.2.5 OWNER + PROFESSIONAL USER FINAL REPORT + V9.3.0 OWNER + PROFESSIONAL USER FINAL REPORT 老板先决策;专业用户再看图片证据 图片完整展示,不裁切 ========================================================= */ @@ -251,7 +251,7 @@ body{background:#fff;color:var(--v92-text);font-size:14px;line-height:1.55} .v924-plan-body dt{font-size:8px;color:var(--v92-muted);font-weight:760} .v924-plan-body dd{margin:0;font-size:10.5px;color:var(--v92-text);line-height:1.45} -/* V9.2.5 removes repeated explanatory subcopy from default reading layer. */ +/* V9.3.0 removes repeated explanatory subcopy from default reading layer. */ .v92-evidence-note{font-size:9px} .v92-insights article p{display:none} .v92-summary3 p:empty{display:none} @@ -266,7 +266,7 @@ body{background:#fff;color:var(--v92-text);font-size:14px;line-height:1.55} /* ========================================================= - V9.2.5 BIG VISUAL / BIG TYPE FINAL REPORT + V9.3.0 BIG VISUAL / BIG TYPE FINAL REPORT Big images tell facts; big type tells judgement; short lines tell actions. ========================================================= */ :root{--v925-ink:#141b24;--v925-text:#44505d;--v925-muted:#8a95a3;--v925-line:#e8edf2;--v925-soft:#f6f8fa;--v925-blue:#315fba;--v925-blue-soft:#eef4ff} @@ -292,6 +292,307 @@ body{background:#fff;color:var(--v92-text);font-size:14px;line-height:1.55} .v92-footer{font-size:12px;padding:34px 0 54px} @media(max-width:900px){.wrap{width:min(100% - 28px,1240px)}.v92-hero-grid,.v92-sku-layout,.v92-review-layout,.v925-detail-block,.v925-detail-block:nth-child(even){grid-template-columns:1fr}.v92-hero h1{font-size:38px}.v925-hero-claim{font-size:21px}.v92-summary3,.v924-title2,.v925-plan-grid{grid-template-columns:1fr}.v92-main-grid{grid-template-columns:1fr}.v925-main-card:nth-child(5){grid-column:auto;max-width:none}.v925-detail-block:nth-child(even) .visual{order:0}.v92-head h2{font-size:30px}.v92-head p{display:none}.v92-sku-mosaic{grid-template-columns:1fr 1fr}.v92-buyer-grid{grid-template-columns:1fr 1fr}} @media(max-width:560px){.v92-hero h1{font-size:32px}.v925-hero-claim{font-size:19px}.v925-main-card .main-ratio,.v925-main-card:nth-child(5) .main-ratio{height:440px}.v925-detail-block .wide-ratio{min-height:400px}.v92-sku-mosaic .square-ratio,.v92-buyer-grid .square-ratio{height:220px}} + +/* Evidence report view: detail proof chain, review visualizations, and full role outputs. */ +.report-nav{position:sticky;top:0;z-index:20;background:rgba(255,255,255,.96);border-bottom:1px solid var(--v92-line);backdrop-filter:blur(10px)} +.report-nav .wrap{min-height:58px;display:flex;align-items:center;justify-content:space-between;gap:20px}.report-nav a{color:var(--v92-muted);font-size:13px;text-decoration:none;margin-left:20px}.report-nav a:hover{color:var(--v92-blue)}.report-nav .brand{color:var(--v92-ink);font-weight:800;margin-left:0} +.report-hero{padding:62px 0 52px;border-bottom:1px solid var(--v92-line)}.report-kicker{color:var(--v92-blue);font-size:11px;font-weight:800;letter-spacing:.08em}.report-hero h1{margin:12px 0 14px;color:var(--v925-ink);font-size:46px;line-height:1.15}.report-hero p{max-width:760px;margin:0;color:var(--v92-text);font-size:20px;line-height:1.6}.hero-meta{display:flex;gap:24px;flex-wrap:wrap;margin-top:24px;color:var(--v92-muted);font-size:13px} +.report-heading{display:flex;gap:14px;align-items:flex-start;margin-bottom:30px}.report-heading>span{color:var(--v92-blue);font-size:12px;font-weight:800;padding-top:7px}.report-heading h2{margin:0;color:var(--v925-ink);font-size:34px;line-height:1.2}.report-heading p{margin:7px 0 0;color:var(--v92-muted);font-size:13px}.report-section{padding:64px 0;border-bottom:1px solid var(--v92-line)}.lead-opinion{max-width:920px;margin:0 0 26px;color:var(--v925-ink);font-size:19px;line-height:1.6;font-weight:700} +.decision-grid,.plan-grid,.title-compare,.sku-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.decision-grid article,.title-compare article,.sku-grid article{padding:22px;border-top:2px solid var(--v925-ink);background:var(--v925-soft)}.decision-grid span,.title-compare span,.sku-grid span,.image-copy>span,.detail-proof-copy>span,.sample-strip span,.signal-card>span,.buyer-section header span,.topic-panel header span,.signal-panel header span{display:block;color:var(--v92-muted);font-size:12px;font-weight:750}.decision-grid h3,.title-compare b{display:block;margin:10px 0 0;color:var(--v925-ink);font-size:21px;line-height:1.4}.decision-grid p{margin:8px 0 0;color:var(--v92-text);line-height:1.55}.decision-grid ul{margin:12px 0 0;padding-left:18px;color:var(--v92-text);font-size:13px}.plan-grid{grid-template-columns:repeat(3,1fr)}.plan-grid article{padding:22px;border:1px solid var(--v92-line);background:#fff}.plan-grid article>span{color:var(--v92-blue);font-size:12px;font-weight:800}.plan-grid h3{margin:9px 0 10px;color:var(--v925-ink);font-size:22px;line-height:1.35}.plan-grid p{min-height:48px;margin:0;color:var(--v92-text);line-height:1.55}.plan-grid dl{margin:18px 0 0}.plan-grid dl div{display:grid;grid-template-columns:40px 1fr;gap:10px;padding:10px 0;border-top:1px solid var(--v92-line)}.plan-grid dt{color:var(--v92-muted);font-size:12px;font-weight:800}.plan-grid dd{margin:0;color:var(--v925-ink);font-size:13px;line-height:1.5} +.current-title{padding:18px 20px;background:var(--v925-soft);color:var(--v925-ink);font-size:17px;line-height:1.55}.current-title span{display:block;margin-bottom:5px;color:var(--v92-muted);font-size:12px}.title-compare{margin-top:18px;grid-template-columns:1fr 1fr}.evidence-drawer{margin-top:22px;border-top:1px solid var(--v92-line)}.evidence-drawer summary{cursor:pointer;padding:16px 0;color:var(--v92-blue);font-size:13px;font-weight:800}.evidence-drawer ol{margin:0 0 18px;padding-left:22px;color:var(--v92-text);line-height:1.8} +.main-analysis-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:34px 22px}.main-analysis-grid>article{min-width:0}.report-figure{margin:0;overflow:hidden;border-radius:10px;background:var(--v925-soft)}.report-figure img{display:block;width:100%;height:100%;min-height:220px;object-fit:contain}.main-analysis-grid .report-figure{height:460px}.report-figure figcaption{display:flex;justify-content:space-between;gap:12px;padding:9px 12px;color:var(--v92-text);font-size:12px}.report-figure figcaption small{color:var(--v92-muted)}.image-copy{padding:14px 2px}.image-copy h3{margin:5px 0 8px;color:var(--v925-ink);font-size:23px}.image-copy b{display:block;color:var(--v925-ink);font-size:16px;line-height:1.5}.image-copy p{margin:8px 0;color:var(--v92-text);line-height:1.5}.image-copy em{display:block;margin-top:10px;color:var(--v92-blue);font-style:normal;font-size:13px;font-weight:800} +.detail-sequence{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 30px}.detail-sequence span{padding:8px 11px;border-radius:999px;background:var(--v925-blue-soft);color:var(--v92-blue);font-size:12px;font-weight:800}.detail-proof-list{display:grid;gap:48px}.detail-proof-row{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(260px,.85fr);gap:34px;align-items:center}.detail-proof-media{position:relative}.detail-proof-media .report-figure{height:430px}.image-source-badge{position:absolute;left:12px;top:12px;padding:5px 8px;border-radius:4px;background:rgba(20,27,36,.78);color:#fff;font-size:11px}.detail-proof-copy h3{margin:7px 0 12px;color:var(--v925-ink);font-size:29px}.detail-proof-copy>b{display:block;color:var(--v925-ink);font-size:18px;line-height:1.55}.detail-proof-copy>p{margin:10px 0;color:var(--v92-text);line-height:1.6}.next-action{margin-top:18px;padding:14px 16px;background:var(--v925-blue-soft);border-left:3px solid var(--v92-blue);color:var(--v92-blue);font-size:14px;line-height:1.5;font-weight:700}.next-action small{display:block;margin-bottom:4px;font-size:11px;color:var(--v92-muted)}.drawer-note{margin:0 0 14px;color:var(--v92-muted);font-size:12px}.evidence-gallery{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.evidence-gallery .report-figure{height:180px}.evidence-gallery figcaption{font-size:10px} +.sku-grid{grid-template-columns:1fr 1fr}.sku-grid article h3{margin:8px 0 0;color:var(--v925-ink);font-size:18px;line-height:1.5}.action-banner{display:flex;gap:18px;align-items:baseline;margin-top:20px;padding:17px 20px;background:var(--v925-blue-soft);border-left:3px solid var(--v92-blue)}.action-banner span{color:var(--v92-muted);font-size:12px}.action-banner b{color:var(--v92-blue);font-size:15px} +.sample-strip{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:22px 0 30px}.sample-strip article{padding:17px;border:1px solid var(--v92-line);background:var(--v925-soft)}.sample-strip article.limited{border-color:#d7b47c;background:#fffaf1}.sample-strip b{display:block;margin-top:8px;color:var(--v925-ink);font-size:20px;line-height:1.35}.sample-strip .limited b{color:#9a6a2b;font-size:14px}.review-visual-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:26px}.topic-panel,.signal-panel{border-top:2px solid var(--v925-ink);padding-top:16px}.topic-panel header,.signal-panel header,.buyer-section>header{display:flex;justify-content:space-between;align-items:flex-end;gap:12px}.topic-panel h3,.signal-panel h3,.buyer-section h3{margin:5px 0 0;color:var(--v925-ink);font-size:20px}.topic-panel header small{color:var(--v92-muted);font-size:11px}.topic-bars{margin-top:18px}.topic-row{display:grid;grid-template-columns:106px minmax(80px,1fr) 45px 52px;gap:10px;align-items:center;margin:13px 0}.topic-row>span{color:var(--v92-text);font-size:12px}.topic-track{height:10px;overflow:hidden;background:#edf1f4;border-radius:99px}.topic-track i{display:block;height:100%;background:var(--v92-blue);border-radius:inherit}.topic-row b{color:var(--v925-ink);font-size:12px;text-align:right}.topic-row em{color:var(--v92-muted);font-size:11px;font-style:normal;text-align:right}.signal-panel{padding-left:22px;border-left:1px solid var(--v92-line);border-top:0}.signal-card{padding:15px 0;border-bottom:1px solid var(--v92-line)}.signal-card h4{margin:5px 0 6px;color:var(--v925-ink);font-size:17px;line-height:1.45}.signal-card small{color:var(--v92-muted);font-size:11px}.buyer-section{margin-top:44px;padding-top:24px;border-top:1px solid var(--v92-line)}.buyer-story{margin-top:18px;padding:16px;border:1px solid var(--v92-line)}.buyer-story-head{display:flex;justify-content:space-between;gap:10px;color:var(--v92-blue);font-size:12px;font-weight:800}.buyer-story-head small{color:var(--v92-muted);font-weight:400}.buyer-photo-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px}.buyer-photo-grid .report-figure{height:180px}.buyer-story blockquote{margin:14px 0 0;padding:12px 14px;background:var(--v925-soft);color:var(--v925-ink);font-size:15px;line-height:1.55}.buyer-story>p{margin:12px 0 0;color:var(--v92-muted);font-size:12px} +.role-list{display:grid;gap:12px}.role-block{border:1px solid var(--v92-line);background:#fff}.role-block summary{display:grid;grid-template-columns:40px 1fr auto;gap:14px;align-items:center;cursor:pointer;padding:17px 20px;list-style:none}.role-block summary::-webkit-details-marker{display:none}.role-block summary>span{color:var(--v92-blue);font-size:12px;font-weight:800}.role-block summary h3{margin:0;color:var(--v925-ink);font-size:17px}.role-block summary p{margin:4px 0 0;color:var(--v92-muted);font-size:12px}.role-block summary>b{color:var(--v92-blue);font-size:11px}.role-content{padding:0 20px 22px;border-top:1px solid var(--v92-line)}.structured-object{display:grid;gap:12px}.structured-field{padding-top:12px;border-top:1px solid var(--v92-line)}.structured-field>span{display:block;margin-bottom:4px;color:var(--v92-muted);font-size:11px;font-weight:800}.structured-field p{margin:0;color:var(--v92-text);line-height:1.55}.structured-field ul{margin:6px 0 0;padding-left:20px;color:var(--v92-text);line-height:1.6}.structured-list{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.structured-list>article{padding:13px;background:var(--v925-soft)}.structured-list .structured-field{border-top:0;padding-top:0}.evidence-field code{display:inline-block;margin:2px 4px 2px 0;padding:2px 5px;background:var(--v925-blue-soft);color:var(--v92-blue);font-size:10px}.compact-field{display:inline-block;margin-right:20px}.compact-field b{color:var(--v92-blue)}.muted,.empty-state{color:var(--v92-muted);font-size:13px}.empty-state{padding:18px;background:var(--v925-soft)}.report-image-missing{display:grid;place-items:center;height:100%;min-height:220px;color:var(--v92-muted);background:var(--v925-soft);font-size:12px}.report-footer{padding:28px 0 46px;color:var(--v92-muted);font-size:12px}.report-footer b{color:var(--v925-ink)}.report-error{max-width:720px;margin:100px auto;padding:30px}.report-error h1{color:var(--v925-ink)} +@media(max-width:900px){.report-nav .wrap{padding:0 20px}.report-nav a{margin-left:12px}.report-hero h1{font-size:36px}.report-hero p{font-size:17px}.report-section{padding:46px 0}.report-heading h2{font-size:28px}.decision-grid,.plan-grid,.main-analysis-grid,.review-visual-grid{grid-template-columns:1fr 1fr}.detail-proof-row{grid-template-columns:1fr}.detail-proof-copy{padding:0 4px}.signal-panel{padding-left:0;border-left:0;border-top:1px solid var(--v92-line);padding-top:20px}.sample-strip{grid-template-columns:1fr 1fr}} +@media(max-width:560px){.report-nav .wrap{min-height:52px}.report-nav .brand{font-size:12px}.report-nav>div>div{overflow:auto;white-space:nowrap}.report-nav a{font-size:11px;margin-left:9px}.report-hero{padding:42px 0 34px}.report-hero h1{font-size:30px}.report-hero p{font-size:16px}.hero-meta{gap:10px;font-size:11px}.report-heading{margin-bottom:22px}.report-heading h2{font-size:25px}.lead-opinion{font-size:17px}.decision-grid,.plan-grid,.title-compare,.main-analysis-grid,.sample-strip,.review-visual-grid,.sku-grid{grid-template-columns:1fr}.main-analysis-grid .report-figure,.detail-proof-media .report-figure{height:340px}.evidence-gallery{grid-template-columns:repeat(2,1fr)}.evidence-gallery .report-figure,.buyer-photo-grid .report-figure{height:150px}.buyer-photo-grid{grid-template-columns:repeat(2,1fr)}.topic-row{grid-template-columns:94px minmax(60px,1fr) 38px 46px;gap:6px}.role-block summary{grid-template-columns:28px 1fr}.role-block summary>b{display:none}.structured-list{grid-template-columns:1fr}} + +/* Compact detail proof cards: keep every conclusion bound to its image without a long page. */ +.detail-proof-list{grid-template-columns:repeat(2,minmax(0,1fr));gap:18px} +.detail-proof-row{display:block;min-width:0;overflow:hidden;border:1px solid var(--v92-line);border-radius:12px;background:#fff} +.detail-proof-media .report-figure{height:260px;border-radius:0} +.detail-proof-copy{padding:17px 18px 18px} +.detail-proof-copy h3{margin:5px 0 8px;font-size:23px} +.detail-proof-copy>b{font-size:15px;line-height:1.5} +.detail-proof-copy>p{margin:7px 0 0;font-size:13px;line-height:1.5} +.next-action{margin-top:12px;padding:10px 12px;font-size:13px;line-height:1.45} +.next-action small{display:inline;margin:0 8px 0 0;font-size:10px} +@media(max-width:900px){.detail-proof-list{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.detail-proof-media .report-figure{height:220px}.detail-proof-copy{padding:14px 15px 15px}.detail-proof-copy h3{font-size:20px}.detail-proof-copy>b{font-size:14px}.detail-proof-copy>p{font-size:12px}.next-action{padding:9px 10px;font-size:12px}} +@media(max-width:560px){.detail-proof-list{grid-template-columns:1fr}.detail-proof-media .report-figure{height:250px}.detail-proof-copy h3{font-size:21px}} + +/* Compact main-image analysis: portrait assets and their conclusions share one card. */ +.main-analysis-grid{gap:18px} +.main-analysis-grid>article{display:grid;grid-template-columns:minmax(190px,.78fr) minmax(0,1.22fr);overflow:hidden;border:1px solid var(--v92-line);border-radius:12px;background:#fff} +.main-analysis-grid .report-figure{display:grid;grid-template-rows:minmax(0,1fr) auto;height:310px;border-radius:0} +.main-analysis-grid .report-figure img{min-height:0} +.main-analysis-grid .image-copy{align-self:center;padding:18px} +.main-analysis-grid .image-copy h3{font-size:21px} +.main-analysis-grid .image-copy b{font-size:14px} +.main-analysis-grid .image-copy p{font-size:12px} +@media(max-width:900px){.main-analysis-grid{grid-template-columns:1fr}.main-analysis-grid>article{grid-template-columns:190px minmax(0,1fr)}.main-analysis-grid .report-figure{height:270px}.main-analysis-grid .image-copy{padding:15px}} +@media(max-width:560px){.main-analysis-grid>article{grid-template-columns:140px minmax(0,1fr)}.main-analysis-grid .report-figure{height:245px}.main-analysis-grid .image-copy{padding:12px}.main-analysis-grid .image-copy h3{font-size:18px}.main-analysis-grid .image-copy b{font-size:13px}.main-analysis-grid .image-copy p{display:none}} + +/* Review issue insights: frequency becomes an actionable product diagnosis. */ +.problem-section{margin-top:42px;padding-top:24px;border-top:1px solid var(--v92-line)} +.problem-section>header{margin-bottom:16px}.problem-section>header span{color:var(--v92-muted);font-size:12px;font-weight:800}.problem-section h3{margin:5px 0 0;color:var(--v925-ink);font-size:20px}.problem-section>header p{margin:5px 0 0;color:var(--v92-muted);font-size:12px} +.problem-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.problem-card{padding:15px 16px;border:1px solid var(--v92-line);border-top:3px solid #c38a43;background:#fff}.problem-card.gap{border-top-color:#b87953}.problem-card.question{border-top-color:var(--v92-blue)} +.problem-card>span{color:var(--v92-muted);font-size:11px;font-weight:800}.problem-card h4{margin:6px 0 9px;color:var(--v925-ink);font-size:16px;line-height:1.45}.problem-card p{margin:5px 0;color:var(--v92-text);font-size:12px;line-height:1.5}.problem-card p b{color:var(--v92-muted);font-size:11px}.problem-card small{display:block;margin-top:10px;color:var(--v92-muted);font-size:10px} +@media(max-width:900px){.problem-grid{grid-template-columns:1fr 1fr}} +@media(max-width:560px){.problem-grid{grid-template-columns:1fr}} + +/* Deterministic detail sequence: one semantic axis, one visual rule. */ +.sequence-rule{display:flex;align-items:baseline;gap:12px;margin-bottom:12px}.sequence-rule>span{color:var(--v92-blue);font-size:11px;font-weight:800}.sequence-rule>b{color:var(--v925-ink);font-size:13px}.sequence-rule>small{margin-left:auto;color:var(--v92-muted);font-size:10px} +.detail-sequence{display:grid;grid-template-columns:repeat(auto-fit,minmax(128px,1fr));gap:0;margin:0 0 24px;border:1px solid var(--v92-line);border-radius:10px;overflow:hidden;background:#fff} +.detail-sequence span{display:grid;grid-template-columns:28px 1fr;gap:1px 7px;align-items:center;min-width:0;padding:11px 12px;border-radius:0;border-right:1px solid var(--v92-line);background:#fff;color:var(--v925-ink)} +.detail-sequence span:last-child{border-right:0}.detail-sequence span i{grid-row:1/3;color:var(--v92-blue);font-size:11px;font-style:normal;font-weight:800}.detail-sequence span b{font-size:12px;line-height:1.3}.detail-sequence span small{overflow:hidden;color:var(--v92-muted);font-size:10px;line-height:1.3;text-overflow:ellipsis;white-space:nowrap} +@media(max-width:900px){.sequence-rule{align-items:flex-start;flex-wrap:wrap}.sequence-rule>small{width:100%;margin-left:0}.detail-sequence{grid-template-columns:repeat(3,1fr)}.detail-sequence span:nth-child(3n){border-right:0}.detail-sequence span:nth-child(n+4){border-top:1px solid var(--v92-line)}} +@media(max-width:560px){.detail-sequence{grid-template-columns:1fr 1fr}.detail-sequence span:nth-child(3n){border-right:1px solid var(--v92-line)}.detail-sequence span:nth-child(2n){border-right:0}.detail-sequence span:nth-child(n+3){border-top:1px solid var(--v92-line)}} + +/* Report cover: the declared coverImageId is always a product main image. */ +.hero-layout{display:grid;grid-template-columns:minmax(0,1.08fr) minmax(320px,.72fr);gap:48px;align-items:center}.hero-copy{min-width:0}.hero-product{display:grid;grid-template-rows:minmax(0,1fr) auto;height:390px;margin:0;overflow:hidden;border-radius:14px;background:var(--v925-soft)}.hero-product img{display:block;width:100%;height:100%;min-height:0;object-fit:contain}.hero-product figcaption{display:flex;justify-content:space-between;gap:12px;padding:9px 12px;background:#fff;color:var(--v92-muted);font-size:11px}.hero-product figcaption small{font-size:10px}.hero-product-empty{place-items:center;color:var(--v92-muted);font-size:12px} +@media(max-width:900px){.hero-layout{grid-template-columns:minmax(0,1fr) minmax(250px,.7fr);gap:24px}.hero-product{height:320px}.report-hero h1{font-size:32px}} +@media(max-width:700px){.hero-layout{grid-template-columns:1fr}.hero-product{height:340px}.report-hero{padding-bottom:28px}} +@media(max-width:560px){.hero-product{height:300px}} + +/* Product imagery fills every allocated visual frame. */ +.report-app .report-figure img,.report-app .hero-product img{width:100%;height:100%;object-fit:cover!important;object-position:center top} + +/* Full-screen main-image carousel with a seamless top navigation. */ +.report-top{position:relative;background:#111}.report-top .report-nav{position:absolute;inset:0 0 auto;z-index:30;background:transparent;border:0;backdrop-filter:none}.report-top .report-nav .wrap{border-bottom:1px solid rgba(255,255,255,.28)}.report-top .report-nav a,.report-top .report-nav .brand{color:#fff;text-shadow:0 1px 12px rgba(0,0,0,.35)}.report-top .report-nav a:hover{color:#dce9ff} +.report-hero.hero-carousel{position:relative;isolation:isolate;height:100svh;min-height:650px;padding:0;overflow:hidden;border:0;background:#111}.hero-slides,.hero-slide,.hero-shade{position:absolute;inset:0}.hero-slide{margin:0;opacity:0;transition:opacity .65s ease}.hero-slide.active{opacity:1}.hero-slide img{display:block;width:100%;height:100%;object-fit:cover!important;object-position:center center}.hero-slide figcaption{position:absolute;right:28px;top:76px;z-index:2;color:rgba(255,255,255,.78);font-size:10px}.hero-slide-empty{display:grid;place-items:center;color:#fff}.hero-shade{z-index:1;background:linear-gradient(180deg,rgba(7,15,25,.34) 0%,rgba(7,15,25,.03) 35%,rgba(7,15,25,.78) 100%);pointer-events:none} +.hero-overlay{position:relative;z-index:3;height:100%;display:flex;align-items:flex-end;justify-content:space-between;gap:32px;padding-bottom:54px}.hero-carousel .hero-copy{max-width:780px}.hero-carousel .report-kicker{color:#dce9ff}.report-hero.hero-carousel h1{margin:10px 0 12px;color:#fff;font-size:52px;text-shadow:0 2px 22px rgba(0,0,0,.28)}.report-hero.hero-carousel p{color:rgba(255,255,255,.92);font-size:21px;text-shadow:0 1px 16px rgba(0,0,0,.32)}.hero-carousel .hero-meta{color:rgba(255,255,255,.78)} +.hero-carousel-ui{display:flex;align-items:center;gap:12px;flex:0 0 auto;padding-bottom:4px}.hero-carousel-ui>button{width:38px;height:38px;padding:0;border:1px solid rgba(255,255,255,.5);border-radius:50%;background:rgba(0,0,0,.15);color:#fff;cursor:pointer;font-size:18px}.hero-carousel-ui>button:hover{background:rgba(255,255,255,.18)}.hero-dots{display:flex;gap:6px}.hero-dot{width:7px!important;height:7px!important;border:0!important;border-radius:50%;background:rgba(255,255,255,.45)!important}.hero-dot.active{width:22px!important;border-radius:10px;background:#fff!important}.hero-carousel-count{min-width:48px;color:#fff;font-size:11px;text-align:center} +@media(max-width:900px){.report-hero.hero-carousel h1{font-size:40px}.hero-overlay{padding-bottom:38px}.hero-carousel-ui{position:absolute;right:28px;bottom:38px}.hero-carousel .hero-copy{padding-right:170px}} +@media(max-width:700px){.report-top .report-nav .wrap{padding:0 20px}.report-hero.hero-carousel{height:100svh;min-height:620px}.hero-overlay{display:flex;padding:0 22px 28px}.hero-carousel .hero-copy{padding-right:0;padding-bottom:62px}.report-hero.hero-carousel h1{font-size:32px}.report-hero.hero-carousel p{font-size:16px}.hero-carousel-ui{left:22px;right:auto;bottom:28px}.hero-slide figcaption{right:18px;top:66px}} +@media(prefers-reduced-motion:reduce){.hero-slide{transition:none}} + +/* Main-image display correction: full bleed atmosphere plus an uncropped product image. */ +.hero-slide .hero-slide-backdrop{position:absolute;inset:-24px;width:calc(100% + 48px);height:calc(100% + 48px);object-fit:cover!important;filter:blur(22px) saturate(.82);transform:scale(1.04);opacity:.78} +.hero-slide .hero-slide-product{position:absolute;inset:0;width:100%;height:100%;object-fit:contain!important;object-position:center center;filter:none} +.report-app .report-figure,.report-app .hero-product,.main-analysis-grid>article,.detail-proof-row{border-radius:0!important} + +/* Fanned-card hero gallery. */ +.hero-slide .hero-slide-product{opacity:.2;filter:saturate(.8)} +.hero-deck{position:absolute;right:7%;top:50%;width:390px;height:500px;transform:translateY(-52%);z-index:5;pointer-events:none} +.hero-deck-card{position:absolute;left:50%;bottom:0;width:235px;height:380px;padding:0;overflow:hidden;border:1px solid rgba(255,255,255,.72);border-radius:0;background:#fff;box-shadow:0 24px 55px rgba(0,0,0,.32);transform-origin:50% 115%;transform:translateX(calc(-50% + var(--deck-x))) rotate(var(--deck-r));transition:transform .5s ease,filter .35s ease,opacity .35s ease;z-index:calc(var(--deck-index) + 1);cursor:pointer;pointer-events:auto} +.hero-deck-card img{display:block;width:100%;height:100%;object-fit:cover;object-position:center top}.hero-deck-card span{position:absolute;left:10px;bottom:9px;padding:3px 6px;background:rgba(10,20,32,.74);color:#fff;font-size:10px;font-weight:800}.hero-deck-card.active{z-index:20;transform:translateX(calc(-50% + var(--deck-x))) translateY(-24px) rotate(0);box-shadow:0 30px 70px rgba(0,0,0,.45)}.hero-deck-card:not(.active){filter:saturate(.78) brightness(.9)} +.hero-carousel .hero-copy{position:relative;z-index:8;max-width:570px}.hero-carousel-ui{position:absolute;right:7%;bottom:38px;z-index:9} + +/* Semantic color hierarchy shared by every report module. */ +.report-section{--module-accent:var(--v92-blue);--module-soft:#f1f6ff} +#plan{--module-accent:#5968b0;--module-soft:#f3f3fb}#title,#main{--module-accent:#276bb5;--module-soft:#eef6ff}#detail,#sku{--module-accent:#087b78;--module-soft:#edf8f7}#review{--module-accent:#b37027;--module-soft:#fff7ec}#roles{--module-accent:#5d6875;--module-soft:#f4f6f8} +.report-section .report-heading>span{color:var(--module-accent)}.report-section .report-heading h2{position:relative}.report-section .report-heading h2:after{content:'';display:block;width:34px;height:3px;margin-top:9px;background:var(--module-accent)} +.report-section .lead-opinion{max-width:none;padding:16px 18px;border-left:4px solid var(--module-accent);background:var(--module-soft);font-size:17px}.report-section .decision-grid article,.report-section .title-compare article,.report-section .sku-grid article{border-top-color:var(--module-accent)} +.report-section .plan-grid article>span,.report-section .image-copy>span,.report-section .detail-proof-copy>span,.report-section .detail-sequence span i,.report-section .sequence-rule>span{color:var(--module-accent)} +.report-section .image-copy em,.report-section .next-action,.report-section .action-banner b{color:#276bc7}.report-section .next-action,.report-section .action-banner{border-left-color:#276bc7;background:#eef5ff} +.evidence-field code{background:#eaf7f5;color:#087b78}.sample-strip .complete{border-color:#78b7a9;background:#eff9f6}.sample-strip .limited,.problem-card{border-color:#dfbd8c}.problem-card{border-top-color:#b37027} + +@media(max-width:1050px){.hero-deck{right:2%;width:330px}.hero-deck-card{width:205px;height:335px}.hero-carousel .hero-copy{max-width:500px}.hero-carousel-ui{right:3%}} +@media(max-width:800px){.hero-deck{right:0;top:38%;width:290px;height:330px}.hero-deck-card{width:165px;height:265px}.hero-deck-card.active{transform:translateX(calc(-50% + var(--deck-x))) translateY(-15px) rotate(0)}.hero-carousel .hero-copy{max-width:none;padding:0 0 78px}.hero-overlay{align-items:flex-end}.hero-carousel-ui{left:22px;right:auto;bottom:24px}} +@media(max-width:560px){.hero-deck{top:36%;left:50%;right:auto;width:250px;height:285px;transform:translate(-50%,-50%)}.hero-deck-card{width:145px;height:230px}.hero-deck-card.active{transform:translateX(calc(-50% + var(--deck-x))) translateY(-12px) rotate(0)}.hero-carousel .hero-copy{padding-bottom:72px}.report-hero.hero-carousel h1{font-size:27px}.report-section .lead-opinion{padding:13px 14px;font-size:15px}} +@media(prefers-reduced-motion:reduce){.hero-deck-card{transition:none}} + +/* Complete-image rule: a full uncropped image over a filled atmospheric backdrop. */ +.report-figure{position:relative}.report-image-stage{position:relative;display:block;width:100%;height:100%;min-height:0;overflow:hidden;background:#eef1f2}.report-app .report-figure img.report-image-backdrop{position:absolute;inset:-16px;width:calc(100% + 32px);height:calc(100% + 32px);min-height:0;object-fit:cover!important;object-position:center;filter:blur(16px) saturate(.75);transform:scale(1.05);opacity:.48}.report-app .report-figure img.report-image-product{position:relative;z-index:1;display:block;width:100%;height:100%;min-height:0;object-fit:contain!important;object-position:center} +.report-figure figcaption{position:relative;z-index:2;background:#fff}.main-analysis-grid .report-figure,.detail-proof-media .report-figure,.evidence-gallery .report-figure,.buyer-photo-grid .report-figure{display:grid;grid-template-rows:minmax(0,1fr) auto}.main-analysis-grid .report-image-stage,.detail-proof-media .report-image-stage,.evidence-gallery .report-image-stage,.buyer-photo-grid .report-image-stage{min-height:0} + +/* Primary header action. */ +.report-nav-actions{display:flex;align-items:center;gap:0}.report-top .report-nav .quick-product-btn{display:inline-flex;align-items:center;gap:12px;margin-left:22px;padding:9px 14px;background:#276bc7;color:#fff;border:1px solid #4d89db;text-shadow:none;font-weight:800}.report-top .report-nav .quick-product-btn:hover{background:#1f59a8;color:#fff}.quick-product-btn span{font-size:15px;line-height:1} +@media(max-width:700px){.report-top .report-nav .quick-product-btn{margin-left:10px;padding:7px 10px}.report-top .report-nav .quick-product-btn span{display:none}} +@media(max-width:560px){.report-nav-actions>a:not(.quick-product-btn){display:none}.report-top .report-nav .quick-product-btn{margin-left:0;font-size:11px}} + +/* Clean complete-image mode: one source image, no blurred duplicate, no crop. */ +.hero-slide .hero-slide-backdrop,.report-image-backdrop{display:none!important}.hero-slide .hero-slide-product{opacity:1;filter:none;object-fit:contain!important;object-position:center}.hero-deck-card img{box-sizing:border-box;padding:5px;object-fit:contain!important;object-position:center;background:#f3f4f2}.report-image-stage{background:#f3f4f2}.report-app .report-figure img.report-image-product{object-fit:contain!important;object-position:center} +.report-top,.report-hero.hero-carousel{background:#171b20}.hero-shade{background:linear-gradient(180deg,rgba(7,15,25,.12) 0%,rgba(7,15,25,0) 48%,rgba(7,15,25,.82) 100%)} +.report-section{padding:52px 0}.report-heading{margin-bottom:24px}.report-heading p{margin-top:5px}.report-section .lead-opinion{margin-bottom:20px;padding:13px 15px;font-size:16px}.decision-grid,.plan-grid{gap:14px}.detail-sequence{margin-bottom:18px}.detail-proof-list{gap:14px}.buyer-section,.problem-section{margin-top:32px;padding-top:20px}.role-list{gap:8px} +@media(max-width:700px){.report-section{padding:40px 0}.report-heading{margin-bottom:18px}.report-section .lead-opinion{margin-bottom:16px}} + +/* Quick-product plan area and actionable generation controls. */ +#plan{background:linear-gradient(180deg,#f2f5ff 0%,#f7f9ff 100%)}#plan .plan-grid article{display:flex;flex-direction:column;background:rgba(255,255,255,.9);border:0;border-top:4px solid #416fc0;box-shadow:0 10px 30px rgba(44,65,105,.08)}#plan .plan-card.plan-upgrade{border-top-color:#16857f}#plan .plan-card.plan-explore{border-top-color:#7662ba}#plan .plan-card>span{color:#416fc0}#plan .plan-upgrade>span{color:#16857f}#plan .plan-explore>span{color:#7662ba}#plan .plan-card dl{flex:1} +.generate-plan-btn{display:flex;align-items:center;justify-content:space-between;width:100%;margin-top:18px;padding:13px 15px;border:0;background:#316dcc;color:#fff;font-size:13px;font-weight:800;cursor:pointer}.generate-plan-btn:hover{background:#255aa9}.plan-upgrade .generate-plan-btn{background:#16857f}.plan-upgrade .generate-plan-btn:hover{background:#0d6c67}.plan-explore .generate-plan-btn{background:#6d5ab1}.plan-explore .generate-plan-btn:hover{background:#574694}.generate-plan-btn span{font-size:17px} +.quick-plan-dialog{width:min(620px,calc(100% - 32px));padding:28px;border:0;border-top:5px solid #316dcc;border-radius:0;box-shadow:0 30px 90px rgba(12,24,42,.28);color:var(--v92-text)}.quick-plan-dialog::backdrop{background:rgba(10,18,30,.58);backdrop-filter:blur(4px)}.quick-plan-dialog>span{color:#316dcc;font-size:10px;font-weight:800;letter-spacing:.08em}.quick-plan-dialog h2{margin:7px 38px 8px 0;color:var(--v925-ink);font-size:28px}.quick-plan-dialog>p{margin:0 0 18px;color:var(--v92-muted);line-height:1.55}.quick-plan-dialog dl{margin:0}.quick-plan-dialog dl div{display:grid;grid-template-columns:76px 1fr;gap:12px;padding:13px 0;border-top:1px solid var(--v92-line)}.quick-plan-dialog dt{color:var(--v92-muted);font-size:12px;font-weight:800}.quick-plan-dialog dd{margin:0;color:var(--v925-ink);line-height:1.55}.dialog-close{position:absolute;right:18px;top:16px;width:32px;height:32px;border:0;background:#eef2f6;color:var(--v925-ink);cursor:pointer;font-size:20px}.copy-plan-btn{width:100%;margin-top:18px;padding:13px;border:0;background:#316dcc;color:#fff;font-weight:800;cursor:pointer} +@media(max-width:700px){#plan .plan-grid{grid-template-columns:1fr}.quick-plan-dialog{padding:22px}.quick-plan-dialog h2{font-size:23px}.quick-plan-dialog dl div{grid-template-columns:1fr;gap:4px}} + +/* Clean white hero: no background image or overlay, larger complete-image card deck. */ +.report-top,.report-hero.hero-carousel{background:#fff}.hero-slides,.hero-shade{display:none}.report-top .report-nav .wrap{border-bottom:1px solid #e6ebf0}.report-top .report-nav a{color:#687381;text-shadow:none}.report-top .report-nav .brand{color:#17202a;text-shadow:none}.report-top .report-nav a:hover{color:#276bc7} +.report-hero.hero-carousel{min-height:680px}.hero-overlay{align-items:center;padding-top:72px;padding-bottom:34px}.hero-carousel .hero-copy{max-width:510px;padding:0}.hero-carousel .report-kicker{color:#276bc7}.report-hero.hero-carousel h1{color:#17202a;text-shadow:none}.report-hero.hero-carousel p{color:#566270;text-shadow:none}.hero-carousel .hero-meta{color:#87919d} +.hero-deck{right:5%;top:51%;width:500px;height:570px}.hero-deck-card{width:290px;height:470px;border-color:#dfe5eb;box-shadow:0 22px 55px rgba(32,44,60,.18)}.hero-deck-card.active{box-shadow:0 28px 70px rgba(32,44,60,.24)}.hero-deck-card:not(.active){filter:saturate(.9) brightness(.97)}.hero-carousel-ui{right:6%;bottom:32px}.hero-carousel-ui>button{border-color:#9aa6b3;background:#fff;color:#27313d}.hero-carousel-ui>button:hover{background:#eef3f8}.hero-dot{background:#c4ccd5!important}.hero-dot.active{background:#276bc7!important}.hero-carousel-count{color:#566270} +.report-top .report-nav .quick-product-btn{border-radius:12px;background:#276bc7;color:#fff;text-shadow:none}.report-top .report-nav .quick-product-btn:hover{background:#1f59a8;color:#fff} +@media(max-width:1050px){.hero-deck{right:1%;width:410px;height:520px}.hero-deck-card{width:250px;height:405px}.hero-carousel .hero-copy{max-width:460px}} +@media(max-width:800px){.report-hero.hero-carousel{min-height:720px}.hero-overlay{align-items:flex-end;padding:64px 24px 34px}.hero-deck{top:38%;right:2%;width:340px;height:360px}.hero-deck-card{width:190px;height:305px}.hero-carousel .hero-copy{max-width:520px;padding-bottom:72px}.hero-carousel-ui{left:24px;right:auto;bottom:26px}} +@media(max-width:560px){.report-hero.hero-carousel{min-height:720px}.hero-deck{top:34%;left:50%;right:auto;width:280px;height:310px}.hero-deck-card{width:165px;height:260px}.hero-carousel .hero-copy{padding-bottom:70px}.report-top .report-nav .quick-product-btn{border-radius:10px}} + +/* Unframed product imagery: preserve every source image without cropping or labels. */ +.report-app .report-figure, +.report-app .report-image-stage, +.report-app .image-analysis figure, +.report-app .story-media, +.report-app .buyer-gallery figure, +.hero-deck-card{ + border:0!important; + border-radius:0!important; + outline:0!important; + background:transparent!important; + box-shadow:none!important; +} +.hero-deck-card.active{box-shadow:none!important} +.hero-deck-card img{ + padding:0!important; + background:transparent!important; + object-fit:contain!important; + object-position:center!important; +} +.report-app .report-image-stage{overflow:visible} +.report-app .report-figure img, +.report-app .report-image-product, +.report-app .image-analysis img, +.report-app .story-media img, +.report-app .buyer-gallery img, +.report-app .visual img{ + max-width:100%; + object-fit:contain!important; + object-position:center!important; + background:transparent!important; +} +.main-analysis-grid .report-figure, +.detail-proof-media .report-figure, +.evidence-gallery .report-figure, +.buyer-photo-grid .report-figure{grid-template-rows:minmax(0,1fr)!important} +.image-source-badge,.hero-deck-card span,.report-figure figcaption,.hero-slide figcaption{display:none!important} + +/* Refined alignment and interaction; reference accent: cyan to sky-blue. */ +:root{--v925-reference-gradient:linear-gradient(100deg,#37dcc9 0%,#83c9ef 100%)} +#plan .plan-grid{align-items:stretch} +#plan .plan-card{ + position:relative; + min-height:100%; + transition:transform .2s ease,box-shadow .2s ease; +} +#plan .plan-card:hover{transform:translateY(-3px);box-shadow:0 14px 34px rgba(42,104,130,.12)} +#plan .plan-card:focus-within{box-shadow:0 0 0 3px rgba(55,220,201,.22),0 14px 34px rgba(42,104,130,.12)} +#plan .plan-card>span{ + align-self:flex-start; + width:max-content; + max-width:100%; + padding:5px 9px; + background:var(--v925-reference-gradient); + color:#fff!important; + font-size:11px; + line-height:1.25; +} +#plan .generate-plan-btn, +.report-top .report-nav .quick-product-btn{ + border-radius:10px; + background:var(--v925-reference-gradient); + color:#fff; + transition:transform .18s ease,filter .18s ease,box-shadow .18s ease; +} +#plan .generate-plan-btn:hover, +.report-top .report-nav .quick-product-btn:hover{ + background:var(--v925-reference-gradient); + filter:saturate(1.08) brightness(.96); + box-shadow:0 8px 22px rgba(45,190,190,.2); +} +#plan .generate-plan-btn:active, +.report-top .report-nav .quick-product-btn:active{transform:translateY(1px);box-shadow:none} +#plan .generate-plan-btn:focus-visible, +.report-top .report-nav .quick-product-btn:focus-visible{outline:3px solid rgba(55,220,201,.3);outline-offset:3px} + +.detail-proof-list{align-items:stretch} +.detail-proof-row{display:grid;grid-template-rows:260px minmax(0,1fr);height:100%} +.detail-proof-row:last-child:nth-child(odd){grid-column:1/-1;grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:300px} +.detail-proof-copy{display:flex;min-height:0;flex-direction:column;align-items:flex-start} +.detail-proof-copy>span{ + width:max-content; + max-width:100%; + padding:5px 9px; + background:var(--v925-reference-gradient); + color:#fff!important; + font-size:11px; + line-height:1.25; +} +.detail-proof-copy .next-action{width:100%;margin-top:auto;box-sizing:border-box} +.detail-proof-media,.detail-proof-media .report-figure,.detail-proof-media .report-image-stage{height:100%} +.detail-proof-media .report-image-stage, +.main-analysis-grid .report-image-stage, +.evidence-gallery .report-image-stage, +.buyer-photo-grid .report-image-stage{display:flex;align-items:flex-end;justify-content:center} +.detail-proof-media img, +.main-analysis-grid img, +.evidence-gallery img, +.buyer-photo-grid img{height:auto!important;max-height:100%;object-fit:contain!important} + +.main-analysis-grid>article{align-items:stretch;transition:background-color .2s ease} +.main-analysis-grid>article:hover{background:#fbfefe} +.main-analysis-grid .image-copy{display:flex;flex-direction:column;justify-content:center;align-self:stretch} +.evidence-drawer summary{display:flex;align-items:center;gap:7px;transition:color .18s ease,background-color .18s ease} +.evidence-drawer summary::before{content:'›';display:inline-block;color:#21b9ba;font-size:18px;line-height:1;transform:rotate(0);transition:transform .18s ease} +.evidence-drawer[open] summary::before{transform:rotate(90deg)} +.evidence-drawer summary:hover{color:#18aeb1;background:linear-gradient(90deg,rgba(55,220,201,.08),transparent)} + +@media(max-width:900px){.detail-proof-row{grid-template-rows:220px minmax(0,1fr)}.detail-proof-row:last-child:nth-child(odd){grid-template-rows:260px}} +@media(max-width:560px){.detail-proof-row,.detail-proof-row:last-child:nth-child(odd){grid-column:auto;grid-template-columns:1fr;grid-template-rows:250px minmax(0,1fr)}#plan .plan-card:hover{transform:none}} +@media(prefers-reduced-motion:reduce){#plan .plan-card,#plan .generate-plan-btn,.report-top .report-nav .quick-product-btn{transition:none}} + +/* Seven-role analysis: decision-first hierarchy with evidence on demand. */ +.role-block{--role-accent:#2aaeb1;--role-soft:#eefafa;border:1px solid #dfe7e9;background:#fff} +.role-productStrategy{--role-accent:#2477c8;--role-soft:#eef6ff} +.role-conversion{--role-accent:#1aa9a7;--role-soft:#edf9f8} +.role-consumerInsight{--role-accent:#c18432;--role-soft:#fff8ed} +.role-productSupply{--role-accent:#426db2;--role-soft:#f1f5fc} +.role-visualDesign{--role-accent:#8a67b5;--role-soft:#f7f2fb} +.role-executiveDecision{--role-accent:#198f78;--role-soft:#eef9f6} +.role-editorReview{--role-accent:#66717d;--role-soft:#f3f5f7} +.role-block>summary{position:relative;grid-template-columns:42px minmax(0,1fr) auto;padding:19px 20px;transition:background-color .18s ease} +.role-block>summary::after{content:'+';position:absolute;right:18px;top:50%;display:none;transform:translateY(-50%);color:var(--role-accent);font-size:21px;font-weight:400} +.role-block>summary:hover,.role-block[open]>summary{background:var(--role-soft)} +.role-block>summary>span{display:grid;width:28px;height:28px;place-items:center;background:var(--role-accent);color:#fff;border-radius:50%;font-size:10px} +.role-block>summary h3{font-size:19px}.role-block>summary>b{padding-right:4px;color:var(--role-accent);font-weight:700} +.role-content{padding:20px;border-top:1px solid #e3e9eb;background:#fbfcfc} +.role-conclusion{margin-bottom:14px;padding:17px 19px;border-left:4px solid var(--role-accent);background:var(--role-soft)} +.role-conclusion>span{display:block;margin-bottom:7px;color:var(--role-accent);font-size:11px;font-weight:850;letter-spacing:.04em} +.role-conclusion>p{margin:0;color:var(--v925-ink);font-size:17px;font-weight:750;line-height:1.65} +.role-section-list{display:grid;gap:8px} +.role-section{overflow:hidden;border:1px solid #e0e7e9;background:#fff} +.role-section>summary{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:13px 15px;list-style:none;cursor:pointer} +.role-section>summary::-webkit-details-marker,.role-evidence>summary::-webkit-details-marker,.role-raw>summary::-webkit-details-marker{display:none} +.role-section>summary>span{color:var(--v925-ink);font-size:14px;font-weight:800} +.role-section>summary>b{color:var(--role-accent);font-size:11px;font-weight:750} +.role-section>summary::before{content:'›';color:var(--role-accent);font-size:19px;line-height:1;transition:transform .18s ease} +.role-section[open]>summary::before{transform:rotate(90deg)} +.role-section[open]>summary{background:var(--role-soft)} +.role-section-body{padding:4px 15px 16px} +.role-section-body>.structured-object>.structured-field:first-child{border-top:0} +.structured-list{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px} +.structured-card{overflow:hidden;border:1px solid #e2e8ea;background:#fff!important;padding:0!important} +.structured-card>header{padding:10px 12px;background:var(--role-soft);border-left:3px solid var(--role-accent)} +.structured-card>header>span{color:var(--v925-ink);font-size:13px;font-weight:800} +.structured-card>.structured-object{padding:2px 12px 12px} +.structured-field{padding:10px 0 0;border-top:1px solid #edf0f1} +.structured-field>span{margin-bottom:5px;color:#7a858e;font-size:10px;letter-spacing:.02em} +.structured-field p{color:#35414b;font-size:13px;line-height:1.6} +.role-chip-list{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px} +.role-chip-list>span{padding:5px 8px;background:var(--role-soft);color:#3d5059;font-size:11px;line-height:1.35} +.role-confidence{display:grid;grid-template-columns:auto minmax(70px,1fr) auto;gap:9px;align-items:center;padding-top:10px;border-top:1px solid #edf0f1;color:#7a858e;font-size:10px} +.role-confidence>i{height:5px;overflow:hidden;background:#e9eef0;border-radius:9px}.role-confidence>i>b{display:block;height:100%;background:var(--role-accent)} +.role-confidence>strong{color:var(--role-accent);font-size:10px} +.role-evidence,.role-raw{margin-top:10px;border-top:1px dashed #d9e1e4} +.role-evidence>summary,.role-raw>summary{display:flex;align-items:center;min-height:auto;padding:9px 0 0;color:#7a858e;font-size:10px;font-weight:750;cursor:pointer} +.role-evidence>summary::before,.role-raw>summary::before{content:'+';margin-right:6px;color:var(--role-accent);font-size:13px} +.role-evidence[open]>summary::before,.role-raw[open]>summary::before{content:'−'} +.role-evidence>div{padding-top:7px}.role-evidence code{display:inline-block;margin:2px 4px 2px 0;padding:3px 6px;background:#f0f4f5;color:#66737c;font-size:9px} +.role-raw>.structured-list,.role-raw>.structured-object{margin-top:9px} +.support-analysis-grid{display:grid;gap:10px} +.support-analysis-card{border:1px solid #dfe7e9;background:#fff} +.support-analysis-card>summary{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;cursor:pointer;list-style:none;background:#f7fafb} +.support-analysis-card>summary::-webkit-details-marker{display:none} +.support-analysis-card>summary>span{color:#20313a;font-size:14px;font-weight:800} +.support-analysis-card>summary>b{color:#2774a8;font-size:11px} +.support-analysis-card>summary::before{content:'›';margin-right:4px;color:#22aeb0;font-size:19px;transition:transform .18s ease} +.support-analysis-card[open]>summary::before{transform:rotate(90deg)} +.support-analysis-body{padding:4px 16px 16px;border-top:1px solid #e4ecee} +@media(max-width:700px){.role-content{padding:12px}.role-block>summary{grid-template-columns:34px minmax(0,1fr);padding:15px}.role-block>summary>b{display:none}.role-block>summary::after{display:block}.role-conclusion{padding:14px}.role-conclusion>p{font-size:15px}.structured-list{grid-template-columns:1fr}.role-section-body{padding:2px 12px 13px}} /* JSON report view */ .report-app .wrap{max-width:1180px;margin:0 auto;padding:0 24px} .report-section{padding:42px 0;border-bottom:1px solid #e8ece7} @@ -301,3 +602,15 @@ body{background:#fff;color:var(--v92-text);font-size:14px;line-height:1.55} .report-app .card h3{margin:0 0 10px;font-size:17px}.report-app .card p{margin:7px 0;line-height:1.6} .report-app .gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:18px} .report-app figure{margin:0;background:#f4f7f3;border-radius:12px;overflow:hidden}.report-app figure img{width:100%;height:220px;object-fit:contain;display:block}.report-app figcaption{padding:7px 10px;font-size:12px;color:#647064} + +/* Human-readable evidence references. */ +.role-evidence>div.role-evidence-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;padding-top:9px} +.role-evidence-item{display:grid;min-width:0;grid-template-columns:1fr;padding:10px;background:#f6f8f9} +.role-evidence-item.has-image{grid-template-columns:68px minmax(0,1fr);gap:10px;align-items:center} +.role-evidence-item>img{display:block;width:68px!important;height:68px!important;min-height:0!important;object-fit:contain!important;background:transparent!important} +.role-evidence-item>div{min-width:0}.role-evidence-item b{display:block;color:var(--role-accent);font-size:10px} +.role-evidence-item p{display:-webkit-box;overflow:hidden;margin:4px 0!important;color:#394650!important;font-size:11px!important;line-height:1.5!important;-webkit-box-orient:vertical;-webkit-line-clamp:3} +.role-evidence-item code{display:block!important;width:max-content;max-width:100%;margin:5px 0 0!important;padding:0!important;background:transparent!important;color:#99a2a9!important;font-size:8px!important} +@media(max-width:700px){.role-evidence>div.role-evidence-list{grid-template-columns:1fr}.role-evidence-item.has-image{grid-template-columns:58px minmax(0,1fr)}.role-evidence-item>img{width:58px!important;height:58px!important}} +/* 审核失败仍输出报告时的非阻断提示 */ +.report-audit-notice{margin-top:14px!important;padding:10px 12px;border-left:3px solid #d59b3f;background:#fff7e8;color:#805a20!important;font-size:13px!important;line-height:1.5!important;text-shadow:none!important} diff --git a/server/report.js b/server/report.js index d75fefa..aa7e34c 100644 --- a/server/report.js +++ b/server/report.js @@ -1,26 +1,351 @@ -/* 独立报告视图:页面只负责渲染 JSON,不在 Python 中拼接 HTML。 */ -const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); -const arr = v => Array.isArray(v) ? v : []; -const text = (v, keys=['value','label','headline','note','insight','action','name']) => { - if (Array.isArray(v)) return v.map(x => text(x, keys)).filter(Boolean).join(' · '); - if (typeof v === 'string' || typeof v === 'number') return String(v); - if (v && typeof v === 'object') for (const k of keys) if (v[k] != null && v[k] !== '') return text(v[k], keys); - return ''; +/* 独立报告视图:以分析 JSON 为唯一数据源,完整展示分析与证据。 */ +const esc = value => String(value ?? '').replace(/[&<>"']/g, char => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char])); +const arr = value => Array.isArray(value) ? value : []; +const compact = value => arr(value).map(item => typeof item === 'object' ? (item.value || item.topic || item.name || item.label || item.action || item.insight || '') : item).filter(Boolean).join(' · '); +const percent = value => `${(Number(value || 0) * 100).toFixed(1).replace('.0','')}%`; +let evidenceIndex = new Map(); +const EMPTY_COPY = /^(暂无|无|未采集|未评级|尚未结构化|图片证据未采集|评论原文未采集|日期未采集)$/; +function hasContent(value) { + if (value == null) return false; + if (typeof value === 'string') return Boolean(value.trim()) && !EMPTY_COPY.test(value.trim()); + if (typeof value === 'number' || typeof value === 'boolean') return true; + if (Array.isArray(value)) return value.some(hasContent); + if (typeof value === 'object') return Object.values(value).some(hasContent); + return false; +} + +const FIELD_LABELS = { + professionalOpinion:'专业判断', positioning:'商品定位', statement:'结论', decisionImpact:'决策意义', valuePillars:'核心产品资产', + claimVsProof:'卖点与证明', positioningRisks:'定位风险', title:'标题表达', visual:'主图与视觉', detail:'详情页信息链', + consumerInsights:'消费者洞察', experienceSignals:'体验信号', productImplications:'产品需求', buyerEvidence:'买家秀验证', + sku:'SKU结构', product:'产品与供应链', parameterFacts:'参数事实', gates:'风险与验证门', baseline:'样本基线', + fashionThesis:'视觉方向', designAssets:'视觉资产', styleSignals:'风格信号', fashionRisks:'视觉风险', trendBoundary:'趋势边界', + chapterTitle:'章节', cards:'决策卡', status:'审核状态', validation:'审核结果', scope:'审核范围', checks:'检查项', missing:'缺失项', + customerValue:'消费者价值', businessMeaning:'经营意义', mustKeep:'必须保留', commercialRisk:'商业风险', businessImpact:'业务影响', + action:'执行动作', nextAction:'下一步动作', developmentAction:'开发动作', observation:'观察', risk:'风险', why:'原因', + originalTitle:'当前标题', currentExpression:'已经表达', reinforce:'建议强化', recommendedActions:'推荐动作', recommendedTitles:'标题建议', + contentGroups:'信息任务', pageSequence:'页面顺序', selectionDimensions:'选择维度', recommendedOrder:'推荐选择顺序', recommendedAction:'推荐动作', + evidenceIds:'证据', confidence:'置信度', name:'名称', type:'类型', value:'内容', label:'标签', headline:'结论', note:'说明', bullets:'要点', + claim:'卖点', productAction:'产品动作', pageAction:'页面动作', commercialMeaning:'商业意义', whatItMeansForProduct:'对产品的意义', + asset:'资产', signal:'信号', field:'字段', actual:'实际', minimum:'最低要求', ok:'通过', originalTitle:'当前标题', + currentExpression:'已表达信息', reinforce:'建议强化', recommendedTitles:'标题建议', items:'图片任务', evidenceDetail:'补充证据', + representativeImageIds:'代表图片', structuredStatus:'结构状态', structuredCount:'结构化SKU数', imageCount:'图片数', reviewCount:'评论样本', + questionCount:'问答样本', ratingBuckets:'评分分布', topics:'评论主题', skuBreakdown:'SKU反馈', imageCounts:'图片统计', skuCount:'SKU数', + attributeCount:'属性数', promotionCount:'促销信息数', reviewCollectionComplete:'评论采集完成', reviewStopReason:'采集停止原因', + publicReviewCount:'页面公开评论', reviewPagesVisited:'评论访问页数', buyerEvidence:'买家秀证据', stages:'消费阶段', signals:'反馈信号', + positive:'正向反馈', questions:'用户疑问', risks:'风险反馈', fashionThesis:'视觉主张', commercialValue:'商业价值', trendBoundary:'趋势判断边界', + fashionRisks:'视觉风险', designAssets:'设计资产', styleSignals:'风格信号', validation:'审核结果', scope:'审核范围', + structuredStatus:'结构化状态', status:'状态', gate:'验证门', examples:'评论原话', mentions:'提及次数', sampleRate:'样本占比' }; -function cards(items, fields=['headline','note']) { - return arr(items).map(x => `

${esc(text(x,['label','name','type']))}

${fields.map(k=>text(x,[k])).filter(Boolean).map(v=>`

${esc(v)}

`).join('')}
`).join(''); + +const ROLE_META = [ + ['productStrategy','01','商品策略','卖什么、为什么值得参考、下一款继承什么'], + ['conversion','02','电商转化','用户为什么点、为什么继续看、为什么愿意买'], + ['consumerInsight','03','消费者洞察','把评论和买家秀转成真实产品需求'], + ['productSupply','04','产品 / 供应链','哪些能落地、哪些必须验证'], + ['visualDesign','05','视觉设计','提炼下一款可复用的视觉资产'], + ['executiveDecision','06','商业决策 / 老板','把专业判断压成直接决策'], + ['editorReview','07','总编审核','统一校验、纠错、去重与压缩'] +]; + +function imageEntries(data, group) { + return arr(data.evidenceLedger).filter(item => item?.type === 'image' && item?.meta?.group === group); } -function images(data, group, limit=8) { - const list = arr(data.evidenceLedger).filter(e => e?.type === 'image' && e?.meta?.group === group).slice(0,limit); - return list.map(e => `
商品${esc(group)}
${esc(e.id)}
`).join('') || '

未采集

'; +function imageUrl(item) { return item?.meta?.localUrl || item?.value || ''; } +function figure(item, caption='', className='') { + if (!item || !imageUrl(item)) return ''; + const source = esc(imageUrl(item)); + return `
${esc(caption || '商品图片')}
`; +} +function section(id, no, title, deck, body) { + if (!hasContent(body)) return ''; + return `
${esc(no)}

${esc(title)}

${esc(deck)}

${body}
`; +} +function decisionCards(items) { + const valid=arr(items).filter(item=>hasContent(item?.headline)||hasContent(item?.name)||hasContent(item?.note)||hasContent(item?.bullets)); + return valid.length ? `
${valid.map(item => `
${hasContent(item.label||item.type)?`${esc(item.label||item.type)}`:''}${hasContent(item.headline||item.name)?`

${esc(item.headline||item.name)}

`:''}${hasContent(item.note)?`

${esc(item.note)}

`:''}${hasContent(item.bullets)?`
    ${arr(item.bullets).filter(hasContent).map(x=>`
  • ${esc(x)}
  • `).join('')}
`:''}
`).join('')}
` : ''; +} +function planCards(items) { + const tone = item => String(item?.sourceType || '').includes('探索') ? 'explore' : String(item?.sourceType || '').includes('反馈') ? 'upgrade' : 'evidence'; + const valid=arr(items).map((item,index)=>({item,index})).filter(({item})=>hasContent(item)); + return valid.length ? `
${valid.map(({item,index}) => {const rows=[['产品',item.productAction],['页面',item.pageAction],['验证',item.validation]].filter(([,value])=>hasContent(value));return `
${hasContent(item.sourceType||item.type)?`${esc(item.sourceType||item.type)}`:''}${hasContent(item.name)?`

${esc(item.name)}

`:''}${hasContent(item.whyThisPlan)?`

${esc(item.whyThisPlan)}

`:''}${rows.length?`
${rows.map(([label,value])=>`
${label}
${esc(value)}
`).join('')}
`:''}
`}).join('')}
` : ''; +} +function titleAnalysis(block) { + const expressed = compact(block.currentExpression); + const reinforce = arr(block.reinforce).map(x=>x?.topic || x?.value || x).filter(Boolean).join(' · '); + const compare=[['已经表达',expressed],['建议强化',reinforce||compact(block.recommendedActions)]].filter(([,value])=>hasContent(value)); + return `${hasContent(block.professionalOpinion)?`

${esc(block.professionalOpinion)}

`:''}${hasContent(block.originalTitle)?`
当前标题${esc(block.originalTitle)}
`:''}${compare.length?`
${compare.map(([label,value])=>`
${label}${esc(value)}
`).join('')}
`:''}${hasContent(block.recommendedTitles)?`
查看推荐标题
    ${arr(block.recommendedTitles).filter(hasContent).map(x=>`
  1. ${esc(x)}
  2. `).join('')}
`:''}`; +} +function mainImages(data, block) { + const entries = new Map(imageEntries(data,'main').map(item=>[item.id,item])); + const items=arr(block.items).filter(item=>hasContent(item)&&entries.get(item.imageEvidenceId)); + return `${hasContent(block.professionalOpinion)?`

${esc(block.professionalOpinion)}

`:''}${items.length?`
${items.map(item=>`
${figure(entries.get(item.imageEvidenceId), item.imageRole || '商品主图')}
主图分析${hasContent(item.imageRole)?`

${esc(item.imageRole)}

`:''}${hasContent(item.visualSignals)?`${esc(compact(item.visualSignals))}`:''}${hasContent(item.businessMeaning)?`

${esc(item.businessMeaning)}

`:''}${hasContent(item.nextAction)?`${esc(item.nextAction)}`:''}
`).join('')}
`:''}`; +} +function classifyDetailGroup(group) { + const source = `${group?.type || ''} ${group?.stage || ''}`; + const rules = [ + [/场景|人群|体验|需求/,10,'使用场景','先回答为什么需要'], + [/卖点|利益|价值/,15,'核心利益','再说明能带来什么'], + [/材质|面料|成分|填充/,20,'材质证明','证明产品基础'], + [/工艺|细节|结构|做工/,30,'工艺细节','证明品质如何实现'], + [/耐用|性能|功能|洗涤|防护|透气/,40,'性能验证','证明长期使用表现'], + [/规格|尺寸|SKU|选择|适配/,50,'规格选择','降低选择门槛'], + [/品牌|服务|质检|证书|信任/,60,'信任背书','最后消除决策顾虑'] + ]; + const found = rules.find(([pattern])=>pattern.test(source)); + return found ? {...group,sequencePriority:found[1],sequenceLabel:found[2],sequenceHint:found[3]} : {...group,sequencePriority:70,sequenceLabel:'补充证据',sequenceHint:'补充未覆盖的信息'}; +} +function detailProof(data, block) { + const allImages = arr(data.evidenceLedger).filter(item=>item?.type === 'image'); + const byId = new Map(allImages.map(item=>[item.id,item])); + const detailImages = imageEntries(data,'detail'); + const groups = arr(block.contentGroups).filter(hasContent).slice(0,6).map(classifyDetailGroup).sort((a,b)=>a.sequencePriority-b.sequencePriority); + const used = new Set(); + const rows = groups.map((group,index) => { + const image = byId.get(group.representativeImageId || group.imageEvidenceId); + if (image) used.add(image.id); + const imageGroup = image?.meta?.group; + const badge = imageGroup === 'detail' ? '详情代表图' : imageGroup === 'main' ? '主图补充证据' : '图片证据'; + return `
${image?`
${figure(image, group.type || '详情任务')}
`:''}
${hasContent(group.sequenceLabel)?`${esc(group.sequenceLabel)}`:''}${hasContent(group.type)?`

${esc(group.type)}

`:''}${hasContent(group.visualSignals)||hasContent(group.businessMeaning)?`${esc(compact(group.visualSignals)||group.businessMeaning)}`:''}${hasContent(group.businessMeaning)?`

${esc(group.businessMeaning)}

`:''}${hasContent(group.nextAction)?`
下一步动作${esc(group.nextAction)}
`:''}
`; + }).join(''); + return `${hasContent(block.professionalOpinion)?`

${esc(block.professionalOpinion)}

`:''}${groups.length?``:''}${rows?`
${rows}
`:''}${detailImages.length?`
查看全部详情图证据 ${detailImages.length} 张

默认展示信息任务代表图,其余图片作为完整证据保留。

`:''}`; +} +function skuAnalysis(block) { + const dimensions=arr(block.selectionDimensions).filter(item=>hasContent(item?.name)||hasContent(item?.values)); + return `${hasContent(block.professionalOpinion)?`

${esc(block.professionalOpinion)}

`:''}${dimensions.length?`
${dimensions.map(item=>`
${hasContent(item.name)?`${esc(item.name)}`:''}${hasContent(item.values)?`

${esc(compact(item.values))}

`:''}
`).join('')}
`:''}${hasContent(block.recommendedAction)?`
推荐动作${esc(block.recommendedAction)}
`:''}`; +} +function topicBars(base) { + const topics = arr(base.topics).slice(0,8); + if (!topics.length) return ''; + return `
评论关注主题

用户实际在讨论什么

主题可重叠,比例不应相加为100%
${topics.map(item=>{const rate = Number(item.sampleRate ?? (base.reviewCount ? item.mentions/base.reviewCount : 0)); return `
${esc(item.topic)}
${Number(item.mentions || 0)}次${percent(rate)}
`}).join('')}
`; +} +function buyerGallery(data, block) { + const items = arr(block?.buyerEvidence?.items); + const byId = new Map(arr(data.evidenceLedger).map(item=>[item.id,item])); + const grouped = new Map(); + items.forEach(item=>{ + const key = item.reviewEvidenceId || item.imageEvidenceId; + if (!grouped.has(key)) grouped.set(key,{reviewId:item.reviewEvidenceId,quote:item.quote,date:item.date,sku:item.sku,images:[]}); + grouped.get(key).images.push(byId.get(item.imageEvidenceId)); + }); + if (!grouped.size) { + const loose = imageEntries(data,'buyerShow'); + return loose.length ? `
${loose.map(item=>figure(item,item.id)).join('')}

买家秀已采集,尚未关联到具体评论。

` : ''; + } + return [...grouped.values()].filter(group=>group.images.some(Boolean)||hasContent(group.quote)||hasContent(group.sku)).map(group=>`
${hasContent(group.reviewId)?`${esc(group.reviewId)}`:''}${group.images.filter(Boolean).length} 张实拍图${hasContent(group.date)?` · ${esc(group.date)}`:''}
${group.images.some(Boolean)?`
${group.images.filter(Boolean).map(item=>figure(item,'')).join('')}
`:''}${hasContent(group.quote)?`
“${esc(group.quote)}”
`:''}${hasContent(group.sku)?`

${esc(group.sku)}

`:''}
`).join(''); +} +function reviews(data, block) { + const base = data.baseline || {}; + const quality = data.dataQuality || {}; + const signals = arr(block?.consumerInsights?.experienceSignals).filter(item=>hasContent(item?.insight)||hasContent(item?.statement)); + const research = data.consumerResearch || {}; + const batchFindings = arr(data.reviewBatchAnalysis).flatMap(batch => arr(batch?.batchFindings)); + const issues = [ + ...arr(research.painPoints).map(item => ({label:'用户痛点', title:item.problem, impact:item.businessImpact, action:item.productAction, evidence:item.evidenceIds, tone:'risk'})), + ...arr(research.expectationGaps).map(item => ({label:'预期落差', title:`${item.expected} → ${item.actual}`, impact:item.commercialRisk, action:item.productOpportunity, evidence:item.evidenceIds, tone:'gap'})), + ...arr(research.prePurchaseConcerns).map(item => ({label:'购前疑问', title:item.concern, impact:item.conversionImpact, action:item.pageAction, evidence:item.evidenceIds, tone:'question'})) + ]; + const fallbackIssues = batchFindings.filter(item=>['pain','expectation_gap','prepurchase_concern'].includes(item.type)).map(item=>({label:item.type==='pain'?'用户痛点':item.type==='expectation_gap'?'预期落差':'购前疑问',title:item.finding,impact:item.businessMeaning,action:'转为产品、页面或履约验证动作',evidence:item.evidenceIds,tone:item.type==='pain'?'risk':'gap'})); + const issueCards = (issues.length ? issues : fallbackIssues).filter(item=>hasContent(item.title)||hasContent(item.impact)||hasContent(item.action)).slice(0,4).map(item=>`
${hasContent(item.label)?`${esc(item.label)}`:''}${hasContent(item.title)?`

${esc(item.title)}

`:''}${hasContent(item.impact)?`

影响 ${esc(item.impact)}

`:''}${hasContent(item.action)?`

动作 ${esc(item.action)}

`:''}${arr(item.evidence).length?`${arr(item.evidence).length} 条对应证据`:''}
`).join(''); + const completed = Boolean(base.reviewCollectionComplete); + const stopText = completed ? '已到达可访问末页' : base.reviewStopReason === 'user_stopped' ? '用户主动停止,当前为样本分析' : '采集未确认完成'; + const reviewCount = base.reviewCount ?? quality.reviewSample; + const publicCount = base.publicReviewCount ?? quality.publicReviewCount; + const stats = [['本次有效评论',reviewCount],['页面公开数量',publicCount],['访问页数',base.reviewPagesVisited],['样本状态',stopText,completed?'complete':'limited']].filter(([,value])=>hasContent(value)); + const topics = topicBars(base); + const signalHtml = signals.length ? `
结论层

认可、差异与购前顾虑

${signals.map(item=>`
${hasContent(item.type)?`${esc(item.type)}`:''}

${esc(item.insight||item.statement)}

${arr(item.evidenceIds).length?`${arr(item.evidenceIds).length} 条对应证据`:''}
`).join('')}
` : ''; + const buyers = buyerGallery(data,block); + return `${hasContent(block.professionalOpinion)?`

${esc(block.professionalOpinion)}

`:''}${stats.length?`
${stats.map(([label,value,tone])=>`
${label}${esc(value)}
`).join('')}
`:''}${topics||signalHtml?`
${topics}${signalHtml}
`:''}${issueCards?`
问题洞察

用户讨论暴露了哪些需要解决的问题

从痛点、预期落差和购前疑问转成可执行动作

${issueCards}
`:''}${buyers?`
买家秀验证

图片按评论合并,不把多张图重复计算为多人反馈

${buyers}
`:''}`; +} +const ROLE_DENSE_FIELDS = new Set(['baseline','buyerEvidence','signals','stages','evidenceDetail']); +function displayValue(value) { + if (typeof value === 'boolean') return value ? '是' : '否'; + const status = {passed:'已通过',confirmed:'已确认',high:'高风险',medium:'中风险',low:'低风险',consumer_supported:'评论支持',conflicted:'存在冲突',page_claim_only:'仅页面宣称',user_stopped:'用户主动停止'}; + return status[value] || value; +} +function itemTitle(item) { + return item?.headline || item?.name || item?.type || item?.asset || item?.signal || item?.claim || item?.risk || item?.gate || item?.topic || item?.field || ''; +} +function evidenceType(item, id='') { + if (item?.type === 'image') return ({main:'商品主图',detail:'详情图片',buyerShow:'买家秀'})[item?.meta?.group] || '图片证据'; + if (item?.type === 'review' || id.startsWith('REV_')) return '用户评论'; + if (item?.type === 'question' || id.startsWith('QA_')) return '用户问答'; + if (item?.type === 'attribute' || id.startsWith('ATTR_')) return '商品属性'; + if (item?.type === 'promotion' || id.startsWith('PROMO_')) return '促销信息'; + if (item?.type === 'sales' || id.startsWith('S_')) return '页面数据'; + if (item?.type === 'product' || id.startsWith('P_')) return '商品信息'; + return '证据'; +} +function evidenceText(item, id='') { + const value = item?.value; + if (item?.type === 'image') return ({main:'商品主图采集',detail:'详情页图片采集',buyerShow:'评论买家秀采集'})[item?.meta?.group] || '页面图片采集'; + if (item?.type === 'attribute') return `${value?.name || '属性'}:${value?.value ?? ''}`; + if (item?.type === 'review') return value?.content || value?.appendContent || '评论内容未采集'; + if (item?.type === 'question') return value?.question || '问答内容未采集'; + if (item?.type === 'promotion') return value?.text || '促销内容未采集'; + const labels = {P_TITLE:'商品标题',P_SHOP:'店铺信息',P_ITEMID:'商品ID',P_SKUID:'SKU ID',S_CURRENTPRICE:'当前价格',S_SOLD:'页面销量',S_RANKING:'榜单排名'}; + return `${labels[id] ? `${labels[id]}:` : ''}${typeof value === 'object' ? compact(Object.values(value)) : (value ?? '未采集')}`; +} +function readableEvidence(id) { + const item = evidenceIndex.get(id); + if (!item || !hasContent(item.value)) return ''; + const text = evidenceText(item,id); + const preview = text.length > 120 ? `${text.slice(0,120)}…` : text; + const image = item?.type === 'image' && imageUrl(item) ? `${esc(evidenceType(item,id))}` : ''; + return `
${image}
${esc(evidenceType(item,id))}

${esc(preview)}

${esc(id)}
`; +} +function renderStructured(value, depth=0) { + if (!hasContent(value)) return ''; + if (typeof value !== 'object') return `

${esc(displayValue(value))}

`; + if (Array.isArray(value)) { + const valid=value.filter(hasContent); + if (!valid.length) return ''; + if (valid.every(item=>typeof item !== 'object')) return `
${valid.map(item=>`${esc(displayValue(item))}`).join('')}
`; + return `
${valid.map(item=>{const title=itemTitle(item);const inner=renderStructured(Object.fromEntries(Object.entries(item || {}).filter(([key])=>!title || !['headline','name','type','asset','signal','claim','risk','gate','topic','field'].includes(key))),depth+1);return title||inner?`
${title?`
${esc(title)}
`:''}${inner}
`:''}).join('')}
`; + } + return `
${Object.entries(value).map(([key,item])=>{ + if (!hasContent(item)) return ''; + const label = FIELD_LABELS[key] || key; + if (key === 'confidence') return `
${esc(label)}${percent(item)}
`; + if (key === 'evidenceIds') { const ids=arr(item).filter(id=>evidenceIndex.has(id)&&hasContent(evidenceIndex.get(id)?.value)); if(!ids.length)return ''; const sources=[...new Set(ids.map(id=>evidenceType(evidenceIndex.get(id),id)))]; return `
查看证据 · ${ids.length} 条${sources.length?` · ${esc(sources.join(' / '))}`:''}
${ids.map(readableEvidence).join('')}
`; } + const dense = depth > 0 && (key === 'examples' || key === 'skuBreakdown'); + if (dense) return `
${esc(label)} · ${arr(item).length} 项${renderStructured(item,depth+1)}
`; + return `
${esc(label)}${renderStructured(item,depth+1)}
`; + }).join('')}
`; +} +function roleConclusion(key, content) { + if (content?.professionalOpinion) return content.professionalOpinion; + if (key === 'productStrategy') return content?.positioning?.statement || ''; + if (key === 'conversion') return [content?.title?.professionalOpinion,content?.visual?.professionalOpinion,content?.detail?.professionalOpinion].filter(Boolean).join(' '); + if (key === 'productSupply') return content?.sku?.professionalOpinion || content?.product?.gates?.[0]?.gate || ''; + if (key === 'visualDesign') return content?.fashionThesis?.statement || ''; + if (key === 'executiveDecision') return arr(content?.cards).map(item=>item.headline).filter(Boolean).slice(0,3).join(';'); + if (key === 'editorReview') return content?.status === 'passed' ? '本次报告已通过事实、证据、对象匹配和表达一致性审核。' : '报告仍有需要复核的项目。'; + return ''; +} +function roleOutputs(roles) { + const visible=ROLE_META.map(([key,no,title,deck])=>{const content=roles?.[key];if(!hasContent(content))return '';const sections=Object.entries(content).filter(([field,value])=>field!=='professionalOpinion'&&hasContent(value)&&hasContent(renderStructured(value)));const conclusion=roleConclusion(key,content);if(!hasContent(conclusion)&&!sections.length)return '';return `
${no}

${esc(title)}

${esc(deck)}

${sections.length} 个分析模块
${hasContent(conclusion)?`
核心判断

${esc(conclusion)}

`:''}${sections.length?`
${sections.map(([field,value],index)=>`
${esc(FIELD_LABELS[field]||field)}${Array.isArray(value)?`${value.filter(hasContent).length} 项`:'查看分析'}
${renderStructured(value)}
`).join('')}
`:''}
`}).filter(Boolean); + return visible.length ? `
${visible.join('')}
` : ''; +} +function supportAnalysis(data) { + const exp=data.experienceSolution||{}; + const blocks=[ + ['reportPlan','报告分析计划',data.reportPlan], + ['dataQuality','数据质量与样本边界',data.dataQuality], + ['reviewBatchAnalysis','评论批次分析',data.reviewBatchAnalysis], + ['productExperience','产品与供应链核验',exp.productExperience], + ['validationLoop','验证闭环',exp.validationLoop], + ['expressionContinuity','卖点承接检查',exp.expressionContinuity], + ['consumerResearch','消费者研究明细',data.consumerResearch], + ['questionResearch','购前问答研究',data.questionResearch], + ['commercialDecision','商业决策依据',data.commercialDecision], + ['competitionDecision','单品边界与竞品数据',data.competitionDecision], + ['merchandisingDecision','经营表达决策明细',data.merchandisingDecision], + ['productDecision','产品决策明细',data.productDecision], + ['visualDecision','视觉决策明细',data.visualDecision] + ].filter(([, ,value])=>hasContent(value)); + if(!blocks.length)return ''; + return `
${blocks.map(([key,title,value])=>`
${esc(title)}查看内容
${renderStructured(value)}
`).join('')}
`; +} +function initHeroCarousel() { + const root = document.querySelector('.hero-carousel'); + if (!root) return; + const slides = [...root.querySelectorAll('.hero-slide')], dots = [...root.querySelectorAll('.hero-dot')], cards = [...root.querySelectorAll('.hero-deck-card')]; + if (slides.length < 2) return; + let current = 0, timer; + const show = index => { + current = (index + slides.length) % slides.length; + slides.forEach((slide,i)=>{slide.classList.toggle('active',i===current); slide.setAttribute('aria-hidden',i===current?'false':'true')}); + dots.forEach((dot,i)=>{dot.classList.toggle('active',i===current); dot.setAttribute('aria-current',i===current?'true':'false')}); + const step = window.innerWidth <= 560 ? 26 : window.innerWidth <= 800 ? 34 : 58; + const angle = window.innerWidth <= 560 ? 5 : 6; + const center = (cards.length - 1) / 2; + cards.forEach((card,i)=>{const slot=i-center;card.classList.toggle('active',i===current);card.setAttribute('aria-current',i===current?'true':'false');card.style.setProperty('--deck-x',`${slot*step}px`);card.style.setProperty('--deck-r',`${slot*angle}deg`)}); + const count = root.querySelector('.hero-carousel-count'); + if (count) count.textContent = `${String(current+1).padStart(2,'0')} / ${String(slides.length).padStart(2,'0')}`; + }; + const stop = () => clearInterval(timer); + const start = () => { stop(); if (!matchMedia('(prefers-reduced-motion: reduce)').matches) timer=setInterval(()=>show(current+1),5000); }; + root.querySelector('[data-carousel-prev]')?.addEventListener('click',()=>{show(current-1);start()}); + root.querySelector('[data-carousel-next]')?.addEventListener('click',()=>{show(current+1);start()}); + dots.forEach((dot,index)=>dot.addEventListener('click',()=>{show(index);start()})); + cards.forEach((card,index)=>card.addEventListener('click',()=>{show(index);start()})); + root.addEventListener('mouseenter',stop); root.addEventListener('mouseleave',start); + root.addEventListener('focusin',stop); root.addEventListener('focusout',start); + root.addEventListener('keydown',event=>{if(event.key==='ArrowLeft')show(current-1);if(event.key==='ArrowRight')show(current+1)}); + window.addEventListener('resize',()=>show(current),{passive:true}); + show(0); start(); +} +function initPlanActions(plans) { + const dialog = document.querySelector('#quick-plan-dialog'); + if (!dialog) return; + const title = dialog.querySelector('[data-plan-title]'), reason = dialog.querySelector('[data-plan-reason]'); + const product = dialog.querySelector('[data-plan-product]'), page = dialog.querySelector('[data-plan-page]'), validation = dialog.querySelector('[data-plan-validation]'); + let currentText = ''; + document.querySelectorAll('.generate-plan-btn').forEach(button=>button.addEventListener('click',()=>{ + const plan = arr(plans)[Number(button.dataset.planIndex)] || {}; + title.textContent = plan.name || '快速开品方案'; + reason.textContent = plan.whyThisPlan || ''; reason.hidden = !hasContent(plan.whyThisPlan); + [[product,plan.productAction],[page,plan.pageAction],[validation,plan.validation]].forEach(([node,value])=>{node.textContent=value||'';node.closest('div').hidden=!hasContent(value)}); + currentText = [[`快速开品任务|${plan.name||''}`,plan.name],['依据:'+(plan.whyThisPlan||''),plan.whyThisPlan],['产品动作:'+(plan.productAction||''),plan.productAction],['页面动作:'+(plan.pageAction||''),plan.pageAction],['验证指标:'+(plan.validation||''),plan.validation]].filter(([,value])=>hasContent(value)).map(([line])=>line).join('\n'); + if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open',''); + })); + dialog.querySelector('[data-close-plan]')?.addEventListener('click',()=>dialog.close ? dialog.close() : dialog.removeAttribute('open')); + dialog.querySelector('[data-copy-plan]')?.addEventListener('click',async event=>{ + try { await navigator.clipboard.writeText(currentText); event.currentTarget.textContent='已复制任务'; } + catch { event.currentTarget.textContent='复制失败,请手动复制'; } + setTimeout(()=>event.currentTarget.textContent='复制开品任务',1600); + }); } -function section(id, title, body) { return `

${esc(title)}

${body}
`; } async function main() { - const q = new URLSearchParams(location.search); const source = window.REPORT_DATA || q.get('data'); + const query = new URLSearchParams(location.search); + const source = window.REPORT_DATA || query.get('data'); if (!source) throw new Error('缺少 data 参数'); - const data = await fetch(source).then(r => r.json()); const exp = data.experienceSolution || {}; const raw = data.facts || {}; const p = raw.product || {}; const s = raw.sales || {}; - const summary = exp.reportSummary || {}; document.title = summary.title || p.title || '商品开品分析报告'; - const owner = exp.ownerOverview?.cards || []; const plans = exp.newProductPlans?.plans || []; const title = exp.titleAnalysis || {}; const visual = exp.visualCommerce || {}; const detail = exp.detailCommerce || {}; - document.getElementById('app').innerHTML = `
PRODUCT DECISION REPORT · JSON VIEW

${esc(summary.title || p.title || '商品分析报告')}

${esc(summary.verdict || '')}

价格 ${esc(s.currentPrice || '未采集')} · 评论 ${esc(data.baseline?.reviewCount || 0)}
${section('decision','老板速览',`
${cards(owner)}
`)}${section('plan','下一款方向',`
${cards(plans,['sourceType','whyThisPlan','productAction','pageAction'])}
`)}${section('title','标题表达',`

${esc(title.professionalOpinion || '')}

`)}${section('main','主图在卖什么',`
${cards(visual.items,['imageRole','visualSignals','nextAction'])}
`)}${section('detail','详情页证明什么',`
${cards(detail.contentGroups,['type','visualSignals','nextAction'])}
`)}${section('sku','SKU怎么选',`

${esc(exp.skuAnalysis?.professionalOpinion || '')}

${cards(exp.skuAnalysis?.selectionDimensions,['name','values','evidenceIds'])}`)}${section('review','消费者实际感受到什么',`${cards(exp.customerExperience?.consumerInsights?.experienceSignals,['type','insight'])}`)}
数据来自本次采集证据;页面由独立 HTML + JSON 渲染
`; + let data; + if (typeof source === 'object') data = source; + else { + const response = await fetch(source,{cache:'no-store'}); + if (!response.ok) throw new Error(`报告数据读取失败(${response.status})`); + data = await response.json(); + } + evidenceIndex = new Map(arr(data.evidenceLedger).map(item=>[item.id,item])); + const exp = data.experienceSolution || {}, roles = data.roleOutputs || {}, facts = data.facts || {}; + const audit = data.meta?.reportValidation; + const auditNotice = data.meta?.reportNotice || (!audit?.ok && audit?.missing?.length ? `审核未通过,报告仍已生成。请优先补齐:${audit.missing.join('、')}` : ''); + const product = facts.product || {}, sales = facts.sales || {}, summary = exp.reportSummary || {}; + const owner = exp.ownerOverview?.cards || roles.executiveDecision?.cards || []; + const plans = exp.newProductPlans?.plans || data.launchPlans?.plans || []; + const title = exp.titleAnalysis || roles.conversion?.title || {}; + const visual = exp.visualCommerce || roles.conversion?.visual || {}; + const detail = exp.detailCommerce || roles.conversion?.detail || {}; + const sku = exp.skuAnalysis || roles.productSupply?.sku || {}; + const customer = exp.customerExperience || roles.consumerInsight || {}; + const mainEvidence = imageEntries(data,'main'); + const coverEvidence = arr(data.evidenceLedger).find(item=>item?.id === summary.coverImageId && item?.type === 'image' && item?.meta?.group === 'main') || mainEvidence[0]; + const orderedMain = coverEvidence ? [coverEvidence,...mainEvidence.filter(item=>item.id!==coverEvidence.id)] : mainEvidence; + const heroSlides = orderedMain.map((item,index)=>`
商品主图
`).join(''); + const heroDots = orderedMain.map((item,index)=>``).join(''); + const heroDeckCenter = (orderedMain.length - 1) / 2; + const heroDeck = orderedMain.map((item,index)=>``).join(''); + const bodies = [ + ['decision','老板速览','先看结论,再进入证据和执行动作',decisionCards(owner)], + ['plan','下一款方向','从证据、反馈和探索方向拆分执行优先级',planCards(plans)], + ['title','标题表达','标题承担点击与首次筛选任务',titleAnalysis(title)], + ['main','主图在卖什么','逐张说明信息任务、视觉信号与下一步动作',mainImages(data,visual)], + ['detail','详情页证明什么','图片与分析绑定,形成连续的页面证明链',detailProof(data,detail)], + ['sku','SKU怎么选','先解决适配,再解决组合与外观偏好',skuAnalysis(sku)], + ['review','消费者实际感受到什么','样本边界、主题频次、体验结论和买家秀共同验证',reviews(data,customer)], + ['verification','产品验证与证据闭环','参数、页面表达、消费者反馈与下一步验证集中核对',supportAnalysis(data)], + ['roles','七角色完整分析','完整保留专业判断,默认折叠避免干扰老板速览',roleOutputs(roles)] + ].filter(([, , ,body])=>hasContent(body)); + const sectionHtml = bodies.map(([id,titleText,deck,body],index)=>section(id,String(index+1).padStart(2,'0'),titleText,deck,body)).join(''); + const navNames = {decision:'老板速览',detail:'详情',review:'评论',verification:'验证闭环',roles:'完整分析'}; + const navLinks = bodies.filter(([id])=>navNames[id]).map(([id])=>`${navNames[id]}`).join(''); + const hasPlans = bodies.some(([id])=>id==='plan'); + const heroStats = [['当前价格',sales.currentPrice],['评论样本',data.baseline?.reviewCount],['数据质量',data.dataQuality?.grade]].filter(([,value])=>hasContent(value)); + document.title = summary.title || product.title || '商品开品分析报告'; + document.getElementById('app').innerHTML = `
${sectionHtml}${hasPlans?'QUICK PRODUCT BRIEF

快速开品方案

产品动作
页面动作
验证指标
':''}
数据边界 本报告仅使用本次采集证据;评论主题可以重叠,未完成采集时不宣称平台全量。
`; + initHeroCarousel(); + initPlanActions(plans); } -main().catch(e => { document.getElementById('app').innerHTML = `

报告加载失败:${esc(e.message)}

`; }); +main().catch(error => { document.getElementById('app').innerHTML = `

报告加载失败

${esc(error.message)}

`; }); diff --git a/server/server.py b/server/server.py index f008687..818bf0c 100644 --- a/server/server.py +++ b/server/server.py @@ -13,7 +13,7 @@ PROMPT_CONFIG=ROOT/'prompts.json' REPORTS=ROOT/'reports'; REPORTS.mkdir(exist_ok=True) MONITOR_DIR=ROOT/'monitor_data'; MONITOR_DIR.mkdir(exist_ok=True) PORT=int(os.getenv('TMALL_AI_PORT','17962')) -SERVER_VERSION='9.2.5' +SERVER_VERSION='9.3.0' MODEL_PROBE={'ok':False,'stage':'startup','error':'尚未检测'} MODEL_PROBE_AT=0 TASKS={} @@ -346,7 +346,12 @@ def prompt_page(): try: saved=json.loads(PROMPT_CONFIG.read_text('utf-8')) except Exception: saved={} items={k:{'name':v.name,'body':saved.get(k,{}).get('body',v.body),'variables':list(v.variables)} for k,v in TEMPLATES.items()} - return '提示词配置

提示词可视化配置

编辑后保存,下一次分析任务生效。

商品变量说明

'+Object.entries(PRODUCT_VARIABLES).map(([k,v])=>`
${html.escape(k)}
${html.escape(v)}
`).join('')+'
'+Object.entries(items).map(([k,v])=>`

${html.escape(k)}

变量:${html.escape(v.variables.join('、')||'自定义')}

`).join('')+'
' + vars_html=''.join(f'
{html.escape(k)}
{html.escape(v)}
' for k,v in PRODUCT_VARIABLES.items()) + template_html=''.join(f'

{html.escape(k)}

变量:{html.escape("、".join(v["variables"]) or "自定义")}

' for k,v in items.items()) + return ('' + '提示词配置' + '

提示词可视化配置

编辑后保存,下一次分析任务生效。

商品变量说明

'+vars_html+'
'+template_html + +'
') class H(BaseHTTPRequestHandler): def log_message(self, fmt, *args): print('[HTTP]',fmt%args) @@ -500,7 +505,7 @@ class H(BaseHTTPRequestHandler): def main(): print(f'Sansong Ecommerce Product Loop V{SERVER_VERSION} running: http://127.0.0.1:{PORT}',flush=True) - print('Dedicated port 17962. Manual single-product collection; no recommendation scraping.',flush=True) + print(f'Dedicated port {PORT}. Manual single-product collection; no recommendation scraping.',flush=True) ThreadingHTTPServer(('127.0.0.1',PORT),H).serve_forever() if __name__=='__main__': main() DYNAMIC_PARAM_FAMILIES = { diff --git a/start-tmall-ai.command b/start-tmall-ai.command index a4cca87..658b9c0 100755 --- a/start-tmall-ai.command +++ b/start-tmall-ai.command @@ -1,7 +1,7 @@ #!/bin/bash ROOT="$(cd "$(dirname "$0")" && pwd)" PORT=17962 -VERSION="9.2.5" +VERSION="9.3.0" SERVER="$ROOT/server" LOG="$SERVER/v9_1_0.log" diff --git a/tests/test_evidence_only_report.py b/tests/test_evidence_only_report.py index b510a52..86aff40 100644 --- a/tests/test_evidence_only_report.py +++ b/tests/test_evidence_only_report.py @@ -47,11 +47,13 @@ class EvidenceOnlyReportTest(unittest.TestCase): 'meta': {}, } - def test_empty_model_result_does_not_generate_fallback_report(self): + def test_empty_model_result_still_generates_reviewable_report(self): result = ensure_experience_solution(self.analysis) self.assertFalse(result['meta']['fallbackReport']) - self.assertFalse(result['meta']['reportReady']) - self.assertIn('不输出兜底报告', result['meta']['reportBlockedReason']) + self.assertTrue(result['meta']['reportReady']) + self.assertFalse(result['meta']['reportAuditPassed']) + self.assertEqual('needs_review', result['meta']['reportQuality']) + self.assertIn('审核未通过', result['meta']['reportNotice']) self.assertEqual('', result['facts']['product']['title']) self.assertEqual(['面料支数', '床单面料材质', '适用床尺寸'], [x['name'] for x in result['facts']['attributes']]) self.assertEqual(2, result['baseline']['reviewCount']) @@ -61,18 +63,20 @@ class EvidenceOnlyReportTest(unittest.TestCase): 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') + html = render(result, 'fixture') + self.assertIn('审核未通过', html) - def test_analysis_runs_without_model_channel(self): + def test_analysis_runs_without_model_channel_uses_evidence_fallback(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('9.3.0', result['meta']['engineVersion']) self.assertEqual('home_textile', result['meta']['industryTemplate']) self.assertFalse(result['meta']['fallbackReport']) - self.assertFalse(result['meta']['reportReady']) + self.assertTrue(result['meta']['reportReady']) + self.assertTrue(result['experienceSolution']['reportSummary']['title']) + self.assertEqual(7, len(result['roleOutputs'])) def test_historical_fallback_json_is_rejected(self): result = ensure_experience_solution({ @@ -198,9 +202,9 @@ class EvidenceOnlyReportTest(unittest.TestCase): self.assertNotIn('决定成交', output) self.assertNotIn('爆款', output) doc = render(result, 'evidence-only') - self.assertIn('老板速览', doc) - self.assertIn('下一款先做什么', doc) - self.assertIn('V9.2.5', doc) + self.assertIn('window.REPORT_DATA=', doc) + self.assertIn('Sansong Product Intelligence V9.3.0', doc) + self.assertIn('', doc) self.assertNotIn('项目负责人', doc) def test_incomplete_final_editor_output_is_repaired_from_real_evidence(self): diff --git a/tests/test_v910_report_regressions.py b/tests/test_v910_report_regressions.py index 5f44915..bcfdca1 100644 --- a/tests/test_v910_report_regressions.py +++ b/tests/test_v910_report_regressions.py @@ -21,8 +21,9 @@ class V910RegressionTest(unittest.TestCase): 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) + self.assertNotIn('结构化SKU 未完整采集',doc) + self.assertNotIn('SKU图片数量不等于SKU数量',doc) + self.assertIn('window.REPORT_DATA=',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'}} diff --git a/tests/test_v920_image_driven_report.py b/tests/test_v920_image_driven_report.py index a840690..c3878ed 100644 --- a/tests/test_v920_image_driven_report.py +++ b/tests/test_v920_image_driven_report.py @@ -33,13 +33,12 @@ class V920ImageDrivenReportTest(unittest.TestCase): 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.assertIn('window.REPORT_DATA=',doc) + self.assertIn('',doc) + self.assertIn('IMG_MAIN_0001',doc) + self.assertIn('IMG_DETAIL_0001',doc) + self.assertNotIn('v92-main-grid',doc) + self.assertNotIn('v925-detail-list',doc) self.assertNotIn('[数据事实]',doc) self.assertNotIn('[分析判断]',doc) self.assertNotIn('[设计建议]',doc) @@ -48,8 +47,9 @@ class V920ImageDrivenReportTest(unittest.TestCase): 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.assertIn('"coverImageId":"IMG_MAIN_0001"',doc) + self.assertNotIn('class="v92-cover"',doc) + self.assertNotIn('cover-ratio',doc) self.assertNotIn('hero-shot',doc) def test_launch_fallback_never_empty_with_real_evidence(self): diff --git a/tests/test_v930_report_audit.py b/tests/test_v930_report_audit.py new file mode 100644 index 0000000..f3e6163 --- /dev/null +++ b/tests/test_v930_report_audit.py @@ -0,0 +1,25 @@ +import json, sys, unittest +from pathlib import Path + +ROOT=Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT/'server')) +from audit_report import audit, has_content + +class V930ReportAuditTest(unittest.TestCase): + def test_zero_and_false_are_real_content(self): + self.assertTrue(has_content(0)) + self.assertTrue(has_content(False)) + self.assertFalse(has_content('未采集')) + self.assertFalse(has_content({})) + + def test_current_reference_report_has_complete_roles_and_evidence(self): + source=ROOT/'server/reports/tmall_1068693788227_20260903_181040_ac150.json' + if not source.exists(): + self.skipTest('reference report is not distributed') + result=audit(json.loads(source.read_text('utf-8'))) + self.assertTrue(result['ok'],result['failures']) + self.assertEqual(7,result['roles']['present']) + self.assertFalse(result['evidence']['unresolved']) + self.assertGreaterEqual(result['sourceCoverage'],12) + +if __name__=='__main__':unittest.main() diff --git a/tests/test_version_consistency_v924.py b/tests/test_version_consistency_v924.py index 09af5d7..470a7b3 100644 --- a/tests/test_version_consistency_v924.py +++ b/tests/test_version_consistency_v924.py @@ -2,7 +2,7 @@ import json, re from pathlib import Path ROOT=Path(__file__).resolve().parents[1] -EXPECTED='9.2.5' +EXPECTED='9.3.0' def test_version_consistency(): assert (ROOT/'VERSION.txt').read_text().strip()==EXPECTED