intake-readiness-smoke.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const { HISTORY_HEADER, REVIEW_HEADER } = require('./create-data-intake-pack');
  7. const { VIDEO_RESOURCE_HEADER } = require('./create-video-intake-pack');
  8. const root = path.resolve(__dirname, '..');
  9. function main() {
  10. const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-intake-readiness-'));
  11. const dataPack = path.join(tmp, 'data-pack');
  12. const videoPack = path.join(tmp, 'video-pack');
  13. run(path.join(root, 'scripts', 'create-data-intake-pack.js'), ['--output', dataPack], 0);
  14. run(path.join(root, 'scripts', 'create-video-intake-pack.js'), ['--output', videoPack], 0);
  15. const templateAudit = path.join(tmp, 'template-audit');
  16. run(path.join(root, 'scripts', 'intake-readiness-audit.js'), [
  17. '--data-pack',
  18. dataPack,
  19. '--video-pack',
  20. videoPack,
  21. '--output',
  22. templateAudit,
  23. '--strict'
  24. ], 2);
  25. const templateSummary = readJson(path.join(templateAudit, 'intake-readiness-summary.json'));
  26. const templateReport = fs.readFileSync(path.join(templateAudit, 'intake-readiness-report.md'), 'utf8').replace(/^\uFEFF/, '');
  27. const templateRepairCsv = path.join(templateAudit, 'intake-readiness-repair-actions.csv');
  28. assert(templateSummary.acceptance.overallReady === false, 'fresh templates should not be treated as real intake data');
  29. assert(templateSummary.failureCount > 0, 'fresh templates should report placeholder/insufficient-data issues');
  30. assert(Array.isArray(templateSummary.repairActions) && templateSummary.repairActions.length === templateSummary.failureCount, 'template audit should expose repair action for each issue');
  31. assert(fs.existsSync(templateRepairCsv), 'template audit should write repair actions CSV');
  32. assert(templateReport.includes('## 修复清单'), 'template report should include repair action section');
  33. assert(fs.readFileSync(templateRepairCsv, 'utf8').includes('修复动作'), 'repair action CSV should include action column');
  34. const realDataPack = path.join(tmp, 'real-data-pack');
  35. const realVideoPack = path.join(tmp, 'real-video-pack');
  36. fs.mkdirSync(realDataPack, { recursive: true });
  37. fs.mkdirSync(realVideoPack, { recursive: true });
  38. writeCsv(path.join(realDataPack, 'history-data-template.csv'), [HISTORY_HEADER, ...historyRows()]);
  39. writeCsv(path.join(realDataPack, 'manual-review-template.csv'), [REVIEW_HEADER, ...reviewRows()]);
  40. writeCsv(path.join(realVideoPack, 'video-resource-template.csv'), [VIDEO_RESOURCE_HEADER, ...videoRows()]);
  41. const realAudit = path.join(tmp, 'real-audit');
  42. run(path.join(root, 'scripts', 'intake-readiness-audit.js'), [
  43. '--data-pack',
  44. realDataPack,
  45. '--video-pack',
  46. realVideoPack,
  47. '--output',
  48. realAudit,
  49. '--strict'
  50. ], 0);
  51. const realSummary = readJson(path.join(realAudit, 'intake-readiness-summary.json'));
  52. const realReport = fs.readFileSync(path.join(realAudit, 'intake-readiness-report.md'), 'utf8').replace(/^\uFEFF/, '');
  53. const realRepairCsv = path.join(realAudit, 'intake-readiness-repair-actions.csv');
  54. assert(realSummary.acceptance.overallReady === true, 'complete intake fixtures should pass readiness');
  55. assert(Array.isArray(realSummary.repairActions) && realSummary.repairActions.length === 0, 'complete intake fixtures should have no repair actions');
  56. assert(fs.existsSync(realRepairCsv), 'real audit should still write repair actions CSV');
  57. assert(realSummary.history.counts.briefCount === 5, 'history fixture should include 5 briefs');
  58. assert(realSummary.video.counts.realReferenceRows >= 1, 'video fixture should include real reference video');
  59. assert(realSummary.video.counts.realCandidateRows >= 1, 'video fixture should include real candidate video');
  60. assert(realSummary.review.readyForCustomerEffectAudit === true, 'review fixture should be ready for customer-effect audit');
  61. assert(realReport.includes('# 真实材料 Intake Readiness 审计'), 'report should be Chinese and business-facing');
  62. assert(!/(sk-[A-Za-z0-9_-]{20,}|r:[A-Za-z0-9]{20,}|Authorization\s*[:=]\s*Bearer)/i.test(realReport), 'report should not contain secrets');
  63. console.log(JSON.stringify({
  64. ok: true,
  65. templateFailureCount: templateSummary.failureCount,
  66. realFailureCount: realSummary.failureCount,
  67. output: realAudit
  68. }, null, 2));
  69. }
  70. function historyRows() {
  71. return Array.from({ length: 5 }, (_, index) => {
  72. const i = index + 1;
  73. const rejected = i % 2 === 0;
  74. return [
  75. `brief-${String(i).padStart(3, '0')}`,
  76. `真实项目${i}`,
  77. ['母婴', '美护', '食品', '家清', '生活方式'][index],
  78. `这是客户真实 Brief ${i},包含人群、品类、预算、禁投项、参考风格和内容场景要求。`,
  79. String(4 + i),
  80. `https://brand-real.test/reference/${i}`,
  81. '小红书',
  82. `真实博主${i}`,
  83. `https://brand-real.test/profile/${i}`,
  84. rejected ? '调性不符' : '可直接发客户',
  85. rejected ? '客户拒绝' : '客户选中',
  86. rejected ? '客户认为账号调性和参考账号差异较大。' : ''
  87. ];
  88. });
  89. }
  90. function reviewRows() {
  91. return [
  92. reviewRow('brief-001', 'reference-account', 1, '真实博主1', '可直接发客户', '客户选中', '', '', 1),
  93. reviewRow('brief-002', 'homepage-evidence', 1, '真实博主2', '调性不符', '客户拒绝', '调性不符', '客户认为账号表达方式不适合品牌。', 2),
  94. reviewRow('brief-003', 'video-enhanced', 1, '真实博主3', '商务复核', '客户选中', '', '', 0),
  95. reviewRow('brief-004', 'baseline-live', 1, '真实博主4', '跑偏', '客户拒绝', '需求解析错', '内容方向偏离客户 Brief。', 3),
  96. reviewRow('brief-005', 'result-first-broad', 1, '真实博主5', '可直接发客户', '客户选中', '', '', 1)
  97. ];
  98. }
  99. function reviewRow(briefId, strategy, rank, creator, label, decision, attribution, reason, supplement) {
  100. return [
  101. briefId,
  102. strategy,
  103. String(rank),
  104. '小红书',
  105. creator,
  106. '88',
  107. '86',
  108. '84',
  109. '85',
  110. '83',
  111. '87',
  112. '5',
  113. '0',
  114. '命中 Brief 人群、参考风格和近期主页内容,适合进入商务复核。',
  115. '需确认报价、档期和近 30 天竞品合作。',
  116. `https://brand-real.test/profile/${briefId}`,
  117. label,
  118. decision,
  119. attribution,
  120. reason,
  121. String(supplement)
  122. ];
  123. }
  124. function videoRows() {
  125. return [
  126. [
  127. 'brief-001',
  128. '真实项目1',
  129. '小红书',
  130. '参考视频',
  131. '真实博主1',
  132. 'https://brand-real.test/profile/1',
  133. 'https://brand-real.test/video/reference-1.mp4',
  134. 'https://brand-real.test/cover/reference-1.jpg',
  135. '真实 ASR 文本描述产品使用场景、表达节奏、口播结构和目标人群。',
  136. 'https://brand-real.test/frame/reference-1-a.jpg|https://brand-real.test/frame/reference-1-b.jpg',
  137. '真实参考内容标题',
  138. '2026-06-01',
  139. '内容摘要覆盖场景、人群、表达结构和品牌适配点。',
  140. '真实记录感|高质感|轻讲解',
  141. '真人出镜、自然光、生活方式场景、节奏稳定',
  142. '需复核广告合规和竞品合作。',
  143. 'VOC social',
  144. '是'
  145. ],
  146. [
  147. 'brief-001',
  148. '真实项目1',
  149. '小红书',
  150. '候选视频',
  151. '真实候选账号1',
  152. 'https://brand-real.test/profile/candidate-1',
  153. 'https://brand-real.test/video/candidate-1.mp4',
  154. 'https://brand-real.test/cover/candidate-1.jpg',
  155. '真实候选 ASR 文本描述内容结构、风格调性和人群匹配。',
  156. 'https://brand-real.test/frame/candidate-1-a.jpg',
  157. '真实候选内容标题',
  158. '2026-06-02',
  159. '候选内容摘要覆盖场景、人设、画面质感和 Brief 匹配点。',
  160. '生活方式|真实测评|轻口播',
  161. '真人出镜、室内自然光、产品近景、节奏稳定',
  162. '需复核报价档期。',
  163. 'VOC social',
  164. '是'
  165. ]
  166. ];
  167. }
  168. function run(script, args, expectedStatus) {
  169. const result = spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
  170. if (result.status !== expectedStatus) {
  171. process.stderr.write(result.stderr || result.stdout || '');
  172. throw new Error(`${path.basename(script)} expected status ${expectedStatus}, got ${result.status}`);
  173. }
  174. }
  175. function writeCsv(file, rows) {
  176. fs.writeFileSync(file, `\uFEFF${rows.map(row => row.map(csvCell).join(',')).join('\n')}`, 'utf8');
  177. }
  178. function csvCell(value) {
  179. const text = String(value ?? '');
  180. return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
  181. }
  182. function readJson(file) {
  183. return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
  184. }
  185. function assert(condition, message) {
  186. if (!condition) throw new Error(message);
  187. }
  188. main();