#!/usr/bin/env node const fs = require('fs'); const http = require('http'); 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 out = path.join(os.tmpdir(), `tihao-homepage-provider-smoke-${Date.now()}`); const mock = await startMockProvider({ mode: 'ok' }); try { const result = await runHomepageSmoke({ mock, output: out, token: 'mock-homepage-token' }); assert(result.status === 'ok', 'homepage provider smoke should return ok'); assert(result.data.criteria.homepageEvidenceStatus.status === 'ok', 'homepage provider status should be ok'); assert(result.data.criteria.homepageEvidenceStatus.evidenceCount >= 2, 'homepage provider should report evidence count'); assert(result.data.candidates.some(item => item.homepageEvidence?.source === 'provider'), 'at least one client candidate should use provider homepage evidence'); await assertProviderVisualRiskDowngrade(); assertMockHomepageContract(mock); const csvPath = path.join(out, 'tihao-sourcing-client-list.csv'); const csv = fs.readFileSync(csvPath, 'utf8'); assert(csv.replace(/^\uFEFF/, '').startsWith(SOFTWARE_TABLE_HEADER.join(',')), 'csv should use software table header'); assert(!/sk-[A-Za-z0-9_-]{20,}|Authorization:\s*Bearer\s+[A-Za-z0-9._-]{8,}/i.test(JSON.stringify(result) + csv), 'outputs should not leak provider token'); await assertUnauthorizedFallback(); console.log(`homepage evidence provider smoke ok: ${out}`); } finally { await mock.close(); } } async function runHomepageSmoke({ mock, output, token }) { return runTihaoSourcing({ collectionMode: 'sample', briefText: [ '客户名称:花漾肌研', '平台与人数:小红书 6 人,抖音 2 人', '粉丝量范围:2万-50万', '预算范围:3000-18000', '偏好真实体验、素人感、成分拆解、敏感肌修护,不要硬广。' ].join('\n'), homepageEvidenceBaseUrl: mock.url, homepageEvidenceToken: token, homepageEvidenceLimit: 10, homepageEvidenceCreatorsLimit: 8, output }); } function startMockProvider({ mode }) { const state = { requests: [] }; const server = http.createServer((req, res) => { if (!req.url.startsWith('/homepage-evidence')) { res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'not_found' })); return; } let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { const payload = body ? JSON.parse(body) : {}; state.requests.push({ method: req.method, url: req.url, headers: req.headers, body: payload }); if (mode === 'unauthorized') { res.writeHead(401, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'hidden by client', detail: 'quota exceeded' })); return; } const creators = payload.creators || []; const records = creators.slice(0, 3).map((creator, index) => ({ platform: creator.platform, platformUserId: creator.platformUserId, displayName: creator.displayName, profileUrl: creator.profileUrl, homepageEvidence: { source: 'provider', recentContentWindow: 10, categoryConsistencyScore: 86 - index, visualQualityScore: 82 - index, toneConsistencyScore: 88 - index, confidence: 'provider', matchedTerms: ['真实体验', '敏感肌', '成分拆解'], riskHits: [], reviewNotes: ['provider返回近10篇内容证据', '封面统一且近期内容垂类一致'] }, recentPosts: [ { title: '敏感肌屏障修护真实体验' }, { title: '通勤护肤空瓶记录' } ] })); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ homepageEvidence: records })); }); }); return new Promise(resolve => { server.listen(0, '127.0.0.1', () => { const port = server.address().port; resolve({ url: `http://127.0.0.1:${port}`, state, close: () => new Promise(done => server.close(done)) }); }); }); } function assertMockHomepageContract(mock) { assert(mock.state.requests.length >= 1, 'homepage provider should receive a request'); const request = mock.state.requests[0]; assert(request.method === 'POST', 'homepage provider should receive POST'); assert(request.headers.authorization === 'Bearer mock-homepage-token', 'homepage provider should receive Authorization header when token is supplied'); assert(request.body.contractVersion === 'tihao-homepage-evidence-v1', 'homepage request should include contractVersion'); assert(Array.isArray(request.body.requestedCapabilities), 'homepage request should include requestedCapabilities'); for (const capability of ['recent_posts', 'cover_quality', 'category_consistency', 'tone_consistency', 'risk_review']) { assert(request.body.requestedCapabilities.includes(capability), `homepage request should include ${capability}`); } assert(request.body.limit === 10, 'homepage request should include requested limit'); assert(Array.isArray(request.body.creators) && request.body.creators.length > 0, 'homepage request should include creators'); assert(request.body.creators.length <= 8, 'homepage request should respect creators limit'); assert(request.body.creators.every(item => item.displayName && item.profileUrl), 'homepage request creators should include displayName and profileUrl'); } async function assertUnauthorizedFallback() { const out = path.join(os.tmpdir(), `tihao-homepage-provider-failure-${Date.now()}`); const mock = await startMockProvider({ mode: 'unauthorized' }); try { const result = await runHomepageSmoke({ mock, output: out, token: 'mock-homepage-token' }); assert(result.status === 'ok', 'homepage unauthorized fallback should still return ok'); assert(result.data.criteria.homepageEvidenceStatus.status === 'unauthorized_or_quota', 'homepage unauthorized fallback should keep structured status'); assert(result.data.candidates.some(item => item.homepageEvidence?.source !== 'provider'), 'homepage unauthorized fallback should use local evidence'); assertPublicOutputSafe({ result, out }); } finally { await mock.close(); } } async function assertProviderVisualRiskDowngrade() { const out = path.join(os.tmpdir(), `tihao-homepage-provider-risk-${Date.now()}`); const records = [{ platform: 'xiaohongshu', platformUserId: 'xhs-skincare-001', homepageEvidence: { source: 'provider', recentContentWindow: 10, categoryConsistencyScore: 92, visualQualityScore: 30, toneConsistencyScore: 90, confidence: 'provider', matchedTerms: ['敏感肌', '真实体验', '成分拆解'], visualRiskSignals: ['封面下沉', '封面混乱'], riskHits: [], reviewNotes: ['provider返回近10篇内容证据', '主页质感风险:封面下沉、封面混乱'] } }]; const fixture = path.join(out, 'homepage-risk.json'); fs.mkdirSync(out, { recursive: true }); fs.writeFileSync(fixture, JSON.stringify({ homepageEvidence: records }, null, 2), 'utf8'); const result = await runTihaoSourcing({ collectionMode: 'sample', briefText: [ '客户名称:花漾肌研', '平台:小红书', '粉丝量范围:2万-50万', '预算范围:3000-18000', '关键词:敏感肌、屏障修护、成分拆解、真实体验', '偏好真实体验、素人感、成分拆解、敏感肌修护。' ].join('\n'), homepageEvidenceFile: fixture, output: path.join(out, 'report') }); const risky = result.data.candidates.find(item => item.platformUserId === 'xhs-skincare-001'); assert(risky, 'provider visual-risk candidate should exist'); assert(risky.recommendStatus !== '强推荐', 'provider visual-risk candidate should not be strong recommendation'); assert((risky.homepageQualityRisks || []).includes('封面下沉'), 'provider visual-risk candidate should keep visual risk signals'); assert(String(risky.riskNote || '').includes('主页质感风险'), 'provider visual-risk candidate should explain homepage quality risk'); } function assertPublicOutputSafe({ 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), 'homepage provider public output should not expose raw 401/403'); assert(!/hidden by client|unauthorized|permission|quota exceeded/i.test(publicText), 'homepage provider public output should not expose raw upstream error text'); assert(!/Authorization\s*:/i.test(publicText), 'homepage provider public output should not expose Authorization header'); assert(!/Bearer\s+[A-Za-z0-9._~+/=-]+/i.test(publicText), 'homepage provider public output should not expose bearer token'); assert(!/sk-[A-Za-z0-9_-]{20,}/.test(publicText), 'homepage provider public output should not expose model token'); } 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); });