feat: 落地聊天记录异步持久化基础设施
- 新增 chatlog 模块、AnalyticalDB 公共层与 common-mq Redis Streams 实现 - 建立 Redis 热态、MySQL 热数据、AnalyticalDB 历史查询与同步链路 - 收紧聊天记录幂等、摘要时序与持久化失败语义
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
public enum ChatChannel {
|
||||
|
||||
ADMIN,
|
||||
USER_CENTER,
|
||||
PUBLIC_API
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user