dump-voc-summary-v2.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #!/usr/bin/env node
  2. /**
  3. * V2:完整提炼 VOC 报告中的"复合体方向"分析所需数据
  4. * 基于 voc-analyze.js 的实际输出结构
  5. */
  6. const fs = require('fs');
  7. const path = require('path');
  8. const { analyze } = require('./voc-analyze.js');
  9. const PRODUCTS = [
  10. { key: 'liver', label: '江中肝纯片(OTC 保肝片剂)', file: 'jiangzhong-liver-voc.json' },
  11. { key: 'probiotic', label: '江中儿童乳酸菌片', file: 'jiangzhong-probiotic-voc.json' },
  12. { key: 'monkey', label: '江中猴姑饼干', file: 'jiangzhong-monkey-voc.json' },
  13. ];
  14. const buffer = [];
  15. const out = (s) => buffer.push(s);
  16. for (const p of PRODUCTS) {
  17. const fp = path.join('data', p.file);
  18. if (!fs.existsSync(fp)) continue;
  19. const voc = JSON.parse(fs.readFileSync(fp, 'utf-8'));
  20. const A = analyze(voc);
  21. out('\n\n' + '='.repeat(80));
  22. out(`【${p.label}】`);
  23. out('='.repeat(80));
  24. // === 规模 ===
  25. out('\n▌规模');
  26. out(` XHS: ${A.metrics.notes}笔记 / ${A.metrics.sampleComments}评论 / ${A.kols?.length || 0}作者`);
  27. out(` 互动: 点赞${A.metrics.totalLikes} 评论${A.metrics.totalComments} 收藏${A.metrics.totalCollected} 分享${A.metrics.totalShared}`);
  28. if (A.amazon && !A.amazon.empty) {
  29. out(` Amazon: ${A.amazon.keywords?.length || 0}关键词 × ${A.amazon.total || 0}款产品`);
  30. }
  31. // === 小红书 Top 原声 (VOC 真实语料) ===
  32. if (A.topComments && A.topComments.length) {
  33. out('\n▌小红书 Top 高赞原声');
  34. A.topComments.slice(0, 15).forEach((c, i) => {
  35. const content = (c.content || '').replace(/\s+/g, ' ').slice(0, 140);
  36. out(` ${String(i + 1).padStart(2, '0')}. [${c.like_count || 0}赞] ${content}`);
  37. });
  38. }
  39. // === Top Notes(爆款笔记标题 → 用户内容倾向)===
  40. if (A.topNotes && A.topNotes.length) {
  41. out('\n▌爆款笔记 Top 10 标题(揭示用户内容兴趣)');
  42. A.topNotes.slice(0, 10).forEach((n, i) => {
  43. const title = (n.title || n.display_title || '').replace(/\s+/g, ' ').slice(0, 80);
  44. out(` ${String(i + 1).padStart(2, '0')}. [${n.liked_count || 0}赞] ${title}`);
  45. });
  46. }
  47. // === Amazon 分析层 ===
  48. if (A.amazon && !A.amazon.empty) {
  49. // 主题:英文 Amazon reviews 的 VOC 痛点/亮点/场景
  50. const th = A.amazon.amazonThemes;
  51. if (th) {
  52. if (th.pain && th.pain.length) {
  53. out('\n▌Amazon 英文评论 · 痛点主题 (海外用户差评)');
  54. th.pain.slice(0, 8).forEach((t, i) => {
  55. out(` ${i + 1}. ${t.tag} (提及 ${t.count} 次 · ${t.brandCoverage}品牌)`);
  56. });
  57. }
  58. if (th.bright && th.bright.length) {
  59. out('\n▌Amazon 英文评论 · 亮点主题 (海外用户好评)');
  60. th.bright.slice(0, 8).forEach((t, i) => {
  61. out(` ${i + 1}. ${t.tag} (提及 ${t.count} 次 · ${t.brandCoverage}品牌)`);
  62. });
  63. }
  64. if (th.scenarios && th.scenarios.length) {
  65. out('\n▌Amazon 英文评论 · 使用场景');
  66. th.scenarios.slice(0, 6).forEach((t, i) => {
  67. out(` ${i + 1}. ${t.tag} (${t.count} 次)`);
  68. });
  69. }
  70. }
  71. // 品类机会评估
  72. if (A.amazon.opportunity) {
  73. const op = A.amazon.opportunity;
  74. out('\n▌品类机会评估');
  75. out(` 等级: ${op.grade} (${op.totalScore}/${op.maxScore}) · 甜点价位: ${op.sweetSpot}`);
  76. if (op.dimensions) {
  77. op.dimensions.forEach((d) => out(` · ${d.name}: ${d.level} — ${(d.note || '').replace(/<[^>]+>/g, '').slice(0, 100)}`));
  78. }
  79. }
  80. // 竞品档案(4 家)
  81. if (A.amazon.competitorDossiers) {
  82. out('\n▌竞品档案 (Top 4)');
  83. A.amazon.competitorDossiers.slice(0, 4).forEach((d, i) => {
  84. out(` ${i + 1}. ${d.brand} · ${d.positioning} · ${d.share?.toFixed(1)}%份额 · 均价$${d.avgPrice?.toFixed(0)} · ★${d.avgRating?.toFixed(2)}`);
  85. if (d.topProduct) out(` Top产品: ${(d.topProduct.title || '').slice(0, 80)}`);
  86. if (d.strengths?.length) out(` 优势: ${d.strengths.slice(0, 3).map(s => `${s.label}×${s.n}`).join(' / ')}`);
  87. if (d.gaps?.length) out(` 缝隙: ${d.gaps.slice(0, 3).map(g => `${g.label}×${g.n}`).join(' / ')}`);
  88. });
  89. }
  90. // 深度产品详情(已有卖点)
  91. if (A.amazon.detailProducts) {
  92. out('\n▌Amazon Top 6 深度产品 (成分卖点参考)');
  93. A.amazon.detailProducts.slice(0, 6).forEach((prod, i) => {
  94. const title = (prod.title || '').slice(0, 100);
  95. const price = prod.price ? (prod.price / 100).toFixed(2) : '-';
  96. out(` ${i + 1}. [${prod.brand || '-'}] ${title}`);
  97. out(` $${price} · ★${prod.rating || '-'} · ${prod.ratingsCount || prod.reviewCount || 0} 评`);
  98. });
  99. }
  100. // 关键发现
  101. if (A.amazon.keyFindings) {
  102. out('\n▌关键发现');
  103. A.amazon.keyFindings.slice(0, 5).forEach((f, i) => {
  104. out(` ${i + 1}. ${f.title}`);
  105. out(` ${(f.body || '').replace(/<[^>]+>/g, '').slice(0, 200)}`);
  106. });
  107. }
  108. // 战略建议
  109. if (A.amazon.strategy) {
  110. const s = A.amazon.strategy;
  111. out('\n▌战略建议 (短/中/长)');
  112. ['short', 'mid', 'long'].forEach((phase) => {
  113. if (s[phase]?.items) {
  114. out(` ${phase.toUpperCase()}:`);
  115. s[phase].items.forEach((it) => out(` · ${it.replace(/<[^>]+>/g, '').slice(0, 140)}`));
  116. }
  117. });
  118. }
  119. }
  120. // === TikTok hashtag 网络 (Amazon 内容端) ===
  121. if (A.tiktok && !A.tiktok.empty) {
  122. out('\n▌TikTok 海外内容端');
  123. if (A.tiktok.topHashtags) {
  124. out(` Top 20 Hashtag: ${A.tiktok.topHashtags.slice(0, 20).map(h => '#' + (h.tag || h[0])).join(' ')}`);
  125. }
  126. if (A.tiktok.topComments) {
  127. out(' Top 8 海外用户原声:');
  128. A.tiktok.topComments.slice(0, 8).forEach((c, i) => {
  129. const text = (c.text || '').replace(/\s+/g, ' ').slice(0, 140);
  130. out(` ${i + 1}. [${c.digg_count || 0}赞] ${text}`);
  131. });
  132. }
  133. }
  134. // === TikTok Ads 关键词洞察 ===
  135. if (A.marketIntel && !A.marketIntel.empty && A.marketIntel.ttAds?.hasData) {
  136. const mi = A.marketIntel.ttAds;
  137. out('\n▌TikTok Ads 关键词洞察 (广告主视角)');
  138. const keys = Object.keys(mi).filter(k => !['hasData'].includes(k));
  139. out(` (可用字段: ${keys.join(', ')})`);
  140. if (mi.byProduct) {
  141. Object.entries(mi.byProduct).forEach(([prod, data]) => {
  142. out(` 产品: ${prod}`);
  143. (data.insights || []).forEach((ins) => {
  144. out(` · keyword="${ins.keyword}" | imp ${ins.impression || 0} | ctr ${ins.ctr ? (ins.ctr*100).toFixed(2)+'%' : '-'} | cvr ${ins.cvr ? (ins.cvr*100).toFixed(2)+'%' : '-'} | post ${ins.post_count || 0} (${ins.post_change ? (ins.post_change>0?'+':'')+(ins.post_change*100).toFixed(0)+'%' : '-'})`);
  145. });
  146. });
  147. }
  148. }
  149. // === Douyin 站级热榜基线 ===
  150. if (A.marketIntel && !A.marketIntel.empty && A.marketIntel.douyin) {
  151. const dy = A.marketIntel.douyin;
  152. out('\n▌Douyin 站级基线');
  153. if (dy.hotWords?.length) out(` 热词: ${dy.hotWords.slice(0,6).map(w => w.word || w[0]).join(' / ')}`);
  154. if (dy.hotChallenges?.length) out(` 热挑战: ${dy.hotChallenges.slice(0,5).map(c => c.title || c[0]).join(' / ')}`);
  155. if (dy.categoryFilterHit !== undefined) out(` 品类过滤命中: ${dy.categoryFilterHit} (${dy.categoryFilterHit === 0 ? '品牌蓝海' : '已有内容'})`);
  156. }
  157. // === XHS 痛点/亮点/场景 主题(来自 voc-analyze 的 extractThemes)===
  158. // 主报告里的 themesSection 是基于 xhs comments 做的
  159. // 让我直接从 voc.xiaohongshu.by_keyword[*].comments 提取
  160. const xhsComments = [];
  161. const byKw = voc.xiaohongshu?.by_keyword || {};
  162. Object.values(byKw).forEach((kwData) => {
  163. // comments 是 { noteId: [comment...] } 的对象结构
  164. const cmap = kwData.comments || {};
  165. if (Array.isArray(cmap)) {
  166. cmap.forEach((c) => { if (c && c.content) xhsComments.push(c.content); });
  167. } else {
  168. Object.values(cmap).forEach((arr) => {
  169. (arr || []).forEach((c) => { if (c && c.content) xhsComments.push(c.content); });
  170. });
  171. }
  172. });
  173. if (xhsComments.length) {
  174. const { extractThemes } = require('./voc-analyze.js');
  175. if (extractThemes) {
  176. const xhsThemes = extractThemes(xhsComments);
  177. if (xhsThemes) {
  178. out('\n▌小红书中文 VOC 主题');
  179. ['pain', 'bright', 'scenarios'].forEach((key) => {
  180. const arr = xhsThemes[key];
  181. if (arr && arr.length) {
  182. out(` ${key === 'pain' ? '痛点' : key === 'bright' ? '亮点' : '场景'}:`);
  183. arr.slice(0, 8).forEach((t, i) => out(` ${i + 1}. ${t.tag || t.label} (${t.count || t.freq || 0})`));
  184. }
  185. });
  186. }
  187. }
  188. }
  189. }
  190. const outPath = 'data/voc-summary-for-product-dev.txt';
  191. fs.writeFileSync(outPath, buffer.join('\n'), 'utf-8');
  192. console.log(`\n✅ ${outPath} (${(fs.statSync(outPath).size / 1024).toFixed(1)}KB)`);