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

13 KiB
Raw Blame History

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.idfileInfo.file_id 构造 content_type: 'object_string',内容为 JSON.stringify([{ type: 'image', file_id: fileId }])
    • fileId 时降级为纯文本“我发送了一张图片,请帮我分析”。

2.3 非流式与轮询

  • 使用 stream: false,不采用 SSE/流式。
  • 发起对话后根据返回的 chat.conversation_idchat.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-messageAI 显示头像与“AI营养师”名称用户不显示。
  • 文本用 message-text,图片用 message-image + @click="previewImage"
  • 加载态: isLoading === true 时在列表底部渲染“打字指示器”(三个动点),与消息项同一列表内。

4.3 滚动

  • scroll-top 绑定 scrollTopscrollToBottom() 通过交替设置 lastScrollTop 为 99998/99999 再赋给 scrollTop 触发到底部。依赖 scroll-with-animation 有动画。
  • sendMessageonInputFocus、以及收到 AI 回复后都会调用 scrollToBottom()

4.4 状态与竞态

  • isLoading: 在 sendToAI 开头置 truegetChatMessages 成功或失败、以及 pollChatStatus 超时/失败时置 false。
  • 多图 + 文本:先顺序 await sendToAI 每张图,再发一条文本。每轮都会把 loading 置 true最后一条请求的结束会置 false逻辑正确但多图会连续多轮 loading体验可优化见下文建议

5. 图片上传和处理逻辑

5.1 选择与上传

  • chooseImage: uni.chooseImagecount 为 3 - pendingImages.length,最多 3 张;sizeType: ['compressed']sourceType: ['album', 'camera']
  • 每选一张即调用 api.cozeUploadFile(filePath)Coze 专用上传),成功则 push 到 pendingImages{ path: filePath, fileInfo: fileInfo }。上传失败 toastuni.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.uploadFilename 为 '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
getAIResponse 用途不清 仅用于 Coze 失败时的兜底,但名字易让人以为是主流程。建议重命名为 getFallbackResponse 或移到工具/常量文件,并注释“仅作接口失败兜底”。

6.5 API 与数据约定

问题 建议
Coze 请求字段命名 当前传 botIduserIdadditionalMessages 等 camelCase若后端期望 snake_casebot_id),需在 api 层或页面层做一层转换,避免 400 或静默失败。
用户图片消息 imageUrl 当前用本地 path跨会话或重新打开可能失效。若后端/Coze 返回可访问的图片 URL建议存该 URL 到 imageUrl,便于持久化与分享。

7. 总结

维度 评价
架构 单页单组件,结构清晰但体积偏大,建议按区域拆组件。
Coze 集成 非流式 + 轮询实现完整,多轮会话、文本/图片消息和错误降级均有考虑;可加强返回结构校验与多图合并发送。
ASR 录音 → 上传 → 腾讯云 ASR 任务 → 轮询 → 解析流程完整,平台与权限处理到位;可缩短超时、避免重复 onStop。
消息与状态 渲染与滚动逻辑正确;建议稳定 key、初始列表去假数据、暴露清空对话。
图片 选择、Coze 上传、待发送预览与发送流程完整;建议用户消息使用持久化 URL、评估多图合并一轮。

按上述问题逐项整改后,可提升可维护性、健壮性和用户体验。