From f2817e7baf45b9cc646a2b607e6cad093f756a19 Mon Sep 17 00:00:00 2001 From: Caoweijie <594100253@qq.com> Date: Thu, 10 Sep 2026 18:39:46 +0800 Subject: [PATCH] Add product analysis extension release --- .gitignore | 1 + extension/auth.js | 80 ++ extension/background.js | 278 +++++- extension/icon-128.png | Bin 0 -> 3313 bytes extension/manifest.json | 19 +- extension/popup.css | 184 ++++ extension/popup.html | 166 +++- extension/popup.js | 383 +++++--- ...s-extension-9.3.0-prod-20260908-233841.crx | Bin 0 -> 39364 bytes .../auth.js | 81 ++ .../background.js | 819 ++++++++++++++++++ .../icon-128.png | Bin 0 -> 3313 bytes .../manifest.json | 47 + .../page-review-hook.js | 123 +++ .../popup.css | 168 ++++ .../popup.html | 131 +++ .../popup.js | 267 ++++++ 17 files changed, 2568 insertions(+), 179 deletions(-) create mode 100644 extension/auth.js create mode 100644 extension/icon-128.png create mode 100644 extension/popup.css create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841.crx create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/auth.js create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/background.js create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/icon-128.png create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/manifest.json create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/page-review-hook.js create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.css create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.html create mode 100644 release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.js diff --git a/.gitignore b/.gitignore index b42c93c..aab6b18 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ server/config.json .env .env.* +*.pem # python __pycache__/ diff --git a/extension/auth.js b/extension/auth.js new file mode 100644 index 0000000..a36cd7f --- /dev/null +++ b/extension/auth.js @@ -0,0 +1,80 @@ +const SANSHENG_AUTH_CONFIG={ + apiBase:'http://localhost:19174', + pcBase:'http://localhost:5173', + clientId:'sansheng-product-analysis-extension', + scope:'product.analysis', + callbackPath:'/extension/authorize' +}; + +const AUTH_ACCESS_KEY='sansheng_extension_access'; +const AUTH_REFRESH_KEY='sansheng_extension_refresh'; +const AUTH_PENDING_KEY='sansheng_extension_auth_pending'; +const AUTH_LAST_CONTEXT_KEY='sansheng_extension_auth_last_context'; + +function authBase64Url(bytes){return btoa(String.fromCharCode(...bytes)).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'')} +function authRandom(size=32){const bytes=new Uint8Array(size);crypto.getRandomValues(bytes);return authBase64Url(bytes)} +async function authChallenge(verifier){const digest=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(verifier));return authBase64Url(new Uint8Array(digest))} +function authCallbackUrl(){return SANSHENG_AUTH_CONFIG.pcBase+SANSHENG_AUTH_CONFIG.callbackPath} + +async function getExtensionAuth(){ + const session=await chrome.storage.session.get(AUTH_ACCESS_KEY); + const local=await chrome.storage.local.get(AUTH_REFRESH_KEY); + return {access:session[AUTH_ACCESS_KEY]||null,refresh:local[AUTH_REFRESH_KEY]||null}; +} + +async function startExtensionLogin(){ + const verifier=authRandom(48),state=authRandom(24),challenge=await authChallenge(verifier); + const tabs=await chrome.tabs.query({active:true,currentWindow:true}); + await chrome.storage.local.set({[AUTH_PENDING_KEY]:{verifier,state,createdAt:Date.now(),tabId:tabs?.[0]?.id||0,windowId:tabs?.[0]?.windowId||0}}); + const query=new URLSearchParams({client_id:SANSHENG_AUTH_CONFIG.clientId,redirect_uri:authCallbackUrl(),scope:SANSHENG_AUTH_CONFIG.scope,state,code_challenge:challenge,code_challenge_method:'S256'}); + return chrome.tabs.create({url:`${SANSHENG_AUTH_CONFIG.pcBase}${SANSHENG_AUTH_CONFIG.callbackPath}?${query}`,active:true}); +} + +async function exchangeExtensionCode(callbackUrl){ + const url=new URL(callbackUrl),code=url.searchParams.get('code'),state=url.searchParams.get('state'); + if(!code||!state)return false; + const pending=(await chrome.storage.local.get(AUTH_PENDING_KEY))[AUTH_PENDING_KEY]; + if(!pending||pending.state!==state||Date.now()-pending.createdAt>10*60*1000)throw new Error('扩展授权请求已过期,请重新登录'); + const body=new URLSearchParams({grant_type:'authorization_code',client_id:SANSHENG_AUTH_CONFIG.clientId,code,redirect_uri:authCallbackUrl(),code_verifier:pending.verifier}); + const response=await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Token`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body}); + const token=await response.json(); + if(!response.ok||!token.access_token)throw new Error(token.error_description||'扩展登录失败'); + await chrome.storage.session.set({[AUTH_ACCESS_KEY]:{token:token.access_token,type:token.token_type||'Sansheng',expiresAt:Date.now()+(token.expires_in||7200)*1000}}); + await chrome.storage.local.set({[AUTH_REFRESH_KEY]:{token:token.refresh_token,expiresAt:Date.now()+(token.refresh_expires_in||2592000)*1000}}); + await chrome.storage.local.set({[AUTH_LAST_CONTEXT_KEY]:{tabId:pending.tabId||0,windowId:pending.windowId||0,updatedAt:Date.now()}}); + await chrome.storage.local.remove(AUTH_PENDING_KEY); + return true; +} + +async function refreshExtensionToken(){ + const auth=await getExtensionAuth(); + if(!auth.refresh?.token)throw new Error('扩展登录已过期'); + const body=new URLSearchParams({grant_type:'refresh_token',client_id:SANSHENG_AUTH_CONFIG.clientId,refresh_token:auth.refresh.token}); + const response=await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Token`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body}); + const token=await response.json(); + if(!response.ok||!token.access_token)throw new Error(token.error_description||'扩展登录已过期'); + await chrome.storage.session.set({[AUTH_ACCESS_KEY]:{token:token.access_token,type:token.token_type||'Sansheng',expiresAt:Date.now()+(token.expires_in||7200)*1000}}); + await chrome.storage.local.set({[AUTH_REFRESH_KEY]:{token:token.refresh_token,expiresAt:Date.now()+(token.refresh_expires_in||2592000)*1000}}); + return token.access_token; +} + +async function extensionApiFetch(path,options={}){ + let auth=await getExtensionAuth(); + if(!auth.access?.token||auth.access.expiresAt-Date.now()<60_000){await refreshExtensionToken();auth=await getExtensionAuth()} + const send=()=>fetch(SANSHENG_AUTH_CONFIG.apiBase+path,{...options,headers:{...(options.headers||{}),Authorization:`Sansheng ${auth.access.token}`,'Sansheng-Source':'chrome-extension'}}); + let response=await send(); + if(response.status===401){await refreshExtensionToken();auth=await getExtensionAuth();response=await send()} + return response; +} + +async function logoutExtension(){ + const auth=await getExtensionAuth(); + if(auth.refresh?.token){ + const body=new URLSearchParams({client_id:SANSHENG_AUTH_CONFIG.clientId,refresh_token:auth.refresh.token,access_token:auth.access?.token||''}); + await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Revoke`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body}).catch(()=>{}); + } + await chrome.storage.session.remove(AUTH_ACCESS_KEY); + await chrome.storage.local.remove([AUTH_REFRESH_KEY,AUTH_PENDING_KEY,AUTH_LAST_CONTEXT_KEY]); +} + +self.sanshengExtensionAuth={config:SANSHENG_AUTH_CONFIG,get:getExtensionAuth,start:startExtensionLogin,exchange:exchangeExtensionCode,fetch:extensionApiFetch,logout:logoutExtension}; diff --git a/extension/background.js b/extension/background.js index 60c5dcb..7599077 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,15 +1,234 @@ -const SERVER='http://127.0.0.1:17962'; +importScripts('auth.js'); const RUN_GAP=8*60*1000; const sleep=ms=>new Promise(r=>setTimeout(r,ms)); const isProduct=u=>/^https:\/\/detail\.tmall\.com\/item\.htm/i.test(u||'') || /item\.taobao\.com\/item\.htm/i.test(u||''); +const ANALYSIS_TASKS_KEY='sansheng_product_analysis_tasks'; +const LAST_ANALYSIS_TASK_KEY='sansheng_product_analysis_last_task'; +const ANALYSIS_ALARM='sansheng_product_analysis_poll'; +const ANALYSIS_NOTIFICATION_PREFIX='sansheng-product-analysis-'; +let serviceHealthCache={checkedAt:0,ready:false,error:''}; async function setStatus(tabId,patch){ const key='status_'+tabId; const old=(await chrome.storage.local.get(key))[key]||{}; await chrome.storage.local.set({[key]:{...old,...patch,updatedAt:Date.now()}}); } -async function health(){ try{const r=await fetch(SERVER+'/health');return r.ok?await r.json():{ok:false}}catch(e){return {ok:false,error:String(e)}} } +async function submitProductAnalysis(raw){ + raw.collection=raw.collection||{}; + raw.collection.clientRequestId=raw.collection.clientRequestId||crypto.randomUUID(); + const controller=new AbortController(); + const timeout=setTimeout(()=>controller.abort(),15000); + let response; + try{ + response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks',{method:'POST',headers:{'Content-Type':'application/json','Idempotency-Key':raw.collection.clientRequestId},body:JSON.stringify({platform:raw?.meta?.platform||'taobao',productId:String(raw?.product?.itemId||''),analysisMode:raw?.collection?.analysisMode==='current_snapshot'?'quick':(raw?.collection?.reviewCollectionMode||'standard'),userInstruction:'',clientRequestId:raw.collection.clientRequestId,snapshot:raw,source:'chrome-extension'}),signal:controller.signal}); + }catch(error){ + if(error?.name==='AbortError')throw new Error('Web API 响应超时,请确认开发 API 已启动后重试'); + throw new Error('无法连接 Web API,请确认 http://localhost:19174 已启动'); + }finally{clearTimeout(timeout)} + const result=await response.json().catch(()=>null); + const code=result?.Code??result?.code; + if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||`商品分析提交失败 HTTP ${response.status}`); + const data=result.Data||result.data||{};const taskId=String(data.TaskId||data.taskId||''); + return {ok:true,taskId,taskUrl:sanshengExtensionAuth.config.pcBase+(data.ReportUrl||data.reportUrl||`/product-analysis/reports/${taskId}`)}; +} +async function clearTabStatus(tabId){ + await chrome.storage.local.remove('status_'+tabId).catch(()=>{}); +} + +async function checkAnalysisService(force=false){ + if(!force&&Date.now()-serviceHealthCache.checkedAt<8000)return serviceHealthCache; + const controller=new AbortController(); + const timeout=setTimeout(()=>controller.abort(),5000); + try{ + const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=1',{signal:controller.signal}); + const result=await response.json().catch(()=>null); + const code=result?.Code??result?.code; + const ready=response.ok&&result&&code===0; + serviceHealthCache={checkedAt:Date.now(),ready,error:ready?'':(result?.Message||result?.message||`HTTP ${response.status}`)}; + }catch(error){ + serviceHealthCache={checkedAt:Date.now(),ready:false,error:error?.name==='AbortError'?'Web API 响应超时':'无法连接 Web API'}; + }finally{clearTimeout(timeout)} + return serviceHealthCache; +} + +function analysisStageMessage(stage,progress){ + const labels={queued:'任务已进入分析队列',product:'正在分析商品定位与标题',visual:'正在分析主图与视觉表达',detail:'正在分析详情页内容',consumer:'正在汇总消费者体验',strategy:'正在生成经营策略与新品方案',assembling:'正在组装分析报告',completed:'报告已经生成'}; + const suffix=progress!=null?` · 分析进度 ${progress}%`:''; + if(String(stage||'').startsWith('review'))return `正在分析评论(${String(stage).replace('review','').trim()||'处理中'})${suffix}`; + return (labels[stage]||'正在生成报告')+suffix; +} + +async function readTrackedAnalysisTasks(){ + const stored=await chrome.storage.local.get(ANALYSIS_TASKS_KEY); + return stored[ANALYSIS_TASKS_KEY]||{}; +} + +async function saveTrackedAnalysisTasks(tasks){ + await chrome.storage.local.set({[ANALYSIS_TASKS_KEY]:tasks}); + if(Object.keys(tasks).length){ + await chrome.alarms.create(ANALYSIS_ALARM,{periodInMinutes:.5}); + }else{ + await chrome.alarms.clear(ANALYSIS_ALARM); + } +} + +async function updateAnalysisBadge(state,progress){ + const text=state==='analysis_completed'?'✓':state==='analysis_failed'?'!':String(Math.max(1,Math.min(99,Math.round(progress||1)))); + const color=state==='analysis_completed'?'#138a70':state==='analysis_failed'?'#b83b4b':'#315fba'; + await chrome.action.setBadgeBackgroundColor({color}).catch(()=>{}); + await chrome.action.setBadgeText({text}).catch(()=>{}); +} + +async function showAnalysisNotification(task,state){ + const completed=state==='analysis_completed'; + await chrome.notifications.create(ANALYSIS_NOTIFICATION_PREFIX+task.taskId,{ + type:'basic', + iconUrl:chrome.runtime.getURL('icon-128.png'), + title:completed?'商品分析报告已生成':'商品分析未完成', + message:completed?'点击通知即可查看完整分析报告。':(task.errorMessage||'请打开扩展查看失败原因。'), + priority:completed?2:1 + }).catch(()=>{}); +} + +async function writeAnalysisPresentation(task,state,message,progress,extra={}){ + const effectiveProgress=progress??task.progress??10; + task.progress=effectiveProgress; + const presentation={state,message,progress:effectiveProgress,taskId:task.taskId,taskUrl:task.taskUrl,productId:task.productId||'',stage:task.stage||'',productTitle:task.productTitle||'',...extra}; + await setStatus(task.tabId,presentation); + await chrome.storage.local.set({[LAST_ANALYSIS_TASK_KEY]:{...presentation,tabId:task.tabId,updatedAt:Date.now()}}); + await updateAnalysisBadge(state,progress); +} + +async function trackProductAnalysis(tabId,submitted,productTitle='',productId=''){ + const tasks=await readTrackedAnalysisTasks(); + const task={taskId:submitted.taskId,taskUrl:submitted.taskUrl,tabId,productId:String(productId||''),productTitle,submittedAt:Date.now(),nextPollAt:0,pollErrors:0,stage:'queued'}; + tasks[task.taskId]=task; + await saveTrackedAnalysisTasks(tasks); + await writeAnalysisPresentation(task,'analyzing','任务已提交,正在排队分析',80,{analysisProgress:10}); + await pollTrackedAnalysisTasks(true); +} + +async function pollTrackedAnalysisTask(task,force=false){ + if(!force&&Number(task.nextPollAt||0)>Date.now())return {keep:true,task}; + try{ + const response=await sanshengExtensionAuth.fetch(`/v1/product-analysis/tasks/${encodeURIComponent(task.taskId)}`); + const result=await response.json().catch(()=>null); + const code=result?.Code??result?.code; + if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||`任务状态查询失败 HTTP ${response.status}`); + const data=result.Data||result.data||{}; + const status=Number(data.Status??data.status??0),progress=Number(data.Progress??data.progress??10),stage=String(data.Stage??data.stage??'queued'); + const displayProgress=Math.min(99,80+Math.round(progress*.2)); + task={...task,stage,progress:displayProgress,analysisProgress:progress,nextPollAt:Date.now()+3000,pollErrors:0}; + if(status===3){ + await writeAnalysisPresentation(task,'analysis_completed','报告已生成,点击下方按钮查看',100,{stage:'completed'}); + await showAnalysisNotification(task,'analysis_completed'); + return {keep:false,task}; + } + if(status===4){ + task.errorMessage=String(data.ErrorMessage??data.errorMessage??'分析任务执行失败'); + await writeAnalysisPresentation(task,'analysis_failed',task.errorMessage,displayProgress,{stage,errorMessage:task.errorMessage,analysisProgress:progress}); + await showAnalysisNotification(task,'analysis_failed'); + return {keep:false,task}; + } + const queuedTooLong=stage==='queued'&&Date.now()-Number(task.submittedAt||Date.now())>90000; + const message=queuedTooLong?'任务排队时间较长,请确认分析 Worker 已启动;扩展仍会继续等待':analysisStageMessage(stage,progress); + await writeAnalysisPresentation(task,'analyzing',message,displayProgress,{stage,analysisProgress:progress}); + return {keep:true,task}; + }catch(error){ + task={...task,nextPollAt:Date.now()+15000,pollErrors:Number(task.pollErrors||0)+1}; + await writeAnalysisPresentation(task,'analyzing',task.pollErrors>=3?'云端状态暂时不可用,后台稍后继续查询':'正在等待云端分析状态',undefined,{stage:task.stage||'queued'}); + return {keep:true,task}; + } +} + +async function pollTrackedAnalysisTasks(force=false){ + const tasks=await readTrackedAnalysisTasks(); + for(const [taskId,task] of Object.entries(tasks)){ + const result=await pollTrackedAnalysisTask(task,force); + if(result.keep)tasks[taskId]=result.task;else delete tasks[taskId]; + } + await saveTrackedAnalysisTasks(tasks); +} + +async function bootstrapLatestAnalysisTask(tabId){ + try{ + const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=1'); + const result=await response.json().catch(()=>null); + const code=result?.Code??result?.code; + if(!response.ok||!result||code!==0)return null; + const page=result.Data||result.data||{}; + const item=(page.List||page.list||[])[0]; + if(!item)return null; + const taskId=String(item.TaskId||item.taskId||''); + if(!taskId)return null; + const reportPath=item.ReportUrl||item.reportUrl||`/product-analysis/reports/${taskId}`; + const taskUrl=/^https?:\/\//i.test(reportPath)?reportPath:sanshengExtensionAuth.config.pcBase+reportPath; + const task={taskId,taskUrl,tabId,productId:String(item.ProductId||item.productId||''),productTitle:item.ProductTitle||item.productTitle||'',stage:item.Stage||item.stage||'queued',progress:Number(item.Progress??item.progress??10),nextPollAt:0,pollErrors:0}; + const status=Number(item.Status??item.status??0); + if(status===1||status===2){ + const tasks=await readTrackedAnalysisTasks(); + tasks[taskId]=task; + await saveTrackedAnalysisTasks(tasks); + const polled=await pollTrackedAnalysisTask(task,true); + return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY]||polled.task; + } + if(status===3){ + await writeAnalysisPresentation(task,'analysis_completed','最近一份报告已生成,点击下方按钮查看',100,{stage:'completed',analysisProgress:100}); + return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY]; + } + if(status===4){ + await writeAnalysisPresentation(task,'analysis_failed','最近一份商品分析未完成',task.progress,{stage:task.stage}); + return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY]; + } + }catch(error){return null} + return null; +} + +async function refreshAnalysisForTab(tabId){ + const tasks=await readTrackedAnalysisTasks(); + const task=Object.values(tasks).find(item=>Number(item.tabId)===Number(tabId)); + if(task){ + const result=await pollTrackedAnalysisTask(task,false); + if(result.keep)tasks[task.taskId]=result.task;else delete tasks[task.taskId]; + await saveTrackedAnalysisTasks(tasks); + } + const stored=await chrome.storage.local.get(['status_'+tabId,LAST_ANALYSIS_TASK_KEY]); + const current=stored['status_'+tabId]; + const last=stored[LAST_ANALYSIS_TASK_KEY]; + if(current?.taskId)return current; + if(last?.taskId)return last; + return await bootstrapLatestAnalysisTask(tabId); +} + +chrome.alarms.onAlarm.addListener(alarm=>{if(alarm.name===ANALYSIS_ALARM)pollTrackedAnalysisTasks(false).catch(()=>{})}); +chrome.notifications.onClicked.addListener(notificationId=>{ + if(!notificationId.startsWith(ANALYSIS_NOTIFICATION_PREFIX))return; + const taskId=notificationId.slice(ANALYSIS_NOTIFICATION_PREFIX.length); + chrome.tabs.create({url:sanshengExtensionAuth.config.pcBase+`/product-analysis/reports/${encodeURIComponent(taskId)}`}); + chrome.notifications.clear(notificationId).catch(()=>{}); +}); +pollTrackedAnalysisTasks(false).catch(()=>{}); chrome.runtime.onMessage.addListener((m,s,send)=>{ + if(m?.type==='SANSHENG_AUTH_START'){sanshengExtensionAuth.start().then(()=>send({ok:true})).catch(e=>send({ok:false,error:String(e)}));return true;} + if(m?.type==='SANSHENG_AUTH_STATE'){sanshengExtensionAuth.get().then(async a=>{const loggedIn=!!(a.access?.token||a.refresh?.token);const health=loggedIn?await checkAnalysisService(false):{ready:false,error:''};send({ok:true,loggedIn,serviceReady:health.ready,serviceError:health.error})}).catch(e=>send({ok:false,loggedIn:false,serviceReady:false,error:String(e)}));return true;} + if(m?.type==='SANSHENG_AUTH_LOGOUT'){sanshengExtensionAuth.logout().then(async()=>{await saveTrackedAnalysisTasks({});await chrome.storage.local.remove(LAST_ANALYSIS_TASK_KEY);await chrome.action.setBadgeText({text:''}).catch(()=>{});send({ok:true})}).catch(e=>send({ok:false,error:String(e)}));return true;} + if(m?.type==='PRODUCT_ANALYSIS_STATUS'){refreshAnalysisForTab(m.tabId).then(status=>send({ok:true,status})).catch(e=>send({ok:false,error:String(e)}));return true;} + if(m?.type==='PRODUCT_ANALYSIS_HISTORY'){ + (async()=>{try{ + const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=8',{signal:AbortSignal.timeout(15000)}); + const result=await response.json().catch(()=>null); const code=result?.Code??result?.code; + if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||'报告记录加载失败'); + send({ok:true,items:result.Data?.List||result.data?.list||[]}); + }catch(error){send({ok:false,error:String(error)})}})(); + return true; + } + if(m?.type==='PRODUCT_ANALYSIS_NOTICE'){ + (async()=>{try{ + await setStatus(m.tabId,{state:m.state||'analysis_blocked',message:String(m.message||'当前暂时无法提交分析'),progress:0,productId:String(m.productId||'')}); + send({ok:true}); + }catch(error){send({ok:false,error:String(error)})}})(); + return true; + } if(m?.type==='ANALYZE_CURRENT_NOW'){ (async()=>{ try{ @@ -45,27 +264,41 @@ chrome.runtime.onMessage.addListener((m,s,send)=>{ raw.collection.analysisStartedBeforeReviewComplete=!raw.collection.reviewCollectionComplete; raw.collection.analysisStartedAt=new Date().toISOString(); - const resp=await fetch('http://127.0.0.1:17962/api/analyze-current',{ - method:'POST',headers:{'Content-Type':'application/json'}, - body:JSON.stringify({raw,allowPartialReviews:true}) - }); - const j=await resp.json().catch(()=>({ok:false,error:'后端返回无法解析'})); + const j=await submitProductAnalysis(raw); + await trackProductAnalysis(tabId,j,raw.product?.title||'',raw.product?.itemId||''); send({...j,reviewCount:raw.reviews?.length||0}); }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,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; } + 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; } }); -async function run(tabId,manual,sampleMode='500'){ +chrome.tabs.onUpdated.addListener((tabId,changeInfo)=>{ + const url=changeInfo.url; + if(url&&isProduct(url))clearTabStatus(tabId).catch(()=>{}); + if(!url||!url.startsWith(sanshengExtensionAuth.config.pcBase+sanshengExtensionAuth.config.callbackPath))return; + const parsed=new URL(url); + if(!parsed.searchParams.get('code'))return; + sanshengExtensionAuth.exchange(url).then(async ok=>{ + if(!ok)return; + const pending=(await chrome.storage.local.get('sansheng_extension_auth_last_context')).sansheng_extension_auth_last_context; + await chrome.tabs.remove(tabId).catch(()=>{}); + if(pending?.tabId){ + await chrome.tabs.update(pending.tabId,{active:true}).catch(()=>{}); + if(pending.windowId)await chrome.windows.update(pending.windowId,{focused:true}).catch(()=>{}); + await setStatus(pending.tabId,{state:'idle',message:'登录成功,请在当前商品页打开扩展开始采集。',progress:0}); + await chrome.action.openPopup({windowId:pending.windowId}).catch(()=>{}); + } + }).catch(error=>console.error('[三笙扩展登录]',error)); +}); + +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(); - if(!h.ok){ await setStatus(tabId,{state:'server_offline',message:'本地 AI 服务未启动'}); throw new Error('本地 AI 服务未启动,请先运行 根目录的「start-tmall-ai.command」'); } - if(!h.modelConfigured){ await setStatus(tabId,{state:'model_unconfigured',message:'模型未配置:请填写 本地设置页'}); throw new Error('模型未配置。请先填写 本地设置页 的 api_key 与 model。'); } + const auth=await sanshengExtensionAuth.get(); + if(!auth.access?.token&&!auth.refresh?.token){await setStatus(tabId,{state:'login_required',message:'请先登录三笙账号'});throw new Error('请先登录三笙账号')} const [{result:pageState}]=await chrome.scripting.executeScript({ target:{tabId},world:'MAIN', func:async()=>{ @@ -80,10 +313,10 @@ async function run(tabId,manual,sampleMode='500'){ 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',sampleMode,message:`正在采集当前商品:主图、SKU、详情、参数、问答与评论(评论样本:${sampleMode==='all'?'全部':sampleMode+' 条'})。不会采集推荐商品…`,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:[]};}}); let collectorDone=false,collectorError=null;const startedAt=Date.now(); - 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}); + 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)); @@ -110,18 +343,17 @@ async function run(tabId,manual,sampleMode='500'){ 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',sampleMode, 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',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}); - const r=await fetch(SERVER+'/api/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(raw)}); - const j=await r.json(); if(!r.ok||!j.ok) throw new Error(j.error||'提交分析失败'); + const j=await submitProductAnalysis(raw); await setStatus(tabId,{state:'analyzing',message:'正在分析全方位产品体验并生成解决方案',progress:82,taskId:j.taskId,taskUrl:j.taskUrl}); - await chrome.tabs.create({url:j.taskUrl}); + await trackProductAnalysis(tabId,j,raw.product?.title||'',raw.product?.itemId||''); 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,sampleMode='500'){ +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,7 +376,7 @@ async function collector(manual=false,sampleMode='500'){ const bodyText=clean(bodyClone?.innerText||document.body?.innerText||''); const captured=[]; const reviews=[]; const questions=[]; - const reviewCap=sampleMode==='all'?Infinity:Math.max(1,Math.min(10000,Number(sampleMode)||500)); + const reviewCap=collectionMode==='quick'?300:(collectionMode==='full'?1500:800); const reviewNetMeta={totals:[],hasMoreFalse:false,lastPageFlags:[],pageHints:[],networkReviewObjects:0,networkResponses:0}; const reviewStartedAt=Date.now(); const processedCaptureKeys=new Set(); @@ -331,7 +563,7 @@ async function collector(manual=false,sampleMode='500'){ // 每轮即时解析已捕获网络响应 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(Number.isFinite(reviewCap) && after>=reviewCap){collectionComplete=true;explicitEnd=true;stopReason='sample_cap';publishReviewProgress(round+1,stagnant,`已采够 ${reviewCap} 条评论样本`);break} + 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} @@ -581,6 +813,6 @@ async function collector(manual=false,sampleMode='500'){ 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: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} + 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/icon-128.png b/extension/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..b4f7dec0ffd121954321fb06b8fa036c268087d8 GIT binary patch literal 3313 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 literal 0 HcmV?d00001 diff --git a/extension/manifest.json b/extension/manifest.json index 8735b79..5c92b74 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,26 +1,35 @@ { "manifest_version": 3, - "name": "三笙 · 电商商品分析闭环 V9.3.0", - "version": "9.3.0", + "name": "三笙 · 电商商品分析闭环 V9.2.5", + "version": "9.2.5", "description": "跨类目电商商品分析:动态参数与SKU采集、图文分源、评论证据、开品与AI视觉方案。", "permissions": [ "scripting", "storage", - "tabs" + "tabs", + "alarms", + "notifications" ], "host_permissions": [ "https://detail.tmall.com/*", "https://*.tmall.com/*", "https://*.taobao.com/*", "https://*.alicdn.com/*", - "http://127.0.0.1:17962/*" + "https://api.sanshengai.net/*", + "https://www.sanshengai.net/*", + "http://localhost:19174/*", + "http://localhost:5173/*" ], "background": { "service_worker": "background.js" }, + "icons": { + "128": "icon-128.png" + }, "action": { "default_popup": "popup.html", - "default_title": "三笙 · 电商商品分析闭环 V9.3.0" + "default_title": "三笙 · 电商商品分析闭环 V9.2.5", + "default_icon": "icon-128.png" }, "content_scripts": [ { diff --git a/extension/popup.css b/extension/popup.css new file mode 100644 index 0000000..fa5025e --- /dev/null +++ b/extension/popup.css @@ -0,0 +1,184 @@ +:root { + color-scheme: light; + --ink: #142126; + --muted: #687a80; + --line: #dbe6e5; + --soft: #f3f8f7; + --green: #00a98f; + --green-dark: #087565; + --blue: #1675d1; + --danger: #b63f4b; +} + +* { box-sizing: border-box; } + +/* 扩展窗口按内容自动定尺寸:根节点必须有独立宽度,不能用 vw 反向依赖窗口。 + 只由根节点滚动,始终预留滚动条位置,历史展开和悬停时不挤压内容。 */ +html { + width: 460px; + min-width: 460px; + max-width: 460px; + max-height: 600px; + overflow-x: hidden; + overflow-y: auto; + scrollbar-gutter: stable; +} + +body { + width: 100%; + min-width: 0; + margin: 0; + background: #f6faf9; + color: var(--ink); + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; +} + +button { font: inherit; } +.brand-row > .text-action { flex: 0 0 auto; white-space: nowrap; } + +.app-header { + padding: 18px 18px 15px; + border-bottom: 1px solid var(--line); + background: #fff; +} + +.brand-row, +.account-row, +.status-head, +.detail-title, +.metric-row, +.section-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.brand { display: flex; align-items: center; gap: 10px; min-width: 0; } +.brand img { width: 34px; height: 34px; border-radius: 8px; } +.brand h1 { margin: 0; font-size: 17px; line-height: 1.2; letter-spacing: 0; } +.brand span { display: block; margin-top: 2px; color: var(--muted); font-size: 11px; } +.app-header p { margin: 12px 0 0; color: var(--muted); font-size: 12px; line-height: 1.55; } + +main { padding: 14px 16px 18px; } + +.text-action, +.compact-button { + border: 0; + background: transparent; + color: var(--green-dark); + cursor: pointer; + font-size: 12px; + font-weight: 700; +} + +.compact-button { + min-width: 52px; + height: 30px; + border: 1px solid var(--line); + border-radius: 7px; + background: #fff; +} +.compact-button.primary { border-color: var(--green); background: var(--green); color: #fff; } + +.account-row { margin-bottom: 12px; } +.account-copy { display: flex; align-items: center; gap: 9px; min-width: 0; } +.account-copy i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: #a9b6b8; } +.account-copy i.online { background: var(--green); box-shadow: 0 0 0 4px rgb(0 169 143 / 12%); } +.account-copy strong { display: block; font-size: 12px; } +.account-copy span { display: block; max-width: 230px; margin-top: 2px; overflow: hidden; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } + +.status-card, +.detail-panel, +.mode-panel { + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; +} + +.status-card { padding: 15px; } +.phase { display: flex; align-items: center; gap: 8px; } +.phase strong { font-size: 14px; } +.phase-indicator { width: 9px; height: 9px; border-radius: 50%; background: #9aa9ac; } +.status-head > b { color: var(--green-dark); font-size: 13px; } +.status-message { min-height: 38px; margin-top: 10px; color: #53676d; font-size: 12px; line-height: 1.55; } +.progress-track { height: 7px; margin-top: 12px; overflow: hidden; border-radius: 6px; background: #e9f0ef; } +.progress-track i { display: block; width: 0; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--green), var(--blue)); transition: width .25s ease; } +.task-meta { margin-top: 10px; overflow: hidden; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.wait-hint { margin-top: 10px; padding: 9px 10px; border-radius: 7px; background: #eef7ff; color: #43616e; font-size: 11px; line-height: 1.45; } + +.status-card[data-state="collecting"] .phase-indicator, +.status-card[data-state="analyzing"] .phase-indicator { + background: var(--blue); + box-shadow: 0 0 0 4px rgb(22 117 209 / 12%); + animation: pulse 1.4s ease-in-out infinite; +} +.status-card[data-state="completed"] .phase-indicator { background: var(--green); box-shadow: 0 0 0 4px rgb(0 169 143 / 12%); } +.status-card[data-state="failed"] .phase-indicator { background: var(--danger); box-shadow: 0 0 0 4px rgb(182 63 75 / 12%); } + +@keyframes pulse { + 50% { opacity: .35; transform: scale(.8); } +} + +.service-line { display: flex; justify-content: flex-end; min-height: 25px; padding: 7px 2px 4px; } +.service-line span { color: var(--muted); font-size: 10px; } +.service-line span.ok { color: var(--green-dark); } +.service-line span.warn { color: #a56a19; } + +.action-section { display: grid; gap: 8px; } +.main-button, +.secondary-button { + width: 100%; + min-height: 42px; + border: 0; + border-radius: 8px; + cursor: pointer; + font-size: 13px; + font-weight: 800; +} +.main-button { background: linear-gradient(105deg, var(--green), var(--blue)); color: #fff; } +.main-button:hover { filter: brightness(.97); } +.main-button:disabled, +.secondary-button:disabled { cursor: not-allowed; opacity: .55; } +.main-button.danger { background: var(--danger); } +.report-button { margin-top: 12px; background: var(--ink); } +.secondary-button { margin-top: 12px; border: 1px solid #a8c8c4; background: #fff; color: #245d58; } +/* 网格列必须允许收缩,否则长商品标题的最小内容宽度会撑出扩展弹窗。 */ +.history-panel { margin-top: 12px; min-width: 0; } +.history-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; margin-top: 10px; min-width: 0; } +.history-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; min-width: 0; max-width: 100%; padding: 10px; border: 1px solid var(--line); border-radius: 7px; background: var(--soft); } +.history-item > div { min-width: 0; } +.history-item strong { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow-wrap: anywhere; font-size: 12px; line-height: 1.5; } +.history-item span { display: block; margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: 11px; line-height: 1.4; } +.history-item[data-status="3"] span { color: var(--green-dark); } +.history-item[data-status="4"] span { color: var(--danger); } +.history-item button { min-width: 42px; min-height: 32px; padding: 6px; border: 1px solid var(--line); border-radius: 6px; background: #fff; color: var(--green-dark); cursor: pointer; font-size: 11px; font-weight: 700; } +.history-item button:hover { border-color: var(--green); } +.history-item button:focus-visible { outline: 2px solid var(--green-dark); outline-offset: 2px; } +.history-list .detail-note { overflow-wrap: anywhere; line-height: 1.5; } +.detail-panel { margin-top: 12px; padding: 13px; } +.detail-title strong, +.section-heading strong { font-size: 13px; } +.detail-title span, +.section-heading span { color: var(--muted); font-size: 11px; } +.metric-row { margin-top: 10px; font-size: 11px; } +.metric-row span { color: var(--muted); } +.metric-row b { font-size: 13px; } +.metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 11px; } +.metric-grid div { padding: 9px; border-radius: 7px; background: var(--soft); } +.metric-grid span { display: block; color: var(--muted); font-size: 10px; } +.metric-grid b { display: block; margin-top: 3px; font-size: 14px; } +.detail-note { margin-top: 9px; color: var(--muted); font-size: 10px; } +.param-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; } +.param-tag { padding: 3px 7px; border: 1px solid var(--line); border-radius: 6px; background: var(--soft); color: #4c6267; font-size: 10px; } +.live-preview { max-height: 68px; margin-top: 9px; overflow: auto; border-top: 1px solid var(--line); padding-top: 8px; color: #53686d; font-size: 10px; line-height: 1.55; } + +.mode-panel { margin-top: 12px; padding: 14px; } +.section-heading span { display: block; margin-top: 2px; } +.mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 11px; } +.mode { min-height: 58px; border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--ink); cursor: pointer; } +.mode strong { display: block; font-size: 12px; } +.mode small { display: block; margin-top: 4px; color: var(--muted); font-size: 9px; } +.mode.active { border-color: var(--ink); background: var(--ink); color: #fff; } +.mode.active small { color: #c6d1d3; } +.option-note { margin-top: 8px; color: var(--muted); font-size: 10px; text-align: center; } diff --git a/extension/popup.html b/extension/popup.html index fae21d5..a2bc6ce 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -1,41 +1,131 @@ - -

三笙 · 电商商品分析引擎

-
采集当前商品后自动识别类目和分析维度,生成标题、主图、详情、SKU、评论、新品设计与 AI 视觉报告。
-
检查本地服务…
等待当前商品页
-
采集前先选择评论样本
只限制评论样本数量;商品信息、参数、图片、问答和完整分析内容都会保留。
- - - - - -
采集开始后不能切换样本档位;如中途停止,可按原档位继续。
- - +
+
+
+ + 等待开始 +
+ 0% +
+
请打开天猫或淘宝商品详情页
+
+ + + + +
- - -
分析入口
已有采集数据时,可直接生成报告;不会清空当前采集进度。
- -
不会采集推荐商品。评论会自动去重、分类和抽样;采集过程中可随时停止并继续。
- - +
+ 正在检查云端服务 +
+ +
+ + +
+ + + + + +
+
+
+ 采集深度 + 决定最多分析的评论数量 +
+
+
+ + + +
+ +
当前数据可能尚未覆盖全部公开评论。
+
+ + + + + + + + diff --git a/extension/popup.js b/extension/popup.js index 3d5b4f5..2975b32 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -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=>`${String(k).replace(/[<>&]/g,s=>({'<':'<','>':'>','&':'&'}[s]))}`).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)=>`
${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'} +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{ - 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=>`${escapeHtml(key)}`).join('')||'暂无'; + 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)=>`
${index+1}. ${escapeHtml(value)}
`).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='
正在加载报告记录…
'; + 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')} +}; \ No newline at end of file diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841.crx b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841.crx new file mode 100644 index 0000000000000000000000000000000000000000..64b9440a3ca20b739bf73b11037c7b3eee1aaa31 GIT binary patch literal 39364 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 RC!2|jF?hdb$*_M${{u4+A8G&q literal 0 HcmV?d00001 diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/auth.js b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/auth.js new file mode 100644 index 0000000..1ffa871 --- /dev/null +++ b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/auth.js @@ -0,0 +1,81 @@ +const SANSHENG_AUTH_CONFIG={ + apiBase:'https://api.sanshengai.net', + pcBase:'https://www.sanshengai.net', + clientId:'sansheng-product-analysis-extension', + scope:'product.analysis', + callbackPath:'/extension/authorize' +}; + +const AUTH_ACCESS_KEY='sansheng_extension_access'; +const AUTH_REFRESH_KEY='sansheng_extension_refresh'; +const AUTH_PENDING_KEY='sansheng_extension_auth_pending'; +const AUTH_LAST_CONTEXT_KEY='sansheng_extension_auth_last_context'; + +function authBase64Url(bytes){return btoa(String.fromCharCode(...bytes)).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'')} +function authRandom(size=32){const bytes=new Uint8Array(size);crypto.getRandomValues(bytes);return authBase64Url(bytes)} +async function authChallenge(verifier){const digest=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(verifier));return authBase64Url(new Uint8Array(digest))} +function authCallbackUrl(){return SANSHENG_AUTH_CONFIG.pcBase+SANSHENG_AUTH_CONFIG.callbackPath} + +async function getExtensionAuth(){ + const session=await chrome.storage.session.get(AUTH_ACCESS_KEY); + const local=await chrome.storage.local.get(AUTH_REFRESH_KEY); + return {access:session[AUTH_ACCESS_KEY]||null,refresh:local[AUTH_REFRESH_KEY]||null}; +} + +async function startExtensionLogin(){ + const verifier=authRandom(48),state=authRandom(24),challenge=await authChallenge(verifier); + const tabs=await chrome.tabs.query({active:true,currentWindow:true}); + await chrome.storage.local.set({[AUTH_PENDING_KEY]:{verifier,state,createdAt:Date.now(),tabId:tabs?.[0]?.id||0,windowId:tabs?.[0]?.windowId||0}}); + const query=new URLSearchParams({client_id:SANSHENG_AUTH_CONFIG.clientId,redirect_uri:authCallbackUrl(),scope:SANSHENG_AUTH_CONFIG.scope,state,code_challenge:challenge,code_challenge_method:'S256'}); + return chrome.tabs.create({url:`${SANSHENG_AUTH_CONFIG.pcBase}${SANSHENG_AUTH_CONFIG.callbackPath}?${query}`,active:true}); +} + +async function exchangeExtensionCode(callbackUrl){ + const url=new URL(callbackUrl),code=url.searchParams.get('code'),state=url.searchParams.get('state'); + if(!code||!state)return false; + const pending=(await chrome.storage.local.get(AUTH_PENDING_KEY))[AUTH_PENDING_KEY]; + if(!pending||pending.state!==state||Date.now()-pending.createdAt>10*60*1000)throw new Error('扩展授权请求已过期,请重新登录'); + const body=new URLSearchParams({grant_type:'authorization_code',client_id:SANSHENG_AUTH_CONFIG.clientId,code,redirect_uri:authCallbackUrl(),code_verifier:pending.verifier}); + const response=await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Token`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body}); + const token=await response.json(); + if(!response.ok||!token.access_token)throw new Error(token.error_description||'扩展登录失败'); + await chrome.storage.session.set({[AUTH_ACCESS_KEY]:{token:token.access_token,type:token.token_type||'Sansheng',expiresAt:Date.now()+(token.expires_in||7200)*1000}}); + await chrome.storage.local.set({[AUTH_REFRESH_KEY]:{token:token.refresh_token,expiresAt:Date.now()+(token.refresh_expires_in||2592000)*1000}}); + await chrome.storage.local.set({[AUTH_LAST_CONTEXT_KEY]:{tabId:pending.tabId||0,windowId:pending.windowId||0,updatedAt:Date.now()}}); + await chrome.storage.local.remove(AUTH_PENDING_KEY); + return true; +} + +async function refreshExtensionToken(){ + const auth=await getExtensionAuth(); + if(!auth.refresh?.token)throw new Error('扩展登录已过期'); + const body=new URLSearchParams({grant_type:'refresh_token',client_id:SANSHENG_AUTH_CONFIG.clientId,refresh_token:auth.refresh.token}); + const response=await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Token`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body}); + const token=await response.json(); + if(!response.ok||!token.access_token)throw new Error(token.error_description||'扩展登录已过期'); + await chrome.storage.session.set({[AUTH_ACCESS_KEY]:{token:token.access_token,type:token.token_type||'Sansheng',expiresAt:Date.now()+(token.expires_in||7200)*1000}}); + await chrome.storage.local.set({[AUTH_REFRESH_KEY]:{token:token.refresh_token,expiresAt:Date.now()+(token.refresh_expires_in||2592000)*1000}}); + return token.access_token; +} + +async function extensionApiFetch(path,options={}){ + let auth=await getExtensionAuth(); + if(!auth.access?.token||auth.access.expiresAt-Date.now()<60_000){await refreshExtensionToken();auth=await getExtensionAuth()} + const send=()=>fetch(SANSHENG_AUTH_CONFIG.apiBase+path,{...options,headers:{...(options.headers||{}),Authorization:`Sansheng ${auth.access.token}`,'Sansheng-Source':'chrome-extension'}}); + let response=await send(); + if(response.status===401){await refreshExtensionToken();auth=await getExtensionAuth();response=await send()} + return response; +} + +async function logoutExtension(){ + const auth=await getExtensionAuth(); + if(auth.refresh?.token){ + const body=new URLSearchParams({client_id:SANSHENG_AUTH_CONFIG.clientId,refresh_token:auth.refresh.token,access_token:auth.access?.token||''}); + await fetch(`${SANSHENG_AUTH_CONFIG.apiBase}/V1/OAuth/Revoke`,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body}).catch(()=>{}); + } + await chrome.storage.session.remove(AUTH_ACCESS_KEY); + await chrome.storage.local.remove([AUTH_REFRESH_KEY,AUTH_PENDING_KEY,AUTH_LAST_CONTEXT_KEY]); +} + +self.sanshengExtensionAuth={config:SANSHENG_AUTH_CONFIG,get:getExtensionAuth,start:startExtensionLogin,exchange:exchangeExtensionCode,fetch:extensionApiFetch,logout:logoutExtension}; + diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/background.js b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/background.js new file mode 100644 index 0000000..2964f71 --- /dev/null +++ b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/background.js @@ -0,0 +1,819 @@ +importScripts('auth.js'); +const RUN_GAP=8*60*1000; +const sleep=ms=>new Promise(r=>setTimeout(r,ms)); +const isProduct=u=>/^https:\/\/detail\.tmall\.com\/item\.htm/i.test(u||'') || /item\.taobao\.com\/item\.htm/i.test(u||''); +const ANALYSIS_TASKS_KEY='sansheng_product_analysis_tasks'; +const LAST_ANALYSIS_TASK_KEY='sansheng_product_analysis_last_task'; +const ANALYSIS_ALARM='sansheng_product_analysis_poll'; +const ANALYSIS_NOTIFICATION_PREFIX='sansheng-product-analysis-'; +let serviceHealthCache={checkedAt:0,ready:false,error:''}; + +async function setStatus(tabId,patch){ + const key='status_'+tabId; const old=(await chrome.storage.local.get(key))[key]||{}; + await chrome.storage.local.set({[key]:{...old,...patch,updatedAt:Date.now()}}); +} +async function submitProductAnalysis(raw){ + raw.collection=raw.collection||{}; + raw.collection.clientRequestId=raw.collection.clientRequestId||crypto.randomUUID(); + const controller=new AbortController(); + const timeout=setTimeout(()=>controller.abort(),15000); + let response; + try{ + response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks',{method:'POST',headers:{'Content-Type':'application/json','Idempotency-Key':raw.collection.clientRequestId},body:JSON.stringify({platform:raw?.meta?.platform||'taobao',productId:String(raw?.product?.itemId||''),analysisMode:raw?.collection?.analysisMode==='current_snapshot'?'quick':(raw?.collection?.reviewCollectionMode||'standard'),userInstruction:'',clientRequestId:raw.collection.clientRequestId,snapshot:raw,source:'chrome-extension'}),signal:controller.signal}); + }catch(error){ + if(error?.name==='AbortError')throw new Error('三笙云端分析服务响应超时,请稍后重试'); + throw new Error('无法连接三笙云端分析服务,请稍后重试'); + }finally{clearTimeout(timeout)} + const result=await response.json().catch(()=>null); + const code=result?.Code??result?.code; + if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||`商品分析提交失败 HTTP ${response.status}`); + const data=result.Data||result.data||{};const taskId=String(data.TaskId||data.taskId||''); + return {ok:true,taskId,taskUrl:sanshengExtensionAuth.config.pcBase+(data.ReportUrl||data.reportUrl||`/product-analysis/reports/${taskId}`)}; +} +async function clearTabStatus(tabId){ + await chrome.storage.local.remove('status_'+tabId).catch(()=>{}); +} + +async function checkAnalysisService(force=false){ + if(!force&&Date.now()-serviceHealthCache.checkedAt<8000)return serviceHealthCache; + const controller=new AbortController(); + const timeout=setTimeout(()=>controller.abort(),5000); + try{ + const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=1',{signal:controller.signal}); + const result=await response.json().catch(()=>null); + const code=result?.Code??result?.code; + const ready=response.ok&&result&&code===0; + serviceHealthCache={checkedAt:Date.now(),ready,error:ready?'':(result?.Message||result?.message||`HTTP ${response.status}`)}; + }catch(error){ + serviceHealthCache={checkedAt:Date.now(),ready:false,error:error?.name==='AbortError'?'Web API 响应超时':'无法连接 Web API'}; + }finally{clearTimeout(timeout)} + return serviceHealthCache; +} + +function analysisStageMessage(stage,progress){ + const labels={queued:'任务已进入分析队列',product:'正在分析商品定位与标题',visual:'正在分析主图与视觉表达',detail:'正在分析详情页内容',consumer:'正在汇总消费者体验',strategy:'正在生成经营策略与新品方案',assembling:'正在组装分析报告',completed:'报告已经生成'}; + const suffix=progress!=null?` · 分析进度 ${progress}%`:''; + if(String(stage||'').startsWith('review'))return `正在分析评论(${String(stage).replace('review','').trim()||'处理中'})${suffix}`; + return (labels[stage]||'正在生成报告')+suffix; +} + +async function readTrackedAnalysisTasks(){ + const stored=await chrome.storage.local.get(ANALYSIS_TASKS_KEY); + return stored[ANALYSIS_TASKS_KEY]||{}; +} + +async function saveTrackedAnalysisTasks(tasks){ + await chrome.storage.local.set({[ANALYSIS_TASKS_KEY]:tasks}); + if(Object.keys(tasks).length){ + await chrome.alarms.create(ANALYSIS_ALARM,{periodInMinutes:.5}); + }else{ + await chrome.alarms.clear(ANALYSIS_ALARM); + } +} + +async function updateAnalysisBadge(state,progress){ + const text=state==='analysis_completed'?'✓':state==='analysis_failed'?'!':String(Math.max(1,Math.min(99,Math.round(progress||1)))); + const color=state==='analysis_completed'?'#138a70':state==='analysis_failed'?'#b83b4b':'#315fba'; + await chrome.action.setBadgeBackgroundColor({color}).catch(()=>{}); + await chrome.action.setBadgeText({text}).catch(()=>{}); +} + +async function showAnalysisNotification(task,state){ + const completed=state==='analysis_completed'; + await chrome.notifications.create(ANALYSIS_NOTIFICATION_PREFIX+task.taskId,{ + type:'basic', + iconUrl:chrome.runtime.getURL('icon-128.png'), + title:completed?'商品分析报告已生成':'商品分析未完成', + message:completed?'点击通知即可查看完整分析报告。':(task.errorMessage||'请打开扩展查看失败原因。'), + priority:completed?2:1 + }).catch(()=>{}); +} + +async function writeAnalysisPresentation(task,state,message,progress,extra={}){ + const effectiveProgress=progress??task.progress??10; + task.progress=effectiveProgress; + const presentation={state,message,progress:effectiveProgress,taskId:task.taskId,taskUrl:task.taskUrl,productId:task.productId||'',stage:task.stage||'',productTitle:task.productTitle||'',...extra}; + await setStatus(task.tabId,presentation); + await chrome.storage.local.set({[LAST_ANALYSIS_TASK_KEY]:{...presentation,tabId:task.tabId,updatedAt:Date.now()}}); + await updateAnalysisBadge(state,progress); +} + +async function trackProductAnalysis(tabId,submitted,productTitle='',productId=''){ + const tasks=await readTrackedAnalysisTasks(); + const task={taskId:submitted.taskId,taskUrl:submitted.taskUrl,tabId,productId:String(productId||''),productTitle,submittedAt:Date.now(),nextPollAt:0,pollErrors:0,stage:'queued'}; + tasks[task.taskId]=task; + await saveTrackedAnalysisTasks(tasks); + await writeAnalysisPresentation(task,'analyzing','任务已提交,正在排队分析',80,{analysisProgress:10}); + await pollTrackedAnalysisTasks(true); +} + +async function pollTrackedAnalysisTask(task,force=false){ + if(!force&&Number(task.nextPollAt||0)>Date.now())return {keep:true,task}; + try{ + const response=await sanshengExtensionAuth.fetch(`/v1/product-analysis/tasks/${encodeURIComponent(task.taskId)}`); + const result=await response.json().catch(()=>null); + const code=result?.Code??result?.code; + if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||`任务状态查询失败 HTTP ${response.status}`); + const data=result.Data||result.data||{}; + const status=Number(data.Status??data.status??0),progress=Number(data.Progress??data.progress??10),stage=String(data.Stage??data.stage??'queued'); + const displayProgress=Math.min(99,80+Math.round(progress*.2)); + task={...task,stage,progress:displayProgress,analysisProgress:progress,nextPollAt:Date.now()+3000,pollErrors:0}; + if(status===3){ + await writeAnalysisPresentation(task,'analysis_completed','报告已生成,点击下方按钮查看',100,{stage:'completed'}); + await showAnalysisNotification(task,'analysis_completed'); + return {keep:false,task}; + } + if(status===4){ + task.errorMessage=String(data.ErrorMessage??data.errorMessage??'分析任务执行失败'); + await writeAnalysisPresentation(task,'analysis_failed',task.errorMessage,displayProgress,{stage,errorMessage:task.errorMessage,analysisProgress:progress}); + await showAnalysisNotification(task,'analysis_failed'); + return {keep:false,task}; + } + const queuedTooLong=stage==='queued'&&Date.now()-Number(task.submittedAt||Date.now())>90000; + const message=queuedTooLong?'任务排队时间较长,请确认分析 Worker 已启动;扩展仍会继续等待':analysisStageMessage(stage,progress); + await writeAnalysisPresentation(task,'analyzing',message,displayProgress,{stage,analysisProgress:progress}); + return {keep:true,task}; + }catch(error){ + task={...task,nextPollAt:Date.now()+15000,pollErrors:Number(task.pollErrors||0)+1}; + await writeAnalysisPresentation(task,'analyzing',task.pollErrors>=3?'云端状态暂时不可用,后台稍后继续查询':'正在等待云端分析状态',undefined,{stage:task.stage||'queued'}); + return {keep:true,task}; + } +} + +async function pollTrackedAnalysisTasks(force=false){ + const tasks=await readTrackedAnalysisTasks(); + for(const [taskId,task] of Object.entries(tasks)){ + const result=await pollTrackedAnalysisTask(task,force); + if(result.keep)tasks[taskId]=result.task;else delete tasks[taskId]; + } + await saveTrackedAnalysisTasks(tasks); +} + +async function bootstrapLatestAnalysisTask(tabId){ + try{ + const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=1'); + const result=await response.json().catch(()=>null); + const code=result?.Code??result?.code; + if(!response.ok||!result||code!==0)return null; + const page=result.Data||result.data||{}; + const item=(page.List||page.list||[])[0]; + if(!item)return null; + const taskId=String(item.TaskId||item.taskId||''); + if(!taskId)return null; + const reportPath=item.ReportUrl||item.reportUrl||`/product-analysis/reports/${taskId}`; + const taskUrl=/^https?:\/\//i.test(reportPath)?reportPath:sanshengExtensionAuth.config.pcBase+reportPath; + const task={taskId,taskUrl,tabId,productId:String(item.ProductId||item.productId||''),productTitle:item.ProductTitle||item.productTitle||'',stage:item.Stage||item.stage||'queued',progress:Number(item.Progress??item.progress??10),nextPollAt:0,pollErrors:0}; + const status=Number(item.Status??item.status??0); + if(status===1||status===2){ + const tasks=await readTrackedAnalysisTasks(); + tasks[taskId]=task; + await saveTrackedAnalysisTasks(tasks); + const polled=await pollTrackedAnalysisTask(task,true); + return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY]||polled.task; + } + if(status===3){ + await writeAnalysisPresentation(task,'analysis_completed','最近一份报告已生成,点击下方按钮查看',100,{stage:'completed',analysisProgress:100}); + return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY]; + } + if(status===4){ + await writeAnalysisPresentation(task,'analysis_failed','最近一份商品分析未完成',task.progress,{stage:task.stage}); + return (await chrome.storage.local.get(LAST_ANALYSIS_TASK_KEY))[LAST_ANALYSIS_TASK_KEY]; + } + }catch(error){return null} + return null; +} + +async function refreshAnalysisForTab(tabId){ + const tasks=await readTrackedAnalysisTasks(); + const task=Object.values(tasks).find(item=>Number(item.tabId)===Number(tabId)); + if(task){ + const result=await pollTrackedAnalysisTask(task,false); + if(result.keep)tasks[task.taskId]=result.task;else delete tasks[task.taskId]; + await saveTrackedAnalysisTasks(tasks); + } + const stored=await chrome.storage.local.get(['status_'+tabId,LAST_ANALYSIS_TASK_KEY]); + const current=stored['status_'+tabId]; + const last=stored[LAST_ANALYSIS_TASK_KEY]; + if(current?.taskId)return current; + if(last?.taskId)return last; + return await bootstrapLatestAnalysisTask(tabId); +} + +chrome.alarms.onAlarm.addListener(alarm=>{if(alarm.name===ANALYSIS_ALARM)pollTrackedAnalysisTasks(false).catch(()=>{})}); +chrome.notifications.onClicked.addListener(notificationId=>{ + if(!notificationId.startsWith(ANALYSIS_NOTIFICATION_PREFIX))return; + const taskId=notificationId.slice(ANALYSIS_NOTIFICATION_PREFIX.length); + chrome.tabs.create({url:sanshengExtensionAuth.config.pcBase+`/product-analysis/reports/${encodeURIComponent(taskId)}`}); + chrome.notifications.clear(notificationId).catch(()=>{}); +}); +pollTrackedAnalysisTasks(false).catch(()=>{}); + +chrome.runtime.onMessage.addListener((m,s,send)=>{ + if(m?.type==='SANSHENG_AUTH_START'){sanshengExtensionAuth.start().then(()=>send({ok:true})).catch(e=>send({ok:false,error:String(e)}));return true;} + if(m?.type==='SANSHENG_AUTH_STATE'){sanshengExtensionAuth.get().then(async a=>{const loggedIn=!!(a.access?.token||a.refresh?.token);const health=loggedIn?await checkAnalysisService(false):{ready:false,error:''};send({ok:true,loggedIn,serviceReady:health.ready,serviceError:health.error})}).catch(e=>send({ok:false,loggedIn:false,serviceReady:false,error:String(e)}));return true;} + if(m?.type==='SANSHENG_AUTH_LOGOUT'){sanshengExtensionAuth.logout().then(async()=>{await saveTrackedAnalysisTasks({});await chrome.storage.local.remove(LAST_ANALYSIS_TASK_KEY);await chrome.action.setBadgeText({text:''}).catch(()=>{});send({ok:true})}).catch(e=>send({ok:false,error:String(e)}));return true;} + if(m?.type==='PRODUCT_ANALYSIS_STATUS'){refreshAnalysisForTab(m.tabId).then(status=>send({ok:true,status})).catch(e=>send({ok:false,error:String(e)}));return true;} + if(m?.type==='PRODUCT_ANALYSIS_HISTORY'){ + (async()=>{try{ + const response=await sanshengExtensionAuth.fetch('/v1/product-analysis/tasks?pageIndex=1&pageSize=8'); + const result=await response.json().catch(()=>null); const code=result?.Code??result?.code; + if(!response.ok||!result||code!==0)throw new Error(result?.Message||result?.message||'报告记录加载失败'); + send({ok:true,items:result.Data?.List||result.data?.list||[]}); + }catch(error){send({ok:false,error:String(error)})}})(); + return true; + } + if(m?.type==='PRODUCT_ANALYSIS_NOTICE'){ + (async()=>{try{ + await setStatus(m.tabId,{state:m.state||'analysis_blocked',message:String(m.message||'当前暂时无法提交分析'),progress:0,productId:String(m.productId||'')}); + send({ok:true}); + }catch(error){send({ok:false,error:String(error)})}})(); + return true; + } + if(m?.type==='ANALYZE_CURRENT_NOW'){ + (async()=>{ + try{ + const tabId=m.tabId; + const [{result:pageInfo}]=await chrome.scripting.executeScript({ + target:{tabId},world:'MAIN', + func:()=>({url:location.href,itemId:(location.href.match(/[?&]id=(\d+)/)||[])[1]||''}) + }); + const itemId=pageInfo?.itemId||''; + if(!itemId){send({ok:false,error:'当前页面没有识别到商品ID'});return} + let raw=null; + try{ + const got=await chrome.storage.local.get('tmall_last_raw_'+itemId); + raw=got['tmall_last_raw_'+itemId]||null; + }catch(e){} + if(raw){ + const hasFacts=!!(raw.product?.title||raw.product?.shop||raw.sales?.currentPrice||raw.sku?.length||raw.attributes?.length||raw.images?.main?.length); + if(!hasFacts)raw=null; + } + if(!raw){ + // 没有历史raw时,做一次“轻采集”:复用 collector,但立即给 stop flag, + // 只保留当前页面已经可见/已捕获的数据,不再长时间采评论。 + await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:()=>{window.__TMALL_AI_STOP_REVIEW__=true;}}); + const [{result:r}]=await chrome.scripting.executeScript({target:{tabId},world:'MAIN',func:collector,args:[true]}); + raw=r; + } + if(!raw){send({ok:false,error:'当前没有可用于分析的数据'});return} + + raw.collection=raw.collection||{}; + raw.collection.analysisMode='current_snapshot'; + raw.collection.reviewCollectionComplete=!!raw.collection.reviewCollectionComplete; + raw.collection.reviewCompleteness=raw.collection.reviewCollectionComplete?'confirmed':'partial'; + raw.collection.analysisStartedBeforeReviewComplete=!raw.collection.reviewCollectionComplete; + raw.collection.analysisStartedAt=new Date().toISOString(); + + const j=await submitProductAnalysis(raw); + await trackProductAnalysis(tabId,j,raw.product?.title||'',raw.product?.itemId||''); + send({...j,reviewCount:raw.reviews?.length||0}); + }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,m.collectionMode||'standard').then(x=>send(x)).catch(e=>send({ok:false,error:String(e)})); return true; } +}); + +chrome.tabs.onUpdated.addListener((tabId,changeInfo)=>{ + const url=changeInfo.url; + if(url&&isProduct(url))clearTabStatus(tabId).catch(()=>{}); + if(!url||!url.startsWith(sanshengExtensionAuth.config.pcBase+sanshengExtensionAuth.config.callbackPath))return; + const parsed=new URL(url); + if(!parsed.searchParams.get('code'))return; + sanshengExtensionAuth.exchange(url).then(async ok=>{ + if(!ok)return; + const pending=(await chrome.storage.local.get('sansheng_extension_auth_last_context')).sansheng_extension_auth_last_context; + await chrome.tabs.remove(tabId).catch(()=>{}); + if(pending?.tabId){ + await chrome.tabs.update(pending.tabId,{active:true}).catch(()=>{}); + if(pending.windowId)await chrome.windows.update(pending.windowId,{focused:true}).catch(()=>{}); + await setStatus(pending.tabId,{state:'idle',message:'登录成功,请在当前商品页打开扩展开始采集。',progress:0}); + await chrome.action.openPopup({windowId:pending.windowId}).catch(()=>{}); + } + }).catch(error=>console.error('[三笙扩展登录]',error)); +}); + +async function run(tabId,manual,collectionMode='standard'){ + const tab=await chrome.tabs.get(tabId); + if(!isProduct(tab.url)) throw new Error('当前标签页不是天猫/淘宝商品详情页'); + const auth=await sanshengExtensionAuth.get(); + if(!auth.access?.token&&!auth.refresh?.token){await setStatus(tabId,{state:'login_required',message:'请先登录三笙账号'});throw new Error('请先登录三笙账号')} + const [{result:pageState}]=await chrome.scripting.executeScript({ + target:{tabId},world:'MAIN', + func:async()=>{ + if(document.readyState==='loading') await new Promise(resolve=>document.addEventListener('DOMContentLoaded',resolve,{once:true})); + for(let i=0;i<8&&(document.body?.innerText||'').trim().length<120;i++) await new Promise(r=>setTimeout(r,350)); + const body=(document.body?.innerText||'').replace(/\s+/g,' ').slice(0,5000); + const login=/login\.taobao\.com|login\.tmall\.com/i.test(location.hostname)||(/密码登录|短信登录|手机扫码登录/.test(body)&&!/商品评价|宝贝详情/.test(body)); + const verification=/验证码|滑块验证|安全验证|访问过于频繁|操作频繁/.test(body); + return {url:location.href,login,verification,bodyLength:(document.body?.innerText||'').length}; + } + }); + 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 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}); + 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)); + const raw=collectorResult?.[0]?.result; + if(!raw?.product?.itemId) throw new Error('未识别到 itemId,请确认商品页已正常打开并刷新后重试'); + const hasProductFacts=!!(raw.product.title||raw.product.shop||raw.sales?.currentPrice||raw.sku?.length||raw.attributes?.length||raw.images?.main?.length); + if(!hasProductFacts){ + await setStatus(tabId,{state:'product_empty',message:'已识别商品ID,但页面商品信息为空。请确认已登录、详情已加载后刷新重试。',progress:0}); + throw new Error('商品字段为空:请确认淘宝已登录且商品详情加载完成,然后刷新页面重试'); + } + try{ + await chrome.storage.local.set({['tmall_last_raw_'+(raw?.product?.itemId||'unknown')]:raw}); + try{ + const p=(raw?.product?.parameterCollection)||{}; + await chrome.storage.local.set({['tmall_param_status_'+(raw?.product?.itemId||'unknown')]:p}); + }catch(e){} + + }catch(e){} + + await chrome.storage.local.set({['last_raw_'+raw.product.itemId]:raw}); + if(!raw.collection?.reviewCollectionComplete){ + const reason=raw.collection?.reviewStopReason||'unknown'; + const human=reason==='human_verification_required';const stopped=reason==='user_stopped'; + 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}); + 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}); + const j=await submitProductAnalysis(raw); + await setStatus(tabId,{state:'analyzing',message:'正在分析全方位产品体验并生成解决方案',progress:82,taskId:j.taskId,taskUrl:j.taskUrl}); + await trackProductAnalysis(tabId,j,raw.product?.title||'',raw.product?.itemId||''); + 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'){ + 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(); + const uniq=a=>[...new Set((a||[]).filter(Boolean))]; + let queryRoots=[]; let queryRootsAt=0; + const refreshQueryRoots=()=>{ + if(Date.now()-queryRootsAt<1200&&queryRoots.length)return; + const roots=[document]; + for(let i=0;i{refreshQueryRoots();const out=[];for(const root of queryRoots){try{out.push(...root.querySelectorAll(s))}catch(e){}}return [...new Set(out)]}; + const assetUrl=u=>{u=String(u||'').trim();if(u.startsWith('//'))u='https:'+u;return u.replace(/&/g,'&')}; + const srcOf=img=>assetUrl(img?.currentSrc||img?.src||img?.getAttribute?.('data-src')||img?.getAttribute?.('data-ks-lazyload')||img?.getAttribute?.('data-lazyload-src')||''); + const url=new URL(location.href); const itemId=url.searchParams.get('id')||location.href.match(/[?&]id=(\d+)/)?.[1]||''; const skuId=url.searchParams.get('skuId')||''; + const bodyClone=document.body?.cloneNode(true); + try{bodyClone?.querySelectorAll?.('[class*=recommend],[class*=Recommend],[class*=guess],[class*=Guess],[class*=related],[class*=Related],[class*=similar],[class*=Similar],[class*=猜你喜欢],[id*=recommend],[id*=guess]').forEach(n=>n.remove())}catch(e){} + const bodyText=clean(bodyClone?.innerText||document.body?.innerText||''); + const captured=[]; + const reviews=[]; const questions=[]; + const reviewCap=collectionMode==='quick'?300:(collectionMode==='full'?1500:800); + const reviewNetMeta={totals:[],hasMoreFalse:false,lastPageFlags:[],pageHints:[],networkReviewObjects:0,networkResponses:0}; + const reviewStartedAt=Date.now(); + const processedCaptureKeys=new Set(); + function dedupReviewCount(){const m=new Set();for(const r of reviews){const k=clean(r.content||'').replace(/\s+/g,'').slice(0,320);if(k)m.add(k)}return m.size} + function publishReviewProgress(round=0,stagnant=0,mode='DOM',extra={}){const seen=new Set(),preview=[];for(let i=reviews.length-1;i>=0&&preview.length<3;i--){const c=clean(reviews[i]?.content||'');const k=c.replace(/\s+/g,'').slice(0,320);if(k&&!seen.has(k)){seen.add(k);preview.push(c.slice(0,70)+(c.length>70?'…':''))}}window.__TMALL_AI_REVIEW_PROGRESS__={ + count:dedupReviewCount(), + rawCount:reviews.length, + networkReviewObjects:reviewNetMeta.networkReviewObjects, + networkResponses:reviewNetMeta.networkResponses, + round,stagnant,mode, + elapsedMs:Date.now()-reviewStartedAt, + preview, + capturePolicy:'passive_code_and_network', + extraRequestsCreated:0, + safety:'no_api_replay_no_auth_bypass_no_rate_limit_bypass', + ...extra + }} + + const looksQuestion=(c)=>{c=clean(c);return /问大家|更多回答|查看全部问答|大家问|^问[::\s]|请问|请教|会不会|有没有|能不能|是否|怎么|如何|多少|适合|兼容|保质期|[??]$/.test(c)}; + const addQuestion=(o,source='unknown')=>{ const q=clean(o?.question||o?.title||o?.ask||o?.content||o?.text||''); const a=clean(o?.answer||o?.reply||o?.answers?.[0]?.content||''); if(q.length>2)questions.push({question:q,answer:a,source}); }; + const reviewImageUrl=v=>assetUrl(typeof v==='string'?v:(v?.url||v?.picUrl||v?.imageUrl||v?.src||v?.bigUrl||v?.thumbnail||'')); + const validReviewImage=u=>/^https?:\/\//.test(u)&&!/(?:avatar|sns_logo|credit|logo|icon|defaultRate|sprite|\/tps\/|\/tfs\/TB1[^/]+-\d{1,4}-\d{1,4}\.png)/i.test(u); + const addReview=(o,source='unknown')=>{ if(!o)return; const c=clean(o.content||o.rateContent||o.feedback||o.comment||o.text||o.contentText||''); if(c.length<3)return; if(looksQuestion(c)){addQuestion({question:c},source);return;} const media=uniq((o.images||o.pics||o.photos||[]).map(reviewImageUrl)).filter(validReviewImage).slice(0,24);reviews.push({content:c,appendContent:clean(o.appendContent||o.append||o.appendComment||''),rating:o.rating||o.rate||o.star||o.score||o.starLevel||'',sku:clean(o.sku||o.skuInfo||o.auctionSku||o.skuMap||''),date:clean(o.date||o.time||o.gmtCreate||o.rateDate||''),images:media,likes:o.likes||o.likeCount||0,source});}; + function scanPageMeta(obj){ + if(!obj||typeof obj!=='object'||Array.isArray(obj))return; + for(const [k,v] of Object.entries(obj)){ + const key=String(k).toLowerCase(); + if(/^(total|totalcount|total_count|reviewcount|ratecount|commentcount|totalnum|totalnumber)$/.test(key)){ + const n=Number(String(v).replace(/,/g,'')); if(Number.isFinite(n)&&n>=0&&n<10000000)reviewNetMeta.totals.push(n); + } + if(/^(hasmore|has_more|hasnext|has_next|hasnextpage|has_next_page|more)$/.test(key) && (v===false||v===0||v==='false'||v==='0')) reviewNetMeta.hasMoreFalse=true; + if(/^(lastpage|islast|is_last|islastpage|is_last_page|last|end|isend|is_end)$/.test(key) && (v===true||v===1||v==='true'||v==='1')) reviewNetMeta.lastPageFlags.push(true); + if(/^(page|pageno|page_no|pagenum|page_num|currentpage|current_page)$/.test(key)){ + const n=Number(v); if(Number.isFinite(n)&&n>0)reviewNetMeta.pageHints.push(n); + } + } + } + function walk(obj,depth=0,sourceUrl=''){ + if(!obj||depth>10)return; + if(Array.isArray(obj)){for(const x of obj.slice(0,2500))walk(x,depth+1,sourceUrl);return;} + if(typeof obj!=='object')return; + scanPageMeta(obj); + const ks=Object.keys(obj); const qaUrl=/ask|question|wdj|qa/i.test(sourceUrl||''); + const hasQ=ks.some(k=>/question|ask/i.test(k)); const hasA=ks.some(k=>/answer|reply/i.test(k)); + if(qaUrl || (hasQ&&hasA)) addQuestion(obj,sourceUrl||'network'); + else if(ks.some(k=>/rateContent|feedback|comment|contentText|reviewContent|rateText/i.test(k)) || ((('content'in obj)||('text'in obj))&&(/review|rate|comment|feedback|evaluate|评价|评论/i.test(sourceUrl||'') || ks.some(k=>/star|rating|sku|rate|append|user|buyer|date|time|pic|photo/i.test(k))))){ + const before=reviews.length; addReview(obj,sourceUrl||'network'); if(reviews.length>before)reviewNetMeta.networkReviewObjects++; + } + for(const k of ks){ const v=obj[k]; if(v&&typeof v==='object')walk(v,depth+1,sourceUrl); } + } + // V8.1.5:评论网络监听由 page-review-hook.js 在 document_start 提前安装。 + // 这里禁止再次重置 __TMALL_AI_CAPTURE__,否则会丢失用户打开商品页后已经收到的评论响应。 + // 如果极端情况下 MAIN-world hook 未安装,只记录状态并继续使用 DOM / inline JSON 兜底,不主动重放接口。 + if(!Array.isArray(window.__TMALL_AI_CAPTURE__)) window.__TMALL_AI_CAPTURE__=[]; + if(!Array.isArray(window.__TMALL_AI_SOURCE_SNAPSHOTS__)) window.__TMALL_AI_SOURCE_SNAPSHOTS__=[]; + + const visible=(el)=>{try{const r=el.getBoundingClientRect?.();return !r||(r.width>0&&r.height>0)}catch(e){return true}}; + const textControl=(patterns)=>{ + const matches=$all('button,a,[role=tab],[role=button],li,div,span').filter(el=>{ + const t=clean(text(el)); + return visible(el)&&t&&t.length<=36&&patterns.some(pattern=>pattern.test(t)); + }); + return matches.sort((a,b)=>{ + const ai=/^(BUTTON|A)$/.test(a.tagName)||a.getAttribute('role')==='tab'||a.getAttribute('role')==='button'; + const bi=/^(BUTTON|A)$/.test(b.tagName)||b.getAttribute('role')==='tab'||b.getAttribute('role')==='button'; + return Number(bi)-Number(ai)||clean(text(a)).length-clean(text(b)).length; + })[0]; + }; + const activatePanel=async(patterns)=>{ + const control=textControl(patterns);if(!control)return false; + try{control.scrollIntoView({block:'center'});await delay(220);control.click();await delay(1000);queryRootsAt=0;return true}catch(e){return false} + }; + const reviewDrawerOpen=()=>{ + const scrollBox=$all('[class*=comments],[class*=Comments],[class*=commentList],[class*=CommentList]').find(el=>{ + try{const style=getComputedStyle(el);return visible(el)&&/(auto|scroll)/.test(style.overflowY)&&el.clientHeight>180} + catch(e){return false} + }); + const drawer=$all('[class*=Drawer],[class*=drawer]').find(el=>visible(el)&&/用户评价/.test(clean(text(el)))); + return Boolean(scrollBox&&drawer); + }; + const clickControl=async(el,waitMs=900)=>{ + if(!el)return false; + try{ + el.scrollIntoView({block:'center',inline:'nearest'});await delay(220); + el.click();await delay(waitMs);queryRootsAt=0;return true; + }catch(e){return false} + }; + const openReviewDrawer=async()=>{ + if(reviewDrawerOpen())return true; + for(let attempt=0;attempt<4;attempt++){ + queryRootsAt=0; + const showAll=$all('button,a,[role=button],div,span').filter(el=>{ + const t=clean(text(el));return visible(el)&&/^(查看全部评价|查看全部评论|全部评价|全部评论)$/.test(t); + }).sort((a,b)=>{ + const score=el=>/ShowButton/i.test(String(el.className||''))?4:(/^(BUTTON|A)$/.test(el.tagName)||el.getAttribute('role')==='button'?3:/footer/i.test(String(el.className||''))?1:2); + return score(b)-score(a)||clean(text(b)).length-clean(text(a)).length; + })[0]; + if(showAll){publishReviewProgress(pagesVisited,0,'打开全部评价');await clickControl(showAll,1200);if(reviewDrawerOpen())return true;} + const reviewTab=$all('button,a,[role=tab],[role=button],div,span').filter(el=>visible(el)&&clean(text(el))==='用户评价').sort((a,b)=>{ + const ac=/tabTitleItem/i.test(String(a.className||''));const bc=/tabTitleItem/i.test(String(b.className||''));return Number(bc)-Number(ac); + })[0]; + if(reviewTab){publishReviewProgress(pagesVisited,0,'定位用户评价');await clickControl(/tabTitle/i.test(String(reviewTab.parentElement?.className||''))?reviewTab.parentElement:reviewTab,700);} + else{window.scrollBy(0,700);await delay(500)} + } + return reviewDrawerOpen(); + }; + // V5: Collect all currently accessible effective reviews. Resume from page-local checkpoint when possible. + const checkpointKey='__TMALL_AI_REVIEWS_V62__'+itemId; + async function idbOpen(){return await new Promise((resolve,reject)=>{try{const req=indexedDB.open('TMALL_AI_V62',1);req.onupgradeneeded=()=>{const db=req.result;if(!db.objectStoreNames.contains('checkpoints'))db.createObjectStore('checkpoints')};req.onsuccess=()=>resolve(req.result);req.onerror=()=>reject(req.error)}catch(e){reject(e)}})} + async function loadCheckpoint(){try{const db=await idbOpen();return await new Promise((resolve,reject)=>{const tx=db.transaction('checkpoints','readonly');const req=tx.objectStore('checkpoints').get(checkpointKey);req.onsuccess=()=>resolve(req.result||{});req.onerror=()=>reject(req.error)})}catch(e){try{return JSON.parse(localStorage.getItem(checkpointKey)||'{}')||{}}catch(_){return {}}}} + async function saveCheckpoint(data){try{const db=await idbOpen();await new Promise((resolve,reject)=>{const tx=db.transaction('checkpoints','readwrite');tx.objectStore('checkpoints').put(data,checkpointKey);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)})}catch(e){try{localStorage.setItem(checkpointKey,JSON.stringify({...data,reviews:(data.reviews||[]).slice(-1500),questions:(data.questions||[]).slice(-500)}))}catch(_){}}} + let checkpoint=await loadCheckpoint(); checkpoint=checkpoint||{}; + for(const r of (checkpoint.reviews||[])) addReview(r); for(const q of (checkpoint.questions||[])) addQuestion(q,q.source||'checkpoint'); + let collectionComplete=false, stopReason='unknown', pagesVisited=Number(checkpoint.round||0), explicitEnd=false; + const domReviewSeen=new Set(); + function collectVisibleReviews(){ + const selectors=[ + '[class^="Comment--"]','[class^="comment--"]','[class*=rate-item]','[class*=rateItem]','[class*=review-item]','[class*=reviewItem]', + '[class*=comment-item]','[class*=commentItem]','[class*=feedback-item]','[class*=feedbackItem]', + '[data-testid*=review-card]','[data-testid=review]','[class*=ReviewItem]','[class*=CommentItem]','[class*=RateItem]','[class*=reviewCard]','[class*=commentCard]','[class*=rateCard]' + ]; + const rawNodes=uniq(selectors.flatMap(s=>{try{return $all(s)}catch(e){return[]}})); + const genericNodes=$all('article,[role=article],li,section,div').filter(el=>{ + const raw=String(el.innerText||'').replace(/\u00a0/g,' ').trim(); + return visible(el)&&raw.length>=10&&raw.length<=1500&&/20\d{2}[-/.年]\s*\d{1,2}[-/.月]\s*\d{1,2}/.test(raw)&&/[。!?!?,,]/.test(raw); + }); + const genericAtomic=genericNodes.filter(n=>![...n.children].some(child=>genericNodes.includes(child)&&String(child.innerText||'').trim().length>=10)); + const nodes=uniq([...rawNodes,...genericAtomic]).filter(n=>!rawNodes.some(o=>o!==n&&o.contains?.(n))&&!genericAtomic.some(o=>o!==n&&o.contains?.(n))); // 只保留独立评论卡,避免父子容器重复/合并 + for(const b of nodes){ + const contentNode=b.querySelector?.('[class^="content--"],[class^="Content--"],[class*=comment-content],[class*=review-content],[class*=rate-content],[data-testid*=content]'); + const c=clean(text(contentNode)||text(b)); + if(c.length<8||c.length>1800) continue; + const k=c.replace(/\s+/g,'').slice(0,240); + if(domReviewSeen.has(k)) continue; + domReviewSeen.add(k); + const ratingText=clean(b.querySelector?.('[class*=star],[class*=score],[class*=rating]')?.getAttribute?.('aria-label')||text(b.querySelector?.('[class*=star],[class*=score],[class*=rating]'))); + const rm=ratingText.match(/([1-5](?:\.\d)?)/); + const sku=clean(text(b.querySelector?.('[class*=sku],[class*=spec],[class*=auction]'))); + const metaText=clean(text(b.querySelector?.('time,[class*=date],[class*=time],[class^="meta--"]'))); + const date=metaText.match(/20\d{2}[^\d]?\d{1,2}[^\d]?\d{1,2}/)?.[0]||metaText; + const purchased=metaText.match(/已购[::]?\s*(.+)$/)?.[1]||''; + const appendContent=clean((clean(text(b)).match(/(?:追评|追加评价)[::]\s*(.{2,1200})/)||[])[1]||''); + const images=uniq([...b.querySelectorAll?.('img')||[]].map(srcOf)).filter(validReviewImage).slice(0,16); + addReview({content:c,appendContent,rating:rm?.[1]||'',sku:purchased||sku,date,images},'dom_visible'); + } + } + function scrollReviewContainers(){ + let moved=false; + const boxes=$all('[class*=comments],[class*=Comments],[class*=commentList],[class*=CommentList],[class*=drawer],[class*=Drawer]').filter(el=>{ + try{return visible(el)&&el.scrollHeight>el.clientHeight+8} + catch(e){return false} + }); + for(const box of boxes){ + try{ + const old=box.scrollTop; const step=Math.max(420,Math.floor(box.clientHeight*.82)); + box.scrollTop=Math.min(box.scrollHeight-box.clientHeight,old+step); + if(box.scrollTop>old+2)moved=true; + box.dispatchEvent(new Event('scroll',{bubbles:true})); + }catch(e){} + } + return moved; + } + function collectVisibleQuestions(){ + const selectors=['[class*=question]','[class*=Question]','[class*=ask]','[class*=Ask]','[class*=qa]','[class*=Qa]']; + const nodes=uniq(selectors.flatMap(sel=>{try{return $all(sel)}catch(e){return[]}})); + for(const b of nodes){const c=clean(text(b));if(c.length>=4&&c.length<=1800&&(/问大家|更多回答|查看全部问答|请问|请教|会不会|有没有|能不能|是否|怎么|如何|多少|适合|兼容|保质期|[??]/.test(c)))addQuestion({question:c},'dom_question');} + } + // Trigger review/question areas if clickable text exists. + // 先按用户通常的顺序打开详情/规格区域;新版页面常把参数放在折叠面板内。 + await activatePanel([/^(?:宝贝)?详情$/,/商品详情/,/规格参数|商品参数|产品参数/]); + // 必须确认“查看全部评价”抽屉已打开;只点“用户评价”页签不算成功。 + const reviewDrawerWasOpened=await openReviewDrawer(); + // V8.0.4:实时、可终止、快速停滞检测。优先分页/加载更多,不再长时间整页空转。 + let stagnant=0;const MAX_ROUNDS=40;publishReviewProgress(pagesVisited,0,reviewDrawerWasOpened?'评论抽屉已打开':'读取页面可见评论',{reviewDrawerOpened:reviewDrawerWasOpened}); + for(let attempt=0;attempt{const t=clean(text(el));const dis=el.getAttribute?.('disabled')!=null||el.getAttribute?.('aria-disabled')==='true'||/disabled|next-disabled/.test(String(el.className||''));const r=el.getBoundingClientRect?.();return !dis&&t&&t.length<20&&/下一页|下页|查看更多|加载更多|更多评价|展开更多|下一批/.test(t)&&(!r||(r.width>0&&r.height>0))}); + const disabledNext=navNodes.find(el=>{const t=clean(text(el));const dis=el.getAttribute?.('disabled')!=null||el.getAttribute?.('aria-disabled')==='true'||/disabled|next-disabled/.test(String(el.className||''));return dis&&/下一页|下页/.test(t)}); + if(next){try{next.scrollIntoView({block:'center'});await delay(180);next.click();publishReviewProgress(round,stagnant,'加载下一批');await delay(1100+Math.floor(Math.random()*500))}catch(e){}} + else{const rev=document.querySelector('[class*=review],[class*=rate],[class*=comment],[data-testid*=review]');try{rev?.scrollIntoView({block:'center'})}catch(e){}for(let i=0;i<4;i++){if(window.__TMALL_AI_STOP_REVIEW__)break;window.scrollBy(0,650);scrollReviewContainers();await delay(240);collectVisibleReviews();collectVisibleQuestions()}await delay(550)} + if(window.__TMALL_AI_STOP_REVIEW__){stopReason='user_stopped';publishReviewProgress(round,stagnant,'用户终止');break} + // 每轮即时解析已捕获网络响应 + 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} + // 连续4轮没有新增才停止;给异步评论接口和虚拟列表更多响应时间,但仍避免长时间空转。 + if(stagnant>=4){collectionComplete=false;explicitEnd=false;stopReason='no_progress_after_4_rounds';publishReviewProgress(round+1,stagnant,'无新增,停止空转');break} + await saveCheckpoint({reviews,questions,round:round+1,updatedAt:Date.now()}); + } + await delay(300); + // V8.1.5 安全模式: + // 只消费页面在正常浏览过程中已经产生的真实 fetch/XHR 响应。 + // 不根据 performance resource URL 主动重放请求,避免额外接口调用、重复页和频控风险。 + captured.push(...(window.__TMALL_AI_CAPTURE__||[]).slice(-100)); + + // 同时读取页面已经内嵌的脚本/JSON快照,不产生任何网络请求。 + const sourceSnapshots=(window.__TMALL_AI_SOURCE_SNAPSHOTS__||[]).slice(-12); + for(const snap of sourceSnapshots){ + captured.push({ + url:'page-source://inline-script', + method:'SOURCE', + status:200, + text:String(snap?.text||'').slice(0,1200000), + passive:true + }); + } + for(const c of captured){ + 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 s=c.text||''; s=s.replace(/^\s*[\w$.]+\(/,'').replace(/\)\s*;?\s*$/,''); + try{walk(JSON.parse(s),0,c.url||'')}catch(e){ const m=s.match(/\{[\s\S]*\}/);if(m)try{walk(JSON.parse(m[0]),0,c.url||'')}catch(_){}} + } + // DOM fallback:仍按“单个评论卡”解析,不读取整块评价容器,避免把多条买家评论合并。 + collectVisibleReviews(); collectVisibleQuestions(); + // 评论采集完成后再切换问大家,读取页面正常加载出来的问答,不额外调用接口。 + const qaNow=$all('a,button,div,span,[role=tab]').find(el=>{const t=clean(text(el));return t&&t.length<32&&/问大家|大家问/.test(t)}); + if(qaNow){ + try{ + qaNow.scrollIntoView({block:'center'});await delay(220);qaNow.click();await delay(1000); + for(let i=0;i<3;i++){collectVisibleQuestions();window.scrollBy(0,420);await delay(220)} + collectVisibleQuestions(); + }catch(e){} + } + // 评论完成后重新打开详情/规格面板,避免问大家面板覆盖参数区域。 + await activatePanel([/^(?:宝贝)?详情$/,/商品详情/,/规格参数|商品参数|产品参数/]); + // 像正常浏览一样有限滚动详情容器,触发页面自身的图片懒加载;不创建额外接口请求。 + const detailRootSelectors=['#J_DivItemDesc','#description','[id="description"]','[class*="detailContent"]','[class*="DetailContent"]','[class*="detail-content"]','[class*="descV8"]','[class^="desc--"]','[data-testid*="product-description"]','[class*="ProductDetail"]','[class*="productDetail"]','[class*="detailDesc"]','[class*="DetailDesc"]','[class*="moduleDesc"]','[class*="ModuleDesc"]','[class*="contentDetail"]','[class*="ContentDetail"]','[class*="aplus"]','[class*="Aplus"]']; + const detailRoots=uniq(detailRootSelectors.flatMap(sel=>{try{return $all(sel)}catch(e){return[]}})); + const detailScanUrls=[]; + const scanRootImages=root=>{try{for(const img of root.querySelectorAll('img')){const u=srcOf(img);if(u&&!detailScanUrls.includes(u))detailScanUrls.push(u)}}catch(e){}}; + for(const root of detailRoots.slice(0,4)){ + try{ + const box=root.getBoundingClientRect();const top=Math.max(0,window.scrollY+box.top-120);const height=Math.max(box.height,root.scrollHeight||0);const rounds=Math.min(14,Math.max(1,Math.ceil(height/700))); + for(let i=0;in.remove())}catch(e){} + const liveBodyRaw=freshClone?.innerText||document.body?.innerText||''; + const liveBodyText=clean(liveBodyRaw); + const firstText=(selectors)=>{for(const sel of selectors){try{const v=clean(text(document.querySelector(sel)));if(v)return v}catch(e){}}return''}; + const meta=(selectors)=>{for(const sel of selectors){try{const v=clean(document.querySelector(sel)?.getAttribute?.('content')||'');if(v)return v}catch(e){}}return''}; + const scalar=v=>(typeof v==='string'||typeof v==='number')?clean(String(v)):''; + const structured={titles:[],shops:[],brands:[],categories:[],prices:[],sold:[],sku:[],attrs:[],promos:[],images:[],imageRecords:[]}; + const addSignal=(group,value,max=300)=>{value=scalar(value);if(value&&value.length<=max&&!structured[group].includes(value))structured[group].push(value)}; + const roots=[]; + for(const script of $all('script[type="application/ld+json"],script#__NEXT_DATA__,script[type="application/json"]')){ + const raw=String(script.textContent||'').trim();if(!raw||raw.length>6000000)continue; + try{roots.push(JSON.parse(raw))}catch(e){} + } + for(const key of ['__INITIAL_STATE__','__INIT_DATA__','__DATA__','__SSR_DATA__','__APOLLO_STATE__']){ + try{const value=window[key];if(value&&typeof value==='object')roots.push(value)}catch(e){} + } + const seenStructured=new WeakSet();let structuredNodes=0; + function walkProduct(obj,path='',depth=0){ + if(!obj||typeof obj!=='object'||depth>11||structuredNodes>30000||seenStructured.has(obj))return; + seenStructured.add(obj);structuredNodes++; + if(Array.isArray(obj)){for(const x of obj.slice(0,2000))walkProduct(x,path,depth+1);return} + const objName=scalar(obj.attrName||obj.propertyName||obj.propName||obj.name||obj.label||obj.key); + const objValue=scalar(obj.attrValue||obj.propertyValue||obj.valueName||obj.value||obj.text); + if(objName&&objValue&&/(?:attr|param|property|prop|feature|规格|参数|属性)/i.test(path))structured.attrs.push({name:objName,value:objValue}); + for(const [key,value] of Object.entries(obj)){ + const lower=String(key).toLowerCase(),next=path?path+'.'+lower:lower; + if(value&&typeof value==='object'){walkProduct(value,next,depth+1);continue} + const valueText=scalar(value);if(!valueText)continue; + if(/^(title|itemtitle|item_title|auctiontitle|producttitle|subject|name)$/.test(lower)&&/(?:item|product|auction|detail|schema|offers?|^$)/i.test(path))addSignal('titles',valueText,260); + if(/^(shopname|shop_name|shoptitle|sellername|seller_name|storename|store_name|nick)$/.test(lower))addSignal('shops',valueText,160); + if(/^(brand|brandname|brand_name)$/.test(lower))addSignal('brands',valueText,120); + if(/^(category|categoryname|category_name|catname)$/.test(lower))addSignal('categories',valueText,160); + if(/^(price|pricetext|price_text|promotionprice|promotion_price|saleprice|sale_price|pricevalue)$/.test(lower))addSignal('prices',valueText,80); + if(/^(sold|soldcount|sold_count|sales|salescount|sales_count|monthsold|monthlysales)$/.test(lower))addSignal('sold',valueText,80); + if(/(?:sku|property|prop|spec)/i.test(path)&&/^(name|text|title|value|valuename|value_name)$/.test(lower))addSignal('sku',valueText,100); + if(/(?:promo|promotion|activity|coupon|discount|benefit|service)/i.test(next))addSignal('promos',valueText,220); + if(/(?:image|img|pic|picture|thumb)/i.test(lower)&&/^https?:\/\/|^\/\//i.test(valueText)){ + const imageUrl=assetUrl(valueText);addSignal('images',imageUrl,1200); + if(imageUrl&&!structured.imageRecords.some(x=>x.url===imageUrl&&x.path===next))structured.imageRecords.push({url:imageUrl,path:next}); + } + } + } + for(const root of roots)walkProduct(root); + const relevantScripts=$all('script').map(s=>String(s.textContent||'')).filter(s=>s.length>20&&s.length<=6000000&&(s.includes(itemId)||/itemTitle|auctionTitle|shopName|skuBase|priceInfo|promotion/i.test(s))).slice(0,16); + const scriptCorpus=relevantScripts.join('\n').slice(0,10000000); + const scriptString=(keys,max=300)=>{ + for(const key of keys){ + const escaped=key.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); + const m=scriptCorpus.match(new RegExp('["\\\']'+escaped+'["\\\']\\s*:\\s*"((?:\\\\.|[^"\\\\]){1,'+max+'})"','i')); + if(m){try{return clean(JSON.parse('"'+m[1]+'"'))}catch(e){return clean(m[1].replace(/\\u([0-9a-f]{4})/gi,(_,h)=>String.fromCharCode(parseInt(h,16))))}} + } + return''; + }; + const validProductTitle=value=>{ + value=clean(value).replace(/[-_–—|]\s*(淘宝网|天猫|淘宝).*$/,''); + if(value.length<4||value.length>260)return''; + if(/用户评价|累计评价|商品评价|问大家|参数信息|规格参数|图文详情|商品详情|登录|验证码/.test(value))return''; + return value; + }; + const titleCandidates=[ + meta(['meta[property="og:title"]','meta[name="twitter:title"]']), + ...structured.titles, + scriptString(['itemTitle','auctionTitle','productTitle']), + firstText(['[data-testid="item-title"]','[data-testid*=item-title]','h1[class*="ItemTitle"]','h1[class*="itemTitle"]','[class*="ItemTitle--"]','[class*="itemTitle--"]','h1']), + clean(document.title) + ]; + const title=titleCandidates.map(validProductTitle).find(Boolean)||''; + const visiblePriceTexts=$all('[class*=price],[class*=Price],[data-testid*=price]').filter(el=>{const r=el.getBoundingClientRect?.();return !r||(r.width>0&&r.height>0&&r.top<1600)}).map(text).map(clean).filter(x=>/\d/.test(x)).slice(0,30); + const metaPrice=meta(['meta[property="product:price:amount"]','meta[property="og:price:amount"]','meta[itemprop="price"]']); + const visiblePrice=(visiblePriceTexts.join(' ')+' '+liveBodyText.slice(0,6000)).match(/(?:到手价|券后价|活动价|促销价|价格)?\s*[¥¥]\s*(\d{1,7}(?:\.\d{1,2})?)/)?.[1]||(liveBodyText.slice(0,6000).match(/(?:到手价|券后价|活动价|促销价|价格)\s*[::]?\s*(\d{1,7}(?:\.\d{1,2})?)/)||[])[1]||''; + const fallbackPrice=structured.prices[0]||scriptString(['promotionPrice','salePrice','priceText','price'],80); + const priceValue=(String(metaPrice).match(/\d{1,7}(?:\.\d{1,2})?/)||[])[0]||visiblePrice||(String(fallbackPrice).match(/\d{1,7}(?:\.\d{1,2})?/)||[])[0]||''; + const soldMatch=liveBodyText.match(/(?:已售|销量|月销|付款|成交)\s*([\d,.万+]+)/); + const soldValue=soldMatch?.[1]||structured.sold[0]||scriptString(['soldCount','salesCount','monthSold','monthlySales'],80); + const rankMatch=liveBodyText.match(/(?:热销榜|榜单|排名)[^\d]{0,8}(?:第)?\s*(\d+)\s*名?/); + const shop=firstText(['[data-testid*=shop]','[class*=shopName]','[class*=shop-name]','[class*=ShopName]','[class*=sellerName]','[class*=SellerName]','[class*=storeName]','[class*=StoreName]'])||meta(['meta[name="seller"]'])||structured.shops[0]||scriptString(['shopName','shopTitle','sellerName','storeName'],160); + const domSkuTexts=$all('[class*=sku] button,[class*=sku] li,[class*=sku] label,[class*=sku] [role=button],[class*=Sku] button,[class*=Sku] li,[class*=prop] button,[class*=Prop] button,[data-testid*=sku] button').map(text).map(clean); + const skuTexts=uniq([...domSkuTexts,...structured.sku]).filter(x=>x&&x.length<100&&!/加入购物车|立即购买|领券购买|购买|收藏|客服|分享|确定|取消|数量/.test(x)).slice(0,160); + // 参数必采:同时读取属性列表、规格参数、表格、dt/dd 等常见结构;只保留短文本,避免把整块详情正文误当参数。 + const paramPairs=[]; + const validParam=(name,value)=>{ + if(!name||!value||name.length>60||value.length>220)return false; + if(/^\d+$/.test(name)||/(?:window\.|console\.|function\b|frontend_data|performance\.|[{}\[\]"'=;<>])/i.test(name))return false; + if(/(?:console\.log|window\.|performance\.now|frontend_data)/i.test(value))return false; + return /[\u4e00-\u9fffA-Za-z]/.test(name); + }; + const pushParam=(name,value)=>{name=clean(name);value=clean(value);if(!validParam(name,value))return;const k=name+'\u0000'+value;if(!paramPairs.some(x=>x._k===k))paramPairs.push({name,value,_k:k});}; + const parseParamText=(s)=>{s=clean(s);if(!s||s.length>260)return;const m=s.match(/^([^::]{1,60})[::]\s*(.{1,220})$/);if(m)pushParam(m[1],m[2]);}; + for(const el of $all('[class*=attr] li,[class*=Attr] li,[class*=parameter] li,[class*=Parameter] li,[class*=prop] li,[class*=Prop] li,[class*=specification] li,[class*=Specification] li,[class*=attribute] li,[class*=Attribute] li')) parseParamText(text(el)); + for(const dl of $all('dl')){const dts=[...dl.querySelectorAll('dt')],dds=[...dl.querySelectorAll('dd')];for(let i=0;i=2)pushParam(cells[0],cells.slice(1).join(' '));} + // 兜底读取页面中常见“键:值”短行。 + const paramTextNodes=$all('[class*=attr],[class*=parameter],[class*=specification],[class*=attribute]').slice(0,80); + for(const el of paramTextNodes){for(const line of String(text(el)||'').split(/\n+/).slice(0,80))parseParamText(line);} + // 淘宝新版经常移除参数容器 class,但可见页面仍保留“字段:值”短行。 + for(const line of String(liveBodyRaw||'').split(/\n+/).slice(0,5000))parseParamText(line); + for(const el of $all('li,dt,dd,tr,td,th,[role=listitem]').slice(0,3000)){ + for(const line of String(el.innerText||'').split(/\n+/).slice(0,12))parseParamText(line); + } + // 当前淘宝新版参数是“值 + 字段名”或“字段名 + 值”两列,没有冒号。 + for(const el of $all('[class*="emphasisParamsInfoItem--"],[class*="generalParamsInfoItem--"]').slice(0,400)){ + const children=[...el.children].filter(child=>clean(child.getAttribute?.('title')||text(child))); + if(children.length<2)continue; + const values=children.map(child=>clean(child.getAttribute?.('title')||text(child))); + const isGeneral=/generalParamsInfoItem/i.test(String(el.className||'')); + const label=isGeneral?values[0]:values[1]; + const value=isGeneral?values[1]:values[0]; + pushParam(label,value); + } + for(const pair of structured.attrs)pushParam(pair.name,pair.value); + const attrs=paramPairs.slice(0,240).map(({name,value})=>({name,value})); + const PARAM_GROUPS={ + '基础身份':['品牌','品类','型号','货号','名称'], + '规格选择':['规格','尺寸','尺码','颜色','容量','净含量','口味','版本','数量','包装'], + '材质/成分/配置':['材质','面料','成分','配料','填充','配置','处理器','内存','存储'], + '功能与适用':['功能','功效','适用','肤质','兼容','性能','版型','风格','场景'], + '工艺与使用':['工艺','做工','使用','洗护','洗涤','清洗','保养','食用方法'], + '合规与服务':['产地','等级','执行标准','保质期','生产日期','认证','质保','售后'] + }; + function evaluateParameterCollectionJS(list){ + const found=[],missing=[];const keys=(list||[]).map(x=>clean(x.name)).filter(Boolean); + for(const [canon,aliases] of Object.entries(PARAM_GROUPS)){ + const hit=(list||[]).some(x=>aliases.some(a=>clean(x.name).includes(a))&&clean(x.value)); + (hit?found:missing).push(canon); + } + const state=(list||[]).length>=3?'complete':((list||[]).length?'partial':'missing'); + return {state,stateText:state==='complete'?'动态参数已采集':state==='partial'?'参数样本较少':'未识别到有效参数',count:(list||[]).length,keys:keys.slice(0,50),found,missing:state==='missing'?['页面动态参数']:[]}; + } + const parameterCollection=evaluateParameterCollectionJS(attrs); + const promoDom=$all('[class*=promo],[class*=Promo],[class*=coupon],[class*=Coupon],[class*=activity],[class*=Activity],[class*=discount],[class*=Discount],[class*=service],[class*=Service]').map(text).map(clean); + const promoLines=String(liveBodyRaw||'').split(/\n+/).map(clean).filter(x=>x.length>=2&&x.length<=180&&/(减|券|折|赠|包邮|价保|退|换新|补贴|立减|满|活动|优惠|会员|服务)/.test(x)); + const promoTexts=uniq([...promoDom,...promoLines,...structured.promos]).filter(x=>x&&x.length>=2&&x.length<=220&&/(减|券|折|赠|包邮|价保|退|换新|补贴|立减|满|活动|优惠|会员|服务)/.test(x)).slice(0,120); + const isRecImg=img=>{try{return !!img.closest('[class*=recommend],[class*=Recommend],[class*=guess],[class*=Guess],[class*=related],[class*=Related],[class*=similar],[class*=Similar],[id*=recommend],[id*=guess]')}catch(e){return false}}; + const validImg=u=>/^https?:\/\//.test(u)&&!/icon|logo|avatar|sprite|shopmanager|\/tps\/|\/tfs\/TB1[^/]+-\d{1,4}-\d{1,4}\.png|(?:-|_)rate(?:\.|_|-)|rate_livephoto|tbbala|alicdn.*\.gif/i.test(u); + const deReview=new Map(); + for(const r of reviews){ + const k=clean(r.content).replace(/\s+/g,'').slice(0,320);if(!k)continue; + 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)} + } + 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{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 inGallery=!!img.closest(mainGallerySelector);const w=img.naturalWidth||img.width||img.getBoundingClientRect().width;const h=img.naturalHeight||img.height||img.getBoundingClientRect().height;return inGallery?(w>=34&&h>=34):isMainRegionImg(img)}catch(e){return false}}; + const mainDom=$all(mainGallerySelector+' img').filter(isMainGalleryImg).map(srcOf); + const skuDom=$all('[class*="SkuContent--"] img,[class*="skuContent--"] img,[class*="SkuItem--"] img,[class*="skuItem--"] img,[data-testid*="sku"] img,[class*="sku"] [role="button"] img,[class*="Sku"] [role="button"] img').filter(img=>!isRecImg(img)).map(srcOf); + const detailDom=uniq([...detailScanUrls,...detailRoots.flatMap(root=>{try{return [...root.querySelectorAll('img')].filter(img=>!isRecImg(img)).map(srcOf)}catch(e){return[]}})]); + const structuredMain=structured.imageRecords.filter(x=>/(?:main|gallery|itempic|auctionpic|primary|header)/i.test(x.path)&&!/(?:detail|desc|review|rate|comment|sku|prop|recommend|shop)/i.test(x.path)).map(x=>x.url); + const structuredDetail=structured.imageRecords.filter(x=>/(?:detail|desc|module|content|aplus)/i.test(x.path)&&!/(?:review|rate|comment|recommend|shop)/i.test(x.path)).map(x=>x.url); + const structuredSku=structured.imageRecords.filter(x=>/(?:sku|prop|spec)/i.test(x.path)&&!/(?:review|rate|comment)/i.test(x.path)).map(x=>x.url); + const buyerSet=new Set(buyerCandidates); + const skuImages=uniq([...skuDom,...structuredSku]).filter(validImg).filter(u=>!buyerSet.has(u)).slice(0,80);const skuSet=new Set(skuImages); + const main=uniq([metaImage,...mainDom,...structuredMain]).filter(validImg).filter(u=>!buyerSet.has(u)&&!skuSet.has(u)).slice(0,24);const mainSet=new Set(main); + const detail=uniq([...detailDom,...structuredDetail]).filter(validImg).filter(u=>!buyerSet.has(u)&&!skuSet.has(u)&&!mainSet.has(u)).slice(0,160); + const buyer=buyerCandidates.slice(0,200); + const allPageCandidates=uniq($all('img').filter(img=>!isRecImg(img)).map(srcOf)).filter(validImg); + const allImgs=uniq([...main,...detail,...skuImages,...buyer]); + const provenance=[]; + for(const u of main)provenance.push({url:u,group:'main',source:u===metaImage?'meta_og_image':structuredMain.includes(u)?'structured_main_gallery':'dom_main_gallery'}); + for(const u of detail)provenance.push({url:u,group:'detail',source:structuredDetail.includes(u)?'structured_product_description':'dom_product_description'}); + for(const u of skuImages)provenance.push({url:u,group:'sku',source:structuredSku.includes(u)?'structured_sku_selector':'dom_sku_selector'}); + reviewRows.forEach((r,reviewIndex)=>{for(const u of r.images||[])if(buyerSet.has(u))provenance.push({url:u,group:'buyerShow',source:'review_record',reviewIndex:reviewIndex+1,reviewSource:r.source||'unknown'})}); + const deQ=new Map(); for(const q of questions){const k=clean(q.question).replace(/\s+/g,'').slice(0,260);if(k&&!deQ.has(k))deQ.set(k,q)} + const publicRaw=(liveBodyText.match(/(?:评价|评论)[^\d]{0,8}([\d,.]+\s*万?\+?)/)||[])[1]||''; + const parseCount=v=>{v=String(v||'').replace(/,/g,'').trim();const m=v.match(/([\d.]+)\s*(万)?/);if(!m)return null;return Math.round(parseFloat(m[1])*(m[2]?10000:1))}; + const publicNumeric=parseCount(publicRaw); + const foldedDefaultMatch=liveBodyText.match(/已折叠\s*([\d,.]+)\s*条[^\n]{0,30}评价/); + const defaultPraiseMatch=liveBodyText.match(/近半年\s*([\d,.]+)\s*位买家默认好评/); + const networkTotal=reviewNetMeta.totals.length?Math.max(...reviewNetMeta.totals.filter(n=>Number.isFinite(n))):null; + if(publicNumeric&&deReview.size>=publicNumeric){collectionComplete=true;explicitEnd=true;stopReason='matched_public_review_count';} + else if(networkTotal!==null&&networkTotal>0&&deReview.size>=networkTotal){collectionComplete=true;explicitEnd=true;stopReason='matched_network_review_total';} + else if((reviewNetMeta.hasMoreFalse||reviewNetMeta.lastPageFlags.length>0) && deReview.size>0){collectionComplete=true;explicitEnd=true;stopReason='network_explicit_end';} + // 严禁“无新增/无下一页控件”单独作为完整依据 + const completeness=collectionComplete?'confirmed':'unconfirmed'; + await saveCheckpoint({reviews:[...deReview.values()],questions:[...deQ.values()],round:pagesVisited,updatedAt:Date.now(),complete:collectionComplete,stopReason,completeness,networkTotal,publicNumeric,reviewNetMeta}); + return { + meta:{platform:location.hostname.includes('tmall')?'tmall':'taobao',collectedAt:new Date().toISOString(),url:location.href,collectorVersion:'9.2.2',imageClassificationVersion:'strict-v1',networkCaptures:captured.length,structuredRoots:roots.length}, + product:{itemId,skuId,title,shop,brand:structured.brands[0]||scriptString(['brandName','brand'],120),category:structured.categories[0]||scriptString(['categoryName','catName'],160),url:location.href,parameterCollection}, + 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} + }; +} + diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/icon-128.png b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..b4f7dec0ffd121954321fb06b8fa036c268087d8 GIT binary patch literal 3313 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 literal 0 HcmV?d00001 diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/manifest.json b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/manifest.json new file mode 100644 index 0000000..75d7d45 --- /dev/null +++ b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/manifest.json @@ -0,0 +1,47 @@ +{ + "manifest_version": 3, + "name": "三笙 · 电商商品分析闭环 V9.3.0", + "version": "9.3.0", + "description": "跨类目电商商品分析:动态参数与SKU采集、图文分源、评论证据、开品与AI视觉方案。", + "permissions": [ + "scripting", + "storage", + "tabs", + "alarms", + "notifications" + ], + "host_permissions": [ + "https://detail.tmall.com/*", + "https://*.tmall.com/*", + "https://*.taobao.com/*", + "https://*.alicdn.com/*", + "https://api.sanshengai.net/*", + "https://www.sanshengai.net/*" + ], + "background": { + "service_worker": "background.js" + }, + "icons": { + "128": "icon-128.png" + }, + "action": { + "default_popup": "popup.html", + "default_title": "三笙 · 电商商品分析闭环 V9.3.0", + "default_icon": "icon-128.png" + }, + "content_scripts": [ + { + "matches": [ + "https://detail.tmall.com/*", + "https://*.tmall.com/*", + "https://item.taobao.com/*", + "https://*.taobao.com/*" + ], + "js": [ + "page-review-hook.js" + ], + "run_at": "document_start", + "world": "MAIN" + } + ] +} diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/page-review-hook.js b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/page-review-hook.js new file mode 100644 index 0000000..ba1a202 --- /dev/null +++ b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/page-review-hook.js @@ -0,0 +1,123 @@ +(() => { + if (window.__TMALL_AI_SAFE_REVIEW_HOOK__) return; + window.__TMALL_AI_SAFE_REVIEW_HOOK__ = true; + + const MAX_RECORDS = 100; + const MAX_TEXT = 1200000; + const MATCH = /review|rate|comment|feedback|evaluate|buyer|remark|ratedetail|ask|question|wdj|评价|评论|问大家/i; + + window.__TMALL_AI_CAPTURE__ = Array.isArray(window.__TMALL_AI_CAPTURE__) + ? window.__TMALL_AI_CAPTURE__ : []; + window.__TMALL_AI_SOURCE_SNAPSHOTS__ = Array.isArray(window.__TMALL_AI_SOURCE_SNAPSHOTS__) + ? window.__TMALL_AI_SOURCE_SNAPSHOTS__ : []; + + function pushCapture(rec) { + try { + if (!rec || !MATCH.test(String(rec.url || ""))) return; + const text = String(rec.text || "").slice(0, MAX_TEXT); + const key = [rec.method || "", rec.url || "", text.slice(0, 240)].join("|"); + const list = window.__TMALL_AI_CAPTURE__; + if (list.some(x => x && x.__safeKey === key)) return; + list.push({...rec, text, __safeKey:key, capturedAt:Date.now(), captureMode:"passive_network"}); + while (list.length > MAX_RECORDS) list.shift(); + } catch (_) {} + } + + // Passive fetch interception: clones only the response the page already requested. + try { + const originalFetch = window.fetch; + if (typeof originalFetch === "function") { + window.fetch = async function(...args) { + const response = await originalFetch.apply(this, args); + try { + const req = args[0]; + const opt = args[1] || {}; + const url = String(req?.url || req || ""); + if (MATCH.test(url)) { + const clone = response.clone(); + clone.text().then(text => { + pushCapture({ + url, + method:String(opt.method || req?.method || "GET"), + status:response.status, + text + }); + }).catch(() => {}); + } + } catch (_) {} + return response; + }; + } + } catch (_) {} + + // Passive XHR interception: reads response after normal page request completes. + try { + const open = XMLHttpRequest.prototype.open; + const send = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function(method, url, ...rest) { + this.__tmall_ai_url = String(url || ""); + this.__tmall_ai_method = String(method || "GET"); + return open.call(this, method, url, ...rest); + }; + XMLHttpRequest.prototype.send = function(...args) { + if (MATCH.test(this.__tmall_ai_url || "")) { + this.addEventListener("load", () => { + try { + const txt = typeof this.responseText === "string" ? this.responseText : ""; + pushCapture({ + url:this.__tmall_ai_url, + method:this.__tmall_ai_method || "XHR", + status:this.status, + text:txt + }); + } catch (_) {} + }, {once:true}); + } + return send.apply(this, args); + }; + } catch (_) {} + + // Page-code snapshots. Only read script text already present in the DOM. + function snapshotScripts() { + try { + const out = []; + const scripts = document.scripts ? Array.from(document.scripts) : []; + for (const s of scripts) { + const txt = String(s.textContent || ""); + if (txt.length < 20) continue; + if (!/review|rate|comment|evaluate|feedback|question|ask|评价|评论|问大家/i.test(txt)) continue; + out.push({ + source:"inline_script", + text:txt.slice(0, MAX_TEXT), + capturedAt:Date.now() + }); + if (out.length >= 12) break; + } + window.__TMALL_AI_SOURCE_SNAPSHOTS__ = out; + } catch (_) {} + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", snapshotScripts, {once:true}); + } else { + snapshotScripts(); + } + + // Mutation is only used to refresh already-present inline JSON snapshots. + try { + let timer = 0; + const mo = new MutationObserver(() => { + clearTimeout(timer); + timer = setTimeout(snapshotScripts, 1200); + }); + mo.observe(document.documentElement, {childList:true, subtree:true}); + } catch (_) {} + + window.__TMALL_AI_SAFE_CAPTURE_STATUS__ = () => ({ + mode:"passive_code_and_network", + networkRecords:(window.__TMALL_AI_CAPTURE__ || []).length, + sourceSnapshots:(window.__TMALL_AI_SOURCE_SNAPSHOTS__ || []).length, + extraRequestsCreated:0, + installed:true + }); +})(); diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.css b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.css new file mode 100644 index 0000000..20b9fb7 --- /dev/null +++ b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.css @@ -0,0 +1,168 @@ +:root { + color-scheme: light; + --ink: #142126; + --muted: #687a80; + --line: #dbe6e5; + --soft: #f3f8f7; + --green: #00a98f; + --green-dark: #087565; + --blue: #1675d1; + --danger: #b63f4b; +} + +* { box-sizing: border-box; } + +body { + width: 390px; + max-height: 620px; + margin: 0; + overflow-y: auto; + background: #f6faf9; + color: var(--ink); + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; +} + +button { font: inherit; } + +.app-header { + padding: 18px 18px 15px; + border-bottom: 1px solid var(--line); + background: #fff; +} + +.brand-row, +.account-row, +.status-head, +.detail-title, +.metric-row, +.section-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.brand { display: flex; align-items: center; gap: 10px; min-width: 0; } +.brand img { width: 34px; height: 34px; border-radius: 8px; } +.brand h1 { margin: 0; font-size: 17px; line-height: 1.2; letter-spacing: 0; } +.brand span { display: block; margin-top: 2px; color: var(--muted); font-size: 11px; } +.app-header p { margin: 12px 0 0; color: var(--muted); font-size: 12px; line-height: 1.55; } + +main { padding: 14px 16px 18px; } + +.text-action, +.compact-button { + border: 0; + background: transparent; + color: var(--green-dark); + cursor: pointer; + font-size: 12px; + font-weight: 700; +} + +.compact-button { + min-width: 52px; + height: 30px; + border: 1px solid var(--line); + border-radius: 7px; + background: #fff; +} +.compact-button.primary { border-color: var(--green); background: var(--green); color: #fff; } + +.account-row { margin-bottom: 12px; } +.account-copy { display: flex; align-items: center; gap: 9px; min-width: 0; } +.account-copy i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: #a9b6b8; } +.account-copy i.online { background: var(--green); box-shadow: 0 0 0 4px rgb(0 169 143 / 12%); } +.account-copy strong { display: block; font-size: 12px; } +.account-copy span { display: block; max-width: 230px; margin-top: 2px; overflow: hidden; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } + +.status-card, +.detail-panel, +.mode-panel { + border: 1px solid var(--line); + border-radius: 8px; + background: #fff; +} + +.status-card { padding: 15px; } +.phase { display: flex; align-items: center; gap: 8px; } +.phase strong { font-size: 14px; } +.phase-indicator { width: 9px; height: 9px; border-radius: 50%; background: #9aa9ac; } +.status-head > b { color: var(--green-dark); font-size: 13px; } +.status-message { min-height: 38px; margin-top: 10px; color: #53676d; font-size: 12px; line-height: 1.55; } +.progress-track { height: 7px; margin-top: 12px; overflow: hidden; border-radius: 6px; background: #e9f0ef; } +.progress-track i { display: block; width: 0; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--green), var(--blue)); transition: width .25s ease; } +.task-meta { margin-top: 10px; overflow: hidden; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.wait-hint { margin-top: 10px; padding: 9px 10px; border-radius: 7px; background: #eef7ff; color: #43616e; font-size: 11px; line-height: 1.45; } + +.status-card[data-state="collecting"] .phase-indicator, +.status-card[data-state="analyzing"] .phase-indicator { + background: var(--blue); + box-shadow: 0 0 0 4px rgb(22 117 209 / 12%); + animation: pulse 1.4s ease-in-out infinite; +} +.status-card[data-state="completed"] .phase-indicator { background: var(--green); box-shadow: 0 0 0 4px rgb(0 169 143 / 12%); } +.status-card[data-state="failed"] .phase-indicator { background: var(--danger); box-shadow: 0 0 0 4px rgb(182 63 75 / 12%); } + +@keyframes pulse { + 50% { opacity: .35; transform: scale(.8); } +} + +.service-line { display: flex; justify-content: flex-end; min-height: 25px; padding: 7px 2px 4px; } +.service-line span { color: var(--muted); font-size: 10px; } +.service-line span.ok { color: var(--green-dark); } +.service-line span.warn { color: #a56a19; } + +.action-section { display: grid; gap: 8px; } +.main-button, +.secondary-button { + width: 100%; + min-height: 42px; + border: 0; + border-radius: 8px; + cursor: pointer; + font-size: 13px; + font-weight: 800; +} +.main-button { background: linear-gradient(105deg, var(--green), var(--blue)); color: #fff; } +.main-button:hover { filter: brightness(.97); } +.main-button:disabled, +.secondary-button:disabled { cursor: not-allowed; opacity: .55; } +.main-button.danger { background: var(--danger); } +.report-button { margin-top: 12px; background: var(--ink); } +.secondary-button { margin-top: 12px; border: 1px solid #a8c8c4; background: #fff; color: #245d58; } +.history-panel { margin-top: 12px; } +.history-list { display: grid; gap: 8px; margin-top: 10px; } +.history-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px; border-radius: 7px; background: var(--soft); } +.history-item div { min-width: 0; } +.history-item strong, .history-item span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.history-item strong { font-size: 11px; } +.history-item span { margin-top: 3px; color: var(--muted); font-size: 10px; } +.history-item button { flex: 0 0 auto; border: 0; background: transparent; color: var(--green-dark); cursor: pointer; font-size: 10px; font-weight: 700; } + +.detail-panel { margin-top: 12px; padding: 13px; } +.detail-title strong, +.section-heading strong { font-size: 13px; } +.detail-title span, +.section-heading span { color: var(--muted); font-size: 11px; } +.metric-row { margin-top: 10px; font-size: 11px; } +.metric-row span { color: var(--muted); } +.metric-row b { font-size: 13px; } +.metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 11px; } +.metric-grid div { padding: 9px; border-radius: 7px; background: var(--soft); } +.metric-grid span { display: block; color: var(--muted); font-size: 10px; } +.metric-grid b { display: block; margin-top: 3px; font-size: 14px; } +.detail-note { margin-top: 9px; color: var(--muted); font-size: 10px; } +.param-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px; } +.param-tag { padding: 3px 7px; border: 1px solid var(--line); border-radius: 6px; background: var(--soft); color: #4c6267; font-size: 10px; } +.live-preview { max-height: 68px; margin-top: 9px; overflow: auto; border-top: 1px solid var(--line); padding-top: 8px; color: #53686d; font-size: 10px; line-height: 1.55; } + +.mode-panel { margin-top: 12px; padding: 14px; } +.section-heading span { display: block; margin-top: 2px; } +.mode-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin-top: 11px; } +.mode { min-height: 58px; border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--ink); cursor: pointer; } +.mode strong { display: block; font-size: 12px; } +.mode small { display: block; margin-top: 4px; color: var(--muted); font-size: 9px; } +.mode.active { border-color: var(--ink); background: var(--ink); color: #fff; } +.mode.active small { color: #c6d1d3; } +.option-note { margin-top: 8px; color: var(--muted); font-size: 10px; text-align: center; } diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.html b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.html new file mode 100644 index 0000000..a2bc6ce --- /dev/null +++ b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.html @@ -0,0 +1,131 @@ + + + + + + + 三笙商品分析 + + +

+
+
+ +
+

三笙商品分析

+ 电商单品洞察 +
+
+ +
+

采集商品、图片、参数和评论,生成可追溯的产品分析报告。

+
+ +
+ + +
+
+
+ + 等待开始 +
+ 0% +
+
请打开天猫或淘宝商品详情页
+
+ + + + +
+ +
+ 正在检查云端服务 +
+ +
+ + +
+ + + + + +
+
+
+ 采集深度 + 决定最多分析的评论数量 +
+
+
+ + + +
+ +
当前数据可能尚未覆盖全部公开评论。
+
+ + +
+ + + + + diff --git a/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.js b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.js new file mode 100644 index 0000000..c9aa9d1 --- /dev/null +++ b/release/sansheng-product-analysis-extension-9.3.0-prod-20260908-233841/popup.js @@ -0,0 +1,267 @@ +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'} +} +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||'请稍后重试'}`:'登录后可使用云端分析服务'; + 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='云端服务不可用,请稍后重试后重试';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='云端服务不可用,请稍后重试后重试';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); + +openReports.onclick=async()=>{ + reportHistory.style.display='block'; + historyList.innerHTML='
正在加载报告记录…
'; + try{ + const result=await chrome.runtime.sendMessage({type:'PRODUCT_ANALYSIS_HISTORY'}); + 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 label=status===3?'已完成':status===4?'失败':`${Number(item.Progress??item.progress??0)}%`; + const path=item.ReportUrl||item.reportUrl||`/product-analysis/reports/${encodeURIComponent(id)}`; + const url=/^https?:\/\//i.test(path)?path:sanshengExtensionAuth.config.pcBase+path; + return `
${title}${label} · ${escapeHtml(item.Stage||item.stage||'')}
`; + }).join(''):'
暂无分析报告
'; + historyList.querySelectorAll('button[data-report-url]').forEach(button=>button.onclick=()=>chrome.tabs.create({url:button.dataset.reportUrl})); + }catch(error){historyList.innerHTML=`
${escapeHtml(error instanceof Error?error.message:'报告记录加载失败')}
`} +}; +