| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const ROOT = path.resolve(__dirname, '..');
- const DEFAULT_TRANSCRIPTS_DIR = path.resolve(ROOT, '..', 'docs', '提号经验');
- const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'experience-transcript-index-latest');
- const RULES = [
- {
- id: 'reference-type-tone',
- title: '参考账号拆类型和调性',
- terms: ['参考账号', '类型', '调性'],
- practicalRule: '参考账号只能拆成账号类型、内容调性、场景和表达方式等锚点;多参考账号冲突时不能强行合并成单一标准。',
- usableFor: '校准 referenceUsability、referenceStyleFingerprints 和强推荐解释。'
- },
- {
- id: 'xhs-pgy-similar-chain',
- title: '小红书/蒲公英相似账号链路',
- terms: ['小红书', '蒲公英', '相似', '推荐'],
- practicalRule: '小红书和蒲公英优先用参考账号相似推荐链路,再按类型和调性二次筛选。',
- usableFor: '校准小红书召回路径和平台策略。'
- },
- {
- id: 'douyin-xingtu-fallback',
- title: '抖音/星图只作入口或兜底',
- terms: ['抖音', '星图', '招募', '关键词'],
- practicalRule: '抖音相似推荐和星图标签不能直接当适配证明;找不到时用招募、APP 关键词或人工补找兜底。',
- usableFor: '校准抖音召回路径和低置信复核边界。'
- },
- {
- id: 'homepage-recent-posts',
- title: '主页近 10/20 篇优先判断',
- terms: ['主页', '前十', '10', '20', '封面', '首图'],
- practicalRule: '人工通常先扫主页近 10 篇左右封面/首图,必要时扩到 20 篇或点进图文/视频深看。',
- usableFor: '校准 homepage evidence readiness、recentContentFitScore、visualQualityScore 和 toneConsistencyScore。'
- },
- {
- id: 'quant-metric-first-filter',
- title: '先用量化指标做第一轮过滤',
- terms: ['CPE', 'CP1', 'CPM', '粉丝', '预算', '城市', '女粉'],
- practicalRule: '先用关键词、预算、CPE/CP1/CPM、粉丝量、男女粉占比和城市等硬指标缩小候选,再做主页风格复核。',
- usableFor: '校准 parseBrief.performanceRequirements、requirementLayers.hardConstraints 和 live 小规模召回参数。'
- },
- {
- id: 'visual-tone-quality',
- title: '视觉质感和调性是核心难点',
- terms: ['质感', '画面清晰', '像素', '排版', '下沉', '风格'],
- practicalRule: '提号难点不只是数据维度,而是账号风格、画面质感、内容垂类和品牌调性是否匹配。',
- usableFor: '校准强推荐门槛、风险提示和人工复核标签。'
- },
- {
- id: 'price-visual-threshold',
- title: '报价 300 元以上提高视觉质感要求',
- terms: ['300', '三百', '画面清晰', '背景', '封面统一', '精致'],
- practicalRule: '低价账号可相对放宽,但报价 300 元以上应优先选择封面清晰、背景统一、画面精致、风格稳定的账号。',
- usableFor: '校准 visualQualityScore、homepageQualityRisks、manualReviewFields 和强推荐门槛。'
- },
- {
- id: 'recent-data-authenticity',
- title: '近期数据和评论真实性复核',
- terms: ['近一个月', '近30天', '平均点赞', '百赞', '重复评论', '同一个人评论', '数据真实性', '停更'],
- practicalRule: '复核近一个月是否更新、近作平均点赞是否过低,以及多篇笔记是否出现同一批评论者;疑似刷评或停更不要强推。',
- usableFor: '校准 homepageEvidence.dataRiskSignals、dataQualityScore、riskNote 和人工复核标签。'
- },
- {
- id: 'implicit-product-audience',
- title: '产品隐性人群规则',
- terms: ['脱毛仪', '美容仪', '女性', '男性账号', '受众'],
- practicalRule: '美容仪、脱毛仪等产品即使 Brief 未写明,也要默认复核女性使用场景和女性博主语境。',
- usableFor: '校准品类隐性规则和硬性降级。'
- },
- {
- id: 'multi-input-brief-product-chat',
- title: 'Brief + 产品 + 聊天记录多输入解析',
- terms: ['Brief', '产品', '聊天记录', '客户聊天', '参考账号', '客户说'],
- practicalRule: '最佳输入不只是 Brief,还应合并产品介绍、参考账号和客户聊天记录;聊天里常有隐藏偏好、禁区和子类方向。',
- usableFor: '校准 parseBrief.contextSources、productIntro/chatRecords schema 和下一轮校准问题。'
- },
- {
- id: 'category-subtype-refinement',
- title: '宽类目必须拆到可执行子类型',
- terms: ['时尚穿搭', '港风', '成熟风', '美妆', '护肤', '妆教', '测评', '产品合集'],
- practicalRule: '精致生活、时尚穿搭、美妆护肤等宽类目不能只按平台标签筛,必须结合产品和参考账号拆到港风/成熟风、护肤/彩妆/妆教/测评等子方向。',
- usableFor: '校准 inferCategory、关键词扩展、categoryRules 和召回关键词。'
- },
- {
- id: 'video-deep-check',
- title: '封面不确定再深看视频内容',
- terms: ['视频', '内容', '点进去', '每一帧', '解析'],
- practicalRule: '封面或主页判断不确定时,再进入图文正文、视频内容、ASR 或帧图分析;没有真实资源时只能标待补证。',
- usableFor: '校准视频资源收集包和视频 A/B 前置门禁。'
- },
- {
- id: 'customer-effect-goal',
- title: '最终目标是客户选中率和效率',
- terms: ['客户', '选中率', '降本增效', '补号', '效果'],
- practicalRule: '提号优化最终要用客户选中、拒绝归因和人工补号量变化证明;经验判断本身不能替代客户效果审计。',
- usableFor: '校准 proof-gap、customer-effect:audit 和禁止误宣称边界。'
- }
- ];
- function main() {
- const args = parseArgs(process.argv.slice(2));
- const transcriptsDir = path.resolve(args.transcriptsDir || DEFAULT_TRANSCRIPTS_DIR);
- const outputDir = path.resolve(args.output || DEFAULT_OUTPUT);
- const summary = buildExperienceTranscriptIndex({ root: ROOT, transcriptsDir });
- fs.mkdirSync(outputDir, { recursive: true });
- const summaryPath = path.join(outputDir, 'experience-transcript-index-summary.json');
- const reportPath = path.join(outputDir, 'experience-transcript-index.md');
- const csvPath = path.join(outputDir, 'experience-transcript-index.csv');
- fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8');
- fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
- fs.writeFileSync(csvPath, withBom(renderCsv(summary)), 'utf8');
- console.log(JSON.stringify({
- outputDir,
- summary: summaryPath,
- report: reportPath,
- csv: csvPath,
- transcriptCount: summary.transcriptCount,
- coveredRuleCount: summary.coveredRuleCount,
- canCloseProofGap: summary.canCloseProofGap
- }, null, 2));
- if (args.strict && summary.directCustomerProof !== false) process.exitCode = 1;
- }
- function buildExperienceTranscriptIndex({ root, transcriptsDir }) {
- const transcripts = inspectTranscripts({ root, transcriptsDir });
- const coverage = RULES.map(rule => inspectRuleCoverage(rule, transcripts));
- const coveredRuleCount = coverage.filter(item => item.status === 'covered').length;
- const transcriptCount = transcripts.filter(item => item.exists).length;
- return {
- generatedAt: new Date().toISOString(),
- root,
- transcriptsDir,
- proofLevel: 'not_business_proof',
- materialType: 'experience_seed',
- directCustomerProof: false,
- canCloseProofGap: false,
- complete: false,
- transcriptCount,
- missingTranscriptCount: transcripts.filter(item => !item.exists).length,
- ruleCount: RULES.length,
- coveredRuleCount,
- transcripts,
- ruleCoverage: coverage,
- guardrails: [
- '逐字稿只证明团队经验来源可追踪,不能证明提号率提升、客户效果达标或人工补号量下降。',
- '逐字稿里的经验规则必须进入真实候选、真实复核和客户效果审计后,才可能参与业务证明。',
- '不要把经验判断、样例项目、PoC 推荐名单或本索引本身当作 customer-effect:audit 证据。',
- '本索引只输出规则覆盖、文件路径和行号,不输出原文长段内容,避免带出敏感信息或噪声。'
- ],
- nextActions: [
- {
- owner: '技术/AI',
- title: '对照规则实现',
- action: '把 ruleCoverage 与 experience-rules、ranker、homepage readiness、video readiness 和 skill references 对齐。',
- acceptance: '规则实现继续覆盖参考账号、主页近作、品类隐性规则、抖音兜底和视频待补证边界。'
- },
- {
- owner: '商务/投放',
- title: '把经验种子转成真实复核数据',
- action: '用逐字稿经验指导真实历史 Brief、候选视频、人工复核标签、客户选择和拒绝归因填写。',
- acceptance: 'intake:readiness failureCount=0,history:audit 与 customer-effect:audit 通过。'
- }
- ]
- };
- }
- function inspectTranscripts({ root, transcriptsDir }) {
- if (!fs.existsSync(transcriptsDir)) {
- return [{
- id: 'transcripts-dir',
- title: '提号经验逐字稿目录',
- path: rel(root, transcriptsDir),
- exists: false,
- proofLevel: 'not_business_proof',
- directCustomerProof: false,
- canCloseProofGap: false,
- lineCount: 0,
- matchedRuleIds: [],
- status: 'missing',
- summary: '未发现逐字稿目录;不影响真实业务证明缺口判断。'
- }];
- }
- return fs.readdirSync(transcriptsDir, { withFileTypes: true })
- .filter(entry => entry.isFile() && entry.name.endsWith('.md'))
- .sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
- .map(entry => inspectTranscript({ root, file: path.join(transcriptsDir, entry.name) }));
- }
- function inspectTranscript({ root, file }) {
- const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
- const lines = text.split(/\r?\n/);
- const matchedRuleIds = RULES
- .filter(rule => findRuleMatches(lines, rule).length > 0)
- .map(rule => rule.id);
- return {
- id: slug(path.basename(file, '.md')),
- title: path.basename(file, '.md'),
- path: rel(root, file),
- exists: true,
- proofLevel: 'not_business_proof',
- materialType: 'experience_seed',
- directCustomerProof: false,
- canCloseProofGap: false,
- lineCount: lines.length,
- matchedRuleIds,
- status: 'available_experience_seed',
- summary: `覆盖经验规则 ${matchedRuleIds.length}/${RULES.length} 条;只能作为规则来源和填表提示,不是业务证明。`
- };
- }
- function inspectRuleCoverage(rule, transcripts) {
- const sources = transcripts
- .filter(item => item.exists)
- .map(item => {
- const file = path.resolve(ROOT, item.path);
- const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
- const lines = text.split(/\r?\n/);
- const matches = findRuleMatches(lines, rule);
- return {
- transcriptId: item.id,
- path: item.path,
- hitCount: matches.length,
- lineNumbers: matches.slice(0, 12).map(match => match.lineNumber)
- };
- })
- .filter(item => item.hitCount > 0);
- const hitCount = sources.reduce((sum, item) => sum + item.hitCount, 0);
- return {
- id: rule.id,
- title: rule.title,
- status: hitCount > 0 ? 'covered' : 'not_found',
- proofLevel: 'not_business_proof',
- directCustomerProof: false,
- canCloseProofGap: false,
- hitCount,
- practicalRule: rule.practicalRule,
- usableFor: rule.usableFor,
- sources,
- missingForProof: [
- '真实历史 Brief 数据集',
- '真实候选视频资源',
- '商务复核标签和客户选择',
- 'customer-effect:audit 通过'
- ]
- };
- }
- function findRuleMatches(lines, rule) {
- return lines.flatMap((line, index) => {
- const text = String(line || '');
- const hitTerms = rule.terms.filter(term => text.includes(term));
- return hitTerms.length ? [{ lineNumber: index + 1, hitTerms }] : [];
- });
- }
- function renderReport(summary) {
- return [
- '# 提号经验逐字稿索引',
- '',
- `- 生成时间:${summary.generatedAt}`,
- `- proofLevel:${summary.proofLevel}`,
- `- materialType:${summary.materialType}`,
- `- directCustomerProof:${summary.directCustomerProof}`,
- `- canCloseProofGap:${summary.canCloseProofGap}`,
- `- 逐字稿:${summary.transcriptCount}`,
- `- 覆盖经验规则:${summary.coveredRuleCount}/${summary.ruleCount}`,
- '',
- '## 边界',
- '',
- ...summary.guardrails.map(item => `- ${item}`),
- '',
- '## 逐字稿',
- '',
- '| ID | 状态 | 路径 | 行数 | 覆盖规则 | 摘要 |',
- '| --- | --- | --- | ---: | --- | --- |',
- ...summary.transcripts.map(item => tableRow([
- item.id,
- item.status,
- item.path,
- item.lineCount,
- item.matchedRuleIds.join(';'),
- item.summary
- ])),
- '',
- '## 经验规则覆盖',
- '',
- '| 规则 | 状态 | 命中数 | 可用于 | 来源行号 | 缺失证明 |',
- '| --- | --- | ---: | --- | --- | --- |',
- ...summary.ruleCoverage.map(item => tableRow([
- item.title,
- item.status,
- item.hitCount,
- item.usableFor,
- item.sources.map(source => `${source.path}:${source.lineNumbers.join('/')}`).join(';'),
- item.missingForProof.join(';')
- ])),
- '',
- '## 下一步',
- '',
- '| 负责人 | 动作 | 验收 |',
- '| --- | --- | --- |',
- ...summary.nextActions.map(item => tableRow([item.owner, `${item.title}:${item.action}`, item.acceptance])),
- ''
- ].join('\n');
- }
- function renderCsv(summary) {
- const rows = [
- ['类型', 'ID', '状态', 'proofLevel', 'directCustomerProof', 'canCloseProofGap', '路径/来源', '摘要/规则', '缺失证明'],
- ...summary.transcripts.map(item => [
- 'experience-transcript',
- item.id,
- item.status,
- item.proofLevel,
- item.directCustomerProof,
- item.canCloseProofGap,
- item.path,
- item.summary,
- '客户选择;人工补号量;customer-effect:audit'
- ]),
- ...summary.ruleCoverage.map(item => [
- 'experience-rule',
- item.id,
- item.status,
- item.proofLevel,
- item.directCustomerProof,
- item.canCloseProofGap,
- item.sources.map(source => `${source.path}:${source.lineNumbers.join('/')}`).join(';'),
- item.practicalRule,
- item.missingForProof.join(';')
- ])
- ];
- return rows.map(row => row.map(csvCell).join(',')).join('\n');
- }
- function parseArgs(argv) {
- const result = {};
- for (let index = 0; index < argv.length; index += 1) {
- const arg = argv[index];
- if (!arg.startsWith('--')) continue;
- const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
- const next = argv[index + 1];
- if (!next || next.startsWith('--')) result[key] = true;
- else {
- result[key] = next;
- index += 1;
- }
- }
- return result;
- }
- function slug(value) {
- return String(value || '')
- .toLowerCase()
- .replace(/[^\p{L}\p{N}]+/gu, '-')
- .replace(/^-+|-+$/g, '') || 'transcript';
- }
- function rel(root, file) {
- const relative = path.relative(root, file).replace(/\\/g, '/');
- return relative || '.';
- }
- function withBom(text) {
- return `\uFEFF${text}`;
- }
- function tableRow(values) {
- return `| ${values.map(value => escapeCell(value)).join(' | ')} |`;
- }
- function escapeCell(value) {
- return String(value ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
- }
- function csvCell(value) {
- return `"${String(value ?? '').replace(/"/g, '""')}"`;
- }
- if (require.main === module) main();
- module.exports = {
- buildExperienceTranscriptIndex,
- inspectRuleCoverage,
- RULES
- };
|