From 6ec94875978a9cbf25c985f89255171f5e74b332 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 25 Mar 2026 15:32:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=90=A5=E5=85=BB=E7=B4=A0AI=E7=94=9F?= =?UTF-8?q?=E6=88=90=E8=90=BD=E5=BA=93=20+=20AI=E8=90=A5=E5=85=BB=E5=B8=88?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=BA=A7loading=E5=8D=A0=E4=BD=8D=EF=BC=88?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3=E5=AF=B9=E9=BD=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - ToolKnowledgeService/Impl 新增 generateNutrientContent() 调用 Coze AI 批量生成6种营养素(蛋白质/钾/磷/钠/钙/水分) 科普内容并写入 v2_knowledge,已存在的自动跳过 - ToolController 新增 POST /tool/knowledge/generate-nutrients 端点(管理端一次性调用后自动补充封面图) - 新增 SQL 备用脚本 migration_2026-03-25_nutrient_knowledge.sql 含6种营养素完整JSON,直接执行可跳过AI生成 前端(ai-nutritionist.vue,对齐功能开发详细设计文档任务3-2): - 新增 sleep(ms) 工具方法 - sendToAI 发起前先推入 {loading:true} 占位气泡 - pollChatStatus 轮询间隔由 1000ms 调整为 1500ms - getChatMessages 回调填充占位气泡(不再 push 新消息) - 所有错误/超时/失败路径统一更新 aiMsg.loading=false Co-Authored-By: Claude Sonnet 4.6 --- ...igration_2026-03-25_nutrient_knowledge.sql | 51 +++++++ .../zbkj/front/controller/ToolController.java | 11 ++ .../impl/tool/ToolKnowledgeServiceImpl.java | 140 ++++++++++++++++++ .../service/tool/ToolKnowledgeService.java | 8 + .../pages/tool/ai-nutritionist.vue | 114 +++++++------- 5 files changed, 269 insertions(+), 55 deletions(-) create mode 100644 docs/sql/migration_2026-03-25_nutrient_knowledge.sql diff --git a/docs/sql/migration_2026-03-25_nutrient_knowledge.sql b/docs/sql/migration_2026-03-25_nutrient_knowledge.sql new file mode 100644 index 0000000..7c7c03a --- /dev/null +++ b/docs/sql/migration_2026-03-25_nutrient_knowledge.sql @@ -0,0 +1,51 @@ +-- ============================================================ +-- 营养素知识内容初始化(备用方案 B:手动 SQL 插入) +-- 日期: 2026-03-25 +-- 说明: 若 Coze AI 生成接口(POST /api/front/tool/knowledge/generate-nutrients) +-- 效果不理想,可直接执行本脚本将内置内容写入 v2_knowledge 表。 +-- 内容已按 CKD 营养管理临床指南整理。 +-- ============================================================ + +-- 执行前检查(可选): +-- SELECT nutrient_name, status FROM v2_knowledge WHERE type = 'nutrients'; + +INSERT INTO v2_knowledge + (type, nutrient_name, title, content, summary, status, sort_order, view_count, like_count, created_at, published_at) +VALUES +-- 1. 蛋白质 +('nutrients', '蛋白质', '蛋白质 — CKD患者膳食管理', + '{"name":"蛋白质","english":"Protein","icon":"🥩","description":"构成人体组织的重要营养素","status":"需控制","statusDesc":"根据CKD分期调整摄入量","importance":"蛋白质是人体细胞的基本组成成分,参与免疫功能和组织修复。但过多蛋白质会增加肾脏代谢负担,加速肾功能恶化。透析患者因透析过程中蛋白质丢失,反而需要适当增加摄入。","recommendation":"CKD 1-2期:0.8-1.0g/kg体重/天\\nCKD 3-5期(未透析):0.6-0.8g/kg体重/天\\n透析患者:1.0-1.2g/kg体重/天","foodSources":["鸡蛋","鱼类","瘦肉","牛奶","豆腐","鸡胸肉"],"riskWarning":"过多蛋白质摄入会产生大量含氮废物,加重肾脏负担;过少则导致营养不良、免疫力下降,影响透析充分性。","suggestions":["优先选择优质蛋白(鸡蛋、鱼、瘦肉)","控制植物蛋白摄入(豆类适量)","每餐均匀分配蛋白质摄入","透析患者需适当增加蛋白质","定期监测血白蛋白水平","咨询营养师制定个性化方案"],"disclaimer":"以上建议仅供参考,具体方案请咨询您的主治医生或营养师"}', + '了解蛋白质在慢性肾病饮食中的重要性', 'published', 1, 0, 0, NOW(), NOW()), + +-- 2. 钾 +('nutrients', '钾', '钾 — CKD患者膳食管理', + '{"name":"钾","english":"Potassium (K)","icon":"🍌","description":"维持神经肌肉功能的重要元素","status":"严格控制","statusDesc":"高钾血症可危及生命","importance":"钾离子参与维持心脏节律、神经传导和肌肉收缩。肾功能下降时,钾排泄减少,血钾升高可导致心律失常甚至心脏骤停,是CKD患者最危险的并发症之一。","recommendation":"CKD 3-5期:1500-2000mg/天\\n透析患者:2000-2500mg/天(透析间期严格控制)\\n血钾目标:3.5-5.0mmol/L","foodSources":["香蕉","橙子","土豆","番茄","菠菜","蘑菇"],"riskWarning":"高钾血症可导致心律失常、肌肉无力,严重时危及生命;低钾同样有害,需保持动态平衡。","suggestions":["避免高钾水果(香蕉、橙子、猕猴桃)","蔬菜先焯水再烹饪可减少30-50%的钾","避免饮用浓缩果汁和菜汤","少吃坚果、巧克力、干果","定期监测血钾水平","透析日可适当放宽限制"],"disclaimer":"以上建议仅供参考,具体方案请咨询您的主治医生或营养师"}', + '了解钾在慢性肾病饮食中的重要性', 'published', 2, 0, 0, NOW(), NOW()), + +-- 3. 磷 +('nutrients', '磷', '磷 — CKD患者膳食管理', + '{"name":"磷","english":"Phosphorus (P)","icon":"🥜","description":"骨骼健康的重要矿物质","status":"严格控制","statusDesc":"高磷可导致骨病和血管钙化","importance":"磷与钙共同维持骨骼健康。肾功能下降时磷排泄减少,血磷升高可导致继发性甲状旁腺功能亢进、肾性骨病和血管钙化,显著增加心血管疾病风险。","recommendation":"CKD 3-5期及透析患者:800-1000mg/天\\n血磷目标:1.13-1.78mmol/L\\n注意:透析只能清除约900mg磷/次,饮食控制不可或缺","foodSources":["坚果","动物内脏","可乐","加工食品","奶酪","蛋黄"],"riskWarning":"高磷血症可导致皮肤瘙痒、骨痛、血管钙化,增加心血管疾病风险。磷酸盐添加剂(无机磷)吸收率可达90%,危害更大。","suggestions":["避免含磷添加剂的加工食品(看成分表)","限制坚果、动物内脏摄入","少喝碳酸饮料(含磷酸)","按医嘱服用磷结合剂(随餐服用)","选择低磷蛋白质来源(蛋白、鸡胸肉)","烹饪时焯水可减少蔬菜中的磷"],"disclaimer":"以上建议仅供参考,具体方案请咨询您的主治医生或营养师"}', + '了解磷在慢性肾病饮食中的重要性', 'published', 3, 0, 0, NOW(), NOW()), + +-- 4. 钠 +('nutrients', '钠', '钠 — CKD患者膳食管理', + '{"name":"钠","english":"Sodium (Na)","icon":"🧂","description":"调节体液平衡的电解质","status":"适量控制","statusDesc":"减少摄入,保护肾功能","importance":"钠参与调节体液平衡和血压,是控制水肿和高血压的关键。过多钠摄入会导致水肿、血压升高,加速肾功能恶化,增加心血管负担。","recommendation":"CKD患者:<2000mg钠/天(相当于5g食盐)\\n高血压/水肿患者:<1500mg钠/天\\n透析患者:严格控制,防止透析间期体重增加过多","foodSources":["食盐","酱油","腌制食品","加工肉类","咸菜","速食方便面"],"riskWarning":"摄入过多会导致水肿、高血压、心力衰竭等问题,并加重肾脏负担;过少则可能引起低钠血症和低血压。","suggestions":["每日食盐控制在5g以内(约一啤酒瓶盖)","避免腌制、熏制食品","少用酱油、味精等调味品","可用葱姜蒜、柠檬汁增加风味","查看食品标签,选择低钠产品","透析患者严格控制两次透析间体重增长"],"disclaimer":"以上建议仅供参考,具体方案请咨询您的主治医生或营养师"}', + '了解钠在慢性肾病饮食中的重要性', 'published', 4, 0, 0, NOW(), NOW()), + +-- 5. 钙 +('nutrients', '钙', '钙 — CKD患者膳食管理', + '{"name":"钙","english":"Calcium (Ca)","icon":"🥛","description":"骨骼和牙齿的主要成分","status":"注意补充","statusDesc":"CKD患者易发生钙代谢紊乱","importance":"钙是骨骼和牙齿的主要成分,参与肌肉收缩和神经传导。CKD患者因维生素D活化障碍、高磷血症等因素,常出现低钙血症和肾性骨病,需要在医生指导下补充。","recommendation":"CKD 3-5期:800-1000mg/天(含饮食+补充剂)\\n透析患者:按血钙水平调整\\n血钙目标:2.1-2.5mmol/L\\n注意:补钙同时需控磷,避免钙磷乘积过高","foodSources":["低脂牛奶","豆腐","绿叶蔬菜","芝麻","小虾皮","钙强化食品"],"riskWarning":"低钙会导致骨质疏松、肌肉抽搐;高钙(尤其合并高磷时)会加重血管钙化,增加心血管风险。","suggestions":["在医生指导下补充钙剂","选择碳酸钙(随餐服用效果最佳)","补钙的同时需服用活性维生素D","避免与磷结合剂同时服用","定期检测血钙、血磷和PTH","避免高草酸食物(影响钙吸收)"],"disclaimer":"以上建议仅供参考,具体方案请咨询您的主治医生或营养师"}', + '了解钙在慢性肾病饮食中的重要性', 'published', 5, 0, 0, NOW(), NOW()), + +-- 6. 水分 +('nutrients', '水分', '水分 — CKD患者膳食管理', + '{"name":"水分","english":"Water / Fluid","icon":"💧","description":"生命之源,CKD患者需精确管理","status":"严格限制","statusDesc":"透析患者须控制每日摄水量","importance":"水分维持体内环境稳定,参与所有代谢反应。CKD晚期患者肾脏排水能力下降,水分积聚可导致水肿、高血压、肺水肿,甚至危及生命。","recommendation":"CKD 1-3期:通常无需限制,保持正常饮水\\nCKD 4-5期(未透析):根据尿量调整,一般为尿量+500ml\\n透析患者:每日摄入量 = 尿量 + 透析间期允许体重增加量(一般不超过1kg/天)","foodSources":["白开水","茶","汤品","粥","水果","蔬菜(含水量高)"],"riskWarning":"水分摄入过多可导致水肿、呼吸困难、血压升高;过少则可能引起脱水和低血压,影响残余肾功能。","suggestions":["记录每日饮水量和尿量","口渴时小口慢饮,避免大量饮水","减少含水量高的食物(西瓜、汤面)","用冰块含服缓解口渴","避免过咸食物(会增加渴感)","透析患者严格控制两次透析间体重增长不超过5%干体重"],"disclaimer":"以上建议仅供参考,具体方案请咨询您的主治医生或营养师"}', + '了解水分在慢性肾病饮食中的重要性', 'published', 6, 0, 0, NOW(), NOW()) + +ON DUPLICATE KEY UPDATE + content = VALUES(content), + status = 'published', + updated_at = NOW(); + +-- 验证 +SELECT knowledge_id, nutrient_name, status, created_at FROM v2_knowledge WHERE type = 'nutrients' ORDER BY sort_order; diff --git a/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java b/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java index c5f3013..8635fdd 100644 --- a/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java +++ b/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/ToolController.java @@ -349,6 +349,17 @@ public class ToolController { return CommonResult.success(updated); } + /** + * 批量通过 Coze AI 生成 6 种营养素科普内容并写入 v2_knowledge(管理端一次性调用) + * 已存在的营养素自动跳过。调用前请确保 Coze PAT Token 有效。 + */ + @ApiOperation(value = "AI生成营养素知识内容并落库") + @PostMapping("/knowledge/generate-nutrients") + public CommonResult generateNutrientContent() { + int count = toolKnowledgeService.generateNutrientContent(); + return CommonResult.success("成功生成 " + count + " 条营养素内容"); + } + // ==================== AI 营养填充(T06/T07) ==================== /** diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java index c9491b0..031db27 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java @@ -13,8 +13,13 @@ import com.zbkj.service.service.tool.ToolKnowledgeService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import com.zbkj.common.request.coze.CozeChatRequest; +import com.zbkj.common.response.CozeBaseResponse; +import com.zbkj.service.service.tool.ToolCozeService; + import javax.annotation.Resource; import java.util.ArrayList; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,6 +40,9 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService { @Resource private DishImageService dishImageService; + @Resource + private ToolCozeService toolCozeService; + /** * 获取营养知识列表 * @param pageParamRequest 分页参数 @@ -176,4 +184,136 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService { stats.put("byStatus", byStatus); return stats; } + + /** + * 批量通过 Coze AI 生成 6 种营养素科普内容并写入 v2_knowledge 表 + * 已存在的营养素自动跳过,避免重复插入。 + */ + @Override + public int generateNutrientContent() { + String[] nutrients = {"蛋白质", "钾", "磷", "钠", "钙", "水分"}; + int success = 0; + + for (int i = 0; i < nutrients.length; i++) { + String nutrient = nutrients[i]; + try { + // 1. 检查是否已存在已发布的记录 + LambdaQueryWrapper existQuery = new LambdaQueryWrapper<>(); + existQuery.eq(V2Knowledge::getType, "nutrients") + .eq(V2Knowledge::getNutrientName, nutrient) + .eq(V2Knowledge::getStatus, "published"); + Long count = v2KnowledgeDao.selectCount(existQuery); + if (count > 0) { + log.info("[generateNutrient] 营养素 {} 已存在,跳过", nutrient); + continue; + } + + // 2. 调用 Coze AI 生成内容 + String prompt = buildNutrientPrompt(nutrient); + CozeChatRequest req = new CozeChatRequest(); + req.setBotId("7591133240535449654"); + req.setUserId("system_admin"); + req.setStream(false); + CozeChatRequest.ChatMessage msg = new CozeChatRequest.ChatMessage(); + msg.setRole("user"); + msg.setContent(prompt); + msg.setContentType("text"); + req.setAdditionalMessages(java.util.Collections.singletonList(msg)); + + CozeBaseResponse resp = toolCozeService.chat(req); + String content = extractCozeContent(resp); + + if (StrUtil.isBlank(content) || content.startsWith("AI生成失败")) { + log.warn("[generateNutrient] {} 内容生成为空或失败,跳过", nutrient); + continue; + } + + // 3. 写入 v2_knowledge 表 + V2Knowledge knowledge = new V2Knowledge(); + knowledge.setType("nutrients"); + knowledge.setNutrientName(nutrient); + knowledge.setTitle(nutrient + " — CKD患者膳食管理"); + knowledge.setContent(content); + knowledge.setSummary("了解" + nutrient + "在慢性肾病饮食中的重要性"); + knowledge.setStatus("published"); + knowledge.setSortOrder(i + 1); + knowledge.setViewCount(0); + knowledge.setLikeCount(0); + knowledge.setCreatedAt(new Date()); + knowledge.setPublishedAt(new Date()); + v2KnowledgeDao.insert(knowledge); + + success++; + log.info("[generateNutrient] 营养素 {} 生成成功,knowledge_id={}", nutrient, knowledge.getKnowledgeId()); + } catch (Exception e) { + log.error("[generateNutrient] 营养素 {} 生成失败", nutrient, e); + } + } + + // 4. 自动补充封面图(最多 6 张) + if (success > 0) { + try { + fillMissingCoverImages(6); + } catch (Exception e) { + log.warn("[generateNutrient] 封面图生成失败(不影响主流程)", e); + } + } + + return success; + } + + /** + * 构建 Coze AI 营养素科普 Prompt(要求返回严格 JSON) + */ + private String buildNutrientPrompt(String nutrient) { + return "请为慢性肾脏病(CKD)患者生成关于「" + nutrient + "」的科普内容。\n" + + "要求严格按以下 JSON 格式返回,不要包含任何 markdown 标记或代码块:\n" + + "{\n" + + " \"name\": \"" + nutrient + "\",\n" + + " \"english\": \"对应英文名\",\n" + + " \"icon\": \"一个合适的 emoji\",\n" + + " \"description\": \"一句话描述(15字以内)\",\n" + + " \"status\": \"控制建议(如:需控制/严格控制/适量补充)\",\n" + + " \"statusDesc\": \"状态补充说明(15字以内)\",\n" + + " \"importance\": \"为什么重要(2-3句话,针对CKD患者)\",\n" + + " \"recommendation\": \"推荐摄入量(分CKD 1-2期、3-5期未透析、透析患者三种情况说明)\",\n" + + " \"foodSources\": [\"食物1\", \"食物2\", \"食物3\", \"食物4\", \"食物5\", \"食物6\"],\n" + + " \"riskWarning\": \"风险提示(2-3句话)\",\n" + + " \"suggestions\": [\"建议1\", \"建议2\", \"建议3\", \"建议4\", \"建议5\", \"建议6\"],\n" + + " \"disclaimer\": \"以上建议仅供参考,具体方案请咨询您的主治医生或营养师\"\n" + + "}"; + } + + /** + * 从 Coze 响应中提取文本内容(取最后一条 assistant answer 消息) + */ + private String extractCozeContent(CozeBaseResponse resp) { + try { + if (resp == null || resp.getData() == null) return "AI生成失败:响应为空"; + Object data = resp.getData(); + // CozeBaseResponse.data 通常为 Map 结构,content 字段在 messages 列表中 + if (data instanceof Map) { + @SuppressWarnings("unchecked") + Map dataMap = (Map) data; + Object messages = dataMap.get("messages"); + if (messages instanceof List) { + @SuppressWarnings("unchecked") + List> msgList = (List>) messages; + for (int i = msgList.size() - 1; i >= 0; i--) { + Map m = msgList.get(i); + if ("assistant".equals(m.get("role")) && "answer".equals(m.get("type"))) { + return String.valueOf(m.get("content")); + } + } + } + // fallback: 直接取 content + Object content = dataMap.get("content"); + if (content != null) return String.valueOf(content); + } + return String.valueOf(data); + } catch (Exception e) { + log.error("[extractCozeContent] 解析失败", e); + return "AI生成失败:" + e.getMessage(); + } + } } diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolKnowledgeService.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolKnowledgeService.java index 5b934e5..63f953f 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolKnowledgeService.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/tool/ToolKnowledgeService.java @@ -50,5 +50,13 @@ public interface ToolKnowledgeService { * @return 如 total, byType (guide/article/nutrients/recipe), byStatus (published/draft/deleted) */ Map getStats(); + + /** + * 批量通过 Coze AI 生成 6 种营养素科普内容并写入 v2_knowledge 表(一次性管理接口) + * 已存在的营养素自动跳过,避免重复插入。 + * + * @return 本次成功插入条数 + */ + int generateNutrientContent(); } diff --git a/msh_single_uniapp/pages/tool/ai-nutritionist.vue b/msh_single_uniapp/pages/tool/ai-nutritionist.vue index 6a9a534..151240a 100644 --- a/msh_single_uniapp/pages/tool/ai-nutritionist.vue +++ b/msh_single_uniapp/pages/tool/ai-nutritionist.vue @@ -63,11 +63,17 @@ - {{ msg.content }} - + + + + + + {{ msg.content }} + @@ -626,9 +632,19 @@ export default { return String(content); }, + /** 工具方法:sleep ms 毫秒 */ + sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + }, + async sendToAI(content, type) { this.isLoading = true; + // 添加 AI 占位消息(loading 状态,等待 Coze 返回后填充内容) + const aiMsg = { role: 'ai', content: '', loading: true }; + this.messageList.push(aiMsg); + this.scrollToBottom(); + // 统一走 Coze API(文本、多模态、图片均使用 Coze Bot) const userId = this.uid || (uni.getStorageSync('userInfo') || {}).id || 'default_user'; try { @@ -698,7 +714,7 @@ export default { const chatId = chat.id; if (conversationId && chatId) { this.conversationId = conversationId; - await this.pollChatStatus(conversationId, chatId); + await this.pollChatStatus(conversationId, chatId, aiMsg); } else { throw new Error('发起对话失败:未返回会话或对话ID'); } @@ -708,26 +724,23 @@ export default { } catch (error) { console.error('发送消息失败:', error); this.isLoading = false; - this.messageList.push({ - role: 'ai', - content: '抱歉,处理您的请求时出现错误,请稍后再试。' - }); + aiMsg.content = '抱歉,处理您的请求时出现错误,请稍后再试。'; + aiMsg.loading = false; + this.messageList = [...this.messageList]; // 触发响应式更新 this.scrollToBottom(); } }, - async pollChatStatus(conversationId, chatId) { - const maxAttempts = 60; // 最多轮询60次,即60秒 + async pollChatStatus(conversationId, chatId, aiMsg) { + const maxAttempts = 60; // 最多轮询60次(每次1.5秒),即90秒 let attempts = 0; const checkStatus = async () => { attempts++; if (attempts > maxAttempts) { this.isLoading = false; - this.messageList.push({ - role: 'ai', - content: '抱歉,AI 响应超时,请稍后再试。' - }); + if (aiMsg) { aiMsg.content = '抱歉,AI 响应超时,请稍后再试。'; aiMsg.loading = false; this.messageList = [...this.messageList]; } + else { this.messageList.push({ role: 'ai', content: '抱歉,AI 响应超时,请稍后再试。' }); } this.scrollToBottom(); return; } @@ -743,21 +756,19 @@ export default { const chatObj = res.data.chat || res.data; const status = chatObj && chatObj.status; - if (status === 'completed') { - // 对话完成,获取消息详情 - await this.getChatMessages(conversationId, chatId); - } else if (status === 'failed' || status === 'canceled') { - this.isLoading = false; - this.messageList.push({ - role: 'ai', - content: `抱歉,对话${status === 'canceled' ? '已取消' : '失败'}。` - }); - this.scrollToBottom(); - } else { - // 继续轮询 (created, in_progress, requires_action) - // 注意:requires_action 可能需要额外处理,这里暂时视为继续等待或需人工干预 - setTimeout(checkStatus, 1000); - } + if (status === 'completed') { + // 对话完成,获取消息详情 + await this.getChatMessages(conversationId, chatId, aiMsg); + } else if (status === 'failed' || status === 'canceled') { + this.isLoading = false; + const failMsg = `抱歉,对话${status === 'canceled' ? '已取消' : '失败'}。`; + if (aiMsg) { aiMsg.content = failMsg; aiMsg.loading = false; this.messageList = [...this.messageList]; } + else { this.messageList.push({ role: 'ai', content: failMsg }); } + this.scrollToBottom(); + } else { + // 继续轮询 (created, in_progress) 每 1.5 秒 + setTimeout(checkStatus, 1500); + } } else { // 查询失败,重试 setTimeout(checkStatus, 1000); @@ -771,7 +782,7 @@ export default { checkStatus(); }, - async getChatMessages(conversationId, chatId) { + async getChatMessages(conversationId, chatId, aiMsg) { try { const res = await api.cozeMessageList({ conversationId, @@ -786,29 +797,23 @@ export default { const answerMsgs = rawMessages.filter(msg => msg.role === 'assistant' && msg.type === 'answer'); if (answerMsgs.length > 0) { - // 可能有多条回复,依次显示 - for (const msg of answerMsgs) { - this.messageList.push({ - role: 'ai', - content: msg.content - }); + // 用第一条 answer 消息填充占位气泡,多条追加新气泡 + if (aiMsg) { + aiMsg.content = answerMsgs[0].content; + aiMsg.loading = false; + for (let i = 1; i < answerMsgs.length; i++) { + this.messageList.push({ role: 'ai', content: answerMsgs[i].content }); + } + this.messageList = [...this.messageList]; // 触发响应式更新 + } else { + for (const msg of answerMsgs) { this.messageList.push({ role: 'ai', content: msg.content }); } } } else { // 尝试查找其他类型的回复 const otherMsgs = rawMessages.filter(msg => msg.role === 'assistant'); - if (otherMsgs.length > 0) { - for (const msg of otherMsgs) { - this.messageList.push({ - role: 'ai', - content: msg.content - }); - } - } else { - this.messageList.push({ - role: 'ai', - content: '未能获取到有效回复。' - }); - } + const fallback = otherMsgs.length > 0 ? otherMsgs[0].content : '未能获取到有效回复。'; + if (aiMsg) { aiMsg.content = fallback; aiMsg.loading = false; this.messageList = [...this.messageList]; } + else { this.messageList.push({ role: 'ai', content: fallback }); } } this.scrollToBottom(); } else { @@ -817,10 +822,9 @@ export default { } catch (e) { console.error('获取消息详情失败:', e); this.isLoading = false; - this.messageList.push({ - role: 'ai', - content: '获取回复内容失败。' - }); + const errContent = '获取回复内容失败。'; + if (aiMsg) { aiMsg.content = errContent; aiMsg.loading = false; this.messageList = [...this.messageList]; } + else { this.messageList.push({ role: 'ai', content: errContent }); } this.scrollToBottom(); } },