feat(llm): 添加基于OkHttp的SSE流式对话客户端

- 实现SSE流式对话功能,支持实时消息接收
- 集成会话管理功能,包括创建、查询、删除会话
- 提供消息管理功能,支持获取和删除消息
- 实现流式和阻塞两种对话模式
- 添加文件上传支持的对话功能
- 集成重生成回复功能
- 提供完整的数据模型和回调接口
- 包含详细的使用示例和错误处理机制
This commit is contained in:
lixiaolong 2026-03-02 16:05:15 +08:00
parent b36ced91c8
commit 62ea0ccf5b

View File

@ -0,0 +1,629 @@
package com.cmvr.llm.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* SSE 流式对话客户端 OkHttp 实现无额外依赖
* 包含会话管理消息管理流式/阻塞对话
*/
public class SseChatClient {
private static final String DEFAULT_BASE_URL = "https://aiagentplatform.cmft.com/api/proxy/api/v1";
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final ObjectMapper objectMapper = new ObjectMapper();
private final String baseUrl;
private final String apiKey;
private final OkHttpClient httpClient;
// ==================== 数据模型 ====================
/**
* 会话信息
*/
public static class Conversation {
private String conversationId;
private String name;
private long createdAt;
public Conversation(String conversationId, String name, long createdAt) {
this.conversationId = conversationId;
this.name = name;
this.createdAt = createdAt;
}
public String getConversationId() { return conversationId; }
public String getName() { return name; }
public long getCreatedAt() { return createdAt; }
@Override
public String toString() {
return String.format("Conversation{id='%s', name='%s'}", conversationId, name);
}
}
/**
* 消息信息
*/
public static class ChatMessage {
private String messageId;
private String queryId;
private String content;
private String role;
private long createdAt;
public ChatMessage(String messageId, String queryId, String content, String role, long createdAt) {
this.messageId = messageId;
this.queryId = queryId;
this.content = content;
this.role = role;
this.createdAt = createdAt;
}
public String getMessageId() { return messageId; }
public String getQueryId() { return queryId; }
public String getContent() { return content; }
public String getRole() { return role; }
public long getCreatedAt() { return createdAt; }
}
/**
* 流式消息回调接口
*/
public interface StreamCallback {
/**
* 收到消息块时触发实现打字机效果
* @param chunk 文本块
* @param messageId 消息ID
*/
void onMessage(String chunk, String messageId);
/**
* 流式传输完成
* @param fullContent 完整内容
* @param messageId 最终消息ID
*/
default void onComplete(String fullContent, String messageId) {}
/**
* 发生错误
* @param error 错误信息
*/
default void onError(Throwable error) {}
}
// ==================== 构造函数 ====================
public SseChatClient(String apiKey) {
this(DEFAULT_BASE_URL, apiKey);
}
public SseChatClient(String baseUrl, String apiKey) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
}
// ==================== 会话管理 ====================
/**
* 创建新会话
* @param userId 用户ID
* @param name 会话名称可选
* @return 会话对象
*/
public Conversation createConversation(String userId, String name) throws IOException {
Map<String, Object> body = new HashMap<>();
body.put("AppKey", apiKey);
body.put("UserID", userId);
if (name != null && !name.isEmpty()) {
body.put("Name", name);
}
String jsonBody = objectMapper.writeValueAsString(body);
Request request = new Request.Builder()
.url(baseUrl + "/create_conversation")
.header("Apikey", apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(jsonBody, JSON))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("创建会话失败: " + response.code() + " - " + response.body().string());
}
String respJson = response.body().string();
Map<String, Object> respMap = objectMapper.readValue(respJson, Map.class);
// 修正先取 Conversation 对象再取 AppConversationID
Map<String, Object> conversation = (Map<String, Object>) respMap.get("Conversation");
if (conversation == null) {
throw new IOException("返回数据格式错误,缺少 Conversation 字段");
}
String conversationId = (String) conversation.get("AppConversationID");
if (conversationId == null || conversationId.isEmpty()) {
throw new IOException("返回数据格式错误,缺少 AppConversationID");
}
String convName = (String) conversation.getOrDefault("ConversationName", "新会话");
long createdAt = ((Number) conversation.getOrDefault("CreatedAt", System.currentTimeMillis())).longValue();
return new Conversation(conversationId, convName, createdAt);
}
}
/**
* 创建会话自动生成名称
*/
public Conversation createConversation(String userId) throws IOException {
return createConversation(userId, null);
}
/**
* 获取会话列表
*/
public List<Conversation> getConversationList(String userId, Integer limit, Integer offset) throws IOException {
Map<String, Object> body = new HashMap<>();
body.put("AppKey", apiKey);
body.put("UserID", userId);
if (limit != null) body.put("Limit", limit);
if (offset != null) body.put("Offset", offset);
String jsonBody = objectMapper.writeValueAsString(body);
Request request = new Request.Builder()
.url(baseUrl + "/get_conversation_list")
.header("Apikey", apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(jsonBody, JSON))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("获取会话列表失败: " + response.code());
}
String respJson = response.body().string();
Map<String, Object> respMap = objectMapper.readValue(respJson, Map.class);
List<Conversation> conversations = new ArrayList<>();
List<Map<String, Object>> list = (List<Map<String, Object>>) respMap.get("ConversationList");
if (list != null) {
for (Map<String, Object> item : list) {
conversations.add(new Conversation(
(String) item.get("ConversationID"),
(String) item.getOrDefault("Name", "未命名"),
((Number) item.getOrDefault("CreatedAt", 0L)).longValue()
));
}
}
return conversations;
}
}
/**
* 删除会话
*/
public void deleteConversation(String conversationId, String userId) throws IOException {
Map<String, Object> body = new HashMap<>();
body.put("AppKey", apiKey);
body.put("ConversationID", conversationId);
body.put("UserID", userId);
String jsonBody = objectMapper.writeValueAsString(body);
Request request = new Request.Builder()
.url(baseUrl + "/delete_conversation")
.header("Apikey", apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(jsonBody, JSON))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("删除会话失败: " + response.code());
}
}
}
// ==================== 消息管理 ====================
/**
* 获取消息详情
*/
public ChatMessage getMessageInfo(String messageId, String userId) throws IOException {
Map<String, Object> body = new HashMap<>();
body.put("AppKey", apiKey);
body.put("MessageID", messageId);
body.put("UserID", userId);
String jsonBody = objectMapper.writeValueAsString(body);
Request request = new Request.Builder()
.url(baseUrl + "/get_message_info")
.header("Apikey", apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(jsonBody, JSON))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("获取消息失败: " + response.code());
}
String respJson = response.body().string();
Map<String, Object> respMap = objectMapper.readValue(respJson, Map.class);
Map<String, Object> msgInfo = (Map<String, Object>) respMap.get("MessageInfo");
if (msgInfo == null) {
throw new IOException("消息不存在");
}
return new ChatMessage(
(String) msgInfo.get("MessageID"),
(String) msgInfo.get("QueryID"),
(String) msgInfo.get("Content"),
(String) msgInfo.get("Role"),
((Number) msgInfo.getOrDefault("CreatedAt", 0L)).longValue()
);
}
}
/**
* 删除消息
*/
public void deleteMessage(String messageId, String queryId, String userId) throws IOException {
Map<String, Object> body = new HashMap<>();
body.put("AppKey", apiKey);
body.put("MessageID", messageId);
body.put("UserID", userId);
if (queryId != null && !queryId.isEmpty()) {
body.put("QueryID", queryId);
}
String jsonBody = objectMapper.writeValueAsString(body);
Request request = new Request.Builder()
.url(baseUrl + "/delete_message")
.header("Apikey", apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(jsonBody, JSON))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("删除消息失败: " + response.code());
}
}
}
// ==================== 对话功能 ====================
/**
* 流式对话自动创建会话
* @return 创建的会话ID
*/
public String chatStreamingAuto(String query, String userId, StreamCallback callback) throws IOException {
Conversation conv = createConversation(userId, "自动创建-" + System.currentTimeMillis());
String conversationId = conv.getConversationId();
System.out.println("自动创建会话: " + conversationId);
chatStreaming(query, conversationId, userId, null, callback);
return conversationId;
}
/**
* 流式对话使用已有会话
*/
public void chatStreaming(String query, String conversationId, String userId, StreamCallback callback) {
chatStreaming(query, conversationId, userId, null, callback);
}
/**
* 流式对话支持文件上传
*/
public void chatStreaming(String query, String conversationId, String userId,
List<Map<String, Object>> files, StreamCallback callback) {
try {
Map<String, Object> requestBody = buildChatRequestBody(query, conversationId, userId, "streaming", files);
String jsonBody = objectMapper.writeValueAsString(requestBody);
Request request = new Request.Builder()
.url(baseUrl + "/chat_query")
.header("Apikey", apiKey)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.post(RequestBody.create(jsonBody, JSON))
.build();
executeSseRequest(request, callback);
} catch (Exception e) {
callback.onError(e);
}
}
/**
* 阻塞模式对话
*/
public String chatBlocking(String query, String conversationId, String userId) throws IOException {
return chatBlocking(query, conversationId, userId, null);
}
/**
* 阻塞模式对话支持文件
*/
public String chatBlocking(String query, String conversationId, String userId,
List<Map<String, Object>> files) throws IOException {
Map<String, Object> requestBody = buildChatRequestBody(query, conversationId, userId, "blocking", files);
String jsonBody = objectMapper.writeValueAsString(requestBody);
Request request = new Request.Builder()
.url(baseUrl + "/chat_query")
.header("Apikey", apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(jsonBody, JSON))
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("请求失败: " + response.code() + " - " + response.body().string());
}
return response.body().string();
}
}
/**
* 重新生成回复流式
*/
public void regenerateStreaming(String conversationId, String messageId,
String userId, StreamCallback callback) {
try {
Map<String, Object> body = new HashMap<>();
body.put("AppKey", apiKey);
body.put("AppConversationID", conversationId);
body.put("MessageID", messageId);
body.put("UserID", userId);
String jsonBody = objectMapper.writeValueAsString(body);
Request request = new Request.Builder()
.url(baseUrl + "/query_again")
.header("Apikey", apiKey)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.post(RequestBody.create(jsonBody, JSON))
.build();
executeSseRequest(request, callback);
} catch (Exception e) {
callback.onError(e);
}
}
// ==================== 私有辅助方法 ====================
private Map<String, Object> buildChatRequestBody(String query, String conversationId,
String userId, String mode,
List<Map<String, Object>> files) {
Map<String, Object> body = new HashMap<>();
body.put("Query", query);
body.put("AppConversationID", conversationId);
body.put("AppKey", apiKey);
body.put("ResponseMode", mode);
body.put("UserID", userId);
body.put("PubAgentJump", true);
if (files != null && !files.isEmpty()) {
Map<String, Object> queryExtends = new HashMap<>();
queryExtends.put("Files", files);
body.put("QueryExtends", queryExtends);
}
return body;
}
/**
* 执行 SSE 请求核心方法
*/
/**
* 执行 SSE 请求修正版
* 处理实际格式event:text + data:data: {...}
*/
private void executeSseRequest(Request request, StreamCallback callback) throws IOException {
Call call = httpClient.newCall(request);
Response response = call.execute();
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "无错误信息";
throw new IOException("SSE请求失败: " + response.code() + " - " + errorBody);
}
StringBuilder fullContent = new StringBuilder();
String finalMessageId = "";
String currentEvent = "";
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(response.body().byteStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
// 空行表示一个事件结束
if (line.isEmpty()) {
currentEvent = "";
continue;
}
// 解析 event
if (line.startsWith("event:")) {
currentEvent = line.substring(6).trim();
continue;
}
// 解析 data 注意实际是 data:data: {...} 格式
if (line.startsWith("data:")) {
String data = line.substring(5).trim();
// 处理双重 data: 前缀data:data: {...}
if (data.startsWith("data:")) {
data = data.substring(5).trim();
}
// 结束标记
if ("[DONE]".equals(data)) {
callback.onComplete(fullContent.toString(), finalMessageId);
return;
}
try {
Map<String, Object> jsonData = objectMapper.readValue(data, Map.class);
// 提取消息ID
String messageId = extractString(jsonData, "id", "task_id", "message_id");
if (!messageId.isEmpty()) {
finalMessageId = messageId;
}
// 根据事件类型处理
String event = (String) jsonData.getOrDefault("event", currentEvent);
if ("message".equals(event)) {
// 提取回答内容 answer 字段
String answer = extractString(jsonData, "answer", "content", "chunk", "text");
if (!answer.isEmpty()) {
fullContent.append(answer);
callback.onMessage(answer, finalMessageId);
}
} else if ("message_start".equals(event)) {
// 消息开始可忽略或记录
System.out.println("消息开始ID: " + finalMessageId);
} else if ("message_output_start".equals(event)) {
// 输出开始可忽略或记录
System.out.println("输出开始");
} else if ("message_end".equals(event) || "end".equals(event)) {
// 消息结束
callback.onComplete(fullContent.toString(), finalMessageId);
return;
}
// 其他事件类型可扩展...
} catch (Exception e) {
// JSON解析失败打印调试
System.out.println("解析失败,原始数据: " + data);
}
}
}
// 流正常结束没有明确结束标记
callback.onComplete(fullContent.toString(), finalMessageId);
} catch (IOException e) {
callback.onError(e);
throw e;
}
}
/**
* Map 中提取字符串尝试多个 key
*/
private String extractString(Map<String, Object> map, String... keys) {
for (String key : keys) {
Object value = map.get(key);
if (value != null) {
return value.toString();
}
}
return "";
}
/**
* 关闭客户端
*/
public void close() {
httpClient.dispatcher().executorService().shutdown();
httpClient.connectionPool().evictAll();
}
// ==================== 使用示例 ====================
public static void main(String[] args) {
SseChatClient client = new SseChatClient("d6gl6n6llh49on40fqug");
try {
String userId = "user_" + System.currentTimeMillis();
// ========== 示例1标准流程 ==========
System.out.println("=== 步骤1创建会话 ===");
Conversation conv = client.createConversation(userId, "测试会话");
System.out.println("会话创建成功: " + conv.getConversationId());
System.out.println("\n=== 步骤2流式对话 ===");
final StringBuilder contentBuilder = new StringBuilder();
final String[] lastMessageId = {""};
client.chatStreaming(
"给我讲一个关于人工智能的短故事",
conv.getConversationId(),
userId,
new StreamCallback() {
@Override
public void onMessage(String chunk, String messageId) {
System.out.print(chunk);
System.out.flush();
contentBuilder.append(chunk);
if (!messageId.isEmpty()) {
lastMessageId[0] = messageId;
}
}
@Override
public void onComplete(String fullContent, String messageId) {
System.out.println("\n\n[对话完成] 消息ID: " + messageId);
}
@Override
public void onError(Throwable error) {
System.err.println("\n[错误] " + error.getMessage());
}
}
);
String string = contentBuilder.toString();
// Thread.sleep(15000);
// // 等待流式传输完成
// Thread.sleep(15000);
// String string = contentBuilder.toString();
// ========== 示例3获取会话列表 ==========
System.out.println("\n=== 步骤4获取会话列表 ===");
List<Conversation> conversations = client.getConversationList(userId, 10, 0);
System.out.println("共有 " + conversations.size() + " 个会话:");
for (Conversation c : conversations) {
System.out.println(" - " + c);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
client.close();
}
}
}