| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- import { config } from '../config.ts';
- interface BriefAnalysisResult {
- requirements: Array<{ label: string; value: string; confidence: number }>;
- searchCriteria: {
- platforms: string[];
- keywords: string[];
- fanRange: { min: number; max: number };
- budgetRange: { min: number; max: number };
- region?: string;
- gender?: string;
- contentTags?: string[];
- excludeTags?: string[];
- targetCount?: number;
- };
- }
- const BRIEF_ANALYSIS_PROMPT = `你是一个专业的媒介策划助手。请分析以下客户Brief文件内容,提取出结构化的投放需求。
- 要求提取以下信息:
- 1. 客户/品牌名称
- 2. 投放目标(种草、转化、品宣等)
- 3. 平台需求(小红书、抖音、B站等及各平台需求人数)
- 4. 粉丝数要求范围
- 5. 预算范围
- 6. 内容风格要求
- 7. 地区要求
- 8. 排除项(竞品、不合适的类型等)
- 9. 合作形式(图文、视频等)
- 请以JSON格式输出,包含requirements数组和searchCriteria对象。
- requirements格式:[{label: "字段名", value: "提取值", confidence: 0-100}]
- searchCriteria格式:{platforms: [], keywords: [], fanRange: {min, max}, budgetRange: {min, max}, region?, gender?, contentTags?: [], excludeTags?: [], targetCount?}
- 如果 Brief 写明达人数量,请额外输出 targetCount,表示最终需求达人总数(不是候选池数量)。
- Brief内容:
- `;
- export async function analyzeBrief(briefText: string): Promise<BriefAnalysisResult> {
- if (!config.llm.apiKey) {
- console.warn('[LLM] No API key configured, using fallback analysis');
- return fallbackAnalysis(briefText);
- }
- try {
- console.log('[LLM] 发起请求:', { url: `${config.llm.baseUrl}/chat/completions`, model: config.llm.model, briefLength: briefText.length });
- const response = await fetch(`${config.llm.baseUrl}/chat/completions`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${config.llm.apiKey}`,
- },
- body: JSON.stringify({
- model: config.llm.model,
- messages: [
- { role: 'system', content: '你是专业的媒介策划AI助手,擅长从Brief文档中提取结构化投放需求。你的回复必须是纯JSON格式,不要包含markdown代码块标记。' },
- { role: 'user', content: BRIEF_ANALYSIS_PROMPT + briefText },
- ],
- temperature: 0.3,
- }),
- });
- if (!response.ok) {
- const errorBody = await response.text();
- console.error(`[LLM] API error: ${response.status} ${response.statusText}`, errorBody);
- return fallbackAnalysis(briefText);
- }
- const data = await response.json();
- const content = data.choices?.[0]?.message?.content;
- if (!content) {
- return fallbackAnalysis(briefText);
- }
- // 提取 JSON(可能被 markdown 代码块包裹)
- let jsonStr = content.trim();
- const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
- if (codeBlockMatch) {
- jsonStr = codeBlockMatch[1].trim();
- }
- const parsed = JSON.parse(jsonStr);
- const result = {
- requirements: parsed.requirements || [],
- searchCriteria: parsed.searchCriteria || { platforms: [], keywords: [], fanRange: { min: 10000, max: 500000 }, budgetRange: { min: 2000, max: 20000 } },
- };
- result.searchCriteria.targetCount = normalizeTargetCount(result.searchCriteria.targetCount) || inferTargetCount(briefText, result.requirements);
- console.log('[LLM] 解析结果:', JSON.stringify(result, null, 2));
- return result;
- } catch (error) {
- console.error('[LLM] Analysis failed:', error);
- return fallbackAnalysis(briefText);
- }
- }
- function fallbackAnalysis(briefText: string): BriefAnalysisResult {
- // 简单的关键词匹配作为降级方案
- const platforms: string[] = [];
- if (briefText.includes('小红书') || briefText.includes('红书')) platforms.push('xiaohongshu');
- if (briefText.includes('抖音') || briefText.includes('TikTok')) platforms.push('douyin');
- if (briefText.includes('B站') || briefText.includes('哔哩哔哩')) platforms.push('bilibili');
- if (briefText.includes('微博')) platforms.push('weibo');
- if (briefText.includes('微信') || briefText.includes('公众号')) platforms.push('weixin');
- if (platforms.length === 0) platforms.push('xiaohongshu', 'douyin');
- const keywords: string[] = [];
- const keywordPatterns = ['护肤', '美妆', '母婴', '数码', '穿搭', '美食', '家居', '运动', '旅行', '教育'];
- for (const kw of keywordPatterns) {
- if (briefText.includes(kw)) keywords.push(kw);
- }
- if (keywords.length === 0) keywords.push('生活方式');
- return {
- requirements: [
- { label: '平台需求', value: platforms.join('、'), confidence: 75 },
- { label: '内容关键词', value: keywords.join('、'), confidence: 70 },
- { label: '预算范围', value: '2000-20000', confidence: 60 },
- { label: '粉丝范围', value: '1万-50万', confidence: 60 },
- ],
- searchCriteria: {
- platforms,
- keywords,
- fanRange: { min: 10000, max: 500000 },
- budgetRange: { min: 2000, max: 20000 },
- targetCount: inferTargetCount(briefText, []),
- },
- };
- }
- function normalizeTargetCount(value: unknown): number | undefined {
- const count = Number(value || 0);
- return count > 0 ? Math.ceil(count) : undefined;
- }
- function inferTargetCount(
- briefText: string,
- requirements: Array<{ label: string; value: string; confidence: number }>
- ): number | undefined {
- const text = `${briefText}\n${requirements.map((item) => `${item.label}:${item.value}`).join('\n')}`;
- const perCityMatch = text.match(/(\d+)\s*(?:个|座)?城市[\s\S]{0,40}?(\d+)\s*位\s*\/\s*城市/);
- if (perCityMatch) {
- return Number(perCityMatch[1]) * Number(perCityMatch[2]);
- }
- const cityCountMatch = text.match(/(\d+)\s*(?:个|座)?城市/);
- const perCityLooseMatch = text.match(/每(?:个)?城市\s*(\d+)\s*位|(\d+)\s*位\s*\/\s*城市/);
- if (cityCountMatch && perCityLooseMatch) {
- return Number(cityCountMatch[1]) * Number(perCityLooseMatch[1] || perCityLooseMatch[2]);
- }
- const directMatch = text.match(/(?:达人|博主|KOL|账号|人数)[^\d]{0,10}(\d+)\s*位|(\d+)\s*位\s*(?:达人|博主|KOL|账号)/i);
- if (directMatch) {
- return Number(directMatch[1] || directMatch[2]);
- }
- return undefined;
- }
|