import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { OpenAI } from 'openai'; import { Readable } from 'stream'; import { AiMessage, RoleType } from './models/ai-message.model'; import { AiConversation } from './models/ai-conversation.model'; import { PostureAssessment } from './models/posture-assessment.model'; import { UserProfile } from '../users/models/user-profile.model'; import { UsersService } from '../users/users.service'; import { DietAnalysisService, DietAnalysisResult, FoodRecognitionResult, FoodConfirmationOption } from './services/diet-analysis.service'; const SYSTEM_PROMPT = `作为一名资深的健康管家兼营养分析师(Nutrition Analyst)和健身教练,我拥有丰富的专业知识,包括但不限于: 运动领域:运动解剖学、体态评估、疼痛预防、功能性训练、力量与柔韧性训练、运动损伤预防与恢复。 营养领域:基础营养学、饮食结构优化、宏量与微量营养素分析、能量平衡、运动表现与恢复的饮食搭配、特殊人群(如素食者、轻度肥胖人群、体重管理需求者)的饮食指导。 请遵循以下指导原则进行交流: 1. 话题范围 仅限于 健康、健身、普拉提、康复、形体训练、柔韧性提升、力量训练、运动损伤预防与恢复、营养与饮食 等领域。 涉及营养时,我会结合 个体化饮食分析(如热量、蛋白质、碳水、脂肪、维生素、矿物质比例)和 生活方式建议,帮助优化饮食习惯。 2. 拒绝回答的内容 不涉及医疗诊断、处方药建议、情感心理咨询、金融投资分析或编程等高风险或不相关内容。 若遇到超出专业范围的问题,我会礼貌说明并尝试引导回相关话题。 3. 语言风格 回复以 亲切、专业、清晰分点 为主。 会给出 在家可实践的具体步骤,并提供注意事项与替代方案。 针对不同水平或有伤病史的用户,提供调整建议与安全提示。 4. 个性化与安全性 强调每个人身体和饮食需求的独特性。 提供训练和饮食建议时,会提醒用户根据自身情况调整强度与摄入量。 如涉及严重疼痛、慢性病或旧伤复发,强烈建议先咨询医生或注册营养师再执行。 5. 设备与工具要求 运动部分默认用户仅有基础家庭健身器材(瑜伽垫、弹力带、泡沫轴)。 营养部分会给出简单可操作的食材替代方案,避免过度依赖难获取或昂贵的补剂。 所有建议附带大致的 频率/时长/摄入参考量,并分享 自我监测与调整的方法(如训练日志、饮食记录、身体反馈观察)。`; const NUTRITION_ANALYST_PROMPT = `营养分析师模式(仅在检测为营养/饮食相关话题时启用): 原则与优先级: - 本轮以营养分析师视角回答;若与其它系统指令冲突,以本提示为准;话题结束后自动恢复默认角色。 - 只输出结论与结构化内容,不展示推理过程。 - 信息不足时,先提出1-3个关键追问(如餐次、份量、目标、过敏/限制)。 输出结构(精简分点): 1) 饮食分解:按餐次(早餐/午餐/晚餐/加餐)整理;给出每餐热量与三大营养素的估算(用“约/范围”表述)。 2) 营养分析: - 全天热量与宏量营养素比例是否匹配目标(减脂/增肌/维持/恢复/表现)。 - 关键微量营养素关注点(膳食纤维、维生素D、钙、铁、钾、镁、钠等)。 - 指出过量/不足与可观测风险(如蛋白不足、添加糖偏高、钠摄入偏高等)。 3) 优化建议(可执行): - 食材替换:给出2-3条替换示例(如“白米→糙米/藜麦”,“香肠→瘦牛肉/鸡胸”,“含糖酸奶→无糖酸奶+水果”)。 - 结构调整:分配蛋白质到三餐/加餐、碳水时机(训练前后)、蔬果与纤维补足。 - 目标化策略:分别给出减脂/增肌/维持/恢复/表现的要点(热量/蛋白/碳水/脂肪的方向性调整)。 4) 安全与个体差异提醒:过敏与不耐受、疾病或孕期需个体化;必要时建议咨询医生/注册营养师。 表述规范: - 语气亲切专业;分点清晰;避免过度精确(如“约300kcal”、“蛋白约25-35g”)。 - 无法确定时给出区间与假设,并提示用户完善信息。 `; /** * 指令解析结果接口 */ interface CommandResult { isCommand: boolean; command?: 'weight' | 'diet'; originalText: string; cleanText: string; } @Injectable() export class AiCoachService { private readonly logger = new Logger(AiCoachService.name); private readonly client: OpenAI; private readonly model: string; private readonly visionModel: string; constructor( private readonly configService: ConfigService, private readonly usersService: UsersService, private readonly dietAnalysisService: DietAnalysisService, ) { const dashScopeApiKey = this.configService.get('DASHSCOPE_API_KEY') || 'sk-e3ff4494c2f1463a8910d5b3d05d3143'; const baseURL = this.configService.get('DASHSCOPE_BASE_URL') || 'https://dashscope.aliyuncs.com/compatible-mode/v1'; this.client = new OpenAI({ apiKey: dashScopeApiKey, baseURL, }); // 默认选择通义千问对话模型(OpenAI兼容名),可通过环境覆盖 this.model = this.configService.get('DASHSCOPE_MODEL') || 'qwen-flash'; this.visionModel = this.configService.get('DASHSCOPE_VISION_MODEL') || 'qwen-vl-max'; } async createOrAppendMessages(params: { userId: string; conversationId?: string; userContent: string; }): Promise<{ conversationId: string }> { let conversationId = params.conversationId; if (!conversationId) { conversationId = `${params.userId}-${Date.now()}`; await AiConversation.create({ id: conversationId, userId: params.userId, title: null, lastMessageAt: new Date() }); } else { await AiConversation.upsert({ id: conversationId, userId: params.userId, lastMessageAt: new Date() }); } await AiMessage.create({ conversationId, userId: params.userId, role: RoleType.User, content: params.userContent, metadata: null, }); return { conversationId }; } buildChatHistory = async (userId: string, conversationId: string) => { const history = await AiMessage.findAll({ where: { userId, conversationId, deleted: false }, order: [['created_at', 'ASC']], }); const messages = [ { role: 'system' as const, content: SYSTEM_PROMPT }, ...history.map((m) => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content })), ]; return messages; }; async streamChat(params: { userId: string; conversationId: string; userContent: string; systemNotice?: string; imageUrls?: string[]; selectedChoiceId?: string; confirmationData?: any; }): Promise { try { // 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 }); } // 处理饮食确认选择 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') { return await this.handleWeightCommand(params); } 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' } } }; } /** * 处理饮食确认选择 */ 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, 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 }); } 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 }); } 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, stream: true, temperature: 0.7, max_tokens: 500, }); const readable = new Readable({ read() { } }); let assistantContent = ''; (async () => { try { for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta?.content || ''; if (delta) { assistantContent += delta; readable.push(delta); } } await AiMessage.create({ conversationId, userId, role: RoleType.Assistant, content: assistantContent, metadata: { model: this.model }, }); 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[对话发生错误,请稍后重试]'); } finally { readable.push(null); } })(); 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( currentText: string | undefined, messages?: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>, ): boolean { if (!currentText && !messages?.length) return false; const recentTexts: string[] = []; if (currentText) recentTexts.push(currentText); if (messages && messages.length > 0) { const tail = messages.slice(-6).map((m) => (m?.content || '')); recentTexts.push(...tail); } const text = recentTexts.join('\n').toLowerCase(); const keywordPatterns = [ /营养|饮食|配餐|食谱|餐单|膳食|食材|食物|加餐|早餐|午餐|晚餐|零食|控糖|控卡|代餐|膳食纤维|纤维|维生素|矿物质|微量营养素|宏量营养素|热量|卡路里|大卡/i, /protein|carb|carbohydrate|fat|fats|calorie|calories|kcal|macro|micronutrient|vitamin|fiber|diet|meal|breakfast|lunch|dinner|snack|bulking|cutting/i, /蛋白|蛋白质|碳水|脂肪|糖|升糖指数|gi|低碳|生酮|高蛋白|低脂|清淡/i, ]; const structureHints = [ /\b\d+\s*(?:g|克|ml|毫?升|大?卡|kcal)\b/i, /\b[0-9]{2,4}\s*kcal\b/i, /(鸡胸|牛肉|鸡蛋|燕麦|藜麦|糙米|白米|土豆|红薯|酸奶|牛奶|坚果|鳄梨|沙拉|面包|米饭|面条)/i, /(替换|替代|换成).*(食材|主食|配菜|零食)/i, ]; const goalHints = [ /减脂|增肌|维持|控重|体重管理|恢复|训练表现|运动表现/i, /weight\s*loss|fat\s*loss|muscle\s*gain|maintenance|performance|recovery/i, ]; const matched = [...keywordPatterns, ...structureHints, ...goalHints].some((re) => re.test(text)); // 若用户发的是极短的承接语,但上下文包含饮食关键词,也认为是营养话题 if (!matched && currentText && currentText.length <= 8) { const shortFollowUps = /(那早餐呢|那午餐呢|那晚餐呢|那怎么吃|吃什么|怎么搭配|怎么配|怎么安排|如何吃)/i; if (shortFollowUps.test(currentText)) { const context = (messages || []).slice(-8).map((m) => m.content).join('\n'); if ([...keywordPatterns, ...structureHints].some((re) => re.test(context))) return true; } } return matched; } /** * 解析用户输入的指令(以 # 开头) * @param text 用户输入文本 * @returns 指令解析结果 */ private parseCommand(text: string): CommandResult { if (!text || !text.trim().startsWith('#')) { return { isCommand: false, originalText: text || '', cleanText: text || '' }; } const trimmedText = text.trim(); const commandMatch = trimmedText.match(/^#([^\s::]+)[::]?\s*(.*)$/); if (!commandMatch) { return { isCommand: false, originalText: text, cleanText: text }; } const [, commandPart, restText] = commandMatch; const cleanText = restText.trim(); // 识别体重记录指令 if (/^记体重$/.test(commandPart)) { return { isCommand: true, command: 'weight', originalText: text, cleanText: cleanText || '记录体重' }; } // 识别饮食记录指令 if (/^记饮食$/.test(commandPart)) { return { isCommand: true, command: 'diet', originalText: text, cleanText: cleanText || '记录饮食' }; } // 未识别的指令,当作普通文本处理 return { isCommand: false, originalText: text, cleanText: text }; } private deriveTitleIfEmpty(assistantReply: string): string | null { if (!assistantReply) return null; const firstLine = assistantReply.split(/\r?\n/).find(Boolean) || ''; return firstLine.slice(0, 50) || null; } async listConversations(userId: string, params: { page?: number; pageSize?: number }) { const page = Math.max(1, params.page || 1); const pageSize = Math.min(50, Math.max(1, params.pageSize || 20)); const offset = (page - 1) * pageSize; const { rows, count } = await AiConversation.findAndCountAll({ where: { userId, deleted: false }, order: [['last_message_at', 'DESC']], offset, limit: pageSize, }); return { page, pageSize, total: count, items: rows.map((c) => ({ conversationId: c.id, title: c.title, lastMessageAt: c.lastMessageAt, createdAt: c.createdAt, })), }; } async getConversationDetail(userId: string, conversationId: string) { const conv = await AiConversation.findOne({ where: { id: conversationId, userId, deleted: false } }); if (!conv) return null; const messages = await AiMessage.findAll({ where: { userId, conversationId, deleted: false }, order: [['created_at', 'ASC']], }); return { conversationId: conv.id, title: conv.title, lastMessageAt: conv.lastMessageAt, createdAt: conv.createdAt, messages: messages.map((m) => ({ role: m.role, content: m.content, createdAt: m.createdAt })), }; } async deleteConversation(userId: string, conversationId: string): Promise { const conv = await AiConversation.findOne({ where: { id: conversationId, userId, deleted: false } }); if (!conv) return false; await AiMessage.update({ deleted: true }, { where: { userId, conversationId } }); await AiConversation.update({ deleted: true }, { where: { id: conversationId, userId } }); return true; } /** * AI体态评估: * - 汇总用户身高体重 * - 使用视觉模型读取三张图片(正/侧/背) * - 通过强约束的 JSON Schema 产出结构化结果 * - 存储评估记录并返回 */ async assessPosture(params: { userId: string; frontImageUrl: string; sideImageUrl: string; backImageUrl: string; heightCm?: number; weightKg?: number; }) { // 获取默认身高体重 let heightCm: number | undefined = params.heightCm; let weightKg: number | undefined = params.weightKg; if (heightCm == null || weightKg == null) { const profile = await UserProfile.findOne({ where: { userId: params.userId } }); if (heightCm == null) heightCm = profile?.height ?? undefined; if (weightKg == null) weightKg = profile?.weight ?? undefined; } const schemaInstruction = `请以严格合法的JSON返回体态评估结果,键名与类型必须匹配以下Schema,不要输出多余文本: { "overallScore": number(0-5), "radar": { "骨盆中立": number(0-5), "肩带稳": number(0-5), "胸廓控": number(0-5), "主排列": number(0-5), "柔对线": number(0-5), "核心": number(0-5) }, "frontView": { "描述": string, "问题要点": string[], "建议动作": string[] }, "sideView": { "描述": string, "问题要点": string[], "建议动作": string[] }, "backView": { "描述": string, "问题要点": string[], "建议动作": string[] } }`; const persona = `你是一名资深体态评估与普拉提康复教练。结合用户提供的三张照片(正面/侧面/背面)进行体态评估。严格限制话题在健康、姿势、普拉提与训练建议范围内。用词亲切但专业,强调安全、循序渐进与个体差异。用户资料:身高${heightCm ?? '未知'}cm,体重${weightKg ?? '未知'}kg。`; const completion = await this.client.chat.completions.create({ model: this.visionModel, messages: [ { role: 'system', content: persona }, { role: 'user', content: [ { type: 'text', text: schemaInstruction }, { type: 'text', text: '这三张图分别是正面、侧面、背面:' }, { type: 'image_url', image_url: { url: params.frontImageUrl } as any }, { type: 'image_url', image_url: { url: params.sideImageUrl } as any }, { type: 'image_url', image_url: { url: params.backImageUrl } as any }, ] as any, }, ], temperature: 0, response_format: { type: 'json_object' } as any, }); const raw = completion.choices?.[0]?.message?.content || '{}'; let result: any = {}; try { result = JSON.parse(raw); } catch { } const overallScore = typeof result.overallScore === 'number' ? result.overallScore : null; const rec = await PostureAssessment.create({ userId: params.userId, frontImageUrl: params.frontImageUrl, sideImageUrl: params.sideImageUrl, backImageUrl: params.backImageUrl, heightCm: heightCm != null ? heightCm : null, weightKg: weightKg != null ? weightKg : null, overallScore, result, }); return { id: rec.id, overallScore, result }; } /** * 从用户文本中识别体重信息 * 支持多种格式:65kg、65公斤、65.5kg、体重65等 */ private extractWeightFromText(text: string | undefined): number | null { if (!text) return null; const t = text.toLowerCase(); // 匹配各种体重格式的正则表达式 const patterns = [ // 匹配 "#记体重:80 kg" 格式 /#(?:记体重|体重|称重|记录体重)[::]\s*(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)/i, // 匹配带单位的体重 "80kg", "80.5公斤", "80 kg" /(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)/i, // 匹配体重关键词后的数字 "体重65", "weight 70.5" /(?:体重|称重|秤|重量|weight)[::]?\s*(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)?/i, // 匹配口语化表达 "我体重65", "我现在70kg" /我(?:现在|今天)?(?:体重|重量|称重)?(?:是|为|有|:|:)?\s*(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)?/i, // 匹配简单数字+单位格式 "65.5kg" /^(\d+(?:\.\d+)?)\s*(?:kg|公斤|千克)$/i, // 匹配斤单位并转换为kg "130斤" /(\d+(?:\.\d+)?)\s*斤/i, ]; for (const pattern of patterns) { const match = t.match(pattern); if (match) { let weight = parseFloat(match[1]); // 如果是斤单位,转换为kg (1斤 = 0.5kg) // 只有专门匹配斤单位的模式才进行转换,避免"公斤"等词被误判 if (pattern.source.includes('斤') && !pattern.source.includes('公斤')) { weight = weight * 0.5; } // 合理的体重范围检查 (20-400kg) if (weight >= 20 && weight <= 400) { return weight; } } } return null; } /** * 生成体重趋势分析 */ async generateWeightTrendAnalysis(userId: string, weightRecordData: any): Promise { try { const { newWeight } = weightRecordData; const weightAnalysisPrompt = `用户刚刚记录了体重${newWeight}kg,请提供体重趋势分析、健康建议和鼓励。语言风格:亲切、专业、鼓励性。`; 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 '体重趋势分析生成失败,请稍后重试。'; } } }