feat: 支持插件功能

This commit is contained in:
ageerle
2025-03-11 17:32:47 +08:00
parent c98a6deaf6
commit 6a1b544545
47 changed files with 2865 additions and 230 deletions

View File

@@ -12,6 +12,7 @@ import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.system.domain.bo.SysUserBo;
import org.ruoyi.system.domain.bo.SysUserPasswordBo;
import org.ruoyi.system.domain.bo.SysUserProfileBo;
import org.ruoyi.system.domain.vo.AvatarVo;
import org.ruoyi.system.domain.vo.ProfileVo;
@@ -75,23 +76,20 @@ public class SysProfileController extends BaseController {
/**
* 重置密码
*
* @param newPassword 旧密码
* @param oldPassword 新密码
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping("/updatePwd")
public R<Void> updatePwd(String oldPassword, String newPassword) {
public R<Void> updatePwd(@Validated @RequestBody SysUserPasswordBo bo) {
SysUserVo user = userService.selectUserById(LoginHelper.getUserId());
String password = user.getPassword();
if (!BCrypt.checkpw(oldPassword, password)) {
if (!BCrypt.checkpw(bo.getOldPassword(), password)) {
return R.fail("修改密码失败,旧密码错误");
}
if (BCrypt.checkpw(newPassword, password)) {
if (BCrypt.checkpw(bo.getNewPassword(), password)) {
return R.fail("新密码不能与旧密码相同");
}
if (userService.resetUserPwd(user.getUserId(), BCrypt.hashpw(newPassword)) > 0) {
if (userService.resetUserPwd(user.getUserId(), BCrypt.hashpw(bo.getNewPassword())) > 0) {
return R.ok();
}
return R.fail("修改密码异常,请联系管理员");

View File

@@ -0,0 +1,32 @@
package org.ruoyi.system.domain.bo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 描述用户密码修改bo
*
* @author ageerle@163.com
* date 2025/3/9
*/
@Data
public class SysUserPasswordBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 旧密码
*/
@NotBlank(message = "旧密码不能为空")
private String oldPassword;
/**
* 新密码
*/
@NotBlank(message = "新密码不能为空")
private String newPassword;
}

View File

@@ -65,7 +65,7 @@ public class SSEEventSourceListener extends EventSourceListener {
@Override
public void onEvent(@NotNull EventSource eventSource, String id, String type, String data) {
try {
if (data.equals("[DONE]")) {
if ("[DONE]".equals(data)) {
//成功响应
emitter.complete();
if(StringUtils.isNotEmpty(modelName)){

View File

@@ -0,0 +1,212 @@
package org.ruoyi.system.plugin;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.Builder;
import lombok.Data;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import org.junit.Before;
import org.junit.Test;
import org.ruoyi.common.chat.demo.ConsoleEventSourceListenerV3;
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.entity.chat.Parameters;
import org.ruoyi.common.chat.entity.chat.tool.ToolCallFunction;
import org.ruoyi.common.chat.entity.chat.tool.ToolCalls;
import org.ruoyi.common.chat.entity.chat.tool.Tools;
import org.ruoyi.common.chat.entity.chat.tool.ToolsFunction;
import org.ruoyi.common.chat.openai.OpenAiClient;
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
import org.ruoyi.common.chat.openai.function.KeyRandomStrategy;
import org.ruoyi.common.chat.openai.interceptor.DynamicKeyOpenAiAuthInterceptor;
import org.ruoyi.common.chat.openai.interceptor.OpenAILogger;
import org.ruoyi.common.chat.openai.interceptor.OpenAiResponseInterceptor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class WebSearchPlugin {
private OpenAiClient openAiClient;
private OpenAiStreamClient openAiStreamClient;
@Before
public void before() {
//可以为null
// Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 7890));
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
//千万别再生产或者测试环境打开BODY级别日志
//生产或者测试环境建议设置为这三种级别NONE,BASIC,HEADERS,
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient okHttpClient = new OkHttpClient
.Builder()
// .proxy(proxy)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(new OpenAiResponseInterceptor())
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
openAiClient = OpenAiClient.builder()
//支持多key传入请求时候随机选择
.apiKey(Arrays.asList("xx"))
//自定义key的获取策略默认KeyRandomStrategy
//.keyStrategy(new KeyRandomStrategy())
.keyStrategy(new KeyRandomStrategy())
.okHttpClient(okHttpClient)
//自己做了代理就传代理地址,没有可不不传,(关注公众号回复openai ,获取免费的测试代理地址)
.apiHost("https://open.bigmodel.cn/")
.build();
openAiStreamClient = OpenAiStreamClient.builder()
//支持多key传入请求时候随机选择
.apiKey(Arrays.asList("xx"))
//自定义key的获取策略默认KeyRandomStrategy
.keyStrategy(new KeyRandomStrategy())
.authInterceptor(new DynamicKeyOpenAiAuthInterceptor())
.okHttpClient(okHttpClient)
//自己做了代理就传代理地址,没有可不不传,(关注公众号回复openai ,获取免费的测试代理地址)
.apiHost("https://open.bigmodel.cn/")
.build();
}
@Test
public void test() {
Message message = Message.builder().role(Message.Role.USER).content("今天武汉天气怎么样").build();
ChatCompletion chatCompletion = ChatCompletion
.builder()
.messages(Collections.singletonList(message))
// .tools(Collections.singletonList(tools))
.model("web-search-pro")
.build();
ChatCompletionResponse chatCompletionResponse = openAiStreamClient.chatCompletion(chatCompletion);
System.out.printf("chatCompletionResponse=%s\n", JSONUtil.toJsonStr(chatCompletionResponse));
}
@Test
public void streamToolsChat() {
CountDownLatch countDownLatch = new CountDownLatch(1);
ConsoleEventSourceListenerV3 eventSourceListener = new ConsoleEventSourceListenerV3(countDownLatch);
Message message = Message.builder().role(Message.Role.USER).content("给我输出一个长度为2的中文词语并解释下词语对应物品的用途").build();
//属性一
JSONObject wordLength = new JSONObject();
wordLength.put("type", "number");
wordLength.put("description", "词语的长度");
//属性二
JSONObject language = new JSONObject();
language.put("type", "string");
language.put("enum", Arrays.asList("zh", "en"));
language.put("description", "语言类型例如zh代表中文、en代表英语");
//参数
JSONObject properties = new JSONObject();
properties.put("wordLength", wordLength);
properties.put("language", language);
Parameters parameters = Parameters.builder()
.type("object")
.properties(properties)
.required(Collections.singletonList("wordLength")).build();
Tools tools = Tools.builder()
.type(Tools.Type.FUNCTION.getName())
.function(ToolsFunction.builder().name("getOneWord").description("获取一个指定长度和语言类型的词语").parameters(parameters).build())
.build();
ChatCompletion chatCompletion = ChatCompletion
.builder()
.messages(Collections.singletonList(message))
.tools(Collections.singletonList(tools))
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
.build();
openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener);
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
ToolCalls openAiReturnToolCalls = eventSourceListener.getToolCalls();
WordParam wordParam = JSONUtil.toBean(openAiReturnToolCalls.getFunction().getArguments(), WordParam.class);
String oneWord = getOneWord(wordParam);
ToolCallFunction tcf = ToolCallFunction.builder().name("getOneWord").arguments(openAiReturnToolCalls.getFunction().getArguments()).build();
ToolCalls tc = ToolCalls.builder().id(openAiReturnToolCalls.getId()).type(ToolCalls.Type.FUNCTION.getName()).function(tcf).build();
//构造tool call
Message message2 = Message.builder().role(Message.Role.ASSISTANT).content("方法参数").toolCalls(Collections.singletonList(tc)).build();
String content
= "{ " +
"\"wordLength\": \"3\", " +
"\"language\": \"zh\", " +
"\"word\": \"" + oneWord + "\"," +
"\"用途\": [\"直接吃\", \"做沙拉\", \"售卖\"]" +
"}";
Message message3 = Message.builder().toolCallId(openAiReturnToolCalls.getId()).role(Message.Role.TOOL).name("getOneWord").content(content).build();
List<Message> messageList = Arrays.asList(message, message2, message3);
ChatCompletion chatCompletionV2 = ChatCompletion
.builder()
.messages(messageList)
.model(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName())
.build();
CountDownLatch countDownLatch1 = new CountDownLatch(1);
openAiStreamClient.streamChatCompletion(chatCompletionV2, new ConsoleEventSourceListenerV3(countDownLatch));
try {
countDownLatch1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
countDownLatch1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Data
@Builder
static class WordParam {
private int wordLength;
@Builder.Default
private String language = "zh";
}
/**
* 获取一个词语(根据语言和字符长度查询)
* @param wordParam
* @return
*/
public String getOneWord(WordParam wordParam) {
List<String> zh = Arrays.asList("大香蕉", "哈密瓜", "苹果");
List<String> en = Arrays.asList("apple", "banana", "cantaloupe");
if (wordParam.getLanguage().equals("zh")) {
for (String e : zh) {
if (e.length() == wordParam.getWordLength()) {
return e;
}
}
}
if (wordParam.getLanguage().equals("en")) {
for (String e : en) {
if (e.length() == wordParam.getWordLength()) {
return e;
}
}
}
return "西瓜";
}
}

View File

@@ -3,16 +3,11 @@ package org.ruoyi.system.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSONObject;
import com.azure.ai.openai.OpenAIClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.ai.openai.models.*;
import com.azure.core.credential.AzureKeyCredential;
import io.github.ollama4j.OllamaAPI;
import io.github.ollama4j.models.chat.OllamaChatMessageRole;
import io.github.ollama4j.models.chat.OllamaChatRequestBuilder;
import io.github.ollama4j.models.chat.OllamaChatRequestModel;
import io.github.ollama4j.models.generate.OllamaStreamHandler;
import io.github.ollama4j.utils.Options;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -33,6 +28,12 @@ import org.ruoyi.common.chat.entity.images.Item;
import org.ruoyi.common.chat.entity.images.ResponseFormat;
import org.ruoyi.common.chat.entity.whisper.WhisperResponse;
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
import org.ruoyi.common.chat.openai.plugin.PluginAbstract;
import org.ruoyi.common.chat.plugin.CmdPlugin;
import org.ruoyi.common.chat.plugin.CmdReq;
import org.ruoyi.common.chat.plugin.SqlPlugin;
import org.ruoyi.common.chat.plugin.SqlReq;
import org.ruoyi.common.chat.sse.ConsoleEventSourceListener;
import org.ruoyi.common.chat.utils.TikTokensUtil;
import org.ruoyi.common.core.domain.model.LoginUser;
import org.ruoyi.common.core.exception.base.BaseException;
@@ -43,12 +44,10 @@ import org.ruoyi.system.domain.bo.ChatMessageBo;
import org.ruoyi.system.domain.bo.SysModelBo;
import org.ruoyi.system.domain.request.translation.TranslationRequest;
import org.ruoyi.system.domain.vo.SysModelVo;
import org.ruoyi.system.domain.vo.SysUserVo;
import org.ruoyi.system.listener.SSEEventSourceListener;
import org.ruoyi.system.service.*;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
@@ -63,10 +62,10 @@ import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import io.github.ollama4j.utils.OptionsBuilder;
@Service
@Slf4j
@@ -89,9 +88,6 @@ public class SseServiceImpl implements ISseService {
static final OkHttpClient HTTP_CLIENT = new OkHttpClient().newBuilder().build();
private final ISysPackagePlanService sysPackagePlanService;
@Override
public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
openAiStreamClient = chatConfig.getOpenAiStreamClient();
@@ -101,12 +97,7 @@ public class SseServiceImpl implements ISseService {
List<Message> messages = chatRequest.getMessages();
try {
if (StpUtil.isLogin()) {
SysUserVo sysUserVo = userService.selectUserById(getUserId());
// if (!checkModel(sysUserVo.getUserPlan(), chatRequest.getModel())) {
// throw new BaseException("当前套餐不支持此模型!");
// }
LocalCache.CACHE.put("userId", getUserId());
Object content = messages.get(messages.size() - 1).getContent();
String chatString = "";
@@ -161,36 +152,23 @@ public class SseServiceImpl implements ISseService {
}
}
}
// else {
//
// // 初始请求次数
// int number = 1;
// // 获取请求IP
// String realIp = getClientIpAddress(request);
// // 根据IP获取次数
// Integer requestNumber = RedisUtils.getCacheObject(realIp);
// if (requestNumber == null) {
// // 记录ip使用次数
// RedisUtils.setCacheObject(realIp, number);
// } else {
// String configValue = configService.getConfigValue("mail", "free");
// if (requestNumber > Integer.parseInt(configValue)) {
// throw new BaseException("剩余次数不足,请充值后使用");
// }
// RedisUtils.setCacheObject(realIp, requestNumber + 1);
// }
//
// }
ChatCompletion completion = ChatCompletion
.builder()
.messages(messages)
.model(chatRequest.getModel())
.temperature(chatRequest.getTemperature())
.topP(chatRequest.getTop_p())
.stream(true)
.build();
openAiStreamClient.streamChatCompletion(completion, openAIEventSourceListener);
if("openCmd".equals(chatRequest.getModel())) {
sseEmitter.send(cmdPlugin(messages));
sseEmitter.complete();
}else if ("sqlPlugin".equals(chatRequest.getModel())){
sseEmitter.send(sqlPlugin(messages));
sseEmitter.complete();
} else {
ChatCompletion completion = ChatCompletion
.builder()
.messages(messages)
.model(chatRequest.getModel())
.temperature(chatRequest.getTemperature())
.topP(chatRequest.getTop_p())
.stream(true)
.build();
openAiStreamClient.streamChatCompletion(completion, openAIEventSourceListener);
}
} catch (Exception e) {
String message = e.getMessage();
sendErrorEvent(sseEmitter, message);
@@ -199,32 +177,51 @@ public class SseServiceImpl implements ISseService {
return sseEmitter;
}
public String cmdPlugin(List<Message> messages) {
CmdPlugin plugin = new CmdPlugin(CmdReq.class);
// 插件名称
plugin.setName("命令行工具");
// 方法名称
plugin.setFunction("openCmd");
// 方法说明
plugin.setDescription("提供一个命令行指令,比如<记事本>,指令使用中文");
// /**
// * 查当前用户是否可以调用此模型
// *
// * @param planId
// * @return
// */
// public Boolean checkModel(String planId, String modelName) {
// SysPackagePlanBo sysPackagePlanBo = new SysPackagePlanBo();
// if (modelName.startsWith("gpt-4-gizmo")) {
// modelName = "gpt-4-gizmo";
// }
// if (StringUtils.isEmpty(planId)) {
// sysPackagePlanBo.setName("Visitor");
// } else if ("Visitor".equals(planId) || "Free".equals(planId)) {
// sysPackagePlanBo.setName(planId);
// } else {
// // sysPackagePlanBo.setId(Long.valueOf(planId));
// return true;
// }
//
// SysPackagePlanVo sysPackagePlanVo = sysPackagePlanService.queryList(sysPackagePlanBo).get(0);
// // 将字符串转换为数组
// String[] array = sysPackagePlanVo.getPlanDetail().split(",");
// return Arrays.asList(array).contains(modelName);
// }
PluginAbstract.Arg arg = new PluginAbstract.Arg();
// 参数名称
arg.setName("cmd");
// 参数说明
arg.setDescription("命令行指令");
// 参数类型
arg.setType("string");
arg.setRequired(true);
plugin.setArgs(Collections.singletonList(arg));
//有四个重载方法,都可以使用
ChatCompletionResponse response = openAiStreamClient.chatCompletionWithPlugin(messages,"gpt-4o-mini",plugin);
return response.getChoices().get(0).getMessage().getContent().toString();
}
public String sqlPlugin(List<Message> messages) {
SqlPlugin plugin = new SqlPlugin(SqlReq.class);
// 插件名称
plugin.setName("数据库查询插件");
// 方法名称
plugin.setFunction("sqlPlugin");
// 方法说明
plugin.setDescription("提供一个用户名称查询余额信息");
PluginAbstract.Arg arg = new PluginAbstract.Arg();
// 参数名称
arg.setName("username");
// 参数说明
arg.setDescription("用户名称");
// 参数类型
arg.setType("string");
arg.setRequired(true);
plugin.setArgs(Collections.singletonList(arg));
//有四个重载方法,都可以使用
ChatCompletionResponse response = openAiStreamClient.chatCompletionWithPlugin(messages,"gpt-4o-mini",plugin);
return response.getChoices().get(0).getMessage().getContent().toString();
}
/**
* 根据次数扣除余额
@@ -295,25 +292,6 @@ public class SseServiceImpl implements ISseService {
@Override
public String chat(ChatRequest chatRequest, String userId) {
// chatService.deductUserBalance(Long.valueOf(userId), 0.01);
// // 保存消息记录
// ChatMessageBo chatMessageBo = new ChatMessageBo();
// chatMessageBo.setUserId(Long.valueOf(userId));
// chatMessageBo.setModelName(ChatCompletion.Model.GPT_3_5_TURBO.getName());
// chatMessageBo.setContent(chatRequest.getPrompt());
// chatMessageBo.setDeductCost(0.01);
// chatMessageBo.setTotalTokens(0);
// chatMessageService.insertByBo(chatMessageBo);
//
// openAiStreamClient = chatConfig.getOpenAiStreamClient();
// Message message = Message.builder().role(Message.Role.USER).content(chatRequest.getPrompt()).build();
// ChatCompletion chatCompletion = ChatCompletion
// .builder()
// .messages(Collections.singletonList(message))
// .model(chatRequest.getModel())
// .build();
// ChatCompletionResponse chatCompletionResponse = openAiStreamClient.chatCompletion(chatCompletion);
// return chatCompletionResponse.getChoices().get(0).getMessage().getContent();
return null;
}
@@ -540,7 +518,8 @@ public class SseServiceImpl implements ISseService {
@Override
public String translation(TranslationRequest translationRequest) {
// 翻译模型固定为gpt-4o-mini
translationRequest.setModel("gpt-4o-mini");
ChatMessageBo chatMessageBo = new ChatMessageBo();
chatMessageBo.setUserId(getUserId());
chatMessageBo.setModelName(translationRequest.getModel());
@@ -557,17 +536,12 @@ public class SseServiceImpl implements ISseService {
"\n" +
"请将用户输入词语翻译成{" + translationRequest.getTargetLanguage() + "}\n" +
"\n" +
"让我们一步一步来思考\n" +
"==示例输出==\n" +
"**原文** : <这里显示要翻译的原文信息>\n" +
"**翻译** : <这里显示翻译成英语的结果>\n" +
"\n" +
"**造句** : What's the weather like today? Use the 'Weather Query' plugin to find out instantly! <造一个英语句子>\n" +
"\n" +
"**同义词** : Add-on、Extension、Module <这里显示1-3个英文的同义词>\n" +
"\n" +
"==示例结束==\n" +
"\n" +
"注意:请严格按示例进行输出").build();
"注意:请严格按示例进行输出返回markdown格式").build();
messageList.add(sysMessage);
Message message = Message.builder().role(Message.Role.USER).content(translationRequest.getPrompt()).build();
messageList.add(message);
@@ -646,4 +620,6 @@ public class SseServiceImpl implements ISseService {
ChatCompletionResponse chatCompletionResponse = openAiStreamClient.chatCompletion(chatCompletion);
return chatCompletionResponse.getChoices().get(0).getMessage().getContent().toString();
}
}

View File

@@ -557,4 +557,11 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
.select(SysUser::getUserName).eq(SysUser::getUserId, userId));
return ObjectUtil.isNull(sysUser) ? null : sysUser.getUserName();
}
@Override
public String selectUserByName(String userName) {
SysUser sysUser = baseMapper.selectOne(new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getUserName, userName));
return ObjectUtil.isNull(sysUser) ? null : sysUser.getUserBalance().toString();
}
}