Add product analysis extension release
This commit is contained in:
+270
-113
@@ -1,132 +1,289 @@
|
||||
const SERVER='http://127.0.0.1:17962';
|
||||
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');
|
||||
let collectionMode='standard';
|
||||
let analysisMode='standard';
|
||||
let currentReportUrl='';
|
||||
let isAnalysisRunning=false;
|
||||
let refreshRunning=false;
|
||||
|
||||
const byId=id=>document.getElementById(id);
|
||||
const server=byId('server'),status=byId('status'),bar=byId('bar'),runBtn=byId('run'),stopBtn=byId('stop');
|
||||
const reviewLive=byId('reviewLive'),liveCount=byId('liveCount'),liveNet=byId('liveNet'),liveRound=byId('liveRound'),liveStagnant=byId('liveStagnant'),liveMode=byId('liveMode'),liveElapsed=byId('liveElapsed'),livePreview=byId('livePreview');
|
||||
const paramLive=byId('paramLive'),paramState=byId('paramState'),paramCount=byId('paramCount'),paramTags=byId('paramTags'),paramMissing=byId('paramMissing');
|
||||
const accountStatus=byId('accountStatus'),loginAccount=byId('loginAccount'),logoutAccount=byId('logoutAccount'),openReports=byId('openReports');
|
||||
const analyzeNow=byId('analyzeNow'),modeQuick=byId('modeQuick'),modeStandard=byId('modeStandard'),modeFull=byId('modeFull');
|
||||
const viewCurrentReport=byId('viewCurrentReport'),analysisMeta=byId('analysisMeta');
|
||||
const accountDot=byId('accountDot'),statusCard=byId('statusCard'),phaseLabel=byId('phaseLabel'),progressValue=byId('progressValue'),waitHint=byId('waitHint');
|
||||
const collectionControls=byId('collectionControls'),analysisOptions=byId('analysisOptions');
|
||||
const analyzeAgain=byId('analyzeAgain'),reportHistory=byId('reportHistory'),historyList=byId('historyList'),closeHistory=byId('closeHistory');
|
||||
|
||||
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''}}
|
||||
async function refresh(){
|
||||
const h=await chrome.runtime.sendMessage({type:'HEALTH'});
|
||||
backendCompatible=!!(h?.ok && h.serverVersion===EXPECTED_SERVER_VERSION);
|
||||
if(!h?.ok){
|
||||
server.textContent='本地服务未启动 ✕';
|
||||
server.className='state warn';
|
||||
}else if(!backendCompatible){
|
||||
server.textContent=`版本不一致 ✕ 插件 ${EXPECTED_SERVER_VERSION} / 后端 ${h.serverVersion||'未知'},请启动本包最新服务`;
|
||||
server.className='state warn';
|
||||
}else{
|
||||
server.textContent=`本地服务 ✓ V${h.serverVersion} · ${h.modelConfigured?'模型已配置':'模型未配置'}`;
|
||||
server.className='state ok';
|
||||
}
|
||||
const t=await active(); if(!t)return; const id=itemIdFromUrl(t.url||'');
|
||||
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';
|
||||
if(ps){
|
||||
paramState.textContent=ps.stateText||'已采集';
|
||||
paramCount.textContent=ps.count??0;
|
||||
paramTags.innerHTML=(ps.keys||[]).slice(0,10).map(k=>`<span class="param-tag">${String(k).replace(/[<>&]/g,s=>({'<':'<','>':'>','&':'&'}[s]))}</span>`).join('')||'<span class="param-tag">暂无</span>';
|
||||
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=>({'<':'<','>':'>','&':'&'}[c]))}</div>`).join('')||'等待评论数据…'}}else{status.textContent='已识别当前商品,等待手动开始。';bar.style.width='0%';runBtn.textContent='开始深度采集';stopBtn.style.display='none';reviewLive.style.display='none'}
|
||||
function pageKind(u){
|
||||
try{
|
||||
const url=new URL(u||'');
|
||||
if(!/taobao\.com$|tmall\.com$/.test(url.hostname))return 'unsupported';
|
||||
return /(^|\.)detail\.tmall\.com$/.test(url.hostname)&&url.pathname==='/item.htm'||/^item\.taobao\.com$/.test(url.hostname)&&url.pathname==='/item.htm'?'product':'taobao_other';
|
||||
}catch(e){return 'unsupported'}
|
||||
}
|
||||
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();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=>{
|
||||
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||'');
|
||||
if(!itemId){monitorStatus.textContent='请先打开目标商品页';e.target.value='';return}
|
||||
monitorStatus.textContent='正在解析并导入…';
|
||||
try{
|
||||
const buf=await f.arrayBuffer();
|
||||
let binary='';const bytes=new Uint8Array(buf);
|
||||
for(let i=0;i<bytes.length;i+=0x8000)binary+=String.fromCharCode(...bytes.subarray(i,i+0x8000));
|
||||
const base64=btoa(binary);
|
||||
const send=async(forceBind=false)=>{
|
||||
const r=await fetch(SERVER+'/api/monitor-import',{
|
||||
method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({itemId,filename:f.name,dataBase64:base64,forceBind})
|
||||
});
|
||||
const j=await r.json().catch(()=>({ok:false,error:'服务返回内容无法解析'}));
|
||||
return {r,j};
|
||||
};
|
||||
let {r,j}=await send(false);
|
||||
if(!j.ok && j.code==='ITEM_ID_MISMATCH' && j.canForce){
|
||||
const fileId=(j.fileItemIds||[])[0]||'未知';
|
||||
const yes=confirm(`监测文件中的商品ID为 ${fileId},当前页面商品ID为 ${itemId}。
|
||||
/** 转义正文及属性中的特殊字符,长标题可安全放入 title 提示。 */
|
||||
function escapeHtml(value){return String(value||'').replace(/[<>&"']/g,c=>({'<':'<','>':'>','&':'&','"':'"',"'":'''}[c]))}
|
||||
|
||||
如果这份文件确实属于当前商品,点击“确定”后按当前商品绑定;否则点击“取消”。`);
|
||||
if(yes)({r,j}=await send(true));
|
||||
}
|
||||
if(j.ok){
|
||||
monitorStatus.textContent=`已导入 ${j.days} 天 · ${j.firstSales||'—'} → ${j.lastSales||'—'}`;
|
||||
}else{
|
||||
const extra=j.fileItemIds?.length?`(文件商品ID:${j.fileItemIds.slice(0,5).join(', ')})`:'';
|
||||
monitorStatus.textContent=`导入失败:${j.error||'未知错误'}${extra}`;
|
||||
}
|
||||
}catch(err){
|
||||
monitorStatus.textContent='导入失败:'+String(err);
|
||||
}
|
||||
e.target.value='';
|
||||
});
|
||||
document.getElementById('clearMonitor')?.addEventListener('click',async()=>{});
|
||||
refresh();setInterval(refresh,1300);
|
||||
function renderParameterStatus(ps){
|
||||
paramLive.style.display=ps?'block':'none';
|
||||
if(!ps)return;
|
||||
paramState.textContent=ps.stateText||'已采集';
|
||||
paramCount.textContent=ps.count??0;
|
||||
paramTags.innerHTML=(ps.keys||[]).slice(0,10).map(key=>`<span class="param-tag">${escapeHtml(key)}</span>`).join('')||'<span class="param-tag">暂无</span>';
|
||||
paramMissing.textContent='缺失项:'+((ps.missing||[]).length?ps.missing.join('、'):'无');
|
||||
}
|
||||
|
||||
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)};
|
||||
function renderReviewStatus(lp,show){
|
||||
reviewLive.style.display=show?'block':'none';
|
||||
if(!lp)return;
|
||||
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((value,index)=>`<div>${index+1}. ${escapeHtml(value)}</div>`).join('')||'等待评论数据…';
|
||||
}
|
||||
|
||||
const analyzeNow=document.getElementById('analyzeNow');
|
||||
const modeQuick=document.getElementById('modeQuick');
|
||||
const modeStandard=document.getElementById('modeStandard');
|
||||
const modeFull=document.getElementById('modeFull');
|
||||
let analysisMode='standard';
|
||||
function renderTaskStatus(data){
|
||||
const d=data||{};
|
||||
const state=d.state||'idle';
|
||||
const currentTabProductId=d.currentProductId||'';
|
||||
const activeCollect=state==='collecting'||state==='collecting_reviews';
|
||||
const analysisBusy=state==='sending'||state==='analyzing';
|
||||
const reportReady=state==='analysis_completed'&&!!d.taskUrl&&(!currentTabProductId||!d.productId||currentTabProductId===d.productId);
|
||||
const analysisFailed=state==='analysis_failed';
|
||||
const analysisBlocked=state==='analysis_blocked';
|
||||
const waitingForReviews=state==='reviews_incomplete'||state==='reviews_stopped';
|
||||
const progress=Math.max(0,Math.min(100,Number(d.progress||0)));
|
||||
const phaseMap={
|
||||
idle:'等待开始',
|
||||
login_required:'需要登录',
|
||||
verification_required:'需要完成验证',
|
||||
product_empty:'商品数据未就绪',
|
||||
collecting:'采集商品数据',
|
||||
collecting_reviews:'采集商品评论',
|
||||
reviews_incomplete:'数据等待补充',
|
||||
reviews_stopped:'采集已暂停',
|
||||
sending:'正在提交分析',
|
||||
analyzing:'报告生成中',
|
||||
analysis_completed:'报告已完成',
|
||||
analysis_failed:'分析未完成',
|
||||
analysis_blocked:'暂时无法提交'
|
||||
};
|
||||
|
||||
status.textContent=d.message||d.state||'已识别当前商品,等待手动开始。';
|
||||
bar.style.width=progress+'%';
|
||||
progressValue.textContent=progress+'%';
|
||||
phaseLabel.textContent=phaseMap[state]||'任务处理中';
|
||||
statusCard.dataset.state=reportReady?'completed':(analysisFailed||analysisBlocked)?'failed':analysisBusy?'analyzing':activeCollect?'collecting':'idle';
|
||||
isAnalysisRunning=analysisBusy;
|
||||
|
||||
updateRunButtonText(waitingForReviews,false);
|
||||
collectionControls.style.display=analysisBusy||reportReady?'none':'grid';
|
||||
runBtn.style.display=activeCollect?'none':'block';
|
||||
stopBtn.style.display=activeCollect?'block':'none';
|
||||
analysisOptions.style.display=(analysisBusy||activeCollect||reportReady)?'none':'block';
|
||||
waitHint.style.display=analysisBusy?'block':'none';
|
||||
|
||||
const showCollectionDetails=!analysisBusy&&!reportReady&&(activeCollect||waitingForReviews);
|
||||
renderParameterStatus(showCollectionDetails?d.parameterStatus:null);
|
||||
renderReviewStatus(d.reviewLive,showCollectionDetails&&!!(d.reviewLive||activeCollect||waitingForReviews));
|
||||
|
||||
currentReportUrl=d.taskUrl||'';
|
||||
viewCurrentReport.style.display=reportReady?'block':'none';
|
||||
analyzeAgain.style.display=reportReady?'block':'none';
|
||||
analysisMeta.style.display=d.taskId?'block':'none';
|
||||
if(d.taskId){
|
||||
const title=d.productTitle?String(d.productTitle).slice(0,32):'当前商品';
|
||||
analysisMeta.textContent=reportReady?`${title} · 报告已就绪`:`${title} · ${d.stage||'处理中'}`;
|
||||
}
|
||||
analyzeNow.disabled=analysisBusy;
|
||||
analyzeNow.textContent='基于当前已有数据生成报告';
|
||||
}
|
||||
|
||||
async function refresh(){
|
||||
if(refreshRunning)return;
|
||||
refreshRunning=true;
|
||||
try{
|
||||
const auth=await chrome.runtime.sendMessage({type:'SANSHENG_AUTH_STATE'}).catch(()=>({loggedIn:false}));
|
||||
accountStatus.textContent=auth?.loggedIn?'已登录,可提交市场洞察分析任务':'未登录,请先使用 PC 账号授权';
|
||||
accountDot.classList.toggle('online',!!auth?.loggedIn);
|
||||
loginAccount.style.display=auth?.loggedIn?'none':'block';
|
||||
logoutAccount.style.display=auth?.loggedIn?'block':'none';
|
||||
openReports.style.display=auth?.loggedIn?'block':'none';
|
||||
backendCompatible=!!auth?.loggedIn&&!!auth?.serviceReady;
|
||||
server.textContent=backendCompatible?'三笙云端分析服务已就绪 ✓':auth?.loggedIn?`云端服务不可用:${auth?.serviceError||'请确认 Web API 已启动'}`:'登录后可使用云端分析服务';
|
||||
server.className=backendCompatible?'state ok':'state warn';
|
||||
|
||||
const tab=await active();
|
||||
if(!tab?.id){renderTaskStatus(null);return}
|
||||
const kind=pageKind(tab.url||'');
|
||||
if(kind!=='product'){
|
||||
const message='请打开淘宝/天猫商品详情页';
|
||||
renderTaskStatus({state:'idle',message,progress:0,currentProductId:''});
|
||||
collectionControls.style.display='none';
|
||||
analysisOptions.style.display='none';
|
||||
return;
|
||||
}
|
||||
const currentProductId=itemIdFromUrl(tab.url||'');
|
||||
const result=backendCompatible?await chrome.runtime.sendMessage({type:'PRODUCT_ANALYSIS_STATUS',tabId:tab.id}).catch(()=>null):null;
|
||||
let data=result?.status||null;
|
||||
if(!data){
|
||||
const stored=await chrome.storage.local.get(['status_'+tab.id,'sansheng_product_analysis_last_task']);
|
||||
data=stored['status_'+tab.id]||stored.sansheng_product_analysis_last_task||null;
|
||||
}
|
||||
if(data){
|
||||
const dataProductId=String(data.productId||data.itemId||'');
|
||||
if(dataProductId&&dataProductId!==currentProductId)data=null;
|
||||
}
|
||||
if(data)renderTaskStatus({...data,currentProductId});
|
||||
else renderTaskStatus({message:'已打开商品详情页,选择采集深度后开始。',progress:0,currentProductId});
|
||||
}finally{
|
||||
refreshRunning=false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRunButtonText(continueReviews=false){
|
||||
if(continueReviews){runBtn.textContent='继续采集评论并生成';return}
|
||||
runBtn.textContent=collectionMode==='quick'?'快速采集并生成':collectionMode==='full'?'深度采集并生成':'标准采集并生成';
|
||||
}
|
||||
|
||||
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='基于当前数据开始分析';
|
||||
analyzeNow.disabled=false;
|
||||
}
|
||||
analyzeNow.disabled=isAnalysisRunning;
|
||||
updateRunButtonText(false);
|
||||
}
|
||||
modeQuick && (modeQuick.onclick=()=>setMode('quick'));
|
||||
modeStandard && (modeStandard.onclick=()=>setMode('standard'));
|
||||
modeFull && (modeFull.onclick=()=>setMode('full'));
|
||||
setMode('standard');
|
||||
|
||||
analyzeNow && (analyzeNow.onclick=async()=>{
|
||||
if(!backendCompatible){
|
||||
status.textContent='插件与本地后端版本不一致,请先启动本包最新服务';
|
||||
return;
|
||||
}
|
||||
const t=await active();
|
||||
if(!t?.id){status.textContent='请先打开目标商品页';return}
|
||||
analyzeNow.disabled=true;
|
||||
analyzeNow.textContent='正在提交当前数据…';
|
||||
/**
|
||||
* 将接口异常转换成用户可读提示,避免暴露 Error 包装文本。
|
||||
* 入参为未知异常,出参为稳定中文提示。
|
||||
*/
|
||||
function userErrorMessage(error){
|
||||
const message=error instanceof Error?error.message:String(error||'无法开始分析');
|
||||
return message.replace(/^Error:\s*/i,'').trim()||'无法开始分析,请稍后重试';
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化分析提交失败状态,避免自动刷新立刻覆盖错误提示。
|
||||
* 入参为当前标签页和错误文字,无返回值。
|
||||
*/
|
||||
async function showAnalysisSubmitError(tab,message){
|
||||
const currentProductId=itemIdFromUrl(tab?.url||'');
|
||||
await chrome.runtime.sendMessage({type:'PRODUCT_ANALYSIS_NOTICE',tabId:tab?.id,state:'analysis_blocked',productId:currentProductId,message}).catch(()=>{});
|
||||
renderTaskStatus({state:'analysis_blocked',message,progress:0,currentProductId,productId:currentProductId});
|
||||
}
|
||||
|
||||
loginAccount.onclick=()=>chrome.runtime.sendMessage({type:'SANSHENG_AUTH_START'});
|
||||
logoutAccount.onclick=async()=>{await chrome.runtime.sendMessage({type:'SANSHENG_AUTH_LOGOUT'});await refresh()};
|
||||
viewCurrentReport.onclick=()=>{if(currentReportUrl)chrome.tabs.create({url:currentReportUrl})};
|
||||
analyzeAgain.onclick=()=>{analyzeNow.click()};
|
||||
closeHistory.onclick=()=>{reportHistory.style.display='none'};
|
||||
modeQuick.onclick=()=>setMode('quick');
|
||||
modeStandard.onclick=()=>setMode('standard');
|
||||
modeFull.onclick=()=>setMode('full');
|
||||
|
||||
runBtn.onclick=async()=>{
|
||||
if(!backendCompatible){status.textContent='云端服务不可用,请确认 Web API 已启动后重试';return}
|
||||
const tab=await active();
|
||||
renderTaskStatus({state:'collecting',message:'正在准备采集当前商品数据',progress:5});
|
||||
try{
|
||||
const r=await chrome.runtime.sendMessage({type:'ANALYZE_CURRENT_NOW',tabId:t.id});
|
||||
if(r?.ok){
|
||||
status.textContent=`已开始分析当前数据${r.reviewCount!=null?` · 当前评论 ${r.reviewCount} 条`:''}`;
|
||||
if(r.taskUrl) chrome.tabs.create({url:/^https?:\/\//i.test(r.taskUrl)?r.taskUrl:SERVER+r.taskUrl});
|
||||
const result=await chrome.runtime.sendMessage({type:'RUN_CURRENT',tabId:tab.id,collectionMode});
|
||||
if(result?.needsReviewResume){
|
||||
status.textContent=result.message||'评论还没到末页,请继续采集';
|
||||
runBtn.textContent='继续采集评论';
|
||||
}else if(result?.ok){
|
||||
status.textContent='采集已提交,云端正在生成报告。你可以继续浏览商品页。';
|
||||
}else{
|
||||
status.textContent='无法开始分析:'+(r?.error||'未知错误');
|
||||
status.textContent='采集失败:'+(result?.error||'未知错误');
|
||||
}
|
||||
}catch(e){
|
||||
status.textContent='无法开始分析:'+String(e);
|
||||
}finally{
|
||||
analyzeNow.disabled=false;
|
||||
analyzeNow.textContent='基于当前数据开始分析';
|
||||
}catch(error){
|
||||
status.textContent='采集失败:'+String(error);
|
||||
}
|
||||
});
|
||||
setTimeout(refresh,500);
|
||||
};
|
||||
|
||||
stopBtn.onclick=async()=>{
|
||||
const tab=await active();
|
||||
stopBtn.disabled=true;
|
||||
stopBtn.textContent='正在终止…';
|
||||
try{
|
||||
const result=await chrome.runtime.sendMessage({type:'STOP_COLLECTION',tabId:tab?.id});
|
||||
status.textContent=result?.ok?'已发送终止指令,正在保存当前进度…':'终止失败,请重试';
|
||||
}catch(error){
|
||||
status.textContent='终止失败:'+String(error);
|
||||
}
|
||||
setTimeout(()=>{stopBtn.disabled=false;stopBtn.textContent='终止采集并保存进度'},1000);
|
||||
};
|
||||
|
||||
analyzeNow.onclick=async()=>{
|
||||
if(!backendCompatible){status.textContent='云端服务不可用,请确认 Web API 已启动后重试';return}
|
||||
const tab=await active();
|
||||
if(!tab?.id){status.textContent='请先打开目标商品页';return}
|
||||
renderTaskStatus({state:'sending',message:'正在提交当前已有数据,请稍候',progress:78});
|
||||
try{
|
||||
const result=await chrome.runtime.sendMessage({type:'ANALYZE_CURRENT_NOW',tabId:tab.id});
|
||||
if(result?.ok){
|
||||
status.textContent=`分析任务已提交${result.reviewCount!=null?` · 当前评论 ${result.reviewCount} 条`:''},你可以继续浏览商品页。`;
|
||||
}else{
|
||||
await showAnalysisSubmitError(tab,userErrorMessage(result?.error||'无法开始分析,请稍后重试'));
|
||||
}
|
||||
}catch(error){
|
||||
await showAnalysisSubmitError(tab,userErrorMessage(error));
|
||||
}finally{
|
||||
setTimeout(refresh,400);
|
||||
}
|
||||
};
|
||||
|
||||
setMode('standard');
|
||||
void refresh();
|
||||
setInterval(refresh,2000);
|
||||
|
||||
let historyLoading=false;
|
||||
|
||||
/** 将任务状态转成中文;原始阶段未知时使用通用状态,不直接展示内部英文。 */
|
||||
function historyState(item){
|
||||
const status=Number(item.Status??item.status);
|
||||
if(status===3)return '报告已完成';
|
||||
if(status===4)return '分析未完成';
|
||||
const progress=Math.max(0,Math.min(100,Number(item.Progress??item.progress??0)||0));
|
||||
const stages={queued:'排队中',analyzing:'分析中',product:'商品定位分析',visual:'主图分析',detail:'详情分析',consumer:'消费者反馈分析',strategy:'生成开品方案',assembling:'整理报告'};
|
||||
const stage=String(item.Stage??item.stage??'');
|
||||
return `${stages[stage]||(stage.startsWith('review')?'评论分析':status===1?'排队中':'分析中')} · ${progress}%`;
|
||||
}
|
||||
|
||||
/** 读取最近报告;防重复点击,最多等待 16 秒,异常后可再次点击重试。 */
|
||||
openReports.onclick=async()=>{
|
||||
reportHistory.style.display='block';
|
||||
if(historyLoading)return;
|
||||
historyLoading=true;
|
||||
historyList.setAttribute('aria-busy','true');
|
||||
historyList.innerHTML='<div class="detail-note" role="status">正在加载报告记录…</div>';
|
||||
let timer;
|
||||
try{
|
||||
const result=await Promise.race([
|
||||
chrome.runtime.sendMessage({type:'PRODUCT_ANALYSIS_HISTORY'}),
|
||||
new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error('报告记录加载超时,请点击“报告记录”重试。')),16000)})
|
||||
]);
|
||||
if(!result?.ok)throw new Error(result?.error||'报告记录加载失败');
|
||||
const items=result.items||[];
|
||||
historyList.innerHTML=items.length?items.map(item=>{
|
||||
const id=String(item.TaskId||item.taskId||'');
|
||||
const title=escapeHtml(item.ProductTitle||item.productTitle||`商品 ${id}`);
|
||||
const status=Number(item.Status??item.status);
|
||||
const path=item.ReportUrl||item.reportUrl||`/product-analysis/reports/${encodeURIComponent(id)}`;
|
||||
const url=/^https?:\/\//i.test(path)?path:sanshengExtensionAuth.config.pcBase+path;
|
||||
return `<div class="history-item" data-status="${status}"><div><strong title="${title}">${title}</strong><span>${escapeHtml(historyState(item))}</span></div><button data-report-url="${escapeHtml(url)}" title="查看分析报告">查看</button></div>`;
|
||||
}).join(''):'<div class="detail-note">暂无分析报告</div>';
|
||||
historyList.querySelectorAll('button[data-report-url]').forEach(button=>button.onclick=()=>chrome.tabs.create({url:button.dataset.reportUrl}));
|
||||
}catch(error){historyList.innerHTML=`<div class="detail-note" role="alert">${escapeHtml(error instanceof Error?error.message:'报告记录加载失败')}</div>`}
|
||||
finally{clearTimeout(timer);historyLoading=false;historyList.setAttribute('aria-busy','false')}
|
||||
};
|
||||
Reference in New Issue
Block a user