|
|
|
@ -2,6 +2,9 @@ package com.wok.supportbot.converter; |
|
|
|
|
|
|
|
import com.wok.supportbot.entity.ChatMessage; |
|
|
|
import org.springframework.ai.chat.messages.*; |
|
|
|
import org.springframework.ai.content.Media; |
|
|
|
import org.springframework.util.MimeType; |
|
|
|
import org.springframework.util.MimeTypeUtils; |
|
|
|
|
|
|
|
import java.util.*; |
|
|
|
|
|
|
|
@ -16,6 +19,8 @@ public class MessageConverter { |
|
|
|
private static final String META_KEY_TOOL_CALLS = "_toolCalls"; |
|
|
|
/** metadata 中存储 toolResponses 的内部键名 */ |
|
|
|
private static final String META_KEY_TOOL_RESPONSES = "_toolResponses"; |
|
|
|
/** metadata 中存储多模态图片 Media 的内部键名 */ |
|
|
|
private static final String META_KEY_MEDIA = "_media"; |
|
|
|
|
|
|
|
/** |
|
|
|
* 将 Spring AI Message 转换为数据库实体 ChatMessage |
|
|
|
@ -54,6 +59,26 @@ public class MessageConverter { |
|
|
|
metadata.put(META_KEY_TOOL_RESPONSES, responsesJson); |
|
|
|
} |
|
|
|
|
|
|
|
// 处理多模态图片 Media(仅用户消息携带),序列化到 metadata 供历史会话还原图片附件 |
|
|
|
if (message instanceof UserMessage userMessage) { |
|
|
|
List<Media> mediaList = userMessage.getMedia(); |
|
|
|
if (mediaList != null && !mediaList.isEmpty()) { |
|
|
|
List<Map<String, String>> mediaJson = new ArrayList<>(); |
|
|
|
for (Media media : mediaList) { |
|
|
|
Map<String, String> m = new LinkedHashMap<>(); |
|
|
|
Object data = media.getData(); |
|
|
|
if (data != null) { |
|
|
|
m.put("url", String.valueOf(data)); |
|
|
|
} |
|
|
|
if (media.getMimeType() != null) { |
|
|
|
m.put("mimeType", media.getMimeType().toString()); |
|
|
|
} |
|
|
|
mediaJson.add(m); |
|
|
|
} |
|
|
|
metadata.put(META_KEY_MEDIA, mediaJson); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return ChatMessage.builder() |
|
|
|
.conversationId(conversationId) |
|
|
|
.messageType(message.getMessageType()) |
|
|
|
@ -78,7 +103,11 @@ public class MessageConverter { |
|
|
|
: new HashMap<>(); |
|
|
|
|
|
|
|
return switch (messageType) { |
|
|
|
case USER -> new UserMessage(text); |
|
|
|
case USER -> { |
|
|
|
List<Media> media = extractMedia(cleanMetadata); |
|
|
|
yield media.isEmpty() ? new UserMessage(text) |
|
|
|
: UserMessage.builder().text(text).media(media).build(); |
|
|
|
} |
|
|
|
case ASSISTANT -> { |
|
|
|
List<AssistantMessage.ToolCall> toolCalls = extractToolCalls(cleanMetadata); |
|
|
|
yield new AssistantMessage(text, cleanMetadata, toolCalls); |
|
|
|
@ -135,4 +164,43 @@ public class MessageConverter { |
|
|
|
} |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 从 metadata 中提取并移除 _media 键,反序列化为 Media 列表。 |
|
|
|
* 无媒体时返回空列表,保证多轮记忆不丢图片。 |
|
|
|
*/ |
|
|
|
@SuppressWarnings("unchecked") |
|
|
|
private static List<Media> extractMedia(Map<String, Object> metadata) { |
|
|
|
Object raw = metadata.remove(META_KEY_MEDIA); |
|
|
|
if (!(raw instanceof List<?> list) || list.isEmpty()) { |
|
|
|
return List.of(); |
|
|
|
} |
|
|
|
List<Media> result = new ArrayList<>(); |
|
|
|
for (Object item : list) { |
|
|
|
if (item instanceof Map) { |
|
|
|
Map<?, ?> map = (Map<?, ?>) item; |
|
|
|
Object url = map.get("url"); |
|
|
|
if (url == null) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
result.add(Media.builder() |
|
|
|
.data(url.toString()) |
|
|
|
.mimeType(parseMimeTypeSafely(map.get("mimeType"))) |
|
|
|
.build()); |
|
|
|
} |
|
|
|
} |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
/** 安全解析 MIME 类型,缺失或非法时兜底为 image/png(与图片扩展名推断逻辑一致) */ |
|
|
|
private static MimeType parseMimeTypeSafely(Object mimeType) { |
|
|
|
if (mimeType == null) { |
|
|
|
return MimeTypeUtils.IMAGE_PNG; |
|
|
|
} |
|
|
|
try { |
|
|
|
return MimeTypeUtils.parseMimeType(mimeType.toString()); |
|
|
|
} catch (Exception e) { |
|
|
|
return MimeTypeUtils.IMAGE_PNG; |
|
|
|
} |
|
|
|
} |
|
|
|
} |