Browse Source

fix(sdk): 修复历史会话不显示图片附件

- MessageConverter 落库时持久化多模态 Media 到 metadata._media
- ConversationService 历史消息接口提取 _media 为顶层 images 字段
- SDK 前端历史映射补齐 images,复用 renderUserBubble 附件渲染
Spring-AI-1.1.2
wanghanlin 3 weeks ago
parent
commit
9fe3ee6828
  1. 2
      client/src/api.ts
  2. 12
      client/src/chat.ts
  3. 70
      src/main/java/com/wok/supportbot/converter/MessageConverter.java
  4. 9
      src/main/java/com/wok/supportbot/service/ConversationService.java

2
client/src/api.ts

@ -649,7 +649,7 @@ export async function fetchConversationList(
* 使 /ai/sdk/conversation/{id}/messages * 使 /ai/sdk/conversation/{id}/messages
*/ */
export async function fetchConversationMessages(conversationId: string): Promise<{ export async function fetchConversationMessages(conversationId: string): Promise<{
messages: Array<{ messageType: string; content: string; createTime: string }>;
messages: Array<{ messageType: string; content: string; createTime: string; images?: ImageAttachment[] }>;
total: number; total: number;
}> { }> {
const params = new URLSearchParams(); const params = new URLSearchParams();

12
client/src/chat.ts

@ -181,6 +181,12 @@ async function loadHistoryFromBackend(): Promise<void> {
id: uuid(), id: uuid(),
role: msg.messageType === 'USER' ? 'user' : 'ai' as const, role: msg.messageType === 'USER' ? 'user' : 'ai' as const,
content: msg.content, content: msg.content,
images: (msg.images || []).map((i) => ({
name: i.name || '',
url: i.url,
mimeType: i.mimeType,
size: i.size,
})),
timestamp: new Date(msg.createTime).getTime(), timestamp: new Date(msg.createTime).getTime(),
})); }));
@ -1019,6 +1025,12 @@ async function switchToConversation(conversationId: string): Promise<void> {
id: uuid(), id: uuid(),
role: msg.messageType === 'USER' ? 'user' : 'ai' as const, role: msg.messageType === 'USER' ? 'user' : 'ai' as const,
content: msg.content, content: msg.content,
images: (msg.images || []).map((i) => ({
name: i.name || '',
url: i.url,
mimeType: i.mimeType,
size: i.size,
})),
timestamp: new Date(msg.createTime).getTime(), timestamp: new Date(msg.createTime).getTime(),
})); }));

70
src/main/java/com/wok/supportbot/converter/MessageConverter.java

@ -2,6 +2,9 @@ package com.wok.supportbot.converter;
import com.wok.supportbot.entity.ChatMessage; import com.wok.supportbot.entity.ChatMessage;
import org.springframework.ai.chat.messages.*; 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.*; import java.util.*;
@ -16,6 +19,8 @@ public class MessageConverter {
private static final String META_KEY_TOOL_CALLS = "_toolCalls"; private static final String META_KEY_TOOL_CALLS = "_toolCalls";
/** metadata 中存储 toolResponses 的内部键名 */ /** metadata 中存储 toolResponses 的内部键名 */
private static final String META_KEY_TOOL_RESPONSES = "_toolResponses"; private static final String META_KEY_TOOL_RESPONSES = "_toolResponses";
/** metadata 中存储多模态图片 Media 的内部键名 */
private static final String META_KEY_MEDIA = "_media";
/** /**
* Spring AI Message 转换为数据库实体 ChatMessage * Spring AI Message 转换为数据库实体 ChatMessage
@ -54,6 +59,26 @@ public class MessageConverter {
metadata.put(META_KEY_TOOL_RESPONSES, responsesJson); 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() return ChatMessage.builder()
.conversationId(conversationId) .conversationId(conversationId)
.messageType(message.getMessageType()) .messageType(message.getMessageType())
@ -78,7 +103,11 @@ public class MessageConverter {
: new HashMap<>(); : new HashMap<>();
return switch (messageType) { 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 -> { case ASSISTANT -> {
List<AssistantMessage.ToolCall> toolCalls = extractToolCalls(cleanMetadata); List<AssistantMessage.ToolCall> toolCalls = extractToolCalls(cleanMetadata);
yield new AssistantMessage(text, cleanMetadata, toolCalls); yield new AssistantMessage(text, cleanMetadata, toolCalls);
@ -135,4 +164,43 @@ public class MessageConverter {
} }
return result; 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;
}
}
} }

9
src/main/java/com/wok/supportbot/service/ConversationService.java

@ -283,7 +283,14 @@ public class ConversationService {
item.put("conversationId", msg.getConversationId()); item.put("conversationId", msg.getConversationId());
item.put("messageType", msg.getMessageType()); item.put("messageType", msg.getMessageType());
item.put("content", msg.getContent()); item.put("content", msg.getContent());
item.put("metadata", msg.getMetadata());
// 复制 metadata提取内部键 _media 为顶层 images 字段供历史会话还原图片附件避免内部键泄漏
Map<String, Object> metadata = msg.getMetadata() != null
? new LinkedHashMap<>(msg.getMetadata()) : new LinkedHashMap<>();
Object media = metadata.remove("_media");
item.put("metadata", metadata);
if (media instanceof List<?> mediaList && !mediaList.isEmpty()) {
item.put("images", mediaList);
}
item.put("createTime", msg.getCreateTime()); item.put("createTime", msg.getCreateTime());
result.add(item); result.add(item);
} }

Loading…
Cancel
Save