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 的确定性路径 * (表单弹出/澄清/写提议/确认结果),修补「确定性路径不进对话记忆」的记忆空洞;前端历史从账本重放。 * *

键: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 data) { if (convId == null || convId.isBlank()) { return; } try { Map 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> events(String convId) { List> out = new ArrayList<>(); try { List raw = redis.opsForList().range(PREFIX + convId, 0, -1); if (raw == null) { return out; } for (String s : raw) { try { @SuppressWarnings("unchecked") Map 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) { } } }