集成ai部分
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
package com.ruoyi.cms.config;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.cms.domain.chat.ChatRequest;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import lombok.var;
|
||||
import okhttp3.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
public class ChatClient {
|
||||
// 超时设置(单位:秒)
|
||||
private static final int CONNECT_TIMEOUT = 30;
|
||||
private static final int WRITE_TIMEOUT = 30;
|
||||
private static final int READ_TIMEOUT = 300; // 流式响应不设置读取超时
|
||||
|
||||
// 单例 OkHttp 客户端(复用连接池)
|
||||
private static final OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
private static final String CHAT_ENDPOINT = "chat";
|
||||
|
||||
private static final String CHAT_HISTORY = "history";
|
||||
public static final List<String> TEXT_FILE_EXTENSIONS= Arrays.asList(".txt", ".md", ".html", ".doc", ".docx", ".pdf", ".ppt", ".pptx", ".csv", ".xls", ".xlsx");
|
||||
public static final List<String> IMAGE_FILE_EXTENSIONS=Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp");
|
||||
|
||||
|
||||
private ChatConfig chatConfig;
|
||||
@Autowired
|
||||
public void setChatConfig(ChatConfig chatConfig) {
|
||||
this.chatConfig = chatConfig;
|
||||
}
|
||||
@Value("${spring.profiles.active}")
|
||||
private String env;
|
||||
/**
|
||||
* 发送流式聊天请求
|
||||
* @param chatRequest 查询请求体
|
||||
* @param callback 流式响应回调接口
|
||||
*/
|
||||
public void sendStreamingChat(ChatRequest chatRequest, StreamCallback callback) {
|
||||
String url=chatConfig.getBaseUrl()+chatConfig.getChatUrl();
|
||||
// 构建请求体
|
||||
String jsonBody = buildChatRequestBody(chatRequest, CHAT_ENDPOINT);
|
||||
// 构建请求
|
||||
Request request = null;
|
||||
|
||||
try {
|
||||
RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"),jsonBody);
|
||||
request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("Authorization", "Bearer " + chatConfig.getApiKey())
|
||||
.post(body).build();
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
callback.onError(new RuntimeException("构建请求失败: " + e.getMessage(), e));
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送异步请求
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
callback.onError(new RuntimeException("请求发送失败: " + e.getMessage(), e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
try {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "无错误信息";
|
||||
String errorMsg = String.format("API 响应错误: 状态码=%d, 错误信息=%s",
|
||||
response.code(), errorBody);
|
||||
System.err.println(errorMsg); // 打印详细错误
|
||||
callback.onError(new RuntimeException(errorMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理流式响应
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
callback.onError(new RuntimeException("响应体为空"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 逐行读取 SSE 格式的响应
|
||||
try (var bufferedSource = body.source()) {
|
||||
while (!bufferedSource.exhausted()) {
|
||||
String chunk = bufferedSource.readUtf8Line();
|
||||
if (chunk != null && !chunk.trim().isEmpty()) {
|
||||
callback.onData(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知流结束
|
||||
callback.onComplete();
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMsg = "处理响应失败: " + e.getMessage();
|
||||
System.err.println(errorMsg);
|
||||
callback.onError(new RuntimeException(errorMsg, e));
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建聊天请求的 JSON 体
|
||||
*/
|
||||
/**
|
||||
* 构建聊天请求的 JSON 体
|
||||
* 根据文档 [cite: 162-172] 修改为 Vision API 兼容格式
|
||||
*/
|
||||
private String buildChatRequestBody(ChatRequest chatRequest, String key) {
|
||||
JSONObject chatObject = new JSONObject();
|
||||
|
||||
// 1. 设置会话ID (如果有)
|
||||
if(StringUtils.isNotEmpty(chatRequest.getSessionId())){
|
||||
chatObject.put("chatId", chatRequest.getSessionId());
|
||||
}
|
||||
|
||||
if("chat".equals(key)){
|
||||
// 基础参数设置
|
||||
chatObject.put("stream", true);
|
||||
chatObject.put("model", chatConfig.getModel()); // 需确保为 "qd-job-turbo"
|
||||
chatObject.put("user", chatRequest.getSessionId());
|
||||
|
||||
// 2. 获取历史消息列表,如果为空则初始化
|
||||
JSONArray messages = chatRequest.getMessages();
|
||||
if (messages == null) {
|
||||
messages = new JSONArray();
|
||||
}
|
||||
|
||||
// 3. 构建当前用户的多模态消息内容 (Multimodal Content) [cite: 162]
|
||||
JSONArray contentArray = new JSONArray();
|
||||
|
||||
// 3.1 添加文本内容 [cite: 166, 172]
|
||||
if(StringUtils.isNotEmpty(chatRequest.getData())){
|
||||
JSONObject textPart = new JSONObject();
|
||||
textPart.put("type", "text");
|
||||
textPart.put("text", chatRequest.getData());
|
||||
contentArray.add(textPart);
|
||||
}
|
||||
|
||||
// 3.2 添加文件内容 (图片、PDF、Excel等) [cite: 174, 180, 232]
|
||||
// 文档说明:image_url 字段兼容 PDF, Excel, PPT 等所有 OCR 支持的文件
|
||||
if(!CollectionUtils.isEmpty(chatRequest.getFileUrl())){
|
||||
for(String url : chatRequest.getFileUrl()){
|
||||
String finalUrl = "";
|
||||
if(Objects.equals(env, "pro")){
|
||||
finalUrl = url.replace("https://fw.rc.qingdao.gov.cn/rgpp-api/api/ng", "http://10.213.6.207:19010");
|
||||
}else {
|
||||
finalUrl = url;
|
||||
}
|
||||
// 处理内网/外网地址映射 (保留你原有的逻辑)
|
||||
|
||||
JSONObject filePart = new JSONObject();
|
||||
filePart.put("type", "image_url"); // 固定为 image_url [cite: 174]
|
||||
|
||||
JSONObject imageUrlObj = new JSONObject();
|
||||
imageUrlObj.put("url", finalUrl); // 文件地址 [cite: 172]
|
||||
|
||||
filePart.put("image_url", imageUrlObj);
|
||||
contentArray.add(filePart);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 将当前消息封装为 User Message 对象并加入消息列表 [cite: 151, 163]
|
||||
// 只有当有内容(文本或文件)时才添加
|
||||
if (!contentArray.isEmpty()) {
|
||||
JSONObject currentUserMessage = new JSONObject();
|
||||
currentUserMessage.put("role", "user");
|
||||
currentUserMessage.put("content", contentArray); // content 为数组格式
|
||||
messages.add(currentUserMessage);
|
||||
}
|
||||
|
||||
// 5. 将完整的消息列表放入请求体 [cite: 135]
|
||||
chatObject.put("messages", messages);
|
||||
|
||||
} else {
|
||||
// 非 chat 场景的逻辑保留
|
||||
chatObject.put("appId", chatConfig.getAppId());
|
||||
}
|
||||
|
||||
return chatObject.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的 JSON 转义处理(防止特殊字符破坏 JSON 格式)
|
||||
*/
|
||||
private String escapeJson(String value) {
|
||||
if (value == null) return "";
|
||||
return value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\b", "\\b")
|
||||
.replace("\f", "\\f")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天请求并返回完整JSON响应
|
||||
* @param chatRequest 用户输入的查询内容
|
||||
* @return 完整的JSON响应字符串
|
||||
* @throws IOException 网络请求异常
|
||||
*/
|
||||
public String sendChatGuest(ChatRequest chatRequest) throws IOException {
|
||||
String url=chatConfig.getBaseUrl()+chatConfig.getGuestUrl();
|
||||
|
||||
JSONArray array = chatRequest.getMessages();
|
||||
if(array==null||array.isEmpty()||array.size()==0){
|
||||
array = new JSONArray();
|
||||
}
|
||||
JSONObject contentObject = new JSONObject();
|
||||
contentObject.put("content","你是一个岗位招聘专家,请根据用户的问题生成用户下一步想要提出的问题。需要以用户的口吻进行生成。结合上下文中用户提出的问题以及助手回复的答案,需要猜测用户更进一步的需求,例如期望的薪资,期望的工作地点,掌握的技能。生成的问题举例:有没有薪资在9000以上的工作?我的学历是本科。我希望找国企。注意不仅限于这些,还要根据上下文。其次所有的问题应该限定在青岛。并且只生成3到4个。");
|
||||
contentObject.put("role","system");
|
||||
array.add(contentObject);
|
||||
JSONObject jsonBody = new JSONObject();
|
||||
jsonBody.put("stream",false);
|
||||
jsonBody.put("model",chatConfig.getModel());
|
||||
jsonBody.put("messages",array);
|
||||
|
||||
// 构建请求(使用非流式响应模式)
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), String.valueOf(jsonBody));
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.addHeader("Authorization", "Bearer " + chatConfig.getApiKey())
|
||||
.post(requestBody).build();
|
||||
|
||||
// 发送同步请求
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
String errorBody = response.body() != null ? response.body().string() : "无错误信息";
|
||||
String errorMsg = String.format("API 响应错误: 状态码=%d, 错误信息=%s",
|
||||
response.code(), errorBody);
|
||||
throw new IOException(errorMsg);
|
||||
}
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
throw new IOException("响应体为空");
|
||||
}
|
||||
JSONObject object = JSONObject.parseObject(body.string());
|
||||
String choices = object.getString("choices");
|
||||
if (choices != null && !choices.trim().isEmpty()) {
|
||||
JSONArray jsonArray = JSONArray.parseArray(choices);
|
||||
object = JSONObject.parseObject(jsonArray.getString(0));
|
||||
object = object.getJSONObject("message");
|
||||
String content = object.getString("content");// 消息内容
|
||||
|
||||
return content;
|
||||
}
|
||||
return body.string();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式响应回调接口
|
||||
*/
|
||||
public interface StreamCallback {
|
||||
/**
|
||||
* 接收分片数据
|
||||
* @param chunk SSE 格式的分片数据
|
||||
*/
|
||||
void onData(String chunk);
|
||||
|
||||
/**
|
||||
* 响应结束
|
||||
*/
|
||||
void onComplete();
|
||||
|
||||
/**
|
||||
* 发生错误
|
||||
* @param e 异常信息
|
||||
*/
|
||||
void onError(Throwable e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user