let backendCompatible=false; 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''}} 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'} } /** 转义正文及属性中的特殊字符,长标题可安全放入 title 提示。 */ function escapeHtml(value){return String(value||'').replace(/[<>&"']/g,c=>({'<':'<','>':'>','&':'&','"':'"',"'":'''}[c]))} 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=>`${escapeHtml(key)}`).join('')||'暂无'; paramMissing.textContent='缺失项:'+((ps.missing||[]).length?ps.missing.join('、'):'无'); } 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)=>`
${index+1}. ${escapeHtml(value)}
`).join('')||'等待评论数据…'; } 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'); analyzeNow.disabled=isAnalysisRunning; updateRunButtonText(false); } /** * 将接口异常转换成用户可读提示,避免暴露 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 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='采集失败:'+(result?.error||'未知错误'); } }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='
正在加载报告记录…
'; 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 `
${title}${escapeHtml(historyState(item))}
`; }).join(''):'
暂无分析报告
'; historyList.querySelectorAll('button[data-report-url]').forEach(button=>button.onclick=()=>chrome.tabs.create({url:button.dataset.reportUrl})); }catch(error){historyList.innerHTML=``} finally{clearTimeout(timer);historyLoading=false;historyList.setAttribute('aria-busy','false')} };