#!/usr/bin/env node const http = require('http'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { runTihaoSourcing } = require('../mcp/src/tools/tihao-brief-sourcing-run'); async function main() { const args = new Set(process.argv.slice(2)); const useMock = args.has('--mock'); const mock = useMock ? await startMockProvider() : null; try { const referenceBaseUrl = mock?.url || process.env.SOCIAL_ANALYSIS_BASE_URL || process.env.TIHAO_REFERENCE_ENRICHMENT_BASE_URL || ''; const evidenceBaseUrl = mock?.url || process.env.TIHAO_EVIDENCE_BASE_URL || process.env.MULTIMODAL_EVIDENCE_BASE_URL || ''; if (!referenceBaseUrl && !evidenceBaseUrl) { console.log('provider contract audit skipped: set SOCIAL_ANALYSIS_BASE_URL/TIHAO_EVIDENCE_BASE_URL or run with --mock'); return; } const out = path.join(os.tmpdir(), `tihao-provider-contract-${Date.now()}`); const input = { collectionMode: 'sample', briefText: [ '平台:小红书', '产品:DHA', '干货科普参考账号:', '图文 http://xhslink.com/o/7KZdAn1bNgj', '视频 http://xhslink.com/o/2fg3MzG2Iau' ].join('\n'), referenceEnrichmentBaseUrl: referenceBaseUrl || undefined, multimodalEvidenceBaseUrl: evidenceBaseUrl || undefined, socialAnalysisToken: process.env.VOC_SOCIAL_TOKEN || process.env.TIHAO_SESSION_TOKEN || undefined, evidenceProviderToken: mock ? 'mock-evidence-token' : (process.env.TIHAO_EVIDENCE_TOKEN || process.env.MULTIMODAL_EVIDENCE_TOKEN || undefined), output: out }; const result = await runTihaoSourcing(input); assert(result.status === 'ok', 'provider contract sample run should remain ok'); assert(result.files && result.files.length === 3, 'provider contract should write report files'); if (referenceBaseUrl) { assert(result.data.criteria.referenceBaselines.length >= 1, 'reference provider should return at least one baseline'); assert(result.data.criteria.referenceResources.length >= 1, 'reference provider should return reference resources'); assert(result.data.criteria.referenceStyleFingerprints.some(item => String(item.coverOrVideoResource || '').includes('reference-video.mp4')), 'reference video resource should become a style fingerprint'); assert(result.data.candidates.some(item => item.referenceSimilarity > 0), 'reference provider should affect candidate reference similarity'); } if (evidenceBaseUrl) { assert(result.data.criteria.evidenceCards.length >= 1, 'evidence provider should return at least one evidence card'); assert(result.data.candidates.some(item => Array.isArray(item.evidenceSignals) && item.evidenceSignals.length), 'evidence provider should attach evidence signals'); } if (mock) { assertMockProviderContract(mock); await assertMockDoubaoVisionContract(mock); await assertMockDoubaoOpenAIChatContract(mock); await assertMockProviderFailureStates(); } const serialized = JSON.stringify(result); assert(!/Authorization:\s*Bearer/i.test(serialized), 'provider contract result should not leak Authorization headers'); assert(!/(VOC_SOCIAL_TOKEN|TIHAO_SESSION_TOKEN|VOC_ECOMMERCE_TOKEN)\s*=/.test(serialized), 'provider contract result should not leak env tokens'); console.log(`provider contract audit ok: ${out}`); } finally { if (mock) await mock.close(); } } function startMockProvider() { const state = { referenceRequests: [], evidenceRequests: [] }; const server = http.createServer(async (req, res) => { if (req.url.startsWith('/reference-baselines')) { state.referenceRequests.push(await readRequest(req)); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ referenceBaselines: [ { bucket: 'contract-reference', name: 'Contract Nutrition Mom', redId: 'contract-ref-001', userId: 'contract-user-001', fans: 52000, topSignals: ['DHA', '母婴', '营养师', '育婴师'] } ], referenceResources: [ { referenceId: 'contract-video-note-001', url: 'http://xhslink.com/o/2fg3MzG2Iau', platform: 'xiaohongshu', contentType: 'video', accountName: 'Contract Nutrition Mom', title: 'DHA 618 囤货怎么选', bodySummary: '营养师妈妈讲 DHA 选择和宝宝食用场景', authorInfo: '营养师妈妈', videoUrl: 'https://cdn.example.test/reference-video.mp4', coverUrl: 'https://cdn.example.test/reference-cover.jpg', asrText: '这条视频讲宝宝 DHA 怎么选', frameUrls: ['https://cdn.example.test/frame-001.jpg'] } ] })); return; } if (req.url.startsWith('/evidence-cards')) { state.evidenceRequests.push(await readRequest(req)); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ evidenceCards: [ { creatorId: 'xhs-dha-001', platform: 'xiaohongshu', displayName: '营养师妈妈DHA笔记', sourceLevel: 'contract_multimodal_verified', contentEvidence: { textSignals: ['DHA', '母婴'], asrSignals: ['宝宝营养'], visualSignals: ['真人出镜', '产品实拍'] }, referenceFit: { score: 86, level: 'high', hitPoints: ['DHA', '母婴'] }, riskEvidence: { needsManualReview: ['宝宝营养功效表达需人工复核'] } } ] })); return; } if (req.url.startsWith('/doubao-vision')) { state.evidenceRequests.push(await readRequest(req)); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ choices: [ { message: { content: JSON.stringify({ evidenceCards: [ { creatorId: 'xhs-dha-001', platform: 'xiaohongshu', displayName: '营养师妈妈DHA笔记', sourceLevel: 'doubao_vision_verified', contentEvidence: { textSignals: ['DHA'], asrSignals: ['宝宝营养'], visualSignals: ['真人出镜', '产品实拍'], frameEvidence: [ { framePath: 'frames/doubao-001.jpg', observation: '产品实拍' } ] }, referenceFit: { score: 87, level: 'high', hitPoints: ['DHA'] }, riskEvidence: { needsManualReview: ['功效表达需人工复核'] } } ] }) } } ] })); return; } if (req.url.startsWith('/v1/chat/completions')) { state.evidenceRequests.push(await readRequest(req)); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ choices: [ { message: { content: JSON.stringify({ evidenceCards: [ { creatorId: 'xhs-dha-001', platform: 'xiaohongshu', displayName: '营养师妈妈DHA笔记', sourceLevel: 'doubao_vision_verified', contentEvidence: { textSignals: ['DHA', '营养师'], asrSignals: [], visualSignals: [], frameEvidence: [] }, referenceFit: { score: 92, level: 'high', hitPoints: ['DHA', '营养师'] }, riskEvidence: { needsManualReview: ['No actual video frame/transcript provided'] } } ] }) } } ] })); return; } res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'not found' })); }); return new Promise(resolve => { server.listen(0, '127.0.0.1', () => { const { port } = server.address(); resolve({ url: `http://127.0.0.1:${port}`, state, close: () => new Promise(done => server.close(done)) }); }); }); } async function assertMockDoubaoVisionContract(mock) { const beforeCount = mock.state.evidenceRequests.length; const out = path.join(os.tmpdir(), `tihao-doubao-contract-${Date.now()}`); const result = await runTihaoSourcing({ collectionMode: 'sample', briefText: [ '平台:小红书', '产品:DHA', '参考账号:http://xhslink.com/o/7KZdAn1bNgj' ].join('\n'), doubaoVisionBaseUrl: `${mock.url}/doubao-vision`, doubaoVisionModel: 'doubao-seed-2-0-pro-260215', doubaoVisionToken: 'mock-doubao-token', output: out }); assert(result.status === 'ok', 'mock doubao vision run should remain ok'); assert(result.data.criteria.evidenceCards.length >= 1, 'mock doubao vision should return evidence cards'); assert(result.data.candidates.some(item => item.sourceLevel === 'doubao_vision_verified'), 'mock doubao vision should attach parsed evidence card'); assert(result.assistantMessage.includes('## 多模态证据卡'), 'mock doubao vision report should include evidence card section'); assert(result.assistantMessage.includes('doubao_vision_verified'), 'mock doubao vision report should include evidence source level'); const request = mock.state.evidenceRequests[beforeCount]; assert(request.headers.authorization === 'Bearer mock-doubao-token', 'doubao vision request should receive Authorization header'); assert(request.body.providerType === 'doubao_vision_video_analysis', 'doubao vision request should include providerType'); assert(request.body.model === 'doubao-seed-2-0-pro-260215', 'doubao vision request should include requested model id'); assert(Array.isArray(request.body.requestedCapabilities) && request.body.requestedCapabilities.includes('visual_signals'), 'doubao vision request should include visual capability'); } async function assertMockDoubaoOpenAIChatContract(mock) { const beforeCount = mock.state.evidenceRequests.length; const out = path.join(os.tmpdir(), `tihao-doubao-openai-contract-${Date.now()}`); const result = await runTihaoSourcing({ collectionMode: 'sample', briefText: [ '平台:小红书', '产品:DHA', '参考账号:http://xhslink.com/o/7KZdAn1bNgj' ].join('\n'), doubaoVisionBaseUrl: mock.url, doubaoVisionPath: 'v1/chat/completions', doubaoVisionMode: 'openai_chat', doubaoVisionModel: 'doubao-seed-2-0-pro-260215', doubaoVisionToken: 'mock-doubao-token', output: out }); assert(result.status === 'ok', 'mock doubao openai chat run should remain ok'); assert(result.data.criteria.evidenceCards.length >= 1, 'mock doubao openai chat should return evidence cards'); assert(result.data.criteria.evidenceStatus.providerStatus === 'ok', 'mock doubao openai chat provider status should be ok'); assert(result.data.candidates.some(item => item.sourceLevel === 'doubao_vision_verified'), 'mock doubao openai chat should attach parsed evidence card'); const request = mock.state.evidenceRequests[beforeCount]; assert(request.headers.authorization === 'Bearer mock-doubao-token', 'doubao openai chat request should receive Authorization header'); assert(request.body.model === 'doubao-seed-2-0-pro-260215', 'doubao openai chat request should include requested model id'); assert(Array.isArray(request.body.messages), 'doubao openai chat request should include messages'); const userMessage = request.body.messages.find(item => item.role === 'user'); assert(userMessage && typeof userMessage.content === 'string', 'doubao openai chat request should include a user JSON payload'); const contract = JSON.parse(userMessage.content); assert(contract.contractVersion === 'tihao-provider-v1', 'doubao openai chat user payload should include contractVersion'); assert(contract.providerType === 'doubao_vision_video_analysis', 'doubao openai chat user payload should include providerType'); assert(Array.isArray(contract.requestedCapabilities) && contract.requestedCapabilities.includes('visual_signals'), 'doubao openai chat user payload should include visual capability'); } async function assertMockProviderFailureStates() { const mock = await startFailureProvider(); try { const out = path.join(os.tmpdir(), `tihao-provider-failure-${Date.now()}`); const result = await runTihaoSourcing({ collectionMode: 'sample', briefText: [ '平台:小红书', '产品:DHA', '参考账号:http://xhslink.com/o/7KZdAn1bNgj' ].join('\n'), referenceEnrichmentBaseUrl: mock.url, multimodalEvidenceBaseUrl: mock.url, output: out }); assert(result.status === 'ok', 'provider failure sample run should remain ok'); assert(result.data.criteria.referenceEvidenceStatus.providerRequested === true, 'reference failure should mark provider requested'); assert(result.data.criteria.referenceEvidenceStatus.providerStatus === 'unauthorized_or_quota', 'reference failure should keep provider status'); assert(result.data.criteria.referenceEvidenceStatus.providerHttpStatus === 403, 'reference failure should keep http status'); assert(result.data.criteria.evidenceStatus.providerRequested === true, 'evidence empty result should mark provider requested'); assert(result.data.criteria.evidenceStatus.providerStatus === 'empty_result', 'evidence empty result should keep provider status'); assert(result.warnings.some(item => String(item).includes('社媒分析服务')), 'reference failure warning should be surfaced'); assert(result.warnings.some(item => String(item).includes('未返回可用 evidenceCards')), 'evidence empty warning should be surfaced'); assertProviderFailurePublicOutputSafe({ result, out }); } finally { await mock.close(); } } function assertProviderFailurePublicOutputSafe({ result, out }) { const markdownPath = path.join(out, 'tihao-sourcing-report.md'); const csvPath = path.join(out, 'tihao-sourcing-client-list.csv'); const publicText = [ result.assistantMessage || '', fs.existsSync(markdownPath) ? fs.readFileSync(markdownPath, 'utf8') : '', fs.existsSync(csvPath) ? fs.readFileSync(csvPath, 'utf8') : '' ].join('\n'); assert(!/\b(401|403)\b/.test(publicText), 'provider failure public output should not expose raw 401/403'); assert(!/hidden by client|not found|unauthorized|permission|quota/i.test(publicText), 'provider failure public output should not expose raw upstream error text'); assert(!/Authorization\s*:/i.test(publicText), 'provider failure public output should not expose Authorization header'); assert(!/Bearer\s+[A-Za-z0-9._~+/=-]+/i.test(publicText), 'provider failure public output should not expose bearer token'); assert(!/sk-[A-Za-z0-9_-]{20,}/.test(publicText), 'provider failure public output should not expose model token'); } function startFailureProvider() { const server = http.createServer((req, res) => { if (req.url.startsWith('/reference-baselines')) { res.writeHead(403, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'hidden by client' })); return; } if (req.url.startsWith('/evidence-cards')) { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ evidenceCards: [] })); return; } res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'not found' })); }); return new Promise(resolve => { server.listen(0, '127.0.0.1', () => { const { port } = server.address(); resolve({ url: `http://127.0.0.1:${port}`, close: () => new Promise(done => server.close(done)) }); }); }); } function readRequest(req) { return new Promise(resolve => { const chunks = []; req.on('data', chunk => chunks.push(chunk)); req.on('end', () => { const raw = Buffer.concat(chunks).toString('utf8'); let body = {}; try { body = raw ? JSON.parse(raw) : {}; } catch { body = { raw }; } resolve({ method: req.method, headers: req.headers, body }); }); }); } function assertMockProviderContract(mock) { assert(mock.state.referenceRequests.length >= 1, 'mock reference provider should receive a request'); assert(mock.state.evidenceRequests.length >= 1, 'mock evidence provider should receive a request'); const reference = mock.state.referenceRequests[0]; assert(reference.method === 'POST', 'reference provider should receive POST'); assert(reference.body.contractVersion === 'tihao-provider-v1', 'reference request should include contractVersion'); assert(Array.isArray(reference.body.referenceLinks) && reference.body.referenceLinks.length >= 2, 'reference request should include referenceLinks'); assert(Array.isArray(reference.body.referenceStyleAnchors) && reference.body.referenceStyleAnchors.length >= 1, 'reference request should include referenceStyleAnchors'); assert(reference.body.criteria && Array.isArray(reference.body.criteria.keywords), 'reference request should include criteria keywords'); const evidence = mock.state.evidenceRequests[0]; assert(evidence.method === 'POST', 'evidence provider should receive POST'); assert(evidence.headers.authorization === 'Bearer mock-evidence-token', 'evidence provider should receive Authorization header when token is supplied'); assert(evidence.body.contractVersion === 'tihao-provider-v1', 'evidence request should include contractVersion'); assert(Array.isArray(evidence.body.requestedCapabilities) && evidence.body.requestedCapabilities.includes('asr_signals'), 'evidence request should include requested capabilities'); assert(evidence.body.criteria && Array.isArray(evidence.body.criteria.referenceBaselines), 'evidence request should include reference baselines'); assert(evidence.body.criteria && Array.isArray(evidence.body.criteria.referenceResources), 'evidence request should include reference resources'); assert(JSON.stringify(evidence.body.criteria.referenceResources).includes('reference-video.mp4'), 'evidence request should include reference video URL resource'); assert(Array.isArray(evidence.body.creators) && evidence.body.creators.length >= 1, 'evidence request should include creators'); assert(evidence.body.creators[0].platformUserId || evidence.body.creators[0].displayName, 'evidence creator payload should include identity'); } 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); });