| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const { parseBrief } = require('../mcp/src/features/tihao-sourcing/brief-parser');
- function main() {
- const repoRoot = path.resolve(__dirname, '..', '..');
- const docsDir = path.join(repoRoot, 'docs');
- const dhaBrief = path.join(repoRoot, 'dha_brief.xlsx');
- if (fs.existsSync(dhaBrief)) {
- const parsed = parseBrief({ brief: dhaBrief });
- assert(parsed.brand === 'DHA', 'DHA xlsx should parse product as brand');
- assert(parsed.platforms.length === 1 && parsed.platforms[0] === 'xiaohongshu', 'DHA xlsx should parse Xiaohongshu platform');
- assert(parsed.keywords.includes('DHA') && parsed.keywords.includes('营养师'), 'DHA xlsx should parse maternal nutrition keywords');
- assert(parsed.performanceRequirements.includes('CPE≤3'), 'DHA xlsx should parse CPE requirement');
- assert(parsed.referenceLinks.length === 4, 'DHA xlsx should parse 4 reference account links');
- assert(parsed.referenceStyleAnchors.length >= 4, 'DHA xlsx should build reference style anchors');
- assert(parsed.referenceSignals.includes('干货科普') && parsed.referenceSignals.includes('618合集'), 'DHA xlsx should parse reference signals');
- }
- const markdownBrief = findBriefFile(docsDir, '.md');
- if (markdownBrief) {
- const parsed = parseBrief({ brief: markdownBrief });
- assert(parsed.brand.includes('花漾肌研'), 'markdown brief should parse brand');
- assert(parsed.targetCount === 32, 'markdown brief should parse target count sum');
- assert(parsed.platformTargets.xiaohongshu === 20, 'markdown brief should parse Xiaohongshu target');
- assert(parsed.platformTargets.douyin === 8, 'markdown brief should parse Douyin target');
- assert(parsed.platformTargets.bilibili === 4, 'markdown brief should parse Bilibili target');
- assert(parsed.fanRange.min === 20000 && parsed.fanRange.max === 500000, 'markdown brief should parse fan range');
- assert(parsed.referenceSignals.includes('素人感'), 'markdown brief should parse reference style preference');
- }
- const htmlXlsBrief = findBriefFile(docsDir, '.xls');
- if (htmlXlsBrief) {
- const parsed = parseBrief({ brief: htmlXlsBrief });
- assert(parsed.brand.includes('花漾肌研'), 'html xls brief should parse brand from key-value cells');
- assert(parsed.targetCount === 32, 'html xls brief should parse target count sum');
- assert(parsed.platformTargets.xiaohongshu === 20, 'html xls brief should parse Xiaohongshu target');
- assert(parsed.platformTargets.douyin === 8, 'html xls brief should parse Douyin target');
- assert(parsed.platformTargets.bilibili === 4, 'html xls brief should parse Bilibili target');
- assert(parsed.fanRange.min === 20000 && parsed.fanRange.max === 500000, 'html xls brief should parse horizontal fan range table');
- assert(parsed.referenceSignals.includes('真实测评') && parsed.referenceSignals.includes('成分党'), 'html xls brief should parse reference keyword signals');
- }
- assertNaturalLanguageBriefCoverage();
- console.log('brief fixture smoke ok');
- }
- function findBriefFile(dir, ext) {
- if (!fs.existsSync(dir)) return '';
- return fs.readdirSync(dir)
- .map(name => path.join(dir, name))
- .find(file => fs.statSync(file).isFile() && path.basename(file).includes('Brief') && file.endsWith(ext)) || '';
- }
- function assertNaturalLanguageBriefCoverage() {
- const cases = [
- {
- name: 'skincare',
- text: [
- '客户名称:明澈肌研',
- '产品:敏感肌屏障修护精华',
- '平台与人数:小红书 6 人,抖音 2 人',
- '关键词:敏感肌、修护、成分、真实测评',
- '粉丝范围:2万-50万',
- '预算范围:3000-18000',
- '偏好真实体验、素人感,不要硬广。'
- ].join('\n'),
- assert(parsed) {
- assert(parsed.brand === '明澈肌研', 'skincare should parse brand');
- assert(parsed.category === '护肤美妆', 'skincare should infer beauty category');
- assert(parsed.platformTargets.xiaohongshu === 6 && parsed.platformTargets.douyin === 2, 'skincare should parse platform targets');
- assert(parsed.fanRange.min === 20000 && parsed.fanRange.max === 500000, 'skincare should parse fan range');
- assert(parsed.keywords.includes('敏感肌') && parsed.keywords.includes('修护'), 'skincare should parse skincare keywords');
- }
- },
- {
- name: 'real-douyin-beauty-brief',
- text: [
- '平台:抖音',
- '账号类型:美妆护肤',
- '粉丝要求:1k-10w',
- '单个预算:2000内',
- '数据要求:主页互动数据200+',
- '备注:产品是客单价比较高的面膜,想找调性高的美妆护肤博主,想要主页之前合作过大牌护肤品的',
- '',
- '参考账号:',
- 'https://www.douyin.com/user/MS4wLjABAAAAZ31qrM27_cxcps8cc2sGKlnUWIOUDuVmaGB7BhNEpYAcZfhxjgicQ3VVXfRNgcWv'
- ].join('\n'),
- assert(parsed) {
- assert(parsed.category === '护肤美妆', 'real douyin beauty brief should infer beauty category');
- assert(parsed.platforms.length === 1 && parsed.platforms[0] === 'douyin', 'real douyin beauty brief should parse Douyin only');
- assert(parsed.fanRange.min === 1000 && parsed.fanRange.max === 100000, 'real douyin beauty brief should parse 1k-10w fan range');
- assert(parsed.budgetRange.min === 0 && parsed.budgetRange.max === 2000, 'real douyin beauty brief should parse 2000 upper-bound budget');
- assert(parsed.performanceRequirements.includes('主页互动数据≥200'), 'real douyin beauty brief should parse homepage interaction requirement');
- assert(parsed.keywords.includes('护肤') && parsed.keywords.includes('美妆') && parsed.keywords.includes('面膜'), 'real douyin beauty brief should keep beauty keywords');
- assert(parsed.referenceLinks.length === 1, 'real douyin beauty brief should parse reference account link');
- }
- },
- {
- name: 'healthy-snack',
- text: [
- '品牌:谷物星球',
- '本次推广产品:高蛋白低脂燕麦棒',
- '目标:找小红书 5 个、抖音 3 个健身/控糖/办公室零食博主',
- '粉丝数范围:1万-30万',
- '单篇预算:1000-8000',
- '风格偏生活化开箱、真实吃法、成分党,不要医美和夸大减肥。'
- ].join('\n'),
- assert(parsed) {
- assert(parsed.brand === '谷物星球', 'healthy snack should parse brand');
- assert(parsed.category === '食品餐饮', 'healthy snack should infer food category instead of beauty');
- assert(parsed.targetCount === 8, 'healthy snack should parse platform target sum');
- assert(parsed.fanRange.min === 10000 && parsed.fanRange.max === 300000, 'healthy snack should parse fan range');
- assert(parsed.keywords.includes('控糖') && parsed.keywords.includes('办公室零食'), 'healthy snack should parse food creator keywords');
- assert(parsed.exclusions.includes('医美') && parsed.exclusions.includes('夸大减肥'), 'healthy snack should parse exclusions');
- }
- },
- {
- name: 'home-cleaning',
- text: [
- '客户:净住研究所',
- '产品:厨房重油污清洁喷雾',
- '需求人数:小红书 8 人',
- '要求:家居清洁、收纳、租房生活、妈妈人群,粉丝 5000-20万,预算 800-5000',
- '排除:硬广、低质搬运、争议账号。'
- ].join('\n'),
- assert(parsed) {
- assert(parsed.brand === '净住研究所', 'home cleaning should parse brand');
- assert(parsed.category === '家居生活', 'home cleaning should infer home category');
- assert(parsed.platforms.length === 1 && parsed.platforms[0] === 'xiaohongshu', 'home cleaning should parse Xiaohongshu only');
- assert(parsed.fanRange.min === 5000 && parsed.fanRange.max === 200000, 'home cleaning should not read budget as fan range');
- assert(parsed.budgetRange.min === 800 && parsed.budgetRange.max === 5000, 'home cleaning should parse budget on same line');
- assert(parsed.keywords.includes('清洁') && parsed.keywords.includes('收纳'), 'home cleaning should parse home keywords');
- }
- },
- {
- name: 'b2b-saas',
- text: [
- '品牌:FlowOps',
- '产品:企业项目管理 SaaS',
- '需求:找 B站 4 个、抖音 2 个效率工具/创业/企业管理类内容创作者',
- '粉丝:1万-40万',
- '预算:3000-20000',
- '偏好教程型、案例型、职场效率,不要纯娱乐账号。'
- ].join('\n'),
- assert(parsed) {
- assert(parsed.brand === 'FlowOps', 'B2B SaaS should parse brand');
- assert(parsed.category === 'B2B软件', 'B2B SaaS should infer software category');
- assert(parsed.platformTargets.bilibili === 4 && parsed.platformTargets.douyin === 2, 'B2B SaaS should parse Bilibili and Douyin targets');
- assert(parsed.keywords.includes('效率工具') && parsed.keywords.includes('企业管理'), 'B2B SaaS should parse software keywords');
- assert(parsed.exclusions.includes('纯娱乐') || parsed.exclusions.includes('纯娱乐账号'), 'B2B SaaS should parse entertainment exclusion');
- }
- },
- {
- name: 'local-food',
- text: [
- '品牌:山城小面加盟',
- '任务:找本地生活探店、餐饮创业类达人,重庆/成都优先',
- '平台:抖音 10 人',
- '粉丝 2万-80万,预算 2000-12000',
- '重点看同城转化、门店拍摄经验,排除泛娱乐流量号。'
- ].join('\n'),
- assert(parsed) {
- assert(parsed.brand === '山城小面加盟', 'local food should parse brand');
- assert(parsed.category === '食品餐饮', 'local food should infer food/local category');
- assert(parsed.platforms.length === 1 && parsed.platforms[0] === 'douyin', 'local food should parse Douyin only');
- assert(parsed.targetCount === 10, 'local food should parse target count');
- assert(parsed.fanRange.min === 20000 && parsed.fanRange.max === 800000, 'local food should parse fan range and ignore budget');
- assert(parsed.region.includes('重庆') && parsed.region.includes('成都'), 'local food should parse city priority');
- assert(parsed.keywords.includes('本地生活') && parsed.keywords.includes('同城转化'), 'local food should parse local conversion keywords');
- }
- },
- {
- name: 'fashion-product-chat-refinement',
- text: [
- '客户:南栖衣橱',
- 'Brief:找精致生活/时尚穿搭类账号,小红书 4 人',
- '粉丝:1万-20万,预算:300-3000',
- '数据要求:CPE≤3,CP1≤20,CPM≤80',
- '不要纯自拍。'
- ].join('\n'),
- input: {
- productIntro: '产品是港风通勤女装,偏成熟风、轻熟高级感,不是泛生活方式。',
- chatRecords: [
- '客户说要看最近一个月有更新,平均点赞不要百赞以下。',
- '如果多篇都是同一个人评论,怀疑刷评,先不要强推。'
- ],
- referenceAccounts: ['港风穿搭Alice']
- },
- assert(parsed) {
- assert(parsed.brand === '南栖衣橱', 'fashion should parse brand');
- assert(parsed.category === '服饰穿搭', 'fashion product/chat should refine broad lifestyle into apparel category');
- assert(parsed.contextSources.product && parsed.contextSources.chat && parsed.contextSources.reference, 'fashion should mark product/chat/reference context sources');
- assert(parsed.platformTargets.xiaohongshu === 4, 'fashion should parse Xiaohongshu target');
- assert(parsed.keywords.includes('港风') && parsed.keywords.includes('成熟风'), 'fashion should extract subtype keywords from product/chat context');
- assert(parsed.keywords.includes('时尚穿搭') || parsed.keywords.includes('穿搭'), 'fashion should keep broad fashion keyword');
- assert(parsed.exclusions.includes('纯自拍') && !parsed.exclusions.includes('穿搭'), 'fashion should not treat wearing/fashion as an exclusion');
- assert(parsed.performanceRequirements.includes('CPE≤3') && parsed.performanceRequirements.includes('CP1≤20') && parsed.performanceRequirements.includes('CPM≤80'), 'fashion should parse cost metrics');
- assert(parsed.performanceRequirements.includes('近30天平均点赞≥100'), 'fashion should turn low-like warning into average-like requirement');
- assert(parsed.performanceRequirements.includes('复核数据真实性/重复评论风险'), 'fashion should parse repeated-comment authenticity review');
- }
- }
- ];
- for (const item of cases) {
- const parsed = parseBrief({ briefText: item.text, ...(item.input || {}) });
- item.assert(parsed);
- }
- }
- function assert(condition, message) {
- if (!condition) throw new Error(message);
- }
- main();
|