fix: 修复 fillNutrition 中 ClassCastException 及 parseWorkflowResponse 解析失败

- 新增 extractOutputMap() 支持两种策略:
  Strategy 1:output 以 '"' 开头时,先用 JSON.parseObject(str, String.class)
  解码 \" \n 等转义序列,再提取内部 {...}
  Strategy 2:直接 indexOf('{') / lastIndexOf('}') 提取 JSON 对象
- 新增 extractJsonObject() 辅助方法统一提取 {...} 片段
- 修复 line 684 unsafe cast:改为 instanceof Map 检查,避免 ClassCastException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
panchengyong
2026-03-09 15:27:36 +08:00
parent 5d4f79fe03
commit 4ace4452a0

View File

@@ -680,8 +680,14 @@ public class ToolCommunityServiceImpl implements ToolCommunityService {
}
// 提取 output 中的营养数据(嵌套格式)
Object outputObj = workflowResult.get("output");
if (outputObj == null) {
throw new CrmebException("AI 返回数据格式错误");
}
@SuppressWarnings("unchecked")
Map<String, Object> output = (Map<String, Object>) workflowResult.get("output");
Map<String, Object> output = (outputObj instanceof Map)
? (Map<String, Object>) outputObj
: JSON.parseObject(outputObj.toString(), Map.class);
if (output == null || output.isEmpty()) {
throw new CrmebException("AI 返回数据格式错误");
}
@@ -720,31 +726,22 @@ public class ToolCommunityServiceImpl implements ToolCommunityService {
if (dataMap == null) return null;
// 处理 Coze 工作流返回的 output 字段(可能是双编码的 JSON 字符串,可能包含额外的文本)
// 处理 Coze 工作流返回的 output 字段
// Coze 返回的 output 可能是双编码 JSON 字符串:
// - 外层 JSON parse 后得到 String如 "\"{\n \"calories\":...}\""
// - 其中 \" 和 \n 是字面转义字符,需再做一次 JSON 字符串解码
Object outputField = dataMap.get("output");
if (outputField != null) {
try {
String outputStr = outputField instanceof String
? (String) outputField
: outputField.toString();
// 提取 JSON 对象部分(从 { 开始到 } 结束)
int jsonStart = outputStr.indexOf('{');
int jsonEnd = outputStr.lastIndexOf('}');
if (jsonStart >= 0 && jsonEnd > jsonStart) {
String jsonPart = outputStr.substring(jsonStart, jsonEnd + 1);
// 解析 JSON 对象
Map<String, Object> output = JSON.parseObject(jsonPart, Map.class);
result.put("output", output);
} else {
// 如果找不到有效的 JSON尝试直接解析
Map<String, Object> output = JSON.parseObject(outputStr, Map.class);
result.put("output", output);
}
} catch (Exception e) {
log.warn("Failed to parse workflow output field: {}", e.getMessage());
if (outputField instanceof Map) {
// FastJSON 自动解析时已是 Map直接使用
result.put("output", outputField);
} else if (outputField != null) {
String outputStr = outputField.toString();
Map<String, Object> outputMap = extractOutputMap(outputStr);
if (outputMap != null) {
result.put("output", outputMap);
} else {
log.warn("Failed to extract nutrition map from output. First 200 chars: {}",
outputStr.substring(0, Math.min(200, outputStr.length())));
}
}
@@ -758,6 +755,50 @@ public class ToolCommunityServiceImpl implements ToolCommunityService {
return result;
}
/**
* 从 Coze output 字符串中提取营养数据 Map。
*
* Coze 返回的 output 格式可能有两种:
* 1. 直接的 JSON 字符串(已解码),如 {@code {"calories":{...},...}}
* 2. JSON 编码的字符串(含转义序列),如 {@code "{\n \"calories\":{...}...注:..."}
* 此时需先将其作为 JSON 字符串再解析一次,得到真正的 JSON 对象文本,
* 再提取 {@code {...}} 部分进行解析。
*/
@SuppressWarnings("unchecked")
private Map<String, Object> extractOutputMap(String outputStr) {
if (outputStr == null || outputStr.isEmpty()) return null;
// 策略 1若以 " 开头,先做 JSON 字符串解码(处理 \" \n 等转义字符)
if (outputStr.startsWith("\"")) {
try {
String jsonStr = outputStr.endsWith("\"") ? outputStr : outputStr + "\"";
String decoded = JSON.parseObject(jsonStr, String.class);
Map<String, Object> m = extractJsonObject(decoded);
if (m != null) return m;
} catch (Exception e1) {
log.debug("extractOutputMap strategy1 failed: {}", e1.getMessage());
}
}
// 策略 2直接从字符串中提取 {...} 部分并解析
return extractJsonObject(outputStr);
}
/** 从字符串中截取首个 { 到末尾 } 的子串,并用 FastJSON 解析为 Map。 */
@SuppressWarnings("unchecked")
private Map<String, Object> extractJsonObject(String str) {
if (str == null) return null;
int start = str.indexOf('{');
int end = str.lastIndexOf('}');
if (start < 0 || end <= start) return null;
try {
return JSON.parseObject(str.substring(start, end + 1), Map.class);
} catch (Exception e) {
log.debug("extractJsonObject failed: {}", e.getMessage());
return null;
}
}
/**
* 将嵌套格式转为扁平格式
* 输入: {"calories":{"value":103,"unit":"kcal"},...}