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 { 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; }