Files
msh-system/msh_single_uniapp/pages/tool/ai-nutritionist-REVIEW.md
scottpan 6122f94818 Add automated fix workflow for AI nutritionist page
- Create .fixes/ directory structure for tracking repairs
- Add FIX-001 to FIX-009 repair tasks based on Cursor review
- Add automation scripts (start-fix.sh, complete-fix.sh)
- Update HEARTBEAT.md with repair checklist
- Create AUTOMATION_PLAN.md with workflow documentation

Fixes address:
- Remove fake initial data
- Add clear chat button
- Split oversized component
- Optimize multi-image upload
- Fix scroll behavior
- Remove dead code
- Extract hardcoded config
2026-02-28 06:56:43 +08:00

207 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AI 营养师页面代码审查报告
**文件**: `pages/tool/ai-nutritionist.vue`
**审查范围**: 页面架构、Coze 集成、腾讯云 ASR、消息与状态、图片上传、问题与优化建议
---
## 1. 页面整体架构和组件设计
### 1.1 布局结构
页面采用**单文件 Vue 组件**,自上而下分为:
| 区域 | 说明 |
|------|------|
| **header-container** | 顶部宣传横幅慢生活守护健康、AI 营养师入驻) |
| **chat-container** | 可滚动聊天区scroll-view包含欢迎语、消息列表、加载态 |
| **quick-questions** | 快捷问题2 个固定问题按钮) |
| **input-container** | 输入区:图片预览、相机/麦克风/输入框、发送按钮 |
整体为 `flex` 纵向布局,`height: 100vh`,聊天区 `flex: 1` 占满中间,保证输入框始终在底部。
### 1.2 组件与复用
- **无子组件拆分**:所有 UI 均在当前页面内,未抽取为独立组件(如消息气泡、打字指示器、语音按钮等)。
- **逻辑集中**:对话、语音、图片、滚动等逻辑均在当前页 `methods` 中,约 550+ 行,可维护性一般。
### 1.3 数据流
- **状态来源**`data()` 本地状态 + Vuex `mapGetters(['userInfo','uid'])`
- **无专门状态库**:未使用 Pinia/Vuex 管理对话或会话,适合单页场景,多页共享会话需后续扩展。
---
## 2. Coze AI 对话集成的实现方式
### 2.1 调用链路
```
sendMessage()
→ sendToAI(content, type)
→ api.cozeChat(requestData)
→ pollChatStatus(conversationId, chatId)
→ getChatMessages(conversationId, chatId)
```
- **入口**: 文本在 `sendMessage` 中拼好 `userMessage` 后调用 `sendToAI(text, 'text')`;图片在循环中对每张调用 `sendToAI(img.fileInfo || img.path, 'image')`
- **会话保持**: `conversationId` 存在时通过 `requestData.conversationId` 传给 Coze实现多轮对话。
### 2.2 请求参数构造
- **文本消息**type === 'text'
- `additionalMessages`: `[{ role: 'user', type: 'question', content, content_type: 'text' }]`
- **图片消息**type === 'image'
- 使用 `fileInfo.id``fileInfo.file_id` 构造 `content_type: 'object_string'`,内容为 `JSON.stringify([{ type: 'image', file_id: fileId }])`
-`fileId` 时降级为纯文本“我发送了一张图片,请帮我分析”。
### 2.3 非流式与轮询
- 使用 **stream: false**,不采用 SSE/流式。
- 发起对话后根据返回的 `chat.conversation_id``chat.id`**轮询**`pollChatStatus` 每 1 秒请求 `api.cozeRetrieveChat`,最多 60 次。
-`status === 'completed'` 时调用 `getChatMessages` 拉取消息列表,再从中筛 `role === 'assistant'``type === 'answer'` 的消息,逐条 push 到 `messageList`
### 2.4 错误与降级
- `cozeChat` 或后续轮询失败时,在 catch 里 push 一条 AI 文案(文本用 `getAIResponse(content)`,图片用固定“处理出错”提示)。
- `getAIResponse` 为**本地兜底逻辑**:按关键词(如“香蕉”“苹果”“蛋白质”)返回预设回复,与真实 Coze 无关,仅用于接口失败时的展示。
---
## 3. 腾讯云 ASR 语音识别功能的实现
### 3.1 流程概览
1. **录音**: 使用 `uni.getRecorderManager()`(仅 `#ifdef MP-WEIXIN || APP-PLUS`参数60s、16k、单声道、mp3。
2. **上传**: 录音结束后用 `api.uploadFile(res.tempFilePath, { model: 'audio', pid: '8' })` 上传到业务后端,得到 `fullUrl`
3. **创建任务**: `api.createAsrTask({ url: audioUrl, engineModelType: '16k_zh', channelNum: 1, resTextFormat: 0, sourceType: 0 })`,拿到 `taskId`
4. **轮询结果**: `pollAsrResult(taskId)`,每 2 秒查一次,最多 30 次(约 60 秒),直到 `status === 2` 取结果,`status === 3` 视为失败。
5. **解析**: `parseAsrResult(data)` 去掉时间轴格式、换行,整理空格,得到纯文本。
6. **回填**: 将识别结果写入 `inputText`,并自动切回文本模式并聚焦输入框。
### 3.2 平台与权限
- **H5**: `startRecord` 内直接弹窗“H5 暂不支持语音”,不调用录音。
- **微信/App**: 先 `uni.authorize({ scope: 'scope.record' })`,拒绝则引导去设置页。
### 3.3 细节行为
- 录音时长 < 1 秒会 toast“录音时间太短”不发起识别。
- 最长录音 60 秒,`startRecordTimer` 到 60 秒会调用 `stopRecord()`;注意 `stopRecord` 内未传参,实际停止依赖 `recorderManager.onStop` 回调里的 `handleRecordResult(res)`
---
## 4. 消息列表渲染和状态管理
### 4.1 数据结构
- **欢迎语**: 写死在模板里,内容来自 `welcomeMessage`,不进入 `messageList`
- **messageList 单项**:
- 用户: `{ role: 'user', content?, type: 'text' | 'image', imageUrl? }`
- AI: `{ role: 'ai', content }`(从 Coze 拉取时未统一写 `type: 'text'`,模板按 `msg.type !== 'image'` 判断,故正常按文本显示)。
### 4.2 渲染逻辑
- `v-for="(msg, index) in messageList"`**:key="index"**:用 index 作 key在列表中间增删时可能影响复用与动画建议改为稳定 id。
- 根据 `msg.role` 切换 `user-message` / `ai-message`AI 显示头像与“AI营养师”名称用户不显示。
- 文本用 `message-text`,图片用 `message-image` + `@click="previewImage"`
- **加载态**: `isLoading === true` 时在列表底部渲染“打字指示器”(三个动点),与消息项同一列表内。
### 4.3 滚动
- `scroll-top` 绑定 `scrollTop``scrollToBottom()` 通过交替设置 `lastScrollTop` 为 99998/99999 再赋给 `scrollTop` 触发到底部。依赖 `scroll-with-animation` 有动画。
-`sendMessage``onInputFocus`、以及收到 AI 回复后都会调用 `scrollToBottom()`
### 4.4 状态与竞态
- **isLoading**: 在 `sendToAI` 开头置 true`getChatMessages` 成功或失败、以及 `pollChatStatus` 超时/失败时置 false。
- **多图 + 文本**:先顺序 `await sendToAI` 每张图,再发一条文本。每轮都会把 loading 置 true最后一条请求的结束会置 false逻辑正确但多图会连续多轮 loading体验可优化见下文建议
---
## 5. 图片上传和处理逻辑
### 5.1 选择与上传
- **chooseImage**: `uni.chooseImage`count 为 `3 - pendingImages.length`,最多 3 张;`sizeType: ['compressed']``sourceType: ['album', 'camera']`
- 每选一张即调用 **api.cozeUploadFile(filePath)**Coze 专用上传),成功则 push 到 `pendingImages``{ path: filePath, fileInfo: fileInfo }`。上传失败 toast`uni.hideLoading()`
### 5.2 发送时处理
-`sendMessage` 中先 `imagesToSend = [...pendingImages]`,再清空 `pendingImages`
- 对每张图:
- 立即 push 一条用户消息:`role: 'user', type: 'image', imageUrl: img.path, content: '[图片]'`
- 注意这里 **imageUrl 用的是本地 path**,在部分环境下可能一段时间有效,若需长期展示建议用上传后的线上 URL若后端有返回
- 然后 `await sendToAI(img.fileInfo || img.path, 'image')`。多图会顺序产生多轮对话,每轮一次 AI 回复。
### 5.3 预览与删除
- 待发送区:`pendingImages` 列表展示缩略图,点击删除调用 `removeImage(index)` splice 掉。
- 已发送图片:点击气泡内图片 `previewImage(msg.imageUrl)`,使用 `uni.previewImage`
### 5.4 Coze 上传接口
- `api.cozeUploadFile` 使用 `uni.uploadFile`name 为 `'file'`,成功时 `JSON.parse(res.data)` 后 resolve。
- 页面侧兼容 `uploadRes.code === 0 || uploadRes.code === 200` 以及 `fileInfo.id` / `fileInfo.file_id`,兼容不同后端约定。
---
## 6. 发现的问题和优化建议
### 6.1 功能与逻辑问题
| 问题 | 严重程度 | 说明与建议 |
|------|----------|------------|
| **初始 messageList 为假数据** | 中 | 当前写死两条“用户+AI”示例消息易让用户误以为是历史。建议首次进入时 `messageList` 为空,仅保留欢迎语;或从服务端拉取真实历史再渲染。 |
| **clearChat 未在界面暴露** | 低 | `clearChat()` 已实现(清空列表 + 清空 conversationId但模板中无按钮调用用户无法清空对话。建议在头部或输入区增加“清空对话”入口。 |
| **showCommonQuestions 仅 toast** | 低 | 仅提示“常见问题功能开发中”,若暂无规划可移除或改为跳转帮助页。 |
| **sendImageMessage 空实现** | 低 | 注释“已废弃,逻辑合并到 sendMessage”建议删除该方法避免误导。 |
| **多图多轮对话** | 中 | 多图时每张单独一轮 CozeAI 回复会一条条出现,且无法把“多图+一句说明”作为同一上下文。若 Coze 支持多附件一轮,建议改为:一次 `additionalMessages` 里带多条(文本+多图),减少轮次、统一上下文。 |
### 6.2 健壮性与错误处理
| 问题 | 建议 |
|------|------|
| **cozeChat 返回结构变化** | 当前直接取 `response.data.chat`,若后端结构调整易报错。建议加存在性判断(如 `response?.data?.chat`),并对缺少 `conversation_id` 的情况做提示或重试。 |
| **cozeUploadFile 未统一解析 code** | `cozeUploadFile` 的 success 里只 `JSON.parse(res.data)` 未校验 code页面侧用 code 0/200 判断。建议在 api 层统一解析并 reject 非成功 code页面只处理成功结果。 |
| **ASR 轮询超时 60s** | 60 秒对语音识别偏长,可适当缩短(如 20 次 × 2 秒)并提示“识别超时,请重试”。 |
| **录音 60 秒自动 stop** | `stopRecord()` 无参,`recorderManager.stop()` 会触发 `onStop`,但若用户 60 秒松手,与定时器几乎同时可能产生两次结束,可加防抖或状态位避免重复处理。 |
### 6.3 性能与体验
| 问题 | 建议 |
|------|------|
| **消息列表 key 用 index** | 为每条消息生成唯一 id如 uuid 或服务端 id`:key="msg.id"`,避免中间插入导致错位或动画异常。 |
| **scrollToBottom 依赖 magic number** | 99998/99999 在部分机型上可能不生效。可改为:先设一个很大的数触底,再在 `@scroll` 里记录实际 scrollTop下次用 `lastScrollTop + 1` 等小步进触发一次滚动。 |
| **多图连续 loading** | 多图时会出现多次“加载中 → 回复 → 加载中 → 回复”。若后端支持一次请求多图,可合并为一次 loading否则可考虑“仅最后一条显示 loading”或“所有图片请求发完再统一轮询一次”需后端支持。 |
### 6.4 代码与维护
| 问题 | 建议 |
|------|------|
| **单文件过长** | 可拆分为:消息列表(含欢迎语、气泡、打字指示器)、输入栏(文本/语音/图片预览/发送)、快捷问题等子组件,便于复用和单测。 |
| **魔法数字与配置** | `botId`、轮询次数(60/30)、轮询间隔(1000/2000)、最大录音 60s 等建议提到 `data` 或单独 config便于环境区分与调参。 |
| **条件编译分散** | `#ifdef MP-WEIXIN || APP-PLUS``#ifdef H5` 分散在 initRecorder、startRecord、stopRecord 等处,可集中到 mixin 或 composable如 useRecorderH5 直接返回“不支持”的接口。 |
| **getAIResponse 用途不清** | 仅用于 Coze 失败时的兜底,但名字易让人以为是主流程。建议重命名为 `getFallbackResponse` 或移到工具/常量文件,并注释“仅作接口失败兜底”。 |
### 6.5 API 与数据约定
| 问题 | 建议 |
|------|------|
| **Coze 请求字段命名** | 当前传 `botId``userId``additionalMessages` 等 camelCase若后端期望 snake_case`bot_id`),需在 api 层或页面层做一层转换,避免 400 或静默失败。 |
| **用户图片消息 imageUrl** | 当前用本地 path跨会话或重新打开可能失效。若后端/Coze 返回可访问的图片 URL建议存该 URL 到 `imageUrl`,便于持久化与分享。 |
---
## 7. 总结
| 维度 | 评价 |
|------|------|
| **架构** | 单页单组件,结构清晰但体积偏大,建议按区域拆组件。 |
| **Coze 集成** | 非流式 + 轮询实现完整,多轮会话、文本/图片消息和错误降级均有考虑;可加强返回结构校验与多图合并发送。 |
| **ASR** | 录音 → 上传 → 腾讯云 ASR 任务 → 轮询 → 解析流程完整,平台与权限处理到位;可缩短超时、避免重复 onStop。 |
| **消息与状态** | 渲染与滚动逻辑正确;建议稳定 key、初始列表去假数据、暴露清空对话。 |
| **图片** | 选择、Coze 上传、待发送预览与发送流程完整;建议用户消息使用持久化 URL、评估多图合并一轮。 |
按上述问题逐项整改后,可提升可维护性、健壮性和用户体验。