Files
sansyn-pc-extension/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/background.js
T

820 lines
68 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
importScripts('auth.js');
const RUN_GAP=8*60*1000;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const isProduct=u=>/^https:\/\/detail\.tmall\.com\/item\.htm/i.test(u||'') || /item\.taobao\.com\/item\.htm/i.test(u||'');
const ANALYSIS_TASKS_KEY='sansheng_product_analysis_tasks';
const LAST_ANALYSIS_TASK_KEY='sansheng_product_analysis_last_task';
const ANALYSIS_ALARM='sansheng_product_analysis_poll';
const ANALYSIS_NOTIFICATION_PREFIX='sansheng-product-analysis-';
let serviceHealthCache={checkedAt:0,ready:false,error:''};
async function setStatus(tabId,patch){
const key='status_'+tabId; const old=(await chrome.storage.local.get(key))[key]||{};
await chrome.storage.local.set({[key]:{...old,...patch,updatedAt:Date.now()}});
}
async function submitProductAnalysis(raw){
raw.collection=raw.collection||{};
raw.collection.clientRequestId=raw.collection.clientRequestId||crypto.randomUUID();
const controller=new AbortController();
const timeout=setTimeout(()=>controller.abort(),15000);
let response;
try{
response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks',{method:'POST',headers:{'Content-Type':'application/json','Idempotency-Key':raw.collection.clientRequestId},body:JSON.stringify({platform:raw?.meta?.platform||'taobao',productId:String(raw?.product?.itemId||''),analysisMode:raw?.collection?.analysisMode==='current_snapshot'?'quick':(raw?.collection?.reviewCollectionMode||'standard'),userInstruction:'',clientRequestId:raw.collection.clientRequestId,snapshot:raw,source:'chrome-extension'}),signal:controller.signal});
}catch(error){
if(error?.name==='AbortError')throw new Error('三笙云端分析服务响应超时,请稍后重试');
throw new Error('无法连接三笙云端分析服务,请稍后重试');
}finally{clearTimeout(timeout)}
const result=await response.json().catch(()=>null);
const code=result?.Code??result?.code;
if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||`商品分析提交失败 HTTP ${response.status}`);
const data=result.Data||result.data||{};const taskId=String(data.TaskId||data.taskId||'');
return {ok:true,taskId,taskUrl:sanshengExtensionAuth.config.pcBase+(data.ReportUrl||data.reportUrl||`/product-analysis/reports/${taskId}`)};
}
async function clearTabStatus(tabId){
await chrome.storage.local.remove('status_'+tabId).catch(()=>{});
}
async function checkAnalysisService(force=false){
if(!force&&Date.now()-serviceHealthCache.checkedAt<8000)return serviceHealthCache;
const controller=new AbortController();
const timeout=setTimeout(()=>controller.abort(),5000);
try{
const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=1',{signal:controller.signal});
const result=await response.json().catch(()=>null);
const code=result?.Code??result?.code;
const ready=response.ok&&result&&code===0;
serviceHealthCache={checkedAt:Date.now(),ready,error:ready?'':(result?.Message||result?.message||`HTTP ${response.status}`)};
}catch(error){
serviceHealthCache={checkedAt:Date.now(),ready:false,error:error?.name==='AbortError'?'Web API 响应超时':'无法连接 Web API'};
}finally{clearTimeout(timeout)}
return serviceHealthCache;
}
function analysisStageMessage(stage,progress){
const labels={queued:'任务已进入分析队列',product:'正在分析商品定位与标题',visual:'正在分析主图与视觉表达',detail:'正在分析详情页内容',consumer:'正在汇总消费者体验',strategy:'正在生成经营策略与新品方案',assembling:'正在组装分析报告',completed:'报告已经生成'};
const suffix=progress!=null?` · 分析进度 ${progress}%`:'';
if(String(stage||'').startsWith('review'))return `正在分析评论(${String(stage).replace('review','').trim()||'处理中'})${suffix}`;
return (labels[stage]||'正在生成报告')+suffix;
}
async function readTrackedAnalysisTasks(){
const stored=await chrome.storage.local.get(ANALYSIS_TASKS_KEY);
return stored[ANALYSIS_TASKS_KEY]||{};
}
async function saveTrackedAnalysisTasks(tasks){
await chrome.storage.local.set({[ANALYSIS_TASKS_KEY]:tasks});
if(Object.keys(tasks).length){
await chrome.alarms.create(ANALYSIS_ALARM,{periodInMinutes:.5});
}else{
await chrome.alarms.clear(ANALYSIS_ALARM);
}
}
async function updateAnalysisBadge(state,progress){
const text=state==='analysis_completed'?'✓':state==='analysis_failed'?'!':String(Math.max(1,Math.min(99,Math.round(progress||1))));
const color=state==='analysis_completed'?'#138a70':state==='analysis_failed'?'#b83b4b':'#315fba';
await chrome.action.setBadgeBackgroundColor({color}).catch(()=>{});
await chrome.action.setBadgeText({text}).catch(()=>{});
}
async function showAnalysisNotification(task,state){
const completed=state==='analysis_completed';
await chrome.notifications.create(ANALYSIS_NOTIFICATION_PREFIX+task.taskId,{
type:'basic',
iconUrl:chrome.runtime.getURL('icon-128.png'),
title:completed?'商品分析报告已生成':'商品分析未完成',
message:completed?'点击通知即可查看完整分析报告。':(task.errorMessage||'请打开扩展查看失败原因。'),
priority:completed?2:1
}).catch(()=>{});
}
async function writeAnalysisPresentation(task,state,message,progress,extra={}){
const effectiveProgress=progress??task.progress??10;
task.progress=effectiveProgress;
const presentation={state,message,progress:effectiveProgress,taskId:task.taskId,taskUrl:task.taskUrl,productId:task.productId||'',stage:task.stage||'',productTitle:task.productTitle||'',...extra};
await setStatus(task.tabId,presentation);
await chrome.storage.local.set({[LAST_ANALYSIS_TASK_KEY]:{...presentation,tabId:task.tabId,updatedAt:Date.now()}});
await updateAnalysisBadge(state,progress);
}
async function trackProductAnalysis(tabId,submitted,productTitle='',productId=''){
const tasks=await readTrackedAnalysisTasks();
const task={taskId:submitted.taskId,taskUrl:submitted.taskUrl,tabId,productId:String(productId||''),productTitle,submittedAt:Date.now(),nextPollAt:0,pollErrors:0,stage:'queued'};
tasks[task.taskId]=task;
await saveTrackedAnalysisTasks(tasks);
await writeAnalysisPresentation(task,'analyzing','任务已提交,正在排队分析',80,{analysisProgress:10});
await pollTrackedAnalysisTasks(true);
}
async function pollTrackedAnalysisTask(task,force=false){
if(!force&&Number(task.nextPollAt||0)>Date.now())return {keep:true,task};
try{
const response=await sanshengExtensionAuth.fetch(`/v1/product-analysis/tasks/${encodeURIComponent(task.taskId)}`);
const result=await response.json().catch(()=>null);
const code=result?.Code??result?.code;
if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||`任务状态查询失败 HTTP ${response.status}`);
const data=result.Data||result.data||{};
const status=Number(data.Status??data.status??0),progress=Number(data.Progress??data.progress??10),stage=String(data.Stage??data.stage??'queued');
const displayProgress=Math.min(99,80+Math.round(progress*.2));
task={...task,stage,progress:displayProgress,analysisProgress:progress,nextPollAt:Date.now()+3000,pollErrors:0};
if(status===3){
await writeAnalysisPresentation(task,'analysis_completed','报告已生成,点击下方按钮查看',100,{stage:'completed'});
await showAnalysisNotification(task,'analysis_completed');
return {keep:false,task};
}
if(status===4){
task.errorMessage=String(data.ErrorMessage??data.errorMessage??'分析任务执行失败');
await writeAnalysisPresentation(task,'analysis_failed',task.errorMessage,displayProgress,{stage,errorMessage:task.errorMessage,analysisProgress:progress});
await showAnalysisNotification(task,'analysis_failed');
return {keep:false,task};
}
const queuedTooLong=stage==='queued'&&Date.now()-Number(task.submittedAt||Date.now())>90000;
const message=queuedTooLong?'任务排队时间较长,请确认分析 Worker 已启动;扩展仍会继续等待':analysisStageMessage(stage,progress);
await writeAnalysisPresentation(task,'analyzing',message,displayProgress,{stage,analysisProgress:progress});
return {keep:true,task};
}catch(error){
task={...task,nextPollAt:Date.now()+15000,pollErrors:Number(task.pollErrors||0)+1};
await writeAnalysisPresentation(task,'analyzing',task.pollErrors>=3?'云端状态暂时不可用,后台稍后继续查询':'正在等待云端分析状态',undefined,{stage:task.stage||'queued'});
return {keep:true,task};
}
}
async function pollTrackedAnalysisTasks(force=false){
const tasks=await readTrackedAnalysisTasks();
for(const [taskId,task] of Object.entries(tasks)){
const result=await pollTrackedAnalysisTask(task,force);
if(result.keep)tasks[taskId]=result.task;else delete tasks[taskId];
}
await saveTrackedAnalysisTasks(tasks);
}
async function bootstrapLatestAnalysisTask(tabId){
try{
const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=1');
const result=await response.json().catch(()=>null);
const code=result?.Code??result?.code;
if(!response.ok||!result||code!==0)return null;
const page=result.Data||result.data||{};
const item=(page.List||page.list||[])[0];
if(!item)return null;
const taskId=String(item.TaskId||item.taskId||'');
if(!taskId)return null;
const reportPath=item.ReportUrl||item.reportUrl||`/product-analysis/reports/${taskId}`;
const taskUrl=/^https?:\/\//i.test(reportPath)?reportPath:sanshengExtensionAuth.config.pcBase+reportPath;
const task={taskId,taskUrl,tabId,productId:String(item.ProductId||item.productId||''),productTitle:item.ProductTitle||item.productTitle||'',stage:item.Stage||item.stage||'queued',progress:Number(item.Progress??item.progress??10),nextPollAt:0,pollErrors:0};
const status=Number(item.Status??item.status??0);
if(status===1||status===2){
const tasks=await readTrackedAnalysisTasks();
tasks[taskId]=task;
await saveTrackedAnalysisTasks(tasks);
const polled=await pollTrackedAnalysisTask(task,true);
return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY]||polled.task;
}
if(status===3){
await writeAnalysisPresentation(task,'analysis_completed','最近一份报告已生成,点击下方按钮查看',100,{stage:'completed',analysisProgress:100});
return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY];
}
if(status===4){
await writeAnalysisPresentation(task,'analysis_failed','最近一份商品分析未完成',task.progress,{stage:task.stage});
return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY];
}
}catch(error){return null}
return null;
}
async function refreshAnalysisForTab(tabId){
const tasks=await readTrackedAnalysisTasks();
const task=Object.values(tasks).find(item=>Number(item.tabId)===Number(tabId));
if(task){
const result=await pollTrackedAnalysisTask(task,false);
if(result.keep)tasks[task.taskId]=result.task;else delete tasks[task.taskId];
await saveTrackedAnalysisTasks(tasks);
}
const stored=await chrome.storage.local.get(['status_'+tabId,LAST_ANALYSIS_TASK_KEY]);
const current=stored['status_'+tabId];
const last=stored[LAST_ANALYSIS_TASK_KEY];
if(current?.taskId)return current;
if(last?.taskId)return last;
return await bootstrapLatestAnalysisTask(tabId);
}
chrome.alarms.onAlarm.addListener(alarm=>{if(alarm.name===ANALYSIS_ALARM)pollTrackedAnalysisTasks(false).catch(()=>{})});
chrome.notifications.onClicked.addListener(notificationId=>{
if(!notificationId.startsWith(ANALYSIS_NOTIFICATION_PREFIX))return;
const taskId=notificationId.slice(ANALYSIS_NOTIFICATION_PREFIX.length);
chrome.tabs.create({url:sanshengExtensionAuth.config.pcBase+`/product-analysis/reports/${encodeURIComponent(taskId)}`});
chrome.notifications.clear(notificationId).catch(()=>{});
});
pollTrackedAnalysisTasks(false).catch(()=>{});
chrome.runtime.onMessage.addListener((m,s,send)=>{
if(m?.type==='SANSHENG_AUTH_START'){sanshengExtensionAuth.start().then(()=>send({ok:true})).catch(e=>send({ok:false,error:String(e)}));return true;}
if(m?.type==='SANSHENG_AUTH_STATE'){sanshengExtensionAuth.get().then(async a=>{const loggedIn=!!(a.access?.token||a.refresh?.token);const health=loggedIn?await checkAnalysisService(false):{ready:false,error:''};send({ok:true,loggedIn,serviceReady:health.ready,serviceError:health.error})}).catch(e=>send({ok:false,loggedIn:false,serviceReady:false,error:String(e)}));return true;}
if(m?.type==='SANSHENG_AUTH_LOGOUT'){sanshengExtensionAuth.logout().then(async()=>{await saveTrackedAnalysisTasks({});await chrome.storage.local.remove(LAST_ANALYSIS_TASK_KEY);await chrome.action.setBadgeText({text:''}).catch(()=>{});send({ok:true})}).catch(e=>send({ok:false,error:String(e)}));return true;}
if(m?.type==='PRODUCT_ANALYSIS_STATUS'){refreshAnalysisForTab(m.tabId).then(status=>send({ok:true,status})).catch(e=>send({ok:false,error:String(e)}));return true;}
if(m?.type==='PRODUCT_ANALYSIS_HISTORY'){
(async()=>{try{
const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=8');
const result=await response.json().catch(()=>null); const code=result?.Code??result?.code;
if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||'报告记录加载失败');
send({ok:true,items:result.Data?.List||result.data?.list||[]});
}catch(error){send({ok:false,error:String(error)})}})();
return true;
}
if(m?.type==='PRODUCT_ANALYSIS_NOTICE'){
(async()=>{try{
await setStatus(m.tabId,{state:m.state||'analysis_blocked',message:String(m.message||'当前暂时无法提交分析'),progress:0,productId:String(m.productId||'')});
send({ok:true});
}catch(error){send({ok:false,error:String(error)})}})();
return true;
}
if(m?.type==='ANALYZE_CURRENT_NOW'){
(async()=>{
try{
const tabId=m.tabId;
const [{result:pageInfo}]=await chrome.scripting.executeScript({
target:{tabId},world:'MAIN',
func:()=>({url:location.href,itemId:(location.href.match(/[?&]id=(\d+)/)||[])[1]||''})
});
const itemId=pageInfo?.itemId||'';
if(!itemId){send({ok:false,error:'当前页面没有识别到商品ID'});return}
let raw=null;
try{
const got=await chrome.storage.local.get('tmall_last_raw_'+itemId);
raw=got['tmall_last_raw_'+itemId]||null;
}catch(e){}
if(raw){
const hasFacts=!!(raw.product?.title||raw.product?.shop||raw.sales?.currentPrice||raw.sku?.length||raw.attributes?.length||raw.images?.main?.length);
if(!hasFacts)raw=null;
}
if(!raw){
// 没有历史raw时,做一次“轻采集”:复用 collector,但立即给 stop flag,
// 只保留当前页面已经可见/已捕获的数据,不再长时间采评论。
await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:()=>{window.__TMALL_AI_STOP_REVIEW__=true;}});
const [{result:r}]=await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:collector,args:[true]});
raw=r;
}
if(!raw){send({ok:false,error:'当前没有可用于分析的数据'});return}
raw.collection=raw.collection||{};
raw.collection.analysisMode='current_snapshot';
raw.collection.reviewCollectionComplete=!!raw.collection.reviewCollectionComplete;
raw.collection.reviewCompleteness=raw.collection.reviewCollectionComplete?'confirmed':'partial';
raw.collection.analysisStartedBeforeReviewComplete=!raw.collection.reviewCollectionComplete;
raw.collection.analysisStartedAt=new Date().toISOString();
const j=await submitProductAnalysis(raw);
await trackProductAnalysis(tabId,j,raw.product?.title||'',raw.product?.itemId||'');
send({...j,reviewCount:raw.reviews?.length||0});
}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; }
});
chrome.tabs.onUpdated.addListener((tabId,changeInfo)=>{
const url=changeInfo.url;
if(url&&isProduct(url))clearTabStatus(tabId).catch(()=>{});
if(!url||!url.startsWith(sanshengExtensionAuth.config.pcBase+sanshengExtensionAuth.config.callbackPath))return;
const parsed=new URL(url);
if(!parsed.searchParams.get('code'))return;
sanshengExtensionAuth.exchange(url).then(async ok=>{
if(!ok)return;
const pending=(await chrome.storage.local.get('sansheng_extension_auth_last_context')).sansheng_extension_auth_last_context;
await chrome.tabs.remove(tabId).catch(()=>{});
if(pending?.tabId){
await chrome.tabs.update(pending.tabId,{active:true}).catch(()=>{});
if(pending.windowId)await chrome.windows.update(pending.windowId,{focused:true}).catch(()=>{});
await setStatus(pending.tabId,{state:'idle',message:'登录成功,请在当前商品页打开扩展开始采集。',progress:0});
await chrome.action.openPopup({windowId:pending.windowId}).catch(()=>{});
}
}).catch(error=>console.error('[三笙扩展登录]',error));
});
async function run(tabId,manual,collectionMode='standard'){
const tab=await chrome.tabs.get(tabId);
if(!isProduct(tab.url)) throw new Error('当前标签页不是天猫/淘宝商品详情页');
const auth=await sanshengExtensionAuth.get();
if(!auth.access?.token&&!auth.refresh?.token){await setStatus(tabId,{state:'login_required',message:'请先登录三笙账号'});throw new Error('请先登录三笙账号')}
const [{result:pageState}]=await chrome.scripting.executeScript({
target:{tabId},world:'MAIN',
func:async()=>{
if(document.readyState==='loading') await new Promise(resolve=>document.addEventListener('DOMContentLoaded',resolve,{once:true}));
for(let i=0;i<8&&(document.body?.innerText||'').trim().length<120;i++) await new Promise(r=>setTimeout(r,350));
const body=(document.body?.innerText||'').replace(/\s+/g,' ').slice(0,5000);
const login=/login\.taobao\.com|login\.tmall\.com/i.test(location.hostname)||(/密码登录|短信登录|手机扫码登录/.test(body)&&!/商品评价|宝贝详情/.test(body));
const verification=/验证码|滑块验证|安全验证|访问过于频繁|操作频繁/.test(body);
return {url:location.href,login,verification,bodyLength:(document.body?.innerText||'').length};
}
});
if(pageState?.login){await setStatus(tabId,{state:'login_required',message:'淘宝要求登录。请先在当前浏览器登录淘宝,再刷新商品页重试。',progress:0});throw new Error('淘宝商品页未登录:请登录后刷新当前商品页再采集')}
if(pageState?.verification){await setStatus(tabId,{state:'verification_required',message:'页面出现淘宝安全验证,请手动完成后刷新重试。',progress:0});throw new Error('页面出现淘宝安全验证,请手动完成验证后重试')}
if(!isProduct(pageState?.url)){throw new Error('商品页已跳转,当前不是可采集的淘宝/天猫详情页')}
await setStatus(tabId,{state:'collecting',message:'正在采集当前商品:主图、SKU、详情、参数、问答与评论。不会采集推荐商品…',progress:10});
await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:()=>{window.__TMALL_AI_STOP_REVIEW__=false;window.__TMALL_AI_REVIEW_PROGRESS__={count:0,round:0,stagnant:0,mode:'启动',elapsedMs:0,preview:[]};}});
let collectorDone=false,collectorError=null;const startedAt=Date.now();
const collectorPromise=chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:collector,args:[manual,collectionMode]}).then(v=>v).catch(e=>{collectorError=e;return null}).finally(()=>{collectorDone=true});
while(!collectorDone){try{const rr=await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:()=>window.__TMALL_AI_REVIEW_PROGRESS__||null});const live=rr?.[0]?.result;if(live)await setStatus(tabId,{state:'collecting_reviews',message:`评论采集中:${live.count||0} 条 · ${live.mode||'处理中'}${live.stagnant?` · ${live.stagnant}轮无新增`:''}`,progress:Math.min(67,18+(Number(live.round||0)*2)),reviewLive:{...live,elapsedMs:Date.now()-startedAt}})}catch(e){}await sleep(600)}
const collectorResult=await collectorPromise;
if(collectorError)throw new Error('页面采集脚本执行失败:'+String(collectorError?.message||collectorError));
const raw=collectorResult?.[0]?.result;
if(!raw?.product?.itemId) throw new Error('未识别到 itemId,请确认商品页已正常打开并刷新后重试');
const hasProductFacts=!!(raw.product.title||raw.product.shop||raw.sales?.currentPrice||raw.sku?.length||raw.attributes?.length||raw.images?.main?.length);
if(!hasProductFacts){
await setStatus(tabId,{state:'product_empty',message:'已识别商品ID,但页面商品信息为空。请确认已登录、详情已加载后刷新重试。',progress:0});
throw new Error('商品字段为空:请确认淘宝已登录且商品详情加载完成,然后刷新页面重试');
}
try{
await chrome.storage.local.set({['tmall_last_raw_'+(raw?.product?.itemId||'unknown')]:raw});
try{
const p=(raw?.product?.parameterCollection)||{};
await chrome.storage.local.set({['tmall_param_status_'+(raw?.product?.itemId||'unknown')]:p});
}catch(e){}
}catch(e){}
await chrome.storage.local.set({['last_raw_'+raw.product.itemId]:raw});
if(!raw.collection?.reviewCollectionComplete){
const reason=raw.collection?.reviewStopReason||'unknown';
const human=reason==='human_verification_required';const stopped=reason==='user_stopped';
const noReviewData=!raw.reviews?.length&&!raw.collection?.networkReviewResponses;
const folded=raw.collection?.foldedDefaultReviewCount?`;平台另折叠 ${raw.collection.foldedDefaultReviewCount} 条默认好评,未提供逐条内容`:'';
const msg=stopped?`已终止采集,并保存当前 ${raw.reviews?.length||0} 条评论。可随时继续。`:human?`评论已保存 ${raw.reviews?.length||0} 条,页面出现验证。请手动完成验证后继续。`:noReviewData?'页面未释放可读取的评论数据。商品信息已保存;请在当前页打开“评价/评论”后再次点击继续采集。':`评论已保存 ${raw.reviews?.length||0} 条,尚未确认完整${folded}。已停止低效空转,可继续采集。`;
await setStatus(tabId,{state:stopped?'reviews_stopped':'reviews_incomplete',message:msg,progress:68,itemId:raw.product.itemId,reviewStopReason:reason,reviewLive:raw.collection?.reviewLive||null,parameterStatus:raw.product?.parameterCollection||null});
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});
const j=await submitProductAnalysis(raw);
await setStatus(tabId,{state:'analyzing',message:'正在分析全方位产品体验并生成解决方案',progress:82,taskId:j.taskId,taskUrl:j.taskUrl});
await trackProductAnalysis(tabId,j,raw.product?.title||'',raw.product?.itemId||'');
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'){
const delay=ms=>new Promise(r=>setTimeout(r,ms));
const text=(el)=>el?.innerText?.trim()||el?.textContent?.trim()||'';
const clean=s=>(s||'').replace(/\s+/g,' ').trim();
const uniq=a=>[...new Set((a||[]).filter(Boolean))];
let queryRoots=[]; let queryRootsAt=0;
const refreshQueryRoots=()=>{
if(Date.now()-queryRootsAt<1200&&queryRoots.length)return;
const roots=[document];
for(let i=0;i<roots.length;i++){
try{for(const el of roots[i].querySelectorAll('*'))if(el.shadowRoot&&!roots.includes(el.shadowRoot))roots.push(el.shadowRoot)}catch(e){}
}
queryRoots=roots;queryRootsAt=Date.now();
};
const $all=s=>{refreshQueryRoots();const out=[];for(const root of queryRoots){try{out.push(...root.querySelectorAll(s))}catch(e){}}return [...new Set(out)]};
const assetUrl=u=>{u=String(u||'').trim();if(u.startsWith('//'))u='https:'+u;return u.replace(/&amp;/g,'&')};
const srcOf=img=>assetUrl(img?.currentSrc||img?.src||img?.getAttribute?.('data-src')||img?.getAttribute?.('data-ks-lazyload')||img?.getAttribute?.('data-lazyload-src')||'');
const url=new URL(location.href); const itemId=url.searchParams.get('id')||location.href.match(/[?&]id=(\d+)/)?.[1]||''; const skuId=url.searchParams.get('skuId')||'';
const bodyClone=document.body?.cloneNode(true);
try{bodyClone?.querySelectorAll?.('[class*=recommend],[class*=Recommend],[class*=guess],[class*=Guess],[class*=related],[class*=Related],[class*=similar],[class*=Similar],[class*=猜你喜欢],[id*=recommend],[id*=guess]').forEach(n=>n.remove())}catch(e){}
const bodyText=clean(bodyClone?.innerText||document.body?.innerText||'');
const captured=[];
const reviews=[]; const questions=[];
const reviewCap=collectionMode==='quick'?300:(collectionMode==='full'?1500:800);
const reviewNetMeta={totals:[],hasMoreFalse:false,lastPageFlags:[],pageHints:[],networkReviewObjects:0,networkResponses:0};
const reviewStartedAt=Date.now();
const processedCaptureKeys=new Set();
function dedupReviewCount(){const m=new Set();for(const r of reviews){const k=clean(r.content||'').replace(/\s+/g,'').slice(0,320);if(k)m.add(k)}return m.size}
function publishReviewProgress(round=0,stagnant=0,mode='DOM',extra={}){const seen=new Set(),preview=[];for(let i=reviews.length-1;i>=0&&preview.length<3;i--){const c=clean(reviews[i]?.content||'');const k=c.replace(/\s+/g,'').slice(0,320);if(k&&!seen.has(k)){seen.add(k);preview.push(c.slice(0,70)+(c.length>70?'…':''))}}window.__TMALL_AI_REVIEW_PROGRESS__={
count:dedupReviewCount(),
rawCount:reviews.length,
networkReviewObjects:reviewNetMeta.networkReviewObjects,
networkResponses:reviewNetMeta.networkResponses,
round,stagnant,mode,
elapsedMs:Date.now()-reviewStartedAt,
preview,
capturePolicy:'passive_code_and_network',
extraRequestsCreated:0,
safety:'no_api_replay_no_auth_bypass_no_rate_limit_bypass',
...extra
}}
const looksQuestion=(c)=>{c=clean(c);return /问大家|更多回答|查看全部问答|大家问|^问[::\s]|请问|请教|会不会|有没有|能不能|是否|怎么|如何|多少|适合|兼容|保质期|[??]$/.test(c)};
const addQuestion=(o,source='unknown')=>{ const q=clean(o?.question||o?.title||o?.ask||o?.content||o?.text||''); const a=clean(o?.answer||o?.reply||o?.answers?.[0]?.content||''); if(q.length>2)questions.push({question:q,answer:a,source}); };
const reviewImageUrl=v=>assetUrl(typeof v==='string'?v:(v?.url||v?.picUrl||v?.imageUrl||v?.src||v?.bigUrl||v?.thumbnail||''));
const validReviewImage=u=>/^https?:\/\//.test(u)&&!/(?:avatar|sns_logo|credit|logo|icon|defaultRate|sprite|\/tps\/|\/tfs\/TB1[^/]+-\d{1,4}-\d{1,4}\.png)/i.test(u);
const addReview=(o,source='unknown')=>{ if(!o)return; const c=clean(o.content||o.rateContent||o.feedback||o.comment||o.text||o.contentText||''); if(c.length<3)return; if(looksQuestion(c)){addQuestion({question:c},source);return;} const media=uniq((o.images||o.pics||o.photos||[]).map(reviewImageUrl)).filter(validReviewImage).slice(0,24);reviews.push({content:c,appendContent:clean(o.appendContent||o.append||o.appendComment||''),rating:o.rating||o.rate||o.star||o.score||o.starLevel||'',sku:clean(o.sku||o.skuInfo||o.auctionSku||o.skuMap||''),date:clean(o.date||o.time||o.gmtCreate||o.rateDate||''),images:media,likes:o.likes||o.likeCount||0,source});};
function scanPageMeta(obj){
if(!obj||typeof obj!=='object'||Array.isArray(obj))return;
for(const [k,v] of Object.entries(obj)){
const key=String(k).toLowerCase();
if(/^(total|totalcount|total_count|reviewcount|ratecount|commentcount|totalnum|totalnumber)$/.test(key)){
const n=Number(String(v).replace(/,/g,'')); if(Number.isFinite(n)&&n>=0&&n<10000000)reviewNetMeta.totals.push(n);
}
if(/^(hasmore|has_more|hasnext|has_next|hasnextpage|has_next_page|more)$/.test(key) && (v===false||v===0||v==='false'||v==='0')) reviewNetMeta.hasMoreFalse=true;
if(/^(lastpage|islast|is_last|islastpage|is_last_page|last|end|isend|is_end)$/.test(key) && (v===true||v===1||v==='true'||v==='1')) reviewNetMeta.lastPageFlags.push(true);
if(/^(page|pageno|page_no|pagenum|page_num|currentpage|current_page)$/.test(key)){
const n=Number(v); if(Number.isFinite(n)&&n>0)reviewNetMeta.pageHints.push(n);
}
}
}
function walk(obj,depth=0,sourceUrl=''){
if(!obj||depth>10)return;
if(Array.isArray(obj)){for(const x of obj.slice(0,2500))walk(x,depth+1,sourceUrl);return;}
if(typeof obj!=='object')return;
scanPageMeta(obj);
const ks=Object.keys(obj); const qaUrl=/ask|question|wdj|qa/i.test(sourceUrl||'');
const hasQ=ks.some(k=>/question|ask/i.test(k)); const hasA=ks.some(k=>/answer|reply/i.test(k));
if(qaUrl || (hasQ&&hasA)) addQuestion(obj,sourceUrl||'network');
else if(ks.some(k=>/rateContent|feedback|comment|contentText|reviewContent|rateText/i.test(k)) || ((('content'in obj)||('text'in obj))&&(/review|rate|comment|feedback|evaluate|评价|评论/i.test(sourceUrl||'') || ks.some(k=>/star|rating|sku|rate|append|user|buyer|date|time|pic|photo/i.test(k))))){
const before=reviews.length; addReview(obj,sourceUrl||'network'); if(reviews.length>before)reviewNetMeta.networkReviewObjects++;
}
for(const k of ks){ const v=obj[k]; if(v&&typeof v==='object')walk(v,depth+1,sourceUrl); }
}
// V8.1.5:评论网络监听由 page-review-hook.js 在 document_start 提前安装。
// 这里禁止再次重置 __TMALL_AI_CAPTURE__,否则会丢失用户打开商品页后已经收到的评论响应。
// 如果极端情况下 MAIN-world hook 未安装,只记录状态并继续使用 DOM / inline JSON 兜底,不主动重放接口。
if(!Array.isArray(window.__TMALL_AI_CAPTURE__)) window.__TMALL_AI_CAPTURE__=[];
if(!Array.isArray(window.__TMALL_AI_SOURCE_SNAPSHOTS__)) window.__TMALL_AI_SOURCE_SNAPSHOTS__=[];
const visible=(el)=>{try{const r=el.getBoundingClientRect?.();return !r||(r.width>0&&r.height>0)}catch(e){return true}};
const textControl=(patterns)=>{
const matches=$all('button,a,[role=tab],[role=button],li,div,span').filter(el=>{
const t=clean(text(el));
return visible(el)&&t&&t.length<=36&&patterns.some(pattern=>pattern.test(t));
});
return matches.sort((a,b)=>{
const ai=/^(BUTTON|A)$/.test(a.tagName)||a.getAttribute('role')==='tab'||a.getAttribute('role')==='button';
const bi=/^(BUTTON|A)$/.test(b.tagName)||b.getAttribute('role')==='tab'||b.getAttribute('role')==='button';
return Number(bi)-Number(ai)||clean(text(a)).length-clean(text(b)).length;
})[0];
};
const activatePanel=async(patterns)=>{
const control=textControl(patterns);if(!control)return false;
try{control.scrollIntoView({block:'center'});await delay(220);control.click();await delay(1000);queryRootsAt=0;return true}catch(e){return false}
};
const reviewDrawerOpen=()=>{
const scrollBox=$all('[class*=comments],[class*=Comments],[class*=commentList],[class*=CommentList]').find(el=>{
try{const style=getComputedStyle(el);return visible(el)&&/(auto|scroll)/.test(style.overflowY)&&el.clientHeight>180}
catch(e){return false}
});
const drawer=$all('[class*=Drawer],[class*=drawer]').find(el=>visible(el)&&/用户评价/.test(clean(text(el))));
return Boolean(scrollBox&&drawer);
};
const clickControl=async(el,waitMs=900)=>{
if(!el)return false;
try{
el.scrollIntoView({block:'center',inline:'nearest'});await delay(220);
el.click();await delay(waitMs);queryRootsAt=0;return true;
}catch(e){return false}
};
const openReviewDrawer=async()=>{
if(reviewDrawerOpen())return true;
for(let attempt=0;attempt<4;attempt++){
queryRootsAt=0;
const showAll=$all('button,a,[role=button],div,span').filter(el=>{
const t=clean(text(el));return visible(el)&&/^(查看全部评价|查看全部评论|全部评价|全部评论)$/.test(t);
}).sort((a,b)=>{
const score=el=>/ShowButton/i.test(String(el.className||''))?4:(/^(BUTTON|A)$/.test(el.tagName)||el.getAttribute('role')==='button'?3:/footer/i.test(String(el.className||''))?1:2);
return score(b)-score(a)||clean(text(b)).length-clean(text(a)).length;
})[0];
if(showAll){publishReviewProgress(pagesVisited,0,'打开全部评价');await clickControl(showAll,1200);if(reviewDrawerOpen())return true;}
const reviewTab=$all('button,a,[role=tab],[role=button],div,span').filter(el=>visible(el)&&clean(text(el))==='用户评价').sort((a,b)=>{
const ac=/tabTitleItem/i.test(String(a.className||''));const bc=/tabTitleItem/i.test(String(b.className||''));return Number(bc)-Number(ac);
})[0];
if(reviewTab){publishReviewProgress(pagesVisited,0,'定位用户评价');await clickControl(/tabTitle/i.test(String(reviewTab.parentElement?.className||''))?reviewTab.parentElement:reviewTab,700);}
else{window.scrollBy(0,700);await delay(500)}
}
return reviewDrawerOpen();
};
// V5: Collect all currently accessible effective reviews. Resume from page-local checkpoint when possible.
const checkpointKey='__TMALL_AI_REVIEWS_V62__'+itemId;
async function idbOpen(){return await new Promise((resolve,reject)=>{try{const req=indexedDB.open('TMALL_AI_V62',1);req.onupgradeneeded=()=>{const db=req.result;if(!db.objectStoreNames.contains('checkpoints'))db.createObjectStore('checkpoints')};req.onsuccess=()=>resolve(req.result);req.onerror=()=>reject(req.error)}catch(e){reject(e)}})}
async function loadCheckpoint(){try{const db=await idbOpen();return await new Promise((resolve,reject)=>{const tx=db.transaction('checkpoints','readonly');const req=tx.objectStore('checkpoints').get(checkpointKey);req.onsuccess=()=>resolve(req.result||{});req.onerror=()=>reject(req.error)})}catch(e){try{return JSON.parse(localStorage.getItem(checkpointKey)||'{}')||{}}catch(_){return {}}}}
async function saveCheckpoint(data){try{const db=await idbOpen();await new Promise((resolve,reject)=>{const tx=db.transaction('checkpoints','readwrite');tx.objectStore('checkpoints').put(data,checkpointKey);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)})}catch(e){try{localStorage.setItem(checkpointKey,JSON.stringify({...data,reviews:(data.reviews||[]).slice(-1500),questions:(data.questions||[]).slice(-500)}))}catch(_){}}}
let checkpoint=await loadCheckpoint(); checkpoint=checkpoint||{};
for(const r of (checkpoint.reviews||[])) addReview(r); for(const q of (checkpoint.questions||[])) addQuestion(q,q.source||'checkpoint');
let collectionComplete=false, stopReason='unknown', pagesVisited=Number(checkpoint.round||0), explicitEnd=false;
const domReviewSeen=new Set();
function collectVisibleReviews(){
const selectors=[
'[class^="Comment--"]','[class^="comment--"]','[class*=rate-item]','[class*=rateItem]','[class*=review-item]','[class*=reviewItem]',
'[class*=comment-item]','[class*=commentItem]','[class*=feedback-item]','[class*=feedbackItem]',
'[data-testid*=review-card]','[data-testid=review]','[class*=ReviewItem]','[class*=CommentItem]','[class*=RateItem]','[class*=reviewCard]','[class*=commentCard]','[class*=rateCard]'
];
const rawNodes=uniq(selectors.flatMap(s=>{try{return $all(s)}catch(e){return[]}}));
const genericNodes=$all('article,[role=article],li,section,div').filter(el=>{
const raw=String(el.innerText||'').replace(/\u00a0/g,' ').trim();
return visible(el)&&raw.length>=10&&raw.length<=1500&&/20\d{2}[-/.年]\s*\d{1,2}[-/.月]\s*\d{1,2}/.test(raw)&&/[。!?!?,,]/.test(raw);
});
const genericAtomic=genericNodes.filter(n=>![...n.children].some(child=>genericNodes.includes(child)&&String(child.innerText||'').trim().length>=10));
const nodes=uniq([...rawNodes,...genericAtomic]).filter(n=>!rawNodes.some(o=>o!==n&&o.contains?.(n))&&!genericAtomic.some(o=>o!==n&&o.contains?.(n))); // 只保留独立评论卡,避免父子容器重复/合并
for(const b of nodes){
const contentNode=b.querySelector?.('[class^="content--"],[class^="Content--"],[class*=comment-content],[class*=review-content],[class*=rate-content],[data-testid*=content]');
const c=clean(text(contentNode)||text(b));
if(c.length<8||c.length>1800) continue;
const k=c.replace(/\s+/g,'').slice(0,240);
if(domReviewSeen.has(k)) continue;
domReviewSeen.add(k);
const ratingText=clean(b.querySelector?.('[class*=star],[class*=score],[class*=rating]')?.getAttribute?.('aria-label')||text(b.querySelector?.('[class*=star],[class*=score],[class*=rating]')));
const rm=ratingText.match(/([1-5](?:\.\d)?)/);
const sku=clean(text(b.querySelector?.('[class*=sku],[class*=spec],[class*=auction]')));
const metaText=clean(text(b.querySelector?.('time,[class*=date],[class*=time],[class^="meta--"]')));
const date=metaText.match(/20\d{2}[^\d]?\d{1,2}[^\d]?\d{1,2}/)?.[0]||metaText;
const purchased=metaText.match(/已购[::]?\s*(.+)$/)?.[1]||'';
const appendContent=clean((clean(text(b)).match(/(?:追评|追加评价)[::]\s*(.{2,1200})/)||[])[1]||'');
const images=uniq([...b.querySelectorAll?.('img')||[]].map(srcOf)).filter(validReviewImage).slice(0,16);
addReview({content:c,appendContent,rating:rm?.[1]||'',sku:purchased||sku,date,images},'dom_visible');
}
}
function scrollReviewContainers(){
let moved=false;
const boxes=$all('[class*=comments],[class*=Comments],[class*=commentList],[class*=CommentList],[class*=drawer],[class*=Drawer]').filter(el=>{
try{return visible(el)&&el.scrollHeight>el.clientHeight+8}
catch(e){return false}
});
for(const box of boxes){
try{
const old=box.scrollTop; const step=Math.max(420,Math.floor(box.clientHeight*.82));
box.scrollTop=Math.min(box.scrollHeight-box.clientHeight,old+step);
if(box.scrollTop>old+2)moved=true;
box.dispatchEvent(new Event('scroll',{bubbles:true}));
}catch(e){}
}
return moved;
}
function collectVisibleQuestions(){
const selectors=['[class*=question]','[class*=Question]','[class*=ask]','[class*=Ask]','[class*=qa]','[class*=Qa]'];
const nodes=uniq(selectors.flatMap(sel=>{try{return $all(sel)}catch(e){return[]}}));
for(const b of nodes){const c=clean(text(b));if(c.length>=4&&c.length<=1800&&(/问大家|更多回答|查看全部问答|请问|请教|会不会|有没有|能不能|是否|怎么|如何|多少|适合|兼容|保质期|[??]/.test(c)))addQuestion({question:c},'dom_question');}
}
// Trigger review/question areas if clickable text exists.
// 先按用户通常的顺序打开详情/规格区域;新版页面常把参数放在折叠面板内。
await activatePanel([/^(?:宝贝)?详情$/,/商品详情/,/规格参数|商品参数|产品参数/]);
// 必须确认“查看全部评价”抽屉已打开;只点“用户评价”页签不算成功。
const reviewDrawerWasOpened=await openReviewDrawer();
// V8.0.4:实时、可终止、快速停滞检测。优先分页/加载更多,不再长时间整页空转。
let stagnant=0;const MAX_ROUNDS=40;publishReviewProgress(pagesVisited,0,reviewDrawerWasOpened?'评论抽屉已打开':'读取页面可见评论',{reviewDrawerOpened:reviewDrawerWasOpened});
for(let attempt=0;attempt<MAX_ROUNDS;attempt++){
const round=pagesVisited;pagesVisited+=1;if(window.__TMALL_AI_STOP_REVIEW__){stopReason='user_stopped';publishReviewProgress(round,stagnant,'用户终止');break}
const before=dedupReviewCount();const capBefore=(window.__TMALL_AI_CAPTURE__||[]).length;collectVisibleReviews();collectVisibleQuestions();publishReviewProgress(round,stagnant,'读取当前评论');
const navNodes=$all('button,a,span,div,[role=button]');
const next=navNodes.find(el=>{const t=clean(text(el));const dis=el.getAttribute?.('disabled')!=null||el.getAttribute?.('aria-disabled')==='true'||/disabled|next-disabled/.test(String(el.className||''));const r=el.getBoundingClientRect?.();return !dis&&t&&t.length<20&&/下一页|下页|查看更多|加载更多|更多评价|展开更多|下一批/.test(t)&&(!r||(r.width>0&&r.height>0))});
const disabledNext=navNodes.find(el=>{const t=clean(text(el));const dis=el.getAttribute?.('disabled')!=null||el.getAttribute?.('aria-disabled')==='true'||/disabled|next-disabled/.test(String(el.className||''));return dis&&/下一页|下页/.test(t)});
if(next){try{next.scrollIntoView({block:'center'});await delay(180);next.click();publishReviewProgress(round,stagnant,'加载下一批');await delay(1100+Math.floor(Math.random()*500))}catch(e){}}
else{const rev=document.querySelector('[class*=review],[class*=rate],[class*=comment],[data-testid*=review]');try{rev?.scrollIntoView({block:'center'})}catch(e){}for(let i=0;i<4;i++){if(window.__TMALL_AI_STOP_REVIEW__)break;window.scrollBy(0,650);scrollReviewContainers();await delay(240);collectVisibleReviews();collectVisibleQuestions()}await delay(550)}
if(window.__TMALL_AI_STOP_REVIEW__){stopReason='user_stopped';publishReviewProgress(round,stagnant,'用户终止');break}
// 每轮即时解析已捕获网络响应
const capNow=(window.__TMALL_AI_CAPTURE__||[]).slice(-120);for(const c of capNow){try{const ck=String(c.__safeKey||[c.method||'',c.url||'',String(c.text||'').slice(0,240)].join('|'));if(processedCaptureKeys.has(ck))continue;processedCaptureKeys.add(ck);if(/review|rate|comment|feed/i.test(c.url||''))reviewNetMeta.networkResponses++;let z=(c.text||'').replace(/^\s*[\w$.]+\(/,'').replace(/\)\s*;?\s*$/,'');try{walk(JSON.parse(z),0,c.url||'')}catch(_){const m=z.match(/\{[\s\S]*\}/);if(m)try{walk(JSON.parse(m[0]),0,c.url||'')}catch(__){}}}catch(e){}}
collectVisibleReviews();collectVisibleQuestions();const after=dedupReviewCount();stagnant=after<=before?stagnant+1:0;publishReviewProgress(round+1,stagnant,next?'翻页/加载更多':'短滚动',{lastDelta:after-before});
if(collectionMode!=='full' && after>=reviewCap){collectionComplete=true;explicitEnd=true;stopReason='sample_cap';publishReviewProgress(round+1,stagnant,collectionMode==='quick'?'已采够快速样本':'已采够标准样本');break}
const bodyNow=clean(document.body?.innerText||'');const noMoreText=/没有更多(?:评价|评论)|已显示全部(?:评价|评论)|到底了|暂无更多(?:评价|评论)/.test(bodyNow);const captcha=/验证码|滑块|安全验证|访问过于频繁|操作频繁/.test(bodyNow);
if(captcha){stopReason='human_verification_required';publishReviewProgress(round+1,stagnant,'需要人工验证');break}
if((disabledNext||noMoreText)&&stagnant>=1){explicitEnd=true;collectionComplete=true;stopReason='explicit_last_page';publishReviewProgress(round+1,stagnant,'页面明确结束');break}
// 连续4轮没有新增才停止;给异步评论接口和虚拟列表更多响应时间,但仍避免长时间空转。
if(stagnant>=4){collectionComplete=false;explicitEnd=false;stopReason='no_progress_after_4_rounds';publishReviewProgress(round+1,stagnant,'无新增,停止空转');break}
await saveCheckpoint({reviews,questions,round:round+1,updatedAt:Date.now()});
}
await delay(300);
// V8.1.5 安全模式:
// 只消费页面在正常浏览过程中已经产生的真实 fetch/XHR 响应。
// 不根据 performance resource URL 主动重放请求,避免额外接口调用、重复页和频控风险。
captured.push(...(window.__TMALL_AI_CAPTURE__||[]).slice(-100));
// 同时读取页面已经内嵌的脚本/JSON快照,不产生任何网络请求。
const sourceSnapshots=(window.__TMALL_AI_SOURCE_SNAPSHOTS__||[]).slice(-12);
for(const snap of sourceSnapshots){
captured.push({
url:'page-source://inline-script',
method:'SOURCE',
status:200,
text:String(snap?.text||'').slice(0,1200000),
passive:true
});
}
for(const c of captured){
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 s=c.text||''; s=s.replace(/^\s*[\w$.]+\(/,'').replace(/\)\s*;?\s*$/,'');
try{walk(JSON.parse(s),0,c.url||'')}catch(e){ const m=s.match(/\{[\s\S]*\}/);if(m)try{walk(JSON.parse(m[0]),0,c.url||'')}catch(_){}}
}
// DOM fallback:仍按“单个评论卡”解析,不读取整块评价容器,避免把多条买家评论合并。
collectVisibleReviews(); collectVisibleQuestions();
// 评论采集完成后再切换问大家,读取页面正常加载出来的问答,不额外调用接口。
const qaNow=$all('a,button,div,span,[role=tab]').find(el=>{const t=clean(text(el));return t&&t.length<32&&/问大家|大家问/.test(t)});
if(qaNow){
try{
qaNow.scrollIntoView({block:'center'});await delay(220);qaNow.click();await delay(1000);
for(let i=0;i<3;i++){collectVisibleQuestions();window.scrollBy(0,420);await delay(220)}
collectVisibleQuestions();
}catch(e){}
}
// 评论完成后重新打开详情/规格面板,避免问大家面板覆盖参数区域。
await activatePanel([/^(?:宝贝)?详情$/,/商品详情/,/规格参数|商品参数|产品参数/]);
// 像正常浏览一样有限滚动详情容器,触发页面自身的图片懒加载;不创建额外接口请求。
const detailRootSelectors=['#J_DivItemDesc','#description','[id="description"]','[class*="detailContent"]','[class*="DetailContent"]','[class*="detail-content"]','[class*="descV8"]','[class^="desc--"]','[data-testid*="product-description"]','[class*="ProductDetail"]','[class*="productDetail"]','[class*="detailDesc"]','[class*="DetailDesc"]','[class*="moduleDesc"]','[class*="ModuleDesc"]','[class*="contentDetail"]','[class*="ContentDetail"]','[class*="aplus"]','[class*="Aplus"]'];
const detailRoots=uniq(detailRootSelectors.flatMap(sel=>{try{return $all(sel)}catch(e){return[]}}));
const detailScanUrls=[];
const scanRootImages=root=>{try{for(const img of root.querySelectorAll('img')){const u=srcOf(img);if(u&&!detailScanUrls.includes(u))detailScanUrls.push(u)}}catch(e){}};
for(const root of detailRoots.slice(0,4)){
try{
const box=root.getBoundingClientRect();const top=Math.max(0,window.scrollY+box.top-120);const height=Math.max(box.height,root.scrollHeight||0);const rounds=Math.min(14,Math.max(1,Math.ceil(height/700)));
for(let i=0;i<rounds;i++){window.scrollTo({top:top+Math.min(height-1,i*700),behavior:'auto'});await delay(150);scanRootImages(root)}
}catch(e){}
}
// Product facts: DOM first, then schema/meta and page-owned inline state for the 2025 Taobao/Tmall detail app.
const freshClone=document.body?.cloneNode(true);
try{freshClone?.querySelectorAll?.('[class*=recommend],[class*=Recommend],[class*=guess],[class*=Guess],[class*=related],[class*=Related],[class*=similar],[class*=Similar],[id*=recommend],[id*=guess]').forEach(n=>n.remove())}catch(e){}
const liveBodyRaw=freshClone?.innerText||document.body?.innerText||'';
const liveBodyText=clean(liveBodyRaw);
const firstText=(selectors)=>{for(const sel of selectors){try{const v=clean(text(document.querySelector(sel)));if(v)return v}catch(e){}}return''};
const meta=(selectors)=>{for(const sel of selectors){try{const v=clean(document.querySelector(sel)?.getAttribute?.('content')||'');if(v)return v}catch(e){}}return''};
const scalar=v=>(typeof v==='string'||typeof v==='number')?clean(String(v)):'';
const structured={titles:[],shops:[],brands:[],categories:[],prices:[],sold:[],sku:[],attrs:[],promos:[],images:[],imageRecords:[]};
const addSignal=(group,value,max=300)=>{value=scalar(value);if(value&&value.length<=max&&!structured[group].includes(value))structured[group].push(value)};
const roots=[];
for(const script of $all('script[type="application/ld+json"],script#__NEXT_DATA__,script[type="application/json"]')){
const raw=String(script.textContent||'').trim();if(!raw||raw.length>6000000)continue;
try{roots.push(JSON.parse(raw))}catch(e){}
}
for(const key of ['__INITIAL_STATE__','__INIT_DATA__','__DATA__','__SSR_DATA__','__APOLLO_STATE__']){
try{const value=window[key];if(value&&typeof value==='object')roots.push(value)}catch(e){}
}
const seenStructured=new WeakSet();let structuredNodes=0;
function walkProduct(obj,path='',depth=0){
if(!obj||typeof obj!=='object'||depth>11||structuredNodes>30000||seenStructured.has(obj))return;
seenStructured.add(obj);structuredNodes++;
if(Array.isArray(obj)){for(const x of obj.slice(0,2000))walkProduct(x,path,depth+1);return}
const objName=scalar(obj.attrName||obj.propertyName||obj.propName||obj.name||obj.label||obj.key);
const objValue=scalar(obj.attrValue||obj.propertyValue||obj.valueName||obj.value||obj.text);
if(objName&&objValue&&/(?:attr|param|property|prop|feature|规格|参数|属性)/i.test(path))structured.attrs.push({name:objName,value:objValue});
for(const [key,value] of Object.entries(obj)){
const lower=String(key).toLowerCase(),next=path?path+'.'+lower:lower;
if(value&&typeof value==='object'){walkProduct(value,next,depth+1);continue}
const valueText=scalar(value);if(!valueText)continue;
if(/^(title|itemtitle|item_title|auctiontitle|producttitle|subject|name)$/.test(lower)&&/(?:item|product|auction|detail|schema|offers?|^$)/i.test(path))addSignal('titles',valueText,260);
if(/^(shopname|shop_name|shoptitle|sellername|seller_name|storename|store_name|nick)$/.test(lower))addSignal('shops',valueText,160);
if(/^(brand|brandname|brand_name)$/.test(lower))addSignal('brands',valueText,120);
if(/^(category|categoryname|category_name|catname)$/.test(lower))addSignal('categories',valueText,160);
if(/^(price|pricetext|price_text|promotionprice|promotion_price|saleprice|sale_price|pricevalue)$/.test(lower))addSignal('prices',valueText,80);
if(/^(sold|soldcount|sold_count|sales|salescount|sales_count|monthsold|monthlysales)$/.test(lower))addSignal('sold',valueText,80);
if(/(?:sku|property|prop|spec)/i.test(path)&&/^(name|text|title|value|valuename|value_name)$/.test(lower))addSignal('sku',valueText,100);
if(/(?:promo|promotion|activity|coupon|discount|benefit|service)/i.test(next))addSignal('promos',valueText,220);
if(/(?:image|img|pic|picture|thumb)/i.test(lower)&&/^https?:\/\/|^\/\//i.test(valueText)){
const imageUrl=assetUrl(valueText);addSignal('images',imageUrl,1200);
if(imageUrl&&!structured.imageRecords.some(x=>x.url===imageUrl&&x.path===next))structured.imageRecords.push({url:imageUrl,path:next});
}
}
}
for(const root of roots)walkProduct(root);
const relevantScripts=$all('script').map(s=>String(s.textContent||'')).filter(s=>s.length>20&&s.length<=6000000&&(s.includes(itemId)||/itemTitle|auctionTitle|shopName|skuBase|priceInfo|promotion/i.test(s))).slice(0,16);
const scriptCorpus=relevantScripts.join('\n').slice(0,10000000);
const scriptString=(keys,max=300)=>{
for(const key of keys){
const escaped=key.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
const m=scriptCorpus.match(new RegExp('["\\\']'+escaped+'["\\\']\\s*:\\s*"((?:\\\\.|[^"\\\\]){1,'+max+'})"','i'));
if(m){try{return clean(JSON.parse('"'+m[1]+'"'))}catch(e){return clean(m[1].replace(/\\u([0-9a-f]{4})/gi,(_,h)=>String.fromCharCode(parseInt(h,16))))}}
}
return'';
};
const validProductTitle=value=>{
value=clean(value).replace(/[-_–—|]\s*(淘宝网|天猫|淘宝).*$/,'');
if(value.length<4||value.length>260)return'';
if(/用户评价|累计评价|商品评价|问大家|参数信息|规格参数|图文详情|商品详情|登录|验证码/.test(value))return'';
return value;
};
const titleCandidates=[
meta(['meta[property="og:title"]','meta[name="twitter:title"]']),
...structured.titles,
scriptString(['itemTitle','auctionTitle','productTitle']),
firstText(['[data-testid="item-title"]','[data-testid*=item-title]','h1[class*="ItemTitle"]','h1[class*="itemTitle"]','[class*="ItemTitle--"]','[class*="itemTitle--"]','h1']),
clean(document.title)
];
const title=titleCandidates.map(validProductTitle).find(Boolean)||'';
const visiblePriceTexts=$all('[class*=price],[class*=Price],[data-testid*=price]').filter(el=>{const r=el.getBoundingClientRect?.();return !r||(r.width>0&&r.height>0&&r.top<1600)}).map(text).map(clean).filter(x=>/\d/.test(x)).slice(0,30);
const metaPrice=meta(['meta[property="product:price:amount"]','meta[property="og:price:amount"]','meta[itemprop="price"]']);
const visiblePrice=(visiblePriceTexts.join(' ')+' '+liveBodyText.slice(0,6000)).match(/(?:到手价|券后价|活动价|促销价|价格)?\s*[¥¥]\s*(\d{1,7}(?:\.\d{1,2})?)/)?.[1]||(liveBodyText.slice(0,6000).match(/(?:到手价|券后价|活动价|促销价|价格)\s*[::]?\s*(\d{1,7}(?:\.\d{1,2})?)/)||[])[1]||'';
const fallbackPrice=structured.prices[0]||scriptString(['promotionPrice','salePrice','priceText','price'],80);
const priceValue=(String(metaPrice).match(/\d{1,7}(?:\.\d{1,2})?/)||[])[0]||visiblePrice||(String(fallbackPrice).match(/\d{1,7}(?:\.\d{1,2})?/)||[])[0]||'';
const soldMatch=liveBodyText.match(/(?:已售|销量|月销|付款|成交)\s*([\d,.万+]+)/);
const soldValue=soldMatch?.[1]||structured.sold[0]||scriptString(['soldCount','salesCount','monthSold','monthlySales'],80);
const rankMatch=liveBodyText.match(/(?:热销榜|榜单|排名)[^\d]{0,8}(?:第)?\s*(\d+)\s*名?/);
const shop=firstText(['[data-testid*=shop]','[class*=shopName]','[class*=shop-name]','[class*=ShopName]','[class*=sellerName]','[class*=SellerName]','[class*=storeName]','[class*=StoreName]'])||meta(['meta[name="seller"]'])||structured.shops[0]||scriptString(['shopName','shopTitle','sellerName','storeName'],160);
const domSkuTexts=$all('[class*=sku] button,[class*=sku] li,[class*=sku] label,[class*=sku] [role=button],[class*=Sku] button,[class*=Sku] li,[class*=prop] button,[class*=Prop] button,[data-testid*=sku] button').map(text).map(clean);
const skuTexts=uniq([...domSkuTexts,...structured.sku]).filter(x=>x&&x.length<100&&!/加入购物车|立即购买|领券购买|购买|收藏|客服|分享|确定|取消|数量/.test(x)).slice(0,160);
// 参数必采:同时读取属性列表、规格参数、表格、dt/dd 等常见结构;只保留短文本,避免把整块详情正文误当参数。
const paramPairs=[];
const validParam=(name,value)=>{
if(!name||!value||name.length>60||value.length>220)return false;
if(/^\d+$/.test(name)||/(?:window\.|console\.|function\b|frontend_data|performance\.|[{}\[\]"'=;<>])/i.test(name))return false;
if(/(?:console\.log|window\.|performance\.now|frontend_data)/i.test(value))return false;
return /[\u4e00-\u9fffA-Za-z]/.test(name);
};
const pushParam=(name,value)=>{name=clean(name);value=clean(value);if(!validParam(name,value))return;const k=name+'\u0000'+value;if(!paramPairs.some(x=>x._k===k))paramPairs.push({name,value,_k:k});};
const parseParamText=(s)=>{s=clean(s);if(!s||s.length>260)return;const m=s.match(/^([^::]{1,60})[::]\s*(.{1,220})$/);if(m)pushParam(m[1],m[2]);};
for(const el of $all('[class*=attr] li,[class*=Attr] li,[class*=parameter] li,[class*=Parameter] li,[class*=prop] li,[class*=Prop] li,[class*=specification] li,[class*=Specification] li,[class*=attribute] li,[class*=Attribute] li')) parseParamText(text(el));
for(const dl of $all('dl')){const dts=[...dl.querySelectorAll('dt')],dds=[...dl.querySelectorAll('dd')];for(let i=0;i<Math.min(dts.length,dds.length);i++)pushParam(text(dts[i]),text(dds[i]));}
for(const tr of $all('table tr')){const cells=[...tr.querySelectorAll('th,td')].map(text).map(clean).filter(Boolean);if(cells.length>=2)pushParam(cells[0],cells.slice(1).join(' '));}
// 兜底读取页面中常见“键:值”短行。
const paramTextNodes=$all('[class*=attr],[class*=parameter],[class*=specification],[class*=attribute]').slice(0,80);
for(const el of paramTextNodes){for(const line of String(text(el)||'').split(/\n+/).slice(0,80))parseParamText(line);}
// 淘宝新版经常移除参数容器 class,但可见页面仍保留“字段:值”短行。
for(const line of String(liveBodyRaw||'').split(/\n+/).slice(0,5000))parseParamText(line);
for(const el of $all('li,dt,dd,tr,td,th,[role=listitem]').slice(0,3000)){
for(const line of String(el.innerText||'').split(/\n+/).slice(0,12))parseParamText(line);
}
// 当前淘宝新版参数是“值 + 字段名”或“字段名 + 值”两列,没有冒号。
for(const el of $all('[class*="emphasisParamsInfoItem--"],[class*="generalParamsInfoItem--"]').slice(0,400)){
const children=[...el.children].filter(child=>clean(child.getAttribute?.('title')||text(child)));
if(children.length<2)continue;
const values=children.map(child=>clean(child.getAttribute?.('title')||text(child)));
const isGeneral=/generalParamsInfoItem/i.test(String(el.className||''));
const label=isGeneral?values[0]:values[1];
const value=isGeneral?values[1]:values[0];
pushParam(label,value);
}
for(const pair of structured.attrs)pushParam(pair.name,pair.value);
const attrs=paramPairs.slice(0,240).map(({name,value})=>({name,value}));
const PARAM_GROUPS={
'基础身份':['品牌','品类','型号','货号','名称'],
'规格选择':['规格','尺寸','尺码','颜色','容量','净含量','口味','版本','数量','包装'],
'材质/成分/配置':['材质','面料','成分','配料','填充','配置','处理器','内存','存储'],
'功能与适用':['功能','功效','适用','肤质','兼容','性能','版型','风格','场景'],
'工艺与使用':['工艺','做工','使用','洗护','洗涤','清洗','保养','食用方法'],
'合规与服务':['产地','等级','执行标准','保质期','生产日期','认证','质保','售后']
};
function evaluateParameterCollectionJS(list){
const found=[],missing=[];const keys=(list||[]).map(x=>clean(x.name)).filter(Boolean);
for(const [canon,aliases] of Object.entries(PARAM_GROUPS)){
const hit=(list||[]).some(x=>aliases.some(a=>clean(x.name).includes(a))&&clean(x.value));
(hit?found:missing).push(canon);
}
const state=(list||[]).length>=3?'complete':((list||[]).length?'partial':'missing');
return {state,stateText:state==='complete'?'动态参数已采集':state==='partial'?'参数样本较少':'未识别到有效参数',count:(list||[]).length,keys:keys.slice(0,50),found,missing:state==='missing'?['页面动态参数']:[]};
}
const parameterCollection=evaluateParameterCollectionJS(attrs);
const promoDom=$all('[class*=promo],[class*=Promo],[class*=coupon],[class*=Coupon],[class*=activity],[class*=Activity],[class*=discount],[class*=Discount],[class*=service],[class*=Service]').map(text).map(clean);
const promoLines=String(liveBodyRaw||'').split(/\n+/).map(clean).filter(x=>x.length>=2&&x.length<=180&&/(减|券|折|赠|包邮|价保|退|换新|补贴|立减|满|活动|优惠|会员|服务)/.test(x));
const promoTexts=uniq([...promoDom,...promoLines,...structured.promos]).filter(x=>x&&x.length>=2&&x.length<=220&&/(减|券|折|赠|包邮|价保|退|换新|补贴|立减|满|活动|优惠|会员|服务)/.test(x)).slice(0,120);
const isRecImg=img=>{try{return !!img.closest('[class*=recommend],[class*=Recommend],[class*=guess],[class*=Guess],[class*=related],[class*=Related],[class*=similar],[class*=Similar],[id*=recommend],[id*=guess]')}catch(e){return false}};
const validImg=u=>/^https?:\/\//.test(u)&&!/icon|logo|avatar|sprite|shopmanager|\/tps\/|\/tfs\/TB1[^/]+-\d{1,4}-\d{1,4}\.png|(?:-|_)rate(?:\.|_|-)|rate_livephoto|tbbala|alicdn.*\.gif/i.test(u);
const deReview=new Map();
for(const r of reviews){
const k=clean(r.content).replace(/\s+/g,'').slice(0,320);if(!k)continue;
if(!deReview.has(k))deReview.set(k,{...r,images:uniq((r.images||[]).map(reviewImageUrl)).filter(validReviewImage)});
else{const old=deReview.get(k);old.images=uniq([...(old.images||[]),...(r.images||[]).map(reviewImageUrl)]).filter(validReviewImage).slice(0,24)}
}
let reviewRows=[...deReview.values()];
// 评论过多时保留最有决策价值的样本:差评、带图、最新评论优先,其余按原顺序补齐。
let reviewSampled=false;
if(reviewRows.length>reviewCap){
const scored=reviewRows.map((r,i)=>{const rating=Number(String(r.rating||'').match(/[1-5]/)?.[0]||5);const bad=rating<=3?5:0;const media=(r.images||[]).length?3:0;const date=Date.parse(String(r.date||''));const rec=Number.isFinite(date)?Math.max(0,Math.min(2,(date-Date.now()+31536000000)/31536000000)):0;return {r,i,score:bad+media+rec}});
scored.sort((a,b)=>b.score-a.score||a.i-b.i);
const keep=new Set(scored.slice(0,reviewCap).map(x=>x.i));
reviewRows=reviewRows.filter((_,i)=>keep.has(i)); reviewSampled=true;
}
const buyerCandidates=uniq(reviewRows.flatMap(r=>r.images||[])).filter(validReviewImage);
const metaImage=assetUrl(meta(['meta[property="og:image"]','meta[name="twitter:image"]','meta[itemprop="image"]']));
const isMainRegionImg=img=>{try{if(isRecImg(img)||img.closest('[class*=review],[class*=Review],[class*=rate],[class*=Rate],[class*=comment],[class*=Comment],[class*=detail],[class*=Detail],[class*=desc],[class*=Desc]'))return false;const r=img.getBoundingClientRect();return r.top<1800&&r.bottom>-80&&r.width>=34&&r.height>=34&&r.left<innerWidth*.76}catch(e){return false}};
const mainGallerySelector='[class*="PicGallery--"],[class*="picGallery--"],[class*="MainPic--"],[class*="mainPic--"],[class*="main-picture"],[class*="mainPicture"],[data-testid*="main-image"],[data-testid*="gallery"],#J_UlThumb,[class*="Gallery"],[class*="gallery"],[class*="Photo"],[class*="photo"],[class*="Thumb"],[class*="thumb"],[class*="PicList"],[class*="picList"]';
const isMainGalleryImg=img=>{try{if(isRecImg(img)||img.closest('[class*=review],[class*=Review],[class*=rate],[class*=Rate],[class*=comment],[class*=Comment],[class*=detail],[class*=Detail],[class*=desc],[class*=Desc]'))return false;const inGallery=!!img.closest(mainGallerySelector);const w=img.naturalWidth||img.width||img.getBoundingClientRect().width;const h=img.naturalHeight||img.height||img.getBoundingClientRect().height;return inGallery?(w>=34&&h>=34):isMainRegionImg(img)}catch(e){return false}};
const mainDom=$all(mainGallerySelector+' img').filter(isMainGalleryImg).map(srcOf);
const skuDom=$all('[class*="SkuContent--"] img,[class*="skuContent--"] img,[class*="SkuItem--"] img,[class*="skuItem--"] img,[data-testid*="sku"] img,[class*="sku"] [role="button"] img,[class*="Sku"] [role="button"] img').filter(img=>!isRecImg(img)).map(srcOf);
const detailDom=uniq([...detailScanUrls,...detailRoots.flatMap(root=>{try{return [...root.querySelectorAll('img')].filter(img=>!isRecImg(img)).map(srcOf)}catch(e){return[]}})]);
const structuredMain=structured.imageRecords.filter(x=>/(?:main|gallery|itempic|auctionpic|primary|header)/i.test(x.path)&&!/(?:detail|desc|review|rate|comment|sku|prop|recommend|shop)/i.test(x.path)).map(x=>x.url);
const structuredDetail=structured.imageRecords.filter(x=>/(?:detail|desc|module|content|aplus)/i.test(x.path)&&!/(?:review|rate|comment|recommend|shop)/i.test(x.path)).map(x=>x.url);
const structuredSku=structured.imageRecords.filter(x=>/(?:sku|prop|spec)/i.test(x.path)&&!/(?:review|rate|comment)/i.test(x.path)).map(x=>x.url);
const buyerSet=new Set(buyerCandidates);
const skuImages=uniq([...skuDom,...structuredSku]).filter(validImg).filter(u=>!buyerSet.has(u)).slice(0,80);const skuSet=new Set(skuImages);
const main=uniq([metaImage,...mainDom,...structuredMain]).filter(validImg).filter(u=>!buyerSet.has(u)&&!skuSet.has(u)).slice(0,24);const mainSet=new Set(main);
const detail=uniq([...detailDom,...structuredDetail]).filter(validImg).filter(u=>!buyerSet.has(u)&&!skuSet.has(u)&&!mainSet.has(u)).slice(0,160);
const buyer=buyerCandidates.slice(0,200);
const allPageCandidates=uniq($all('img').filter(img=>!isRecImg(img)).map(srcOf)).filter(validImg);
const allImgs=uniq([...main,...detail,...skuImages,...buyer]);
const provenance=[];
for(const u of main)provenance.push({url:u,group:'main',source:u===metaImage?'meta_og_image':structuredMain.includes(u)?'structured_main_gallery':'dom_main_gallery'});
for(const u of detail)provenance.push({url:u,group:'detail',source:structuredDetail.includes(u)?'structured_product_description':'dom_product_description'});
for(const u of skuImages)provenance.push({url:u,group:'sku',source:structuredSku.includes(u)?'structured_sku_selector':'dom_sku_selector'});
reviewRows.forEach((r,reviewIndex)=>{for(const u of r.images||[])if(buyerSet.has(u))provenance.push({url:u,group:'buyerShow',source:'review_record',reviewIndex:reviewIndex+1,reviewSource:r.source||'unknown'})});
const deQ=new Map(); for(const q of questions){const k=clean(q.question).replace(/\s+/g,'').slice(0,260);if(k&&!deQ.has(k))deQ.set(k,q)}
const publicRaw=(liveBodyText.match(/(?:评价|评论)[^\d]{0,8}([\d,.]+\s*万?\+?)/)||[])[1]||'';
const parseCount=v=>{v=String(v||'').replace(/,/g,'').trim();const m=v.match(/([\d.]+)\s*(万)?/);if(!m)return null;return Math.round(parseFloat(m[1])*(m[2]?10000:1))};
const publicNumeric=parseCount(publicRaw);
const foldedDefaultMatch=liveBodyText.match(/已折叠\s*([\d,.]+)\s*条[^\n]{0,30}评价/);
const defaultPraiseMatch=liveBodyText.match(/近半年\s*([\d,.]+)\s*位买家默认好评/);
const networkTotal=reviewNetMeta.totals.length?Math.max(...reviewNetMeta.totals.filter(n=>Number.isFinite(n))):null;
if(publicNumeric&&deReview.size>=publicNumeric){collectionComplete=true;explicitEnd=true;stopReason='matched_public_review_count';}
else if(networkTotal!==null&&networkTotal>0&&deReview.size>=networkTotal){collectionComplete=true;explicitEnd=true;stopReason='matched_network_review_total';}
else if((reviewNetMeta.hasMoreFalse||reviewNetMeta.lastPageFlags.length>0) && deReview.size>0){collectionComplete=true;explicitEnd=true;stopReason='network_explicit_end';}
// 严禁“无新增/无下一页控件”单独作为完整依据
const completeness=collectionComplete?'confirmed':'unconfirmed';
await saveCheckpoint({reviews:[...deReview.values()],questions:[...deQ.values()],round:pagesVisited,updatedAt:Date.now(),complete:collectionComplete,stopReason,completeness,networkTotal,publicNumeric,reviewNetMeta});
return {
meta:{platform:location.hostname.includes('tmall')?'tmall':'taobao',collectedAt:new Date().toISOString(),url:location.href,collectorVersion:'9.2.2',imageClassificationVersion:'strict-v1',networkCaptures:captured.length,structuredRoots:roots.length},
product:{itemId,skuId,title,shop,brand:structured.brands[0]||scriptString(['brandName','brand'],120),category:structured.categories[0]||scriptString(['categoryName','catName'],160),url:location.href,parameterCollection},
sales:{currentPrice:priceValue,sold:soldValue,ranking:rankMatch?.[1]||''},
sku:skuTexts.map(name=>({name})),attributes:attrs,promotions:promoTexts.map(text=>({text})),
images:{main,detail,sku:skuImages,buyerShow:buyer,all:allImgs,provenance,classification:{version:'strict-v1',classifiedCount:allImgs.length,unclassifiedCount:Math.max(0,allPageCandidates.length-allImgs.length),groupsAreDisjoint:true}},
reviews:reviewRows,questions:[...deQ.values()],pageText:liveBodyText.slice(0,120000),collection:{reviewActual:deReview.size,reviewSampled,reviewSampleCap:reviewCap,reviewCollectionMode:collectionMode,questionActual:deQ.size,reviewDrawerOpened:reviewDrawerWasOpened,reviewCollectionComplete:collectionComplete,reviewCompleteness:reviewSampled?'sampled':(collectionComplete?'confirmed':'unconfirmed'),reviewStopReason:reviewSampled?'sample_cap':stopReason,reviewPagesVisited:pagesVisited,publicReviewCount:publicRaw,publicReviewCountNumeric:publicNumeric,foldedDefaultReviewCount:foldedDefaultMatch?parseCount(foldedDefaultMatch[1]):null,defaultPraiseBuyerCount:defaultPraiseMatch?parseCount(defaultPraiseMatch[1]):null,networkReviewTotal:networkTotal,networkReviewObjects:reviewNetMeta.networkReviewObjects,networkReviewResponses:reviewNetMeta.networkResponses,networkExplicitEnd:reviewNetMeta.hasMoreFalse||reviewNetMeta.lastPageFlags.length>0,explicitEnd,reviewLive:window.__TMALL_AI_REVIEW_PROGRESS__||null}
};
}