feat: 实现饮食记录确认流程
- 新增饮食记录确认流程,将自动记录模式升级为用户确认模式,提升用户交互体验。 - 实现两阶段饮食记录流程,支持AI识别食物并生成确认选项,用户选择后记录到数据库并提供营养分析。 - 扩展DTO层,新增相关数据结构以支持确认流程。 - 更新服务层,新增处理确认逻辑的方法,优化饮食记录的创建流程。 - 增强API文档,详细说明新流程及使用建议,确保开发者理解和使用新功能。
This commit is contained in:
148
docs/DIET_CONFIRMATION_IMPLEMENTATION_SUMMARY.md
Normal file
148
docs/DIET_CONFIRMATION_IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 饮食记录确认流程实现总结
|
||||
|
||||
## 项目概述
|
||||
|
||||
将原有的饮食记录功能从"自动记录模式"升级为"用户确认模式",类似于 Cline、Kilo 等开源 AI 工具的交互体验。
|
||||
|
||||
## 实现的功能
|
||||
|
||||
### 1. 两阶段饮食记录流程
|
||||
|
||||
- **第一阶段**:AI识别图片中的食物,生成多个确认选项
|
||||
- **第二阶段**:用户选择确认选项后,系统记录到数据库并提供营养分析
|
||||
|
||||
### 2. 新增数据结构
|
||||
|
||||
#### AiChoiceOptionDto
|
||||
```typescript
|
||||
{
|
||||
id: string; // 选项唯一标识符
|
||||
label: string; // 显示给用户的文本(如"一条鱼 200卡")
|
||||
value: any; // 选项对应的数据
|
||||
recommended?: boolean; // 是否为推荐选项
|
||||
}
|
||||
```
|
||||
|
||||
#### AiResponseDataDto
|
||||
```typescript
|
||||
{
|
||||
content: string; // AI回复的文本内容
|
||||
choices?: AiChoiceOptionDto[]; // 选择选项(可选)
|
||||
interactionType?: string; // 交互类型
|
||||
pendingData?: any; // 需要用户确认的数据
|
||||
context?: any; // 上下文信息
|
||||
}
|
||||
```
|
||||
|
||||
#### FoodConfirmationOption
|
||||
```typescript
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
foodName: string;
|
||||
portion: string;
|
||||
calories: number;
|
||||
mealType: MealType;
|
||||
nutritionData: { ... };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. API 增强
|
||||
|
||||
#### 请求参数新增
|
||||
- `selectedChoiceId?: string` - 用户选择的选项ID
|
||||
- `confirmationData?: any` - 用户确认的数据
|
||||
|
||||
#### 响应结构新增
|
||||
- 支持返回结构化数据(选择选项)
|
||||
- 支持返回传统流式文本
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. DTO 层
|
||||
- **src/ai-coach/dto/ai-chat.dto.ts**
|
||||
- 新增 `AiChoiceOptionDto`
|
||||
- 新增 `AiResponseDataDto`
|
||||
- 扩展 `AiChatRequestDto` 和 `AiChatResponseDto`
|
||||
|
||||
### 2. 服务层
|
||||
- **src/ai-coach/services/diet-analysis.service.ts**
|
||||
- 新增 `FoodConfirmationOption` 接口
|
||||
- 新增 `FoodRecognitionResult` 接口
|
||||
- 新增 `recognizeFoodForConfirmation()` 方法
|
||||
- 新增 `createDietRecordFromConfirmation()` 方法
|
||||
- 新增 `buildFoodRecognitionPrompt()` 方法
|
||||
- 新增 `parseRecognitionResult()` 方法
|
||||
|
||||
- **src/ai-coach/ai-coach.service.ts**
|
||||
- 更新 `streamChat()` 方法参数和返回类型
|
||||
- 重构饮食记录逻辑,支持两阶段确认流程
|
||||
- 新增结构化数据返回逻辑
|
||||
|
||||
### 3. 控制器层
|
||||
- **src/ai-coach/ai-coach.controller.ts**
|
||||
- 更新 `chat()` 方法,支持结构化响应
|
||||
- 新增确认数据处理逻辑
|
||||
|
||||
### 4. 文档
|
||||
- **docs/diet-confirmation-flow-api.md** - 新增API使用文档
|
||||
- **docs/DIET_CONFIRMATION_IMPLEMENTATION_SUMMARY.md** - 本总结文档
|
||||
|
||||
## 流程示例
|
||||
|
||||
### 用户上传图片
|
||||
1. 用户发送 `#记饮食` 指令并上传图片
|
||||
2. AI识别食物,返回确认选项:
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
{"id": "food_0", "label": "一条鱼 200卡", "value": {...}},
|
||||
{"id": "food_1", "label": "一根玉米 40卡", "value": {...}}
|
||||
],
|
||||
"interactionType": "food_confirmation"
|
||||
}
|
||||
```
|
||||
|
||||
### 用户确认选择
|
||||
1. 用户选择某个选项
|
||||
2. 客户端发送确认请求,包含 `selectedChoiceId` 和 `confirmationData`
|
||||
3. 系统记录到数据库,返回营养分析
|
||||
|
||||
## 技术特点
|
||||
|
||||
### 1. 向后兼容
|
||||
- 保留原有的自动记录逻辑(`analyzeDietImageEnhanced` 方法)
|
||||
- 新流程不影响其他功能
|
||||
|
||||
### 2. 类型安全
|
||||
- 所有新增接口都有完整的 TypeScript 类型定义
|
||||
- 使用 class-validator 进行数据验证
|
||||
|
||||
### 3. 错误处理
|
||||
- 图片识别失败时回退到普通文本响应
|
||||
- 确认数据无效时提供友好错误提示
|
||||
|
||||
### 4. 用户体验
|
||||
- 类似 Cline/Kilo 的交互体验
|
||||
- 清晰的选项展示(如"一条鱼 200卡")
|
||||
- 推荐选项标识
|
||||
|
||||
## 部署说明
|
||||
|
||||
1. 代码已通过编译测试,无 TypeScript 错误
|
||||
2. 保持向后兼容性,可以平滑部署
|
||||
3. 建议先在测试环境验证新流程
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **客户端适配**:需要客户端支持处理结构化响应和选择选项
|
||||
2. **图片质量**:提醒用户上传清晰的食物图片
|
||||
3. **用户引导**:在界面上提供使用说明
|
||||
|
||||
## 后续优化方向
|
||||
|
||||
1. 支持批量选择多个食物
|
||||
2. 支持用户自定义修改份量和热量
|
||||
3. 添加更多营养素信息展示
|
||||
4. 支持语音确认
|
||||
5. 添加食物历史记录快速选择
|
||||
239
docs/diet-confirmation-flow-api.md
Normal file
239
docs/diet-confirmation-flow-api.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# 饮食记录确认流程 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
新的饮食记录流程分为两个阶段:
|
||||
1. **图片识别阶段**:AI识别食物并返回确认选项
|
||||
2. **用户确认阶段**:用户选择确认选项后记录到数据库
|
||||
|
||||
## 重要说明
|
||||
|
||||
⚠️ **流式响应兼容性**:当系统需要返回确认选项时,会自动使用非流式模式返回JSON结构,即使客户端请求了 `stream: true`。这确保了确认选项的正确显示。
|
||||
|
||||
## API 流程
|
||||
|
||||
### 第一阶段:图片识别(返回确认选项)
|
||||
|
||||
**请求示例:**
|
||||
```json
|
||||
POST /ai-coach/chat
|
||||
{
|
||||
"conversationId": "user123-1234567890",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "#记饮食"
|
||||
}
|
||||
],
|
||||
"imageUrls": ["https://example.com/food-image.jpg"],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"conversationId": "user123-1234567890",
|
||||
"data": {
|
||||
"content": "我识别到了以下食物,请选择要记录的内容:\n\n图片中识别到烤鱼和米饭,看起来是一份营养均衡的晚餐。",
|
||||
"choices": [
|
||||
{
|
||||
"id": "food_0",
|
||||
"label": "一条烤鱼 220卡",
|
||||
"value": {
|
||||
"id": "food_0",
|
||||
"foodName": "烤鱼",
|
||||
"portion": "1条",
|
||||
"calories": 220,
|
||||
"mealType": "dinner",
|
||||
"nutritionData": {
|
||||
"proteinGrams": 35,
|
||||
"carbohydrateGrams": 2,
|
||||
"fatGrams": 8,
|
||||
"fiberGrams": 0
|
||||
}
|
||||
},
|
||||
"recommended": true
|
||||
},
|
||||
{
|
||||
"id": "food_1",
|
||||
"label": "一碗米饭 150卡",
|
||||
"value": {
|
||||
"id": "food_1",
|
||||
"foodName": "米饭",
|
||||
"portion": "1碗",
|
||||
"calories": 150,
|
||||
"mealType": "dinner",
|
||||
"nutritionData": {
|
||||
"proteinGrams": 3,
|
||||
"carbohydrateGrams": 32,
|
||||
"fatGrams": 0.5,
|
||||
"fiberGrams": 1
|
||||
}
|
||||
},
|
||||
"recommended": false
|
||||
}
|
||||
],
|
||||
"interactionType": "food_confirmation",
|
||||
"pendingData": {
|
||||
"imageUrl": "https://example.com/food-image.jpg",
|
||||
"recognitionResult": {
|
||||
"recognizedItems": [...],
|
||||
"analysisText": "图片中识别到烤鱼和米饭...",
|
||||
"confidence": 85
|
||||
}
|
||||
},
|
||||
"context": {
|
||||
"command": "diet",
|
||||
"step": "confirmation"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第二阶段:用户确认选择
|
||||
|
||||
**请求示例:**
|
||||
```json
|
||||
POST /ai-coach/chat
|
||||
{
|
||||
"conversationId": "user123-1234567890",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "我选择记录烤鱼"
|
||||
}
|
||||
],
|
||||
"selectedChoiceId": "food_0",
|
||||
"confirmationData": {
|
||||
"selectedOption": {
|
||||
"id": "food_0",
|
||||
"foodName": "烤鱼",
|
||||
"portion": "1条",
|
||||
"calories": 220,
|
||||
"mealType": "dinner",
|
||||
"nutritionData": {
|
||||
"proteinGrams": 35,
|
||||
"carbohydrateGrams": 2,
|
||||
"fatGrams": 8,
|
||||
"fiberGrams": 0
|
||||
}
|
||||
},
|
||||
"imageUrl": "https://example.com/food-image.jpg"
|
||||
},
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"conversationId": "user123-1234567890",
|
||||
"text": "很好!我已经为您记录了这份烤鱼(1条,约220卡路里)。\n\n根据您的饮食记录,这是一份优质的蛋白质来源,包含35克蛋白质,脂肪含量适中。建议搭配一些蔬菜来增加膳食纤维的摄入。\n\n您今天的饮食营养搭配看起来不错,记得保持均衡的饮食习惯!"
|
||||
}
|
||||
```
|
||||
|
||||
## 数据结构说明
|
||||
|
||||
### AiChoiceOptionDto
|
||||
```typescript
|
||||
{
|
||||
id: string; // 选项唯一标识符
|
||||
label: string; // 显示给用户的文本(如"一条鱼 200卡")
|
||||
value: any; // 选项对应的数据
|
||||
recommended?: boolean; // 是否为推荐选项
|
||||
}
|
||||
```
|
||||
|
||||
### AiResponseDataDto
|
||||
```typescript
|
||||
{
|
||||
content: string; // AI回复的文本内容
|
||||
choices?: AiChoiceOptionDto[]; // 选择选项(可选)
|
||||
interactionType?: string; // 交互类型:'text' | 'food_confirmation' | 'selection'
|
||||
pendingData?: any; // 需要用户确认的数据(可选)
|
||||
context?: any; // 上下文信息(可选)
|
||||
}
|
||||
```
|
||||
|
||||
### FoodConfirmationOption
|
||||
```typescript
|
||||
{
|
||||
id: string; // 唯一标识符
|
||||
label: string; // 显示文本
|
||||
foodName: string; // 食物名称
|
||||
portion: string; // 份量描述
|
||||
calories: number; // 估算热量
|
||||
mealType: MealType; // 餐次类型
|
||||
nutritionData: { // 营养数据
|
||||
proteinGrams?: number; // 蛋白质(克)
|
||||
carbohydrateGrams?: number; // 碳水化合物(克)
|
||||
fatGrams?: number; // 脂肪(克)
|
||||
fiberGrams?: number; // 膳食纤维(克)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 图片识别失败
|
||||
如果图片模糊或无法识别食物,API会返回正常的文本响应:
|
||||
```json
|
||||
{
|
||||
"conversationId": "user123-1234567890",
|
||||
"text": "抱歉,我无法清晰地识别图片中的食物。请确保图片清晰,光线充足,食物在画面中清晰可见,然后重新上传。"
|
||||
}
|
||||
```
|
||||
|
||||
### 无效的确认数据
|
||||
如果第二阶段的确认数据无效,系统会返回错误提示:
|
||||
```json
|
||||
{
|
||||
"conversationId": "user123-1234567890",
|
||||
"text": "确认数据无效,请重新选择要记录的食物。"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **图片质量**:确保上传的图片清晰,光线充足,食物在画面中清晰可见
|
||||
2. **选择确认**:用户可以选择多个食物选项,每次确认记录一种食物
|
||||
3. **营养分析**:系统会基于用户的历史饮食记录提供个性化的营养分析和建议
|
||||
4. **流式响应处理**:
|
||||
- 客户端应该检查响应的 `Content-Type`
|
||||
- `application/json`:结构化数据(确认选项)
|
||||
- `text/plain`:流式文本
|
||||
- 当返回确认选项时,系统会忽略 `stream` 参数并返回JSON
|
||||
|
||||
## 客户端适配指南
|
||||
|
||||
### 响应类型检测
|
||||
```javascript
|
||||
// 检查响应类型
|
||||
if (response.headers['content-type'].includes('application/json')) {
|
||||
// 处理结构化数据(确认选项)
|
||||
const data = await response.json();
|
||||
if (data.data && data.data.choices) {
|
||||
// 显示选择选项
|
||||
showFoodConfirmationOptions(data.data.choices);
|
||||
}
|
||||
} else {
|
||||
// 处理流式文本
|
||||
handleStreamResponse(response);
|
||||
}
|
||||
```
|
||||
|
||||
### 确认选择发送
|
||||
```javascript
|
||||
// 用户选择后发送确认
|
||||
const confirmationRequest = {
|
||||
conversationId: "user123-1234567890",
|
||||
messages: [{ role: "user", content: "我选择记录烤鱼" }],
|
||||
selectedChoiceId: "food_0",
|
||||
confirmationData: {
|
||||
selectedOption: selectedFoodOption,
|
||||
imageUrl: originalImageUrl
|
||||
},
|
||||
stream: true // 第二阶段可以使用流式
|
||||
};
|
||||
```
|
||||
151
docs/stream-response-solution-options.md
Normal file
151
docs/stream-response-solution-options.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# 流式响应与结构化数据冲突解决方案
|
||||
|
||||
## 问题描述
|
||||
|
||||
当前实现中,`#记饮食` 指令在第一阶段需要返回结构化数据(确认选项),但客户端可能设置了 `stream: true`,导致响应类型冲突。
|
||||
|
||||
## 解决方案对比
|
||||
|
||||
### 方案1:强制非流式模式 ⭐ (当前实现)
|
||||
|
||||
**优点:**
|
||||
- 实现简单,改动最小
|
||||
- 完全向后兼容
|
||||
- 客户端只需检查 Content-Type
|
||||
|
||||
**缺点:**
|
||||
- 行为不够明确(忽略stream参数)
|
||||
- 客户端需要额外处理响应类型检测
|
||||
|
||||
**实现:**
|
||||
```typescript
|
||||
// 当需要返回确认选项时,自动使用JSON响应
|
||||
if (typeof result === 'object' && 'type' in result) {
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.send({ conversationId, data: result.data });
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 方案2:分离API端点
|
||||
|
||||
**优点:**
|
||||
- API语义清晰
|
||||
- 响应类型明确
|
||||
- 易于测试和维护
|
||||
|
||||
**缺点:**
|
||||
- 需要新增API端点
|
||||
- 客户端需要适配新API
|
||||
|
||||
**建议API设计:**
|
||||
```typescript
|
||||
// 专门的食物识别API
|
||||
@Post('analyze-food')
|
||||
async analyzeFood(body: { imageUrls: string[] }): Promise<FoodRecognitionResponseDto>
|
||||
|
||||
// 确认并记录API
|
||||
@Post('confirm-food-record')
|
||||
async confirmFoodRecord(body: { selectedOption: any, imageUrl: string }): Promise<DietRecordResponseDto>
|
||||
|
||||
// 原有聊天API保持纯文本
|
||||
@Post('chat')
|
||||
async chat(): Promise<StreamableFile | { text: string }>
|
||||
```
|
||||
|
||||
### 方案3:统一JSON响应格式
|
||||
|
||||
**优点:**
|
||||
- 响应格式统一
|
||||
- 可以在JSON中指示是否需要流式处理
|
||||
|
||||
**缺点:**
|
||||
- 破坏向后兼容性
|
||||
- 所有客户端都需要修改
|
||||
|
||||
**实现示例:**
|
||||
```typescript
|
||||
// 统一响应格式
|
||||
{
|
||||
conversationId: string;
|
||||
responseType: 'text' | 'choices' | 'stream';
|
||||
data: {
|
||||
content?: string;
|
||||
choices?: any[];
|
||||
streamUrl?: string; // 流式数据的WebSocket URL
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方案4:SSE (Server-Sent Events) 统一
|
||||
|
||||
**优点:**
|
||||
- 可以发送不同类型的事件
|
||||
- 保持连接状态
|
||||
- 支持实时交互
|
||||
|
||||
**缺点:**
|
||||
- 实现复杂度高
|
||||
- 需要客户端支持SSE
|
||||
|
||||
**实现示例:**
|
||||
```typescript
|
||||
// SSE事件类型
|
||||
event: text
|
||||
data: {"chunk": "AI回复的文本片段"}
|
||||
|
||||
event: choices
|
||||
data: {"choices": [...], "content": "请选择食物"}
|
||||
|
||||
event: complete
|
||||
data: {"conversationId": "..."}
|
||||
```
|
||||
|
||||
## 推荐方案
|
||||
|
||||
### 短期:方案1 (当前实现) ✅
|
||||
- 快速解决问题
|
||||
- 最小化影响
|
||||
- 保持兼容性
|
||||
|
||||
### 长期:方案2 (分离API端点)
|
||||
- 更清晰的API设计
|
||||
- 更好的可维护性
|
||||
- 更明确的职责分离
|
||||
|
||||
## 当前方案的客户端适配
|
||||
|
||||
```javascript
|
||||
async function sendDietRequest(imageUrls, conversationId, stream = true) {
|
||||
const response = await fetch('/ai-coach/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
conversationId,
|
||||
messages: [{ role: 'user', content: '#记饮食' }],
|
||||
imageUrls,
|
||||
stream
|
||||
})
|
||||
});
|
||||
|
||||
// 检查响应类型
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
// 结构化数据(确认选项)
|
||||
const data = await response.json();
|
||||
return { type: 'choices', data };
|
||||
} else {
|
||||
// 流式文本
|
||||
return { type: 'stream', stream: response.body };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
当前的方案1实现简单有效,能够解决流式响应冲突问题。虽然在语义上不够完美,但在实际使用中是可行的。建议:
|
||||
|
||||
1. **立即采用方案1**,解决当前问题
|
||||
2. **文档中明确说明**响应类型检测的必要性
|
||||
3. **后续版本考虑方案2**,提供更清晰的API设计
|
||||
Reference in New Issue
Block a user