#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const { HISTORY_HEADER, REVIEW_HEADER } = require('./create-data-intake-pack'); const { VIDEO_RESOURCE_HEADER } = require('./create-video-intake-pack'); const root = path.resolve(__dirname, '..'); function main() { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-intake-readiness-')); const dataPack = path.join(tmp, 'data-pack'); const videoPack = path.join(tmp, 'video-pack'); run(path.join(root, 'scripts', 'create-data-intake-pack.js'), ['--output', dataPack], 0); run(path.join(root, 'scripts', 'create-video-intake-pack.js'), ['--output', videoPack], 0); const templateAudit = path.join(tmp, 'template-audit'); run(path.join(root, 'scripts', 'intake-readiness-audit.js'), [ '--data-pack', dataPack, '--video-pack', videoPack, '--output', templateAudit, '--strict' ], 2); const templateSummary = readJson(path.join(templateAudit, 'intake-readiness-summary.json')); const templateReport = fs.readFileSync(path.join(templateAudit, 'intake-readiness-report.md'), 'utf8').replace(/^\uFEFF/, ''); const templateRepairCsv = path.join(templateAudit, 'intake-readiness-repair-actions.csv'); assert(templateSummary.acceptance.overallReady === false, 'fresh templates should not be treated as real intake data'); assert(templateSummary.failureCount > 0, 'fresh templates should report placeholder/insufficient-data issues'); assert(Array.isArray(templateSummary.repairActions) && templateSummary.repairActions.length === templateSummary.failureCount, 'template audit should expose repair action for each issue'); assert(fs.existsSync(templateRepairCsv), 'template audit should write repair actions CSV'); assert(templateReport.includes('## 修复清单'), 'template report should include repair action section'); assert(fs.readFileSync(templateRepairCsv, 'utf8').includes('修复动作'), 'repair action CSV should include action column'); const realDataPack = path.join(tmp, 'real-data-pack'); const realVideoPack = path.join(tmp, 'real-video-pack'); fs.mkdirSync(realDataPack, { recursive: true }); fs.mkdirSync(realVideoPack, { recursive: true }); writeCsv(path.join(realDataPack, 'history-data-template.csv'), [HISTORY_HEADER, ...historyRows()]); writeCsv(path.join(realDataPack, 'manual-review-template.csv'), [REVIEW_HEADER, ...reviewRows()]); writeCsv(path.join(realVideoPack, 'video-resource-template.csv'), [VIDEO_RESOURCE_HEADER, ...videoRows()]); const realAudit = path.join(tmp, 'real-audit'); run(path.join(root, 'scripts', 'intake-readiness-audit.js'), [ '--data-pack', realDataPack, '--video-pack', realVideoPack, '--output', realAudit, '--strict' ], 0); const realSummary = readJson(path.join(realAudit, 'intake-readiness-summary.json')); const realReport = fs.readFileSync(path.join(realAudit, 'intake-readiness-report.md'), 'utf8').replace(/^\uFEFF/, ''); const realRepairCsv = path.join(realAudit, 'intake-readiness-repair-actions.csv'); assert(realSummary.acceptance.overallReady === true, 'complete intake fixtures should pass readiness'); assert(Array.isArray(realSummary.repairActions) && realSummary.repairActions.length === 0, 'complete intake fixtures should have no repair actions'); assert(fs.existsSync(realRepairCsv), 'real audit should still write repair actions CSV'); assert(realSummary.history.counts.briefCount === 5, 'history fixture should include 5 briefs'); assert(realSummary.video.counts.realReferenceRows >= 1, 'video fixture should include real reference video'); assert(realSummary.video.counts.realCandidateRows >= 1, 'video fixture should include real candidate video'); assert(realSummary.review.readyForCustomerEffectAudit === true, 'review fixture should be ready for customer-effect audit'); assert(realReport.includes('# 真实材料 Intake Readiness 审计'), 'report should be Chinese and business-facing'); 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'); console.log(JSON.stringify({ ok: true, templateFailureCount: templateSummary.failureCount, realFailureCount: realSummary.failureCount, output: realAudit }, null, 2)); } function historyRows() { return Array.from({ length: 5 }, (_, index) => { const i = index + 1; const rejected = i % 2 === 0; return [ `brief-${String(i).padStart(3, '0')}`, `真实项目${i}`, ['母婴', '美护', '食品', '家清', '生活方式'][index], `这是客户真实 Brief ${i},包含人群、品类、预算、禁投项、参考风格和内容场景要求。`, String(4 + i), `https://brand-real.test/reference/${i}`, '小红书', `真实博主${i}`, `https://brand-real.test/profile/${i}`, rejected ? '调性不符' : '可直接发客户', rejected ? '客户拒绝' : '客户选中', rejected ? '客户认为账号调性和参考账号差异较大。' : '' ]; }); } function reviewRows() { return [ reviewRow('brief-001', 'reference-account', 1, '真实博主1', '可直接发客户', '客户选中', '', '', 1), reviewRow('brief-002', 'homepage-evidence', 1, '真实博主2', '调性不符', '客户拒绝', '调性不符', '客户认为账号表达方式不适合品牌。', 2), reviewRow('brief-003', 'video-enhanced', 1, '真实博主3', '商务复核', '客户选中', '', '', 0), reviewRow('brief-004', 'baseline-live', 1, '真实博主4', '跑偏', '客户拒绝', '需求解析错', '内容方向偏离客户 Brief。', 3), reviewRow('brief-005', 'result-first-broad', 1, '真实博主5', '可直接发客户', '客户选中', '', '', 1) ]; } function reviewRow(briefId, strategy, rank, creator, label, decision, attribution, reason, supplement) { return [ briefId, strategy, String(rank), '小红书', creator, '88', '86', '84', '85', '83', '87', '5', '0', '命中 Brief 人群、参考风格和近期主页内容,适合进入商务复核。', '需确认报价、档期和近 30 天竞品合作。', `https://brand-real.test/profile/${briefId}`, label, decision, attribution, reason, String(supplement) ]; } function videoRows() { return [ [ 'brief-001', '真实项目1', '小红书', '参考视频', '真实博主1', 'https://brand-real.test/profile/1', 'https://brand-real.test/video/reference-1.mp4', 'https://brand-real.test/cover/reference-1.jpg', '真实 ASR 文本描述产品使用场景、表达节奏、口播结构和目标人群。', 'https://brand-real.test/frame/reference-1-a.jpg|https://brand-real.test/frame/reference-1-b.jpg', '真实参考内容标题', '2026-06-01', '内容摘要覆盖场景、人群、表达结构和品牌适配点。', '真实记录感|高质感|轻讲解', '真人出镜、自然光、生活方式场景、节奏稳定', '需复核广告合规和竞品合作。', 'VOC social', '是' ], [ 'brief-001', '真实项目1', '小红书', '候选视频', '真实候选账号1', 'https://brand-real.test/profile/candidate-1', 'https://brand-real.test/video/candidate-1.mp4', 'https://brand-real.test/cover/candidate-1.jpg', '真实候选 ASR 文本描述内容结构、风格调性和人群匹配。', 'https://brand-real.test/frame/candidate-1-a.jpg', '真实候选内容标题', '2026-06-02', '候选内容摘要覆盖场景、人设、画面质感和 Brief 匹配点。', '生活方式|真实测评|轻口播', '真人出镜、室内自然光、产品近景、节奏稳定', '需复核报价档期。', 'VOC social', '是' ] ]; } function run(script, args, expectedStatus) { const result = spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' }); if (result.status !== expectedStatus) { process.stderr.write(result.stderr || result.stdout || ''); throw new Error(`${path.basename(script)} expected status ${expectedStatus}, got ${result.status}`); } } function writeCsv(file, rows) { fs.writeFileSync(file, `\uFEFF${rows.map(row => row.map(csvCell).join(',')).join('\n')}`, 'utf8'); } function csvCell(value) { const text = String(value ?? ''); return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; } function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } function assert(condition, message) { if (!condition) throw new Error(message); } main();