#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { runTihaoSourcing } = require('../mcp/src/tools/tihao-brief-sourcing-run'); const { SOFTWARE_TABLE_HEADER } = require('../mcp/src/features/tihao-sourcing/report'); async function main() { const root = path.resolve(__dirname, '..'); const repoRoot = path.resolve(root, '..'); const out = path.join(os.tmpdir(), `tihao-product-acceptance-${Date.now()}`); const dhaBrief = path.join(repoRoot, 'dha_brief.xlsx'); const baselinePath = path.join(repoRoot, 'output', 'dha-tihao-poc', 'reference_baseline.json'); const installGuide = path.join(root, 'docs', 'install-guide.md'); const roadmap = path.join(root, 'docs', 'reference-evidence-roadmap.md'); const runbook = path.join(root, 'docs', 'live-provider-integration-runbook.md'); const readme = path.join(root, 'README.md'); assert(fs.existsSync(installGuide), 'install guide should exist'); assert(fs.existsSync(roadmap), 'reference roadmap should exist'); assert(fs.existsSync(runbook), 'live provider runbook should exist'); assert(fs.existsSync(readme), 'README should exist'); assertNoPricingLeak(fs.readFileSync(installGuide, 'utf8'), 'install guide'); const roadmapText = fs.readFileSync(roadmap, 'utf8'); const runbookText = fs.readFileSync(runbook, 'utf8'); const readmeText = fs.readFileSync(readme, 'utf8'); assertNoSecretLeak(roadmapText, 'reference roadmap'); assertNoSecretLeak(runbookText, 'live provider runbook'); assertIncludes(roadmapText, 'docs/live-provider-integration-runbook.md', 'reference roadmap runbook link'); assertIncludes(runbookText, '不能宣称', 'live provider wording guard'); assertIncludes(runbookText, '没有跑通 `acceptance:providers`,不能说参考补证 provider 已接通', 'live provider reference guard'); assertIncludes(readmeText, 'docs/live-provider-integration-runbook.md', 'README runbook link'); scanPackageFiles(root); const input = { collectionMode: 'sample', brief: fs.existsSync(dhaBrief) ? dhaBrief : undefined, briefText: fs.existsSync(dhaBrief) ? undefined : [ '平台:小红书', '产品:DHA', '干货科普参考账号:', '图文 http://xhslink.com/o/7KZdAn1bNgj', '视频 http://xhslink.com/o/2fg3MzG2Iau', '618合集参考账号:', '图文 http://xhslink.com/o/12gLsjgndoG', '视频 http://xhslink.com/o/1KYJBBeEhP4' ].join('\n'), referenceBaselinePath: fs.existsSync(baselinePath) ? baselinePath : undefined, evidenceCards: [ { creatorId: 'xhs-dha-001', platform: 'xiaohongshu', displayName: '营养师妈妈DHA笔记', sourceLevel: 'multimodal_verified', contentEvidence: { textSignals: ['DHA', '母婴'], asrSignals: ['宝宝营养', '真实体验'], visualSignals: ['真人出镜', '产品实拍', '母婴家庭场景'], transcriptSnippets: ['这条主要讲宝宝 DHA 怎么选'] }, referenceFit: { score: 88, level: 'high', hitPoints: ['DHA', '母婴', '真实体验'] }, riskEvidence: { needsManualReview: ['宝宝营养功效表达需人工复核'] } } ], output: out }; const result = await runTihaoSourcing(input); assert(result.status === 'ok', 'product audit sample run should be ok'); assert(result.files && result.files.length === 3, 'product audit should write three files'); for (const file of result.files) assert(fs.existsSync(file), `output file should exist: ${file}`); const mdPath = path.join(out, 'tihao-sourcing-report.md'); const jsonPath = path.join(out, 'tihao-sourcing-result.json'); const csvPath = path.join(out, 'tihao-sourcing-client-list.csv'); const markdown = fs.readFileSync(mdPath, 'utf8'); const json = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); const csv = fs.readFileSync(csvPath, 'utf8'); assertIncludes(markdown, '# 提号博主推荐名单', 'markdown title'); assertIncludes(markdown, '## Brief 解析摘要', 'brief summary section'); assertIncludes(markdown, '## 需求三分层与隐性规则', 'requirement layers section'); assertIncludes(markdown, '## 参考账号可参考性判断', 'reference usability section'); assertIncludes(markdown, '## 参考视频风格指纹', 'reference fingerprint section'); assertIncludes(markdown, '## 参考账号/风格锚点', 'reference anchor section'); assertIncludes(markdown, '## 候选召回记录', 'recall record section'); assertIncludes(markdown, '## 主页最近内容证据', 'homepage evidence section'); assertIncludes(markdown, '## 多模态证据卡', 'multimodal evidence section'); assertIncludes(markdown, '## 商务可用名单', 'business list section'); assertIncludes(markdown, '## 剔除/降级原因', 'excluded or downgraded section'); assertIncludes(markdown, '## 下一轮校准问题', 'calibration questions section'); assertIncludes(markdown, '## 生成文件', 'generated files section'); assertIncludes(markdown, '不是最终投放名单', 'first-round caveat'); assertIncludes(markdown, '证据卡用于辅助复核', 'evidence-card caveat'); assert(json.criteria.referenceLinks.length >= 4, 'criteria should keep reference links'); assert(json.criteria.referenceStyleAnchors.length >= 4, 'criteria should keep reference style anchors'); assert(Array.isArray(json.criteria.referenceStyleFingerprints) && json.criteria.referenceStyleFingerprints.length >= 4, 'criteria should keep reference style fingerprints'); assert(json.criteria.referenceStyleFingerprints.some(item => item.contentType === 'video'), 'reference style fingerprints should include video links'); assert(json.criteria.referenceStyleFingerprints.some(item => (item.missingEvidenceNotes || []).includes('待补口播证据')), 'video fingerprints should disclose missing ASR evidence'); assert(json.criteria.referenceStyleFingerprints.some(item => (item.missingEvidenceNotes || []).includes('待补帧图证据')), 'video fingerprints should disclose missing frame evidence'); assert(json.criteria.referenceEvidenceStatus && typeof json.criteria.referenceEvidenceStatus.providerStatus === 'string', 'criteria should keep reference provider status'); assert(json.criteria.referenceFingerprintStatus && json.criteria.referenceFingerprintStatus.fingerprintCount >= 4, 'criteria should keep reference fingerprint status'); assert(json.criteria.requirementLayers && Array.isArray(json.criteria.requirementLayers.hardConstraints), 'criteria should keep requirement layers'); assert(Array.isArray(json.criteria.categoryRules) && json.criteria.categoryRules.length >= 1, 'criteria should keep category rules'); assert(json.criteria.referenceUsability && typeof json.criteria.referenceUsability.status === 'string', 'criteria should keep reference usability'); assert(Array.isArray(json.criteria.sourcingStrategy) && json.criteria.sourcingStrategy.length >= 1, 'criteria should keep platform sourcing strategy'); assert(Array.isArray(json.criteria.evidenceCards) && json.criteria.evidenceCards.length >= 1, 'criteria should keep evidence cards'); assert(json.criteria.evidenceStatus && typeof json.criteria.evidenceStatus.providerStatus === 'string', 'criteria should keep evidence provider status'); assert(Array.isArray(json.calibrationQuestions) && json.calibrationQuestions.length >= 3, 'json should keep calibration questions'); assert(Array.isArray(json.nextActions) && json.nextActions.length >= 3, 'json should keep next actions'); assert(Array.isArray(result.nextActions) && result.nextActions.some(item => item.includes('校准问题')), 'tool result should expose calibration next action'); assert(json.candidates.some(item => item.briefFitScore > 0), 'candidates should include briefFitScore'); assert(json.candidates.some(item => item.referenceStyleFitScore > 0), 'candidates should include referenceStyleFitScore'); assert(json.candidates.every(item => Array.isArray(item.briefHitConditions)), 'candidates should include brief hit conditions'); assert(json.candidates.every(item => Array.isArray(item.referenceStyleHitPoints)), 'candidates should include reference style hit points'); assert(json.candidates.every(item => Array.isArray(item.homepageEvidenceHitPoints)), 'candidates should include homepage evidence hit points'); assert(json.candidates.every(item => Array.isArray(item.missingKeyConditions)), 'candidates should include missing key conditions'); assert(json.candidates.every(item => Array.isArray(item.manualReviewFields)), 'candidates should include manual review fields'); assert(json.candidates.every(item => item.homepageEvidence && Array.isArray(item.homepageEvidence.reviewNotes)), 'candidates should include homepage evidence'); assert(json.candidates.every(item => typeof item.recentContentFitScore === 'number'), 'candidates should include recent content fit score'); assert(json.candidates.every(item => item.implicitRuleFit && Array.isArray(item.implicitRuleFit.hitPoints)), 'candidates should include implicit rule fit'); assert(json.candidates.filter(item => item.recommendStatus === '强推荐').every(item => (item.briefHitConditions || []).length >= 2 && ((item.referenceStyleHitPoints || []).length + (item.homepageEvidenceHitPoints || []).length) >= 1), 'strong candidates should hit brief and reference/homepage evidence gates'); assert(json.candidates.some(item => Array.isArray(item.evidenceSignals) && item.evidenceSignals.includes('真人出镜')), 'candidates should include evidence signals'); assert(json.candidates.some(item => Array.isArray(item.evidenceRiskHints) && item.evidenceRiskHints.includes('宝宝营养功效表达需人工复核')), 'candidates should include manual-review hints'); const csvHeader = csv.split(/\r?\n/)[0].replace(/^\uFEFF/, ''); const softwareHeader = SOFTWARE_TABLE_HEADER.join(','); assert(csvHeader === softwareHeader, 'csv should use fixed software table header'); const header = csvHeader.split(','); const column = name => header.indexOf(name); const csvRows = csv.trim().split(/\r?\n/).slice(1).filter(Boolean).map(line => parseCsvLine(line)); const seenKeys = new Set(); for (const [index, row] of csvRows.entries()) { const key = row[column('主页链接')] ? `${row[column('brief编号')]}|${row[column('平台')]}|${row[column('主页链接')]}` : `${row[column('brief编号')]}|${row[column('平台')]}|${row[column('账号名称')]}`; assert(!seenKeys.has(key), 'software table should not include duplicate creator keys'); seenKeys.add(key); assert(Number(row[column('序号')]) === index + 1, 'software table ranks should be continuous after dedupe'); } assertNoSecretLeak(markdown, 'markdown report'); assertNoSecretLeak(JSON.stringify(json), 'json result'); assertNoSecretLeak(csv, 'csv list'); console.log(`product acceptance audit ok: ${out}`); } function parseCsvLine(line) { const cells = []; let cell = ''; let quoted = false; for (let index = 0; index < line.length; index += 1) { const char = line[index]; if (char === "\"" && quoted && line[index + 1] === "\"") { cell += "\""; index += 1; continue; } if (char === "\"") { quoted = !quoted; continue; } if (char === ',' && !quoted) { cells.push(cell); cell = ''; continue; } cell += char; } cells.push(cell); return cells; } function assertNoPricingLeak(text, label) { const forbidden = [/priceStep/i, /"price"\s*:/i, /(price|cost|定价|价格)[^\n]{0,40}\b0\.1\b/i, /\b1000\s*[,,]\s*100\b/]; for (const pattern of forbidden) { assert(!pattern.test(text), `${label} should not expose provisional pricing: ${pattern}`); } } function assertNoSecretLeak(text, label) { const forbidden = [ /ark-[A-Za-z0-9-]{20,}/i, /sk-[A-Za-z0-9_-]{20,}/i, /SecretKey\s*\r?\n\s*[A-Za-z0-9_-]{16,}/i, /XFYUN_SECRET_KEY\s*=\s*['"]?(?!<)[^\s'"]+/i, /DOUBAO_API_KEY\s*=\s*['"]?(?!<)[^\s'"]+/i, /TIHAO_SESSION_TOKEN\s*=\s*['"]?(?!<)[^\s'"]+/, /VOC_ECOMMERCE_TOKEN\s*=\s*['"]?(?!<)[^\s'"]+/, /Authorization:\s*Bearer\s+[A-Za-z0-9._-]{8,}/i, /sessionToken['"]?\s*[:=]\s*['"]?[A-Za-z0-9._-]{12,}/i ]; for (const pattern of forbidden) { assert(!pattern.test(text), `${label} should not leak token-like content: ${pattern}`); } } // 字段 schema 是「未来建库」的结构化元数据(字段定义/DDL/查询映射),不含实际报价数值, // 不应被「临时定价泄露」扫描误伤(上方 assertNoSecretLeak 仍会覆盖它们做 token 泄露检查)。 const PRICING_SCAN_SKIP = [ 'docs/pgy-field-schema.json', 'docs/media-library-field-schema.json' ]; function scanPackageFiles(root) { const files = []; walk(root, files); for (const file of files) { const rel = path.relative(root, file).replace(/\\/g, '/'); if (shouldSkipScan(rel)) continue; const text = fs.readFileSync(file, 'utf8'); assertNoSecretLeak(text, rel); if ((rel.startsWith('docs/') || rel.endsWith('.md')) && !PRICING_SCAN_SKIP.includes(rel)) assertNoPricingLeak(text, rel); } } function walk(dir, files) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { if (['node_modules', '.claude', 'outputs'].includes(entry.name) || entry.name.startsWith('.npm-cache')) continue; walk(full, files); } else if (entry.isFile()) { files.push(full); } } } function shouldSkipScan(rel) { return rel === 'package-lock.json' || rel.endsWith('.tgz') || rel.startsWith('.git/'); } function assertIncludes(text, expected, label) { assert(String(text).includes(expected), `${label} should include ${expected}`); } function assert(condition, message) { if (!condition) throw new Error(message); } main().catch(error => { console.error(error && error.stack ? error.stack : String(error)); process.exit(1); });