初始化

This commit is contained in:
2026-02-22 18:56:10 +08:00
commit 26677972a6
3112 changed files with 255972 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-commons</artifactId>
<version>${revision}</version>
</parent>
<name>easyflow-common-chat-protocol</name>
<artifactId>easyflow-common-chat-protocol</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,12 @@
package tech.easyflow.core.chat.protocol;
public enum ChatDomain {
LLM,
TOOL,
SYSTEM,
BUSINESS,
WORKFLOW,
INTERACTION,
DEBUG
}

View File

@@ -0,0 +1,89 @@
package tech.easyflow.core.chat.protocol;
public class ChatEnvelope<T> {
private String protocol = "easyflow-chat";
private String version = "1.1";
private ChatDomain domain;
private ChatType type;
private String conversationId;
private String messageId;
private Integer index;
private T payload;
private Object meta;
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public ChatDomain getDomain() {
return domain;
}
public void setDomain(ChatDomain domain) {
this.domain = domain;
}
public ChatType getType() {
return type;
}
public void setType(ChatType type) {
this.type = type;
}
public String getConversationId() {
return conversationId;
}
public void setConversationId(String conversationId) {
this.conversationId = conversationId;
}
public String getMessageId() {
return messageId;
}
public void setMessageId(String messageId) {
this.messageId = messageId;
}
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public T getPayload() {
return payload;
}
public void setPayload(T payload) {
this.payload = payload;
}
public Object getMeta() {
return meta;
}
public void setMeta(Object meta) {
this.meta = meta;
}
}

View File

@@ -0,0 +1,53 @@
package tech.easyflow.core.chat.protocol;
import tech.easyflow.core.chat.protocol.payload.MessageDeltaPayload;
import tech.easyflow.core.chat.protocol.payload.ThinkingPayload;
public final class ChatEventFactory {
private ChatEventFactory() {
}
public static ChatEnvelope<ThinkingPayload> thinking(String conversationId, String content) {
ChatEnvelope<ThinkingPayload> e = base(conversationId);
e.setDomain(ChatDomain.LLM);
e.setType(ChatType.THINKING);
ThinkingPayload p = new ThinkingPayload();
p.setContent(content);
// p.setVisibility("hidden");
e.setPayload(p);
return e;
}
public static ChatEnvelope<MessageDeltaPayload> messageDelta(
String conversationId,
String delta
) {
ChatEnvelope<MessageDeltaPayload> e = base(conversationId);
e.setDomain(ChatDomain.LLM);
e.setType(ChatType.MESSAGE);
MessageDeltaPayload p = new MessageDeltaPayload();
p.setDelta(delta);
e.setPayload(p);
return e;
}
public static ChatEnvelope<Void> done(String conversationId) {
ChatEnvelope<Void> e = base(conversationId);
e.setDomain(ChatDomain.SYSTEM);
e.setType(ChatType.DONE);
e.setPayload(null);
return e;
}
private static <T> ChatEnvelope<T> base(String conversationId) {
ChatEnvelope<T> e = new ChatEnvelope<>();
e.setConversationId(conversationId);
return e;
}
}

View File

@@ -0,0 +1,13 @@
package tech.easyflow.core.chat.protocol;
public enum ChatType {
THINKING,
MESSAGE,
TOOL_CALL,
TOOL_RESULT,
STATUS,
ERROR,
FORM_REQUEST,
FORM_CANCEL,
DONE
}

View File

@@ -0,0 +1,53 @@
package tech.easyflow.core.chat.protocol;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* 对话消息角色枚举
*/
public enum MessageRole {
/**
* 用户角色:用户发送的消息
*/
USER("user"),
/**
* 助手角色AI/机器人返回的消息
*/
ASSISTANT("assistant"),
/**
* 系统角色:系统级提示、日志、状态消息
*/
SYSTEM("system"),
/**
* 工具角色:工具自动发送/返回的消息
*/
TOOL("tool");
private final String value;
/**
* 私有构造方法,绑定枚举常量与对应值
* @param value 数据库存储/协议传输的字符串值
*/
MessageRole(String value) {
this.value = value;
}
/**
* 获取枚举对应的字符串值(用于数据库持久化、协议传输)
* @JsonValue 注解Jackson 序列化时,自动返回该值(避免序列化出枚举名称)
* @return 小写字符串(如 "user"、"assistant"
*/
@JsonValue
public String getValue() {
return value;
}
@Override
public String toString() {
return this.value;
}
}

View File

@@ -0,0 +1,40 @@
package tech.easyflow.core.chat.protocol.payload;
public class ErrorPayload {
private String code;
private String message;
private Boolean retryable;
private Object detail;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Boolean getRetryable() {
return retryable;
}
public void setRetryable(Boolean retryable) {
this.retryable = retryable;
}
public Object getDetail() {
return detail;
}
public void setDetail(Object detail) {
this.detail = detail;
}
}

View File

@@ -0,0 +1,13 @@
package tech.easyflow.core.chat.protocol.payload;
public class FormCancelPayload {
private String formId;
public String getFormId() {
return formId;
}
public void setFormId(String formId) {
this.formId = formId;
}
}

View File

@@ -0,0 +1,51 @@
package tech.easyflow.core.chat.protocol.payload;
import java.util.Map;
public class FormRequestPayload {
private String formId;
private String title;
private String description;
private Map<String, Object> schema; // JSON Schema
private Map<String, String> ui;
public String getFormId() {
return formId;
}
public void setFormId(String formId) {
this.formId = formId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Map<String, Object> getSchema() {
return schema;
}
public void setSchema(Map<String, Object> schema) {
this.schema = schema;
}
public Map<String, String> getUi() {
return ui;
}
public void setUi(Map<String, String> ui) {
this.ui = ui;
}
}

View File

@@ -0,0 +1,14 @@
package tech.easyflow.core.chat.protocol.payload;
public class MessageDeltaPayload {
private String delta;
public String getDelta() {
return delta;
}
public void setDelta(String delta) {
this.delta = delta;
}
}

View File

@@ -0,0 +1,13 @@
package tech.easyflow.core.chat.protocol.payload;
public class MessageFullPayload {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@@ -0,0 +1,22 @@
package tech.easyflow.core.chat.protocol.payload;
public class StatusPayload {
private String state;
private String reason;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
}

View File

@@ -0,0 +1,15 @@
package tech.easyflow.core.chat.protocol.payload;
public class ThinkingPayload {
private String content;
// private String visibility; // hidden | visible
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@@ -0,0 +1,34 @@
package tech.easyflow.core.chat.protocol.payload;
import java.util.Map;
public class ToolCallPayload {
private String toolCallId;
private String name;
private Map<String, Object> arguments;
public String getToolCallId() {
return toolCallId;
}
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Map<String, Object> getArguments() {
return arguments;
}
public void setArguments(Map<String, Object> arguments) {
this.arguments = arguments;
}
}

View File

@@ -0,0 +1,31 @@
package tech.easyflow.core.chat.protocol.payload;
public class ToolResultPayload {
private String toolCallId;
private String status; // success | error
private Object result;
public String getToolCallId() {
return toolCallId;
}
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
}

View File

@@ -0,0 +1,86 @@
package tech.easyflow.core.chat.protocol.sse;
import com.alibaba.fastjson.JSON;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.common.util.SpringContextUtil;
import tech.easyflow.core.chat.protocol.ChatEnvelope;
import java.io.IOException;
import java.time.Duration;
public class ChatSseEmitter {
private static final long DEFAULT_TIMEOUT = Duration.ofMinutes(5).toMillis();
private final SseEmitter emitter;
public ChatSseEmitter() {
this(DEFAULT_TIMEOUT);
}
public ChatSseEmitter(long timeoutMillis) {
this.emitter = new SseEmitter(timeoutMillis);
}
public SseEmitter getEmitter() {
return emitter;
}
/** 发送普通 ChatEnvelopeevent: message */
public void send(ChatEnvelope<?> envelope) {
send("message", envelope);
}
/** 发送 error 事件 */
public void sendError(ChatEnvelope<?> envelope) {
send("error", envelope);
}
/** 发送 done 事件并关闭 */
public void sendDone(ChatEnvelope<?> envelope) {
send("done", envelope);
complete();
}
/** 🔥 新增:发送并立即关闭 */
public void sendAndClose(ChatEnvelope<?> envelope) {
send("message", envelope);
ThreadPoolTaskExecutor threadPoolTaskExecutor = SpringContextUtil.getBean("sseThreadPool");
threadPoolTaskExecutor.execute(() -> {
try {
Thread.sleep(500);
complete();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
/** 通知前端保存该消息 */
public void sendMessageNeedSave(ChatEnvelope<?> envelope) {
send("needSaveMessage", envelope);
}
/** SSE 底层发送 */
private void send(String event, ChatEnvelope<?> envelope) {
try {
String json = JSON.toJSONString(envelope);
emitter.send(
SseEmitter.event()
.name(event)
.data(json)
);
} catch (IOException e) {
emitter.completeWithError(e);
}
}
public void complete() {
emitter.complete();
}
public void completeWithError(Throwable ex) {
emitter.completeWithError(ex);
}
}

View File

@@ -0,0 +1,97 @@
package tech.easyflow.core.chat.protocol.sse;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.core.chat.protocol.ChatDomain;
import tech.easyflow.core.chat.protocol.ChatEnvelope;
import tech.easyflow.core.chat.protocol.ChatType;
import tech.easyflow.core.chat.protocol.payload.ErrorPayload;
public class ChatSseUtil {
private static final long DEFAULT_TIMEOUT = 1000 * 60 * 2; // 2分钟
/**
* 快速发送一次系统错误消息,并立即关闭 SSE
*
* @param conversationId 会话ID
* @param message 错误提示信息
* @param code 错误码,可选
* @return SseEmitter
*/
public static SseEmitter sendSystemError(String conversationId, String message, String code) {
ChatSseEmitter emitter = new ChatSseEmitter(DEFAULT_TIMEOUT);
ChatEnvelope<ErrorPayload> envelope = new ChatEnvelope<>();
// envelope.setProtocol("easyflow-ai-chat");
// envelope.setVersion("1.1");
envelope.setDomain(ChatDomain.SYSTEM);
envelope.setType(ChatType.ERROR);
envelope.setConversationId(conversationId);
ErrorPayload payload = new ErrorPayload();
payload.setMessage(message);
payload.setCode(code != null ? code : "SYSTEM_ERROR");
payload.setRetryable(false);
envelope.setPayload(payload);
// 发送并立即关闭
emitter.sendAndClose(envelope);
return emitter.getEmitter();
}
/**
* 快速发送一次系统错误消息(默认错误码)
*/
public static SseEmitter sendSystemError(String conversationId, String message) {
return sendSystemError(conversationId, message, null);
}
/**
* 快速发送任意 ChatEnvelope 消息并立即关闭 SSE
*
* @param conversationId 会话ID
* @param domain 消息 domain
* @param type 消息 type
* @param payload 消息 payload
* @param meta 可选 meta 信息
* @return SseEmitter 已经发送并关闭
*/
public static <T> SseEmitter sendAndClose(
String conversationId,
ChatDomain domain,
ChatType type,
T payload,
Object meta
) {
ChatSseEmitter emitter = new ChatSseEmitter(DEFAULT_TIMEOUT);
ChatEnvelope<T> envelope = new ChatEnvelope<>();
// envelope.setProtocol("easyflow-ai-chat");
// envelope.setVersion("1.1");
envelope.setConversationId(conversationId);
envelope.setDomain(domain);
envelope.setType(type);
envelope.setPayload(payload);
envelope.setMeta(meta);
// 发送并立即关闭
emitter.sendAndClose(envelope);
return emitter.getEmitter();
}
/**
* 快速发送任意 ChatEnvelope 消息,无 meta
*/
public static <T> SseEmitter sendAndClose(
String conversationId,
ChatDomain domain,
ChatType type,
T payload
) {
return sendAndClose(conversationId, domain, type, payload, null);
}
}

View File

@@ -0,0 +1,60 @@
package tech.easyflow.core.chat.protocol.websocket;
import com.alibaba.fastjson.JSON;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import tech.easyflow.core.chat.protocol.ChatEnvelope;
import java.io.IOException;
public class ChatWebSocketSender {
private final WebSocketSession session;
public ChatWebSocketSender(WebSocketSession session) {
this.session = session;
}
/**
* 发送 ChatEnvelope 消息
*/
public void send(ChatEnvelope<?> envelope) throws IOException {
checkOpen();
String json = JSON.toJSONString(envelope);
session.sendMessage(new TextMessage(json));
}
/**
* 发送 error 消息
*/
public void sendError(ChatEnvelope<?> envelope) throws IOException {
checkOpen();
send(envelope); // 前端可根据 envelope.type 判断 error
}
/**
* 发送 done 并关闭 WebSocket
*/
public void sendDone(ChatEnvelope<?> envelope) throws IOException {
send(envelope);
close();
}
/**
* 关闭 WebSocket
*/
public void close() throws IOException {
if (session.isOpen()) {
session.close();
}
}
/**
* 检查 session 是否仍然打开
*/
private void checkOpen() throws IOException {
if (!session.isOpen()) {
throw new IOException("WebSocket session is closed");
}
}
}