| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- #!/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(/<section class="report-section"/g) || []).length;
- const comingSoon = (html.match(/Coming in Batch|COMING IN BATCH/gi) || []).length;
- const vocCards = (html.match(/border-left:3px solid/g) || []).length;
- const decisionLists = (html.match(/落地决策|🎯/g) || []).length;
- const emptySection = (html.match(/<section[^>]*>\s*<div class="section-inner">\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('==========================================');
|