| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- #!/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();
|