| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- #!/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);
- });
|