Files
ks/ruoyi-bussiness/src/main/java/com/ruoyi/cms/config/ChatClient.java

297 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}