feat: 增强饮食分析服务,支持文本饮食记录处理
- 新增分析用户文本中的饮食信息功能,自动记录饮食信息并提供营养分析。 - 优化饮食记录处理逻辑,支持无图片的文本记录,提升用户体验。 - 添加单元测试,确保文本分析功能的准确性和稳定性。 - 更新相关文档,详细说明新功能的使用方法和示例。
This commit is contained in:
@@ -61,6 +61,7 @@ export class DietAnalysisService {
|
||||
private readonly logger = new Logger(DietAnalysisService.name);
|
||||
private readonly client: OpenAI;
|
||||
private readonly visionModel: string;
|
||||
private readonly model: string;
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
@@ -74,6 +75,7 @@ export class DietAnalysisService {
|
||||
baseURL,
|
||||
});
|
||||
|
||||
this.model = this.configService.get<string>('DASHSCOPE_MODEL') || 'qwen-flash';
|
||||
this.visionModel = this.configService.get<string>('DASHSCOPE_VISION_MODEL') || 'qwen-vl-max';
|
||||
}
|
||||
|
||||
@@ -159,6 +161,44 @@ export class DietAnalysisService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析用户文本中的饮食信息
|
||||
* @param userText 用户输入的文本
|
||||
* @returns 饮食分析结果
|
||||
*/
|
||||
async analyzeDietFromText(userText: string): Promise<DietAnalysisResult> {
|
||||
try {
|
||||
const currentHour = new Date().getHours();
|
||||
const suggestedMealType = this.getSuggestedMealType(currentHour);
|
||||
|
||||
const prompt = this.buildTextDietAnalysisPrompt(suggestedMealType);
|
||||
|
||||
const completion = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `${prompt}\n\n用户描述:${userText}`
|
||||
}
|
||||
],
|
||||
temperature: 0.3,
|
||||
response_format: { type: 'json_object' } as any,
|
||||
});
|
||||
|
||||
const rawResult = completion.choices?.[0]?.message?.content || '{}';
|
||||
this.logger.log(`Text diet analysis result: ${rawResult}`);
|
||||
|
||||
return this.parseAndValidateResult(rawResult, suggestedMealType);
|
||||
} catch (error) {
|
||||
this.logger.error(`文本饮食分析失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return {
|
||||
shouldRecord: false,
|
||||
confidence: 0,
|
||||
analysisText: '文本饮食分析失败,请稍后重试'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户确认的选项创建饮食记录
|
||||
* @param userId 用户ID
|
||||
@@ -212,15 +252,18 @@ export class DietAnalysisService {
|
||||
* 处理饮食记录并添加到数据库
|
||||
* @param userId 用户ID
|
||||
* @param analysisResult 分析结果
|
||||
* @param imageUrl 图片URL
|
||||
* @param imageUrl 图片URL(可选,文本记录时为空)
|
||||
* @returns 饮食记录响应
|
||||
*/
|
||||
async processDietRecord(userId: string, analysisResult: DietAnalysisResult, imageUrl: string): Promise<CreateDietRecordDto | null> {
|
||||
async processDietRecord(userId: string, analysisResult: DietAnalysisResult, imageUrl?: string): Promise<CreateDietRecordDto | null> {
|
||||
if (!analysisResult.shouldRecord || !analysisResult.extractedData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 根据是否有图片URL来确定数据源
|
||||
const source = imageUrl ? DietRecordSource.Vision : DietRecordSource.Manual;
|
||||
|
||||
const createDto: CreateDietRecordDto = {
|
||||
mealType: analysisResult.extractedData.mealType,
|
||||
foodName: analysisResult.extractedData.foodName,
|
||||
@@ -230,8 +273,8 @@ export class DietAnalysisService {
|
||||
carbohydrateGrams: analysisResult.extractedData.carbohydrateGrams,
|
||||
fatGrams: analysisResult.extractedData.fatGrams,
|
||||
fiberGrams: analysisResult.extractedData.fiberGrams,
|
||||
source: DietRecordSource.Vision,
|
||||
imageUrl: imageUrl,
|
||||
source: source,
|
||||
imageUrl: imageUrl || undefined,
|
||||
aiAnalysisResult: analysisResult,
|
||||
};
|
||||
|
||||
@@ -399,6 +442,54 @@ export class DietAnalysisService {
|
||||
4. analysisText要详细说明识别的食物和营养分析`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建文本饮食分析提示
|
||||
* @param suggestedMealType 建议的餐次类型
|
||||
* @returns 提示文本
|
||||
*/
|
||||
private buildTextDietAnalysisPrompt(suggestedMealType: MealType): string {
|
||||
return `作为专业营养分析师,请分析用户描述的饮食内容并以严格JSON格式返回结果。
|
||||
|
||||
当前时间建议餐次:${suggestedMealType}
|
||||
|
||||
请返回以下格式的JSON(不要包含其他文本):
|
||||
{
|
||||
"shouldRecord": boolean, // 是否应该记录(如果描述包含具体食物则为true)
|
||||
"confidence": number, // 识别置信度 0-100
|
||||
"extractedData": {
|
||||
"foodName": string, // 主要食物名称(简洁,如"鸡胸肉沙拉"、"牛肉面"等)
|
||||
"mealType": "${suggestedMealType}", // 餐次类型,优先使用建议值
|
||||
"portionDescription": string, // 份量描述(如"1碗"、"200g"、"一份"等)
|
||||
"estimatedCalories": number, // 估算总热量
|
||||
"proteinGrams": number, // 蛋白质含量(克)
|
||||
"carbohydrateGrams": number, // 碳水化合物含量(克)
|
||||
"fatGrams": number, // 脂肪含量(克)
|
||||
"fiberGrams": number, // 膳食纤维含量(克)
|
||||
"nutritionDetails": { // 其他营养信息
|
||||
"mainIngredients": string[], // 主要食材列表
|
||||
"cookingMethod": string, // 烹饪方式(如"清蒸"、"炒制"、"生食"等)
|
||||
"foodCategories": string[] // 食物分类(如"主食"、"蛋白质"、"蔬菜"等)
|
||||
}
|
||||
},
|
||||
"analysisText": string // 详细的文字分析说明
|
||||
}
|
||||
|
||||
分析要求:
|
||||
1. 仔细识别用户描述中的食物名称、数量、烹饪方式等信息
|
||||
2. 如果描述模糊或不包含具体食物,设置shouldRecord为false
|
||||
3. 营养数据要基于识别的食物种类和分量合理估算
|
||||
4. foodName要简洁明了,便于记录和查找
|
||||
5. 支持中文食物描述,如"一碗米饭"、"两个鸡蛋"、"一份青菜"等
|
||||
6. analysisText要详细说明识别的食物和营养分析
|
||||
7. 如果用户提到多种食物,选择主要的食物作为记录对象,或合并为一餐记录
|
||||
|
||||
示例用户输入:
|
||||
- "今天早餐吃了一碗燕麦粥加香蕉"
|
||||
- "午餐点了一份鸡胸肉沙拉"
|
||||
- "晚上吃了牛肉面,还有小菜"
|
||||
- "刚吃了两个苹果当零食"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析食物识别结果
|
||||
* @param rawResult 原始结果字符串
|
||||
|
||||
Reference in New Issue
Block a user