| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- #!/usr/bin/env node
- const fs = require('fs');
- const os = require('os');
- const path = require('path');
- const { spawnSync } = require('child_process');
- const { SOFTWARE_TABLE_HEADER } = require('../mcp/src/features/tihao-sourcing/report');
- const root = path.resolve(__dirname, '..');
- const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-software-table-'));
- const input = path.join(fixtureDir, 'manual-review-sample.csv');
- const output = path.join(fixtureDir, 'software-client-list.dedup.csv');
- const expectedHeader = SOFTWARE_TABLE_HEADER.join(',');
- const rows = [
- [
- 'brief编号',
- '策略',
- '排名',
- '平台',
- '博主名称',
- '综合分',
- 'brief匹配分',
- '参考风格分',
- '证据加分',
- '证据风险扣分',
- '推荐理由',
- '风险提示',
- '主页链接',
- '人工复核标签'
- ],
- ['brief-a', 'result-first', '1', 'xiaohongshu', '账号A', '88', '90', '80', '1', '0', '标签覆盖 测评、测评、开箱,理由A', '风险A', 'http://m.xiaohongshu.com/user/profile/a?x=1', ''],
- ['brief-a', 'video-enhanced', '2', 'xhs', '账号A(备孕中)', '90', '91', '81', '2', '0', '标签覆盖 测评、开箱,理由A2', '风险A2', 'https://www.xiaohongshu.com/user/profile/a#note', ''],
- ['brief-a', 'result-first', '3', 'douyin', '账号B', '80', '82', '70', '0', '0', '理由B', '风险B', '', ''],
- ['brief-a', 'result-first-risk', '4', 'dy', '账号B', '79', '81', '69', '0', '0', '理由B2', '风险B2', '', ''],
- ['brief-b', 'result-first', '5', 'xiaohongshu', '账号C', '81', '83', '71', '0', '0', '理由C', '风险C', 'https://example.com/c', ''],
- ['brief-b', 'result-first', '6', 'xiaohongshu', '账号D', '79', '81', '69', '0', '0', '理由D', '风险D', 'https://example.com/d', ''],
- ['brief-b', 'result-first', '7', 'xiaohongshu', '账号A', '85', '86', '78', '1', '0', '跨 brief 重复账号', '风险A3', 'https://www.xiaohongshu.com/user/profile/a?brief=b', '']
- ];
- fs.writeFileSync(input, '\uFEFF' + rows.map(row => row.map(csvCell).join(',')).join('\n'));
- const result = spawnSync(process.execPath, [
- path.join(root, 'scripts', 'export-software-table.js'),
- '--input',
- input,
- '--output',
- output
- ], { cwd: root, encoding: 'utf8' });
- if (result.status !== 0) {
- process.stderr.write(result.stderr || result.stdout || '');
- throw new Error('export-software-table 执行失败');
- }
- const text = fs.readFileSync(output, 'utf8').replace(/^\uFEFF/, '');
- const lines = text.trim().split(/\r?\n/);
- assert(lines[0] === expectedHeader, '软件端表头必须固定为中文表头');
- assert(lines.length === 5, '重复博主必须被移除');
- assert(lines[1].includes('video-enhanced'), '重复项应保留证据更完整且分数更高的记录');
- assert(lines[1].includes(',小红书,'), '平台必须导出为中文展示名');
- assert(lines[1].includes('标签覆盖 测评、开箱'), '推荐理由里的重复标签必须清理');
- assert(lines.some(line => line.includes(',抖音,')), '抖音平台必须导出为中文展示名');
- const body = parseCsv(text).slice(1);
- assert(body.filter(row => row[0] === 'brief-a').map(row => row[2]).join(',') === '1,2', 'brief-a 去重后排名必须连续');
- assert(body.filter(row => row[0] === 'brief-b').map(row => row[2]).join(',') === '1,2', 'brief-b 去重后排名必须连续');
- const summary = JSON.parse(fs.readFileSync(output.replace(/\.csv$/i, '.summary.json'), 'utf8'));
- assert(summary.duplicateCount === 3, '摘要应报告 3 条重复记录');
- assert(summary.sourceDuplicateRemovedCount === 3, 'summary should expose source duplicate rows removed');
- assert(summary.duplicateCountMeaning === 'source_duplicate_removed_count', 'summary should define legacy duplicateCount meaning');
- assert(summary.finalDuplicateGroupCount === 0, 'summary should expose zero final duplicate groups');
- assert(summary.rankContinuous === true, '摘要应报告排名连续');
- assert(summary.duplicateAudit.sameBriefUrl.length === 0, '同 brief URL 重复组必须为 0');
- assert(summary.duplicateAudit.sameBriefName.length === 0, '同 brief 名称重复组必须为 0');
- assert(summary.duplicateAudit.globalUrl.length === 0, '全局 URL 重复组必须为 0');
- assert(summary.duplicateAudit.globalNamePlatform.length === 0, '全局平台+名称重复组必须为 0');
- const markdownOutput = output.replace(/\.csv$/i, '.md');
- const markdown = spawnSync(process.execPath, [
- path.join(root, 'scripts', 'export-software-markdown.js'),
- '--input',
- output,
- '--output',
- markdownOutput
- ], { cwd: root, encoding: 'utf8' });
- if (markdown.status !== 0) {
- process.stderr.write(markdown.stderr || markdown.stdout || '');
- throw new Error('export-software-markdown 执行失败');
- }
- const markdownText = fs.readFileSync(markdownOutput, 'utf8');
- assert(markdownText.includes('# 软件端去重博主名单'), 'Markdown 标题必须为中文');
- assert(markdownText.includes('源表去重丢弃行数:3'), 'Markdown should clarify removed rows are source duplicates');
- assert(markdownText.includes('最终名单重复组数:0'), 'Markdown should expose final duplicate group count');
- assert(markdownText.includes('同 brief URL 重复组:0'), 'Markdown 必须输出重复审计摘要');
- console.log(JSON.stringify({
- ok: true,
- input,
- output,
- markdownOutput,
- duplicateCount: summary.duplicateCount,
- sourceDuplicateRemovedCount: summary.sourceDuplicateRemovedCount,
- finalDuplicateGroupCount: summary.finalDuplicateGroupCount,
- outputRows: summary.outputRows
- }, null, 2));
- function assert(condition, message) {
- if (!condition) throw new Error(message);
- }
- function csvCell(value) {
- const text = String(value ?? '');
- return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
- }
- function parseCsv(text) {
- const rows = [];
- let row = [];
- let cell = '';
- let quoted = false;
- for (let index = 0; index < text.length; index += 1) {
- const char = text[index];
- if (char === '\r') continue;
- if (char === '"' && quoted && text[index + 1] === '"') {
- cell += '"';
- index += 1;
- } else if (char === '"') {
- quoted = !quoted;
- } else if (char === ',' && !quoted) {
- row.push(cell);
- cell = '';
- } else if (char === '\n' && !quoted) {
- row.push(cell);
- rows.push(row);
- row = [];
- cell = '';
- } else {
- cell += char;
- }
- }
- if (cell || row.length) {
- row.push(cell);
- rows.push(row);
- }
- return rows;
- }
|