From 94e1b124df30530e69cda90f88ac310e3120b621 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 21 Aug 2025 10:24:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0AI=E6=95=99=E7=BB=83?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=87=8D=E8=AE=B0=E5=BD=95=E5=92=8C=E5=88=86=E6=9E=90?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20-=20=E6=96=B0=E5=A2=9E=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=94=A8=E6=88=B7=E9=80=89=E6=8B=A9=E5=92=8C?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E8=A7=A3=E6=9E=90=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E4=BD=93=E9=AA=8C=E3=80=82=20-=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=BD=93=E9=87=8D=E8=AE=B0=E5=BD=95=E7=9A=84=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E5=92=8C=E8=B6=8B=E5=8A=BF=E5=88=86=E6=9E=90=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E7=94=A8=E6=88=B7=E5=8F=AF=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E4=BD=93=E9=87=8D=E5=8F=98=E5=8C=96=E5=8F=8A=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E5=BB=BA=E8=AE=AE=E3=80=82=20-=20=E6=89=A9=E5=B1=95DTO?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E4=BA=A4=E4=BA=92=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0=E7=9A=84=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E7=9A=84=E5=AE=8C=E6=95=B4=E6=80=A7=E3=80=82=20-=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AE=B0=E5=BD=95=EF=BC=8C=E6=8F=90=E5=8D=87=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=A8=B3=E5=AE=9A=E6=80=A7=E5=92=8C=E5=8F=AF=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ai-coach/ai-coach.service.ts | 755 ++++++++++++++++--------------- src/ai-coach/dto/ai-chat.dto.ts | 4 +- 2 files changed, 393 insertions(+), 366 deletions(-) diff --git a/src/ai-coach/ai-coach.service.ts b/src/ai-coach/ai-coach.service.ts index 5c40789..9d76185 100644 --- a/src/ai-coach/ai-coach.service.ts +++ b/src/ai-coach/ai-coach.service.ts @@ -148,59 +148,7 @@ export class AiCoachService { return messages; }; - /** - * 构建包含用户体重信息的系统提示 - */ - private async buildUserWeightContext(userId: string): Promise { - try { - const { currentWeight, history } = await this.getUserWeightHistory(userId, 10); - if (!currentWeight && history.length === 0) { - return ''; - } - - let context = '\n\n=== 用户体重信息 ===\n'; - - if (currentWeight) { - context += `当前体重:${currentWeight}kg\n`; - } - - if (history.length > 0) { - context += `体重历史记录(最近${Math.min(history.length, 10)}次):\n`; - - // 按时间倒序显示最近的记录 - const recentHistory = history.slice(0, 10).reverse(); - recentHistory.forEach((record, index) => { - const date = new Date(record.createdAt).toLocaleDateString('zh-CN'); - context += `${index + 1}. ${date}: ${record.weight}kg (${record.source === 'manual' ? '手动记录' : record.source === 'vision' ? '视觉识别' : '其他'})\n`; - }); - - // 分析体重趋势 - if (recentHistory.length >= 2) { - const trend = this.analyzeWeightTrend(recentHistory, currentWeight || recentHistory[0].weight); - context += `\n体重趋势分析:${trend}\n`; - - // 计算变化幅度 - const firstWeight = recentHistory[recentHistory.length - 1].weight; - const lastWeight = recentHistory[0].weight; - const totalChange = lastWeight - firstWeight; - const changePercent = ((totalChange / firstWeight) * 100).toFixed(1); - - if (Math.abs(totalChange) > 0.5) { - const changeDirection = totalChange > 0 ? '增加' : '减少'; - context += `总体变化:${changeDirection}${Math.abs(totalChange).toFixed(1)}kg (${changePercent}%)\n`; - } - } - } - - context += '\n请根据用户的体重记录趋势,提供个性化的健康建议。'; - - return context; - } catch (error) { - this.logger.error(`构建用户体重上下文失败: ${error instanceof Error ? error.message : String(error)}`); - return ''; - } - } @@ -213,197 +161,373 @@ export class AiCoachService { selectedChoiceId?: string; confirmationData?: any; }): Promise { - // 解析指令(如果以 # 开头) - const commandResult = this.parseCommand(params.userContent); + try { - // 上下文:系统提示 + 历史 + 当前用户消息 - const messages = await this.buildChatHistory(params.userId, params.conversationId); - if (params.systemNotice) { - messages.unshift({ role: 'system', content: params.systemNotice }); + // 1. 优先处理用户选择(选择逻辑) + if (params.selectedChoiceId) { + return await this.handleUserChoice({ + userId: params.userId, + conversationId: params.conversationId, + userContent: params.userContent, + selectedChoiceId: params.selectedChoiceId, + confirmationData: params.confirmationData + }); + } + + // 2. 解析用户输入的指令 + const commandResult = this.parseCommand(params.userContent); + + // 3. 构建基础消息上下文 + const messages = await this.buildChatHistory(params.userId, params.conversationId); + if (params.systemNotice) { + messages.unshift({ role: 'system', content: params.systemNotice }); + } + + // 4. 处理指令 + if (commandResult.command) { + return await this.handleCommand(commandResult, params, messages); + } + + // 5. 处理普通对话(包括营养话题检测) + return await this.handleNormalChat(params, messages); + } catch (error) { + this.logger.error(`streamChat error: ${error instanceof Error ? error.message : String(error)}`); + return this.createStreamFromText('处理失败,请稍后重试'); + } + } + + /** + * 处理用户选择 + */ + private async handleUserChoice(params: { + userId: string; + conversationId: string; + userContent: string; + selectedChoiceId: string; + confirmationData?: any; + }): Promise { + + // 处理体重趋势分析选择 + if (params.selectedChoiceId === 'trend_analysis' && params.confirmationData?.weightRecordData) { + return await this.handleWeightTrendAnalysis({ + userId: params.userId, + conversationId: params.conversationId, + confirmationData: params.confirmationData + }); } - this.logger.log(`commandResult: ${JSON.stringify(commandResult, null, 2)}`); + // 处理饮食确认选择 + if (params.selectedChoiceId && params.confirmationData) { + return await this.handleDietConfirmation({ + userId: params.userId, + conversationId: params.conversationId, + selectedChoiceId: params.selectedChoiceId, + confirmationData: params.confirmationData + }); + } + + // 其他选择类型的处理... + throw new Error(`未知的选择类型: ${params.selectedChoiceId}`); + } + + /** + * 处理指令 + */ + private async handleCommand( + commandResult: CommandResult, + params: any, + messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> + ): Promise { - // 处理体重记录指令 if (commandResult.command === 'weight') { - const textWeightInfo = await this.processWeightFromText(params.userId, params.userContent); - if (textWeightInfo.systemNotice) { - params.systemNotice = textWeightInfo.systemNotice; - } + return await this.handleWeightCommand(params); + } - // 为体重相关话题提供用户体重信息上下文 - const weightContext = await this.buildUserWeightContext(params.userId); - if (weightContext) { - messages.unshift({ role: 'system', content: weightContext }); + if (commandResult.command === 'diet') { + return await this.handleDietCommand(commandResult, params, messages); + } + + // 其他指令处理... + throw new Error(`未知的指令: ${commandResult.command}`); + } + + /** + * 处理体重趋势分析选择 + */ + private async handleWeightTrendAnalysis(params: { + userId: string; + conversationId: string; + confirmationData: { weightRecordData: any }; + }): Promise { + const analysisContent = await this.generateWeightTrendAnalysis( + params.userId, + params.confirmationData.weightRecordData + ); + + // 保存消息记录 + await Promise.all([ + AiMessage.create({ + conversationId: params.conversationId, + userId: params.userId, + role: RoleType.User, + content: '用户选择查看体重趋势分析', + metadata: null, + }), + AiMessage.create({ + conversationId: params.conversationId, + userId: params.userId, + role: RoleType.Assistant, + content: analysisContent, + metadata: { model: this.model, interactionType: 'weight_trend_analysis' }, + }) + ]); + + // 更新对话时间 + await AiConversation.update( + { lastMessageAt: new Date(), title: this.deriveTitleIfEmpty(analysisContent) }, + { where: { id: params.conversationId, userId: params.userId } } + ); + + return this.createStreamFromText(analysisContent); + } + + /** + * 处理体重指令 + */ + private async handleWeightCommand(params: { + userId: string; + conversationId: string; + userContent: string; + }): Promise<{ type: 'structured'; data: any }> { + const weightKg = this.extractWeightFromText(params.userContent); + + if (!weightKg) { + throw new Error('无法提取有效的体重数值'); + } + + // 更新体重到数据库 + await this.usersService.addWeightByVision(params.userId, weightKg); + + // 构建成功消息 + const responseContent = `已成功记录体重:${weightKg}kg`; + + // 保存消息 + await AiMessage.create({ + conversationId: params.conversationId, + userId: params.userId, + role: RoleType.Assistant, + content: responseContent, + metadata: { + model: this.model, + interactionType: 'weight_record_success', + weightData: { newWeight: weightKg, recordedAt: new Date().toISOString() } + }, + }); + + // 更新对话时间 + await AiConversation.update( + { lastMessageAt: new Date(), title: this.deriveTitleIfEmpty(responseContent) }, + { where: { id: params.conversationId, userId: params.userId } } + ); + + return { + type: 'structured', + data: { + content: responseContent, + choices: [ + { + id: 'trend_analysis', + label: '查看体重趋势分析', + value: 'weight_trend_analysis', + recommended: true + }, + { + id: 'continue_chat', + label: '继续对话', + value: 'continue_normal_chat', + recommended: false + } + ], + interactionType: 'weight_record_success', + pendingData: { + weightRecordData: { newWeight: weightKg, recordedAt: new Date().toISOString() } + }, + context: { command: 'weight', step: 'record_success' } } - } else if (commandResult.command === 'diet') { - // 处理饮食记录指令 - if (params.selectedChoiceId && params.confirmationData) { - // 第二阶段:用户已确认选择,记录饮食 - // confirmationData应该包含 { selectedOption: FoodConfirmationOption, imageUrl: string } - const { selectedOption, imageUrl } = params.confirmationData; - const createDto = await this.dietAnalysisService.createDietRecordFromConfirmation( + }; + } + + /** + * 处理饮食确认选择 + */ + private async handleDietConfirmation(params: { + userId: string; + conversationId: string; + selectedChoiceId: string; + confirmationData: any; + }): Promise { + // 饮食确认逻辑保持原样 + const { selectedOption, imageUrl } = params.confirmationData; + const createDto = await this.dietAnalysisService.createDietRecordFromConfirmation( + params.userId, + selectedOption, + imageUrl || '' + ); + + if (!createDto) { + throw new Error('饮食记录创建失败'); + } + + const messages = await this.buildChatHistory(params.userId, params.conversationId); + const nutritionContext = await this.dietAnalysisService.buildUserNutritionContext(params.userId); + + if (nutritionContext) { + messages.unshift({ role: 'system', content: nutritionContext }); + } + + messages.push({ + role: 'user', + content: `用户确认记录饮食:${selectedOption.label}` + }); + + messages.unshift({ + role: 'system', + content: this.dietAnalysisService.buildEnhancedDietAnalysisPrompt() + }); + + return this.generateAIResponse(params.conversationId, params.userId, messages); + } + + /** + * 处理饮食指令 + */ + private async handleDietCommand( + commandResult: CommandResult, + params: any, + messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> + ): Promise { + if (params.imageUrls) { + // 处理图片饮食记录 + const recognitionResult = await this.dietAnalysisService.recognizeFoodForConfirmation(params.imageUrls); + + if (recognitionResult.recognizedItems.length > 0) { + const choices = recognitionResult.recognizedItems.map(item => ({ + id: item.id, + label: item.label, + value: item, + recommended: recognitionResult.recognizedItems.indexOf(item) === 0 + })); + + const responseContent = `我识别到了以下食物,请选择要记录的内容:\n\n${recognitionResult.analysisText}`; + + await AiMessage.create({ + conversationId: params.conversationId, + userId: params.userId, + role: RoleType.Assistant, + content: responseContent, + metadata: { + model: this.model, + interactionType: 'food_confirmation', + choices: choices.length + }, + }); + + await AiConversation.update( + { lastMessageAt: new Date(), title: this.deriveTitleIfEmpty(responseContent) }, + { where: { id: params.conversationId, userId: params.userId } } + ); + + return { + type: 'structured', + data: { + content: responseContent, + choices, + interactionType: 'food_confirmation', + pendingData: { + imageUrl: params.imageUrls[0], + recognitionResult + }, + context: { + command: 'diet', + step: 'confirmation' + } + } + }; + } else { + messages.push({ + role: 'user', + content: `用户尝试记录饮食但识别失败:${recognitionResult.analysisText}` + }); + return this.generateAIResponse(params.conversationId, params.userId, messages); + } + } else { + // 处理文本饮食记录 + const textAnalysisResult = await this.dietAnalysisService.analyzeDietFromText(commandResult.cleanText); + + if (textAnalysisResult.shouldRecord && textAnalysisResult.extractedData) { + const createDto = await this.dietAnalysisService.processDietRecord( params.userId, - selectedOption, - imageUrl || '' + textAnalysisResult, + '' ); if (createDto) { - // 构建用户的最近饮食上下文用于营养分析 const nutritionContext = await this.dietAnalysisService.buildUserNutritionContext(params.userId); if (nutritionContext) { messages.unshift({ role: 'system', content: nutritionContext }); } params.systemNotice = `系统提示:已成功为您记录了${createDto.foodName}的饮食信息(${createDto.portionDescription || ''},约${createDto.estimatedCalories || 0}卡路里)。`; - messages.push({ role: 'user', - content: `用户确认记录饮食:${selectedOption.label}` - }); - messages.unshift({ role: 'system', content: this.dietAnalysisService.buildEnhancedDietAnalysisPrompt() }); - } - } else if (params.imageUrls) { - // 第一阶段:图片识别,返回确认选项 - const recognitionResult = await this.dietAnalysisService.recognizeFoodForConfirmation(params.imageUrls); - - if (recognitionResult.recognizedItems.length > 0) { - // 返回结构化数据供用户确认 - const choices = recognitionResult.recognizedItems.map(item => ({ - id: item.id, - label: item.label, - value: item, - recommended: recognitionResult.recognizedItems.indexOf(item) === 0 // 第一个选项为推荐 - })); - - const responseContent = `我识别到了以下食物,请选择要记录的内容:\n\n${recognitionResult.analysisText}`; - - // 保存AI助手的响应消息到数据库 - await AiMessage.create({ - conversationId: params.conversationId, - userId: params.userId, - role: RoleType.Assistant, - content: responseContent, - metadata: { - model: this.model, - interactionType: 'food_confirmation', - choices: choices.length - }, - }); - - // 更新对话的最后消息时间 - await AiConversation.update( - { lastMessageAt: new Date(), title: this.deriveTitleIfEmpty(responseContent) }, - { where: { id: params.conversationId, userId: params.userId } } - ); - - return { - type: 'structured', - data: { - content: responseContent, - choices, - interactionType: 'food_confirmation', - pendingData: { - imageUrl: params.imageUrls[0], - recognitionResult - }, - context: { - command: 'diet', - step: 'confirmation' - } - } - }; - } else { - // 识别失败,返回普通文本响应 - messages.push({ - role: 'user', - content: `用户尝试记录饮食但识别失败:${recognitionResult.analysisText}` - }); - } - } else { - // 处理文本饮食记录:没有图片,分析用户文本中的饮食信息 - const textAnalysisResult = await this.dietAnalysisService.analyzeDietFromText(commandResult.cleanText); - - if (textAnalysisResult.shouldRecord && textAnalysisResult.extractedData) { - // 自动记录饮食信息 - const createDto = await this.dietAnalysisService.processDietRecord( - params.userId, - textAnalysisResult, - '' // 文本记录没有图片 - ); - - if (createDto) { - // 构建用户的最近饮食上下文用于营养分析 - const nutritionContext = await this.dietAnalysisService.buildUserNutritionContext(params.userId); - if (nutritionContext) { - messages.unshift({ role: 'system', content: nutritionContext }); - } - - params.systemNotice = `系统提示:已成功为您记录了${createDto.foodName}的饮食信息(${createDto.portionDescription || ''},约${createDto.estimatedCalories || 0}卡路里)。`; - - messages.push({ - role: 'user', - content: `用户通过文本记录饮食:${textAnalysisResult.analysisText}` - }); - messages.unshift({ role: 'system', content: this.dietAnalysisService.buildEnhancedDietAnalysisPrompt() }); - } - } else { - // 分析失败或置信度不够,提供普通的饮食建议 - messages.push({ - role: 'user', - content: `用户提到饮食相关内容:${commandResult.cleanText}。分析结果:${textAnalysisResult.analysisText}` - }); - - // 为饮食相关话题提供营养分析上下文 - const nutritionContext = await this.dietAnalysisService.buildUserNutritionContext(params.userId); - if (nutritionContext) { - messages.unshift({ role: 'system', content: nutritionContext }); - } - messages.unshift({ role: 'system', content: NUTRITION_ANALYST_PROMPT }); - } - } - } - - // 检测是否为饮食相关话题但不是指令形式 - if (!commandResult.isCommand && this.isLikelyNutritionTopic(params.userContent, messages)) { - // 尝试从用户文本中分析饮食信息 - const textAnalysisResult = await this.dietAnalysisService.analyzeDietFromText(params.userContent); - - if (textAnalysisResult.shouldRecord && textAnalysisResult.extractedData && textAnalysisResult.confidence > 70) { - // 置信度较高,自动记录饮食信息 - const createDto = await this.dietAnalysisService.processDietRecord( - params.userId, - textAnalysisResult, - '' // 文本记录没有图片 - ); - - if (createDto) { - // 构建用户的最近饮食上下文用于营养分析 - const nutritionContext = await this.dietAnalysisService.buildUserNutritionContext(params.userId); - if (nutritionContext) { - messages.unshift({ role: 'system', content: nutritionContext }); - } - - params.systemNotice = `系统提示:检测到您提到了具体的饮食信息,已自动为您记录了${createDto.foodName}(${createDto.portionDescription || ''},约${createDto.estimatedCalories || 0}卡路里)。`; - - messages.push({ - role: 'user', - content: `${params.userContent}\n\n[系统已自动识别并记录饮食信息:${textAnalysisResult.analysisText}]` + content: `用户通过文本记录饮食:${textAnalysisResult.analysisText}` }); messages.unshift({ role: 'system', content: this.dietAnalysisService.buildEnhancedDietAnalysisPrompt() }); } } else { - // 置信度不够或无法识别具体食物,提供营养分析模式 + messages.push({ + role: 'user', + content: `用户提到饮食相关内容:${commandResult.cleanText}。分析结果:${textAnalysisResult.analysisText}` + }); + const nutritionContext = await this.dietAnalysisService.buildUserNutritionContext(params.userId); if (nutritionContext) { messages.unshift({ role: 'system', content: nutritionContext }); } messages.unshift({ role: 'system', content: NUTRITION_ANALYST_PROMPT }); } + + return this.generateAIResponse(params.conversationId, params.userId, messages); + } + } + + /** + * 处理普通对话 + */ + private async handleNormalChat( + params: any, + messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> + ): Promise { + // 检测营养话题 + if (this.isLikelyNutritionTopic(params.userContent, messages)) { + const nutritionContext = await this.dietAnalysisService.buildUserNutritionContext(params.userId); + if (nutritionContext) { + messages.unshift({ role: 'system', content: nutritionContext }); + } + messages.unshift({ role: 'system', content: NUTRITION_ANALYST_PROMPT }); } - this.logger.log(`messages: ${JSON.stringify(messages)}`); + return this.generateAIResponse(params.conversationId, params.userId, messages); + } + /** + * 生成AI响应的通用方法 + */ + private async generateAIResponse( + conversationId: string, + userId: string, + messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> + ): Promise { const stream = await this.client.chat.completions.create({ model: this.model, messages, @@ -424,15 +548,19 @@ export class AiCoachService { readable.push(delta); } } - // 结束:将assistant消息入库 + await AiMessage.create({ - conversationId: params.conversationId, - userId: params.userId, + conversationId, + userId, role: RoleType.Assistant, content: assistantContent, metadata: { model: this.model }, }); - await AiConversation.update({ lastMessageAt: new Date(), title: this.deriveTitleIfEmpty(assistantContent) }, { where: { id: params.conversationId, userId: params.userId } }); + + await AiConversation.update( + { lastMessageAt: new Date(), title: this.deriveTitleIfEmpty(assistantContent) }, + { where: { id: conversationId, userId } } + ); } catch (error) { this.logger.error(`stream error: ${error?.message || error}`); readable.push('\n[对话发生错误,请稍后重试]'); @@ -444,6 +572,32 @@ export class AiCoachService { return readable; } + /** + * 从文本创建流式响应 + */ + private createStreamFromText(text: string): Readable { + const readable = new Readable({ read() { } }); + + setTimeout(() => { + const chunks = text.split(''); + let index = 0; + + const pushChunk = () => { + if (index < chunks.length) { + readable.push(chunks[index]); + index++; + setTimeout(pushChunk, 20); + } else { + readable.push(null); + } + }; + + pushChunk(); + }, 100); + + return readable; + } + private isLikelyNutritionTopic( @@ -549,32 +703,7 @@ export class AiCoachService { - /** - * 构建饮食分析提示(保留原有方法用于兼容) - * @returns 饮食分析提示文本 - */ - private buildDietAnalysisPrompt(): string { - return `饮食分析专家模式: -你是一位专业的营养分析师和饮食评估专家。用户将通过拍照记录饮食,你需要: - -1. 饮食识别: - - 仔细观察图片中的食物种类、分量 - - 识别主要食材和烹饪方式 - - 估算食物的重量或体积 - -2. 热量分析: - - 基于识别的食物,计算总热量 - - 分析三大营养素比例(蛋白质、碳水化合物、脂肪) - - 估算主要维生素和矿物质含量 - -3. 健康建议: - - 评估该餐的营养均衡性 - - 提供个性化的饮食改进建议 - - 针对用户的健康目标给出指导 - -请以结构化、清晰的方式输出结果,使用亲切专业的语言风格。`; - } @@ -775,135 +904,33 @@ export class AiCoachService { return null; } + + + + /** - * 获取用户体重历史记录 - */ - async getUserWeightHistory(userId: string, limit: number = 10): Promise<{ - currentWeight?: number; - history: Array<{ weight: number; source: string; createdAt: Date }>; - }> { + * 生成体重趋势分析 + */ + async generateWeightTrendAnalysis(userId: string, weightRecordData: any): Promise { try { - // 获取当前体重 - const profile = await UserProfile.findOne({ where: { userId } }); - const currentWeight = profile?.weight; + const { newWeight } = weightRecordData; - // 获取体重历史 - const history = await this.usersService.getWeightHistory(userId, { limit }); + const weightAnalysisPrompt = `用户刚刚记录了体重${newWeight}kg,请提供体重趋势分析、健康建议和鼓励。语言风格:亲切、专业、鼓励性。`; - return { - currentWeight: currentWeight || undefined, - history - }; + const completion = await this.client.chat.completions.create({ + model: this.model, + messages: [ + { role: 'system', content: SYSTEM_PROMPT }, + { role: 'user', content: weightAnalysisPrompt } + ], + temperature: 0.7, + max_tokens: 300, + }); + + return completion.choices?.[0]?.message?.content || '体重趋势分析生成失败,请稍后重试。'; } catch (error) { - this.logger.error(`获取用户体重历史失败: ${error instanceof Error ? error.message : String(error)}`); - return { history: [] }; - } - } - - /** - * 构建体重更新的系统提示信息 - */ - private buildWeightUpdateSystemNotice( - newWeight: number, - currentWeight?: number, - history: Array<{ weight: number; source: string; createdAt: Date }> = [] - ): string { - let notice = `系统提示:已为你更新体重为${newWeight}kg。`; - - if (currentWeight && currentWeight !== newWeight) { - const diff = newWeight - currentWeight; - const diffText = diff > 0 ? `增加了${diff.toFixed(1)}kg` : `减少了${Math.abs(diff).toFixed(1)}kg`; - notice += `相比之前的${currentWeight}kg,你${diffText}。`; - } - - // 添加历史对比信息 - if (history.length > 0) { - const recentWeights = history.slice(0, 3); - if (recentWeights.length > 1) { - const trend = this.analyzeWeightTrend(recentWeights, newWeight); - notice += trend; - } - } - - return notice; - } - - /** - * 分析体重趋势 - */ - private analyzeWeightTrend( - recentWeights: Array<{ weight: number; createdAt: Date }>, - newWeight: number - ): string { - if (recentWeights.length < 2) return ''; - - const weights = [newWeight, ...recentWeights.map(w => w.weight)]; - let trend = ''; - - // 计算最近几次的平均变化 - let totalChange = 0; - for (let i = 0; i < weights.length - 1; i++) { - totalChange += weights[i] - weights[i + 1]; - } - const avgChange = totalChange / (weights.length - 1); - - if (Math.abs(avgChange) < 0.5) { - trend = '你的体重保持相对稳定,继续保持良好的生活习惯!'; - } else if (avgChange > 0) { - trend = `最近体重呈上升趋势,建议加强运动和注意饮食控制。`; - } else { - trend = `最近体重呈下降趋势,很棒的进步!继续坚持健康的生活方式。`; - } - - return trend; - } - - /** - * 处理体重记录和更新(无图片版本) - * 从用户文本中识别体重,更新记录,并返回相关信息 - */ - async processWeightFromText(userId: string, userText?: string): Promise<{ - weightKg?: number; - systemNotice?: string; - shouldSkipChat?: boolean; - }> { - if (!userText) return {}; - - - try { - // 从文本中提取体重 - const extractedWeight = this.extractWeightFromText(userText); - this.logger.log(`extractedWeight: ${extractedWeight}`); - - if (!extractedWeight) { - return {}; - } - - // 获取用户体重历史 - const { currentWeight, history } = await this.getUserWeightHistory(userId); - - // 更新体重到数据库 - await this.usersService.addWeightByVision(userId, extractedWeight); - - this.logger.log(`currentWeight: ${currentWeight}`); - this.logger.log(`history: ${JSON.stringify(history)}`); - - // 构建系统提示 - const systemNotice = this.buildWeightUpdateSystemNotice( - extractedWeight, - currentWeight || undefined, - history - ); - - return { - weightKg: extractedWeight, - systemNotice, - shouldSkipChat: false // 仍然需要与AI聊天,让AI给出激励回复 - }; - - } catch (error) { - this.logger.error(`处理文本体重记录失败: ${error instanceof Error ? error.message : String(error)}`); - return {}; + this.logger.error(`生成体重趋势分析失败: ${error instanceof Error ? error.message : String(error)}`); + return '体重趋势分析生成失败,请稍后重试。'; } } } diff --git a/src/ai-coach/dto/ai-chat.dto.ts b/src/ai-coach/dto/ai-chat.dto.ts index 5e97ed6..beb89f4 100644 --- a/src/ai-coach/dto/ai-chat.dto.ts +++ b/src/ai-coach/dto/ai-chat.dto.ts @@ -77,10 +77,10 @@ export class AiResponseDataDto { @IsArray() choices?: AiChoiceOptionDto[]; - @ApiProperty({ description: '交互类型', enum: ['text', 'food_confirmation', 'selection'], required: false }) + @ApiProperty({ description: '交互类型', enum: ['text', 'food_confirmation', 'weight_record_success', 'selection'], required: false }) @IsOptional() @IsString() - interactionType?: 'text' | 'food_confirmation' | 'selection'; + interactionType?: 'text' | 'food_confirmation' | 'weight_record_success' | 'selection'; @ApiProperty({ description: '需要用户确认的数据(可选)', required: false }) @IsOptional()