#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { buildSourcingReport } = require('../mcp/src/features/tihao-sourcing/report'); function main() { const root = path.resolve(__dirname, '..'); const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-candidate-pool-')); const fullPool = buildSourcingReport({ criteria: { briefId: '候选池验收', brand: '候选池验收', targetCount: 4, platforms: ['xiaohongshu'] }, candidates: sampleCandidates(), collectionMode: 'sample', outputDir: path.join(outputRoot, 'full') }); assert(fullPool.summary.reviewPoolTarget === 6, 'targetCount=4 should require a 1.5x review pool of 6'); assert(fullPool.summary.clientReadyCount === 6, 'software table should output 6 reviewable candidates'); assert(fullPool.summary.duplicateCount === 1, 'duplicate homepage URL should be reported'); assertNoDuplicateSoftwareRows(path.join(outputRoot, 'full', 'tihao-sourcing-client-list.csv')); const shortPool = buildSourcingReport({ criteria: { briefId: '候选池不足验收', brand: '候选池不足验收', targetCount: 10, platforms: ['xiaohongshu'] }, candidates: sampleCandidates(), collectionMode: 'sample', outputDir: path.join(outputRoot, 'short') }); const shortJson = JSON.parse(fs.readFileSync(path.join(outputRoot, 'short', 'tihao-sourcing-result.json'), 'utf8')); assert(shortPool.summary.reviewPoolTarget === 15, 'targetCount=10 should require a 1.5x review pool of 15'); assert(shortPool.summary.clientReadyCount === 7, 'short software table should keep all available non-excluded candidates'); assert(shortJson.warnings.some(item => item.includes('1.5 倍候选池 15 位')), 'short pool should warn about 1.5x candidate pool gap'); assert(shortPool.summary.calibrationQuestions.some(item => item.includes('1.5 倍候选池')), 'calibration question should mention 1.5x pool'); console.log(JSON.stringify({ ok: true, outputRoot, fullPool: fullPool.summary, shortPool: shortPool.summary }, null, 2)); } function sampleCandidates() { return [ creator('账号A', 88, 'https://example.com/a'), creator('账号A-高分重复', 93, 'https://example.com/a'), creator('账号B', 87, 'https://example.com/b'), creator('账号C', 86, 'https://example.com/c'), creator('账号D', 85, 'https://example.com/d'), creator('账号E', 84, 'https://example.com/e'), creator('账号F', 83, 'https://example.com/f'), creator('账号G', 82, 'https://example.com/g') ]; } function creator(displayName, score, profileUrl) { return { platform: 'xiaohongshu', displayName, profileUrl, score, briefFitScore: score, referenceStyleFitScore: 80, recentContentFitScore: 78, visualQualityScore: 76, toneConsistencyScore: 79, recommendStatus: score >= 86 ? '强推荐' : '备选', recommendReason: `${displayName} 命中 Brief 核心人群和参考风格。`, riskNote: '需商务复核报价有效期。' }; } function assertNoDuplicateSoftwareRows(csvPath) { const lines = fs.readFileSync(csvPath, 'utf8').replace(/^\uFEFF/, '').trim().split(/\r?\n/); const header = lines[0].split(','); const rows = lines.slice(1).map(line => parseCsvLine(line, header)); const keys = new Set(); for (const row of rows) { const briefId = pick(row, ['brief编号']); const platform = pick(row, ['平台']); const profileUrl = pick(row, ['主页链接']); const displayName = pick(row, ['账号名称', '博主名称']); const key = profileUrl ? `${briefId}|${platform}|${profileUrl.toLowerCase()}` : `${briefId}|${platform}|${displayName.toLowerCase()}`; assert(!keys.has(key), `duplicate software row key: ${key}`); keys.add(key); } assert(rows.map(row => Number(pick(row, ['序号', '排名']))).join(',') === '1,2,3,4,5,6', 'software rows should be reranked continuously'); } function pick(row, keys) { for (const key of keys) { if (row[key] !== undefined && row[key] !== '') return row[key]; } return ''; } function parseCsvLine(line, header) { const cells = []; let current = ''; let quoted = false; for (let i = 0; i < line.length; i += 1) { const ch = line[i]; if (ch === '"' && quoted && line[i + 1] === '"') { current += '"'; i += 1; } else if (ch === '"') { quoted = !quoted; } else if (ch === ',' && !quoted) { cells.push(current); current = ''; } else { current += ch; } } cells.push(current); return Object.fromEntries(header.map((key, index) => [key, cells[index] || ''])); } function assert(condition, message) { if (!condition) throw new Error(message); } main();