Release v9.3.0 report updates without committing local API keys.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
喻长发
2026-09-04 16:57:04 +08:00
co-authored by Cursor
parent 19410caf76
commit 8c33d08537
29 changed files with 1111 additions and 131 deletions
+1
View File
@@ -12,6 +12,7 @@ venv/
# generated # generated
*.log *.log
*.zip
server/reports/ server/reports/
server/monitor_data/ server/monitor_data/
+12
View File
@@ -0,0 +1,12 @@
# V9.3.0 — Unified Evidence Report
- 新增七角色完整分析的核心判断和模块化折叠结构。
- 证据从字符编号升级为来源类型、真实内容摘要、图片预览和次要追溯ID。
- 最终报告统一过滤空字段、空卡片、空章节、缺失图片和占位文案。
- 评论增加主题频次可视化、问题洞察、影响和产品动作。
- 买家秀按评论合并,避免多图被误算为多人反馈。
- 首焦改为无装饰商品主图牌组;所有图片完整显示且不裁切。
- 详情卡统一图片与动作对齐,奇数最后一张使用横向通栏。
- 新品方向增加参考色标签、底部对齐的立即生成按钮和任务弹窗。
- JSON 报告与静态重建报告统一复用同一个渲染器。
- 同步扩展、服务、分析引擎和内置采集扩展版本至 9.3.0。
+35
View File
@@ -0,0 +1,35 @@
# Prompt Engineering V9.3.0
## 目标
让七角色从同一证据账本生产可决策、可执行、可追溯的商品开品报告。模型负责提炼内容,渲染器负责信息层级,不向用户展示推理过程。
## 七角色协作
1. 商品策略、电商转化、消费者洞察、产品/供应链、视觉设计分别在职责范围内输出。
2. 商业决策角色只综合前序证据,不创造新事实。
3. 总编审核统一检查事实、证据、对象、图片分类、冲突、重复和字数。
4. 每条结论、风险和动作必须引用 ACTUAL_EVIDENCE。
## 输出规则
- 统一使用“核心判断—关键发现—执行动作—风险/验证”。
- 图片先提取可核验的材质、规格、权益、功能、构图和场景信息,再形成动作。
- 评论先按程序统计计算频次,再由模型解释体验差异、影响和产品动作。
- 新品方向区分证据驱动、反馈驱动和探索性方向。
- 没有真实内容的可选字段直接省略。
- 禁止输出空字符串、空数组、空对象或“暂无、未采集、待补充、无”等占位文案。
- evidenceIds 只用于机器追溯,不作为用户可见说明。
- 未采集经营数据时,不推断市场规模、转化率、利润或竞品表现。
## 文本长度
- 核心判断与专业意见不超过55字。
- 老板结论不超过28字,补充说明不超过28字。
- 主图和详情标签每项不超过12字,动作不超过22字。
- 评论洞察不超过35字。
- 新品方案产品/页面动作不超过35字。
## 最终可见性
最终页面只展示有效内容。真实数值 `0`、布尔状态、评论采集停止原因和样本边界属于有效事实,必须保留。
+46 -24
View File
@@ -1,32 +1,54 @@
# 三笙 · 商品开品分析报告引擎 V9.2.5 # 三笙 · 商品开品分析工具 V9.3.0
V9.2.5 是“大图大字决策报告”最终版:老板先看结论,专业用户再看图片证据。大图讲事实,大字讲判断,短句讲动作。 V9.3.0 将采集、七角色分析、证据追溯和最终报告统一为一条输出链。报告面向不熟悉数据工具的商品、运营、设计、供应链和老板用户:先看结论,再按需展开专业分析与证据。
## 最终报告结构 ## 使用流程
1. 老板速览:为什么能卖 / 值得继承 / 下一款优先 1. 在淘宝或天猫商品页打开扩展。
2. 下一款方向:证据驱动优化 / 反馈驱动升级 / 探索性方向 2. 分析前选择评论样本:50、100、500、全部或自定义。
3. 标题表达:已表达 / 建议强化 3. 开始采集;中途停止时保留当前进度,也可直接基于现有数据分析。
4. 主图在卖什么:完整图片 + 图片信息标签 + 1条动作 4. 等待七角色分析与总编审核完成。
5. 详情页证明什么:代表图 + 图片信息标签 + 1条动作 5. 在报告中查看老板速览、下一款方向、页面转化、消费者洞察和完整角色分析。
6. SKU怎么选:SKU图 + 选择维度与顺序
7. 消费者实际感受到什么:买家秀 + 聚合体验信号
## V9.2.5 关键变化 ## V9.3.0 输出结构
- Prompt 从“解释图片”改为“提炼图片上的商业信息”。 1. 老板速览:为什么能卖、值得继承、下一款优先。
- 主图/详情新增 `visualSignals`,优先输出图片可核验的卖点与参数标签。 2. 下一款方向:证据驱动优化、反馈驱动升级、探索性方向,并可立即生成开品任务。
- 同一结论默认只展示一次,详细推理留在 evidenceDetail。 3. 标题表达:当前表达、信息缺口和强化动作。
- 主图 × 详情 × 评论承接矩阵默认折叠。 4. 主图分析:完整原图、信息任务、真实视觉信号和下一步动作。
- 商品价值不再单独重复一章,吸收到老板速览。 5. 详情分析:代表图片、证明内容、页面承接和完整详情证据。
- 新品方案改为横向决策卡,只保留为什么做 / 产品动作 / 页面动作,并标记来源等级。 6. SKU分析:适配维度、组合顺序和供应链动作。
- 图片统一 `object-fit: contain`,封面、主图、详情、SKU、买家秀、方案参考图均优先完整展示,不裁切商品信息。 7. 消费者洞察:评论主题频次、体验差异、问题影响、产品动作和买家秀验证。
- SKU图片数量继续与SKU结构数量分离。 8. 七角色完整分析:核心判断、关键发现、执行动作、风险与验证。
- 保留 V9.2.3 已修复的图片 HTML 标签、图片缓存与旧采集格式兼容能力。
详细规则见 `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/<taskId>.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`。
+42
View File
@@ -0,0 +1,42 @@
# V9.3.0 最终报告输出标准
## 阅读顺序
老板速览 → 下一款方向 → 标题 → 主图 → 详情 → SKU → 消费者洞察 → 七角色完整分析。
只有存在有效内容的章节才进入页面,章节编号按实际出现顺序生成。
## 图片规则
- 首焦使用商品主图牌组,图片完整展示。
- 所有商品图、详情图和买家秀统一使用 `contain`。
- 不使用裁切、模糊背景、装饰底图、边框、圆角、阴影、图片角标和可见序号。
- 图片缺失时不渲染空白图片容器。
- 详情代表图按同排统一高度排列;奇数最后一张使用通栏布局。
## 信息层级
- 每章先显示核心判断,再显示事实、影响和动作。
- 下一款方案卡的操作按钮同排底部对齐。
- 青绿到浅蓝渐变只用于标签与关键操作,不覆盖商品图片。
- 评论总结必须包含主题频次可视化、问题洞察和对应动作。
## 七角色
- 每个角色先显示一条核心判断。
- 专业内容按分析模块折叠。
- 每项依次展示结论、商业意义、动作、风险/验证和置信度。
- 评论原话、大批量基础数据和证据详情默认折叠。
## 证据
- 用户首先看到来源类型和真实内容摘要。
- 图片证据显示完整缩略图。
- evidenceId 缩小显示,仅用于核验与追溯。
- 找不到实际证据内容的引用不进入最终页面。
## 空内容规则
- 不展示空字符串、空数组、空对象、缺失图片或占位文案。
- 子项为空则隐藏子项;卡片为空则隐藏卡片;章节为空则隐藏章节和导航入口。
- `0`、`false`、采集停止原因和样本边界属于有效事实,不得误删。
+1 -1
View File
@@ -1 +1 @@
9.2.5 9.3.0
@@ -1,4 +1,4 @@
# 三笙电商商品分析报告插件 v9.2.5 # 三笙电商商品分析报告插件 v9.3.0
用于把淘宝/天猫当前商品采集结果转成跨类目商品分析、新品设计和 AI 视觉报告。 用于把淘宝/天猫当前商品采集结果转成跨类目商品分析、新品设计和 AI 视觉报告。
@@ -1,3 +1,3 @@
# 三笙电商商品详情采集扩展 v9.2.5 # 三笙电商商品详情采集扩展 v9.3.0
跨类目采集商品标题、类目候选、主图、详情、SKU、动态参数、评论和问答。家纺仅为分析模板分支,不限制采集字段。 跨类目采集商品标题、类目候选、主图、详情、SKU、动态参数、评论和问答。家纺仅为分析模板分支,不限制采集字段。
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "三笙商品详情采集", "name": "三笙商品详情采集",
"version": "9.2.5", "version": "9.3.0",
"description": "跨类目解析商品标题、主图、详情、活动、SKU、动态参数、评论与问答。", "description": "跨类目解析商品标题、主图、详情、活动、SKU、动态参数、评论与问答。",
"permissions": ["activeTab", "scripting", "tabs", "alarms", "cookies"], "permissions": ["activeTab", "scripting", "tabs", "alarms", "cookies"],
"host_permissions": [ "host_permissions": [
+1 -1
View File
@@ -1,4 +1,4 @@
# 三笙电商商品详情采集扩展 v9.2.5 # 三笙电商商品详情采集扩展 v9.3.0
跨类目采集标题、类目候选、主图、详情图、SKU 图、评论图、动态参数、变体、促销、评论与问答。字段由页面结构和文本自动发现,不绑定家纺 BOM。 跨类目采集标题、类目候选、主图、详情图、SKU 图、评论图、动态参数、变体、促销、评论与问答。字段由页面结构和文本自动发现,不绑定家纺 BOM。
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "三笙商品详情采集", "name": "三笙商品详情采集",
"version": "9.2.5", "version": "9.3.0",
"description": "跨类目解析商品标题、主图、详情、活动、SKU、动态参数、评论与问答。", "description": "跨类目解析商品标题、主图、详情、活动、SKU、动态参数、评论与问答。",
"permissions": ["activeTab", "scripting", "tabs", "alarms", "cookies"], "permissions": ["activeTab", "scripting", "tabs", "alarms", "cookies"],
"host_permissions": [ "host_permissions": [
+9 -9
View File
@@ -56,11 +56,11 @@ chrome.runtime.onMessage.addListener((m,s,send)=>{
return true; 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==='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; } 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); const tab=await chrome.tabs.get(tabId);
if(!isProduct(tab.url)) throw new Error('当前标签页不是天猫/淘宝商品详情页'); if(!isProduct(tab.url)) throw new Error('当前标签页不是天猫/淘宝商品详情页');
const h=await health(); 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?.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(pageState?.verification){await setStatus(tabId,{state:'verification_required',message:'页面出现淘宝安全验证,请手动完成后刷新重试。',progress:0});throw new Error('页面出现淘宝安全验证,请手动完成验证后重试')}
if(!isProduct(pageState?.url)){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:[]};}}); 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(); 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)} 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; const collectorResult=await collectorPromise;
if(collectorError)throw new Error('页面采集脚本执行失败:'+String(collectorError?.message||collectorError)); 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 noReviewData=!raw.reviews?.length&&!raw.collection?.networkReviewResponses;
const folded=raw.collection?.foldedDefaultReviewCount?`;平台另折叠 ${raw.collection.foldedDefaultReviewCount} 条默认好评,未提供逐条内容`:''; 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}。已停止低效空转,可继续采集。`; 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}}; 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}); 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}}; 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 delay=ms=>new Promise(r=>setTimeout(r,ms));
const text=(el)=>el?.innerText?.trim()||el?.textContent?.trim()||''; const text=(el)=>el?.innerText?.trim()||el?.textContent?.trim()||'';
const clean=s=>(s||'').replace(/\s+/g,' ').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 bodyText=clean(bodyClone?.innerText||document.body?.innerText||'');
const captured=[]; const captured=[];
const reviews=[]; const questions=[]; 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 reviewNetMeta={totals:[],hasMoreFalse:false,lastPageFlags:[],pageHints:[],networkReviewObjects:0,networkResponses:0};
const reviewStartedAt=Date.now(); const reviewStartedAt=Date.now();
const processedCaptureKeys=new Set(); 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){}} 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}); 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); 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(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} 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]||''}, sales:{currentPrice:priceValue,sold:soldValue,ranking:rankMatch?.[1]||''},
sku:skuTexts.map(name=>({name})),attributes:attrs,promotions:promoTexts.map(text=>({text})), 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}}, 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}
}; };
} }
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "三笙 · 电商商品分析闭环 V9.2.5", "name": "三笙 · 电商商品分析闭环 V9.3.0",
"version": "9.2.5", "version": "9.3.0",
"description": "跨类目电商商品分析:动态参数与SKU采集、图文分源、评论证据、开品与AI视觉方案。", "description": "跨类目电商商品分析:动态参数与SKU采集、图文分源、评论证据、开品与AI视觉方案。",
"permissions": [ "permissions": [
"scripting", "scripting",
@@ -20,7 +20,7 @@
}, },
"action": { "action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_title": "三笙 · 电商商品分析闭环 V9.2.5" "default_title": "三笙 · 电商商品分析闭环 V9.3.0"
}, },
"content_scripts": [ "content_scripts": [
{ {
+8 -10
View File
@@ -16,6 +16,13 @@ body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemF
<h1>三笙 · 电商商品分析引擎</h1> <h1>三笙 · 电商商品分析引擎</h1>
<div class="sub">采集当前商品后自动识别类目和分析维度,生成标题、主图、详情、SKU、评论、新品设计与 AI 视觉报告。</div> <div class="sub">采集当前商品后自动识别类目和分析维度,生成标题、主图、详情、SKU、评论、新品设计与 AI 视觉报告。</div>
<div class="card"><div id="server" class="state">检查本地服务…</div><div id="status" class="sub">等待当前商品页</div><div class="bar"><i id="bar"></i></div></div> <div class="card"><div id="server" class="state">检查本地服务…</div><div id="status" class="sub">等待当前商品页</div><div class="bar"><i id="bar"></i></div></div>
<div class="card sample-card"><div class="state">采集前先选择评论样本</div><div class="sub">只限制评论样本数量;商品信息、参数、图片、问答和完整分析内容都会保留。</div><div class="mode-row">
<button data-sample="50" class="mode">50 条<small>快速判断</small></button>
<button data-sample="100" class="mode">100 条<small>常规小样本</small></button>
<button data-sample="500" class="mode active">500 条<small>推荐</small></button>
<button data-sample="all" class="mode">全部<small>采到可访问末页</small></button>
<button data-sample="custom" class="mode">自定义<small>输入数量</small></button>
</div><div id="customSampleWrap" style="display:none;margin-top:10px"><input id="customSample" type="number" min="1" max="10000" placeholder="输入评论条数(1-10000)"></div><div class="tiny">采集开始后不能切换样本档位;如中途停止,可按原档位继续。</div></div>
<button class="btn" id="run">开始深度采集</button> <button class="btn" id="run">开始深度采集</button>
<button class="btn stop" id="stop" style="display:none">终止采集并保存进度</button> <button class="btn stop" id="stop" style="display:none">终止采集并保存进度</button>
@@ -27,17 +34,8 @@ body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemF
<div class="param-miss" id="paramMissing">缺失项:—</div> <div class="param-miss" id="paramMissing">缺失项:—</div>
</div> </div>
<div id="reviewLive" class="live" style="display:none"><strong>评论实时采集</strong><div class="live-grid"><span>去重:<b id="liveCount">0</b></span><span>网络对象:<b id="liveNet">0</b></span><span>当前轮次:<b id="liveRound">0</b></span><span>无新增:<b id="liveStagnant">0</b></span><span>模式:<b id="liveMode">—</b></span><span>耗时:<b id="liveElapsed">0s</b></span></div><div id="livePreview" class="live-preview">等待评论数据…</div></div> <div id="reviewLive" class="live" style="display:none"><strong>评论实时采集</strong><div class="live-grid"><span>去重:<b id="liveCount">0</b></span><span>网络对象:<b id="liveNet">0</b></span><span>当前轮次:<b id="liveRound">0</b></span><span>无新增:<b id="liveStagnant">0</b></span><span>模式:<b id="liveMode">—</b></span><span>耗时:<b id="liveElapsed">0s</b></span></div><div id="livePreview" class="live-preview">等待评论数据…</div></div>
<div class="card"><div class="state">分析入口</div><div class="sub">已有采集数据时,可直接生成报告;不会清空当前采集进度。</div><button id="analyzeNow" class="btn analysis-now">基于当前数据开始分析</button></div>
<button class="btn secondary" id="open">打开本地服务</button> <button class="btn secondary" id="open">打开本地服务</button>
<div class="tiny">不会采集推荐商品。评论会自动去重、分类和抽样;采集过程中可随时停止并继续。</div> <div class="tiny">不会采集推荐商品。评论会自动去重、分类和抽样;采集过程中可随时停止并继续。</div>
<div class="card">
<div class="state">分析方式</div>
<div class="mode-row">
<button id="modeQuick" class="mode">快速看结论<small>最多 300 条,约 1 分钟</small></button>
<button id="modeStandard" class="mode active">标准采集<small>最多 800 条,适合大多数商品</small></button>
<button id="modeFull" class="mode">完整采集<small>尽可能采到末页,耗时较长</small></button>
</div>
<button id="analyzeNow" class="btn analysis-now">先看当前结论</button>
<div class="tiny">系统优先保留差评、带图、最新评论和不同主题评论。</div>
</div>
<script src="popup.js"></script> <script src="popup.js"></script>
</body></html> </body></html>
+9 -5
View File
@@ -1,5 +1,5 @@
const SERVER='http://127.0.0.1:17962'; 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; 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 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'); 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)=>`<div>${i+1}. ${String(x).replace(/[<>&]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]))}</div>`).join('')||'等待评论数据…'}}else{status.textContent='已识别当前商品,等待手动开始。';bar.style.width='0%';runBtn.textContent='开始深度采集';stopBtn.style.display='none';reviewLive.style.display='none'} 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)=>`<div>${i+1}. ${String(x).replace(/[<>&]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]))}</div>`).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()=>{ runBtn.onclick=async()=>{
if(!backendCompatible){status.textContent='插件与本地后端版本不一致,请先双击本包“start-tmall-ai.command”';return} 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('open').onclick=()=>chrome.tabs.create({url:SERVER+'/'});
document.getElementById('importMonitor')?.addEventListener('click',()=>document.getElementById('monitorFile')?.click()); document.getElementById('importMonitor')?.addEventListener('click',()=>document.getElementById('monitorFile')?.click());
document.getElementById('monitorFile')?.addEventListener('change',async e=>{ document.getElementById('monitorFile')?.addEventListener('change',async e=>{
@@ -88,12 +93,11 @@ let analysisMode='standard';
function setMode(mode){ function setMode(mode){
analysisMode=mode; analysisMode=mode;
collectionMode=mode;
modeQuick?.classList.toggle('active',mode==='quick'); modeQuick?.classList.toggle('active',mode==='quick');
modeStandard?.classList.toggle('active',mode==='standard'); modeStandard?.classList.toggle('active',mode==='standard');
modeFull?.classList.toggle('active',mode==='full'); modeFull?.classList.toggle('active',mode==='full');
if(analyzeNow){ if(analyzeNow){
analyzeNow.textContent=mode==='quick'?'先看当前结论':mode==='standard'?'采集并生成报告':'完整采集后自动分析'; analyzeNow.textContent='基于当前数据开始分析';
analyzeNow.disabled=false; analyzeNow.disabled=false;
} }
} }
+6 -2
View File
@@ -2,7 +2,7 @@ import json, os, re, ssl, time, urllib.request, urllib.error
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).resolve().parent ROOT = Path(__file__).resolve().parent
SERVER_VERSION = '9.2.5' SERVER_VERSION = '9.3.0'
def load_config(): def load_config():
cfg = {} cfg = {}
@@ -64,7 +64,7 @@ def _ssl_context(c):
return ctx return ctx
def _headers(c): 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',{})) h.update(c.get('extra_headers',{}))
return h return h
@@ -145,4 +145,8 @@ def chat_json(system, user, images=None, max_tokens=5000):
last=RuntimeError(f'模型接口调用失败: {e}') last=RuntimeError(f'模型接口调用失败: {e}')
if attempt<c['retries']: if attempt<c['retries']:
time.sleep(min(1.5*(attempt+1),4)) time.sleep(min(1.5*(attempt+1),4))
# 电商详情页偶有 1-2px 占位图或超长切片;部分多模态模型会拒绝整批图片。
# 证据文本仍在 user 中,因此自动改用纯文本证据重试,不能让单张坏图吞掉整份报告。
if images and last and 'image length and width' in str(last).lower():
return chat_json(system, user, images=None, max_tokens=max_tokens)
raise last or RuntimeError('模型接口调用失败') raise last or RuntimeError('模型接口调用失败')
+87 -8
View File
@@ -3,10 +3,27 @@ from collections import Counter, defaultdict
from ai_client import chat_json, configured from ai_client import chat_json, configured
from prompt_templates import build_system_prompt, build_task_prompt from prompt_templates import build_system_prompt, build_task_prompt
ENGINE_VERSION='9.2.5' ENGINE_VERSION='9.3.0'
ROLE_CONTRACT_VERSION='1.1'
ROLE_CONTRACT={
'productStrategy': {'responsibility':'判断商品卖什么、为什么值得参考、下一款继承什么','outputs':['商品定位','核心产品资产','核心卖点结构','目标使用场景','值得继承的产品逻辑','需要调整的方向','商品与同类差异点','下一款策略假设']},
'conversion': {'responsibility':'判断用户为什么点击、继续看并愿意购买','outputs':['标题表达','主图任务分工','主图核心信息','详情页信息链','卖点承接','信任证明','页面信息缺口','标题/主图/详情优化动作']},
'consumerInsight': {'responsibility':'从评论和买家秀提炼真实使用体验并转成产品需求','outputs':['稳定正向体验','体验差异','高频顾虑','购前关注','售后问题','消费者语言','真实使用场景','买家秀验证','下一款必须解决的用户需求']},
'productSupply': {'responsibility':'判断产品怎么改、哪些能落地、哪些必须验证','outputs':['材质','工艺','支数/克重','尺寸','套件','SKU结构','功能参数','规格适配','产品调整项','生产风险','打样验证项','首批验证项','不可直接下结论的产品假设']},
'visualDesign': {'responsibility':'拆解视觉资产并转成下一款设计和生图方向','outputs':['风格','颜色','花型','元素','材质视觉','构图','拍摄方式','场景','光线','画面氛围','视觉锤','值得继承的视觉资产','下一款视觉变化方向','AI生图参考要求']},
'executiveDecision': {'responsibility':'把专业判断压缩成老板可直接决策的结论','outputs':['为什么值得做','为什么能卖','最强资产','最大机会','最需要关注的问题','下一款第一优先级','建议保留什么','建议升级什么','探索方向','执行优先顺序']},
'editorReview': {'responsibility':'对外发布前统一审核、压缩、纠错和去重','outputs':['事实一致性','证据支持','经营数据边界','产品对象与卖点匹配','图片分类','章节一致性','重复删除','措辞统一','字数压缩','最终结构']}
}
SYS='''你是“三笙电商商品开品分析引擎”,面向两类读者:老板/决策者与专业商品开发用户。你的工作不是展示AI分析过程,而是把真实商品证据压缩成可决策、可复用、可开品的专业报告。 SYS='''你是“三笙电商商品开品分析引擎”,面向两类读者:老板/决策者与专业商品开发用户。你的工作不是展示AI分析过程,而是把真实商品证据压缩成可决策、可复用、可开品的专业报告。
角色协作规则(内容生产链):
1. 商品策略、电商转化、消费者洞察、产品/供应链、视觉设计分别只判断自己的职责范围;可并行,但不得互相越权。
2. 商业决策/老板角色只综合前序角色的证据,不重新发明事实;输出为什么值得做、为什么能卖、第一优先级和执行顺序。
3. 总编审核角色是发布前质量门,不是新的观点来源;必须删除重复、纠正错配、标记假设并保留证据边界。
4. 每个角色的结论、风险、动作和假设都必须能回溯到 ACTUAL_EVIDENCE;没有证据时标记 hypothesis 或 unknown。
最终阅读逻辑: 最终阅读逻辑:
1. 老板先看:为什么能卖、哪些资产值得继承、下一款优先做什么。 1. 老板先看:为什么能卖、哪些资产值得继承、下一款优先做什么。
2. 专业用户再看:标题怎么表达、主图在卖什么、详情证明什么、SKU怎么选、消费者实际感受到什么。 2. 专业用户再看:标题怎么表达、主图在卖什么、详情证明什么、SKU怎么选、消费者实际感受到什么。
@@ -27,9 +44,10 @@ SYS='''你是“三笙电商商品开品分析引擎”,面向两类读者:
12. 图片原图完整展示优先于铺满容器;报告渲染必须使用 contain,不以裁切换取整齐。 12. 图片原图完整展示优先于铺满容器;报告渲染必须使用 contain,不以裁切换取整齐。
13. 默认可见层不使用小卡片堆叠思维:每屏只突出1个主结论、1-2张大图或一组大规格图片。 13. 默认可见层不使用小卡片堆叠思维:每屏只突出1个主结论、1-2张大图或一组大规格图片。
14. 图片上的文字/卖点是核心证据,visualSignals必须优先提取具体词,如规格、材质、权益、功能,而不是抽象评价。 14. 图片上的文字/卖点是核心证据,visualSignals必须优先提取具体词,如规格、材质、权益、功能,而不是抽象评价。
15. 不输出私有思维过程。严格输出 JSON,不要 Markdown 或 HTML。''' 15. 不输出私有思维过程。严格输出 JSON,不要 Markdown 或 HTML。
16. 没有真实内容的可选字段直接省略,不得输出空字符串、空数组、空对象或“暂无/未采集/待补充”等占位文案。'''
STYLE_RULES='''V9.2.5 输出压缩规则: STYLE_RULES='''V9.3.0 输出压缩与可见性规则:
- reportSummary.verdict / professionalOpinion:<=55字。 - reportSummary.verdict / professionalOpinion:<=55字。
- 老板速览 headline:<=28字;note:<=28字。 - 老板速览 headline:<=28字;note:<=28字。
- 标题 currentExpression/reinforce:短词组,不写完整分析段落。 - 标题 currentExpression/reinforce:短词组,不写完整分析段落。
@@ -41,9 +59,14 @@ STYLE_RULES='''V9.2.5 输出压缩规则:
- 禁止通过Renderer用省略号硬截断来制造短文;模型必须直接生成短文本。 - 禁止通过Renderer用省略号硬截断来制造短文;模型必须直接生成短文本。
- 详细构图/OCR/颜色/材质推理放 evidenceDetail,默认报告不展示。 - 详细构图/OCR/颜色/材质推理放 evidenceDetail,默认报告不展示。
- 同一事实不在同一章节重复三次。 - 同一事实不在同一章节重复三次。
- 没有真实内容的字段直接省略;禁止为了补结构输出“暂无、未采集、待补充、无”。
- 空字符串、空数组和空对象不得进入用户可见结论。数值0和明确的采集状态属于有效内容,可保留。
- evidenceIds 只用于追溯;用户可见层应由渲染器显示“来源类型+真实内容摘要”,不得让用户只看到字符编号。
''' '''
FINAL_REPORT_PROMPT='''任务G|V9.2.5 老板与专业用户双层报告编排器。你只能压缩和映射前序分析,不重新发明事实。 FINAL_REPORT_PROMPT='''任务G|V9.3.0 七角色总编报告编排器。你只能压缩和映射前序分析,不重新发明事实。
输入中的 roleOutputs 来自七个职责角色。编排要求:商品策略进入“为什么能卖/值得继承”;电商转化进入标题、主图、详情;消费者洞察进入真实体验;产品/供应链进入SKU、参数与验证;视觉设计进入视觉资产与下一款视觉变化;老板决策压缩为首屏;总编审核负责删除角色间重复和冲突。角色输出不是额外证据,所有结论仍必须保留 evidenceIds。
最终顺序:老板速览 → 下一款方向 → 标题 → 主图 → 详情 → SKU → 消费者反馈。商品价值不要单独重复成一整章,吸收到老板速览。 最终顺序:老板速览 → 下一款方向 → 标题 → 主图 → 详情 → SKU → 消费者反馈。商品价值不要单独重复成一整章,吸收到老板速览。
@@ -73,13 +96,16 @@ FINAL_REPORT_PROMPT='''任务G|V9.2.5 老板与专业用户双层报告编排
- newProductPlans.plans>=1;第三类创新若不是直接证据结论必须标 sourceType=探索性方向。 - newProductPlans.plans>=1;第三类创新若不是直接证据结论必须标 sourceType=探索性方向。
- productExperience.parameterFacts>=2、productExperience.gates>=2、validationLoop.rows>=3,继续用于工程证据校验,但默认报告不展示。 - 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。 强制最小结构:reportSummary.title/verdict非空;ownerOverview.cards>=3;productExperience.parameterFacts>=2;productExperience.gates>=2;newProductPlans.plans>=1;validationLoop.rows>=3。
如果 customerExperience 有评论证据,优先保留 experienceSignals;无评论时允许为空。 如果 customerExperience 有评论证据,优先保留 experienceSignals;无评论时允许为空。
所有 evidenceIds 只能来自 ACTUAL_EVIDENCE。不要输出 Markdown、解释或包装层。 所有 evidenceIds 只能来自 ACTUAL_EVIDENCE。不要输出 Markdown、解释或包装层。
可选字段没有真实内容时直接省略,不得填入“暂无、未采集、待补充”或空容器。
''' '''
TOPICS={ TOPICS={
@@ -1005,10 +1031,15 @@ def ensure_experience_solution(analysis):
validation=core_solution_diagnostics(exp) validation=core_solution_diagnostics(exp)
analysis['meta']['reportValidation']=validation analysis['meta']['reportValidation']=validation
if not validation['ok']: 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 return analysis
analysis['meta']['reportReady']=True analysis['meta']['reportReady']=True
analysis['meta']['reportAuditPassed']=True
analysis['meta']['reportQuality']='passed'
analysis['meta'].pop('reportBlockedReason',None) analysis['meta'].pop('reportBlockedReason',None)
customer=exp.setdefault('customerExperience',{}) 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))}] 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) 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} raw=normalize(raw); base=baseline(raw); ledger=evidence_ledger(raw); valid={x['id'] for x in ledger}
route=detect_category(raw) 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) progress('evidence','processing',28)
limitations=[] limitations=[]
@@ -1326,6 +1357,15 @@ visualSignals必须来自图片可见文字、商品、场景或已确认参数
'detailDecision':out.get('detailDecision',{}), 'consumerResearch':out.get('consumerResearch',{}), 'detailDecision':out.get('detailDecision',{}), 'consumerResearch':out.get('consumerResearch',{}),
'questionResearch':out.get('questionResearch',{}), 'commercialDecision':out.get('commercialDecision',{}), 'questionResearch':out.get('questionResearch',{}), 'commercialDecision':out.get('commercialDecision',{}),
'launchPlans':out.get('launchPlans',{}), '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) 'evidence':compact_evidence(ledger,{'monitoring','product','sales','sku','attribute','promotion','review','question','image'},650)
} }
prompt=FINAL_REPORT_PROMPT prompt=FINAL_REPORT_PROMPT
@@ -1335,7 +1375,39 @@ visualSignals必须来自图片可见文字、商品、场景或已确认参数
out['experienceSolution']=require_refs(z,valid) out['experienceSolution']=require_refs(z,valid)
else: else:
out['experienceSolution']={} 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) 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'): if use_ai and not result['meta'].get('reportReady'):
first_validation=result['meta'].get('reportValidation') or core_solution_diagnostics(result.get('experienceSolution')) first_validation=result['meta'].get('reportValidation') or core_solution_diagnostics(result.get('experienceSolution'))
progress('editor','processing repair',99) progress('editor','processing repair',99)
@@ -1358,5 +1430,12 @@ visualSignals必须来自图片可见文字、商品、场景或已确认参数
out['meta']['evidenceStructuralRepairApplied']=True out['meta']['evidenceStructuralRepairApplied']=True
out['meta']['preStructuralRepairValidation']=before out['meta']['preStructuralRepairValidation']=before
result=ensure_experience_solution(out) 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) progress('editor','done' if result['meta'].get('reportReady') else 'failed',99)
return result return result
+118
View File
@@ -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 <report.json>')
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()
+2
View File
@@ -15,6 +15,8 @@ PRODUCT_VARIABLES = {
"questions": "问大家/购买问答:问题、回答及 QA evidenceId", "questions": "问大家/购买问答:问题、回答及 QA evidenceId",
"programStats": "程序统计:评论数量、图片数量、参数数量等可计算指标", "programStats": "程序统计:评论数量、图片数量、参数数量等可计算指标",
"evidence": "统一证据账本;模型只能引用其中的 evidenceId", "evidence": "统一证据账本;模型只能引用其中的 evidenceId",
"roleOutputs": "七角色中间结论:商品策略、电商转化、消费者洞察、产品/供应链、视觉设计、老板决策与总编审核;只能交叉校验和编排,不得当作无证据事实",
"renderingPolicy": "最终可见性规则:只展示真实非空内容;证据以来源类型和内容摘要呈现,evidenceId仅用于追溯",
} }
@dataclass(frozen=True) @dataclass(frozen=True)
+4 -14
View File
@@ -1,4 +1,5 @@
import html import html
import json
import re import re
from analysis import ensure_experience_solution from analysis import ensure_experience_solution
@@ -669,18 +670,7 @@ def _plans_v92(exp,a,idx):
def render(a, task_id): def render(a, task_id):
a=ensure_experience_solution(a) a=ensure_experience_solution(a)
if not (a.get('meta') or {}).get('reportReady'): raise ValueError((a.get('meta') or {}).get('reportBlockedReason') or '本次没有可输出的证据结论') 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) raw=a.get('facts') or {}; product=raw.get('product') or {}; exp=a.get('experienceSolution') or {}; summary=exp.get('reportSummary') or {}
summary=exp.get('reportSummary') or {}
title=clean_display(summary.get('title') or product.get('title') or '商品分析报告',60) title=clean_display(summary.get('title') or product.get('title') or '商品分析报告',60)
verdict=first_text(summary.get('verdict'),limit=58) or '基于当前商品页面、图片、参数与评论证据形成开品建议。' payload=json.dumps(a,ensure_ascii=False,separators=(',',':')).replace('</','<\\/').replace('\u2028','\\u2028').replace('\u2029','\\u2029')
cover_id=safe(summary.get('coverImageId')); cover_e=idx.get(cover_id) if cover_id else None return '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="generator" content="Sansong Product Intelligence V9.3.0"><title>'+H(title)+'</title><link rel="stylesheet" href="/report.css"></head><body><main id="app" class="report-app"></main><script>window.REPORT_DATA='+payload+';</script><script src="/report.js"></script></body></html>'
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='<nav class="v92-nav"><div class="wrap"><div class="brand">三笙 · 商品开品分析</div><div class="v92-links"><a href="#decision">老板速览</a><a href="#plan">新品方向</a><a href="#title">标题</a><a href="#main">主图</a><a href="#detail">详情</a><a href="#sku">SKU</a><a href="#review">评价</a></div></div></nav>'
cover=_visual_figure(cover_e,'商品封面图','cover-ratio') if cover_e else '<div class="v92-cover-empty">主图未采集</div>'
hero=f'<header class="v92-hero"><div class="wrap v92-hero-grid"><div><div class="v92-kicker">PRODUCT DECISION REPORT · BIG VISUAL · V9.2.5</div><h1>{H(title)}</h1><p class="v92-meta">{H(meta_line)}</p><div class="v925-hero-claim">{H(clean_display(verdict,45))}</div></div><div class="v92-cover">{cover}</div></div></header>'
doc='<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>'+H(title)+'</title><link rel="stylesheet" href="/report.css"></head><body>'+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'<footer class="v92-footer"><div class="wrap">Task {H(task_id)} · 三笙商品开品分析报告引擎 V9.2.5 · 未采集经营指标不作推断</div></footer><script>document.addEventListener("error",e=>{{if(e.target&&e.target.tagName==="IMG"){{const f=e.target.closest("figure.visual");if(f)f.classList.add("failed");e.target.style.display="none";}}}},true);</script></body></html>'
return doc
+316 -3
View File
@@ -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)}} @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 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} .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-evidence-note{font-size:9px}
.v92-insights article p{display:none} .v92-insights article p{display:none}
.v92-summary3 p:empty{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. 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} :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} .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: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}} @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 */ /* JSON report view */
.report-app .wrap{max-width:1180px;margin:0 auto;padding:0 24px} .report-app .wrap{max-width:1180px;margin:0 auto;padding:0 24px}
.report-section{padding:42px 0;border-bottom:1px solid #e8ece7} .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 .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 .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} .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}
+345 -20
View File
@@ -1,26 +1,351 @@
/* 独立报告视图:页面只负责渲染 JSON,不在 Python 中拼接 HTML。 */ /* 独立报告视图:以分析 JSON 为唯一数据源,完整展示分析与证据。 */
const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); const esc = value => String(value ?? '').replace(/[&<>"']/g, char => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
const arr = v => Array.isArray(v) ? v : []; const arr = value => Array.isArray(value) ? value : [];
const text = (v, keys=['value','label','headline','note','insight','action','name']) => { 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(' · ');
if (Array.isArray(v)) return v.map(x => text(x, keys)).filter(Boolean).join(' · '); const percent = value => `${(Number(value || 0) * 100).toFixed(1).replace('.0','')}%`;
if (typeof v === 'string' || typeof v === 'number') return String(v); let evidenceIndex = new Map();
if (v && typeof v === 'object') for (const k of keys) if (v[k] != null && v[k] !== '') return text(v[k], keys); const EMPTY_COPY = /^(暂无|无|未采集|未评级|尚未结构化|图片证据未采集|评论原文未采集|日期未采集)$/;
return ''; 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 => `<article class="card"><h3>${esc(text(x,['label','name','type']))}</h3>${fields.map(k=>text(x,[k])).filter(Boolean).map(v=>`<p>${esc(v)}</p>`).join('')}</article>`).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) { function imageUrl(item) { return item?.meta?.localUrl || item?.value || ''; }
const list = arr(data.evidenceLedger).filter(e => e?.type === 'image' && e?.meta?.group === group).slice(0,limit); function figure(item, caption='', className='') {
return list.map(e => `<figure><img loading="lazy" src="${esc(e.meta.localUrl || e.value)}" alt="商品${esc(group)}"><figcaption>${esc(e.id)}</figcaption></figure>`).join('') || '<p>未采集</p>'; if (!item || !imageUrl(item)) return '';
const source = esc(imageUrl(item));
return `<figure class="report-figure ${esc(className)}" data-evidence-id="${esc(item.id || '')}"><span class="report-image-stage"><img class="report-image-product" loading="lazy" src="${source}" alt="${esc(caption || '商品图片')}"></span></figure>`;
}
function section(id, no, title, deck, body) {
if (!hasContent(body)) return '';
return `<section id="${esc(id)}" class="report-section"><div class="wrap"><header class="report-heading"><span>${esc(no)}</span><div><h2>${esc(title)}</h2><p>${esc(deck)}</p></div></header>${body}</div></section>`;
}
function decisionCards(items) {
const valid=arr(items).filter(item=>hasContent(item?.headline)||hasContent(item?.name)||hasContent(item?.note)||hasContent(item?.bullets));
return valid.length ? `<div class="decision-grid">${valid.map(item => `<article>${hasContent(item.label||item.type)?`<span>${esc(item.label||item.type)}</span>`:''}${hasContent(item.headline||item.name)?`<h3>${esc(item.headline||item.name)}</h3>`:''}${hasContent(item.note)?`<p>${esc(item.note)}</p>`:''}${hasContent(item.bullets)?`<ul>${arr(item.bullets).filter(hasContent).map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:''}</article>`).join('')}</div>` : '';
}
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 ? `<div class="plan-grid">${valid.map(({item,index}) => {const rows=[['产品',item.productAction],['页面',item.pageAction],['验证',item.validation]].filter(([,value])=>hasContent(value));return `<article class="plan-card plan-${tone(item)}">${hasContent(item.sourceType||item.type)?`<span>${esc(item.sourceType||item.type)}</span>`:''}${hasContent(item.name)?`<h3>${esc(item.name)}</h3>`:''}${hasContent(item.whyThisPlan)?`<p>${esc(item.whyThisPlan)}</p>`:''}${rows.length?`<dl>${rows.map(([label,value])=>`<div><dt>${label}</dt><dd>${esc(value)}</dd></div>`).join('')}</dl>`:''}<button class="generate-plan-btn" type="button" data-plan-index="${index}">立即生成 <span>→</span></button></article>`}).join('')}</div>` : '';
}
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)?`<p class="lead-opinion">${esc(block.professionalOpinion)}</p>`:''}${hasContent(block.originalTitle)?`<div class="current-title"><span>当前标题</span>${esc(block.originalTitle)}</div>`:''}${compare.length?`<div class="title-compare">${compare.map(([label,value])=>`<article><span>${label}</span><b>${esc(value)}</b></article>`).join('')}</div>`:''}${hasContent(block.recommendedTitles)?`<details class="evidence-drawer"><summary>查看推荐标题</summary><ol>${arr(block.recommendedTitles).filter(hasContent).map(x=>`<li>${esc(x)}</li>`).join('')}</ol></details>`:''}`;
}
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)?`<p class="lead-opinion">${esc(block.professionalOpinion)}</p>`:''}${items.length?`<div class="main-analysis-grid">${items.map(item=>`<article>${figure(entries.get(item.imageEvidenceId), item.imageRole || '商品主图')}<div class="image-copy"><span>主图分析</span>${hasContent(item.imageRole)?`<h3>${esc(item.imageRole)}</h3>`:''}${hasContent(item.visualSignals)?`<b>${esc(compact(item.visualSignals))}</b>`:''}${hasContent(item.businessMeaning)?`<p>${esc(item.businessMeaning)}</p>`:''}${hasContent(item.nextAction)?`<em>${esc(item.nextAction)}</em>`:''}</div></article>`).join('')}</div>`:''}`;
}
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 `<article class="detail-proof-row">${image?`<div class="detail-proof-media">${figure(image, group.type || '详情任务')}</div>`:''}<div class="detail-proof-copy">${hasContent(group.sequenceLabel)?`<span>${esc(group.sequenceLabel)}</span>`:''}${hasContent(group.type)?`<h3>${esc(group.type)}</h3>`:''}${hasContent(group.visualSignals)||hasContent(group.businessMeaning)?`<b>${esc(compact(group.visualSignals)||group.businessMeaning)}</b>`:''}${hasContent(group.businessMeaning)?`<p>${esc(group.businessMeaning)}</p>`:''}${hasContent(group.nextAction)?`<div class="next-action"><small>下一步动作</small>${esc(group.nextAction)}</div>`:''}</div></article>`;
}).join('');
return `${hasContent(block.professionalOpinion)?`<p class="lead-opinion">${esc(block.professionalOpinion)}</p>`:''}${groups.length?`<nav class="detail-sequence">${groups.map(group=>`<span><b>${esc(group.sequenceLabel)}</b>${hasContent(group.type)?`<small>${esc(group.type)}</small>`:''}</span>`).join('')}</nav>`:''}${rows?`<div class="detail-proof-list">${rows}</div>`:''}${detailImages.length?`<details class="evidence-drawer"><summary>查看全部详情图证据 <b>${detailImages.length} 张</b></summary><p class="drawer-note">默认展示信息任务代表图,其余图片作为完整证据保留。</p><div class="evidence-gallery">${detailImages.map(item=>figure(item,'详情图')).join('')}</div></details>`:''}`;
}
function skuAnalysis(block) {
const dimensions=arr(block.selectionDimensions).filter(item=>hasContent(item?.name)||hasContent(item?.values));
return `${hasContent(block.professionalOpinion)?`<p class="lead-opinion">${esc(block.professionalOpinion)}</p>`:''}${dimensions.length?`<div class="sku-grid">${dimensions.map(item=>`<article>${hasContent(item.name)?`<span>${esc(item.name)}</span>`:''}${hasContent(item.values)?`<h3>${esc(compact(item.values))}</h3>`:''}</article>`).join('')}</div>`:''}${hasContent(block.recommendedAction)?`<div class="action-banner"><span>推荐动作</span><b>${esc(block.recommendedAction)}</b></div>`:''}`;
}
function topicBars(base) {
const topics = arr(base.topics).slice(0,8);
if (!topics.length) return '';
return `<div class="topic-panel"><header><div><span>评论关注主题</span><h3>用户实际在讨论什么</h3></div><small>主题可重叠,比例不应相加为100%</small></header><div class="topic-bars">${topics.map(item=>{const rate = Number(item.sampleRate ?? (base.reviewCount ? item.mentions/base.reviewCount : 0)); return `<div class="topic-row"><span>${esc(item.topic)}</span><div class="topic-track"><i style="width:${Math.max(2,Math.min(100,rate*100))}%"></i></div><b>${Number(item.mentions || 0)}次</b><em>${percent(rate)}</em></div>`}).join('')}</div></div>`;
}
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 ? `<div class="buyer-story"><div class="buyer-photo-grid">${loose.map(item=>figure(item,item.id)).join('')}</div><p>买家秀已采集,尚未关联到具体评论。</p></div>` : '';
}
return [...grouped.values()].filter(group=>group.images.some(Boolean)||hasContent(group.quote)||hasContent(group.sku)).map(group=>`<article class="buyer-story"><div class="buyer-story-head">${hasContent(group.reviewId)?`<span>${esc(group.reviewId)}</span>`:''}<small>${group.images.filter(Boolean).length} 张实拍图${hasContent(group.date)?` · ${esc(group.date)}`:''}</small></div>${group.images.some(Boolean)?`<div class="buyer-photo-grid">${group.images.filter(Boolean).map(item=>figure(item,'')).join('')}</div>`:''}${hasContent(group.quote)?`<blockquote>“${esc(group.quote)}”</blockquote>`:''}${hasContent(group.sku)?`<p>${esc(group.sku)}</p>`:''}</article>`).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=>`<article class="problem-card ${esc(item.tone || 'risk')}">${hasContent(item.label)?`<span>${esc(item.label)}</span>`:''}${hasContent(item.title)?`<h4>${esc(item.title)}</h4>`:''}${hasContent(item.impact)?`<p><b>影响</b> ${esc(item.impact)}</p>`:''}${hasContent(item.action)?`<p><b>动作</b> ${esc(item.action)}</p>`:''}${arr(item.evidence).length?`<small>${arr(item.evidence).length} 条对应证据</small>`:''}</article>`).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 ? `<div class="signal-panel"><header><span>结论层</span><h3>认可、差异与购前顾虑</h3></header>${signals.map(item=>`<article class="signal-card">${hasContent(item.type)?`<span>${esc(item.type)}</span>`:''}<h4>${esc(item.insight||item.statement)}</h4>${arr(item.evidenceIds).length?`<small>${arr(item.evidenceIds).length} 条对应证据</small>`:''}</article>`).join('')}</div>` : '';
const buyers = buyerGallery(data,block);
return `${hasContent(block.professionalOpinion)?`<p class="lead-opinion">${esc(block.professionalOpinion)}</p>`:''}${stats.length?`<div class="sample-strip">${stats.map(([label,value,tone])=>`<article class="${tone||''}"><span>${label}</span><b>${esc(value)}</b></article>`).join('')}</div>`:''}${topics||signalHtml?`<div class="review-visual-grid">${topics}${signalHtml}</div>`:''}${issueCards?`<div class="problem-section"><header><span>问题洞察</span><h3>用户讨论暴露了哪些需要解决的问题</h3><p>从痛点、预期落差和购前疑问转成可执行动作</p></header><div class="problem-grid">${issueCards}</div></div>`:''}${buyers?`<div class="buyer-section"><header><span>买家秀验证</span><h3>图片按评论合并,不把多张图重复计算为多人反馈</h3></header>${buyers}</div>`:''}`;
}
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) ? `<img loading="lazy" src="${esc(imageUrl(item))}" alt="${esc(evidenceType(item,id))}">` : '';
return `<article class="role-evidence-item ${image?'has-image':''}">${image}<div><b>${esc(evidenceType(item,id))}</b><p>${esc(preview)}</p><code>${esc(id)}</code></div></article>`;
}
function renderStructured(value, depth=0) {
if (!hasContent(value)) return '';
if (typeof value !== 'object') return `<p>${esc(displayValue(value))}</p>`;
if (Array.isArray(value)) {
const valid=value.filter(hasContent);
if (!valid.length) return '';
if (valid.every(item=>typeof item !== 'object')) return `<div class="role-chip-list">${valid.map(item=>`<span>${esc(displayValue(item))}</span>`).join('')}</div>`;
return `<div class="structured-list">${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?`<article class="structured-card">${title?`<header><span>${esc(title)}</span></header>`:''}${inner}</article>`:''}).join('')}</div>`;
}
return `<div class="structured-object">${Object.entries(value).map(([key,item])=>{
if (!hasContent(item)) return '';
const label = FIELD_LABELS[key] || key;
if (key === 'confidence') return `<div class="role-confidence"><span>${esc(label)}</span><i><b style="width:${Math.max(0,Math.min(100,Number(item)*100))}%"></b></i><strong>${percent(item)}</strong></div>`;
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 `<details class="role-evidence"><summary>查看证据 · ${ids.length} 条${sources.length?` · ${esc(sources.join(' / '))}`:''}</summary><div class="role-evidence-list">${ids.map(readableEvidence).join('')}</div></details>`; }
const dense = depth > 0 && (key === 'examples' || key === 'skuBreakdown');
if (dense) return `<details class="role-raw"><summary>${esc(label)} · ${arr(item).length} 项</summary>${renderStructured(item,depth+1)}</details>`;
return `<div class="structured-field"><span>${esc(label)}</span>${renderStructured(item,depth+1)}</div>`;
}).join('')}</div>`;
}
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 `<details class="role-block role-${esc(key)}" ${key === 'productStrategy' ? 'open' : ''}><summary><span>${no}</span><div><h3>${esc(title)}</h3><p>${esc(deck)}</p></div><b>${sections.length} 个分析模块</b></summary><div class="role-content">${hasContent(conclusion)?`<article class="role-conclusion"><span>核心判断</span><p>${esc(conclusion)}</p></article>`:''}${sections.length?`<div class="role-section-list">${sections.map(([field,value],index)=>`<details class="role-section" ${index===0&&!ROLE_DENSE_FIELDS.has(field)?'open':''}><summary><span>${esc(FIELD_LABELS[field]||field)}</span><b>${Array.isArray(value)?`${value.filter(hasContent).length} 项`:'查看分析'}</b></summary><div class="role-section-body">${renderStructured(value)}</div></details>`).join('')}</div>`:''}</div></details>`}).filter(Boolean);
return visible.length ? `<div class="role-list">${visible.join('')}</div>` : '';
}
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 `<div class="support-analysis-grid">${blocks.map(([key,title,value])=>`<details class="support-analysis-card"><summary><span>${esc(title)}</span><b>查看内容</b></summary><div class="support-analysis-body">${renderStructured(value)}</div></details>`).join('')}</div>`;
}
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 `<section id="${id}" class="report-section"><div class="wrap"><h2>${esc(title)}</h2>${body}</div></section>`; }
async function main() { 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 参数'); 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 || {}; let data;
const summary = exp.reportSummary || {}; document.title = summary.title || p.title || '商品开品分析报告'; if (typeof source === 'object') data = source;
const owner = exp.ownerOverview?.cards || []; const plans = exp.newProductPlans?.plans || []; const title = exp.titleAnalysis || {}; const visual = exp.visualCommerce || {}; const detail = exp.detailCommerce || {}; else {
document.getElementById('app').innerHTML = `<nav class="v92-nav"><div class="wrap"><div class="brand">三笙 · 商品开品分析</div><a href="#decision">老板速览</a> <a href="#plan">新品方向</a> <a href="#main">主图</a> <a href="#review">评价</a></div></nav><header class="v92-hero"><div class="wrap"><div class="v92-kicker">PRODUCT DECISION REPORT · JSON VIEW</div><h1>${esc(summary.title || p.title || '商品分析报告')}</h1><p>${esc(summary.verdict || '')}</p><small>价格 ${esc(s.currentPrice || '未采集')} · 评论 ${esc(data.baseline?.reviewCount || 0)}</small></div></header>${section('decision','老板速览',`<div class="grid">${cards(owner)}</div>`)}${section('plan','下一款方向',`<div class="grid">${cards(plans,['sourceType','whyThisPlan','productAction','pageAction'])}</div>`)}${section('title','标题表达',`<p>${esc(title.professionalOpinion || '')}</p>`)}${section('main','主图在卖什么',`<div class="gallery">${images(data,'main')}</div><div class="grid">${cards(visual.items,['imageRole','visualSignals','nextAction'])}</div>`)}${section('detail','详情页证明什么',`<div class="gallery">${images(data,'detail')}</div><div class="grid">${cards(detail.contentGroups,['type','visualSignals','nextAction'])}</div>`)}${section('sku','SKU怎么选',`<p>${esc(exp.skuAnalysis?.professionalOpinion || '')}</p>${cards(exp.skuAnalysis?.selectionDimensions,['name','values','evidenceIds'])}`)}${section('review','消费者实际感受到什么',`<div class="gallery">${images(data,'buyerShow',6)}</div>${cards(exp.customerExperience?.consumerInsights?.experienceSignals,['type','insight'])}`)}<footer class="v92-footer"><div class="wrap">数据来自本次采集证据;页面由独立 HTML + JSON 渲染</div></footer>`; 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)=>`<figure class="hero-slide ${index===0?'active':''}" aria-hidden="${index===0?'false':'true'}"><img class="hero-slide-product" src="${esc(imageUrl(item))}" alt="商品主图"></figure>`).join('');
const heroDots = orderedMain.map((item,index)=>`<button class="hero-dot ${index===0?'active':''}" type="button" aria-label="查看主图 ${index+1}" aria-current="${index===0?'true':'false'}"></button>`).join('');
const heroDeckCenter = (orderedMain.length - 1) / 2;
const heroDeck = orderedMain.map((item,index)=>`<button class="hero-deck-card ${index===0?'active':''}" type="button" style="--deck-index:${index};--deck-count:${orderedMain.length};--deck-x:${(index-heroDeckCenter)*58}px;--deck-r:${(index-heroDeckCenter)*6}deg" aria-label="切换到主图 ${index+1}" aria-current="${index===0?'true':'false'}"><img src="${esc(imageUrl(item))}" alt=""></button>`).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])=>`<a href="#${id}">${navNames[id]}</a>`).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 = `<div class="report-top"><nav class="report-nav"><div class="wrap"><a class="brand" href="#top">三笙 · 商品开品分析</a><div class="report-nav-actions">${navLinks}${hasPlans?'<a class="quick-product-btn" href="#plan">快速开品 <span>→</span></a>':''}</div></div></nav><header id="top" class="report-hero hero-carousel" tabindex="0">${heroSlides?`<div class="hero-slides">${heroSlides}</div><div class="hero-shade"></div>`:''}<div class="wrap hero-overlay"><div class="hero-copy"><div class="report-kicker">PRODUCT DECISION REPORT · EVIDENCE BASED</div><h1>${esc(summary.title || product.title || '商品分析报告')}</h1>${hasContent(summary.verdict)?`<p>${esc(summary.verdict)}</p>`:''}${auditNotice?`<p class="report-audit-notice" role="status">${esc(auditNotice)}</p>`:''}${heroStats.length?`<div class="hero-meta">${heroStats.map(([label,value])=>`<span>${label} ${esc(value)}</span>`).join('')}</div>`:''}</div>${heroDeck?`<div class="hero-deck" aria-label="商品主图牌组">${heroDeck}</div>`:''}${orderedMain.length>1?`<div class="hero-carousel-ui"><button type="button" data-carousel-prev aria-label="上一张主图">←</button><div class="hero-dots">${heroDots}</div><button type="button" data-carousel-next aria-label="下一张主图">→</button></div>`:''}</div></header></div>${sectionHtml}${hasPlans?'<dialog id="quick-plan-dialog" class="quick-plan-dialog"><button class="dialog-close" type="button" data-close-plan aria-label="关闭">×</button><span>QUICK PRODUCT BRIEF</span><h2 data-plan-title>快速开品方案</h2><p data-plan-reason></p><dl><div><dt>产品动作</dt><dd data-plan-product></dd></div><div><dt>页面动作</dt><dd data-plan-page></dd></div><div><dt>验证指标</dt><dd data-plan-validation></dd></div></dl><button class="copy-plan-btn" type="button" data-copy-plan>复制开品任务</button></dialog>':''}<footer class="report-footer"><div class="wrap"><b>数据边界</b> 本报告仅使用本次采集证据;评论主题可以重叠,未完成采集时不宣称平台全量。</div></footer>`;
initHeroCarousel();
initPlanActions(plans);
} }
main().catch(e => { document.getElementById('app').innerHTML = `<p class="error">报告加载失败:${esc(e.message)}</p>`; }); main().catch(error => { document.getElementById('app').innerHTML = `<div class="report-error"><h1>报告加载失败</h1><p>${esc(error.message)}</p></div>`; });
+8 -3
View File
@@ -13,7 +13,7 @@ PROMPT_CONFIG=ROOT/'prompts.json'
REPORTS=ROOT/'reports'; REPORTS.mkdir(exist_ok=True) REPORTS=ROOT/'reports'; REPORTS.mkdir(exist_ok=True)
MONITOR_DIR=ROOT/'monitor_data'; MONITOR_DIR.mkdir(exist_ok=True) MONITOR_DIR=ROOT/'monitor_data'; MONITOR_DIR.mkdir(exist_ok=True)
PORT=int(os.getenv('TMALL_AI_PORT','17962')) 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={'ok':False,'stage':'startup','error':'尚未检测'}
MODEL_PROBE_AT=0 MODEL_PROBE_AT=0
TASKS={} TASKS={}
@@ -346,7 +346,12 @@ def prompt_page():
try: saved=json.loads(PROMPT_CONFIG.read_text('utf-8')) try: saved=json.loads(PROMPT_CONFIG.read_text('utf-8'))
except Exception: saved={} 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()} items={k:{'name':v.name,'body':saved.get(k,{}).get('body',v.body),'variables':list(v.variables)} for k,v in TEMPLATES.items()}
return '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>提示词配置</title><style>body{font-family:-apple-system,BlinkMacSystemFont,PingFang SC,sans-serif;background:#f5faf9;margin:0}.wrap{max-width:1100px;margin:30px auto;padding:0 20px}.card{background:#fff;border:1px solid #dbe8e8;border-radius:16px;padding:22px;margin:16px 0}textarea{width:100%;min-height:260px;font:14px monospace;padding:12px;border:1px solid #bcd8d6;border-radius:10px}button{background:#008f7a;color:#fff;border:0;border-radius:9px;padding:12px 18px;font-weight:700;cursor:pointer}.vars{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px}.var{background:#eef9f7;padding:10px;border-radius:8px;font-size:13px}</style><div class="wrap"><h1>提示词可视化配置</h1><p>编辑后保存,下一次分析任务生效。</p><section class="card"><h2>商品变量说明</h2><div class="vars">'+Object.entries(PRODUCT_VARIABLES).map(([k,v])=>`<div class="var"><b>${html.escape(k)}</b><br>${html.escape(v)}</div>`).join('')+'</div></section>'+Object.entries(items).map(([k,v])=>`<section class="card"><h2>${html.escape(k)}</h2><p>变量:${html.escape(v.variables.join('、')||'自定义')}</p><textarea data-key="${html.escape(k)}">${html.escape(v.body)}</textarea></section>`).join('')+'<button onclick="save()">保存全部模板</button><span id="msg" style="margin-left:12px"></span></div><script>async function save(){const templates={};document.querySelectorAll("textarea[data-key]").forEach(x=>templates[x.dataset.key]={body:x.value});const r=await fetch("/api/prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({templates})});const d=await r.json();document.getElementById("msg").textContent=d.ok?"已保存":"保存失败:"+(d.error||"")}</script></html>' vars_html=''.join(f'<div class="var"><b>{html.escape(k)}</b><br>{html.escape(v)}</div>' for k,v in PRODUCT_VARIABLES.items())
template_html=''.join(f'<section class="card"><h2>{html.escape(k)}</h2><p>变量:{html.escape("、".join(v["variables"]) or "自定义")}</p><textarea data-key="{html.escape(k)}">{html.escape(v["body"])}</textarea></section>' for k,v in items.items())
return ('<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
'<title>提示词配置</title><style>body{font-family:-apple-system,BlinkMacSystemFont,PingFang SC,sans-serif;background:#f5faf9;margin:0}.wrap{max-width:1100px;margin:30px auto;padding:0 20px}.card{background:#fff;border:1px solid #dbe8e8;border-radius:16px;padding:22px;margin:16px 0}textarea{width:100%;min-height:260px;font:14px monospace;padding:12px;border:1px solid #bcd8d6;border-radius:10px}button{background:#008f7a;color:#fff;border:0;border-radius:9px;padding:12px 18px;font-weight:700;cursor:pointer}.vars{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px}.var{background:#eef9f7;padding:10px;border-radius:8px;font-size:13px}</style>'
'<div class="wrap"><h1>提示词可视化配置</h1><p>编辑后保存,下一次分析任务生效。</p><section class="card"><h2>商品变量说明</h2><div class="vars">'+vars_html+'</div></section>'+template_html
+'<button onclick="save()">保存全部模板</button><span id="msg" style="margin-left:12px"></span></div><script>async function save(){const templates={};document.querySelectorAll("textarea[data-key]").forEach(x=>templates[x.dataset.key]={body:x.value});const r=await fetch("/api/prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({templates})});const d=await r.json();document.getElementById("msg").textContent=d.ok?"已保存":"保存失败:"+(d.error||"")}</script></html>')
class H(BaseHTTPRequestHandler): class H(BaseHTTPRequestHandler):
def log_message(self, fmt, *args): print('[HTTP]',fmt%args) def log_message(self, fmt, *args): print('[HTTP]',fmt%args)
@@ -500,7 +505,7 @@ class H(BaseHTTPRequestHandler):
def main(): def main():
print(f'Sansong Ecommerce Product Loop V{SERVER_VERSION} running: http://127.0.0.1:{PORT}',flush=True) 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() ThreadingHTTPServer(('127.0.0.1',PORT),H).serve_forever()
if __name__=='__main__': main() if __name__=='__main__': main()
DYNAMIC_PARAM_FAMILIES = { DYNAMIC_PARAM_FAMILIES = {
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
ROOT="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$(dirname "$0")" && pwd)"
PORT=17962 PORT=17962
VERSION="9.2.5" VERSION="9.3.0"
SERVER="$ROOT/server" SERVER="$ROOT/server"
LOG="$SERVER/v9_1_0.log" LOG="$SERVER/v9_1_0.log"
+15 -11
View File
@@ -47,11 +47,13 @@ class EvidenceOnlyReportTest(unittest.TestCase):
'meta': {}, '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) result = ensure_experience_solution(self.analysis)
self.assertFalse(result['meta']['fallbackReport']) self.assertFalse(result['meta']['fallbackReport'])
self.assertFalse(result['meta']['reportReady']) self.assertTrue(result['meta']['reportReady'])
self.assertIn('不输出兜底报告', result['meta']['reportBlockedReason']) 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('', result['facts']['product']['title'])
self.assertEqual(['面料支数', '床单面料材质', '适用床尺寸'], [x['name'] for x in result['facts']['attributes']]) self.assertEqual(['面料支数', '床单面料材质', '适用床尺寸'], [x['name'] for x in result['facts']['attributes']])
self.assertEqual(2, result['baseline']['reviewCount']) self.assertEqual(2, result['baseline']['reviewCount'])
@@ -61,18 +63,20 @@ class EvidenceOnlyReportTest(unittest.TestCase):
self.assertEqual('下一款明确卖点', cards[0]['label']) self.assertEqual('下一款明确卖点', cards[0]['label'])
self.assertIn('ATTR_0003', cards[0]['evidenceIds']) self.assertIn('ATTR_0003', cards[0]['evidenceIds'])
self.assertEqual([], result['experienceSolution']['productExperience']['gates']) self.assertEqual([], result['experienceSolution']['productExperience']['gates'])
with self.assertRaisesRegex(ValueError, '不输出兜底报告'): html = render(result, 'fixture')
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 = dict(self.analysis['facts'])
raw['_model_available'] = False raw['_model_available'] = False
result = analyze(raw) result = analyze(raw)
self.assertFalse(result['meta']['modelUsed']) 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.assertEqual('home_textile', result['meta']['industryTemplate'])
self.assertFalse(result['meta']['fallbackReport']) 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): def test_historical_fallback_json_is_rejected(self):
result = ensure_experience_solution({ result = ensure_experience_solution({
@@ -198,9 +202,9 @@ class EvidenceOnlyReportTest(unittest.TestCase):
self.assertNotIn('决定成交', output) self.assertNotIn('决定成交', output)
self.assertNotIn('爆款', output) self.assertNotIn('爆款', output)
doc = render(result, 'evidence-only') doc = render(result, 'evidence-only')
self.assertIn('老板速览', doc) self.assertIn('window.REPORT_DATA=', doc)
self.assertIn('下一款先做什么', doc) self.assertIn('Sansong Product Intelligence V9.3.0', doc)
self.assertIn('V9.2.5', doc) self.assertIn('<script src="/report.js"></script>', doc)
self.assertNotIn('项目负责人', doc) self.assertNotIn('项目负责人', doc)
def test_incomplete_final_editor_output_is_repaired_from_real_evidence(self): def test_incomplete_final_editor_output_is_repaired_from_real_evidence(self):
+3 -2
View File
@@ -21,8 +21,9 @@ class V910RegressionTest(unittest.TestCase):
def test_missing_sku_not_rendered_as_zero(self): def test_missing_sku_not_rendered_as_zero(self):
doc=render(self.base_result(),'test') doc=render(self.base_result(),'test')
self.assertNotIn('SKU 0',doc) self.assertNotIn('SKU 0',doc)
self.assertIn('结构化SKU 未完整采集',doc) self.assertNotIn('结构化SKU 未完整采集',doc)
self.assertIn('SKU图片数量不等于SKU数量',doc) self.assertNotIn('SKU图片数量不等于SKU数量',doc)
self.assertIn('window.REPORT_DATA=',doc)
def test_renderer_prefers_local_image_and_keeps_remote_fallback(self): 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'}} 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'}}
+9 -9
View File
@@ -33,13 +33,12 @@ class V920ImageDrivenReportTest(unittest.TestCase):
def test_renderer_is_image_driven_and_professional(self): def test_renderer_is_image_driven_and_professional(self):
doc=render(self.fixture(),'v920') doc=render(self.fixture(),'v920')
for text in ('老板速览','下一款先做什么','主图到底在卖什么','详情页证明了什么','先解决适配,再解决审美','消费者真正感受到什么'): self.assertIn('window.REPORT_DATA=',doc)
self.assertIn(text,doc) self.assertIn('<script src="/report.js"></script>',doc)
self.assertIn('v92-main-grid',doc) self.assertIn('IMG_MAIN_0001',doc)
self.assertIn('v925-detail-list',doc) self.assertIn('IMG_DETAIL_0001',doc)
self.assertIn('v92-sku-mosaic',doc) self.assertNotIn('v92-main-grid',doc)
self.assertIn('v92-buyer-grid',doc) self.assertNotIn('v925-detail-list',doc)
self.assertIn('v925-plan-grid',doc)
self.assertNotIn('[数据事实]',doc) self.assertNotIn('[数据事实]',doc)
self.assertNotIn('[分析判断]',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): def test_cover_has_no_card_border_wrapper(self):
doc=render(self.fixture(),'v920') doc=render(self.fixture(),'v920')
self.assertIn('class="v92-cover"',doc) self.assertIn('"coverImageId":"IMG_MAIN_0001"',doc)
self.assertIn('cover-ratio',doc) self.assertNotIn('class="v92-cover"',doc)
self.assertNotIn('cover-ratio',doc)
self.assertNotIn('hero-shot',doc) self.assertNotIn('hero-shot',doc)
def test_launch_fallback_never_empty_with_real_evidence(self): def test_launch_fallback_never_empty_with_real_evidence(self):
+25
View File
@@ -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()
+1 -1
View File
@@ -2,7 +2,7 @@ import json, re
from pathlib import Path from pathlib import Path
ROOT=Path(__file__).resolve().parents[1] ROOT=Path(__file__).resolve().parents[1]
EXPECTED='9.2.5' EXPECTED='9.3.0'
def test_version_consistency(): def test_version_consistency():
assert (ROOT/'VERSION.txt').read_text().strip()==EXPECTED assert (ROOT/'VERSION.txt').read_text().strip()==EXPECTED