1. 创建Appbuilder应用
AppBuilder的快速入门:快速入门 - 千帆AppBuilder-产品文档
SDK调用AppBuilder的组件:应用调用 - 千帆AppBuilder-产品文档
然后获取到 AppId 和 Token 然后配置到 application.yml 中
baiduQianFan:
appBuilder:
appId: xxx
token: xxxxxxx
2. 添加依赖
<dependency>
<groupId>com.baidubce</groupId>
<artifactId>appbuilder</artifactId>
<version>0.9.4</version>
</dependency>
<dependency>
<groupId>org.yeauty</groupId>
<artifactId>netty-websocket-spring-boot-starter</artifactId>
<version>0.9.5</version>
</dependency>
3. 新建WebSocket服务
package org.jeecg.modules.system.myWebsocket;
import com.alibaba.fastjson.JSON;
import com.baidubce.appbuilder.base.exception.AppBuilderServerException;
import com.baidubce.appbuilder.model.appbuilderclient.AppBuilderClientIterator;
import com.baidubce.appbuilder.model.appbuilderclient.AppBuilderClientResult;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.modules.system.constant.AIChatMessageType;
import org.jeecg.modules.system.entity.AppBuilderNewClient;
import org.jeecg.modules.system.entity.XxstudyAiChat;
import org.jeecg.modules.system.entity.XxstudyAiChatMessage;
import org.jeecg.modules.system.mapper.XxstudyAiChatMapper;
import org.jeecg.modules.system.mapper.XxstudyAiChatMessageMapper;
import org.jeecg.modules.system.util.ImageFileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.yeauty.annotation.*;
import org.yeauty.pojo.Session;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*智慧问答WebSocket服务
*/
@Slf4j
@ServerEndpoint(port = "9097", path = "/ai/chat/{chatId}/{userId}", maxFramePayloadLength = "6553600", allIdleTimeSeconds = "300")
public class WebSocketAiChatServer {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, List<String>> userIdPool = new ConcurrentHashMap<>();
private static final CopyOnWriteArraySet<WebSocketAiChatServer> webSocketSet = new CopyOnWriteArraySet<>();
private Session session;
private String userId;
private String chatId;
private final Map<Session, String> sessionToUserIdMap = new ConcurrentHashMap<>();
@Value("${baiduQianFan.appBuilder.appId}")
private String appBuilderAppId;
@Value("${baiduQianFan.appBuilder.token}")
private String appBuilderToken;
@Autowired
private XxstudyAiChatMessageMapper xxstudyAiChatMessageMapper;
@Autowired
private XxstudyAiChatMapper xxstudyAiChatMapper;
@Autowired
private RedisUtil redisUtil;
private final String AI_AVATAR = "https://www.ddnn.vip/static/img/logo.png";
private final String NO_RESULT = "我暂时无法回答这个问题";
@OnOpen
public void onOpen(Session session, @PathVariable String chatId, @PathVariable String userId) {
webSocketSet.add(this);
this.userId = userId;
this.chatId = chatId;
this.session = session;
String sessionKey = chatId + "||" + userId;
sessionPool.put(sessionKey, session);
sessionToUserIdMap.put(session, sessionKey);
System.setProperty("APPBUILDER_TOKEN", appBuilderToken);
userIdPool.computeIfAbsent(chatId, k -> new ArrayList<>()).add(sessionKey);
log.info("【websocketAIChat消息】有新的连接,连接用户为:{},总数为:{}", userId, webSocketSet.size());
}
@OnMessage
public void onMessage(Session session, String message) {
String userId = sessionToUserIdMap.getOrDefault(session, "");
if ("ping".equals(message) || message.equals("heartbeat")) {
sendOneMessage(userId, "pong");
return;
}
processMessage(message);
}
private void processMessage(String message) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
log.info("处理AI聊天消息=====" + message);
try {
executorService.submit(() -> {
try {
XxstudyAiChatMessage xxstudyAiChatMessage = JSON.parseObject(message, XxstudyAiChatMessage.class);
String sceneId = xxstudyAiChatMessage.getSceneId(), chatId = xxstudyAiChatMessage.getChatId(), userId;
Integer type = xxstudyAiChatMessage.getType();
if (StringUtils.isNotBlank(sceneId)) {
String[] scenes = redisUtil.get(sceneId).toString().split("_");
chatId = scenes[0];
userId = scenes[1];
xxstudyAiChatMessage.setChatId(chatId);
xxstudyAiChatMessage.setUserId(userId);
xxstudyAiChatMessage.setUserName(scenes[2]);
xxstudyAiChatMessage.setAvatar(scenes[3]);
}
//根据会话id查询此会话中所有消息
QueryWrapper<XxstudyAiChatMessage> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("chat_id", chatId);
queryWrapper.orderByDesc("create_time");
List<XxstudyAiChatMessage> xxstudyAiChatMessageList = xxstudyAiChatMessageMapper.selectList(queryWrapper);
if (xxstudyAiChatMessageList.isEmpty()) {
AppBuilderNewClient appBuilderClient = new AppBuilderNewClient(appBuilderAppId);
// 生成会话id(需保存下来,可以传入下次聊天中进行对话续写)
String conversationId = appBuilderClient.createConversation();
XxstudyAiChat xxstudyAiChat = xxstudyAiChatMapper.selectById(chatId);
xxstudyAiChat.setSessionId(conversationId);
xxstudyAiChatMapper.updateById(xxstudyAiChat);
}
if (type == AIChatMessageType.TEXT.getValue()) {
//保存消息
xxstudyAiChatMessage.setRole(1);
xxstudyAiChatMessage.setCreateTime(new Date());
xxstudyAiChatMessageMapper.insert(xxstudyAiChatMessage);
xxstudyAiChatMessageList = xxstudyAiChatMessageMapper.selectList(queryWrapper);
//用户发送文字
handleTextMessages(xxstudyAiChatMessageList, chatId);
}else if(type == AIChatMessageType.IMAGE.getValue()) {
//用户发送图片
handleImgMessages(xxstudyAiChatMessage);
}else {
//用户发送视频
}
}catch (Exception e) {
e.printStackTrace();
}
});
} finally {
executorService.shutdown();
}
}
/**
* 处理用户发送的文本消息
* @param xxstudyAiChatMessageList 消息列表
* @param chatId 会话id
* @throws IOException
*/
private void handleTextMessages(List<XxstudyAiChatMessage> xxstudyAiChatMessageList, String chatId) throws Exception {
String userIdKey = chatId + "||" + userId;
XxstudyAiChatMessage message = new XxstudyAiChatMessage();
AppBuilderNewClient appBuilderClient = new AppBuilderNewClient(appBuilderAppId);
XxstudyAiChat xxstudyAiChat = xxstudyAiChatMapper.selectById(chatId);
StringBuilder result = new StringBuilder();
message.setChatId(chatId);
message.setUserName("AI");
message.setType(1);
message.setRole(2);
log.info("【AIChat文本消息】------------");
// 多轮对话 流式请求
XxstudyAiChatMessage finalMessage = message;
AppBuilderClientIterator iterator = appBuilderClient.run(xxstudyAiChatMessageList.get(0).getMessage(), xxstudyAiChat.getSessionId(), new String[]{}, true);
while(iterator.hasNext())
{
AppBuilderClientResult response = iterator.next();
result.append(response.getAnswer());
finalMessage.setMessage(response.getAnswer());
finalMessage.setIsEnd(!iterator.hasNext());
finalMessage.setAvatar(AI_AVATAR);
if (!iterator.hasNext() && !StringUtils.isNotBlank(result.toString().trim())) {
result.append(NO_RESULT);
finalMessage.setMessage(NO_RESULT);
}
sendOneMessage(userIdKey, serializeToJson(finalMessage));
log.info(response.getAnswer());
}
message.setMessage(ImageFileUtils.getMarkDownImageContent(result.toString()));
message.setAvatar(AI_AVATAR);
message.setCreateTime(new Date());
xxstudyAiChatMessageMapper.insert(message);
}
/**
* 处理用户发送的图片消息
* @param xxstudyAiChatMessage
* @throws IOException
*/
private void handleImgMessages(XxstudyAiChatMessage xxstudyAiChatMessage) throws IOException, AppBuilderServerException {
XxstudyAiChatMessage message = new XxstudyAiChatMessage();
AppBuilderNewClient appBuilderClient = new AppBuilderNewClient(appBuilderAppId);
String chatId = xxstudyAiChatMessage.getChatId(), userId = xxstudyAiChatMessage.getUserId();
StringBuilder result = new StringBuilder();
XxstudyAiChat xxstudyAiChat = xxstudyAiChatMapper.selectById(xxstudyAiChatMessage.getChatId());
xxstudyAiChatMessage.setRole(1);
String userIdKey = chatId + "||" + userId;
message.setChatId(chatId);
message.setUserName("AI");
message.setType(1);
message.setRole(2);
XxstudyAiChatMessage finalMessage = message;
//如果发送过来的是图片则需要上传到会话中
String fileId = StringUtils.isNotBlank(xxstudyAiChatMessage.getSceneId()) ? appBuilderClient.uploadLocalFile(xxstudyAiChat.getSessionId(), xxstudyAiChatMessage.getMessage()) : "";
AppBuilderClientIterator iterator = appBuilderClient.run(StringUtils.isNotBlank(xxstudyAiChatMessage.getSceneId()) ? " " : xxstudyAiChatMessage.getMessage(), xxstudyAiChat.getSessionId(), new String[]{fileId}, true);
xxstudyAiChatMessage.setFileId(fileId);
xxstudyAiChatMessage.setMessage(" + ")");
//保存消息
xxstudyAiChatMessageMapper.insert(xxstudyAiChatMessage);
if (StringUtils.isNotBlank(xxstudyAiChatMessage.getSceneId())) {
xxstudyAiChatMessage.setIsEnd(true);
sendOneMessage(userIdKey, serializeToJson(xxstudyAiChatMessage));
}
while(iterator.hasNext())
{
AppBuilderClientResult response = iterator.next();
result.append(response.getAnswer());
finalMessage.setMessage(response.getAnswer());
finalMessage.setIsEnd(!iterator.hasNext());
finalMessage.setAvatar(AI_AVATAR);
finalMessage.setFileId(fileId);
if (!iterator.hasNext() && !StringUtils.isNotBlank(result.toString().trim())) {
result.append(NO_RESULT);
finalMessage.setMessage(NO_RESULT);
}
sendOneMessage(userIdKey, serializeToJson(finalMessage));
log.info(response.getAnswer());
}
log.info("【AIChat文本消息】---图生文返回");
log.info(result.toString());
message.setMessage(result.toString());
message.setAvatar(AI_AVATAR);
message.setCreateTime(new Date());
xxstudyAiChatMessageMapper.insert(message);
}
private String serializeToJson(XxstudyAiChatMessage message) {
try {
return OBJECT_MAPPER.writeValueAsString(message);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@OnClose
public void onClose(Session session) {
// 从 set 中移除当前 WebSocket 对象
webSocketSet.remove(this);
// 移除 sessionPool 中的记录
String sessionKey = sessionToUserIdMap.get(session);
if (sessionKey != null) {
sessionPool.remove(sessionKey);
}
// 更新 userIdPool 中的信息
List<String> userList = userIdPool.get(chatId);
if (userList != null) {
userList.remove(sessionKey);
if (userList.isEmpty()) {
userIdPool.remove(chatId);
}
}
log.info("【websocketAIChat消息】连接断开,断连接用户为:{},剩余总数为:{}", userId, webSocketSet.size());
}
@OnError
public void onError(Session session, Throwable cause) {
// 获取用户 ID 或其他相关信息
String sessionKey = sessionToUserIdMap.get(session);
// 记录详细的错误信息
log.error("websocketAIChat 错误, 用户: {}, 会话 ID: {}, 原因: {}",
sessionKey,
session,
cause.getMessage(),
cause);
}
public void sendOneMessage(String userId, String message) {
log.info("【WebSocketAiChat消息】进入发送消息流程....");
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
session.sendText(message);
}
}
}
因原有Appbuilder SDK自带的文件上传并不符合需求,所以重写了一个如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.jeecg.modules.system.entity;
import com.baidubce.appbuilder.base.component.Component;
import com.baidubce.appbuilder.base.exception.AppBuilderServerException;
import com.baidubce.appbuilder.base.utils.http.HttpResponse;
import com.baidubce.appbuilder.base.utils.json.JsonUtils;
import com.baidubce.appbuilder.model.appbuilderclient.AppBuilderClientIterator;
import com.baidubce.appbuilder.model.appbuilderclient.AppBuilderClientResponse;
import com.baidubce.appbuilder.model.appbuilderclient.AppBuilderClientRunRequest;
import com.baidubce.appbuilder.model.appbuilderclient.ConversationResponse;
import com.baidubce.appbuilder.model.appbuilderclient.FileUploadResponse;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.hc.client5.http.entity.mime.HttpMultipartMode;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.jeecg.modules.system.util.ImageFileUtils;
public class AppBuilderNewClient extends Component {
public String appID;
public AppBuilderNewClient(String appID) {
this.appID = appID;
}
public AppBuilderNewClient(String appID, String secretKey) {
super(secretKey);
this.appID = appID;
}
public AppBuilderNewClient(String appID, String secretKey, String gateway) {
super(secretKey, gateway);
this.appID = appID;
}
public String createConversation() throws IOException, AppBuilderServerException {
return this.innerCreateConversation();
}
private String innerCreateConversation() throws IOException, AppBuilderServerException {
String url = "/app/conversation";
if (this.appID != null && !this.appID.isEmpty()) {
Map<String, String> requestBody = new HashMap();
requestBody.put("app_id", this.appID);
String jsonBody = JsonUtils.serialize(requestBody);
ClassicHttpRequest postRequest = this.httpClient.createPostRequestV2(url, new StringEntity(jsonBody, StandardCharsets.UTF_8));
postRequest.setHeader("Content-Type", "application/json");
HttpResponse<ConversationResponse> response = this.httpClient.execute(postRequest, ConversationResponse.class);
ConversationResponse respBody = (ConversationResponse)response.getBody();
return respBody.getConversationId();
} else {
throw new RuntimeException("Param 'appID' is required!");
}
}
public String uploadLocalFile(String conversationId, String fileUrl) throws IOException, AppBuilderServerException {
return this.innerUploadLocalFileByUrl(conversationId, fileUrl);
}
private String innerUploadLocalFile(String conversationId, String filePath) throws IOException, AppBuilderServerException {
String url = "/app/conversation/file/upload";
if (this.appID != null && !this.appID.isEmpty()) {
MultipartEntityBuilder builder = MultipartEntityBuilder.create().setMode(HttpMultipartMode.LEGACY).setCharset(StandardCharsets.UTF_8);
builder.addBinaryBody("file", new File(filePath));
builder.addTextBody("app_id", this.appID);
builder.addTextBody("conversation_id", conversationId);
builder.addTextBody("scenario", "assistant");
ClassicHttpRequest postRequest = this.httpClient.createPostRequestV2(url, builder.build());
HttpResponse<FileUploadResponse> response = this.httpClient.execute(postRequest, FileUploadResponse.class);
FileUploadResponse respBody = (FileUploadResponse)response.getBody();
return respBody.getFileId();
} else {
throw new RuntimeException("Param 'appID' is required!");
}
}
private String innerUploadLocalFileByUrl(String conversationId, String fileUrl) throws IOException, AppBuilderServerException {
String url = "/app/conversation/file/upload";
if (this.appID != null && !this.appID.isEmpty()) {
MultipartEntityBuilder builder = MultipartEntityBuilder.create().setMode(HttpMultipartMode.LEGACY).setCharset(StandardCharsets.UTF_8);
builder.addBinaryBody("file", ImageFileUtils.getFile(fileUrl));
builder.addTextBody("app_id", this.appID);
builder.addTextBody("conversation_id", conversationId);
builder.addTextBody("scenario", "assistant");
ClassicHttpRequest postRequest = this.httpClient.createPostRequestV2(url, builder.build());
HttpResponse<FileUploadResponse> response = this.httpClient.execute(postRequest, FileUploadResponse.class);
FileUploadResponse respBody = (FileUploadResponse)response.getBody();
return respBody.getFileId();
} else {
throw new RuntimeException("Param 'appID' is required!");
}
}
public AppBuilderClientIterator run(String query, String conversationId, String[] fileIds, boolean stream) throws IOException, AppBuilderServerException {
String url = "/app/conversation/runs";
if (this.appID != null && !this.appID.isEmpty()) {
Map<String, Object> requestBody = new HashMap();
requestBody.put("app_id", this.appID);
requestBody.put("query", query);
requestBody.put("conversation_id", conversationId);
requestBody.put("file_ids", fileIds);
requestBody.put("stream", stream);
String jsonBody = JsonUtils.serialize(requestBody);
ClassicHttpRequest postRequest = this.httpClient.createPostRequestV2(url, new StringEntity(jsonBody, StandardCharsets.UTF_8));
postRequest.setHeader("Content-Type", "application/json");
HttpResponse<Iterator<AppBuilderClientResponse>> response = this.httpClient.executeSSE(postRequest, AppBuilderClientResponse.class);
return new AppBuilderClientIterator((Iterator)response.getBody());
} else {
throw new RuntimeException("Param 'appID' is required!");
}
}
public AppBuilderClientIterator run(AppBuilderClientRunRequest requestBody) throws IOException, AppBuilderServerException {
String url = "/app/conversation/runs";
if (this.appID != null && !this.appID.isEmpty()) {
String jsonBody = JsonUtils.serialize(requestBody);
ClassicHttpRequest postRequest = this.httpClient.createPostRequestV2(url, new StringEntity(jsonBody, StandardCharsets.UTF_8));
postRequest.setHeader("Content-Type", "application/json");
HttpResponse<Iterator<AppBuilderClientResponse>> response = this.httpClient.executeSSE(postRequest, AppBuilderClientResponse.class);
return new AppBuilderClientIterator((Iterator)response.getBody());
} else {
throw new RuntimeException("Param 'appID' is required!");
}
}
}
涉及到的表结构
会话表:
消息表:
4. 基本逻辑
此websocket功能包括 普通纯文本对话 、文生图、 图生文;
主要逻辑是:使用 AppBuilderNewClient 创建会话
AppBuilderNewClient appBuilderClient = new AppBuilderNewClient(appBuilderAppId);
然后使用
String conversationId = appBuilderClient.createConversation();
获取到会话id并保存在数据库中,下次会话直接传入此id就可以实现续写
然后实现流式请求
AppBuilderClientIterator iterator = appBuilderClient.run(xxstudyAiChatMessageList.get(0).getMessage(), xxstudyAiChat.getSessionId(), new String[]{}, true);
while(iterator.hasNext())
{
AppBuilderClientResult response = iterator.next();
result.append(response.getAnswer());
finalMessage.setMessage(response.getAnswer());
finalMessage.setIsEnd(!iterator.hasNext());
finalMessage.setAvatar(AI_AVATAR);
if (!iterator.hasNext() && !StringUtils.isNotBlank(result.toString().trim())) {
result.append(NO_RESULT);
finalMessage.setMessage(NO_RESULT);
}
sendOneMessage(userIdKey, serializeToJson(finalMessage));
log.info(response.getAnswer());
}
图生文时需要先上传文件
String fileId = appBuilderClient.uploadLocalFile(xxstudyAiChat.getSessionId(), imgUrl);
获取到文件id后再传入对话
AppBuilderClientIterator iterator = appBuilderClient.run( xxstudyAiChatMessage.getMessage(), xxstudyAiChat.getSessionId(), new String[]{fileId}, true);
完结下机!