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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
喻长发
2026-09-04 16:57:04 +08:00
co-authored by Cursor
parent 19410caf76
commit 8c33d08537
29 changed files with 1111 additions and 131 deletions
+9 -9
View File
@@ -56,11 +56,11 @@ chrome.runtime.onMessage.addListener((m,s,send)=>{
return true;
}
if(m?.type==='STOP_COLLECTION'){(async()=>{try{await chrome.scripting.executeScript({target:{tabId:m.tabId},world:'MAIN',func:()=>{window.__TMALL_AI_STOP_REVIEW__=true;}});send({ok:true})}catch(e){send({ok:false,error:String(e)})}})();return true;}
if(m?.type==='RUN_CURRENT'){ run(m.tabId,true,m.collectionMode||'standard').then(x=>send(x)).catch(e=>send({ok:false,error:String(e)})); return true; }
if(m?.type==='RUN_CURRENT'){ run(m.tabId,true,m.sampleMode||'500').then(x=>send(x)).catch(e=>send({ok:false,error:String(e)})); return true; }
if(m?.type==='HEALTH'){ health().then(send); return true; }
});
async function run(tabId,manual,collectionMode='standard'){
async function run(tabId,manual,sampleMode='500'){
const tab=await chrome.tabs.get(tabId);
if(!isProduct(tab.url)) throw new Error('当前标签页不是天猫/淘宝商品详情页');
const h=await health();
@@ -80,10 +80,10 @@ async function run(tabId,manual,collectionMode='standard'){
if(pageState?.login){await setStatus(tabId,{state:'login_required',message:'淘宝要求登录。请先在当前浏览器登录淘宝,再刷新商品页重试。',progress:0});throw new Error('淘宝商品页未登录:请登录后刷新当前商品页再采集')}
if(pageState?.verification){await setStatus(tabId,{state:'verification_required',message:'页面出现淘宝安全验证,请手动完成后刷新重试。',progress:0});throw new Error('页面出现淘宝安全验证,请手动完成验证后重试')}
if(!isProduct(pageState?.url)){throw new Error('商品页已跳转,当前不是可采集的淘宝/天猫详情页')}
await setStatus(tabId,{state:'collecting',message:'正在采集当前商品:主图、SKU、详情、参数、问答与评论。不会采集推荐商品…',progress:10});
await setStatus(tabId,{state:'collecting',sampleMode,message:`正在采集当前商品:主图、SKU、详情、参数、问答与评论(评论样本:${sampleMode==='all'?'全部':sampleMode+' 条'})。不会采集推荐商品…`,progress:10});
await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:()=>{window.__TMALL_AI_STOP_REVIEW__=false;window.__TMALL_AI_REVIEW_PROGRESS__={count:0,round:0,stagnant:0,mode:'启动',elapsedMs:0,preview:[]};}});
let collectorDone=false,collectorError=null;const startedAt=Date.now();
const collectorPromise=chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:collector,args:[manual,collectionMode]}).then(v=>v).catch(e=>{collectorError=e;return null}).finally(()=>{collectorDone=true});
const collectorPromise=chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:collector,args:[manual,sampleMode]}).then(v=>v).catch(e=>{collectorError=e;return null}).finally(()=>{collectorDone=true});
while(!collectorDone){try{const rr=await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:()=>window.__TMALL_AI_REVIEW_PROGRESS__||null});const live=rr?.[0]?.result;if(live)await setStatus(tabId,{state:'collecting_reviews',message:`评论采集中:${live.count||0} 条 · ${live.mode||'处理中'}${live.stagnant?` · ${live.stagnant}轮无新增`:''}`,progress:Math.min(67,18+(Number(live.round||0)*2)),reviewLive:{...live,elapsedMs:Date.now()-startedAt}})}catch(e){}await sleep(600)}
const collectorResult=await collectorPromise;
if(collectorError)throw new Error('页面采集脚本执行失败:'+String(collectorError?.message||collectorError));
@@ -110,7 +110,7 @@ async function run(tabId,manual,collectionMode='standard'){
const noReviewData=!raw.reviews?.length&&!raw.collection?.networkReviewResponses;
const folded=raw.collection?.foldedDefaultReviewCount?`;平台另折叠 ${raw.collection.foldedDefaultReviewCount} 条默认好评,未提供逐条内容`:'';
const msg=stopped?`已终止采集,并保存当前 ${raw.reviews?.length||0} 条评论。可随时继续。`:human?`评论已保存 ${raw.reviews?.length||0} 条,页面出现验证。请手动完成验证后继续。`:noReviewData?'页面未释放可读取的评论数据。商品信息已保存;请在当前页打开“评价/评论”后再次点击继续采集。':`评论已保存 ${raw.reviews?.length||0} 条,尚未确认完整${folded}。已停止低效空转,可继续采集。`;
await setStatus(tabId,{state:stopped?'reviews_stopped':'reviews_incomplete',message:msg,progress:68,itemId:raw.product.itemId,reviewStopReason:reason,reviewLive:raw.collection?.reviewLive||null,parameterStatus:raw.product?.parameterCollection||null});
await setStatus(tabId,{state:stopped?'reviews_stopped':'reviews_incomplete',sampleMode, message:msg,progress:68,itemId:raw.product.itemId,reviewStopReason:reason,reviewLive:raw.collection?.reviewLive||null,parameterStatus:raw.product?.parameterCollection||null});
return {ok:false,needsReviewResume:true,message:msg,reviewStopReason:reason,rawStats:{title:raw.product?.title||'',reviews:raw.reviews?.length||0,questions:raw.questions?.length||0,sku:raw.sku?.length||0,attributes:raw.attributes?.length||0,promotions:raw.promotions?.length||0,images:raw.images?.all?.length||0}};
}
await setStatus(tabId,{state:'sending',message:`评论完整性已确认,共 ${raw.reviews?.length||0} 条。正在生成新品开品闭环…`,progress:78,itemId:raw.product.itemId});
@@ -121,7 +121,7 @@ async function run(tabId,manual,collectionMode='standard'){
return {ok:true,taskId:j.taskId,rawStats:{title:raw.product?.title||'',reviews:raw.reviews?.length||0,questions:raw.questions?.length||0,sku:raw.sku?.length||0,attributes:raw.attributes?.length||0,promotions:raw.promotions?.length||0,images:raw.images?.all?.length||0}};
}
async function collector(manual=false,collectionMode='standard'){
async function collector(manual=false,sampleMode='500'){
const delay=ms=>new Promise(r=>setTimeout(r,ms));
const text=(el)=>el?.innerText?.trim()||el?.textContent?.trim()||'';
const clean=s=>(s||'').replace(/\s+/g,' ').trim();
@@ -144,7 +144,7 @@ async function collector(manual=false,collectionMode='standard'){
const bodyText=clean(bodyClone?.innerText||document.body?.innerText||'');
const captured=[];
const reviews=[]; const questions=[];
const reviewCap=collectionMode==='quick'?300:(collectionMode==='full'?3000:800);
const reviewCap=sampleMode==='all'?Infinity:Math.max(1,Math.min(10000,Number(sampleMode)||500));
const reviewNetMeta={totals:[],hasMoreFalse:false,lastPageFlags:[],pageHints:[],networkReviewObjects:0,networkResponses:0};
const reviewStartedAt=Date.now();
const processedCaptureKeys=new Set();
@@ -331,7 +331,7 @@ async function collector(manual=false,collectionMode='standard'){
// 每轮即时解析已捕获网络响应
const capNow=(window.__TMALL_AI_CAPTURE__||[]).slice(-120);for(const c of capNow){try{const ck=String(c.__safeKey||[c.method||'',c.url||'',String(c.text||'').slice(0,240)].join('|'));if(processedCaptureKeys.has(ck))continue;processedCaptureKeys.add(ck);if(/review|rate|comment|feed/i.test(c.url||''))reviewNetMeta.networkResponses++;let z=(c.text||'').replace(/^\s*[\w$.]+\(/,'').replace(/\)\s*;?\s*$/,'');try{walk(JSON.parse(z),0,c.url||'')}catch(_){const m=z.match(/\{[\s\S]*\}/);if(m)try{walk(JSON.parse(m[0]),0,c.url||'')}catch(__){}}}catch(e){}}
collectVisibleReviews();collectVisibleQuestions();const after=dedupReviewCount();stagnant=after<=before?stagnant+1:0;publishReviewProgress(round+1,stagnant,next?'翻页/加载更多':'短滚动',{lastDelta:after-before});
if(collectionMode!=='full' && after>=reviewCap){collectionComplete=true;explicitEnd=true;stopReason='sample_cap';publishReviewProgress(round+1,stagnant,collectionMode==='quick'?'已采够快速样本':'已采够标准样本');break}
if(Number.isFinite(reviewCap) && after>=reviewCap){collectionComplete=true;explicitEnd=true;stopReason='sample_cap';publishReviewProgress(round+1,stagnant,`已采够 ${reviewCap} 条评论样本`);break}
const bodyNow=clean(document.body?.innerText||'');const noMoreText=/没有更多(?:评价|评论)|已显示全部(?:评价|评论)|到底了|暂无更多(?:评价|评论)/.test(bodyNow);const captcha=/验证码|滑块|安全验证|访问过于频繁|操作频繁/.test(bodyNow);
if(captcha){stopReason='human_verification_required';publishReviewProgress(round+1,stagnant,'需要人工验证');break}
if((disabledNext||noMoreText)&&stagnant>=1){explicitEnd=true;collectionComplete=true;stopReason='explicit_last_page';publishReviewProgress(round+1,stagnant,'页面明确结束');break}
@@ -581,6 +581,6 @@ async function collector(manual=false,collectionMode='standard'){
sales:{currentPrice:priceValue,sold:soldValue,ranking:rankMatch?.[1]||''},
sku:skuTexts.map(name=>({name})),attributes:attrs,promotions:promoTexts.map(text=>({text})),
images:{main,detail,sku:skuImages,buyerShow:buyer,all:allImgs,provenance,classification:{version:'strict-v1',classifiedCount:allImgs.length,unclassifiedCount:Math.max(0,allPageCandidates.length-allImgs.length),groupsAreDisjoint:true}},
reviews:reviewRows,questions:[...deQ.values()],pageText:liveBodyText.slice(0,120000),collection:{reviewActual:deReview.size,reviewSampled,reviewSampleCap:reviewCap,reviewCollectionMode:collectionMode,questionActual:deQ.size,reviewDrawerOpened:reviewDrawerWasOpened,reviewCollectionComplete:collectionComplete,reviewCompleteness:reviewSampled?'sampled':(collectionComplete?'confirmed':'unconfirmed'),reviewStopReason:reviewSampled?'sample_cap':stopReason,reviewPagesVisited:pagesVisited,publicReviewCount:publicRaw,publicReviewCountNumeric:publicNumeric,foldedDefaultReviewCount:foldedDefaultMatch?parseCount(foldedDefaultMatch[1]):null,defaultPraiseBuyerCount:defaultPraiseMatch?parseCount(defaultPraiseMatch[1]):null,networkReviewTotal:networkTotal,networkReviewObjects:reviewNetMeta.networkReviewObjects,networkReviewResponses:reviewNetMeta.networkResponses,networkExplicitEnd:reviewNetMeta.hasMoreFalse||reviewNetMeta.lastPageFlags.length>0,explicitEnd,reviewLive:window.__TMALL_AI_REVIEW_PROGRESS__||null}
reviews:reviewRows,questions:[...deQ.values()],pageText:liveBodyText.slice(0,120000),collection:{reviewActual:deReview.size,reviewSampled,reviewSampleCap:Number.isFinite(reviewCap)?reviewCap:null,reviewCollectionMode:sampleMode,questionActual:deQ.size,reviewDrawerOpened:reviewDrawerWasOpened,reviewCollectionComplete:collectionComplete,reviewCompleteness:reviewSampled?'sampled':(collectionComplete?'confirmed':'unconfirmed'),reviewStopReason:reviewSampled?'sample_cap':stopReason,reviewPagesVisited:pagesVisited,publicReviewCount:publicRaw,publicReviewCountNumeric:publicNumeric,foldedDefaultReviewCount:foldedDefaultMatch?parseCount(foldedDefaultMatch[1]):null,defaultPraiseBuyerCount:defaultPraiseMatch?parseCount(defaultPraiseMatch[1]):null,networkReviewTotal:networkTotal,networkReviewObjects:reviewNetMeta.networkReviewObjects,networkReviewResponses:reviewNetMeta.networkResponses,networkExplicitEnd:reviewNetMeta.hasMoreFalse||reviewNetMeta.lastPageFlags.length>0,explicitEnd,reviewLive:window.__TMALL_AI_REVIEW_PROGRESS__||null}
};
}
+3 -3
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "三笙 · 电商商品分析闭环 V9.2.5",
"version": "9.2.5",
"name": "三笙 · 电商商品分析闭环 V9.3.0",
"version": "9.3.0",
"description": "跨类目电商商品分析:动态参数与SKU采集、图文分源、评论证据、开品与AI视觉方案。",
"permissions": [
"scripting",
@@ -20,7 +20,7 @@
},
"action": {
"default_popup": "popup.html",
"default_title": "三笙 · 电商商品分析闭环 V9.2.5"
"default_title": "三笙 · 电商商品分析闭环 V9.3.0"
},
"content_scripts": [
{
+8 -10
View File
@@ -16,6 +16,13 @@ body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemF
<h1>三笙 · 电商商品分析引擎</h1>
<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 sample-card"><div class="state">采集前先选择评论样本</div><div class="sub">只限制评论样本数量;商品信息、参数、图片、问答和完整分析内容都会保留。</div><div class="mode-row">
<button data-sample="50" class="mode">50 条<small>快速判断</small></button>
<button data-sample="100" class="mode">100 条<small>常规小样本</small></button>
<button data-sample="500" class="mode active">500 条<small>推荐</small></button>
<button data-sample="all" class="mode">全部<small>采到可访问末页</small></button>
<button data-sample="custom" class="mode">自定义<small>输入数量</small></button>
</div><div id="customSampleWrap" style="display:none;margin-top:10px"><input id="customSample" type="number" min="1" max="10000" placeholder="输入评论条数(1-10000)"></div><div class="tiny">采集开始后不能切换样本档位;如中途停止,可按原档位继续。</div></div>
<button class="btn" id="run">开始深度采集</button>
<button class="btn stop" id="stop" style="display:none">终止采集并保存进度</button>
@@ -27,17 +34,8 @@ body{width:360px;margin:0;padding:18px;font-family:-apple-system,BlinkMacSystemF
<div class="param-miss" id="paramMissing">缺失项:—</div>
</div>
<div id="reviewLive" class="live" style="display:none"><strong>评论实时采集</strong><div class="live-grid"><span>去重:<b id="liveCount">0</b></span><span>网络对象:<b id="liveNet">0</b></span><span>当前轮次:<b id="liveRound">0</b></span><span>无新增:<b id="liveStagnant">0</b></span><span>模式:<b id="liveMode">—</b></span><span>耗时:<b id="liveElapsed">0s</b></span></div><div id="livePreview" class="live-preview">等待评论数据…</div></div>
<div class="card"><div class="state">分析入口</div><div class="sub">已有采集数据时,可直接生成报告;不会清空当前采集进度。</div><button id="analyzeNow" class="btn analysis-now">基于当前数据开始分析</button></div>
<button class="btn secondary" id="open">打开本地服务</button>
<div class="tiny">不会采集推荐商品。评论会自动去重、分类和抽样;采集过程中可随时停止并继续。</div>
<div class="card">
<div class="state">分析方式</div>
<div class="mode-row">
<button id="modeQuick" class="mode">快速看结论<small>最多 300 条,约 1 分钟</small></button>
<button id="modeStandard" class="mode active">标准采集<small>最多 800 条,适合大多数商品</small></button>
<button id="modeFull" class="mode">完整采集<small>尽可能采到末页,耗时较长</small></button>
</div>
<button id="analyzeNow" class="btn analysis-now">先看当前结论</button>
<div class="tiny">系统优先保留差评、带图、最新评论和不同主题评论。</div>
</div>
<script src="popup.js"></script>
</body></html>
+9 -5
View File
@@ -1,5 +1,5 @@
const SERVER='http://127.0.0.1:17962';
const EXPECTED_SERVER_VERSION='9.2.5';
const EXPECTED_SERVER_VERSION='9.3.0';
let backendCompatible=false;
const server=document.getElementById('server'),status=document.getElementById('status'),bar=document.getElementById('bar'),runBtn=document.getElementById('run'),stopBtn=document.getElementById('stop'),reviewLive=document.getElementById('reviewLive'),liveCount=document.getElementById('liveCount'),liveNet=document.getElementById('liveNet'),liveRound=document.getElementById('liveRound'),liveStagnant=document.getElementById('liveStagnant'),liveMode=document.getElementById('liveMode'),liveElapsed=document.getElementById('liveElapsed'),livePreview=document.getElementById('livePreview');
const paramLive=document.getElementById('paramLive'),paramState=document.getElementById('paramState'),paramCount=document.getElementById('paramCount'),paramTags=document.getElementById('paramTags'),paramMissing=document.getElementById('paramMissing');
@@ -31,10 +31,15 @@ async function refresh(){
}
if(lp){liveCount.textContent=lp.count??0;liveNet.textContent=lp.networkReviewObjects??0;liveRound.textContent=lp.round??0;liveStagnant.textContent=lp.stagnant??0;liveMode.textContent=lp.mode||'—';liveElapsed.textContent=Math.round((lp.elapsedMs||0)/1000)+'s';livePreview.innerHTML=(lp.preview||[]).map((x,i)=>`<div>${i+1}. ${String(x).replace(/[<>&]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]))}</div>`).join('')||'等待评论数据…'}}else{status.textContent='已识别当前商品,等待手动开始。';bar.style.width='0%';runBtn.textContent='开始深度采集';stopBtn.style.display='none';reviewLive.style.display='none'}
}
let collectionMode='standard';
let collectionMode='500';
const sampleButtons=[...document.querySelectorAll('[data-sample]')];
const customSampleWrap=document.getElementById('customSampleWrap');
const customSample=document.getElementById('customSample');
sampleButtons.forEach(btn=>btn.addEventListener('click',()=>{sampleButtons.forEach(x=>x.classList.remove('active'));btn.classList.add('active');collectionMode=btn.dataset.sample;customSampleWrap.style.display=collectionMode==='custom'?'block':'none';}));
;(async()=>{try{const t=await active();const p=(await chrome.storage.local.get('status_'+t.id))['status_'+t.id];if((p?.state==='reviews_incomplete'||p?.state==='reviews_stopped')&&p.sampleMode){collectionMode=p.sampleMode;sampleButtons.forEach(x=>x.classList.toggle('active',x.dataset.sample===collectionMode));customSampleWrap.style.display=collectionMode==='custom'?'block':'none'}}catch(e){}})();
runBtn.onclick=async()=>{
if(!backendCompatible){status.textContent='插件与本地后端版本不一致,请先双击本包“start-tmall-ai.command”';return}
const t=await active();status.textContent='开始采集当前商品…';try{const r=await chrome.runtime.sendMessage({type:'RUN_CURRENT',tabId:t.id,collectionMode});if(r?.needsReviewResume){status.textContent=r.message||'评论还没到末页,请继续采集';runBtn.textContent='继续采集评论'}else if(!r?.ok){status.textContent='采集失败:'+(r?.error||'未知错误')}}catch(e){status.textContent='采集失败:'+String(e)}setTimeout(refresh,500)};
const t=await active();let sample=collectionMode;if(sample==='custom'){const n=Number(customSample?.value||0);if(!Number.isInteger(n)||n<1||n>10000){status.textContent='请输入 1-10000 的评论数量';return}sample=String(n)}status.textContent=`开始采集当前商品(评论样本 ${sample==='all'?'全部':sample+' 条'})…`;try{const r=await chrome.runtime.sendMessage({type:'RUN_CURRENT',tabId:t.id,sampleMode:sample});if(r?.needsReviewResume){status.textContent=r.message||'评论采集已暂停,可继续';runBtn.textContent='继续采集评论'}else if(!r?.ok){status.textContent='采集失败:'+(r?.error||'未知错误')}}catch(e){status.textContent='采集失败:'+String(e)}setTimeout(refresh,500)};
document.getElementById('open').onclick=()=>chrome.tabs.create({url:SERVER+'/'});
document.getElementById('importMonitor')?.addEventListener('click',()=>document.getElementById('monitorFile')?.click());
document.getElementById('monitorFile')?.addEventListener('change',async e=>{
@@ -88,12 +93,11 @@ let analysisMode='standard';
function setMode(mode){
analysisMode=mode;
collectionMode=mode;
modeQuick?.classList.toggle('active',mode==='quick');
modeStandard?.classList.toggle('active',mode==='standard');
modeFull?.classList.toggle('active',mode==='full');
if(analyzeNow){
analyzeNow.textContent=mode==='quick'?'先看当前结论':mode==='standard'?'采集并生成报告':'完整采集后自动分析';
analyzeNow.textContent='基于当前数据开始分析';
analyzeNow.disabled=false;
}
}