#!/usr/bin/env node // ============================================================================== // <品类名> · VOC 质量审计脚本(模板) // ============================================================================== // 检查: // 1. 各章节核心 VOC 证据主题一致性 // 2. H1-H8 假设覆盖(应每条 ≥ 100) // 3. 重复内容(前 40 字相同) // 4. 高赞跳题(无相关词 & likes > 100) // 5. HTML 产出质量(大小 / sections / Coming Soon) // 6. VOC 证据卡数量 // 7. 决策列表数量 // 8. 空 section 检查 // 9. 封面数字与 merged 一致性 // ============================================================================== const fs = require('fs'); const path = require('path'); const CATEGORY = '<品类>'; // TODO const REPORT_HTML = path.resolve(__dirname, '..', '..', 'reports', `<品类>-voc-insight-report.html`); const LA = require('./<品类>-analyze.js'); const data = LA.loadMerged(); const items = data.items; // 通用 dump 工具 function dump(title, arr, maxShow = 5) { console.log('\n' + '─'.repeat(60)); console.log(`【${title}】`); console.log('─'.repeat(60)); arr.slice(0, maxShow).forEach((it, i) => { console.log(` [${i + 1}] ${(it.platform || '').toUpperCase()} · ${it.keyword} · ${it.nickname} · ♥${it.likes}`); console.log(` 「${String(it.content || '').slice(0, 160)}」`); }); } console.log('=========================================='); console.log(`<品类名> · VOC 质量审计`); console.log(`总条数:${items.length} / 平台:${Object.keys(data.meta?.platforms || {}).join('+')}`); console.log('=========================================='); // ========================================================== // 1. 各章节核心 VOC 抽查 // TODO: 按实际章节填写探针 // ========================================================== const CH_PROBES = [ { ch: 'Ch1.1 <章节主题>', opts: { keyword: '<本品名>', contentMatch: /<相关词>/, top: 3 }, }, { ch: 'Ch2.1 <竞品主题>', opts: { keyword: '<竞品名>', contentMatch: /<场景词>/, top: 3 }, }, // ... 每章至少 1-2 项探针 ]; console.log('\n▶ 章节 VOC 证据一致性抽查'); for (const p of CH_PROBES) { const hit = LA.getEvidence(items, p.opts); const ok = hit.length >= 3; console.log(` ${ok ? '✓' : '⚠️'} ${p.ch}: ${hit.length} 条 ${ok ? '' : '← 证据不足'}`); } // ========================================================== // 2. H1-H8 假设覆盖 // ========================================================== console.log('\n▶ H1-H8 假设覆盖检查'); for (let i = 1; i <= 8; i++) { const n = items.filter((it) => (it.hypotheses || []).includes('H' + i)).length; const status = n >= 100 ? '✓' : n >= 50 ? '⚠️' : '❌'; console.log(` H${i}: ${String(n).padStart(5)} 条 ${status} ${n < 100 ? '(建议补采)' : ''}`); } // ========================================================== // 3. 重复检查(前 40 字相同) // ========================================================== const bucket = new Map(); for (const it of items) { const key = String(it.content || '').slice(0, 40); if (!bucket.has(key)) bucket.set(key, []); bucket.get(key).push(it); } const dupGroups = [...bucket.values()].filter((g) => g.length > 1).sort((a, b) => b.length - a.length); console.log(`\n▶ 重复组 ${dupGroups.length} 组(前 5):`); dupGroups.slice(0, 5).forEach((g, i) => { console.log(` [${i + 1}] ${g.length} 条 · 「${String(g[0].content || '').slice(0, 60)}」`); console.log(` kw: ${[...new Set(g.map((x) => x.keyword))].slice(0, 5).join(', ')}`); }); // ========================================================== // 4. 跳题检查(高赞 & 无相关词) // TODO: 修改 relevantRe 为品类相关正则 // ========================================================== const relevantRe = /<品类词 1>|<品类词 2>|<人群词>|<场景词>/; const offtopic = items.filter((it) => { const c = it.content || ''; if (c.length < 15) return false; const hasRelevant = relevantRe.test(c); return !hasRelevant && (it.likes || 0) >= 100; }); console.log(`\n▶ 高赞跳题 VOC(likes ≥ 100 且无相关词)${offtopic.length} 条(前 10):`); offtopic.sort((a, b) => (b.likes || 0) - (a.likes || 0)).slice(0, 10).forEach((it, i) => { console.log(` [${i + 1}] ♥${it.likes} · ${it.keyword} · ${it.nickname}: ${String(it.content || '').slice(0, 100)}`); }); // ========================================================== // 5-9. HTML 产出质量检查 // ========================================================== console.log('\n▶ HTML 产出质量'); if (!fs.existsSync(REPORT_HTML)) { console.log(` ❌ HTML 不存在: ${REPORT_HTML}`); console.log(` 请先跑 node scripts/tools/gen-<品类>.js`); } else { const html = fs.readFileSync(REPORT_HTML, 'utf8'); const sizeKB = (html.length / 1024).toFixed(1); const sections = (html.match(/
]*>\s*
\s*<\/div>\s*<\/section>/g) || []).length; console.log(` 大小: ${sizeKB} KB ${sizeKB < 300 ? '❌ 过小' : sizeKB > 1000 ? '⚠️ 过大' : '✓'}`); console.log(` sections: ${sections}`); console.log(` Coming Soon: ${comingSoon} ${comingSoon > 0 ? '❌ 应为 0' : '✓'}`); console.log(` VOC 证据卡: ${vocCards} ${vocCards < 60 ? '❌ 过少' : vocCards < 100 ? '⚠️' : '✓'}`); console.log(` 决策列表: ${decisionLists} ${decisionLists < 20 ? '⚠️ 过少' : '✓'}`); console.log(` 空 section: ${emptySection} ${emptySection > 0 ? '❌' : '✓'}`); // 9. 封面数字 vs merged const coverMatch = html.match(/(\d{3,5})\s*条真实 VOC/); if (coverMatch && Math.abs(parseInt(coverMatch[1]) - items.length) > 10) { console.log(` ⚠️ 封面 ${coverMatch[1]} 条 ≠ merged ${items.length} 条`); } } // ========================================================== // 总结 // ========================================================== const errs = (dupGroups.filter((g) => g.length > 10).length) + (offtopic.length > 50 ? 1 : 0); console.log('\n=========================================='); if (errs === 0) { console.log('✅ 审计完成 · 未发现严重问题'); } else { console.log(`⚠️ 审计完成 · 发现 ${errs} 项红灯,建议处理后再交付`); } console.log('==========================================');