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