LedgerService.java 3.25 KB
package com.xly.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 会话账本(append-only 事件流)—— 会话里发生过的**一切**按序落账,包括不经过 LLM 的确定性路径
 * (表单弹出/澄清/写提议/确认结果),修补「确定性路径不进对话记忆」的记忆空洞;前端历史从账本重放。
 *
 * <p>键:Redis LIST {@code chat:ledger:{convId}},元素为事件 JSON {@code {t,type,...}},30 天 TTL。
 * 事件类型:{@code user}(text)/ {@code assistant}(text)/ {@code clarify}(text)/
 * {@code form}(entity,message)/ {@code question}(question,options)/ {@code tool}(name,digest)/
 * {@code proposal}(opId,summary)/ {@code confirm}(opId,status,msg)/ {@code cancel}(opId,description)。
 */
@Service
public class LedgerService {

    private static final Logger log = LoggerFactory.getLogger(LedgerService.class);
    private static final String PREFIX = "chat:ledger:";
    private static final Duration TTL = Duration.ofDays(30);

    private final StringRedisTemplate redis;
    private final ObjectMapper mapper;

    public LedgerService(StringRedisTemplate redis, ObjectMapper mapper) {
        this.redis = redis;
        this.mapper = mapper;
    }

    /** 追加一条事件(绝不抛异常——账本失败不能影响对话主流程)。 */
    public void append(String convId, String type, Map<String, Object> data) {
        if (convId == null || convId.isBlank()) {
            return;
        }
        try {
            Map<String, Object> ev = new LinkedHashMap<>();
            ev.put("t", System.currentTimeMillis());
            ev.put("type", type);
            if (data != null) {
                ev.putAll(data);
            }
            String key = PREFIX + convId;
            redis.opsForList().rightPush(key, mapper.writeValueAsString(ev));
            redis.expire(key, TTL);
        } catch (Exception e) {
            log.warn("ledger append failed (conv={}, type={}): {}", convId, type, e.getMessage());
        }
    }

    /** 全量事件(按发生顺序),供前端历史重放。 */
    public List<Map<String, Object>> events(String convId) {
        List<Map<String, Object>> out = new ArrayList<>();
        try {
            List<String> raw = redis.opsForList().range(PREFIX + convId, 0, -1);
            if (raw == null) {
                return out;
            }
            for (String s : raw) {
                try {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> m = mapper.readValue(s, Map.class);
                    out.add(m);
                } catch (Exception ignore) {
                }
            }
        } catch (Exception e) {
            log.warn("ledger read failed (conv={}): {}", convId, e.getMessage());
        }
        return out;
    }

    public void delete(String convId) {
        try {
            redis.delete(PREFIX + convId);
        } catch (Exception ignore) {
        }
    }
}