feat: 落地聊天记录异步持久化基础设施

- 新增 chatlog 模块、AnalyticalDB 公共层与 common-mq Redis Streams 实现

- 建立 Redis 热态、MySQL 热数据、AnalyticalDB 历史查询与同步链路

- 收紧聊天记录幂等、摘要时序与持久化失败语义
This commit is contained in:
2026-04-05 11:35:05 +08:00
parent 1ecc28e498
commit 25e80433a5
105 changed files with 8050 additions and 2 deletions

View File

@@ -0,0 +1,74 @@
package tech.easyflow.core.runtime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ChatAssistantAccumulator {
private final StringBuilder content = new StringBuilder();
private final StringBuilder reasoning = new StringBuilder();
private final List<Map<String, Object>> chains = new ArrayList<>();
public void appendContent(String delta) {
if (delta != null && !delta.isEmpty()) {
content.append(delta);
}
}
public void appendReasoning(String delta) {
if (delta != null && !delta.isEmpty()) {
reasoning.append(delta);
}
}
public void appendToolCall(String id, String name, Object arguments) {
Map<String, Object> chain = findToolChain(id, name);
chain.put("status", "TOOL_CALL");
chain.put("result", arguments);
}
public void appendToolResult(String id, String name, Object result) {
Map<String, Object> chain = findToolChain(id, name);
chain.put("status", "TOOL_RESULT");
chain.put("result", result);
}
public String getContent() {
return content.toString();
}
public Map<String, Object> buildPayload() {
Map<String, Object> payload = new LinkedHashMap<>();
List<Map<String, Object>> payloadChains = new ArrayList<>();
if (reasoning.length() > 0) {
Map<String, Object> think = new LinkedHashMap<>();
think.put("reasoning_content", reasoning.toString());
think.put("thinkingStatus", "end");
think.put("thinlCollapse", Boolean.TRUE);
payloadChains.add(think);
}
payloadChains.addAll(chains);
if (!payloadChains.isEmpty()) {
payload.put("chains", payloadChains);
}
return payload;
}
private Map<String, Object> findToolChain(String id, String name) {
for (Map<String, Object> chain : chains) {
if (String.valueOf(chain.get("id")).equals(id)) {
if (name != null && !name.isEmpty()) {
chain.put("name", name);
}
return chain;
}
}
Map<String, Object> chain = new LinkedHashMap<>();
chain.put("id", id);
chain.put("name", name);
chains.add(chain);
return chain;
}
}

View File

@@ -0,0 +1,8 @@
package tech.easyflow.core.runtime;
public enum ChatChannel {
ADMIN,
USER_CENTER,
PUBLIC_API
}

View File

@@ -0,0 +1,138 @@
package tech.easyflow.core.runtime;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ChatRuntimeContext implements Serializable {
private ChatChannel channel;
private BigInteger sessionId;
private BigInteger tenantId;
private BigInteger deptId;
private BigInteger userId;
private String userAccount;
private String userName;
private BigInteger assistantId;
private String assistantCode;
private String assistantName;
private String sessionTitle;
private boolean anonymous;
private List<String> attachments = new ArrayList<>();
private Map<String, Object> ext = new LinkedHashMap<>();
public ChatChannel getChannel() {
return channel;
}
public void setChannel(ChatChannel channel) {
this.channel = channel;
}
public BigInteger getSessionId() {
return sessionId;
}
public void setSessionId(BigInteger sessionId) {
this.sessionId = sessionId;
}
public BigInteger getTenantId() {
return tenantId;
}
public void setTenantId(BigInteger tenantId) {
this.tenantId = tenantId;
}
public BigInteger getDeptId() {
return deptId;
}
public void setDeptId(BigInteger deptId) {
this.deptId = deptId;
}
public BigInteger getUserId() {
return userId;
}
public void setUserId(BigInteger userId) {
this.userId = userId;
}
public String getUserAccount() {
return userAccount;
}
public void setUserAccount(String userAccount) {
this.userAccount = userAccount;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public BigInteger getAssistantId() {
return assistantId;
}
public void setAssistantId(BigInteger assistantId) {
this.assistantId = assistantId;
}
public String getAssistantCode() {
return assistantCode;
}
public void setAssistantCode(String assistantCode) {
this.assistantCode = assistantCode;
}
public String getAssistantName() {
return assistantName;
}
public void setAssistantName(String assistantName) {
this.assistantName = assistantName;
}
public String getSessionTitle() {
return sessionTitle;
}
public void setSessionTitle(String sessionTitle) {
this.sessionTitle = sessionTitle;
}
public boolean isAnonymous() {
return anonymous;
}
public void setAnonymous(boolean anonymous) {
this.anonymous = anonymous;
}
public List<String> getAttachments() {
return attachments;
}
public void setAttachments(List<String> attachments) {
this.attachments = attachments == null ? new ArrayList<>() : attachments;
}
public Map<String, Object> getExt() {
return ext;
}
public void setExt(Map<String, Object> ext) {
this.ext = ext == null ? new LinkedHashMap<>() : ext;
}
}

View File

@@ -0,0 +1,29 @@
package tech.easyflow.core.runtime;
import java.util.Collections;
import java.util.List;
public interface ChatRuntimeListener {
default void onSessionPrepared(ChatRuntimeContext context) {
}
default void onUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) {
}
default void onAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message) {
}
default void onAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) {
}
default void onChatFailed(ChatRuntimeContext context, Throwable throwable) {
}
default void onChatCompleted(ChatRuntimeContext context) {
}
default List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,20 @@
package tech.easyflow.core.runtime;
import java.util.List;
public interface ChatRuntimeManager {
void prepareSession(ChatRuntimeContext context);
void recordUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message);
void recordAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message);
void recordAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message);
void recordFailure(ChatRuntimeContext context, Throwable throwable);
void recordCompleted(ChatRuntimeContext context);
List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit);
}

View File

@@ -0,0 +1,83 @@
package tech.easyflow.core.runtime;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
public class ChatRuntimeMessage implements Serializable {
private BigInteger messageId;
private String role;
private String contentType = "TEXT";
private String contentText;
private Map<String, Object> contentPayload = new LinkedHashMap<>();
private Date createdAt = new Date();
private BigInteger senderId;
private String senderName;
public BigInteger getMessageId() {
return messageId;
}
public void setMessageId(BigInteger messageId) {
this.messageId = messageId;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public String getContentText() {
return contentText;
}
public void setContentText(String contentText) {
this.contentText = contentText;
}
public Map<String, Object> getContentPayload() {
return contentPayload;
}
public void setContentPayload(Map<String, Object> contentPayload) {
this.contentPayload = contentPayload == null ? new LinkedHashMap<>() : contentPayload;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
public BigInteger getSenderId() {
return senderId;
}
public void setSenderId(BigInteger senderId) {
this.senderId = senderId;
}
public String getSenderName() {
return senderName;
}
public void setSenderName(String senderName) {
this.senderName = senderName;
}
}

View File

@@ -0,0 +1,93 @@
package tech.easyflow.core.runtime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.util.Collections;
import java.util.List;
@Component
public class CompositeChatRuntimeManager implements ChatRuntimeManager {
private static final Logger log = LoggerFactory.getLogger(CompositeChatRuntimeManager.class);
private final ObjectProvider<ChatRuntimeListener> listenerProvider;
public CompositeChatRuntimeManager(ObjectProvider<ChatRuntimeListener> listenerProvider) {
this.listenerProvider = listenerProvider;
}
@Override
public void prepareSession(ChatRuntimeContext context) {
forEach(listener -> listener.onSessionPrepared(context), "prepareSession", context);
}
@Override
public void recordUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) {
forEach(listener -> listener.onUserMessage(context, message), "recordUserMessage", context);
}
@Override
public void recordAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message) {
forEach(listener -> listener.onAssistantDelta(context, message), "recordAssistantDelta", context);
}
@Override
public void recordAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) {
forEach(listener -> listener.onAssistantCompleted(context, message), "recordAssistantCompleted", context);
}
@Override
public void recordFailure(ChatRuntimeContext context, Throwable throwable) {
forEach(listener -> listener.onChatFailed(context, throwable), "recordFailure", context);
}
@Override
public void recordCompleted(ChatRuntimeContext context) {
forEach(listener -> listener.onChatCompleted(context), "recordCompleted", context);
}
@Override
public List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
for (ChatRuntimeListener listener : listenerProvider.orderedStream().toList()) {
try {
List<ChatRuntimeMessage> messages = listener.loadMessages(context, limit);
if (messages != null && !messages.isEmpty()) {
return messages;
}
} catch (Exception ex) {
log.warn("chat runtime loadMessages failed, channel={}, sessionId={}, listener={}",
context == null || context.getChannel() == null ? null : context.getChannel().name(),
context == null ? null : context.getSessionId(),
listener.getClass().getName(),
ex);
}
}
return Collections.emptyList();
}
private void forEach(ListenerConsumer consumer, String action, ChatRuntimeContext context) {
for (ChatRuntimeListener listener : listenerProvider.orderedStream().toList()) {
try {
consumer.accept(listener);
} catch (BusinessException ex) {
throw ex;
} catch (Exception ex) {
log.warn("chat runtime {} failed, channel={}, sessionId={}, listener={}",
action,
context == null || context.getChannel() == null ? null : context.getChannel().name(),
context == null ? null : context.getSessionId(),
listener.getClass().getName(),
ex);
}
}
}
@FunctionalInterface
private interface ListenerConsumer {
void accept(ChatRuntimeListener listener);
}
}