xiaobo ren
Fix 'Assignment to constant variable' error: change content from const to let in parallelAnalyzer
c440ffa
| /** | |
| * 并行分析器 - 6个Agent同时工作 | |
| * 实现渐进式SSE推送,用户感知到的等待时间 = 最快Agent的返回时间 | |
| * K线分为过去(出生到今年)和未来(今年到100岁)两个并行请求,提升生成速度 | |
| */ | |
| import fetch from 'node-fetch'; | |
| import { nanoid } from 'nanoid'; | |
| import { | |
| AGENT_PROMPTS, | |
| AGENT_CORE_PROMPT, | |
| AGENT_KLINE_PAST_PROMPT, | |
| AGENT_KLINE_FUTURE_PROMPT, | |
| AGENT_CAREER_PROMPT, | |
| AGENT_MARRIAGE_PROMPT, | |
| AGENT_CRYPTO_PROMPT, | |
| } from './agentPrompts.js'; | |
| import { generateFallbackKLine } from './baziCalculator.js'; | |
| import { buildApiRequest, parseApiResponse } from './apiConfig.js'; | |
| // 为不同Agent分配最适合的模型 | |
| const AGENT_MODEL_ASSIGNMENT = { | |
| core: 'grok-4-1-fast-reasoning', // 核心命理 - 逻辑推理强 | |
| kline_past: 'gpt-5-mini', // 过去K线 - 数据结构化强 | |
| kline_future: 'gpt-5-mini', // 未来K线 - 数据结构化强 | |
| career: 'gpt-5-mini', // 事业财富 - 综合能力 | |
| marriage: 'grok-4-1-fast-non-reasoning', // 婚姻健康 - 快速响应 | |
| crypto: 'grok-4-0709', // 币圈分析 - 币圈知识丰富 | |
| }; | |
| // 备用模型列表 | |
| const FALLBACK_MODELS = [ | |
| 'gpt-5-mini', | |
| 'gpt-4.1', | |
| 'gpt-4o', | |
| 'grok-4-0709', | |
| 'grok-4-1-fast-reasoning', | |
| 'claude-sonnet-4-5', | |
| 'claude-3-5-haiku-20241022', | |
| ]; | |
| /** | |
| * 发送SSE事件 | |
| */ | |
| export const sendSSE = (res, event, data) => { | |
| if (!res.writableEnded) { | |
| res.write(`event: ${event}\n`); | |
| res.write(`data: ${JSON.stringify(data)}\n\n`); | |
| } | |
| }; | |
| /** | |
| * 单个Agent请求 | |
| */ | |
| const makeAgentRequest = async (agentType, model, systemPrompt, userPrompt, timeoutMs = 60000) => { | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), timeoutMs); | |
| try { | |
| console.log(`[Agent:${agentType}] 使用模型 ${model} 开始请求...`); | |
| const startTime = Date.now(); | |
| // 使用新的 API 配置系统 | |
| const apiRequest = buildApiRequest(model, systemPrompt, userPrompt, 0.6); | |
| const response = await fetch(apiRequest.url, { | |
| method: 'POST', | |
| headers: apiRequest.headers, | |
| signal: controller.signal, | |
| body: JSON.stringify(apiRequest.body), | |
| }); | |
| clearTimeout(timeoutId); | |
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); | |
| if (!response.ok) { | |
| const errText = await response.text(); | |
| console.warn(`[Agent:${agentType}] 请求失败 (${elapsed}s): ${response.status} - ${errText.substring(0, 200)}`); | |
| return { success: false, agentType, error: `HTTP ${response.status}`, elapsed }; | |
| } | |
| const responseText = await response.text(); | |
| let jsonResult; | |
| try { | |
| jsonResult = JSON.parse(responseText); | |
| } catch (e) { | |
| console.warn(`[Agent:${agentType}] JSON解析失败 (${elapsed}s)`); | |
| return { success: false, agentType, error: 'INVALID_API_RESPONSE', elapsed }; | |
| } | |
| // 使用新的响应解析系统 | |
| let content = parseApiResponse(jsonResult, model); | |
| if (!content) { | |
| return { success: false, agentType, error: 'EMPTY_RESPONSE', elapsed }; | |
| } | |
| // 清理内容 | |
| content = content.trim(); | |
| content = content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim(); | |
| content = content.replace(/^[\s\S]*?(?=\{)/m, ''); | |
| if (content.startsWith('```json')) content = content.slice(7); | |
| else if (content.startsWith('```')) content = content.slice(3); | |
| if (content.endsWith('```')) content = content.slice(0, -3); | |
| content = content.trim(); | |
| const jsonStart = content.indexOf('{'); | |
| const jsonEnd = content.lastIndexOf('}'); | |
| if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) { | |
| content = content.slice(jsonStart, jsonEnd + 1); | |
| } | |
| let data; | |
| try { | |
| data = JSON.parse(content); | |
| } catch (parseErr) { | |
| console.warn(`[Agent:${agentType}] 内容JSON解析失败 (${elapsed}s): ${content.substring(0, 100)}`); | |
| return { success: false, agentType, error: 'INVALID_JSON_FORMAT', elapsed }; | |
| } | |
| console.log(`[Agent:${agentType}] ✓ 成功 (${elapsed}s)`); | |
| return { success: true, agentType, data, elapsed, model }; | |
| } catch (error) { | |
| clearTimeout(timeoutId); | |
| if (error.name === 'AbortError') { | |
| console.warn(`[Agent:${agentType}] 请求超时`); | |
| return { success: false, agentType, error: 'TIMEOUT' }; | |
| } | |
| console.warn(`[Agent:${agentType}] 请求异常: ${error.message}`); | |
| return { success: false, agentType, error: error.message }; | |
| } | |
| }; | |
| /** | |
| * 验证Agent返回数据是否完整 | |
| */ | |
| const validateAgentResponse = (agentType, data) => { | |
| if (!data || typeof data !== 'object') return false; | |
| const requiredFields = { | |
| core: ['summary', 'personality'], | |
| career: ['industry', 'wealth'], | |
| marriage: ['marriage', 'health'], | |
| crypto: ['crypto'], | |
| kline_past: ['chartPoints'], | |
| kline_future: ['chartPoints'], | |
| }; | |
| const fields = requiredFields[agentType] || []; | |
| for (const field of fields) { | |
| if (!data[field] || (typeof data[field] === 'string' && data[field].trim().length < 10)) { | |
| console.warn(`[Agent:${agentType}] 字段 ${field} 缺失或内容太短`); | |
| return false; | |
| } | |
| } | |
| return true; | |
| }; | |
| /** | |
| * 带重试的Agent请求 | |
| */ | |
| const makeAgentRequestWithRetry = async (agentType, apiBaseUrl, apiKey, systemPrompt, userPrompt, maxRetries = 2) => { | |
| const primaryModel = AGENT_MODEL_ASSIGNMENT[agentType] || 'gemini-1.5-pro'; | |
| const modelsToTry = [primaryModel, ...FALLBACK_MODELS.filter(m => m !== primaryModel)]; | |
| for (const model of modelsToTry) { | |
| for (let attempt = 1; attempt <= maxRetries; attempt++) { | |
| const result = await makeAgentRequest(agentType, model, systemPrompt, userPrompt); | |
| if (result.success) { | |
| // 验证返回数据是否完整 | |
| if (validateAgentResponse(agentType, result.data)) { | |
| return result; | |
| } | |
| console.warn(`[Agent:${agentType}] 模型 ${model} 返回数据不完整,尝试重新请求...`); | |
| } | |
| // 如果是最后一次尝试这个模型,切换到下一个模型 | |
| if (attempt === maxRetries) { | |
| console.warn(`[Agent:${agentType}] 模型 ${model} 失败,尝试备用模型...`); | |
| } else { | |
| // 等待后重试 | |
| await new Promise(r => setTimeout(r, 1500)); | |
| } | |
| } | |
| } | |
| return { success: false, agentType, error: 'ALL_ATTEMPTS_FAILED' }; | |
| }; | |
| /** | |
| * 构建Agent用户提示词 | |
| */ | |
| const buildAgentUserPrompt = (input, skeletonData, agentType) => { | |
| const genderStr = input.gender === 'Male' ? '男 (乾造)' : '女 (坤造)'; | |
| // 精简的时间线数据 | |
| const timelineStr = JSON.stringify(skeletonData.timeline.slice(0, 30).map(t => ({ | |
| a: t.age, | |
| y: t.year, | |
| gz: t.ganZhi, | |
| dy: t.daYun | |
| }))); | |
| const baseInfo = ` | |
| 【命主信息】 | |
| 性别:${genderStr} | |
| 姓名:${input.name || '未提供'} | |
| 出生年份:${input.birthYear}年 | |
| 出生地点:${input.birthPlace || '未提供'} | |
| 【八字四柱】 | |
| 年柱:${skeletonData.bazi[0]} | |
| 月柱:${skeletonData.bazi[1]} | |
| 日柱:${skeletonData.bazi[2]} | |
| 时柱:${skeletonData.bazi[3]} | |
| 【大运信息】 | |
| 起运年龄:${skeletonData.startAge} 岁 | |
| 大运顺逆:${skeletonData.direction} | |
| `; | |
| // 根据Agent类型添加特定信息 | |
| const currentYear = new Date().getFullYear(); | |
| const birthYear = parseInt(input.birthYear, 10); | |
| const currentAge = currentYear - birthYear + 1; | |
| switch (agentType) { | |
| case 'kline_past': { | |
| // 过去K线:从出生到今年 | |
| const pastTimeline = skeletonData.timeline.filter(t => t.year <= currentYear); | |
| const pastTimelineStr = JSON.stringify(pastTimeline.map(t => ({ | |
| a: t.age, | |
| y: t.year, | |
| gz: t.ganZhi, | |
| dy: t.daYun | |
| }))); | |
| return baseInfo + `\n【当前年份】${currentYear}年(${currentAge}岁)\n【待填充的过去时间轴(出生到今年)】\n${pastTimelineStr}`; | |
| } | |
| case 'kline_future': { | |
| // 未来K线:从今年到100岁 | |
| const futureTimeline = skeletonData.timeline.filter(t => t.year >= currentYear); | |
| const futureTimelineStr = JSON.stringify(futureTimeline.map(t => ({ | |
| a: t.age, | |
| y: t.year, | |
| gz: t.ganZhi, | |
| dy: t.daYun | |
| }))); | |
| return baseInfo + `\n【当前年份】${currentYear}年(${currentAge}岁)\n【待填充的未来时间轴(今年到100岁)】\n${futureTimelineStr}`; | |
| } | |
| case 'core': | |
| return baseInfo + `\n【前30年时间轴参考】\n${timelineStr}\n\n请深度分析此八字的核心命理结构。`; | |
| case 'career': | |
| return baseInfo + `\n请专注分析此八字的事业财富运势。`; | |
| case 'marriage': | |
| return baseInfo + `\n请专注分析此八字的婚姻感情和健康状况。`; | |
| case 'crypto': | |
| return baseInfo + `\n当前年份:${currentYear}\n请专注分析此八字的币圈交易运势和投机潜力。`; | |
| default: | |
| return baseInfo; | |
| } | |
| }; | |
| /** | |
| * 并行执行6个Agent分析 | |
| * @param {object} input - 用户输入 | |
| * @param {object} skeletonData - 时间线骨架 | |
| * @param {object} res - SSE响应对象 | |
| * @param {function} onProgress - 进度回调 | |
| */ | |
| export const runParallelAgents = async (input, skeletonData, res, onProgress) => { | |
| // 不再需要固定的 API URL 和 Key,由 apiConfig.js 根据模型自动选择 | |
| const apiBaseUrl = null; // 不再使用 | |
| const apiKey = null; // 不再使用 | |
| const agents = [ | |
| { type: 'core', prompt: AGENT_CORE_PROMPT, priority: 1 }, | |
| { type: 'kline_past', prompt: AGENT_KLINE_PAST_PROMPT, priority: 2 }, | |
| { type: 'kline_future', prompt: AGENT_KLINE_FUTURE_PROMPT, priority: 3 }, | |
| { type: 'career', prompt: AGENT_CAREER_PROMPT, priority: 4 }, | |
| { type: 'marriage', prompt: AGENT_MARRIAGE_PROMPT, priority: 5 }, | |
| { type: 'crypto', prompt: AGENT_CRYPTO_PROMPT, priority: 6 }, | |
| ]; | |
| onProgress(`启动 ${agents.length} 个专业Agent并行分析...`); | |
| const results = {}; | |
| const completedAgents = []; | |
| // 创建所有Agent的Promise | |
| const agentPromises = agents.map(agent => { | |
| const userPrompt = buildAgentUserPrompt(input, skeletonData, agent.type); | |
| return makeAgentRequestWithRetry( | |
| agent.type, | |
| apiBaseUrl, | |
| apiKey, | |
| agent.prompt, | |
| userPrompt | |
| ).then(result => { | |
| if (result.success) { | |
| results[agent.type] = result.data; | |
| completedAgents.push(agent.type); | |
| // 立即推送该Agent的结果 | |
| sendSSE(res, `agent_${agent.type}_complete`, { | |
| agentType: agent.type, | |
| data: result.data, | |
| elapsed: result.elapsed, | |
| model: result.model, | |
| completedCount: completedAgents.length, | |
| totalAgents: agents.length, | |
| }); | |
| onProgress(`✓ Agent[${agent.type}] 完成 (${result.elapsed}s) - 已完成 ${completedAgents.length}/${agents.length}`); | |
| } else { | |
| onProgress(`✗ Agent[${agent.type}] 失败: ${result.error}`); | |
| sendSSE(res, `agent_${agent.type}_error`, { | |
| agentType: agent.type, | |
| error: result.error, | |
| }); | |
| } | |
| return result; | |
| }); | |
| }); | |
| // 等待所有Agent完成 | |
| const allResults = await Promise.allSettled(agentPromises); | |
| // 汇总结果 | |
| const successCount = allResults.filter(r => r.status === 'fulfilled' && r.value?.success).length; | |
| onProgress(`并行分析完成: ${successCount}/${agents.length} 成功`); | |
| return { | |
| success: successCount > 0, | |
| results, | |
| completedAgents, | |
| totalAgents: agents.length, | |
| successCount, | |
| }; | |
| }; | |
| /** | |
| * 基于八字生成事业财富降级内容 | |
| * @param {object} core - 核心分析结果 | |
| * @param {object} skeletonData - 时间线骨架数据 | |
| */ | |
| const generateCareerFallback = (core, skeletonData) => { | |
| const bazi = core?.bazi || skeletonData?.bazi || []; | |
| const dayPillar = bazi[2] || ''; | |
| const dayGan = dayPillar ? dayPillar[0] : ''; | |
| // 基于日主五行推断适合行业 | |
| const dayGanIndustries = { | |
| '甲': { industries: '教育、文化、出版、环保、园艺', element: '木' }, | |
| '乙': { industries: '设计、美容、花艺、服装、医药', element: '木' }, | |
| '丙': { industries: '能源、娱乐、餐饮、传媒、演艺', element: '火' }, | |
| '丁': { industries: '科技、电子、文化创意、教育培训', element: '火' }, | |
| '戊': { industries: '房地产、建筑、矿业、农业、物流', element: '土' }, | |
| '己': { industries: '农业、食品、陶瓷、中介、服务业', element: '土' }, | |
| '庚': { industries: '金融、机械、汽车、五金、军工', element: '金' }, | |
| '辛': { industries: '珠宝、精密仪器、法律、金融、美容', element: '金' }, | |
| '壬': { industries: '物流、航运、旅游、水产、饮料', element: '水' }, | |
| '癸': { industries: '咨询、教育、医疗、心理、艺术', element: '水' }, | |
| }; | |
| const info = dayGanIndustries[dayGan] || { industries: '综合服务类行业', element: '平衡' }; | |
| const industry = `根据八字日主「${dayGan || '未知'}」分析,您五行属${info.element},事业适合方向包括:${info.industries}。日主强弱影响事业发展模式,建议结合实际情况选择最适合自己的发展道路。命局中官杀星代表事业机遇,财星代表财富获取能力,需综合判断以获得最佳事业规划。`; | |
| const wealth = `从财运角度分析,八字中财星的强弱决定了财富获取的方式和规模。${dayGan ? `日主${dayGan}` : '您的命局'}具有一定的理财天赋,建议稳健投资为主。正财代表稳定收入如工资薪金,偏财代表投资理财等非固定收入。根据大运流年的不同,财运会有起伏变化,宜把握财运旺盛的年份积极进取。`; | |
| return { | |
| industry, | |
| industryScore: 6, | |
| wealth, | |
| wealthScore: 6, | |
| recommendedIndustries: [ | |
| { name: info.industries.split('、')[0], reason: `五行属${info.element},与命局相合` }, | |
| { name: info.industries.split('、')[1] || '综合服务', reason: '命理分析推荐' } | |
| ], | |
| wealthPattern: '正偏财兼有', | |
| wealthPotential: '中等偏上', | |
| }; | |
| }; | |
| /** | |
| * 合并多个Agent的结果为最终分析 | |
| * @param {object} agentResults - 各Agent返回的结果 | |
| * @param {object} skeletonData - 时间线骨架数据(用于K线降级) | |
| */ | |
| export const mergeAgentResults = (agentResults, skeletonData = null) => { | |
| const { core, kline_past, kline_future, career, marriage, crypto } = agentResults; | |
| // 如果career数据缺失,生成降级内容 | |
| const careerFallback = (!career?.industry || !career?.wealth) | |
| ? generateCareerFallback(core, skeletonData) | |
| : null; | |
| if (careerFallback) { | |
| console.log('[mergeAgentResults] Career数据缺失,使用降级内容生成事业财富分析'); | |
| } | |
| // K线数据:合并过去和未来的K线数据 | |
| let chartPoints = []; | |
| const currentYear = new Date().getFullYear(); | |
| // 获取过去K线数据 | |
| const pastPoints = kline_past?.chartPoints || []; | |
| // 获取未来K线数据 | |
| const futurePoints = kline_future?.chartPoints || []; | |
| // 检查两个K线Agent是否都有数据 | |
| const hasPastData = pastPoints.length > 0; | |
| const hasFutureData = futurePoints.length > 0; | |
| if (hasPastData && hasFutureData) { | |
| // 最佳情况:两段K线数据都有,正常合并 | |
| const allPoints = [...pastPoints]; | |
| // 未来K线避免重复年份 | |
| for (const point of futurePoints) { | |
| if (!allPoints.some(p => p.year === point.year && p.age === point.age)) { | |
| allPoints.push(point); | |
| } | |
| } | |
| chartPoints = allPoints.sort((a, b) => a.age - b.age); | |
| console.log(`[mergeAgentResults] K线完整合并: 过去${pastPoints.length}年 + 未来${futurePoints.length}年 = 总${chartPoints.length}年`); | |
| } else if (hasPastData || hasFutureData) { | |
| // 部分数据:只有一段K线数据,使用fallback补全缺失部分 | |
| console.warn(`[mergeAgentResults] K线数据不完整: 过去=${pastPoints.length}年, 未来=${futurePoints.length}年,使用fallback补全`); | |
| if (skeletonData) { | |
| const fallbackPoints = generateFallbackKLine(skeletonData); | |
| const existingYears = new Set([ | |
| ...pastPoints.map(p => p.year), | |
| ...futurePoints.map(p => p.year) | |
| ]); | |
| // 合并已有数据 | |
| const allPoints = [...pastPoints, ...futurePoints]; | |
| // 用fallback填补缺失的年份 | |
| for (const point of fallbackPoints) { | |
| if (!existingYears.has(point.year)) { | |
| allPoints.push(point); | |
| } | |
| } | |
| chartPoints = allPoints.sort((a, b) => a.age - b.age); | |
| console.log(`[mergeAgentResults] K线混合合并: AI数据${pastPoints.length + futurePoints.length}年 + Fallback补全 = 总${chartPoints.length}年`); | |
| } else { | |
| // 无skeleton数据,只能用现有数据 | |
| chartPoints = [...pastPoints, ...futurePoints].sort((a, b) => a.age - b.age); | |
| console.warn(`[mergeAgentResults] 无skeleton数据,仅使用部分K线: ${chartPoints.length}年`); | |
| } | |
| } else if (skeletonData) { | |
| // 两段K线都失败,完全使用降级算法 | |
| console.log('[mergeAgentResults] K线Agent全部失败,使用完整降级算法生成K线数据'); | |
| chartPoints = generateFallbackKLine(skeletonData); | |
| } | |
| // 最终数据验证:确保数据点数量足够(至少50年) | |
| const MIN_CHART_POINTS = 50; | |
| if (chartPoints.length < MIN_CHART_POINTS && skeletonData) { | |
| console.warn(`[mergeAgentResults] K线数据不足(${chartPoints.length}点 < ${MIN_CHART_POINTS}点),使用完整fallback替换`); | |
| chartPoints = generateFallbackKLine(skeletonData); | |
| console.log(`[mergeAgentResults] Fallback生成完成: ${chartPoints.length}年`); | |
| } | |
| // 合并过去和未来的关键事件 | |
| const pastEvents = kline_past?.pastEvents || core?.pastEvents || []; | |
| const futureEvents = kline_future?.futureEvents || core?.futureEvents || []; | |
| const keyYears = [ | |
| ...(kline_past?.keyYears || []), | |
| ...(kline_future?.keyYears || []) | |
| ].sort((a, b) => a.year - b.year); | |
| return { | |
| // 基础信息 | |
| bazi: core?.bazi || [], | |
| summary: core?.summary || '命理分析完成', | |
| summaryScore: core?.summaryScore || 5, | |
| // 核心Agent - 性格/六亲/风水 | |
| personality: core?.personality || '', | |
| personalityScore: core?.personalityScore || 5, | |
| family: core?.family || '', | |
| familyScore: core?.familyScore || 5, | |
| fengShui: core?.fengShui || '', | |
| fengShuiScore: core?.fengShuiScore || 5, | |
| // 个人特征 | |
| appearance: core?.appearance || '', | |
| bodyType: core?.bodyType || '', | |
| skin: core?.skin || '', | |
| characterSummary: core?.characterSummary || '', | |
| // 事业Agent - 使用fallback如果原数据缺失 | |
| industry: career?.industry || careerFallback?.industry || '暂无事业分析,请稍后重试', | |
| industryScore: career?.industryScore || careerFallback?.industryScore || 5, | |
| wealth: career?.wealth || careerFallback?.wealth || '暂无财富分析,请稍后重试', | |
| wealthScore: career?.wealthScore || careerFallback?.wealthScore || 5, | |
| // 婚姻健康Agent | |
| marriage: marriage?.marriage || '', | |
| marriageScore: marriage?.marriageScore || 5, | |
| health: marriage?.health || '', | |
| healthScore: marriage?.healthScore || 5, | |
| healthBodyParts: marriage?.healthBodyParts || [], | |
| // 币圈Agent | |
| crypto: crypto?.crypto || '', | |
| cryptoScore: crypto?.cryptoScore || 5, | |
| cryptoYear: crypto?.cryptoYear || '待定', | |
| cryptoStyle: crypto?.cryptoStyle || '现货定投', | |
| // K线Agent - 使用已计算的chartPoints(含降级逻辑) | |
| chartPoints: chartPoints, | |
| // 运势预测 | |
| monthlyFortune: core?.monthlyFortune || marriage?.monthlyFortune || '', | |
| monthlyHighlights: core?.monthlyHighlights || [], | |
| yearlyFortune: core?.yearlyFortune || career?.yearlyFortune || '', | |
| yearlyKeyEvents: core?.yearlyKeyEvents || career?.yearlyKeyEvents || [], | |
| // 幸运元素 | |
| luckyColors: core?.luckyColors || [], | |
| luckyDirections: core?.luckyDirections || [], | |
| luckyZodiac: core?.luckyZodiac || [], | |
| luckyNumbers: core?.luckyNumbers || [], | |
| // 重点日期和事件 | |
| keyDatesThisYear: core?.keyDatesThisYear || [], | |
| keyDatesThisMonth: core?.keyDatesThisMonth || [], | |
| pastEvents: pastEvents, | |
| futureEvents: futureEvents, | |
| keyYears: keyYears, | |
| }; | |
| }; | |
| export default { | |
| runParallelAgents, | |
| mergeAgentResults, | |
| sendSSE, | |
| }; | |