candidate-pool-smoke.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { buildSourcingReport } = require('../mcp/src/features/tihao-sourcing/report');
  6. function main() {
  7. const root = path.resolve(__dirname, '..');
  8. const outputRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-candidate-pool-'));
  9. const fullPool = buildSourcingReport({
  10. criteria: {
  11. briefId: '候选池验收',
  12. brand: '候选池验收',
  13. targetCount: 4,
  14. platforms: ['xiaohongshu']
  15. },
  16. candidates: sampleCandidates(),
  17. collectionMode: 'sample',
  18. outputDir: path.join(outputRoot, 'full')
  19. });
  20. assert(fullPool.summary.reviewPoolTarget === 6, 'targetCount=4 should require a 1.5x review pool of 6');
  21. assert(fullPool.summary.clientReadyCount === 6, 'software table should output 6 reviewable candidates');
  22. assert(fullPool.summary.duplicateCount === 1, 'duplicate homepage URL should be reported');
  23. assertNoDuplicateSoftwareRows(path.join(outputRoot, 'full', 'tihao-sourcing-client-list.csv'));
  24. const shortPool = buildSourcingReport({
  25. criteria: {
  26. briefId: '候选池不足验收',
  27. brand: '候选池不足验收',
  28. targetCount: 10,
  29. platforms: ['xiaohongshu']
  30. },
  31. candidates: sampleCandidates(),
  32. collectionMode: 'sample',
  33. outputDir: path.join(outputRoot, 'short')
  34. });
  35. const shortJson = JSON.parse(fs.readFileSync(path.join(outputRoot, 'short', 'tihao-sourcing-result.json'), 'utf8'));
  36. assert(shortPool.summary.reviewPoolTarget === 15, 'targetCount=10 should require a 1.5x review pool of 15');
  37. assert(shortPool.summary.clientReadyCount === 7, 'short software table should keep all available non-excluded candidates');
  38. assert(shortJson.warnings.some(item => item.includes('1.5 倍候选池 15 位')), 'short pool should warn about 1.5x candidate pool gap');
  39. assert(shortPool.summary.calibrationQuestions.some(item => item.includes('1.5 倍候选池')), 'calibration question should mention 1.5x pool');
  40. console.log(JSON.stringify({
  41. ok: true,
  42. outputRoot,
  43. fullPool: fullPool.summary,
  44. shortPool: shortPool.summary
  45. }, null, 2));
  46. }
  47. function sampleCandidates() {
  48. return [
  49. creator('账号A', 88, 'https://example.com/a'),
  50. creator('账号A-高分重复', 93, 'https://example.com/a'),
  51. creator('账号B', 87, 'https://example.com/b'),
  52. creator('账号C', 86, 'https://example.com/c'),
  53. creator('账号D', 85, 'https://example.com/d'),
  54. creator('账号E', 84, 'https://example.com/e'),
  55. creator('账号F', 83, 'https://example.com/f'),
  56. creator('账号G', 82, 'https://example.com/g')
  57. ];
  58. }
  59. function creator(displayName, score, profileUrl) {
  60. return {
  61. platform: 'xiaohongshu',
  62. displayName,
  63. profileUrl,
  64. score,
  65. briefFitScore: score,
  66. referenceStyleFitScore: 80,
  67. recentContentFitScore: 78,
  68. visualQualityScore: 76,
  69. toneConsistencyScore: 79,
  70. recommendStatus: score >= 86 ? '强推荐' : '备选',
  71. recommendReason: `${displayName} 命中 Brief 核心人群和参考风格。`,
  72. riskNote: '需商务复核报价有效期。'
  73. };
  74. }
  75. function assertNoDuplicateSoftwareRows(csvPath) {
  76. const lines = fs.readFileSync(csvPath, 'utf8').replace(/^\uFEFF/, '').trim().split(/\r?\n/);
  77. const header = lines[0].split(',');
  78. const rows = lines.slice(1).map(line => parseCsvLine(line, header));
  79. const keys = new Set();
  80. for (const row of rows) {
  81. const briefId = pick(row, ['brief编号']);
  82. const platform = pick(row, ['平台']);
  83. const profileUrl = pick(row, ['主页链接']);
  84. const displayName = pick(row, ['账号名称', '博主名称']);
  85. const key = profileUrl
  86. ? `${briefId}|${platform}|${profileUrl.toLowerCase()}`
  87. : `${briefId}|${platform}|${displayName.toLowerCase()}`;
  88. assert(!keys.has(key), `duplicate software row key: ${key}`);
  89. keys.add(key);
  90. }
  91. assert(rows.map(row => Number(pick(row, ['序号', '排名']))).join(',') === '1,2,3,4,5,6', 'software rows should be reranked continuously');
  92. }
  93. function pick(row, keys) {
  94. for (const key of keys) {
  95. if (row[key] !== undefined && row[key] !== '') return row[key];
  96. }
  97. return '';
  98. }
  99. function parseCsvLine(line, header) {
  100. const cells = [];
  101. let current = '';
  102. let quoted = false;
  103. for (let i = 0; i < line.length; i += 1) {
  104. const ch = line[i];
  105. if (ch === '"' && quoted && line[i + 1] === '"') {
  106. current += '"';
  107. i += 1;
  108. } else if (ch === '"') {
  109. quoted = !quoted;
  110. } else if (ch === ',' && !quoted) {
  111. cells.push(current);
  112. current = '';
  113. } else {
  114. current += ch;
  115. }
  116. }
  117. cells.push(current);
  118. return Object.fromEntries(header.map((key, index) => [key, cells[index] || '']));
  119. }
  120. function assert(condition, message) {
  121. if (!condition) throw new Error(message);
  122. }
  123. main();