fix: 修复Tool模块相关问题 - 优化签到、社区、食物和AI服务功能

This commit is contained in:
msh-agent
2026-04-12 09:31:00 +08:00
parent b164d8ba11
commit 77632510cf
12 changed files with 1158 additions and 756 deletions

View File

@@ -712,20 +712,28 @@ public class ToolCheckinServiceImpl implements ToolCheckinService {
String mealTypeLabel = getMealTypeLabel(sign.getMealType());
String title = mealTypeLabel + "打卡";
// 5. 准备营养数据JSON如果有AI分析结果
// 5. 准备营养数据 JSON:有 AI 结果时写入识别串;无论是否有 AI都写入打卡汇总热量/蛋白质),供社区详情营养卡展示
String nutritionDataJson = null;
if (StrUtil.isNotBlank(sign.getAiRecognizedFoodsJson())) {
try {
Map<String, Object> nutritionData = new HashMap<>();
try {
Map<String, Object> nutritionData = new HashMap<>();
if (sign.getActualEnergy() != null) {
nutritionData.put("actualEnergy", sign.getActualEnergy());
}
if (sign.getActualProtein() != null) {
nutritionData.put("actualProtein", sign.getActualProtein());
}
if (sign.getNutritionScore() != null) {
nutritionData.put("nutritionScore", sign.getNutritionScore());
}
if (StrUtil.isNotBlank(sign.getAiRecognizedFoodsJson())) {
nutritionData.put("aiRecognizedFoods", sign.getAiRecognizedFoodsJson());
nutritionData.put("aiStatus", sign.getAiRecognitionStatus());
nutritionData.put("actualProtein", sign.getActualProtein());
nutritionData.put("actualEnergy", sign.getActualEnergy());
nutritionData.put("nutritionScore", sign.getNutritionScore());
nutritionDataJson = com.alibaba.fastjson.JSON.toJSONString(nutritionData);
} catch (Exception e) {
log.warn("生成营养数据JSON失败: {}", e.getMessage());
}
if (!nutritionData.isEmpty()) {
nutritionDataJson = com.alibaba.fastjson.JSON.toJSONString(nutritionData);
}
} catch (Exception e) {
log.warn("生成营养数据JSON失败: {}", e.getMessage());
}
// 6. 解析封面图

View File

@@ -300,6 +300,11 @@ public class ToolCommunityServiceImpl implements ToolCommunityService {
UserSign userSign = userSignService.getOne(signLqw);
if (userSign != null) {
map.put("mealType", userSign.getMealType());
// 帖子详情页营养卡:无 nutritionDataJson 时前端仍可从顶层 actualEnergy/actualProtein 构建统计
map.put("actualEnergy", userSign.getActualEnergy());
map.put("actualProtein", userSign.getActualProtein());
// 与打卡详情接口字段对齐:仅有 AI 菜品识别、无 actual 汇总时,前端可聚合营养统计
map.put("aiResult", userSign.getAiRecognizedFoodsJson());
map.put("enableAIVideo", userSign.getEnableAiVideo() != null && userSign.getEnableAiVideo() == 1);
log.info("找到打卡记录checkInRecordId: {}, taskId: {}, enableAIVideo: {}",

View File

@@ -69,7 +69,10 @@ public class ToolFoodServiceImpl implements ToolFoodService {
map.put("protein", food.getProtein());
map.put("potassium", food.getPotassium());
map.put("phosphorus", food.getPhosphorus());
map.put("sodium", food.getSodium());
map.put("calcium", food.getCalcium());
map.put("suitabilityLevel", food.getSuitabilityLevel());
map.put("nutrientsJson", food.getNutrientsJson());
result.add(map);
}
return result;
@@ -124,6 +127,7 @@ public class ToolFoodServiceImpl implements ToolFoodService {
map.put("vitaminC", food.getVitaminC());
map.put("purine", food.getPurine());
map.put("servingSize", food.getServingSize());
map.put("nutrientsJson", food.getNutrientsJson());
return map;
}

View File

@@ -304,7 +304,7 @@ public class ToolKieAIServiceImpl implements ToolKieAIService {
String url = config.getBaseUrl() + GEMINI_CHAT_PATH;
Map<String, Object> body = buildGeminiRequestBody(request, false);
HttpHeaders headers = helper.createHeaders();
HttpEntity<String> entity = new HttpEntity<>(helper.toJsonString(body), headers);
HttpEntity<String> entity = new HttpEntity<>(toGeminiUpstreamJson(body), headers);
RestTemplate client = restTemplateWithLongTimeout();
ResponseEntity<String> response = client.exchange(url, HttpMethod.POST, entity, String.class);
if (response.getStatusCode() != HttpStatus.OK || response.getBody() == null) {
@@ -314,7 +314,181 @@ public class ToolKieAIServiceImpl implements ToolKieAIService {
if (result == null) {
throw new RuntimeException("Gemini Chat 响应解析失败");
}
return result;
// 上游可能将 OpenAI 形态包在 data / result 等字段内,解包后再规范化,避免前端拿不到 choices
result = unwrapUpstreamChatCompletionMap(result);
Map<String, Object> normalized = normalizeToOpenAiChoicesFormat(result);
flattenChoicesMessageContentForClient(normalized);
return normalized;
}
/**
* BUG-005将 choices[0].message.content 统一压成字符串,便于前端只读 data.choices[0].message.content 展示;
* 避免上游返回 List/Map 嵌套时前端解析为空却误判为「无有效回复」。
*/
@SuppressWarnings("unchecked")
private void flattenChoicesMessageContentForClient(Map<String, Object> result) {
Object choicesObj = result.get("choices");
if (!(choicesObj instanceof List) || ((List<?>) choicesObj).isEmpty()) {
return;
}
Object c0 = ((List<?>) choicesObj).get(0);
if (!(c0 instanceof Map)) {
return;
}
Object msg = ((Map<?, ?>) c0).get("message");
if (!(msg instanceof Map)) {
return;
}
Map<String, Object> message = (Map<String, Object>) msg;
Object raw = message.get("content");
Object flat = flattenGeminiMessageContent(raw);
message.put("content", flat != null ? flat : "");
}
/**
* 上游偶发返回 Google 风格 {@code candidates} 而非 OpenAI 风格 {@code choices}
* 或 {@code choices} 为空但 {@code candidates} 有正文;规范为 choices[0].message.content
* 便于前端 BUG-005 统一从 data.choices[0].message.content 取值。
*/
@SuppressWarnings("unchecked")
private Map<String, Object> normalizeToOpenAiChoicesFormat(Map<String, Object> result) {
Object choicesObj = result.get("choices");
if (hasNonEmptyFirstChoiceAssistantContent(choicesObj)) {
return result;
}
Object candidatesObj = result.get("candidates");
if (!(candidatesObj instanceof List) || ((List<?>) candidatesObj).isEmpty()) {
return result;
}
Object c0 = ((List<?>) candidatesObj).get(0);
if (!(c0 instanceof Map)) {
return result;
}
Map<String, Object> candidate = (Map<String, Object>) c0;
Object flatContent = flattenGeminiMessageContent(candidate.get("content"));
if (isBlankAssistantContent(flatContent)) {
Object t = candidate.get("text");
if (t != null) {
flatContent = t.toString();
}
}
Map<String, Object> message = new HashMap<>();
message.put("role", "assistant");
message.put("content", flatContent != null ? flatContent : "");
Map<String, Object> choice = new HashMap<>();
choice.put("message", message);
List<Object> newChoices = new ArrayList<>();
newChoices.add(choice);
Map<String, Object> out = new LinkedHashMap<>(result);
out.put("choices", newChoices);
return out;
}
private boolean hasNonEmptyFirstChoiceAssistantContent(Object choicesObj) {
if (!(choicesObj instanceof List) || ((List<?>) choicesObj).isEmpty()) {
return false;
}
Object c0 = ((List<?>) choicesObj).get(0);
if (!(c0 instanceof Map)) {
return false;
}
Object msg = ((Map<?, ?>) c0).get("message");
if (!(msg instanceof Map)) {
return false;
}
return !isBlankAssistantContent(((Map<?, ?>) msg).get("content"));
}
/**
* KieAI 网关偶发返回 { "data": { "choices": [...] } } 等与 OpenAI 兼容体嵌套结构;
* 若根上无有效 choices/candidates则尝试取内层 Map含非空 choices 或非空 candidates
*/
@SuppressWarnings("unchecked")
private Map<String, Object> unwrapUpstreamChatCompletionMap(Map<String, Object> root) {
if (root == null) {
return null;
}
if (hasUsableChoicesOrCandidates(root)) {
return root;
}
String[] keys = {"data", "result", "output"};
for (String key : keys) {
Object nested = root.get(key);
if (!(nested instanceof Map)) {
continue;
}
Map<String, Object> m = (Map<String, Object>) nested;
if (hasUsableChoicesOrCandidates(m)) {
return m;
}
Object inner = m.get("data");
if (inner instanceof Map) {
Map<String, Object> innerMap = (Map<String, Object>) inner;
if (hasUsableChoicesOrCandidates(innerMap)) {
return innerMap;
}
}
}
return root;
}
private boolean hasUsableChoicesOrCandidates(Map<String, Object> m) {
Object choices = m.get("choices");
if (choices instanceof List && !((List<?>) choices).isEmpty()) {
return true;
}
Object candidates = m.get("candidates");
return candidates instanceof List && !((List<?>) candidates).isEmpty();
}
private boolean isBlankAssistantContent(Object content) {
if (content == null) {
return true;
}
if (content instanceof String) {
return ((String) content).trim().isEmpty();
}
if (content instanceof List) {
return ((List<?>) content).isEmpty();
}
if (content instanceof Map) {
return ((Map<?, ?>) content).isEmpty();
}
return false;
}
@SuppressWarnings("unchecked")
private Object flattenGeminiMessageContent(Object contentObj) {
if (contentObj == null) {
return "";
}
if (contentObj instanceof String) {
return contentObj;
}
if (contentObj instanceof List) {
StringBuilder sb = new StringBuilder();
for (Object part : (List<?>) contentObj) {
if (part instanceof Map) {
Object t = ((Map<?, ?>) part).get("text");
if (t != null) {
sb.append(t.toString());
}
}
}
return sb.toString();
}
if (contentObj instanceof Map) {
Map<?, ?> m = (Map<?, ?>) contentObj;
Object parts = m.get("parts");
if (parts instanceof List) {
return flattenGeminiMessageContent(parts);
}
Object text = m.get("text");
if (text != null) {
return text;
}
}
return contentObj.toString();
}
private RestTemplate restTemplateWithLongTimeout() {
@@ -341,7 +515,7 @@ public class ToolKieAIServiceImpl implements ToolKieAIService {
.build();
RequestBody requestBody = RequestBody.create(
okhttp3.MediaType.parse("application/json; charset=utf-8"),
helper.toJsonString(body));
toGeminiUpstreamJson(body));
Request okRequest = new Request.Builder()
.url(url)
.post(requestBody)
@@ -390,68 +564,26 @@ public class ToolKieAIServiceImpl implements ToolKieAIService {
return emitter;
}
/** BUG-005: 仅使用 request.getMessages() 透传用户/助手消息,不注入硬编码 prompt */
/** 发往 KieAI Gemini 的请求体 JSON含 FastJSON 解析的 messages需用 FastJSON 序列化) */
private static String toGeminiUpstreamJson(Map<String, Object> body) {
return com.alibaba.fastjson.JSON.toJSONString(body);
}
/**
* BUG-005仅透传 request.getMessages(),不注入系统 prompt。
* 经 FastJSON 往返序列化,保证多模态等嵌套结构与客户端一致(避免 Jackson 再写出时丢字段)。
*/
private Map<String, Object> buildGeminiRequestBody(KieAIGeminiChatRequest request, boolean stream) {
Map<String, Object> body = new HashMap<>();
List<Map<String, Object>> messagesOut = new ArrayList<>();
for (KieAIGeminiChatRequest.Message msg : request.getMessages()) {
Map<String, Object> m = new HashMap<>();
m.put("role", msg.getRole());
Object content = msg.getContent();
if (content instanceof String) {
List<Map<String, Object>> parts = new ArrayList<>();
Map<String, Object> textPart = new HashMap<>();
textPart.put("type", "text");
textPart.put("text", content);
parts.add(textPart);
m.put("content", parts);
} else if (content instanceof List) {
@SuppressWarnings("unchecked")
List<?> list = (List<?>) content;
List<Map<String, Object>> parts = new ArrayList<>();
for (Object item : list) {
Map<String, Object> part = new HashMap<>();
if (item instanceof KieAIGeminiChatRequest.ContentItem) {
KieAIGeminiChatRequest.ContentItem ci = (KieAIGeminiChatRequest.ContentItem) item;
part.put("type", ci.getType());
if ("text".equals(ci.getType())) {
part.put("text", ci.getText());
} else if ("image_url".equals(ci.getType()) && ci.getImageUrl() != null) {
Map<String, Object> iu = new HashMap<>();
iu.put("url", ci.getImageUrl().getUrl());
part.put("image_url", iu);
}
parts.add(part);
} else if (item instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) item;
String t = (String) map.get("type");
if ("text".equals(t)) {
part.put("type", "text");
part.put("text", map.get("text"));
parts.add(part);
} else if ("image_url".equals(t)) {
Object iu = map.get("image_url");
if (iu instanceof Map) {
String url = (String) ((Map<?, ?>) iu).get("url");
if (url != null) {
part.put("type", "image_url");
Map<String, Object> iuOut = new HashMap<>();
iuOut.put("url", url);
part.put("image_url", iuOut);
parts.add(part);
}
}
}
}
}
m.put("content", parts);
} else {
m.put("content", content);
}
messagesOut.add(m);
if (request.getMessages() == null || request.getMessages().isEmpty()) {
throw new IllegalArgumentException("messages 不能为空");
}
body.put("messages", messagesOut);
Map<String, Object> body = new HashMap<>();
// BUG-005仅用 request.getMessages() 透传;用 FastJSON 序列化再 parse与上游 toJSONString 一致,避免 Jackson 对 Object content 往返变形
String messagesJson = com.alibaba.fastjson.JSON.toJSONString(request.getMessages());
if (messagesJson == null || messagesJson.isEmpty() || "null".equals(messagesJson)) {
throw new IllegalArgumentException("messages 序列化失败");
}
body.put("messages", com.alibaba.fastjson.JSON.parse(messagesJson));
body.put("stream", stream);
if (request.getIncludeThoughts() != null) {
body.put("include_thoughts", request.getIncludeThoughts());