feat: 营养素AI生成落库 + AI营养师消息级loading占位(设计文档对齐)
后端:
- 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 <noreply@anthropic.com>
This commit is contained in:
51
docs/sql/migration_2026-03-25_nutrient_knowledge.sql
Normal file
51
docs/sql/migration_2026-03-25_nutrient_knowledge.sql
Normal file
@@ -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;
|
||||
@@ -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<String> generateNutrientContent() {
|
||||
int count = toolKnowledgeService.generateNutrientContent();
|
||||
return CommonResult.success("成功生成 " + count + " 条营养素内容");
|
||||
}
|
||||
|
||||
// ==================== AI 营养填充(T06/T07) ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<V2Knowledge> 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<Object> 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<Object> 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<String, Object> dataMap = (Map<String, Object>) data;
|
||||
Object messages = dataMap.get("messages");
|
||||
if (messages instanceof List) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> msgList = (List<Map<String, Object>>) messages;
|
||||
for (int i = msgList.size() - 1; i >= 0; i--) {
|
||||
Map<String, Object> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,5 +50,13 @@ public interface ToolKnowledgeService {
|
||||
* @return 如 total, byType (guide/article/nutrients/recipe), byStatus (published/draft/deleted)
|
||||
*/
|
||||
Map<String, Object> getStats();
|
||||
|
||||
/**
|
||||
* 批量通过 Coze AI 生成 6 种营养素科普内容并写入 v2_knowledge 表(一次性管理接口)
|
||||
* 已存在的营养素自动跳过,避免重复插入。
|
||||
*
|
||||
* @return 本次成功插入条数
|
||||
*/
|
||||
int generateNutrientContent();
|
||||
}
|
||||
|
||||
|
||||
@@ -63,11 +63,17 @@
|
||||
|
||||
<!-- 消息气泡 -->
|
||||
<view :class="['message-bubble', msg.role === 'user' ? 'user-bubble' : 'ai-bubble']">
|
||||
<text v-if="msg.type !== 'image'" class="message-text">{{ msg.content }}</text>
|
||||
<image
|
||||
v-else
|
||||
:src="msg.imageUrl"
|
||||
mode="widthFix"
|
||||
<!-- AI 消息 loading 占位(等待回复时显示打字动画)-->
|
||||
<view v-if="msg.role === 'ai' && msg.loading" class="typing-indicator">
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
</view>
|
||||
<text v-else-if="msg.type !== 'image'" class="message-text">{{ msg.content }}</text>
|
||||
<image
|
||||
v-else
|
||||
:src="msg.imageUrl"
|
||||
mode="widthFix"
|
||||
class="message-image"
|
||||
@click="previewImage(msg.imageUrl)"
|
||||
></image>
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user