ConversationService.java 4.63 KB
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;
        }
    }
}