Optimize review collection for beginners

This commit is contained in:
喻长发
2026-09-03 16:29:08 +08:00
parent 42223969db
commit 19410caf76
3 changed files with 39 additions and 25 deletions
+16 -6
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).then(x=>send(x)).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==='HEALTH'){ health().then(send); return true; } if(m?.type==='HEALTH'){ health().then(send); return true; }
}); });
async function run(tabId,manual){ async function run(tabId,manual,collectionMode='standard'){
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();
@@ -83,7 +83,7 @@ async function run(tabId,manual){
await setStatus(tabId,{state:'collecting',message:'正在采集当前商品:主图、SKU、详情、参数、问答与评论。不会采集推荐商品…',progress:10}); 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:[]};}}); 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]}).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,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)} 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));
@@ -121,7 +121,7 @@ async function run(tabId,manual){
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){ async function collector(manual=false,collectionMode='standard'){
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,6 +144,7 @@ async function collector(manual=false){
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 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();
@@ -330,6 +331,7 @@ async function collector(manual=false){
// 每轮即时解析已捕获网络响应 // 每轮即时解析已捕获网络响应
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}
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}
@@ -528,7 +530,15 @@ async function collector(manual=false){
if(!deReview.has(k))deReview.set(k,{...r,images:uniq((r.images||[]).map(reviewImageUrl)).filter(validReviewImage)}); 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)} else{const old=deReview.get(k);old.images=uniq([...(old.images||[]),...(r.images||[]).map(reviewImageUrl)]).filter(validReviewImage).slice(0,24)}
} }
const reviewRows=[...deReview.values()]; 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 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 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 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}};
@@ -571,6 +581,6 @@ async function collector(manual=false){
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,questionActual:deQ.size,reviewDrawerOpened:reviewDrawerWasOpened,reviewCollectionComplete:collectionComplete,reviewCompleteness:collectionComplete?'confirmed':'unconfirmed',reviewStopReason: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: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}
}; };
} }
+7 -7
View File
@@ -2,7 +2,7 @@
body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#f5faf9;color:#10191d}h1{font-size:18px;margin:0 0 6px;background:linear-gradient(110deg,#00d9b6,#008bd8);-webkit-background-clip:text;color:transparent}.sub{font-size:12px;color:#60757c;line-height:1.6}.card{margin-top:14px;padding:14px;background:#fff;border:1px solid #dbe8e8;border-radius:14px}.state{font-weight:800;margin-bottom:8px}.bar{height:6px;background:#e6f0ef;border-radius:8px;overflow:hidden}.bar i{display:block;height:100%;background:linear-gradient(110deg,#00d9b6,#008bd8);width:0}.btn{width:100%;margin-top:12px;border:0;background:linear-gradient(110deg,#00cdb0,#008fd8);color:#fff;padding:11px;border-radius:999px;font-weight:800;cursor:pointer}.secondary{background:#fff;color:#17434b;border:1px solid #a9cfcc}.tiny{font-size:11px;color:#71858a;margin-top:9px}.monitor{display:flex;gap:8px}.monitor .btn{flex:1}.ok{color:#008b78}.warn{color:#a66d18}.file{display:none}.live{margin-top:10px;padding:10px;background:#edf9f7;border-radius:10px;font-size:11px;line-height:1.6;color:#526a70}.live strong{font-size:12px;color:#10191d}.live-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 10px;margin-top:6px}.live-preview{margin-top:7px;padding-top:7px;border-top:1px solid #d6e8e5;max-height:78px;overflow:auto}.stop{background:#a83f49!important;color:#fff!important;border:0!important} body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#f5faf9;color:#10191d}h1{font-size:18px;margin:0 0 6px;background:linear-gradient(110deg,#00d9b6,#008bd8);-webkit-background-clip:text;color:transparent}.sub{font-size:12px;color:#60757c;line-height:1.6}.card{margin-top:14px;padding:14px;background:#fff;border:1px solid #dbe8e8;border-radius:14px}.state{font-weight:800;margin-bottom:8px}.bar{height:6px;background:#e6f0ef;border-radius:8px;overflow:hidden}.bar i{display:block;height:100%;background:linear-gradient(110deg,#00d9b6,#008bd8);width:0}.btn{width:100%;margin-top:12px;border:0;background:linear-gradient(110deg,#00cdb0,#008fd8);color:#fff;padding:11px;border-radius:999px;font-weight:800;cursor:pointer}.secondary{background:#fff;color:#17434b;border:1px solid #a9cfcc}.tiny{font-size:11px;color:#71858a;margin-top:9px}.monitor{display:flex;gap:8px}.monitor .btn{flex:1}.ok{color:#008b78}.warn{color:#a66d18}.file{display:none}.live{margin-top:10px;padding:10px;background:#edf9f7;border-radius:10px;font-size:11px;line-height:1.6;color:#526a70}.live strong{font-size:12px;color:#10191d}.live-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 10px;margin-top:6px}.live-preview{margin-top:7px;padding-top:7px;border-top:1px solid #d6e8e5;max-height:78px;overflow:auto}.stop{background:#a83f49!important;color:#fff!important;border:0!important}
.analysis-now{background:#fff;color:#1f1d1a;border:1px solid #1f1d1a} .analysis-now{background:#fff;color:#1f1d1a;border:1px solid #1f1d1a}
.mode-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px} .mode-row{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}
.mode{padding:10px;border:1px solid #ded7ce;border-radius:12px;background:#fff;cursor:pointer;font-size:12px} .mode{padding:10px;border:1px solid #ded7ce;border-radius:12px;background:#fff;cursor:pointer;font-size:12px}
.mode.active{background:#1f1d1a;color:#fff;border-color:#1f1d1a} .mode.active{background:#1f1d1a;color:#fff;border-color:#1f1d1a}
.mode small{display:block;opacity:.72;margin-top:4px;line-height:1.45} .mode small{display:block;opacity:.72;margin-top:4px;line-height:1.45}
@@ -16,7 +16,6 @@ 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"><div class="state">当前商品监测数据</div><div id="monitorStatus" class="sub">未检测</div><div class="monitor"><button class="btn secondary" id="importMonitor">添加监测数据</button><button class="btn secondary" id="clearMonitor">清除</button></div><input class="file" id="monitorFile" type="file" accept=".xlsx,.csv,.json"></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>
@@ -29,15 +28,16 @@ body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemF
</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>
<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="card">
<div class="state">分析方式</div> <div class="state">分析方式</div>
<div class="mode-row"> <div class="mode-row">
<button id="modeQuick" class="mode active">快速分析<small>当前已采数据即可开始</small></button> <button id="modeQuick" class="mode">快速看结论<small>最多 300 条,约 1 分钟</small></button>
<button id="modeFull" class="mode">完整采集<small>继续采到可确认末页</small></button> <button id="modeStandard" class="mode active">标准采集<small>最多 800 条,适合大多数商品</small></button>
<button id="modeFull" class="mode">完整采集<small>尽可能采到末页,耗时较长</small></button>
</div> </div>
<button id="analyzeNow" class="btn analysis-now">基于当前数据开始分析</button> <button id="analyzeNow" class="btn analysis-now">先看当前结论</button>
<div class="tiny">分析不会清空采集进度。评论可继续采;报告会明确标注当前评论数量与完整性。</div> <div class="tiny">系统优先保留差评、带图、最新评论和不同主题评论。</div>
</div> </div>
<script src="popup.js"></script> <script src="popup.js"></script>
</body></html> </body></html>
+16 -12
View File
@@ -1,7 +1,7 @@
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.2.5';
let backendCompatible=false; let backendCompatible=false;
const server=document.getElementById('server'),status=document.getElementById('status'),bar=document.getElementById('bar'),monitorStatus=document.getElementById('monitorStatus'),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');
async function active(){return (await chrome.tabs.query({active:true,currentWindow:true}))[0]} async function active(){return (await chrome.tabs.query({active:true,currentWindow:true}))[0]}
function itemIdFromUrl(u){try{return new URL(u).searchParams.get('id')||((u||'').match(/[?&]id=(\d+)/)||[])[1]||''}catch(e){return''}} function itemIdFromUrl(u){try{return new URL(u).searchParams.get('id')||((u||'').match(/[?&]id=(\d+)/)||[])[1]||''}catch(e){return''}}
@@ -19,7 +19,7 @@ async function refresh(){
server.className='state ok'; server.className='state ok';
} }
const t=await active(); if(!t)return; const id=itemIdFromUrl(t.url||''); const t=await active(); if(!t)return; const id=itemIdFromUrl(t.url||'');
if(!id){status.textContent='请打开天猫/淘宝商品详情页';monitorStatus.textContent='当前页面未识别商品ID';return} if(!id){status.textContent='请打开天猫/淘宝商品详情页';return}
const k='status_'+t.id; const d=(await chrome.storage.local.get(k))[k]; const k='status_'+t.id; const d=(await chrome.storage.local.get(k))[k];
if(d){status.textContent=d.message||d.state;bar.style.width=(d.progress||0)+'%';runBtn.textContent=(d.state==='reviews_incomplete'||d.state==='reviews_stopped')?'继续采集评论':'开始深度采集';const lp=d.reviewLive;const activeCollect=d.state==='collecting'||d.state==='collecting_reviews';stopBtn.style.display=activeCollect?'block':'none';reviewLive.style.display=(lp||activeCollect||d.state==='reviews_incomplete'||d.state==='reviews_stopped')?'block':'none';const ps=d.parameterStatus; if(d){status.textContent=d.message||d.state;bar.style.width=(d.progress||0)+'%';runBtn.textContent=(d.state==='reviews_incomplete'||d.state==='reviews_stopped')?'继续采集评论':'开始深度采集';const lp=d.reviewLive;const activeCollect=d.state==='collecting'||d.state==='collecting_reviews';stopBtn.style.display=activeCollect?'block':'none';reviewLive.style.display=(lp||activeCollect||d.state==='reviews_incomplete'||d.state==='reviews_stopped')?'block':'none';const ps=d.parameterStatus;
paramLive.style.display=ps?'block':'none'; paramLive.style.display=ps?'block':'none';
@@ -30,14 +30,14 @@ async function refresh(){
paramMissing.textContent='缺失项:'+((ps.missing||[]).length?(ps.missing.join('、')):'无'); paramMissing.textContent='缺失项:'+((ps.missing||[]).length?(ps.missing.join('、')):'无');
} }
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'}
try{const r=await fetch(`${SERVER}/api/monitor/${encodeURIComponent(id)}`);const j=await r.json();monitorStatus.textContent=j.ok&&j.days?`已导入 ${j.days} 天 · ${j.firstSales||'—'} → ${j.lastSales||'—'}`:'未导入(可选)'}catch(e){monitorStatus.textContent='本地服务启动后可导入'}
} }
let collectionMode='standard';
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});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();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)};
document.getElementById('open').onclick=()=>chrome.tabs.create({url:SERVER+'/'}); document.getElementById('open').onclick=()=>chrome.tabs.create({url:SERVER+'/'});
document.getElementById('importMonitor').onclick=()=>document.getElementById('monitorFile').click(); document.getElementById('importMonitor')?.addEventListener('click',()=>document.getElementById('monitorFile')?.click());
document.getElementById('monitorFile').onchange=async e=>{ document.getElementById('monitorFile')?.addEventListener('change',async e=>{
const f=e.target.files?.[0];if(!f)return; const f=e.target.files?.[0];if(!f)return;
if(!backendCompatible){monitorStatus.textContent='导入失败:插件与后端版本不一致,请启动本包最新服务';e.target.value='';return} if(!backendCompatible){monitorStatus.textContent='导入失败:插件与后端版本不一致,请启动本包最新服务';e.target.value='';return}
const t=await active();const itemId=itemIdFromUrl(t?.url||''); const t=await active();const itemId=itemIdFromUrl(t?.url||'');
@@ -74,29 +74,33 @@ document.getElementById('monitorFile').onchange=async e=>{
monitorStatus.textContent='导入失败:'+String(err); monitorStatus.textContent='导入失败:'+String(err);
} }
e.target.value=''; e.target.value='';
}; });
document.getElementById('clearMonitor').onclick=async()=>{const t=await active();const itemId=itemIdFromUrl(t?.url||'');if(!itemId)return;try{await fetch(SERVER+'/api/monitor-clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({itemId})});monitorStatus.textContent='已清除'}catch(e){monitorStatus.textContent='清除失败'}}; document.getElementById('clearMonitor')?.addEventListener('click',async()=>{});
refresh();setInterval(refresh,1300); refresh();setInterval(refresh,1300);
stopBtn.onclick=async()=>{const t=await active();stopBtn.disabled=true;stopBtn.textContent='正在终止…';try{const r=await chrome.runtime.sendMessage({type:'STOP_COLLECTION',tabId:t?.id});status.textContent=r?.ok?'已发送终止指令,正在保存当前进度…':'终止失败,请重试'}catch(e){status.textContent='终止失败:'+String(e)}setTimeout(()=>{stopBtn.disabled=false;stopBtn.textContent='终止采集并保存进度'},1000)}; stopBtn.onclick=async()=>{const t=await active();stopBtn.disabled=true;stopBtn.textContent='正在终止…';try{const r=await chrome.runtime.sendMessage({type:'STOP_COLLECTION',tabId:t?.id});status.textContent=r?.ok?'已发送终止指令,正在保存当前进度…':'终止失败,请重试'}catch(e){status.textContent='终止失败:'+String(e)}setTimeout(()=>{stopBtn.disabled=false;stopBtn.textContent='终止采集并保存进度'},1000)};
const analyzeNow=document.getElementById('analyzeNow'); const analyzeNow=document.getElementById('analyzeNow');
const modeQuick=document.getElementById('modeQuick'); const modeQuick=document.getElementById('modeQuick');
const modeStandard=document.getElementById('modeStandard');
const modeFull=document.getElementById('modeFull'); const modeFull=document.getElementById('modeFull');
let analysisMode='quick'; 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');
modeFull?.classList.toggle('active',mode==='full'); modeFull?.classList.toggle('active',mode==='full');
if(analyzeNow){ if(analyzeNow){
analyzeNow.textContent=mode==='quick'?'基于当前数据开始分析':'完整采集后自动分析'; analyzeNow.textContent=mode==='quick'?'先看当前结论':mode==='standard'?'采集并生成报告':'完整采集后自动分析';
analyzeNow.disabled=mode==='full'; analyzeNow.disabled=false;
} }
} }
modeQuick && (modeQuick.onclick=()=>setMode('quick')); modeQuick && (modeQuick.onclick=()=>setMode('quick'));
modeStandard && (modeStandard.onclick=()=>setMode('standard'));
modeFull && (modeFull.onclick=()=>setMode('full')); modeFull && (modeFull.onclick=()=>setMode('full'));
setMode('quick'); setMode('standard');
analyzeNow && (analyzeNow.onclick=async()=>{ analyzeNow && (analyzeNow.onclick=async()=>{
if(!backendCompatible){ if(!backendCompatible){