From 19410caf76e778b169ea04bed52034d9325ebc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=96=BB=E9=95=BF=E5=8F=91?= Date: Thu, 3 Sep 2026 16:29:08 +0800 Subject: [PATCH] Optimize review collection for beginners --- extension/background.js | 22 ++++++++++++++++------ extension/popup.html | 14 +++++++------- extension/popup.js | 28 ++++++++++++++++------------ 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/extension/background.js b/extension/background.js index 4b8c833..e298d7d 100644 --- a/extension/background.js +++ b/extension/background.js @@ -56,11 +56,11 @@ chrome.runtime.onMessage.addListener((m,s,send)=>{ return true; } if(m?.type==='STOP_COLLECTION'){(async()=>{try{await chrome.scripting.executeScript({target:{tabId:m.tabId},world:'MAIN',func:()=>{window.__TMALL_AI_STOP_REVIEW__=true;}});send({ok:true})}catch(e){send({ok:false,error:String(e)})}})();return true;} - if(m?.type==='RUN_CURRENT'){ run(m.tabId,true).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; } }); -async function run(tabId,manual){ +async function run(tabId,manual,collectionMode='standard'){ const tab=await chrome.tabs.get(tabId); if(!isProduct(tab.url)) throw new Error('当前标签页不是天猫/淘宝商品详情页'); const h=await health(); @@ -83,7 +83,7 @@ async function run(tabId,manual){ 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]}).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)} const collectorResult=await collectorPromise; 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}}; } -async function collector(manual=false){ +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(); @@ -144,6 +144,7 @@ async function collector(manual=false){ const bodyText=clean(bodyClone?.innerText||document.body?.innerText||''); const captured=[]; 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 reviewStartedAt=Date.now(); 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){}} 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} @@ -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)}); 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 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({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,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} }; } diff --git a/extension/popup.html b/extension/popup.html index d604e1b..9e1142f 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -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} .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.active{background:#1f1d1a;color:#fff;border-color:#1f1d1a} .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

三笙 · 电商商品分析引擎

采集当前商品后自动识别类目和分析维度,生成标题、主图、详情、SKU、评论、新品设计与 AI 视觉报告。
检查本地服务…
等待当前商品页
-
当前商品监测数据
未检测
@@ -29,15 +28,16 @@ body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemF -
不会自动采集;不会采集推荐商品。完整采集模式会继续采到可确认末页;快速分析可随时使用当前已采数据生成报告。
+
不会采集推荐商品。评论会自动去重、分类和抽样;采集过程中可随时停止并继续。
分析方式
- - + + +
- -
分析不会清空采集进度。评论可继续采;报告会明确标注当前评论数量与完整性。
+ +
系统优先保留差评、带图、最新评论和不同主题评论。
diff --git a/extension/popup.js b/extension/popup.js index 755e950..de96f37 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -1,7 +1,7 @@ const SERVER='http://127.0.0.1:17962'; const EXPECTED_SERVER_VERSION='9.2.5'; 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'); 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''}} @@ -19,7 +19,7 @@ async function refresh(){ server.className='state ok'; } 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]; 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'; @@ -30,14 +30,14 @@ async function refresh(){ 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)=>`
${i+1}. ${String(x).replace(/[<>&]/g,c=>({'<':'<','>':'>','&':'&'}[c]))}
`).join('')||'等待评论数据…'}}else{status.textContent='已识别当前商品,等待手动开始。';bar.style.width='0%';runBtn.textContent='开始深度采集';stopBtn.style.display='none';reviewLive.style.display='none'} - 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()=>{ 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('importMonitor').onclick=()=>document.getElementById('monitorFile').click(); -document.getElementById('monitorFile').onchange=async e=>{ +document.getElementById('importMonitor')?.addEventListener('click',()=>document.getElementById('monitorFile')?.click()); +document.getElementById('monitorFile')?.addEventListener('change',async e=>{ const f=e.target.files?.[0];if(!f)return; if(!backendCompatible){monitorStatus.textContent='导入失败:插件与后端版本不一致,请启动本包最新服务';e.target.value='';return} const t=await active();const itemId=itemIdFromUrl(t?.url||''); @@ -74,29 +74,33 @@ document.getElementById('monitorFile').onchange=async e=>{ monitorStatus.textContent='导入失败:'+String(err); } 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); 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 modeQuick=document.getElementById('modeQuick'); +const modeStandard=document.getElementById('modeStandard'); const modeFull=document.getElementById('modeFull'); -let analysisMode='quick'; +let analysisMode='standard'; function setMode(mode){ analysisMode=mode; + collectionMode=mode; modeQuick?.classList.toggle('active',mode==='quick'); + modeStandard?.classList.toggle('active',mode==='standard'); modeFull?.classList.toggle('active',mode==='full'); if(analyzeNow){ - analyzeNow.textContent=mode==='quick'?'基于当前数据开始分析':'完整采集后自动分析'; - analyzeNow.disabled=mode==='full'; + analyzeNow.textContent=mode==='quick'?'先看当前结论':mode==='standard'?'采集并生成报告':'完整采集后自动分析'; + analyzeNow.disabled=false; } } modeQuick && (modeQuick.onclick=()=>setMode('quick')); +modeStandard && (modeStandard.onclick=()=>setMode('standard')); modeFull && (modeFull.onclick=()=>setMode('full')); -setMode('quick'); +setMode('standard'); analyzeNow && (analyzeNow.onclick=async()=>{ if(!backendCompatible){