Initial commit of Tmall new-product analysis loop v9.2.5.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
喻长发
2026-09-03 15:42:05 +08:00
co-authored by Cursor
commit 42223969db
55 changed files with 9760 additions and 0 deletions
@@ -0,0 +1,23 @@
{
"name": "sansong-tmall-product-report",
"version": "9.0.0",
"description": "三笙跨类目电商商品采集、分析、开品与AI视觉报告插件。",
"author": {
"name": "三笙市场洞察"
},
"skills": "./skills/",
"interface": {
"displayName": "三笙电商商品分析报告",
"shortDescription": "跨类目采集商品证据并输出图文开品报告。",
"longDescription": "根据类目、标题、主图、详情、SKU、参数、评论与问答动态生成有证据链的电商商品分析报告、新品设计方案和AI视觉参数。",
"developerName": "三笙市场洞察",
"category": "Productivity",
"capabilities": ["Read", "Write", "Analysis", "Reporting"],
"brandColor": "#00AFC5",
"defaultPrompt": [
"基于最新采集生成商品分析报告",
"检查商品字段采集是否完整",
"输出下一款开品卖点与落地方案"
]
}
}
@@ -0,0 +1,10 @@
# 三笙电商商品分析报告插件 v9.2.5
用于把淘宝/天猫当前商品采集结果转成跨类目商品分析、新品设计和 AI 视觉报告。
- `skills/tmall-product-analysis-report/SKILL.md`:跨类目分析与报告规范。
- `scripts/normalize_capture.mjs`:采集 JSON 标准化。
- `assets/capture-extension/`:通用商品页采集扩展参考。
- `assets/taobao_detail_fields_v8.3.6.xlsx`:保持原文件名的跨类目字段对照表。
报告明确展开标题、主图、详情、SKU、评论、商品价值结构、设计资产分级及 AI 生成规格;用户只负责采集并生成。
@@ -0,0 +1,3 @@
# 三笙电商商品详情采集扩展 v9.2.5
跨类目采集商品标题、类目候选、主图、详情、SKU、动态参数、评论和问答。家纺仅为分析模板分支,不限制采集字段。
@@ -0,0 +1,26 @@
{
"manifest_version": 3,
"name": "三笙商品详情采集",
"version": "9.2.5",
"description": "跨类目解析商品标题、主图、详情、活动、SKU、动态参数、评论与问答。",
"permissions": ["activeTab", "scripting", "tabs", "alarms", "cookies"],
"host_permissions": [
"http://127.0.0.1:3300/*",
"http://localhost:3300/*",
"http://127.0.0.1/*",
"http://localhost/*",
"https://*.taobao.com/*",
"https://item.jd.com/*",
"https://*.jd.com/*",
"https://item.taobao.com/*",
"https://detail.tmall.com/*",
"https://*.tmall.com/*"
],
"content_scripts": [{
"matches": ["http://127.0.0.1/*", "http://localhost/*"],
"js": ["page_bridge.js"],
"run_at": "document_start"
}],
"background": { "service_worker": "service_worker.js" },
"action": { "default_title": "重新采集当前商品页" }
}
@@ -0,0 +1,6 @@
window.addEventListener('message',event=>{
if(event.source!==window||event.data?.type!=='SANSONG_COLLECTOR_PING')return
chrome.runtime.sendMessage({type:'SANSONG_COLLECTOR_WAKE'},response=>{
window.postMessage({type:'SANSONG_COLLECTOR_PONG',requestId:event.data.requestId,installed:true,version:chrome.runtime.getManifest().version,connected:Boolean(response?.connected)},'*')
})
})
@@ -0,0 +1,187 @@
const API_BASE='http://127.0.0.1:3300/api/collector/product-page'
async function capture(tabId){
const result=await chrome.scripting.executeScript({target:{tabId},func:()=>{
const clean=value=>String(value||'').replace(/\s+/g,' ').trim()
const uniq=rows=>[...new Set(rows.filter(Boolean))]
const bodyText=document.body?.innerText||''
const lines=bodyText.split(/\n+/).map(clean).filter(Boolean)
const url=location.href
const itemId=url.match(/[?&]id=(\d+)/)?.[1]||url.match(/\/(\d+)\.html/)?.[1]||''
const skuId=url.match(/[?&]skuId=(\d+)/)?.[1]||''
const platform=/tmall/i.test(location.hostname)?'tmall':/taobao/i.test(location.hostname)?'taobao':/jd\.com/i.test(location.hostname)?'jd':'unknown'
const title=clean(document.querySelector('h1')?.textContent||document.querySelector('meta[property="og:title"]')?.content||document.title)
const readAfter=patterns=>{const row=lines.find(line=>patterns.some(pattern=>pattern.test(line)));if(!row)return'';const parts=row.split(/[::]/);return clean(parts.length>1?parts.slice(1).join(':'):row)}
const shop=clean(document.querySelector('[class*="shop-name"],[class*="shopName"],[class*="seller-name"],[class*="sellerName"]')?.textContent)||readAfter([/^店铺/,/^店名/,/^卖家/])
const priceText=clean(document.querySelector('[class*="promotionPrice"],[class*="salePrice"],[class*="price"]')?.textContent||'')
const currentPrice=clean(priceText.match(/[¥¥]?\s*(\d+(?:\.\d+)?)/)?.[1]||bodyText.match(/(?:券后|到手价|活动价|价格)\s*[¥¥]?\s*(\d+(?:\.\d+)?)/)?.[1]||'')
const originalPrice=clean(bodyText.match(/(?:原价|划线价|优惠前)\s*[¥¥]?\s*(\d+(?:\.\d+)?)/)?.[1]||'')
const sold=clean(bodyText.match(/(?:已售|付款|销量|累计销量)\s*([0-9.万wW+]+)/)?.[1]||'')
const publicReviewCount=clean(bodyText.match(/(?:累计评价|全部评价|商品评价|评论)\s*([0-9.万wW+]+)/)?.[1]||'')
const ranking=clean(bodyText.match(/(?:热销榜|排行榜|榜第)\s*第?\s*([0-9]+)\s*名/)?.[0]||'')
const meta={}
document.querySelectorAll('meta').forEach(el=>{const key=el.getAttribute('property')||el.getAttribute('name');const value=el.getAttribute('content');if(key&&value&&!/cookie|authorization|token|password/i.test(key))meta[key]=value})
const jsonld=[]
document.querySelectorAll('script[type="application/ld+json"]').forEach(el=>{try{jsonld.push(JSON.parse(el.textContent||'null'))}catch{}})
const badImage=/avatar|icon|logo|sprite|loading|placeholder|qrcode|qr-code|emoji|badge/i
const imageRows=[...document.images].map(img=>({
url:img.currentSrc||img.src,
width:img.naturalWidth||img.width||0,
height:img.naturalHeight||img.height||0,
alt:clean(img.alt),
context:clean(`${img.alt||''} ${img.closest('[class],[id]')?.className||''} ${img.closest('[class],[id]')?.id||''}`)
})).filter(row=>/^https?:\/\//.test(row.url)&&!badImage.test(`${row.url} ${row.context}`)&&(row.width>=120||row.height>=120))
const uniqueImages=rows=>{const seen=new Set();return rows.filter(row=>{const key=row.url.replace(/_[0-9]+x[0-9]+[^/]*$/,'');if(seen.has(key))return false;seen.add(key);return true})}
const reviewRows=uniqueImages(imageRows.filter(row=>/review|comment|rate|evaluate|buyer|show|评价|评论|买家秀|晒单/i.test(row.context)))
const detailRows=uniqueImages(imageRows.filter(row=>/detail|description|desc|richtext|module|详情|图文|描述/i.test(row.context)&&!reviewRows.some(x=>x.url===row.url)))
const skuRows=uniqueImages(imageRows.filter(row=>/sku|prop|variant|option|规格|颜色|尺码|型号|口味/i.test(row.context)&&!reviewRows.some(x=>x.url===row.url)&&!detailRows.some(x=>x.url===row.url)))
const mainRows=uniqueImages(imageRows.filter(row=>/gallery|thumb|preview|main|swiper|carousel|主图|轮播/i.test(row.context)&&!reviewRows.some(x=>x.url===row.url)&&!detailRows.some(x=>x.url===row.url))).slice(0,30)
const allImages=uniqueImages(imageRows)
const blockedAttribute=/优惠|券|立减|包邮|发货|送达|价保|退货|客服|店铺|销量|付款|评论|评价|问答/
const attributes=[]
const attributeSeen=new Set()
const addAttribute=(name,value,source='dom')=>{name=clean(name).replace(/[::]$/,'');value=clean(value);if(!name||!value||name.length>36||value.length>600||blockedAttribute.test(name))return;const key=`${name}\u0000${value}`;if(attributeSeen.has(key))return;attributeSeen.add(key);attributes.push({name,value,source})}
document.querySelectorAll('table tr,[class*="attribute"] li,[class*="Attribute"] li,[class*="parameter"] li,[class*="Parameter"] li,[class*="property"] li,[class*="Property"] li,[class*="attr"] li').forEach(el=>{
const cells=[...el.querySelectorAll(':scope > th,:scope > td,:scope > span,:scope > div')].map(x=>clean(x.textContent)).filter(Boolean)
if(cells.length>=2)addAttribute(cells[0],cells.slice(1).join(' '),'attribute_dom')
else{const text=clean(el.textContent);const match=text.match(/^([^::]{1,36})[::]\s*(.{1,600})$/);if(match)addAttribute(match[1],match[2],'attribute_dom')}
})
lines.forEach(line=>{const match=line.match(/^([^::]{1,30})[::]\s*(.{1,400})$/);if(match)addAttribute(match[1],match[2],'page_text')})
const breadcrumb=uniq([...document.querySelectorAll('[class*="breadcrumb"] a,[class*="crumb"] a,[class*="category"] a')].map(el=>clean(el.textContent))).filter(x=>x.length<=40).slice(0,12)
const categoryCandidates=uniq([...breadcrumb,...attributes.filter(x=>/类目|分类|品类|产品类型|商品类型/.test(x.name)).map(x=>x.value)]).slice(0,20)
const variants=[]
const variantSeen=new Set()
const addVariant=row=>{const name=clean(row.name);if(!name||name.length>180)return;const key=`${clean(row.group)}\u0000${name}`;if(variantSeen.has(key))return;variantSeen.add(key);variants.push({...row,name,group:clean(row.group),source:row.source||'sku_dom'})}
document.querySelectorAll('[class*="sku"],[class*="Sku"],[class*="variant"],[class*="Variant"],[class*="prop"] ,[class*="Prop"]').forEach(container=>{
const group=clean(container.querySelector('[class*="title"],[class*="label"],[class*="name"]')?.textContent||'')
container.querySelectorAll('button,[role="option"],li,[class*="item"],[class*="Item"]').forEach(el=>{
const name=clean(el.getAttribute('title')||el.getAttribute('aria-label')||el.textContent)
const image=el.querySelector('img')?.currentSrc||el.querySelector('img')?.src||''
addVariant({group,name,image,selected:el.getAttribute('aria-selected')==='true'||/selected|active|current/i.test(el.className),disabled:el.getAttribute('aria-disabled')==='true'||el.hasAttribute('disabled')})
})
})
const variantKeywords=/(颜色|色号|尺码|尺寸|规格|型号|版本|配置|容量|净含量|口味|香型|套装|套餐|款式|适用|包装|重量|数量|码数|度数|功率|内存|存储)/
lines.filter(line=>variantKeywords.test(line)&&line.length<=500).slice(0,180).forEach(line=>line.split(/[,,]/).map(clean).filter(Boolean).forEach(name=>addVariant({group:'',name,source:'page_text'})))
const promotionPattern=/(优惠|券|立减|满减|包邮|免运费|价保|退货|退款|七天无理由|预计.*发货|送达|大促|活动|限时|到手价|店铺优惠|赠品|买一送一)/i
const promotions=uniq(lines.filter(line=>promotionPattern.test(line)&&line.length<=120)).slice(0,100).map(text=>({text,source:'page_text'}))
const questions=[]
document.querySelectorAll('[class*="question"],[class*="Question"],[class*="ask"],[class*="Ask"]').forEach((el,index)=>{
const text=clean(el.innerText);if(!text)return
const question=clean(el.querySelector('[class*="question"],[class*="title"]')?.textContent||text.match(/(.{2,160}[??])/)?.[1])
const answer=clean(el.querySelector('[class*="answer"]')?.textContent||'')
if(question)questions.push({id:`question-${index+1}`,question,answer,source:'question_dom'})
})
const reviewSelectors=['[class*="review-item"]','[class*="comment-item"]','[class*="rate-item"]','[class*="ReviewItem"]','[class*="CommentItem"]','[data-testid*="review"]']
const reviews=[...document.querySelectorAll(reviewSelectors.join(','))].slice(0,500).map((el,index)=>{
const content=clean(el.innerText);if(content.length<6||content.length>3000)return null
const images=uniq([...el.querySelectorAll('img')].map(img=>img.currentSrc||img.src).filter(src=>/^https?:\/\//.test(src)&&!badImage.test(src)))
const date=content.match(/20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?(?:\s+\d{1,2}:\d{2})?/)?.[0]||''
const appendContent=clean(content.match(/(?:追评|追加评价)[::\s]*(.{2,800})/)?.[1]||'')
const sku=clean(el.querySelector('[class*="sku"],[class*="spec"],[class*="attr"]')?.textContent||'')
const merchantReply=clean(el.querySelector('[class*="reply"],[class*="seller"]')?.textContent||'')
const rating=Number(el.getAttribute('data-score')||el.getAttribute('data-rating')||el.querySelector('[data-score]')?.getAttribute('data-score'))||null
return{id:el.getAttribute('data-id')||el.id||`review-${index+1}`,content,appendContent,date,rating,sku,hasImage:images.length>0,images,merchantReply,source:'review_dom'}
}).filter(Boolean)
const collection={
publicReviewCount,
reviewActual:reviews.length,
questionActual:questions.length,
reviewCompleteness:reviews.length?'visible_content_collected':'no_visible_review_nodes',
reviewStopReason:reviews.length?'visible_dom_exhausted':'reviews_not_open_or_platform_folded',
capturedAt:new Date().toISOString(),
collectorVersion:chrome.runtime.getManifest().version,
platform
}
const structured={
meta:{...collection,url},
category:{candidates:categoryCandidates,breadcrumb},
product:{itemId,skuId,title,shop,brand:attributes.find(item=>/^品牌$/.test(item.name))?.value||'',url,platform},
sales:{currentPrice,originalPrice,sold,ranking},
promotions,attributes,sku:variants,questions,reviews,
images:{all:allImages,main:mainRows,detail:detailRows.slice(0,120),sku:skuRows.slice(0,120),buyerShow:reviewRows.slice(0,160)},
collection,pageText:bodyText
}
return{structured,...structured,jsonld,meta}
}})
return result?.[0]?.result
}
const lastCaptured=new Map()
function isProductUrl(url=''){return /^https:\/\/(?:item\.jd\.com\/\d+\.html|item\.taobao\.com\/item\.htm|detail\.tmall\.com\/item\.htm)/i.test(url)}
async function reportAuthState(){
try{
const [taobaoCookies,jdCookies]=await Promise.all([chrome.cookies.getAll({domain:'.taobao.com'}),chrome.cookies.getAll({domain:'.jd.com'})])
const response=await fetch(`${API_BASE}/auth-heartbeat`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({version:chrome.runtime.getManifest().version,platforms:{taobao:{loggedIn:taobaoCookies.some(cookie=>/tracknick|cookie2|_tb_token_|sgcookie/i.test(cookie.name))},jd:{loggedIn:jdCookies.some(cookie=>/pin|thor|unick/i.test(cookie.name))}}})})
return response.ok
}catch(error){console.error('[三笙登录态上报]',error);return false}
}
async function captureAndSend(tab,{force=false}={}){
if(!tab?.id||!isProductUrl(tab.url))return
const key=`${tab.id}:${tab.url}`
if(!force&&Date.now()-(lastCaptured.get(key)||0)<30_000)return
lastCaptured.set(key,Date.now())
try{
await chrome.action.setBadgeText({tabId:tab.id,text:'采'})
await chrome.action.setBadgeBackgroundColor({tabId:tab.id,color:'#2563EB'})
const payload=await capture(tab.id)
const response=await fetch(`${API_BASE}/capture`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
if(!response.ok)throw new Error(await response.text())
await chrome.action.setBadgeText({tabId:tab.id,text:'OK'})
await chrome.action.setBadgeBackgroundColor({tabId:tab.id,color:'#155C4E'})
setTimeout(()=>chrome.action.setBadgeText({tabId:tab.id,text:''}),2500)
}catch(error){
lastCaptured.delete(key)
await chrome.action.setBadgeText({tabId:tab.id,text:'!'})
await chrome.action.setBadgeBackgroundColor({tabId:tab.id,color:'#A54F43'})
console.error('[三笙商品采集]',error)
}
}
chrome.tabs.onUpdated.addListener((tabId,changeInfo,tab)=>{if(changeInfo.status==='complete'&&isProductUrl(tab.url))setTimeout(()=>captureAndSend({...tab,id:tabId}),2500)})
chrome.tabs.onRemoved.addListener(tabId=>{for(const key of lastCaptured.keys())if(key.startsWith(`${tabId}:`))lastCaptured.delete(key)})
chrome.action.onClicked.addListener(tab=>captureAndSend(tab,{force:true}))
let queueBusy=false
let queueBackoffMs=1000
async function processCaptureQueue(){
if(queueBusy)return
queueBusy=true
let tab
try{
const response=await fetch(`${API_BASE}/next`)
if(response.status===204){queueBackoffMs=Math.min(queueBackoffMs*1.5,15_000);return}
if(!response.ok)throw new Error(`任务队列 HTTP ${response.status}`)
const task=await response.json()
tab=await chrome.tabs.create({url:task.url,active:false})
await new Promise((resolve,reject)=>{
const timeout=setTimeout(()=>{chrome.tabs.onUpdated.removeListener(listener);reject(new Error('商品页加载超时'))},45_000)
const listener=(tabId,changeInfo,updated)=>{if(tabId===tab.id&&changeInfo.status==='complete'){clearTimeout(timeout);chrome.tabs.onUpdated.removeListener(listener);resolve(updated)}}
chrome.tabs.onUpdated.addListener(listener)
})
await new Promise(resolve=>setTimeout(resolve,3500))
await captureAndSend(await chrome.tabs.get(tab.id),{force:true})
queueBackoffMs=1000
}catch(error){console.error('[三笙采集队列]',error);queueBackoffMs=Math.min(queueBackoffMs*2,30_000)}
finally{if(tab?.id)await chrome.tabs.remove(tab.id).catch(()=>{});queueBusy=false;setTimeout(processCaptureQueue,queueBackoffMs)}
}
chrome.runtime.onInstalled.addListener(()=>{chrome.alarms.create('sansong-product-queue',{periodInMinutes:.5});processCaptureQueue()})
chrome.runtime.onStartup.addListener(()=>{chrome.alarms.create('sansong-product-queue',{periodInMinutes:.5});processCaptureQueue()})
chrome.alarms.onAlarm.addListener(alarm=>{if(alarm.name==='sansong-product-queue')processCaptureQueue();if(alarm.name==='sansong-auth-heartbeat')reportAuthState()})
chrome.alarms.create('sansong-product-queue',{periodInMinutes:.5})
chrome.alarms.create('sansong-auth-heartbeat',{periodInMinutes:1})
processCaptureQueue()
reportAuthState()
chrome.runtime.onMessage.addListener((message,_sender,sendResponse)=>{if(message?.type!=='SANSONG_COLLECTOR_WAKE')return;reportAuthState().then(connected=>sendResponse({connected})).catch(()=>sendResponse({connected:false}));return true})
chrome.cookies.onChanged.addListener(change=>{if(/(?:^|\.)(?:taobao|tmall|jd)\.com$/i.test(change.cookie.domain))setTimeout(reportAuthState,300)})
@@ -0,0 +1,52 @@
#!/usr/bin/env node
import fs from 'node:fs'
const input = process.argv[2]
if (!input) {
console.error('Usage: node scripts/normalize_capture.mjs <capture.json>')
process.exit(1)
}
const payload = JSON.parse(fs.readFileSync(input, 'utf8'))
const raw = payload.raw || payload.structured || payload
const images = raw.images || raw.image_groups || {}
const product = raw.product || {}
const sales = raw.sales || {}
const out = {
schemaVersion: '9.0.0',
category: raw.category || {candidates: raw.categoryCandidates || [], breadcrumb: raw.breadcrumb || []},
product: {
itemId: product.itemId || raw.itemId || '',
skuId: product.skuId || raw.skuId || '',
title: product.title || raw.title || payload.title || '',
shop: product.shop || raw.shopName || '',
brand: product.brand || '',
category: product.category || raw.categoryName || '',
url: product.url || raw.url || payload.url || ''
},
sales: {
currentPrice: sales.currentPrice || raw.price || '',
sold: sales.sold || raw.sold || '',
ranking: sales.ranking || ''
},
counts: {
attributes: Array.isArray(raw.attributes) ? raw.attributes.length : 0,
sku: Array.isArray(raw.sku) ? raw.sku.length : 0,
promotions: Array.isArray(raw.promotions) ? raw.promotions.length : 0,
reviews: Array.isArray(raw.reviews) ? raw.reviews.length : 0,
questions: Array.isArray(raw.questions) ? raw.questions.length : 0,
mainImages: Array.isArray(images.main || images.hero) ? (images.main || images.hero).length : 0,
detailImages: Array.isArray(images.detail) ? images.detail.length : 0,
skuImages: Array.isArray(images.sku) ? images.sku.length : 0,
buyerShowImages: Array.isArray(images.buyerShow || images.review) ? (images.buyerShow || images.review).length : 0
},
attributes: raw.attributes || [],
sku: raw.sku || [],
promotions: raw.promotions || [],
reviews: raw.reviews || [],
questions: raw.questions || [],
images
}
console.log(JSON.stringify(out, null, 2))
@@ -0,0 +1,54 @@
---
name: tmall-product-analysis-report
description: Generate evidence-based, cross-category ecommerce product analysis reports from captured title, hero images, detail images, promotions, SKU, attributes, reviews and questions.
---
# 三笙电商商品分析报告引擎
本技能把真实电商商品数据转成完整阅读型报告和下一款 AI 视觉方案。用户只负责采集与生成,不要求用户在报告中操作流程。
## 工作顺序
1. 读取类目、标题、参数、SKU、主图、详情图、评论、买家图和问答。
2. 自动识别类目及分析维度。家纺只是分支,不是默认模板。
3. 建立证据编号,并把内容分为数据事实、分析判断、设计建议和证据不足。
4. 输出商品总览、商品画像、标题、主图、详情、SKU、评价、商品价值、新品设计和 AI 生成企划。
## 类目分支
- 服饰:尺码、版型、面料、做工、色差、上身效果。
- 食品:规格、口味、配料、保质期、复购、包装。
- 美妆:功效宣称、肤质、成分、使用反馈、敏感风险。
- 数码:配置、兼容性、性能、售后、使用问题。
- 家纺:结构规格、材质触感、颜色花型、工艺、洗后表现、场景适配。
- 其他:从已采标题、参数、SKU 和评论自动形成维度。
## 证据规则
- 主图、详情图、SKU 图和评论图严格分源,并绑定对应 imageEvidenceId。
- 评论只验证购后体验,问答只表示购买前顾虑。
- 单商品特征不等于竞争力;没有竞品数据时写“商品价值结构”。
- “存在”不等于“必须继承”。设计结论分为已验证资产、候选方向和 AI 探索变量。
- 缺少可靠数据时显示“未采集 / 当前不可分析”,不得用常识补齐。
## 报告结构
1. 商品总览与证据边界。
2. 动态商品画像。
3. 标题与营销表达拆解。
4. 主图逐张深度分析。
5. 详情页分段分析。
6. SKU 与参数矩阵。
7. 评论、问答和买家实拍。
8. 商品价值结构。
9. 新品方案与设计资产分级。
10. AI 视觉生成企划。
11. 样品和页面落地验证。
## AI 视觉输出
必须包含 Reference DNA、Design Delta、Must Keep、Must Change,以及 Product、Style、Scene、Photography、Material Rendering、Composition、Negative Rules。产品图和电商主图逐张写清生成目的、拍摄方式、构图和证明任务。
## 页面设计
使用白色背景和高对比深色文字,青绿到青蓝渐变只承担品牌、进度和重点强调。商品图保持原比例;大段内容用图文并排、卡片分层和表格呈现,桌面与移动端均需可读。