| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const ROOT = path.resolve(__dirname, '..');
- const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'manual-review-label-guide-latest');
- const LABEL_ROWS = [
- {
- label: '可直接发客户',
- category: 'positive',
- whenToUse: '候选人符合 Brief 硬性条件、内容调性、主页证据和风险边界,商务可直接放入客户名单。',
- requiresAttribution: false,
- requiresFeedbackReason: false,
- blocksReviewMetricsStrict: false,
- blocksCustomerEffectAudit: false
- },
- {
- label: '商务复核',
- category: 'positive',
- whenToUse: '候选人大体匹配,但报价、档期、竞品合作、近期内容或平台风险仍需商务确认。',
- requiresAttribution: false,
- requiresFeedbackReason: false,
- blocksReviewMetricsStrict: false,
- blocksCustomerEffectAudit: false
- },
- {
- label: '跑偏',
- category: 'negative',
- whenToUse: '人群、品类、平台、预算、内容方向或合作场景明显偏离 Brief。',
- requiresAttribution: true,
- requiresFeedbackReason: true,
- blocksReviewMetricsStrict: true,
- blocksCustomerEffectAudit: true
- },
- {
- label: '硬性规则违约',
- category: 'negative',
- whenToUse: '命中必须剔除项,例如平台不符、预算超限、地域/粉丝量/类目硬约束不符、客户明确排除。',
- requiresAttribution: true,
- requiresFeedbackReason: true,
- blocksReviewMetricsStrict: true,
- blocksCustomerEffectAudit: true
- },
- {
- label: '调性不符',
- category: 'negative',
- whenToUse: '硬指标可用,但内容表达、视觉质感、语气、人设或参考账号风格明显不一致。',
- requiresAttribution: true,
- requiresFeedbackReason: true,
- blocksReviewMetricsStrict: true,
- blocksCustomerEffectAudit: true
- },
- {
- label: '主页质感不符',
- category: 'negative',
- whenToUse: '主页最近内容质量、封面、互动、更新频率、商业化痕迹或风险信号不支持强推荐。',
- requiresAttribution: true,
- requiresFeedbackReason: true,
- blocksReviewMetricsStrict: true,
- blocksCustomerEffectAudit: true
- },
- {
- label: '参考账号不像',
- category: 'negative',
- whenToUse: '客户给了参考账号或参考视频,但候选人与参考账号的人群、内容结构或风格相似度不足。',
- requiresAttribution: true,
- requiresFeedbackReason: true,
- blocksReviewMetricsStrict: true,
- blocksCustomerEffectAudit: true
- },
- {
- label: '待客户反馈',
- category: 'pending',
- whenToUse: '商务已发客户或准备发客户,但客户尚未给最终选择/拒绝结果。',
- requiresAttribution: false,
- requiresFeedbackReason: false,
- blocksReviewMetricsStrict: false,
- blocksCustomerEffectAudit: true
- },
- {
- label: '客户选中',
- category: 'customer_result',
- whenToUse: '客户最终确认选择该候选人,可用于 customer-effect:audit 的客户选中率统计。',
- requiresAttribution: false,
- requiresFeedbackReason: false,
- blocksReviewMetricsStrict: false,
- blocksCustomerEffectAudit: false
- },
- {
- label: '客户拒绝',
- category: 'customer_result',
- whenToUse: '客户最终拒绝该候选人,必须补拒绝原因或反馈原因,用于下一轮优化。',
- requiresAttribution: true,
- requiresFeedbackReason: true,
- blocksReviewMetricsStrict: false,
- blocksCustomerEffectAudit: true
- }
- ];
- const ATTRIBUTION_ROWS = [
- ['需求解析错', 'Brief 里的品类、人群、预算、平台、地域或硬性限制被解析错。'],
- ['隐性规则漏', '客户偏好、历史排除项、风格禁区或团队经验规则没有进入本轮筛选。'],
- ['召回关键词错', '关键词召回方向太宽、太窄或平台搜索词不贴合真实场景。'],
- ['主页证据不足', '主页最近内容、封面、标题、互动、发布时间或风险证据不足。'],
- ['视频证据误判', '参考视频或候选视频风格、场景、口播、画面质感、ASR 被误判。'],
- ['排序权重错', '候选已召回但排序把弱相关、低质感或高风险账号排得过高。'],
- ['输出解释错', '推荐理由、风险提示或证据卡表达不清,导致商务难以判断。'],
- ['软件端表重复或排名不连续', '同一候选重复出现、重复键异常、排名断档或表格字段不稳定。']
- ];
- function main() {
- const args = parseArgs(process.argv.slice(2));
- const outputDir = path.resolve(args.output || DEFAULT_OUTPUT);
- const summary = buildGuideSummary();
- fs.mkdirSync(outputDir, { recursive: true });
- const summaryPath = path.join(outputDir, 'manual-review-label-guide-summary.json');
- const reportPath = path.join(outputDir, 'manual-review-label-guide.md');
- const csvPath = path.join(outputDir, 'manual-review-label-guide.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,
- passed: summary.passed,
- labelCount: summary.labelCount,
- attributionTypeCount: summary.attributionTypeCount
- }, null, 2));
- if (args.strict && !summary.passed) process.exitCode = 1;
- }
- function buildGuideSummary() {
- const negativeLabels = LABEL_ROWS.filter(item => item.category === 'negative' || item.label === '客户拒绝');
- const positiveLabels = LABEL_ROWS.filter(item => item.category === 'positive');
- const pendingLabels = LABEL_ROWS.filter(item => item.category === 'pending');
- const negativeLabelsMissingAttributionRule = negativeLabels.filter(item => !item.requiresAttribution);
- const rows = LABEL_ROWS.map((item, index) => ({
- id: `MRL-${String(index + 1).padStart(3, '0')}`,
- ...item,
- customerEffectBoundary: item.blocksCustomerEffectAudit
- ? '缺少客户最终选择、反馈原因或负样本归因时,不得进入客户效果证明。'
- : '只能作为复核/客户结果字段使用,仍需 customer-effect:audit 通过后才能宣称客户效果。'
- }));
- const attributionTypes = ATTRIBUTION_ROWS.map(([type, usage], index) => ({
- id: `ATR-${String(index + 1).padStart(3, '0')}`,
- type,
- usage,
- appliesTo: '负样本、客户拒绝、跑偏或调性不符候选'
- }));
- const summary = {
- generatedAt: new Date().toISOString(),
- directCustomerProof: false,
- proofLevel: 'smoke_or_local',
- passed: negativeLabelsMissingAttributionRule.length === 0 &&
- positiveLabels.length >= 2 &&
- negativeLabels.length >= 5 &&
- attributionTypes.length >= 8,
- labelCount: rows.length,
- positiveLabelCount: positiveLabels.length,
- negativeLabelCount: negativeLabels.length,
- pendingLabelCount: pendingLabels.length,
- attributionTypeCount: attributionTypes.length,
- negativeLabelsRequireAttribution: negativeLabelsMissingAttributionRule.length === 0,
- reviewMetricsStrictRules: [
- '人工复核标签不能为空。',
- '负样本标签必须填写归因类型。',
- '重复键必须为 0。',
- '同一 Brief 内排名必须连续。',
- '商务可用率、负样本率和归因覆盖率必须达到 review:metrics 门槛。'
- ],
- customerEffectAuditRules: [
- '客户选中率必须来自真实客户最终选择字段。',
- '客户拒绝样本必须填写拒绝原因或反馈原因。',
- '必须有历史人工补号量基线和本轮人工补号量。',
- '缺少 customer-effect-summary.json 或 overallPass=true 时,不得宣称客户效果。'
- ],
- labels: rows,
- attributionTypes
- };
- return summary;
- }
- function renderReport(summary) {
- return [
- '# 人工复核标签与归因填写指南',
- '',
- `- 生成时间:${summary.generatedAt}`,
- `- 标签数:${summary.labelCount}`,
- `- 归因类型数:${summary.attributionTypeCount}`,
- `- directCustomerProof:${summary.directCustomerProof}`,
- `- proofLevel:${summary.proofLevel}`,
- '',
- '## 使用边界',
- '',
- '- 本指南只规范商务复核字段、负样本归因和客户效果审计前置条件。',
- '- 它不证明命中率提升,也不证明客户效果完成。',
- '- 只有真实客户选择、历史人工补号基线、本轮人工补号量和 `customer-effect:audit` 同时通过,才能进入客户效果结论。',
- '',
- '## 人工复核标签',
- '',
- '| ID | 标签 | 分类 | 什么时候用 | 需要归因 | 需要反馈原因 | 会阻塞 review:metrics --strict | 会阻塞 customer-effect:audit | 边界 |',
- '| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
- ...summary.labels.map(item => tableRow([
- item.id,
- item.label,
- item.category,
- item.whenToUse,
- yesNo(item.requiresAttribution),
- yesNo(item.requiresFeedbackReason),
- yesNo(item.blocksReviewMetricsStrict),
- yesNo(item.blocksCustomerEffectAudit),
- item.customerEffectBoundary
- ])),
- '',
- '## 负样本归因类型',
- '',
- '| ID | 归因类型 | 适用场景 | 说明 |',
- '| --- | --- | --- | --- |',
- ...summary.attributionTypes.map(item => tableRow([item.id, item.type, item.appliesTo, item.usage])),
- '',
- '## 严格复核门禁',
- '',
- ...summary.reviewMetricsStrictRules.map(item => `- ${item}`),
- '',
- '## 客户效果门禁',
- '',
- ...summary.customerEffectAuditRules.map(item => `- ${item}`),
- '',
- '## 建议填写顺序',
- '',
- '1. 先给每个候选填写 `人工复核标签`。',
- '2. 所有负样本和客户拒绝样本补 `归因类型`。',
- '3. 负样本补 `反馈原因`,写到能指导下一轮规则或权重调整。',
- '4. 客户有最终选择后,再填写 `客户选择`。',
- '5. 跑 `npm run review:metrics -- --input <已标注CSV> --output <复核指标输出目录> --strict`。',
- '6. 历史基线和本轮人工补号量到位后,再跑 `customer-effect:audit`。',
- ''
- ].join('\n');
- }
- function renderCsv(summary) {
- const header = [
- '类型',
- 'ID',
- '名称',
- '分类',
- '说明',
- '需要归因',
- '需要反馈原因',
- '阻塞review_metrics_strict',
- '阻塞customer_effect_audit',
- '客户效果边界'
- ];
- const labelRows = summary.labels.map(item => [
- 'label',
- item.id,
- item.label,
- item.category,
- item.whenToUse,
- yesNo(item.requiresAttribution),
- yesNo(item.requiresFeedbackReason),
- yesNo(item.blocksReviewMetricsStrict),
- yesNo(item.blocksCustomerEffectAudit),
- item.customerEffectBoundary
- ]);
- const attributionRows = summary.attributionTypes.map(item => [
- 'attribution',
- item.id,
- item.type,
- 'negative_attribution',
- item.usage,
- '是',
- '建议',
- '负样本缺失时阻塞',
- '负样本缺失时阻塞',
- '只作为归因字段,不证明客户效果'
- ]);
- return [header, ...labelRows, ...attributionRows].map(row => row.map(csvCell).join(',')).join('\n');
- }
- function parseArgs(argv) {
- const args = {};
- for (let index = 0; index < argv.length; index += 1) {
- const raw = argv[index];
- if (!raw.startsWith('--')) continue;
- const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
- const next = argv[index + 1];
- if (!next || next.startsWith('--')) args[key] = true;
- else {
- args[key] = next;
- index += 1;
- }
- }
- return args;
- }
- function yesNo(value) {
- return value ? '是' : '否';
- }
- function csvCell(value) {
- const text = String(value ?? '');
- return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
- }
- function tableRow(cells) {
- return `| ${cells.map(cell => String(cell ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ')).join(' | ')} |`;
- }
- function withBom(text) {
- return `\uFEFF${text}`;
- }
- if (require.main === module) main();
- module.exports = {
- buildGuideSummary,
- renderReport,
- renderCsv
- };
|