package com.xly.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.config.RedisChatMemoryStore; import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.UserMessage; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 会话(conversation)元数据管理 —— 支持「多条命名会话/用户」。 * *
每用户一个会话列表存 Redis Hash {@code chat:convs:{userId}}(field=convId,value=JSON{id,title,updatedAt});
* 消息本体存在 {@link RedisChatMemoryStore}({@code chat:mem:{convId}})。标题取自该会话的首条用户消息。
*/
@Service
public class ConversationService {
private static final String CONVS_KEY = "chat:convs:";
private final StringRedisTemplate redis;
private final RedisChatMemoryStore memoryStore;
private final ObjectMapper mapper;
public ConversationService(StringRedisTemplate redis, RedisChatMemoryStore memoryStore, ObjectMapper mapper) {
this.redis = redis;
this.memoryStore = memoryStore;
this.mapper = mapper;
}
/** 新建一个空会话,返回 convId。 */
public String create(String userId) {
String convId = "c-" + System.currentTimeMillis() + "-" + Integer.toHexString((int) (Math.random() * 0xFFFFF));
writeMeta(userId, convId, "新会话");
return convId;
}
/** 每次对话时调用:会话不存在则建、标题空则用首条消息命名,并刷新 updatedAt。 */
public void touch(String userId, String convId, String firstMsg) {
Object existing = redis.opsForHash().get(CONVS_KEY + userId, convId);
String title = null;
if (existing != null) {
try {
title = mapper.readTree(existing.toString()).path("title").asText(null);
} catch (Exception ignore) {
}
}
if (title == null || title.isBlank() || "新会话".equals(title)) {
title = deriveTitle(firstMsg);
}
writeMeta(userId, convId, title);
}
private void writeMeta(String userId, String convId, String title) {
try {
Map