ConversationService.java
4.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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)元数据管理 —— 支持「多条命名会话/用户」。
*
* <p>每用户一个会话列表存 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<String, Object> m = new LinkedHashMap<>();
m.put("id", convId);
m.put("title", title);
m.put("updatedAt", System.currentTimeMillis());
redis.opsForHash().put(CONVS_KEY + userId, convId, mapper.writeValueAsString(m));
} catch (Exception ignore) {
}
}
/** 该用户的会话列表,按最近更新倒序。 */
public List<Map<String, Object>> list(String userId) {
Map<Object, Object> all = redis.opsForHash().entries(CONVS_KEY + userId);
List<Map<String, Object>> out = new ArrayList<>();
for (Object v : all.values()) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> m = mapper.readValue(v.toString(), Map.class);
out.add(m);
} catch (Exception ignore) {
}
}
out.sort((a, b) -> Long.compare(num(b.get("updatedAt")), num(a.get("updatedAt"))));
return out;
}
public void delete(String userId, String convId) {
redis.opsForHash().delete(CONVS_KEY + userId, convId);
memoryStore.deleteMessages(convId);
}
/** 会话历史,映射为 {role:user|ai, content}。跳过系统消息、工具调用中间消息、工具结果。 */
public List<Map<String, String>> history(String convId) {
List<Map<String, String>> out = new ArrayList<>();
for (ChatMessage m : memoryStore.getMessages(convId)) {
if (m instanceof UserMessage um && um.hasSingleText()) {
out.add(Map.of("role", "user", "content", um.singleText()));
} else if (m instanceof AiMessage am && !am.hasToolExecutionRequests()
&& am.text() != null && !am.text().isBlank()) {
out.add(Map.of("role", "ai", "content", am.text()));
}
}
return out;
}
private String deriveTitle(String s) {
if (s == null) {
return "新会话";
}
String t = s.strip().replace("\n", " ");
if (t.isEmpty()) {
return "新会话";
}
return t.length() > 18 ? t.substring(0, 18) + "…" : t;
}
private long num(Object o) {
try {
return Long.parseLong(String.valueOf(o));
} catch (Exception e) {
return 0L;
}
}
}